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:
@@ -0,0 +1,69 @@
|
||||
# Deployment record — wonder-app-vd03 · Makino Pro 5 (Z-34184) FOCAS
|
||||
|
||||
**Date configured:** 2026-06-25
|
||||
**Host:** `wonder-app-vd03.zmr.zimmer.com` (OtOpcUaHost Windows service, single fused admin+driver node)
|
||||
**Status:** Configured + deployed + served. **Live values blocked** by the FOCAS PDU-v3 driver gap —
|
||||
see [`docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md`](../plans/2026-06-25-focas-pdu-v3-30i-b-support.md).
|
||||
|
||||
## Equipment
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Machine | **Makino Pro 5** (FANUC **30i-B** control) |
|
||||
| **IP / FOCAS endpoint** | **`10.201.31.5:8193`** (FOCAS Ethernet, TCP) — reachable from the host (and from VPN clients) |
|
||||
| **ZTag** | **`Z-34184`** |
|
||||
| Manufacturer / Model | `Makino` / `Pro 5` |
|
||||
| CNC series (driver) | `Thirty_i` (30i) |
|
||||
|
||||
## OtOpcUa host endpoints
|
||||
|
||||
- AdminUI: `http://wonder-app-vd03.zmr.zimmer.com:9000` (login disabled — `Security__Auth__DisableLogin=true`)
|
||||
- OPC UA: `opc.tcp://wonder-app-vd03.zmr.zimmer.com:4840/OtOpcUa`
|
||||
- Akka cluster: `:4053` · ConfigDb: SQL `OtOpcUaConfig` on `:1433`
|
||||
|
||||
## Deployed configuration (cluster `DEV`, Enterprise `zb` / Site `wonder-app-vd03`)
|
||||
|
||||
First config ever deployed on this node. All authored via the AdminUI.
|
||||
|
||||
| Object | Value |
|
||||
|---|---|
|
||||
| Namespace | `dev-equipment` (Equipment), URI `urn:zb:wonder-app-vd03:equipment` |
|
||||
| UNS path | `zb / wonder-app-vd03 / machining / makino` |
|
||||
| FOCAS driver instance | `focas-z-34184` ("Makino Pro 5 Z-34184 (FOCAS)"), backend `wire`, FixedTree enabled |
|
||||
| Device | `10.201.31.5:8193`, series `Thirty_i` |
|
||||
| Equipment | `z-34184` → `EquipmentId = EQ-3686c0272279`, MachineCode `Z-34184`, ZTag `Z-34184` |
|
||||
| Tags | `parts-count` → `MACRO:3901` (Float64/Double, Read); `parts-required` → `MACRO:3902` (Float64/Double, Read) |
|
||||
| Deployment | `0c2db588` (rev `924b59097eba…`) — **Sealed / "In sync"** |
|
||||
|
||||
OPC UA node IDs (verified served via the OtOpcUa CLI client):
|
||||
- Equipment: `ns=2;s=EQ-3686c0272279`
|
||||
- `ns=2;s=EQ-3686c0272279/parts-count`
|
||||
- `ns=2;s=EQ-3686c0272279/parts-required`
|
||||
|
||||
## Host change required to deploy at all (Akka cluster roles)
|
||||
|
||||
This node was joining the Akka cluster **role-less**, so no deployment could ever complete
|
||||
("a task was canceled"; Fleet status: "no driver-role nodes are Up"). Root cause: `OTOPCUA_ROLES`
|
||||
(set to `admin,driver`) drives Program.cs actor wiring but does **not** populate the Akka member roles —
|
||||
those come from config `Cluster:Roles`, which was unset (no `Cluster__Roles` env var; no `Cluster`
|
||||
section in any appsettings). No code bridges `OTOPCUA_ROLES` → `AkkaClusterOptions.Roles`.
|
||||
|
||||
**Fix applied to the host service** (`HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaHost` → `Environment`):
|
||||
added `Cluster__Roles__0=admin` and `Cluster__Roles__1=driver`, then restarted the service. The member
|
||||
now joins **UP with roles ADMIN+DRIVER** and deployments seal. Prior env backed up on the host at
|
||||
`E:\ApiInstall\OtOpcUa\_envbak-20260625T145303.txt`.
|
||||
|
||||
> Product follow-up: either wire `OTOPCUA_ROLES` → `AkkaClusterOptions.Roles`, or bake `Cluster:Roles`
|
||||
> into the deploy template, so a node redeploy doesn't regress to role-less.
|
||||
|
||||
## Live verification (OtOpcUa CLI client → server)
|
||||
|
||||
`connect` + `browse` confirm the equipment + both tags are served at the node IDs above.
|
||||
|
||||
**Driver fix shipped (2026-06-25):** the wire client now accepts PDU v3 (see the linked plan). Validated
|
||||
directly against `10.201.31.5` — `MACRO:3901`/`3902` read **Good** from the live 30i-B. **However, the
|
||||
OtOpcUaHost on this box is still running the pre-v3 driver binary**, so the live OPC UA tags will keep
|
||||
returning `Bad_WaitingForInitialData` until the rebuilt `ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll` (or a fresh
|
||||
self-contained host publish) is deployed to `E:\ApiInstall\OtOpcUa\` and `OtOpcUaHost` is restarted. Once
|
||||
redeployed, `parts-count`/`parts-required` should go Good (FixedTree + PMC/Parameter still pending the
|
||||
follow-on v3 command work).
|
||||
@@ -15,6 +15,21 @@ OtOpcUa is **read-only** against FOCAS; all reads go over the native wire
|
||||
protocol using the documented command IDs. Writes return
|
||||
`BadNotWritable` by design.
|
||||
|
||||
> **PDU version (v3 / FANUC 30i/31i).** The wire client **accepts inbound PDU versions `{1, 3}`**
|
||||
> (`FocasWireProtocol`) while still emitting v1 on requests. Older controls + the docker mock answer
|
||||
> v1; modern controls (Makino Pro 5 / FANUC **31i-B**) answer v3. **Validated live against a real
|
||||
> 31i-B (`10.201.31.5`) 2026-06-25:** sysinfo, axis/spindle names, dynamic positions/feed/spindle,
|
||||
> program/mode, **macros**, **timers** (8-byte payload is little-endian {minute, msec}), **PMC range**
|
||||
> (request widened to the data-type byte width), **servo meters** (8-byte LOADELM stride + names from
|
||||
> the 0x0089 block; load *scaling* still to be confirmed at commissioning), and **alarms** all read
|
||||
> correctly. The driver `cnc_rddynamic2` poll is 1-based. **`cnc_rdparam` is unsupported on this
|
||||
> control** — every request-framing variant returns `EW_FUNC` (likely a wrong v3 command id; needs a
|
||||
> reference FWLIB trace). The Test-Connect probe now runs a real wire session (initiate + cnc_statinfo)
|
||||
> instead of a bare TCP check. Full finding, captured wire bytes, validation results, and analysis:
|
||||
> [`docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md`](../plans/2026-06-25-focas-pdu-v3-30i-b-support.md)
|
||||
> + [`…-implementation-plan.md`](../plans/2026-06-25-focas-pdu-v3-implementation-plan.md).
|
||||
> Capture tools: `scripts/focas/capture-initiate.py`, `scripts/focas/capture-v3.py`.
|
||||
|
||||
## Project split
|
||||
|
||||
| Project | Target | Role |
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# FOCAS wire-protocol PDU v3 — finding + support analysis (Makino Pro 5 / FANUC 30i-B)
|
||||
|
||||
**Date:** 2026-06-25
|
||||
**Author:** field investigation against a live CNC (first real FOCAS hardware contact)
|
||||
**Status:** **RESOLVED + live-validated on a real 31i-B (`10.201.31.5`) 2026-06-25** — version gate, timer, PMC range, servo-meter, alarms, probe all fixed and verified live; `cnc_rdparam` found unsupported on this control (see Resolution). See `2026-06-25-focas-pdu-v3-implementation-plan.md` for the per-phase record.
|
||||
**Components:** `ZB.MOM.WW.OtOpcUa.Driver.FOCAS` (`Wire/FocasWireProtocol.cs`, `Wire/FocasWireClient.cs`)
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The pure-managed `WireFocasClient` only implements **FOCAS Ethernet wire-protocol PDU version 1**
|
||||
and hard-rejects every other version. A real **Makino Pro 5 (FANUC 30i-B)** at `10.201.31.5:8193`
|
||||
speaks **PDU version 3**, so the driver fails the session with
|
||||
`BadCommunicationError` / "Unsupported FOCAS PDU version 3" and no tag ever produces a value.
|
||||
|
||||
A live experiment (relaxing the version gate to accept v3) showed the **v3 initiate handshake and the
|
||||
macro-read data framing are already compatible** — `MACRO:*` reads returned correct values from the
|
||||
real machine. **PMC** and **Parameter** reads still failed and need per-command v3 work. So v3 support
|
||||
is *not* a rewrite, but it is also *not* a one-line version bump if PMC/PARAM/FixedTree are required.
|
||||
|
||||
This was the **first time the driver has ever been pointed at real FOCAS hardware** — the wire
|
||||
implementation was written from `strangesast/fwlib` + public docs with an explicit "needs Wireshark
|
||||
traces to validate" caveat (see `docs/v2/implementation/focas-wire-protocol.md`). This finding is
|
||||
those traces.
|
||||
|
||||
---
|
||||
|
||||
## Resolution (2026-06-25 — implemented + live-validated)
|
||||
|
||||
A full v3 data-PDU capture (`scripts/focas/capture-v3.py`, fixtures under
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/Fixtures/v3/`) **substantially corrected the
|
||||
initial diagnosis** — 4 of the 6 "v3 framing failures" were not framing problems at all. The block
|
||||
envelope is byte-identical to v1; only specific payload structs / request-range math / a client-side
|
||||
robustness gap were wrong. Every fix below was re-validated by reading the live 31i-B through the
|
||||
fixed driver.
|
||||
|
||||
| Surface | Root cause (from the capture) | Fix | Live result |
|
||||
|---|---|---|---|
|
||||
| version gate | hard `version != 1` reject | accept `{1,3}` inbound (`FocasWireProtocol`) | macros + all reads work on v3 |
|
||||
| `cnc_rdtimer` | 8-byte {minute,msec} payload is **little-endian** (only decode with an in-range msec) | `FocasWireClient.ParseTimer` reads LE | cutting = 1,110,700 min / 41,872 ms |
|
||||
| `pmc_rdpmcrng` | request asked for `end=start` (1 byte) but a Word needs its byte width → 0 values → spurious `BadOutOfRange` | `WireFocasClient.ReadPmcAsync` sets `end = start + width − 1`; decode extracted to `ParsePmcRange` | R0 = 7873, R100 = 0, status Good |
|
||||
| `cnc_rdsvmeter` | (a) **no wire hang** — CNC answers fully + promptly; the "hang" was `NetworkStream.ReadAsync` not aborting a genuinely stalled socket. (b) per-axis LOADELM is **8 bytes**, not 12 → 12-byte stride misaligned (→ 655360 garbage); names live in the 0x0089 block | (a) `ReadExactlyAsync` dispose-on-cancel abort. (b) `ParseServoMeters` 8-byte stride + name correlation | 7 axes X,Y,Z,B,C,AA,AA, aligned values (≈0 idle) |
|
||||
| `cnc_rdalmmsg2` | not broken — empty payload = no active alarms | none (parser already handles empty) | returned active alarm `#3080 WRONG PALLET IN MACHINE` |
|
||||
| `cnc_rddynamic2` axis 0 | not a driver bug — the FixedTree poll already iterates 1..N; only a direct harness call used 0 | contract guard in `ReadDynamicAsync` (reject `axisIndex < 1`) | axes 1..N read clean |
|
||||
| Test-Connect probe | degraded to `Ok=true` "TCP reachability only" when FWLIB absent → any TCP listener looked HEALTHY | `FocasDriverProbe` now runs a real wire session (initiate + `cnc_statinfo`) | `Ok=true` vs real CNC, `Ok=false` vs bare listener |
|
||||
|
||||
### `cnc_rdparam` — unsupported on this control (blocked)
|
||||
|
||||
The one genuine v3 problem. A live matrix of **14 request-framing variants × 4 known-present
|
||||
parameters** (8130 / 1320 / 1825 / 3201) — every combination of arg ordering, axis, length,
|
||||
request-class, and extra payload (`scripts/focas/param-probe.py`) — returned **`EW_FUNC(1)`
|
||||
uniformly**. That is not a tweakable-framing bug. `0x000e` is also the ignored post-connect setup
|
||||
command, which makes it a doubtful parameter opcode. Either parameter read is genuinely restricted on
|
||||
this control via the wire path, or the v3 command id differs from `0x000e` and cannot be recovered
|
||||
without a reference FWLIB Wireshark trace (the long-blocked "Stream C.2"). Parameter support is parked
|
||||
on that reference; the deployed config uses macros, not parameters, so nothing live depends on it.
|
||||
|
||||
### Open caveats
|
||||
- **Servo-load magnitude/scaling** (`data / 10^dec`; `dec` read as 10 → idle loads ≈ 0) is inferred
|
||||
from the wire and unconfirmed against the machine's servo-meter screen — confirm at commissioning.
|
||||
- **Timer type→counter mapping**: power-on / operating / cycle read 0 while cutting is non-zero on
|
||||
this control. The *decode* is correct; whether type 0/1/3 map to populated counters here is a
|
||||
CNC-configuration question for commissioning.
|
||||
|
||||
## Environment
|
||||
|
||||
- **CNC:** Makino Pro 5, FANUC 30i-B control, `10.201.31.5:8193` (ZTag `Z-34184`). FOCAS Ethernet
|
||||
reachable (TCP 8193 open from both the OtOpcUa host `wonder-app-vd03` and a dev laptop over VPN).
|
||||
- **Driver backend:** `wire` (the default and now only real backend — `fwlib`/`ipc` were retired in
|
||||
the Wire migration; see `FocasDriverFactoryExtensions.BuildClientFactory`). FANUC FWLIB is NOT
|
||||
installed on the host, and is not used for reads.
|
||||
- **Deployment using this CNC:** see `docs/deployments/wonder-app-vd03-makino-z-34184.md`.
|
||||
|
||||
## Symptom
|
||||
|
||||
Equipment tags backed by this device never leave OPC UA `Bad_WaitingForInitialData` (`0x80320000`),
|
||||
and the FOCAS FixedTree emits no nodes (capability detection against the CNC never succeeds).
|
||||
|
||||
## Reproduction (driver CLI, straight to the CNC — no OPC UA server involved)
|
||||
|
||||
```bash
|
||||
dotnet run --project src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli -- \
|
||||
probe -h 10.201.31.5 -s Thirty_i --timeout-ms 6000 --verbose
|
||||
```
|
||||
|
||||
```
|
||||
CNC: 10.201.31.5:8193
|
||||
Series: Thirty_i
|
||||
Health: Degraded
|
||||
Last error: Unsupported FOCAS PDU version 3.
|
||||
R100 → 0x80050000 (BadCommunicationError)
|
||||
```
|
||||
|
||||
## Root cause (exact)
|
||||
|
||||
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireProtocol.cs`
|
||||
|
||||
- Line 22: `public const ushort Version = 1;`
|
||||
- `ReadPduAsync` (~line 102) and `ReadPdu` (~line 125): `if (version != Version) throw new FocasWireException($"Unsupported FOCAS PDU version {version}.");`
|
||||
|
||||
The 10-byte PDU header is `A0 A0 A0 A0` magic + `u16` version + type byte + direction byte + `u16`
|
||||
body length. The client emits version 1 and **rejects any response whose version field != 1**. The
|
||||
30i-B answers the initiate with version 3.
|
||||
|
||||
## What the wire actually shows (captured live, read-only initiate handshake)
|
||||
|
||||
Request the driver sends (socket-1 initiate, exactly as today):
|
||||
```
|
||||
a0 a0 a0 a0 00 01 01 01 00 02 00 01
|
||||
^magic------ ^v=1 ^t ^d ^len ^sockIdx=1
|
||||
```
|
||||
|
||||
Response from the real 30i-B:
|
||||
```
|
||||
a0 a0 a0 a0 00 03 01 02 01 68 + 360-byte body
|
||||
^magic------ ^v=3 ^t ^d ^len=0x0168(360)
|
||||
body[0..]: 00 08 00 05 00 03 00 20 00 08 00 0a 00 03 00 03 00 0f 00 0f 00 01 00 02 00 01 00 00 00 01 02 00 ...
|
||||
```
|
||||
|
||||
Key observations:
|
||||
- **The 10-byte header framing is byte-identical to v1** — same magic, same `type=0x01` (initiate),
|
||||
same `direction=0x02` (response). **Only the version field differs (3 vs 1).**
|
||||
- The initiate-response **body is 360 bytes** (v3 carries a larger capability/version descriptor block;
|
||||
it parses as a run of big-endian `u16` words). The current client doesn't deeply parse the initiate
|
||||
body, so its size/shape did not block the handshake once the version gate was relaxed.
|
||||
- **No version negotiation:** sending version 3 in our *request* header produced the identical response —
|
||||
the CNC speaks v3 unconditionally.
|
||||
|
||||
(Capture is reproducible with `scripts/focas/capture-initiate.py <host>`.)
|
||||
|
||||
## Implemented + validated — accept v3 on inbound PDUs
|
||||
|
||||
Shipped change (`Wire/FocasWireProtocol.cs`): the hard `version != Version` reject is replaced by a
|
||||
supported-read-version set `{1, 3}` (`SupportedReadVersions` + `IsSupportedReadVersion`). We still
|
||||
**emit** `Version` (v1) on requests — the 30i-B accepts v1 request framing — and now **accept** v1 or v3
|
||||
on inbound PDUs. Covered by `FocasWireProtocolTests.ReadPduAsync_accepts_supported_version` (v1 + v3
|
||||
theory) with the existing v99-rejection test still green; full FOCAS unit suite 218/218.
|
||||
|
||||
Validated live against `10.201.31.5` (30i-B) with the change in source — reading several addresses:
|
||||
|
||||
| Address | Type | Result |
|
||||
|---|---|---|
|
||||
| `MACRO:500` | Float64 | **0.02 — Good (`0x0`)** |
|
||||
| `MACRO:3901` (parts total) | Float64 | **0 — Good (`0x0`)** |
|
||||
| `MACRO:3902` (parts required) | Float64 | Good |
|
||||
| `R100` (PMC R-file) | Int16 | `0x803C0000` BadOutOfRange |
|
||||
| `PARAM:1320/0` | Int32 | `0x803D0000` BadNotSupported |
|
||||
|
||||
Interpretation:
|
||||
- **Initiate handshake + macro command (`cnc_rdmacro`, cmd path) data framing are already v3-compatible.**
|
||||
Macro reads returned correct, plausible values from the real machine. The deployed equipment tags
|
||||
(`MACRO:3901`/`3902`) would go **Good** with nothing more than the version-gate relaxation.
|
||||
- **PMC (`pmc_rdpmcrng`) and Parameter (`cnc_rdparam`) reads still fail.** Two candidate causes, not yet
|
||||
separated: (a) the v3 response *block/struct* framing for these specific commands differs from the
|
||||
v1-shaped parser (so the return-code/value lands at the wrong offset → spurious `BadOutOfRange` /
|
||||
`BadNotSupported`); or (b) genuine CNC restrictions (PMC path/range, parameter not present). Macro
|
||||
working argues the *envelope* is fine, so this is per-command struct work, not a framing rewrite.
|
||||
|
||||
## Status-command validation on v3 (the FixedTree surface) — 2026-06-25
|
||||
|
||||
Drove every `IFocasClient` status call directly against the live control (v3 accepted). **Most of the
|
||||
FixedTree lights up on v3.** Note: sysinfo reveals the control is actually a **31i** (CncType=31, Series
|
||||
`G431`, MaxAxis 32, 7 axes, MtType MM) — the deployment declared `Thirty_i`; same family, reads fine.
|
||||
|
||||
| Call (FOCAS fn) | Result on v3 |
|
||||
|---|---|
|
||||
| `GetSysInfoAsync` (`cnc_sysinfo`) → Identity | ✅ real — CncType 31, Series G431, 7 axes, MtType MM |
|
||||
| `GetAxisNamesAsync` (`cnc_rdaxisname`) | ✅ real — X,Y,Z,B,C,A,A |
|
||||
| `GetSpindleNamesAsync` (`cnc_rdspdlname`) | ✅ real — S1 |
|
||||
| `GetProgramInfoAsync` (program/mode) | ✅ real — `//CNC_MEM/USER/LIBRARY/O1111`, Mode 1 |
|
||||
| `ReadDynamicAsync(n)` (`cnc_rddynamic2`) | ✅ real for axes 1..N (feed 4200, spindle ~15000, live positions). **axis 0 → `EW_4`** — the call is 1-based; FixedTree must iterate 1..N, not 0 |
|
||||
| `GetTimerAsync(*)` | ⚠️ **misparsed** — a running machine shows PowerOn/Operating/Cycle = 0 and Cutting = garbage; the v3 timer struct differs |
|
||||
| `GetServoLoadsAsync` (`cnc_rdsvmeter`) | ❌ **hangs** — blocks awaiting bytes that never arrive (v3 framing differs) *and* ignores the cancellation token (poll-loop-stalling robustness bug; the read must honor CT regardless) |
|
||||
| `ReadAlarmsAsync` (`cnc_rdalmmsg2`) | ❓ untested — ServoLoads hung ahead of it; validate once the hang is fixed |
|
||||
|
||||
Remaining v3 work, now scoped concretely: timer struct; 1-based axis iteration for dynamic; the
|
||||
`cnc_rdsvmeter` framing + a cancellation-honoring read; and the PMC (`pmc_rdpmcrng`) + Parameter
|
||||
(`cnc_rdparam`) struct diffs. Identity / axes / positions / feed / spindle / program-mode already work.
|
||||
|
||||
## Implementation analysis — what "support PDU v3" actually involves
|
||||
|
||||
1. **Accept v3 at the framing layer (cheap, validated). — ✅ DONE.** Replaced the `version != Version`
|
||||
hard reject with the supported-set `{1, 3}`. This alone makes the **initiate handshake + all macro
|
||||
reads** work on a real 30i-B (validated live; deployed `MACRO:3901`/`3902` read Good). We still emit
|
||||
v1 on requests (the CNC accepts it). If a future command turns out to need the request version echoed,
|
||||
thread the negotiated version from the initiate response onto the connection.
|
||||
2. **Validate each command family against v3 response framing.** Capture v3 `0x21` data-PDU responses
|
||||
for `cnc_rdparam`, `pmc_rdpmcrng`, `cnc_statinfo`, `cnc_rddynamic2`, `cnc_rdaxisname`, and the timer
|
||||
reads (the FixedTree set), and diff the block/struct offsets vs the v1 assumptions in
|
||||
`FocasWireModels.cs` / the `ParseX` helpers. Where they differ, add v3 parsing. Capture by extending
|
||||
`scripts/focas/capture-initiate.py` to complete the handshake and issue one data request per command.
|
||||
3. **FixedTree depends on (2).** Identity/Axes/Timers/Program nodes only emit if `cnc_sysinfo` +
|
||||
`cnc_rdaxisname` + dynamic/timer reads succeed at discovery — so they come online once `cnc_statinfo`
|
||||
/ `cnc_rddynamic2` / timer framing is v3-validated.
|
||||
4. **Don't let the gate lie.** Shipping only step 1 makes the driver accept v3 while PMC/PARAM/FixedTree
|
||||
silently misbehave. Either gate macro-only configs as "supported on v3" with the others explicitly
|
||||
flagged, or land steps 1–3 together.
|
||||
|
||||
### Alternative considered: reinstate an FWLIB-backed client
|
||||
The official FANUC FWLIB (`Fwlib64.dll`) handles all protocol versions natively. But the `fwlib`/`ipc`
|
||||
backends were deliberately retired in the Wire migration (native Windows component, x86/x64 + STA, and
|
||||
licensing — the exact coupling the managed client removed). Reintroducing it reverses that decision and
|
||||
is heavier than completing v3 in the wire client; recommend only if multiple controls need surfaces the
|
||||
managed client can't reach.
|
||||
|
||||
## Secondary finding — the Test-Connect / health probe is misleading without FWLIB
|
||||
|
||||
`FocasDriverProbe` Phase 2 (the real `cnc_allclibhndl3` FWLIB handshake) **catches the FWLIB-absent load
|
||||
failure and degrades to `Ok=true` ("TCP reachability only")**. On a host with no FWLIB (the normal case
|
||||
for the managed wire client), the driver therefore reports **HEALTHY off a bare TCP connect** — which is
|
||||
exactly how this CNC looked "healthy" while no data flowed. The probe should exercise the wire-client
|
||||
path (open a `WireFocasClient` session + one sample read) so health reflects real FOCAS reachability,
|
||||
not just an open socket.
|
||||
|
||||
## Recommended next steps
|
||||
|
||||
1. Land step 1 (accept v3) + capture/validate PMC + Parameter + FixedTree command framing (step 2),
|
||||
ideally in one change, tested against `10.201.31.5` while access lasts.
|
||||
2. Fix the probe to use the wire client so HEALTHY means "FOCAS session + read OK," not "TCP open."
|
||||
3. Add a real-hardware row to `docs/v2/focas-version-matrix.md` (currently hardware-free) recording that
|
||||
30i-B = PDU v3, macro reads validated.
|
||||
@@ -0,0 +1,139 @@
|
||||
# FOCAS PDU v3 — implementation plan (finish real 30i/31i-B support)
|
||||
|
||||
**Date:** 2026-06-25
|
||||
**Companion to:** [`2026-06-25-focas-pdu-v3-30i-b-support.md`](2026-06-25-focas-pdu-v3-30i-b-support.md) (finding + live captures + per-command validation) and [`../deployments/wonder-app-vd03-makino-z-34184.md`](../deployments/wonder-app-vd03-makino-z-34184.md) (the deployment this unblocks).
|
||||
**Goal:** make the managed `WireFocasClient` fully interoperate with a real FANUC 30i/31i-B (FOCAS Ethernet **PDU v3**), then light up the live OPC UA data on `wonder-app-vd03` for the Makino Pro 5 (`Z-34184`, `10.201.31.5`).
|
||||
|
||||
## ⏳ Time-boxed asset — capture/validate live FIRST
|
||||
`10.201.31.5:8193` (real 31i-B) is reachable from the dev box and the wonder host **right now**. Every
|
||||
per-command v3 framing fix needs a live capture + live re-validation. **Do all captures in Phase 1 while
|
||||
access lasts**; the parser fixes + unit tests can be finished offline against the captured bytes later.
|
||||
Tools already in place: `scripts/focas/capture-initiate.py <host>` (initiate only — extend it) and the
|
||||
throwaway status harness pattern (see the finding doc).
|
||||
|
||||
## Current status — DONE (2026-06-25), live-validated on the real 31i-B (`10.201.31.5`)
|
||||
The Phase-1 capture corrected the diagnosis (4 of 6 "framing failures" were not framing problems).
|
||||
All tractable phases are implemented + unit-tested (FOCAS suite **234 green**, full solution builds 0
|
||||
errors) + re-validated live. The full corrected diagnosis + per-surface evidence is in the companion
|
||||
finding doc's **Resolution** section.
|
||||
|
||||
| Phase | Item | State |
|
||||
|---|---|---|
|
||||
| 0 | inbound PDU-version gate `{1,3}` | DONE — macros + all status reads work on v3 |
|
||||
| 1 | capture every v3 data PDU | DONE — 20 fixtures under `tests/.../Fixtures/v3/` |
|
||||
| 2 | servo "hang" → CT-bound reads | DONE — `ReadExactlyAsync` dispose-on-cancel; servo answers in 0 ms (no real wire hang) |
|
||||
| 3 | request-version policy | DONE — keep emitting v1 (CNC accepts it); no command needed v3 requests |
|
||||
| 4 | servo + alarms framing | DONE — servo 8-byte stride + names from 0x0089; alarms already correct (read `#3080` live) |
|
||||
| 5 | timer v3 struct | DONE — `ParseTimer` little-endian {minute, msec} |
|
||||
| 6 | dynamic axis iteration | DONE — driver poll already 1-based; added a `ReadDynamicAsync` contract guard |
|
||||
| 7 | PMC framing | DONE — `end = start + width - 1`; **parameter framing BLOCKED** (EW_FUNC across 14 variants — see finding) |
|
||||
| 8 | probe truthfulness | DONE — `FocasDriverProbe` runs a real wire session (initiate + cnc_statinfo) |
|
||||
| 9 | docs + version matrix | DONE — this plan, the finding doc, `FOCAS.md`, `focas-version-matrix.md` |
|
||||
| 10 | deploy to wonder + e2e | PENDING — awaiting go-ahead (production box) |
|
||||
| 11 | commit + push | commit DONE on `feat/focas-pdu-v3`; push PENDING go-ahead |
|
||||
|
||||
**Only genuinely open v3 item:** `cnc_rdparam` (EW_FUNC on every framing — needs a reference FWLIB
|
||||
trace or is restricted on this control). Deferred; the deployed config uses macros, not parameters.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Capture every v3 data-PDU response (live, do first)
|
||||
- Extend `scripts/focas/capture-initiate.py` (or add a C# capture mode to `Driver.FOCAS.Cli`) to: run the
|
||||
two-socket initiate, then send each `0x21` data request (command IDs in
|
||||
`docs/v2/implementation/focas-wire-protocol.md`) and dump the raw v3 response: `cnc_rdtimer`,
|
||||
`cnc_rdsvmeter`, `pmc_rdpmcrng` (R100), `cnc_rdparam` (e.g. 1320), `cnc_rdalmmsg2`, `cnc_rddynamic2`
|
||||
(axis 1 — a known-good — as the v3 reference layout).
|
||||
- Save raw bytes as fixtures under `tests/Drivers/.../Fixtures/v3/` for offline unit tests.
|
||||
- **Acceptance:** raw v3 response bytes captured + checked in for all six commands.
|
||||
|
||||
## Phase 2 — Safety: `cnc_rdsvmeter` must never hang
|
||||
- Root cause: the read blocks waiting for a body length the v3 response never satisfies, and the wait
|
||||
doesn't observe the `CancellationToken`. A hang here can wedge the FixedTree poll loop.
|
||||
- Make the wire read honor the per-operation timeout/CT **regardless of framing** (the socket read path in
|
||||
`FocasWireClient` must be CT-bound), so a bad parse fails fast as `BadCommunicationError`/timeout.
|
||||
- **Acceptance:** `GetServoLoadsAsync` returns or fails within the timeout on the live 31i-B; a unit test
|
||||
proves a truncated/oversized body length cancels rather than blocks. **Gating:** FixedTree must not be
|
||||
enabled on a v3 control until this lands (capability probe could otherwise hang at init).
|
||||
|
||||
## Phase 3 — Decide request-version policy
|
||||
- We currently emit v1 requests and accept v1/v3 responses; macro + most status reads work that way.
|
||||
- If any Phase 5–7 command turns out to need v3-framed *requests*, thread the version negotiated from the
|
||||
initiate response onto `FocasWireClient` and have `BuildPdu` emit it. Otherwise keep emitting v1.
|
||||
- **Acceptance:** documented decision; negotiated version plumbed only if a command requires it.
|
||||
|
||||
## Phase 4 — Servo load + alarms v3 framing
|
||||
- Diff captured `cnc_rdsvmeter` / `cnc_rdalmmsg2` bytes vs the v1 struct assumptions in `FocasWireModels.cs`
|
||||
+ the `ParseServoLoad` / alarm parsers; fix offsets/strides for v3.
|
||||
- **Acceptance:** servo-load % values are plausible; `ReadAlarmsAsync` returns the real active-alarm set;
|
||||
unit tests over the Phase-1 fixtures; live re-validation.
|
||||
|
||||
## Phase 5 — Timer v3 struct
|
||||
- Diff captured `cnc_rdtimer` bytes; fix the timer struct parse (running machine must show non-zero
|
||||
PowerOn/Operating; Cutting sane).
|
||||
- **Acceptance:** all four timers plausible on the live machine; fixture unit test; matches the
|
||||
FixedTree `Timers/*` node expectations.
|
||||
|
||||
## Phase 6 — Dynamic axis iteration (1-based)
|
||||
- FixedTree currently probes axis 0 → `EW_4`. Iterate `1..AxesCount` (from `cnc_sysinfo`); never request 0.
|
||||
- **Acceptance:** every configured axis (per sysinfo `AxesCount`) yields a `FocasDynamicSnapshot`; no `EW_4`.
|
||||
|
||||
## Phase 7 — PMC + Parameter v3 framing
|
||||
- Diff captured `pmc_rdpmcrng` (R100) + `cnc_rdparam` (1320) bytes vs the v1 `IODBPMC0` / `IODBPSD` shapes;
|
||||
fix v3 parsing. Confirm whether the failures are framing or genuine CNC restriction (PMC path / param
|
||||
presence) — macro working proves the envelope is fine, so suspect struct offsets first.
|
||||
- **Acceptance:** `R100` reads a plausible value (or a *correct* status if genuinely restricted); a known
|
||||
parameter reads its value; fixture unit tests; live re-validation.
|
||||
|
||||
## Phase 8 — Probe truthfulness
|
||||
- `FocasDriverProbe` Phase-2 degrades to `Ok=true` ("TCP only") when FWLIB is absent → HEALTHY off a bare
|
||||
socket. Replace with a wire-client probe: open `WireFocasClient` + one sample read (e.g. sysinfo). Keep
|
||||
the TCP preflight for fast rejection.
|
||||
- **Acceptance:** probe reports unhealthy when the CNC TCP-accepts but FOCAS reads fail; HEALTHY only on a
|
||||
real session + read.
|
||||
|
||||
## Phase 9 — Docs + version matrix
|
||||
- Add a real-hardware row to `docs/v2/focas-version-matrix.md`: 30i/31i-B → PDU v3; record which command
|
||||
families are validated. Update `docs/drivers/FOCAS.md` + this plan's status as phases land.
|
||||
|
||||
## Phase 10 — Deploy to wonder + end-to-end verify
|
||||
- Optional: set the device series to `ThirtyOne_i` (sysinfo says CncType 31; capability ranges identical to
|
||||
`Thirty_i`, so cosmetic).
|
||||
- Rebuild a self-contained win-x64 publish of `ZB.MOM.WW.OtOpcUa.Host` (or swap just
|
||||
`ZB.MOM.WW.OtOpcUa.Driver.FOCAS.dll`) into `E:\ApiInstall\OtOpcUa\` on `wonder-app-vd03`, preserving
|
||||
`appsettings*.json` + `data\`; restart `OtOpcUaHost`. (Access: servecli `:2222`, key
|
||||
`~/.ssh/servecli_wonder` — see the deployment doc + memory.)
|
||||
- **Re-run a deployment** in the AdminUI afterward — FixedTree nodes are emitted at `DiscoverAsync`, so the
|
||||
address space must be rebuilt to surface them.
|
||||
- **Acceptance (via the OtOpcUa CLI client → `opc.tcp://wonder-app-vd03.zmr.zimmer.com:4840/OtOpcUa`):**
|
||||
`ns=2;s=EQ-3686c0272279/parts-count` + `/parts-required` read **Good**; FixedTree Identity/Axes/Program
|
||||
nodes present with live values; (timers/servo-load good once Phases 4–5 land).
|
||||
|
||||
## Phase 11 — Commit + push
|
||||
- Commit source + tests + docs on a branch `feat/focas-pdu-v3` (keep it separate from the unrelated
|
||||
pre-existing local edits in the tree). Push to gitea per the repo's flow. The Akka-roles host fix is a
|
||||
separate concern (see deployment doc) — note it but it's a box config change, not repo code.
|
||||
|
||||
---
|
||||
|
||||
## Test strategy
|
||||
- **Offline (CI-safe):** unit tests over the Phase-1 captured v3 byte fixtures for every parser
|
||||
(`FocasWireProtocolTests` + new `FocasWireModels`/parse tests). Keep the docker mock (v1) green.
|
||||
- **Live (env-gated):** the `Driver.FOCAS.Cli` (`probe`/`read`) + the status harness, against
|
||||
`10.201.31.5`. Gate behind an env var / `[Trait]` so CI without a CNC skips.
|
||||
|
||||
## Sequencing notes
|
||||
- Phase 1 (capture) unblocks 4/5/7. Phase 2 (servo-load safety) gates enabling FixedTree on v3. Phases 4–7
|
||||
are independent and parallelizable once captures exist. Phase 10 depends on whichever surfaces you want
|
||||
live (macro tags already work after Phase 0, so a minimal deploy could happen now; full FixedTree wants
|
||||
Phases 2/5/6).
|
||||
- **Keep emitting v1 requests** unless Phase 3 proves otherwise — it's validated and minimal.
|
||||
|
||||
## File map
|
||||
- `src/Drivers/.../Wire/FocasWireProtocol.cs` — version gate (done), request-version policy (Phase 3).
|
||||
- `src/Drivers/.../Wire/FocasWireClient.cs` — CT-bound reads (Phase 2), per-command requests.
|
||||
- `src/Drivers/.../Wire/FocasWireModels.cs` + parse helpers — per-command v3 struct fixes (Phases 4–7).
|
||||
- `src/Drivers/.../FocasDriver.cs` — FixedTree axis iteration (Phase 6), FixedTree enable gating (Phase 2).
|
||||
- `src/Drivers/.../FocasDriverProbe.cs` — wire-client probe (Phase 8).
|
||||
- `scripts/focas/capture-initiate.py` — extend to data PDUs (Phase 1).
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` + `Fixtures/v3/` — fixtures + parser tests.
|
||||
- `docs/v2/focas-version-matrix.md`, `docs/drivers/FOCAS.md` — docs (Phase 9).
|
||||
@@ -143,3 +143,25 @@ The expensive half is Tier-C process isolation so that a crashing
|
||||
`Fwlib64.dll` doesn't take the main OPC UA server down with it. See
|
||||
[`docs/v2/implementation/focas-isolation-plan.md`](implementation/focas-isolation-plan.md)
|
||||
for that plan (task #220).
|
||||
|
||||
## Real-hardware validation (first live FOCAS contact)
|
||||
|
||||
**2026-06-25 — FANUC 31i-B (Makino Pro 5), `10.201.31.5:8193`.** The first time the managed wire
|
||||
client met real FOCAS hardware. The control answers **PDU version 3**; the 10-byte header framing and
|
||||
response block envelope are byte-identical to v1 (only the version field differs). `cnc_sysinfo`
|
||||
reports CncType 31, Series `G431`, 7 axes (X Y Z B C A A), MtType MM.
|
||||
|
||||
| Command family | v3 result | Notes |
|
||||
| --- | --- | --- |
|
||||
| `cnc_sysinfo`, `cnc_rdaxisname`, `cnc_rdspdlname` | ✅ validated | identity / axis / spindle names |
|
||||
| `cnc_rddynamic2` (positions, feed, spindle, program/seq) | ✅ validated | 1-based axis iteration; program O-number reads via big-endian |
|
||||
| `cnc_rdopmode`, `cnc_exeprgname2` | ✅ validated | mode + executing program |
|
||||
| `cnc_rdmacro` | ✅ validated | deployed `MACRO:3901/3902` tags read Good |
|
||||
| `cnc_rdtimer` | ✅ validated | 8-byte {minute, msec} payload is **little-endian** (unlike the big-endian envelope) |
|
||||
| `pmc_rdpmcrng` | ✅ validated | request range must be widened to the data-type byte width (`end = start + width - 1`) |
|
||||
| `cnc_rdsvmeter` | ✅ validated | per-axis LOADELM is **8 bytes**, not 12; names come from the 0x0089 block; load *scaling* unconfirmed |
|
||||
| `cnc_rdalmmsg2` | ✅ validated | read a live active alarm (`#3080`) |
|
||||
| `cnc_rdparam` | ❌ **unsupported** | `EW_FUNC` across 14 request-framing variants × 4 known-present params — likely wrong v3 command id (`0x000e`) or restricted on this control; needs a reference FWLIB trace |
|
||||
|
||||
Full finding + captured wire bytes + the fix per surface:
|
||||
[`../plans/2026-06-25-focas-pdu-v3-30i-b-support.md`](../plans/2026-06-25-focas-pdu-v3-30i-b-support.md).
|
||||
|
||||
Reference in New Issue
Block a user