namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
///
/// Parsed TwinCAT AMS address — six-octet AMS Net ID + port. Canonical form
/// ads://{netId}:{port} where netId is five-dot-separated octets (six of them)
/// and port is the AMS service port (851 = TC3 PLC runtime 1, 852 = runtime 2, 801 /
/// 811 / 821 = TC2 PLC runtimes, 10000 = system service, etc.).
///
///
/// Format examples:
///
/// - ads://5.23.91.23.1.1:851 — remote TC3 runtime
/// - ads://5.23.91.23.1.1 — defaults to port 851 (TC3 PLC runtime 1)
/// - ads://127.0.0.1.1.1:851 — local loopback (when the router is local)
///
/// AMS Net ID is NOT an IP — it's a Beckhoff-specific identifier that the router
/// translates to an IP route. Typically the first four octets match the host's IPv4 and
/// the last two are .1.1, but the router can be configured otherwise.
///
public sealed record TwinCATAmsAddress(string NetId, int Port)
{
/// Default AMS port — TC3 PLC runtime 1.
public const int DefaultPlcPort = 851;
public override string ToString() => Port == DefaultPlcPort
? $"ads://{NetId}"
: $"ads://{NetId}:{Port}";
public static TwinCATAmsAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
const string prefix = "ads://";
if (!value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null;
var body = value[prefix.Length..];
if (string.IsNullOrEmpty(body)) return null;
var colonIdx = body.LastIndexOf(':');
string netId;
var port = DefaultPlcPort;
if (colonIdx >= 0)
{
netId = body[..colonIdx];
if (!int.TryParse(body[(colonIdx + 1)..], out port) || port is <= 0 or > 65535)
return null;
}
else
{
netId = body;
}
if (!IsValidNetId(netId)) return null;
return new TwinCATAmsAddress(netId, port);
}
private static bool IsValidNetId(string netId)
{
var parts = netId.Split('.');
if (parts.Length != 6) return false;
foreach (var p in parts)
if (!byte.TryParse(p, out _)) return false;
return true;
}
}