feat(ablegacy): resolve equipment-tag refs (read + write) via EquipmentTagRefResolver
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||||
|
|
||||||
|
/// <summary>Parses an equipment tag's <c>TagConfig</c> JSON (the shape authored by the AdminUI
|
||||||
|
/// <c>AbLegacyTagConfigModel</c>) into a transient <see cref="AbLegacyTagDefinition"/> whose
|
||||||
|
/// <see cref="AbLegacyTagDefinition.Name"/> equals the reference string itself, so a value the
|
||||||
|
/// driver publishes back keys the runtime's forward router correctly.</summary>
|
||||||
|
public static class AbLegacyEquipmentTagParser
|
||||||
|
{
|
||||||
|
/// <summary>Attempts to parse an equipment-tag reference into a transient definition.</summary>
|
||||||
|
/// <param name="reference">The equipment tag's TagConfig JSON (also used as the def identity).</param>
|
||||||
|
/// <param name="def">The transient definition when parsing succeeds.</param>
|
||||||
|
/// <returns><see langword="true"/> when <paramref name="reference"/> is an AbLegacy address object.</returns>
|
||||||
|
public static bool TryParse(string reference, out AbLegacyTagDefinition def)
|
||||||
|
{
|
||||||
|
def = null!;
|
||||||
|
// Authored tag names never start with '{' (AdminUI name validation), so a leading brace marks an equipment-tag TagConfig blob.
|
||||||
|
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(reference);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
if (root.ValueKind != JsonValueKind.Object
|
||||||
|
|| !root.TryGetProperty("address", out var addr)
|
||||||
|
|| addr.ValueKind != JsonValueKind.String)
|
||||||
|
return false;
|
||||||
|
var address = addr.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(address)) return false;
|
||||||
|
var dataType = ReadEnum(root, "dataType", AbLegacyDataType.Int);
|
||||||
|
var deviceHostAddress = ReadString(root, "deviceHostAddress");
|
||||||
|
def = new AbLegacyTagDefinition(
|
||||||
|
Name: reference, DeviceHostAddress: deviceHostAddress, Address: address,
|
||||||
|
DataType: dataType, Writable: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException) { return false; }
|
||||||
|
catch (FormatException) { return false; }
|
||||||
|
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;
|
||||||
|
|
||||||
|
private static string ReadString(JsonElement o, string name)
|
||||||
|
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
|
||||||
|
? e.GetString() ?? "" : "";
|
||||||
|
}
|
||||||
@@ -21,6 +21,11 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, AbLegacyTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, AbLegacyTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Resolves a read/write/subscribe fullReference to a tag definition, bridging the two
|
||||||
|
// authoring models: an authored tag-table entry (by name) OR an equipment tag whose
|
||||||
|
// reference is its raw TagConfig JSON (parsed once via AbLegacyEquipmentTagParser, cached).
|
||||||
|
private readonly EquipmentTagRefResolver<AbLegacyTagDefinition> _resolver;
|
||||||
|
|
||||||
// volatile: _health is read by GetHealth() on any thread while ReadAsync / WriteAsync /
|
// volatile: _health is read by GetHealth() on any thread while ReadAsync / WriteAsync /
|
||||||
// InitializeAsync write it from worker / poll threads. The record-reference assignment is
|
// InitializeAsync write it from worker / poll threads. The record-reference assignment is
|
||||||
// atomic on all .NET platforms, but without a memory barrier a reader can see a stale
|
// atomic on all .NET platforms, but without a memory barrier a reader can see a stale
|
||||||
@@ -54,6 +59,9 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
_driverInstanceId = driverInstanceId;
|
_driverInstanceId = driverInstanceId;
|
||||||
_tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
|
_tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
|
||||||
_logger = logger ?? NullLogger<AbLegacyDriver>.Instance;
|
_logger = logger ?? NullLogger<AbLegacyDriver>.Instance;
|
||||||
|
_resolver = new EquipmentTagRefResolver<AbLegacyTagDefinition>(
|
||||||
|
r => _tagsByName.TryGetValue(r, out var t) ? t : null,
|
||||||
|
r => AbLegacyEquipmentTagParser.TryParse(r, out var d) ? d : null);
|
||||||
_poll = new PollGroupEngine(
|
_poll = new PollGroupEngine(
|
||||||
reader: ReadAsync,
|
reader: ReadAsync,
|
||||||
onChange: (handle, tagRef, snapshot) =>
|
onChange: (handle, tagRef, snapshot) =>
|
||||||
@@ -143,6 +151,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
}
|
}
|
||||||
_devices.Clear();
|
_devices.Clear();
|
||||||
_tagsByName.Clear();
|
_tagsByName.Clear();
|
||||||
|
_resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -177,6 +186,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
}
|
}
|
||||||
_devices.Clear();
|
_devices.Clear();
|
||||||
_tagsByName.Clear();
|
_tagsByName.Clear();
|
||||||
|
_resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses
|
||||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +240,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
for (var i = 0; i < fullReferences.Count; i++)
|
for (var i = 0; i < fullReferences.Count; i++)
|
||||||
{
|
{
|
||||||
var reference = fullReferences[i];
|
var reference = fullReferences[i];
|
||||||
if (!_tagsByName.TryGetValue(reference, out var def))
|
if (!_resolver.TryResolve(reference, out var def))
|
||||||
{
|
{
|
||||||
results[i] = new DataValueSnapshot(null, AbLegacyStatusMapper.BadNodeIdUnknown, null, now);
|
results[i] = new DataValueSnapshot(null, AbLegacyStatusMapper.BadNodeIdUnknown, null, now);
|
||||||
continue;
|
continue;
|
||||||
@@ -320,7 +330,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
for (var i = 0; i < writes.Count; i++)
|
for (var i = 0; i < writes.Count; i++)
|
||||||
{
|
{
|
||||||
var w = writes[i];
|
var w = writes[i];
|
||||||
if (!_tagsByName.TryGetValue(w.FullReference, out var def))
|
if (!_resolver.TryResolve(w.FullReference, out var def))
|
||||||
{
|
{
|
||||||
results[i] = new WriteResult(AbLegacyStatusMapper.BadNodeIdUnknown);
|
results[i] = new WriteResult(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||||
continue;
|
continue;
|
||||||
@@ -734,6 +744,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
}
|
}
|
||||||
_devices.Clear();
|
_devices.Clear();
|
||||||
_tagsByName.Clear();
|
_tagsByName.Clear();
|
||||||
|
_resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses
|
||||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class AbLegacyEquipmentTagTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Parses_equipment_tagconfig_into_a_transient_definition()
|
||||||
|
{
|
||||||
|
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}""";
|
||||||
|
AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||||
|
def!.Name.ShouldBe(json);
|
||||||
|
def.Address.ShouldBe("N7:0");
|
||||||
|
def.DataType.ShouldBe(AbLegacyDataType.Int);
|
||||||
|
def.DeviceHostAddress.ShouldBe("ab://10.0.0.5/1,0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_a_non_address_blob()
|
||||||
|
=> AbLegacyEquipmentTagParser.TryParse("""{"FullName":"x"}""", out _).ShouldBeFalse();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_garbage()
|
||||||
|
=> AbLegacyEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_address_as_a_json_number()
|
||||||
|
=> AbLegacyEquipmentTagParser.TryParse(
|
||||||
|
"""{"address":7,"dataType":"Int"}""", out _).ShouldBeFalse();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rejects_empty_address()
|
||||||
|
=> AbLegacyEquipmentTagParser.TryParse(
|
||||||
|
"""{"address":"","dataType":"Int"}""", out _).ShouldBeFalse();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End-to-end driver-level proof: an AbLegacy driver with NO authored tags can still read an
|
||||||
|
/// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient
|
||||||
|
/// definition and the read goes to the fake runtime instead of returning BadNodeIdUnknown.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown()
|
||||||
|
{
|
||||||
|
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}""";
|
||||||
|
var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 4242 } };
|
||||||
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
|
||||||
|
Tags = [],
|
||||||
|
}, "ablegacy-eq", factory);
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||||
|
|
||||||
|
r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||||
|
r[0].StatusCode.ShouldNotBe(AbLegacyStatusMapper.BadNodeIdUnknown);
|
||||||
|
r[0].Value.ShouldBe(4242);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user