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