v3(galaxy): re-key GalaxyDriver to RawPath identity (dial attributeRef)
Wave-B driver re-keying. Under v3 the server addresses Galaxy nodes by RawPath and the driver dials the Galaxy attributeRef (tag_name.AttributeName). The dialled address moved into TagConfig under the camelCase key `attributeRef` (renamed from the pre-v3 PascalCase FullName) and rides on RawTagEntry. Options + factory: - GalaxyDriverOptions gains `IReadOnlyList<RawTagEntry> RawTags` (Contracts now references the zero-dep Core.Abstractions leaf for RawTagEntry — same as Modbus.Contracts). Factory binds `List<RawTagEntry>? RawTags` from DriverConfig. Boundary translation (built once from options.RawTags): - RawPath -> attributeRef (forward dial) on Read / Write / Subscribe / Ack. - attributeRef -> RawPath (reverse) on the value fan-out (OnDataChange) and the alarm ConditionId — so the alarm resolves back to the same RawPath the value path surfaces. Both legs fall back to identity on a miss, so the live-browse discovery path (no authored tags) and synthetic alarm sub-refs (attributeRef.InAlarm) dial straight through unchanged. - WriteAsync dials the attributeRef but resolves security via the RawPath-keyed map the discoverer captured (reverse-maps in the resolver closure). Discovery + alarms: - GalaxyDiscoverer emits FullName = RawPath for authored attributes (falls back to attributeRef), threads RawTagEntry.WriteIdempotent onto DriverAttributeInfo, and gains an optional attributeRef -> RawTagEntry resolver ctor param. - AlarmRefBuilder.Build(conditionReference, dialReference): SourceName/ConditionId = RawPath identity; the five live-state/ack sub-refs dial off the attributeRef. ReinitializeAsync equivalence now compares the session-shape sections field-wise, excluding RawTags (address-space state re-applied via rediscovery, and a record's synthesized equality would compare the lists by reference). Tests: +9 (discoverer RawPath/WriteIdempotent + alarm identity; factory RawTags binding; driver read/write/subscribe/alarm boundary translation). Existing 304 preserved via identity fallback. 313 pass, 5 live-gated skips.
This commit is contained in:
@@ -289,6 +289,72 @@ public sealed class GalaxyDiscovererTests
|
||||
info.AckMsgWriteRef.ShouldBe("Tank1_Level.HiHi.AckMsg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// v3 RawPath keying: when a resolver maps a discovered Galaxy attributeRef to its authored
|
||||
/// <see cref="RawTagEntry"/>, the emitted node reference (<see cref="DriverAttributeInfo.FullName"/>)
|
||||
/// is the entry's <b>RawPath</b> — not the attributeRef — and the entry's WriteIdempotent flag flows
|
||||
/// onto the descriptor. Unmapped attributes degrade to the attributeRef with WriteIdempotent=false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_WithRawPathResolver_EmitsRawPathAsFullName_AndThreadsWriteIdempotent()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Obj("Tank1_Level", "Level", Attr("PV", mxDataType: 2), Attr("SP", mxDataType: 2)),
|
||||
]);
|
||||
// Authored: Tank1_Level.PV → RawPath "area/line/tank1/level" (write-idempotent). SP is NOT authored.
|
||||
var authored = new Dictionary<string, RawTagEntry>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Tank1_Level.PV"] = new RawTagEntry(
|
||||
"area/line/tank1/level", "{\"attributeRef\":\"Tank1_Level.PV\"}", WriteIdempotent: true),
|
||||
};
|
||||
var discoverer = new GalaxyDiscoverer(src, attrRef => authored.GetValueOrDefault(attrRef));
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await discoverer.DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
var pv = builder.Variables.Single(v => v.AttributeName == "PV").Info;
|
||||
pv.FullName.ShouldBe("area/line/tank1/level"); // RawPath, not the attributeRef
|
||||
pv.WriteIdempotent.ShouldBeTrue();
|
||||
|
||||
var sp = builder.Variables.Single(v => v.AttributeName == "SP").Info;
|
||||
sp.FullName.ShouldBe("Tank1_Level.SP"); // unmapped → attributeRef fallback
|
||||
sp.WriteIdempotent.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// v3 alarm keying: the alarm condition identity (<see cref="AlarmConditionInfo.SourceName"/>, and
|
||||
/// the ConditionId the driver reports) is the tag's <b>RawPath</b>, while the five live-state /
|
||||
/// ack sub-refs are dialled off the Galaxy <b>attributeRef</b> — so the server subscribes the real
|
||||
/// MXAccess attributes while correlating the condition to its RawPath-keyed node.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_WithRawPathResolver_AlarmConditionUsesRawPathIdentity_AndAttributeRefSubRefs()
|
||||
{
|
||||
var src = new FakeHierarchySource([
|
||||
Obj("Tank1_Level", "Level", Attr("HiHi", mxDataType: 0, isAlarm: true)),
|
||||
]);
|
||||
var authored = new Dictionary<string, RawTagEntry>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Tank1_Level.HiHi"] = new RawTagEntry(
|
||||
"area/line/tank1/hihi", "{\"attributeRef\":\"Tank1_Level.HiHi\"}", WriteIdempotent: false),
|
||||
};
|
||||
var discoverer = new GalaxyDiscoverer(src, attrRef => authored.GetValueOrDefault(attrRef));
|
||||
var builder = new FakeBuilder();
|
||||
|
||||
await discoverer.DiscoverAsync(builder, CancellationToken.None);
|
||||
|
||||
// Alarm declaration is keyed (by the fake builder) on the variable's FullReference = RawPath.
|
||||
builder.AlarmDeclarations.ShouldContainKey("area/line/tank1/hihi");
|
||||
var info = builder.AlarmDeclarations["area/line/tank1/hihi"];
|
||||
info.SourceName.ShouldBe("area/line/tank1/hihi"); // RawPath identity
|
||||
// Sub-refs dial off the Galaxy attributeRef (tag_name.AttributeName), NOT the RawPath.
|
||||
info.InAlarmRef.ShouldBe("Tank1_Level.HiHi.InAlarm");
|
||||
info.PriorityRef.ShouldBe("Tank1_Level.HiHi.Priority");
|
||||
info.DescAttrNameRef.ShouldBe("Tank1_Level.HiHi.DescAttrName");
|
||||
info.AckedRef.ShouldBe("Tank1_Level.HiHi.Acked");
|
||||
info.AckMsgWriteRef.ShouldBe("Tank1_Level.HiHi.AckMsg");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-alarm attributes are not marked as alarm conditions.</summary>
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_NonAlarmAttribute_DoesNotMarkCondition()
|
||||
|
||||
Reference in New Issue
Block a user