1e26b9ab58
Rename S7EquipmentTagParser -> S7TagDefinitionFactory; TryParse(reference)
-> FromTagConfig(tagConfig, rawPath, out def) (drops the leading-{ heuristic,
keys the def by RawPath, adds inverse ToTagConfig). Replace S7DriverOptions.Tags
(pre-declared S7TagDefinition list) with RawTags (IReadOnlyList<RawTagEntry>);
the driver builds its RawPath->def table from RawTags at Initialize via the
factory (skip+log on miss), resolver is byName-only, DiscoverAsync + init guards
+ address pre-parse now run off that table. Factory retires the pre-declared DTO
tag path (RawTags binding only). Cli BuildOptions serialises typed defs to
RawTagEntry. Tests migrated to a S7RawTags helper + FromTagConfig; parser test
files renamed. Coordinator's EquipmentTagConfigInspector S7 rename left to Wave-B
coordinator; Driver.S7.IntegrationTests (Docker-gated) left red per scope.
Driver.S7 + Cli build green; Driver.S7.Tests 264/264, S7.Cli.Tests 49/49.
141 lines
6.8 KiB
C#
141 lines
6.8 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
|
|
|
/// <summary>
|
|
/// Static factory registration helper for <see cref="S7Driver"/>. Server's Program.cs
|
|
/// calls <see cref="Register"/> once at startup; the bootstrapper then materialises S7
|
|
/// DriverInstance rows from the central config DB into live driver instances. Mirrors
|
|
/// <c>GalaxyProxyDriverFactoryExtensions</c>.
|
|
/// </summary>
|
|
public static class S7DriverFactoryExtensions
|
|
{
|
|
public const string DriverTypeName = "S7";
|
|
|
|
/// <summary>Registers the S7 driver factory with the registry.</summary>
|
|
/// <param name="registry">The driver factory registry.</param>
|
|
public static void Register(DriverFactoryRegistry registry)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(registry);
|
|
registry.Register(DriverTypeName, CreateInstance);
|
|
}
|
|
|
|
/// <summary>Creates a new S7 driver instance from configuration.</summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The JSON configuration for the driver.</param>
|
|
/// <returns>A newly created S7 driver instance.</returns>
|
|
internal static S7Driver CreateInstance(string driverInstanceId, string driverConfigJson)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
|
return new S7Driver(ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a driver-config JSON document into a strongly-typed <see cref="S7DriverOptions"/>.
|
|
/// Shared by the factory (instance creation) and by <see cref="S7Driver.InitializeAsync"/>
|
|
/// / <see cref="S7Driver.ReinitializeAsync"/> so a config change delivered through the
|
|
/// <c>IDriver</c> contract is actually applied.
|
|
/// </summary>
|
|
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
|
/// <param name="driverConfigJson">The JSON configuration for the driver.</param>
|
|
/// <returns>Parsed S7 driver options.</returns>
|
|
internal static S7DriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
|
|
|
var dto = JsonSerializer.Deserialize<S7DriverConfigDto>(driverConfigJson, JsonOptions)
|
|
?? throw new InvalidOperationException(
|
|
$"S7 driver config for '{driverInstanceId}' deserialised to null");
|
|
|
|
if (string.IsNullOrWhiteSpace(dto.Host))
|
|
throw new InvalidOperationException(
|
|
$"S7 driver config for '{driverInstanceId}' missing required Host");
|
|
|
|
return new S7DriverOptions
|
|
{
|
|
Host = dto.Host!,
|
|
Port = dto.Port ?? 102,
|
|
CpuType = ParseEnum<S7CpuType>(dto.CpuType, driverInstanceId, "CpuType",
|
|
fallback: S7CpuType.S71500),
|
|
Rack = dto.Rack ?? 0,
|
|
Slot = dto.Slot ?? 0,
|
|
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 5_000),
|
|
// v3: the deploy artifact (Wave C) populates rawTags with the cluster's authored raw S7
|
|
// tags. The driver maps each RawTagEntry's TagConfig blob through S7TagDefinitionFactory
|
|
// at Initialize; the legacy pre-declared DTO tag path (Name/Address → BuildTag) is retired.
|
|
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
|
|
Probe = new S7ProbeOptions
|
|
{
|
|
Enabled = dto.Probe?.Enabled ?? true,
|
|
Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000),
|
|
Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000),
|
|
// ProbeAddress removed — probe uses ReadStatusAsync, not a tag read.
|
|
},
|
|
};
|
|
}
|
|
|
|
private static T ParseEnum<T>(string? raw, string driverInstanceId, string field,
|
|
string? tagName = null, T? fallback = null) where T : struct, Enum
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
{
|
|
if (fallback.HasValue) return fallback.Value;
|
|
throw new InvalidOperationException(
|
|
$"S7 tag '{tagName ?? "<unnamed>"}' in '{driverInstanceId}' missing {field}");
|
|
}
|
|
return Enum.TryParse<T>(raw, ignoreCase: true, out var v)
|
|
? v
|
|
: throw new InvalidOperationException(
|
|
$"S7 {(tagName is null ? "config" : $"tag '{tagName}'")} has unknown {field} '{raw}'. " +
|
|
$"Expected one of {string.Join(", ", Enum.GetNames<T>())}");
|
|
}
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
AllowTrailingCommas = true,
|
|
};
|
|
|
|
/// <summary>Data transfer object for S7 driver configuration.</summary>
|
|
internal sealed class S7DriverConfigDto
|
|
{
|
|
/// <summary>Gets the PLC host address.</summary>
|
|
public string? Host { get; init; }
|
|
/// <summary>Gets the PLC port.</summary>
|
|
public int? Port { get; init; }
|
|
/// <summary>Gets the CPU type name.</summary>
|
|
public string? CpuType { get; init; }
|
|
/// <summary>Gets the rack number.</summary>
|
|
public short? Rack { get; init; }
|
|
/// <summary>Gets the slot number.</summary>
|
|
public short? Slot { get; init; }
|
|
/// <summary>Gets the connection timeout in milliseconds.</summary>
|
|
public int? TimeoutMs { get; init; }
|
|
/// <summary>Gets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent) the deploy
|
|
/// artifact delivers. The driver maps each through <see cref="S7TagDefinitionFactory"/> at Initialize.</summary>
|
|
public List<RawTagEntry>? RawTags { get; init; }
|
|
/// <summary>Gets the probe configuration.</summary>
|
|
public S7ProbeDto? Probe { get; init; }
|
|
}
|
|
|
|
/// <summary>Data transfer object for S7 probe configuration.</summary>
|
|
internal sealed class S7ProbeDto
|
|
{
|
|
/// <summary>Gets a value indicating whether probing is enabled.</summary>
|
|
public bool? Enabled { get; init; }
|
|
/// <summary>Gets the probe interval in milliseconds.</summary>
|
|
public int? IntervalMs { get; init; }
|
|
/// <summary>Gets the probe timeout in milliseconds.</summary>
|
|
public int? TimeoutMs { get; init; }
|
|
// ProbeAddress removed from the configurable surface — the probe uses ReadStatusAsync
|
|
// (CPU status), not a tag-address read. Config documents that previously set
|
|
// probeAddress are safely ignored (unknown JSON fields are tolerated by the deserialiser).
|
|
}
|
|
}
|