namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// /// Parsed FOCAS target address — IP + TCP port. Canonical focas://{ip}[:{port}]. /// Default port 8193 (Fanuc-reserved FOCAS Ethernet port). /// public sealed record FocasHostAddress(string Host, int Port) { /// Fanuc-reserved TCP port for FOCAS Ethernet. public const int DefaultPort = 8193; /// public override string ToString() => Port == DefaultPort ? $"focas://{Host}" : $"focas://{Host}:{Port}"; /// Attempts to parse a FOCAS address string. /// The address string to parse. /// A parsed address, or null if the string is invalid. public static FocasHostAddress? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; const string prefix = "focas://"; // Canonical form is focas://{ip}[:{port}], but the AdminUI persists the device host as a // scheme-less "{ip}[:{port}]" (e.g. "10.201.31.5:8193"). Accept that too: take the body // after focas:// when present, else the whole value when it carries NO other URI scheme // (a "://" that isn't ours — e.g. http:// — is still rejected). The host-contains-colon // guard below then rejects malformed scheme typos like "focas:10.0.0.5:8193". string body; if (value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) body = value[prefix.Length..]; else if (!value.Contains("://", StringComparison.Ordinal)) body = value; else return null; if (string.IsNullOrEmpty(body)) return null; var colonIdx = body.LastIndexOf(':'); string host; var port = DefaultPort; if (colonIdx >= 0) { host = body[..colonIdx]; if (!int.TryParse(body[(colonIdx + 1)..], out port) || port is <= 0 or > 65535) return null; } else { host = body; } // Empty host, or a host still carrying a colon (e.g. the malformed "focas:10.0.0.5" left // when someone wrote "focas:10.0.0.5:8193" without the //), is invalid. if (string.IsNullOrEmpty(host) || host.Contains(':', StringComparison.Ordinal)) return null; return new FocasHostAddress(host, port); } }