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; } }