fix(driver-focas): equipment tags forced read-only until PMC writes exist; shared readers + Inspect (R2-11, 05/UNDER-1)
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
{ "id": "T14", "subject": "AbCip equipment-tag parser: freeze test, shared readers (writable already honoured), Inspect warnings", "status": "completed", "blockedBy": ["T11"] },
|
||||
{ "id": "T15", "subject": "AbLegacy equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings", "status": "completed", "blockedBy": ["T11"] },
|
||||
{ "id": "T16", "subject": "TwinCAT equipment-tag parser: freeze test, shared readers, honour writable key, Inspect warnings", "status": "completed", "blockedBy": ["T11"] },
|
||||
{ "id": "T17", "subject": "FOCAS equipment-tag parser: force Writable:false (05/UNDER-1 correction), shared readers, Inspect warnings (+ amend Contracts banner)", "status": "pending", "blockedBy": ["T11"] },
|
||||
{ "id": "T17", "subject": "FOCAS equipment-tag parser: force Writable:false (05/UNDER-1 correction), shared readers, Inspect warnings (+ amend Contracts banner)", "status": "completed", "blockedBy": ["T11"] },
|
||||
{ "id": "T18", "subject": "FOCAS capability-matrix pre-flight on the equipment-tag _parseRef resolve path (BadNodeIdUnknown on rejection)", "status": "pending", "blockedBy": ["T17"] },
|
||||
{ "id": "T19", "subject": "Modbus probe parses the factory DTO shape (timeoutMs round-trip; OpcUaClient parity rule)", "status": "pending", "blockedBy": [] },
|
||||
{ "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "pending", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] },
|
||||
|
||||
@@ -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>
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// R2-11 (05/CONV-2 + 05/UNDER-1) strictness surface for the FOCAS equipment-tag parser: lenient
|
||||
/// runtime freeze on <c>dataType</c>, forced read-only (<c>Writable == false</c> regardless of any
|
||||
/// authored <c>writable:true</c>), and <c>Inspect</c> reporting the typo + the write-request.
|
||||
/// </summary>
|
||||
public sealed class FocasEquipmentTagParserStrictnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Freeze_typo_dataType_still_defaults_to_Int32()
|
||||
{
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int322\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.DataType.ShouldBe(FocasDataType.Int32);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_is_always_false_even_when_requested_true()
|
||||
{
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int32\",\"writable\":true}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writable_is_false_when_absent()
|
||||
{
|
||||
FocasEquipmentTagParser.TryParse("{\"address\":\"D0100\",\"dataType\":\"Int32\"}", out var def)
|
||||
.ShouldBeTrue();
|
||||
def.Writable.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_warns_on_writable_true_request()
|
||||
{
|
||||
var warnings = FocasEquipmentTagParser.Inspect("{\"address\":\"D0100\",\"dataType\":\"Int32\",\"writable\":true}");
|
||||
warnings.ShouldContain(w => w.Contains("read-only") && w.Contains("writable"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_reports_typo_dataType()
|
||||
{
|
||||
var warnings = FocasEquipmentTagParser.Inspect("{\"address\":\"D0100\",\"dataType\":\"Int322\"}");
|
||||
warnings.ShouldContain(w => w.Contains("Int322") && w.Contains("dataType"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspect_clean_read_only_config_has_no_warnings()
|
||||
{
|
||||
FocasEquipmentTagParser.Inspect("{\"address\":\"D0100\",\"dataType\":\"Int32\"}").ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user