feat(otopcua): multi-device-per-driver FixedTree partition (follow-up E)

This commit is contained in:
Joseph Doherty
2026-06-26 15:00:11 -04:00
parent 51721df563
commit 50f08635ec
4 changed files with 392 additions and 36 deletions
@@ -577,11 +577,23 @@ public static class AddressSpaceComposer
var raw = hostEl.GetString();
if (string.IsNullOrWhiteSpace(raw)) return null;
// Deterministic normalization (trim + lower-case) so both seams produce the identical string.
return raw.Trim().ToLowerInvariant();
return NormalizeDeviceHost(raw);
}
catch (JsonException) { return null; }
}
/// <summary>
/// The SINGLE SOURCE OF TRUTH for device-host normalization: trims surrounding whitespace and
/// lower-cases (invariant). <see cref="TryExtractDeviceHost"/> applies this to a <c>Device</c>'s
/// parsed <c>HostAddress</c>, and the FixedTree-partition path (<c>DriverHostActor</c>) applies the
/// SAME function to a driver-discovered device-host folder segment before comparing the two — so an
/// <see cref="EquipmentNode.DeviceHost"/> and a captured folder segment for the same device compare
/// equal regardless of case/whitespace. Idempotent (a value already normalized is unchanged).
/// </summary>
/// <param name="host">The raw host string (non-null; a non-empty <c>HostAddress</c> or folder segment).</param>
/// <returns>The normalized host (trimmed + lower-cased).</returns>
public static string NormalizeDeviceHost(string host) => host.Trim().ToLowerInvariant();
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
/// artifact-decode side (<c>DeploymentArtifact.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>