Merge R2-11 TagConfig consolidation (arch-review round 2) [PR #434]
Findings 01/C-1, 01/P-1 (single TagConfigIntent.Parse in Commons, 4 copies delegate, parse-once) + 05 CONV-2/UNDER-1/UNDER-6 (shared strict TagConfigJson readers, per-driver Inspect() + writable key, FOCAS capability pre-flight, Modbus probe timeoutMs, deploy-gate Warn|Error). Phase C runtime-strict flip deferred. T22/T24 deferred. Auto-merged AddressSpaceApplier.cs + DriverHostActor.cs with R2-04/R2-10; verified OpcUaServer.Tests 286/286 + Runtime.Tests 396/396. Build clean.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
@@ -42,7 +43,7 @@ public static class AbCipEquipmentTagParser
|
||||
// resolves the full tag path (e.g. "Motor.Speed") without enumerating UDT members.
|
||||
// The address space emits a placeholder String variable; UDT member declarations are
|
||||
// not supported in the equipment-tag flow.
|
||||
var dataType = ReadEnum(root, "dataType", AbCipDataType.DInt);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", AbCipDataType.DInt);
|
||||
// Review I-1 — an equipment tag is an ARRAY ⟺ isArray:true AND arrayLength >= 1. A
|
||||
// 1-element array (isArray:true, arrayLength:1) is a VALID 1-element array — the
|
||||
// foundation materialises a [1] OPC UA array node — so it must read as an array, not a
|
||||
@@ -50,8 +51,9 @@ public static class AbCipEquipmentTagParser
|
||||
// have a count of 1), so the explicit IsArray flag does.
|
||||
var (isArray, elementCount) = ReadArrayShape(root);
|
||||
// "writable" defaults to true when absent — matches AbCipTagDefinition.Writable default.
|
||||
var writable = !root.TryGetProperty("writable", out var writableEl)
|
||||
|| writableEl.ValueKind != JsonValueKind.False;
|
||||
// Now via the shared TagConfigJson.ReadWritable (explicit-false-only), byte-identical to the
|
||||
// hand-rolled read it replaces.
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new AbCipTagDefinition(
|
||||
Name: reference, DeviceHostAddress: deviceHostAddress, TagPath: tagPath,
|
||||
DataType: dataType, Writable: writable, ElementCount: elementCount, IsArray: isArray);
|
||||
@@ -83,9 +85,35 @@ public static class AbCipEquipmentTagParser
|
||||
return (false, 1);
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
|
||||
/// defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean
|
||||
/// or not an equipment-tag object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("AbCip TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
var w = TagConfigJson.DescribeInvalidEnum<AbCipDataType>(root, "dataType");
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("AbCip TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
|
||||
+5
-1
@@ -5,5 +5,9 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<!-- NO PackageReference. -->
|
||||
<!-- NO PackageReference. ProjectReference only to the zero-dependency Core.Abstractions leaf
|
||||
(R2-11 05/CONV-2 — shared TagConfigJson readers). -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+35
-5
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
@@ -38,7 +39,7 @@ public static class AbLegacyEquipmentTagParser
|
||||
return false;
|
||||
var address = addr.GetString();
|
||||
if (string.IsNullOrWhiteSpace(address)) return false;
|
||||
var dataType = ReadEnum(root, "dataType", AbLegacyDataType.Int);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", AbLegacyDataType.Int);
|
||||
var deviceHostAddress = ReadString(root, "deviceHostAddress");
|
||||
int? arrayLength = null;
|
||||
if (IsArrayFlag(root))
|
||||
@@ -47,9 +48,12 @@ public static class AbLegacyEquipmentTagParser
|
||||
if (rawLength >= 1)
|
||||
arrayLength = Math.Min(rawLength, AbLegacyArray.MaxElements);
|
||||
}
|
||||
// "writable" defaults to true when absent (today's value) — makes read-only equipment tags
|
||||
// authorable (UNDER-6).
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new AbLegacyTagDefinition(
|
||||
Name: reference, DeviceHostAddress: deviceHostAddress, Address: address,
|
||||
DataType: dataType, Writable: true, ArrayLength: arrayLength);
|
||||
DataType: dataType, Writable: writable, ArrayLength: arrayLength);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
@@ -57,9 +61,35 @@ public static class AbLegacyEquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
|
||||
/// defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean
|
||||
/// or not an equipment-tag object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("AbLegacy TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
var w = TagConfigJson.DescribeInvalidEnum<AbLegacyDataType>(root, "dataType");
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("AbLegacy TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
|
||||
+5
-1
@@ -5,5 +5,9 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<!-- NO PackageReference. -->
|
||||
<!-- NO PackageReference. ProjectReference only to the zero-dependency Core.Abstractions leaf
|
||||
(R2-11 05/CONV-2 — shared TagConfigJson readers). -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
@@ -28,11 +29,17 @@ public static class FocasEquipmentTagParser
|
||||
return false;
|
||||
var address = addr.GetString();
|
||||
if (string.IsNullOrWhiteSpace(address)) return false;
|
||||
var dataType = ReadEnum(root, "dataType", FocasDataType.Int32);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", FocasDataType.Int32);
|
||||
var deviceHostAddress = ReadString(root, "deviceHostAddress") ?? "";
|
||||
def = new FocasTagDefinition(
|
||||
Name: reference, DeviceHostAddress: deviceHostAddress, Address: address,
|
||||
DataType: dataType, Writable: true);
|
||||
// FOCAS equipment tags are FORCED read-only (05/UNDER-1): the only wire client
|
||||
// (WireFocasClient.cs:71-73) returns BadNotWritable for EVERY address, so advertising a
|
||||
// writable node is a lie — a write that used to reach the backend and always fail with
|
||||
// BadNotWritable now fails at the driver seam with the same family of Bad status (no
|
||||
// successful operation changes). Any authored `writable:true` is ignored + warned by
|
||||
// Inspect. Honour the key here once PMC writes ship in the wire client.
|
||||
DataType: dataType, Writable: false);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
@@ -40,9 +47,38 @@ public static class FocasEquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2, 05/UNDER-1): warns on a present-but-invalid <c>dataType</c>
|
||||
/// (silently defaulted by the lenient runtime), on an authored <c>writable:true</c> (FOCAS writes
|
||||
/// are unsupported — the tag is forced read-only), and on a structurally unparseable TagConfig.
|
||||
/// Empty when clean or not an equipment-tag object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("FOCAS TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
var w = TagConfigJson.DescribeInvalidEnum<FocasDataType>(root, "dataType");
|
||||
if (w is not null) warnings.Add(w);
|
||||
if (root.TryGetProperty("writable", out var wr) && wr.ValueKind == JsonValueKind.True)
|
||||
warnings.Add("FOCAS writes unsupported — 'writable:true' is ignored; the tag is forced read-only until PMC writes exist.");
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("FOCAS TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String ? e.GetString() : null;
|
||||
|
||||
+6
-1
@@ -5,5 +5,10 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<!-- NO PackageReference. NO ProjectReference. -->
|
||||
<!-- NO PackageReference. ProjectReference only to the zero-dependency Core.Abstractions leaf
|
||||
(R2-11 05/CONV-2 — shared TagConfigJson readers beside EquipmentTagRefResolver; no
|
||||
dependency-closure growth for lightweight AdminUI/Cli consumers). -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_logger = logger ?? NullLogger<FocasDriver>.Instance;
|
||||
_resolver = new EquipmentTagRefResolver<FocasTagDefinition>(
|
||||
r => _tagsByName.TryGetValue(r, out var t) ? t : null,
|
||||
r => FocasEquipmentTagParser.TryParse(r, out var d) ? d : null);
|
||||
ResolveEquipmentTagRef);
|
||||
_poll = new PollGroupEngine(
|
||||
reader: ReadAsync,
|
||||
onChange: (handle, tagRef, snapshot) =>
|
||||
@@ -234,6 +234,20 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
internal bool IsParsedAddressCached(string reference) =>
|
||||
_parsedAddressesByTagName.ContainsKey(reference);
|
||||
|
||||
// Resolves an equipment-tag TagConfig reference into a FocasTagDefinition, applying the SAME
|
||||
// capability pre-flight authored tags get at InitializeAsync (:105-119) — R2-11 (05/UNDER-6). A
|
||||
// reference whose address is unparseable, whose bound device is unknown, or whose address violates
|
||||
// the device series' capability matrix returns null ⇒ resolver miss ⇒ BadNodeIdUnknown at read time,
|
||||
// instead of a wrong-status read reaching the wire. The negative is cached by the resolver.
|
||||
private FocasTagDefinition? ResolveEquipmentTagRef(string reference)
|
||||
{
|
||||
if (!FocasEquipmentTagParser.TryParse(reference, out var def)) return null;
|
||||
var parsed = FocasAddress.TryParse(def.Address);
|
||||
if (parsed is null) return null;
|
||||
if (!_devices.TryGetValue(def.DeviceHostAddress, out var device)) return null;
|
||||
return FocasCapabilityMatrix.Validate(device.Options.Series, parsed) is null ? def : null;
|
||||
}
|
||||
|
||||
// Resolves a tag definition to its parsed FocasAddress, caching the result so that
|
||||
// equipment tags (resolver-produced, not seeded at InitializeAsync) don't re-parse the
|
||||
// address string on every ReadAsync / WriteAsync hot-path call.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
@@ -29,9 +30,9 @@ public static class ModbusEquipmentTagParser
|
||||
|| !addr.TryGetInt32(out var address)
|
||||
|| address < 0 || address > ushort.MaxValue)
|
||||
return false;
|
||||
var region = ReadEnum(root, "region", ModbusRegion.HoldingRegisters);
|
||||
var dataType = ReadEnum(root, "dataType", ModbusDataType.Int16);
|
||||
var byteOrder = ReadEnum(root, "byteOrder", ModbusByteOrder.BigEndian);
|
||||
var region = TagConfigJson.ReadEnumOrDefault(root, "region", ModbusRegion.HoldingRegisters);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", ModbusDataType.Int16);
|
||||
var byteOrder = TagConfigJson.ReadEnumOrDefault(root, "byteOrder", ModbusByteOrder.BigEndian);
|
||||
var bitIndex = (byte)ReadInt(root, "bitIndex");
|
||||
var stringLength = (ushort)ReadInt(root, "stringLength");
|
||||
// Guard: String tags require StringLength >= 1. RegisterCount = (StringLength+1)/2,
|
||||
@@ -51,9 +52,12 @@ public static class ModbusEquipmentTagParser
|
||||
var isArray = ReadBool(root, "isArray");
|
||||
var arrayLength = ReadInt(root, "arrayLength");
|
||||
int? arrayCount = (isArray && arrayLength >= 1) ? arrayLength : null;
|
||||
// "writable" defaults to true when absent (today's value) — makes read-only equipment tags
|
||||
// authorable (UNDER-6). Node-level authz remains the effective write gate.
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new ModbusTagDefinition(
|
||||
Name: reference, Region: region, Address: (ushort)address, DataType: dataType,
|
||||
Writable: true, ByteOrder: byteOrder, BitIndex: bitIndex, StringLength: stringLength,
|
||||
Writable: writable, ByteOrder: byteOrder, BitIndex: bitIndex, StringLength: stringLength,
|
||||
ArrayCount: arrayCount);
|
||||
return true;
|
||||
}
|
||||
@@ -62,9 +66,44 @@ public static class ModbusEquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection of an equipment-tag <c>TagConfig</c> blob (05/CONV-2). Returns
|
||||
/// human-readable warnings for present-but-invalid enum values (<c>region</c> / <c>dataType</c> /
|
||||
/// <c>byteOrder</c> — which the lenient runtime silently defaults) and for a structurally
|
||||
/// unparseable TagConfig (which the runtime turns into a silent <c>BadNodeIdUnknown</c>). Empty when
|
||||
/// the blob is clean or is not an equipment-tag TagConfig object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("Modbus TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
foreach (var w in new[]
|
||||
{
|
||||
TagConfigJson.DescribeInvalidEnum<ModbusRegion>(root, "region"),
|
||||
TagConfigJson.DescribeInvalidEnum<ModbusDataType>(root, "dataType"),
|
||||
TagConfigJson.DescribeInvalidEnum<ModbusByteOrder>(root, "byteOrder"),
|
||||
})
|
||||
{
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("Modbus TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static int ReadInt(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
|
||||
|
||||
+3
@@ -12,6 +12,9 @@
|
||||
and was designed for exactly this use — Admin can reference it without transport-layer deps. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing\ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj"/>
|
||||
<!-- R2-11 (05/CONV-2): shared TagConfigJson field readers live in the zero-dependency
|
||||
Core.Abstractions leaf beside EquipmentTagRefResolver. -->
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -29,19 +29,44 @@ public sealed class ModbusDriverProbe : IDriverProbe
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Modbus";
|
||||
|
||||
/// <summary>The parsed probe target — host/port/unitId + the effective connection timeout, all read
|
||||
/// from the SAME factory DTO shape (<c>ModbusDriverConfigDto</c>) the driver factory parses, so
|
||||
/// <c>timeoutMs</c> binds identically to the factory (R2-11, 05/CONV-2; the OpcUaClient parity rule).</summary>
|
||||
internal readonly record struct ProbeTarget(string Host, int Port, byte UnitId, TimeSpan Timeout);
|
||||
|
||||
/// <summary>Parse the driver config JSON into a <see cref="ProbeTarget"/> using the factory DTO shape.
|
||||
/// Returns a null target + an error message when the JSON is invalid or has no host/port. The effective
|
||||
/// timeout mirrors the factory (<c>TimeSpan.FromMilliseconds(dto.TimeoutMs ?? …)</c>); when the config
|
||||
/// omits <c>timeoutMs</c> the caller's <paramref name="fallbackTimeout"/> is used.</summary>
|
||||
/// <param name="configJson">The driver config JSON (factory DTO shape).</param>
|
||||
/// <param name="fallbackTimeout">The timeout used when the config omits <c>timeoutMs</c>.</param>
|
||||
/// <returns>The parsed target, or a null target with an error string.</returns>
|
||||
internal static (ProbeTarget? Target, string? Error) TryParseProbeTarget(string configJson, TimeSpan fallbackTimeout)
|
||||
{
|
||||
ModbusDriverFactoryExtensions.ModbusDriverConfigDto? dto;
|
||||
try { dto = JsonSerializer.Deserialize<ModbusDriverFactoryExtensions.ModbusDriverConfigDto>(configJson, _opts); }
|
||||
catch (Exception ex) { return (null, $"Config JSON is invalid: {ex.Message}"); }
|
||||
if (dto is null) return (null, "Config JSON deserialized to null.");
|
||||
|
||||
var port = dto.Port ?? 502;
|
||||
if (string.IsNullOrWhiteSpace(dto.Host) || port <= 0)
|
||||
return (null, "Config has no host/port to probe.");
|
||||
|
||||
var timeout = dto.TimeoutMs is { } ms && ms > 0 ? TimeSpan.FromMilliseconds(ms) : fallbackTimeout;
|
||||
return (new ProbeTarget(dto.Host!, port, dto.UnitId ?? 1, timeout), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
ModbusDriverOptions? opts;
|
||||
try { opts = JsonSerializer.Deserialize<ModbusDriverOptions>(configJson, _opts); }
|
||||
catch (Exception ex) { return new(false, $"Config JSON is invalid: {ex.Message}", null); }
|
||||
if (opts is null) return new(false, "Config JSON deserialized to null.", null);
|
||||
var (target, error) = TryParseProbeTarget(configJson, timeout);
|
||||
if (target is not { } t) return new(false, error!, null);
|
||||
|
||||
var (host, port) = ExtractTarget(opts);
|
||||
if (string.IsNullOrWhiteSpace(host) || port <= 0)
|
||||
return new(false, "Config has no host/port to probe.", null);
|
||||
|
||||
var unitId = opts.UnitId;
|
||||
var host = t.Host;
|
||||
var port = t.Port;
|
||||
var unitId = t.UnitId;
|
||||
// Honour the config's timeoutMs (factory parity) — the caller's timeout is the fallback.
|
||||
timeout = t.Timeout;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
@@ -93,7 +118,4 @@ public sealed class ModbusDriverProbe : IDriverProbe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (string host, int port) ExtractTarget(ModbusDriverOptions opts)
|
||||
=> (opts.Host, opts.Port);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
|
||||
@@ -31,7 +32,7 @@ public static class S7EquipmentTagParser
|
||||
return false;
|
||||
var address = addrEl.GetString();
|
||||
if (string.IsNullOrWhiteSpace(address)) return false;
|
||||
var dataType = ReadEnum(root, "dataType", S7DataType.Int16);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", S7DataType.Int16);
|
||||
var stringLength = ReadInt(root, "stringLength");
|
||||
// Range-guard applies only to String tags: an S7 string can't exceed 254 chars, and a
|
||||
// negative length is meaningless. For non-String types stringLength is irrelevant and any
|
||||
@@ -42,11 +43,14 @@ public static class S7EquipmentTagParser
|
||||
// that here so the driver's transient def agrees byte-for-byte with the materialised
|
||||
// OPC UA node's ValueRank/ArrayDimensions. Absent / isArray=false ⇒ null (scalar).
|
||||
var arrayCount = ReadArrayCount(root);
|
||||
// "writable" defaults to true when absent (today's value); node-level authz still governs
|
||||
// writes. Honouring the key makes read-only equipment tags authorable (UNDER-6).
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new S7TagDefinition(
|
||||
Name: reference,
|
||||
Address: address,
|
||||
DataType: dataType,
|
||||
Writable: true, // node-level authz governs writes
|
||||
Writable: writable,
|
||||
StringLength: stringLength == 0 ? MaxStringLength : stringLength,
|
||||
ArrayCount: arrayCount);
|
||||
return true;
|
||||
@@ -56,9 +60,35 @@ public static class S7EquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
|
||||
/// defaulted by the lenient runtime) and on a structurally unparseable TagConfig (a silent
|
||||
/// <c>BadNodeIdUnknown</c> at runtime). Empty when clean or not an equipment-tag object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("S7 TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
var w = TagConfigJson.DescribeInvalidEnum<S7DataType>(root, "dataType");
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("S7 TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static int ReadInt(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
|
||||
|
||||
+6
-1
@@ -7,6 +7,11 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- NO PackageReference. NO ProjectReference. -->
|
||||
<!-- NO PackageReference. ProjectReference only to the zero-dependency Core.Abstractions leaf
|
||||
(R2-11 05/CONV-2 — shared TagConfigJson readers beside EquipmentTagRefResolver; adds no
|
||||
dependency-closure growth for lightweight AdminUI/Cli consumers). -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+35
-5
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
@@ -28,15 +29,18 @@ public static class TwinCATEquipmentTagParser
|
||||
var symbolPath = symbol.GetString();
|
||||
if (string.IsNullOrWhiteSpace(symbolPath)) return false;
|
||||
var deviceHostAddress = ReadString(root, "deviceHostAddress");
|
||||
var dataType = ReadEnum(root, "dataType", TwinCATDataType.DInt);
|
||||
var dataType = TagConfigJson.ReadEnumOrDefault(root, "dataType", TwinCATDataType.DInt);
|
||||
// Array intent — same shape the runtime/OPC-UA foundation parses (camelCase
|
||||
// `isArray` bool + `arrayLength` uint). arrayLength is honoured ONLY when isArray
|
||||
// is true AND it is a positive JSON number, so a stale length behind a cleared
|
||||
// isArray never produces an orphan array tag (Phase 4c).
|
||||
var arrayLength = ReadArrayLength(root);
|
||||
// "writable" defaults to true when absent (today's value) — makes read-only equipment tags
|
||||
// authorable (UNDER-6).
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new TwinCATTagDefinition(
|
||||
Name: reference, DeviceHostAddress: deviceHostAddress, SymbolPath: symbolPath,
|
||||
DataType: dataType, Writable: true, ArrayLength: arrayLength);
|
||||
DataType: dataType, Writable: writable, ArrayLength: arrayLength);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
@@ -44,9 +48,35 @@ public static class TwinCATEquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
|
||||
/// defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean
|
||||
/// or not an equipment-tag object. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add("TwinCAT TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
|
||||
return warnings;
|
||||
}
|
||||
var w = TagConfigJson.DescribeInvalidEnum<TwinCATDataType>(root, "dataType");
|
||||
if (w is not null) warnings.Add(w);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("TwinCAT TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private static string ReadString(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||
|
||||
+5
-1
@@ -5,5 +5,9 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<!-- NO PackageReference. -->
|
||||
<!-- NO PackageReference. ProjectReference only to the zero-dependency Core.Abstractions leaf
|
||||
(R2-11 05/CONV-2 — shared TagConfigJson readers). -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user