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:
@@ -107,6 +107,36 @@ public sealed class FocasWireProtocolTests
|
||||
finally { client.Dispose(); server.Dispose(); }
|
||||
}
|
||||
|
||||
// The 10-byte header framing is identical across supported versions (only the version field
|
||||
// differs) — older controls + the mock answer v1, modern controls answer v3 (FANUC 30i-B).
|
||||
// Validated live 2026-06-25; see docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md.
|
||||
[Theory]
|
||||
[InlineData((ushort)1)]
|
||||
[InlineData((ushort)3)]
|
||||
public async Task ReadPduAsync_accepts_supported_version(ushort version)
|
||||
{
|
||||
var body = new byte[] { 9, 8, 7 };
|
||||
var pdu = new byte[10 + body.Length];
|
||||
new byte[] { 0xa0, 0xa0, 0xa0, 0xa0 }.CopyTo(pdu, 0);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(pdu.AsSpan(4, 2), version);
|
||||
pdu[6] = FocasWireProtocol.TypeData;
|
||||
pdu[7] = FocasWireProtocol.DirectionResponse;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(pdu.AsSpan(8, 2), (ushort)body.Length);
|
||||
body.CopyTo(pdu.AsSpan(10));
|
||||
|
||||
var (client, server) = await ConnectedPairAsync();
|
||||
try
|
||||
{
|
||||
await server.GetStream().WriteAsync(pdu);
|
||||
var read = await FocasWireProtocol.ReadPduAsync(client.GetStream(), CancellationToken.None);
|
||||
|
||||
read.Type.ShouldBe(FocasWireProtocol.TypeData);
|
||||
read.Direction.ShouldBe(FocasWireProtocol.DirectionResponse);
|
||||
read.Body.ShouldBe(body);
|
||||
}
|
||||
finally { client.Dispose(); server.Dispose(); }
|
||||
}
|
||||
|
||||
// ---- BuildRequestBody framing ----
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user