namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Parsed ab://gateway[:port]/cip-path host-address string used by the AbCip driver /// as the hostName key across , /// , and the Polly bulkhead key /// (DriverInstanceId, hostName) per v2 plan decision #144. /// /// /// Format matches what libplctag's gateway=... + path=... attributes /// consume, so no translation is needed at the wire layer — the parsed /// is handed to the native library verbatim. /// /// ab://10.0.0.5/1,0 — single-chassis ControlLogix, CPU in slot 0. /// ab://10.0.0.5/1,4 — CPU in slot 4. /// ab://10.0.0.5/1,2,2,192.168.50.20,1,0 — bridged ControlLogix. /// ab://10.0.0.5/ (empty path) — Micro800 / MicroLogix without backplane routing. /// ab://10.0.0.5:44818/1,0 — explicit EIP port (default 44818). /// /// Opaque to the rest of the stack: Admin UI, telemetry, and logs display the full /// string so an incident ticket can be matched to the exact gateway + CIP route. /// public sealed record AbCipHostAddress(string Gateway, int Port, string CipPath) { /// Default EtherNet/IP TCP port — spec-reserved. public const int DefaultEipPort = 44818; /// Recompose the canonical ab://... form. public override string ToString() => Port == DefaultEipPort ? $"ab://{Gateway}/{CipPath}" : $"ab://{Gateway}:{Port}/{CipPath}"; /// /// Parse . Returns null on any malformed input — callers /// should treat a null return as a config-validation failure rather than catching. /// public static AbCipHostAddress? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; const string prefix = "ab://"; if (!value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null; var remainder = value[prefix.Length..]; var slashIdx = remainder.IndexOf('/'); if (slashIdx < 0) return null; var authority = remainder[..slashIdx]; var cipPath = remainder[(slashIdx + 1)..]; if (string.IsNullOrEmpty(authority)) return null; var port = DefaultEipPort; var colonIdx = authority.LastIndexOf(':'); string gateway; if (colonIdx >= 0) { gateway = authority[..colonIdx]; if (!int.TryParse(authority[(colonIdx + 1)..], out port) || port <= 0 || port > 65535) return null; } else { gateway = authority; } if (string.IsNullOrEmpty(gateway)) return null; return new AbCipHostAddress(gateway, port, cipPath); } }