feat(focas): real FANUC 30i/31i-B PDU-v3 support (live-validated on a 31i-B)
First real FOCAS hardware contact (Makino Pro 5 / 31i-B @ 10.201.31.5). A full
v3 data-PDU capture corrected the initial diagnosis: the v3 block envelope is
identical to v1, so only specific payload structs / request math / one client
robustness gap were wrong — not "framing rewrites".
Fixes (all re-validated live through the fixed driver):
- version gate: accept inbound PDU {1,3}, keep emitting v1 (FocasWireProtocol).
- cnc_rdtimer: 8-byte {minute,msec} payload is little-endian (ParseTimer) — the
only decode with an in-range msec field.
- pmc_rdpmcrng: request range widened to the data-type byte width
(end = start + width - 1) so a Word/Long isn't truncated to 0 values
(was spurious BadOutOfRange); decode extracted to ParsePmcRange.
- cnc_rdsvmeter: per-axis LOADELM is 8 bytes (not 12) and names come from the
0x0089 block — ParseServoMeters fixes the misaligned 655360 garbage. Also the
"hang" was NetworkStream.ReadAsync not aborting a stalled socket: ReadExactlyAsync
now disposes the stream on cancellation so a stalled peer can't wedge a poll loop.
- cnc_rddynamic2: contract guard rejecting axis < 1 (driver poll already 1-based).
- FocasDriverProbe: run a real wire session (initiate + cnc_statinfo) instead of
degrading to Ok=true "TCP reachability only" when FWLIB is absent — a bare TCP
listener no longer reports HEALTHY.
cnc_rdparam (0x000e) is unsupported on this control — EW_FUNC across 14
request-framing variants x 4 known-present params; needs a reference FWLIB trace
or is restricted. Deferred (deployed config uses macros, not parameters).
Tests: FOCAS suite 234 green (+16), full solution builds 0 errors. Raw v3
captures checked in under tests/.../Fixtures/v3/. Capture tools under scripts/focas/.
Docs: docs/plans/2026-06-25-focas-pdu-v3-{30i-b-support,implementation-plan}.md,
docs/drivers/FOCAS.md, docs/v2/focas-version-matrix.md,
docs/deployments/wonder-app-vd03-makino-z-34184.md.
This commit is contained in:
@@ -19,7 +19,22 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
/// </remarks>
|
||||
internal static class FocasWireProtocol
|
||||
{
|
||||
/// <summary>The PDU version this client emits in every outgoing request header.</summary>
|
||||
public const ushort Version = 1;
|
||||
|
||||
/// <summary>
|
||||
/// PDU versions accepted on inbound PDUs. The 10-byte header framing is identical across
|
||||
/// these (only the version field differs), so the framing layer accepts both while we keep
|
||||
/// emitting <see cref="Version"/> (v1) on requests. The docker mock + older controls answer
|
||||
/// v1; modern controls answer v3 — FANUC 30i-B validated live 2026-06-25 (macro reads OK).
|
||||
/// See <c>docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md</c>.
|
||||
/// </summary>
|
||||
private static readonly ushort[] SupportedReadVersions = [1, 3];
|
||||
|
||||
/// <summary>True when <paramref name="version"/> is a PDU version this client can frame-parse.</summary>
|
||||
internal static bool IsSupportedReadVersion(ushort version) =>
|
||||
Array.IndexOf(SupportedReadVersions, version) >= 0;
|
||||
|
||||
public const byte DirectionRequest = 0x01;
|
||||
public const byte DirectionResponse = 0x02;
|
||||
public const byte TypeInitiate = 0x01;
|
||||
@@ -99,7 +114,7 @@ internal static class FocasWireProtocol
|
||||
throw new FocasWireException("Invalid FOCAS PDU magic.");
|
||||
|
||||
var version = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2));
|
||||
if (version != Version)
|
||||
if (!IsSupportedReadVersion(version))
|
||||
throw new FocasWireException($"Unsupported FOCAS PDU version {version}.");
|
||||
|
||||
var bodyLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(8, 2));
|
||||
@@ -122,7 +137,7 @@ internal static class FocasWireProtocol
|
||||
throw new FocasWireException("Invalid FOCAS PDU magic.");
|
||||
|
||||
var version = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2));
|
||||
if (version != Version)
|
||||
if (!IsSupportedReadVersion(version))
|
||||
throw new FocasWireException($"Unsupported FOCAS PDU version {version}.");
|
||||
|
||||
var bodyLength = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(8, 2));
|
||||
@@ -135,13 +150,29 @@ internal static class FocasWireProtocol
|
||||
|
||||
private static async Task ReadExactlyAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
// NetworkStream.ReadAsync's CancellationToken does not reliably abort a socket read that is
|
||||
// blocked waiting for bytes the peer never sends — a CNC that TCP-accepts then stalls
|
||||
// mid-PDU (the cnc_rdsvmeter "hang" the 31i-B work chased). Register a hard abort that
|
||||
// disposes the stream on cancellation so a stalled read throws instead of wedging the
|
||||
// caller's poll loop, and normalize the resulting failure to OperationCanceledException so
|
||||
// the request path tears the transport down as a transient. See
|
||||
// docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md (Phase 2).
|
||||
await using var abort = cancellationToken.Register(static s => ((IDisposable)s!).Dispose(), stream);
|
||||
var offset = 0;
|
||||
while (offset < buffer.Length)
|
||||
try
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, cancellationToken).ConfigureAwait(false);
|
||||
if (read == 0)
|
||||
throw new EndOfStreamException("FOCAS socket closed before the expected number of bytes were read.");
|
||||
offset += read;
|
||||
while (offset < buffer.Length)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken).ConfigureAwait(false);
|
||||
if (read == 0)
|
||||
throw new EndOfStreamException("FOCAS socket closed before the expected number of bytes were read.");
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (cancellationToken.IsCancellationRequested && ex is not OperationCanceledException)
|
||||
{
|
||||
// The stalled read was aborted by the dispose-on-cancel registration above.
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user