Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

71 lines
3.2 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Parsed <c>ab://gateway[:port]/cip-path</c> host-address string used by the AbCip driver
/// as the <c>hostName</c> key across <see cref="Core.Abstractions.IHostConnectivityProbe"/>,
/// <see cref="Core.Abstractions.IPerCallHostResolver"/>, and the Polly bulkhead key
/// <c>(DriverInstanceId, hostName)</c> per v2 plan decision #144.
/// </summary>
/// <remarks>
/// <para>Format matches what libplctag's <c>gateway=...</c> + <c>path=...</c> attributes
/// consume, so no translation is needed at the wire layer — the parsed <see cref="CipPath"/>
/// is handed to the native library verbatim.</para>
/// <list type="bullet">
/// <item><c>ab://10.0.0.5/1,0</c> — single-chassis ControlLogix, CPU in slot 0.</item>
/// <item><c>ab://10.0.0.5/1,4</c> — CPU in slot 4.</item>
/// <item><c>ab://10.0.0.5/1,2,2,192.168.50.20,1,0</c> — bridged ControlLogix.</item>
/// <item><c>ab://10.0.0.5/</c> (empty path) — Micro800 / MicroLogix without backplane routing.</item>
/// <item><c>ab://10.0.0.5:44818/1,0</c> — explicit EIP port (default 44818).</item>
/// </list>
/// <para>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.</para>
/// </remarks>
public sealed record AbCipHostAddress(string Gateway, int Port, string CipPath)
{
/// <summary>Default EtherNet/IP TCP port — spec-reserved.</summary>
public const int DefaultEipPort = 44818;
/// <inheritdoc />
public override string ToString() => Port == DefaultEipPort
? $"ab://{Gateway}/{CipPath}"
: $"ab://{Gateway}:{Port}/{CipPath}";
/// <summary>
/// Parses an ab:// host address string. Returns <c>null</c> on any malformed input — callers
/// should treat a null return as a config-validation failure rather than catching.
/// </summary>
/// <param name="value">The ab:// URL string to parse.</param>
/// <returns>A parsed host address, or null if the value is invalid.</returns>
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);
}
}