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;
///
/// Static factory registration helper for . Server's Program.cs
/// calls once at startup; the bootstrapper then materialises S7
/// DriverInstance rows from the central config DB into live driver instances. Mirrors
/// GalaxyProxyDriverFactoryExtensions.
///
public static class S7DriverFactoryExtensions
{
public const string DriverTypeName = "S7";
/// Registers the S7 driver factory with the registry.
/// The driver factory registry.
public static void Register(DriverFactoryRegistry registry)
{
ArgumentNullException.ThrowIfNull(registry);
registry.Register(DriverTypeName, CreateInstance);
}
/// Creates a new S7 driver instance from configuration.
/// The unique identifier for the driver instance.
/// The JSON configuration for the driver.
/// A newly created S7 driver instance.
internal static S7Driver CreateInstance(string driverInstanceId, string driverConfigJson)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
return new S7Driver(ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId);
}
///
/// Parse a driver-config JSON document into a strongly-typed .
/// Shared by the factory (instance creation) and by
/// / so a config change delivered through the
/// IDriver contract is actually applied.
///
/// The unique identifier for the driver instance.
/// The JSON configuration for the driver.
/// Parsed S7 driver options.
internal static S7DriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
var dto = JsonSerializer.Deserialize(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(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(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 ?? ""}' in '{driverInstanceId}' missing {field}");
}
return Enum.TryParse(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())}");
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
/// Data transfer object for S7 driver configuration.
internal sealed class S7DriverConfigDto
{
/// Gets the PLC host address.
public string? Host { get; init; }
/// Gets the PLC port.
public int? Port { get; init; }
/// Gets the CPU type name.
public string? CpuType { get; init; }
/// Gets the rack number.
public short? Rack { get; init; }
/// Gets the slot number.
public short? Slot { get; init; }
/// Gets the connection timeout in milliseconds.
public int? TimeoutMs { get; init; }
/// Gets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent) the deploy
/// artifact delivers. The driver maps each through at Initialize.
public List? RawTags { get; init; }
/// Gets the probe configuration.
public S7ProbeDto? Probe { get; init; }
}
/// Data transfer object for S7 probe configuration.
internal sealed class S7ProbeDto
{
/// Gets a value indicating whether probing is enabled.
public bool? Enabled { get; init; }
/// Gets the probe interval in milliseconds.
public int? IntervalMs { get; init; }
/// Gets the probe timeout in milliseconds.
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).
}
}