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:
Joseph Doherty
2026-07-15 20:12:22 -04:00
parent c379e246d0
commit 636c755b04
9 changed files with 607 additions and 38 deletions
@@ -32,24 +32,36 @@ internal static class AlarmRefBuilder
/// <summary>
/// Build an <see cref="AlarmConditionInfo"/> for an alarm-bearing attribute with all
/// five sub-attribute references populated. <paramref name="fullReference"/> is the
/// attribute's full reference (e.g. <c>"Tank1.Level.HiHi"</c>); the convention prefixes
/// each suffix to it.
/// five sub-attribute references populated.
/// <para>
/// v3 splits identity from dialling: <paramref name="conditionReference"/> is the alarm's
/// <b>RawPath</b> — the condition's stable identity, surfaced as
/// <see cref="AlarmConditionInfo.SourceName"/> and matched against the
/// <c>ConditionId</c> the driver reports on every transition and against the value
/// fan-out key. <paramref name="dialReference"/> is the Galaxy <b>attributeRef</b>
/// (<c>tag_name.AttributeName</c>, e.g. <c>"Tank1.Level.HiHi"</c>) — the address MXAccess
/// understands; the five live-state / ack sub-refs prefix each suffix onto it so the
/// server's <c>AlarmConditionService</c> subscribes + acks the dialled attributes. In the
/// identity case (no RawPath mapping) both arguments are the same string, reproducing the
/// pre-v3 shape exactly.
/// </para>
/// </summary>
/// <param name="fullReference">The full reference of the alarm-bearing attribute.</param>
/// <param name="conditionReference">The alarm condition's RawPath identity (= <see cref="AlarmConditionInfo.SourceName"/>).</param>
/// <param name="dialReference">The Galaxy attributeRef the five sub-refs are dialled from.</param>
/// <param name="initialSeverity">The initial alarm severity level.</param>
/// <param name="initialDescription">The initial alarm description.</param>
/// <returns>The populated <see cref="AlarmConditionInfo"/> with all five sub-attribute references.</returns>
public static AlarmConditionInfo Build(
string fullReference,
string conditionReference,
string dialReference,
AlarmSeverity initialSeverity = AlarmSeverity.Medium,
string? initialDescription = null) => new(
SourceName: fullReference,
SourceName: conditionReference,
InitialSeverity: initialSeverity,
InitialDescription: initialDescription,
InAlarmRef: fullReference + InAlarmSuffix,
PriorityRef: fullReference + PrioritySuffix,
DescAttrNameRef: fullReference + DescAttrNameSuffix,
AckedRef: fullReference + AckedSuffix,
AckMsgWriteRef: fullReference + AckMsgSuffix);
InAlarmRef: dialReference + InAlarmSuffix,
PriorityRef: dialReference + PrioritySuffix,
DescAttrNameRef: dialReference + DescAttrNameSuffix,
AckedRef: dialReference + AckedSuffix,
AckMsgWriteRef: dialReference + AckMsgSuffix);
}
@@ -31,11 +31,28 @@ public sealed class GalaxyDiscoverer
{
private readonly IGalaxyHierarchySource _source;
// v3 RawPath keying. Given a discovered Galaxy attributeRef (tag_name.AttributeName), this
// resolves the authored RawTagEntry the deploy artifact delivered for it (keyed by attributeRef),
// or null when the attribute is not authored. When present, the emitted node reference
// (DriverAttributeInfo.FullName) becomes the entry's RawPath — the v3 wire identity the server
// hands back on read/write/subscribe — and WriteIdempotent flows off the entry. When absent (no
// mapping, or the live-browse path with no authored tags) the reference falls back to the
// attributeRef, reproducing the pre-v3 shape exactly.
private readonly Func<string, RawTagEntry?>? _rawTagByAttributeRef;
/// <summary>Initializes a new GalaxyDiscoverer with the specified hierarchy source.</summary>
/// <param name="source">The Galaxy hierarchy source to use for discovery.</param>
public GalaxyDiscoverer(IGalaxyHierarchySource source)
/// <param name="rawTagByAttributeRef">
/// Optional v3 resolver mapping a discovered Galaxy attributeRef to its authored
/// <see cref="RawTagEntry"/> (RawPath + WriteIdempotent). Null (or a null return) leaves the
/// emitted reference as the attributeRef — the identity / live-browse fallback.
/// </param>
public GalaxyDiscoverer(
IGalaxyHierarchySource source,
Func<string, RawTagEntry?>? rawTagByAttributeRef = null)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_rawTagByAttributeRef = rawTagByAttributeRef;
}
/// <summary>
@@ -79,12 +96,20 @@ public sealed class GalaxyDiscoverer
{
if (string.IsNullOrEmpty(attr.AttributeName)) continue;
var fullReference = !string.IsNullOrEmpty(attr.FullTagReference)
// attributeRef = the Galaxy dialled address (tag_name.AttributeName) MXAccess reads/writes.
var attributeRef = !string.IsNullOrEmpty(attr.FullTagReference)
? StripArraySuffix(attr.FullTagReference)
: obj.TagName + "." + attr.AttributeName;
// v3: the node's driver reference (FullName) is the authored RawPath when this
// attribute is authored; otherwise it degrades to the attributeRef (identity). The
// WriteIdempotent platform flag travels on the RawTagEntry, not inside the blob.
var entry = _rawTagByAttributeRef?.Invoke(attributeRef);
var rawPath = entry?.RawPath ?? attributeRef;
var writeIdempotent = entry?.WriteIdempotent ?? false;
var info = new DriverAttributeInfo(
FullName: fullReference,
FullName: rawPath,
DriverDataType: DataTypeMap.Map(attr.MxDataType),
IsArray: attr.IsArray,
ArrayDim: attr.IsArray && attr.ArrayDimensionPresent && attr.ArrayDimension > 0
@@ -92,15 +117,18 @@ public sealed class GalaxyDiscoverer
: null,
SecurityClass: SecurityMap.Map(attr.SecurityClassification),
IsHistorized: attr.IsHistorized,
IsAlarm: attr.IsAlarm);
IsAlarm: attr.IsAlarm,
WriteIdempotent: writeIdempotent);
var handle = folder.Variable(attr.AttributeName, attr.AttributeName, info);
// Alarm-bearing attributes ship the full sub-attribute ref set so the server's
// AlarmConditionService can subscribe + ack-write without re-deriving the names.
// AlarmConditionService can subscribe + ack-write without re-deriving the names. The
// condition identity (SourceName / ConditionId) is the RawPath — matching the value
// fan-out key — while the five live-state sub-refs dial off the Galaxy attributeRef.
if (attr.IsAlarm)
{
handle.MarkAsAlarmCondition(AlarmRefBuilder.Build(fullReference));
handle.MarkAsAlarmCondition(AlarmRefBuilder.Build(rawPath, attributeRef));
}
}
}