235b8b8e6d
Equipment tags were stuck at Bad_WaitingForInitialData on the deployed driver: the equipment poll, fixed-tree loop, probe and recycle shared one FOCAS/2 socket with no serialization, and the steady-state read had no timeout — concurrent reads collided and a stalled read hung forever, never overwriting the node's initial-data seed.
- SynchronizedFocasClient: per-device SemaphoreSlim gate + per-call timeout around every wire op (Connect/Probe gated, not double-bounded); wired in EnsureConnectedAsync. ReadAsync/WriteAsync map a per-call timeout to BadCommunicationError instead of rethrowing.
- FlexibleStringConverter on FOCAS config Series: the AdminUI persists the enum as a number ("series":6); accept number-or-string instead of throwing -> stub.
- FocasHostAddress.TryParse tolerates a scheme-less {ip}[:{port}] (AdminUI hostAddress form); canonical focas:// unchanged, malformed schemes still rejected.
247 FOCAS tests green; each fix has a regression test. Live-validated on wonder-app-vd03 (tags read Good).
58 lines
2.4 KiB
C#
58 lines
2.4 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
/// <summary>
|
|
/// Parsed FOCAS target address — IP + TCP port. Canonical <c>focas://{ip}[:{port}]</c>.
|
|
/// Default port 8193 (Fanuc-reserved FOCAS Ethernet port).
|
|
/// </summary>
|
|
public sealed record FocasHostAddress(string Host, int Port)
|
|
{
|
|
/// <summary>Fanuc-reserved TCP port for FOCAS Ethernet.</summary>
|
|
public const int DefaultPort = 8193;
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString() => Port == DefaultPort
|
|
? $"focas://{Host}"
|
|
: $"focas://{Host}:{Port}";
|
|
|
|
/// <summary>Attempts to parse a FOCAS address string.</summary>
|
|
/// <param name="value">The address string to parse.</param>
|
|
/// <returns>A parsed address, or null if the string is invalid.</returns>
|
|
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);
|
|
}
|
|
}
|