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:
@@ -7,17 +7,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FocasDriverProbe"/>. Covers the offline-determinable failure
|
||||
/// paths (invalid JSON, missing host/port, unreachable closed port) plus the degrade path:
|
||||
/// on a host with no FANUC FWLIB native library present (this dev box / CI Linux containers),
|
||||
/// the <c>cnc_allclibhndl3</c> P/Invoke throws <see cref="DllNotFoundException"/> at JIT bind
|
||||
/// time, so a TCP-reachable target must still report <c>Ok=true</c> with a "FWLIB absent"
|
||||
/// note — never worse than the pre-Phase-5 TCP-only probe.
|
||||
/// paths (invalid JSON, missing host/port, unreachable closed port) plus the Phase-8
|
||||
/// truthfulness behaviour: a TCP-reachable endpoint that is NOT a FOCAS CNC (a bare listener)
|
||||
/// must report <c>Ok=false</c>, because the probe now completes a real <c>FocasWireClient</c>
|
||||
/// session (initiate handshake + <c>cnc_statinfo</c>) rather than degrading to "TCP
|
||||
/// reachability only" when FWLIB is absent.
|
||||
/// <para>
|
||||
/// <b>Live-verify DEFERRED.</b> The happy path (a real CNC answers <c>cnc_allclibhndl3</c>
|
||||
/// with <c>EW_OK</c> → "FOCAS handle OK") and the CNC-error path (FWLIB present but the
|
||||
/// remote returns e.g. <c>EW_SOCKET</c>/<c>EW_PROTOCOL</c> → "FOCAS handshake failed:
|
||||
/// focas_rc=...") cannot run on this rig: there is neither a FANUC CNC nor the FWLIB native
|
||||
/// library available. Those two paths are verified manually against a real Windows+FWLIB host.
|
||||
/// <b>Live-verify DEFERRED.</b> The happy path (a real CNC completes the handshake + read →
|
||||
/// "FOCAS session OK") cannot run on this rig — there is no FANUC CNC available at unit-test
|
||||
/// time. It is verified against the live 31i-B at <c>10.201.31.5</c> (see the implementation
|
||||
/// plan's deploy/validate step).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
@@ -118,32 +117,32 @@ public sealed class FocasDriverProbeTests
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Degrade path — TCP reachable, FWLIB absent (the key test)
|
||||
// 4. TCP reachable but not a CNC — wire-session probe must say Ok=false (Phase 8)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Against an in-process <see cref="TcpListener"/> that accepts the connection, the TCP
|
||||
/// preflight succeeds. On this box the FANUC FWLIB native library is absent, so the
|
||||
/// <c>cnc_allclibhndl3</c> P/Invoke throws <see cref="DllNotFoundException"/> (or a
|
||||
/// related load failure). The probe MUST degrade gracefully — return <c>Ok=true</c> with
|
||||
/// a "FWLIB absent ... TCP reachability only" note — proving no regression versus the
|
||||
/// pre-Phase-5 TCP-only probe on FWLIB-less hosts.
|
||||
/// Against an in-process <see cref="TcpListener"/> that accepts the connection but speaks no
|
||||
/// FOCAS (drops each accepted socket), the TCP preflight succeeds but the Phase-2 wire
|
||||
/// session can't complete the initiate handshake + <c>cnc_statinfo</c> read. The probe MUST
|
||||
/// report <c>Ok=false</c> — a bare TCP listener is not a CNC. This is the Phase-8 fix: the
|
||||
/// old probe degraded such a listener to <c>Ok=true</c> "FWLIB absent, TCP reachability
|
||||
/// only", which made any TCP listener look HEALTHY.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TcpReachable_FwlibAbsent_Degrades_To_OkTrue_WithReachabilityNote()
|
||||
public async Task TcpReachable_NotACnc_Returns_OkFalse()
|
||||
{
|
||||
// Accept-only listener: completes the TCP handshake but speaks no FOCAS bytes.
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
// Keep accepting so the connect always completes; ignore the accepted socket.
|
||||
// Keep accepting so the connect always completes; drop the accepted socket.
|
||||
_ = AcceptLoopAsync(listener, TestContext.Current.CancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
TestContext.Current.CancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(15));
|
||||
|
||||
var configJson = $"{{\"devices\":[{{\"hostAddress\":\"focas://127.0.0.1:{port}\"}}]}}";
|
||||
var result = await Probe.ProbeAsync(
|
||||
@@ -151,13 +150,11 @@ public sealed class FocasDriverProbeTests
|
||||
TimeSpan.FromSeconds(3),
|
||||
cts.Token);
|
||||
|
||||
// No FWLIB here → degrade, never worse than TCP-only.
|
||||
result.Ok.ShouldBeTrue(
|
||||
$"Expected degrade to Ok=true on an FWLIB-less host but got: {result.Message}");
|
||||
// A bare listener is not a CNC — the FOCAS session fails, so the probe is NOT ok.
|
||||
result.Ok.ShouldBeFalse(
|
||||
$"Expected Ok=false for a non-CNC TCP listener but got: {result.Message}");
|
||||
result.Message.ShouldNotBeNull();
|
||||
result.Message!.ShouldContain("FWLIB absent");
|
||||
result.Message!.ShouldContain("TCP reachability only");
|
||||
result.Latency.ShouldNotBeNull();
|
||||
result.Latency.ShouldBeNull();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user