FOCAS — commit previously-orphaned support files
Brings seven FOCAS-related files into git that shipped as part of earlier FOCAS work but were never staged. Adding them now so the tree reflects the compilable state + pre-empts dead references from the migration commit that follows: - src/.../Driver.FOCAS/FocasAlarmProjection.cs — raise/clear diffing + severity mapping surfaced via IAlarmSource on FocasDriver. Referenced by committed FocasDriver.cs; tests in FocasAlarmProjectionTests.cs. - src/.../Admin/Services/FocasDriverDetailService.cs — Admin UI per-instance detail page data source. - src/.../Admin/Components/Pages/Drivers/FocasDetail.razor — Blazor page rendering the above (from task #69). - tests/.../Admin.Tests/FocasDriverDetailServiceTests.cs — exercises the detail service. - tests/.../Driver.FOCAS.Tests/FocasAlarmProjectionTests.cs — raise/clear diff semantics against FakeFocasClient. - tests/.../Driver.FOCAS.Tests/FocasHandleRecycleTests.cs — proactive recycle cadence test. - docs/v2/implementation/focas-wire-protocol.md — captured FOCAS/2 Ethernet wire protocol reference. Useful going forward even though the Tier-C / simulator plan docs are historical. No runtime behaviour change — these files compile today and the solution build/test pass already depends on them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
291
docs/v2/implementation/focas-wire-protocol.md
Normal file
291
docs/v2/implementation/focas-wire-protocol.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# FOCAS wire protocol — what's authoritative vs. what's guessed
|
||||
|
||||
Companion to [`focas-simulator-plan.md`](focas-simulator-plan.md). Written during
|
||||
Stream B on 2026-04-23 after a research pass through `strangesast/fwlib` +
|
||||
public FOCAS documentation. Purpose: separate what we *know* about the FOCAS
|
||||
wire protocol (can quote with confidence) from what we're *guessing* (will need
|
||||
Wireshark traces to validate in Stream C).
|
||||
|
||||
This document directly informs `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/server/`.
|
||||
|
||||
## Authoritative — from Fanuc's public `fwlib32.h`
|
||||
|
||||
The header file is distributed with the FOCAS Developer Kit and mirrored in OSS
|
||||
repos (notably `strangesast/fwlib`). The **struct layouts** documented there
|
||||
are stable across FOCAS versions and authoritative for the payload shapes our
|
||||
Python mock has to emit.
|
||||
|
||||
### ODBM — macro variable read buffer
|
||||
|
||||
```c
|
||||
typedef struct odbm {
|
||||
short datano; // macro variable number
|
||||
short dummy; // reserved / alignment padding
|
||||
long mcr_val; // 32-bit signed macro value
|
||||
short dec_val; // decimal-point count (0-9)
|
||||
} ODBM;
|
||||
```
|
||||
|
||||
With `#pragma pack(push, 4)` (the FOCAS default), total size is **10 bytes** on
|
||||
Windows: 2 + 2 + 4 + 2. Our `FwlibNative.cs` matches this exactly.
|
||||
|
||||
Our mock's `_READ_RESP_STRUCT = struct.Struct(">iH")` is **only 6 bytes** —
|
||||
missing `datano` + `dummy`. A real Fwlib decoding the scaffold response will
|
||||
read garbage. Stream C fix: prepend two `short` fields.
|
||||
|
||||
### IODBPSD — CNC parameter read/write buffer
|
||||
|
||||
```c
|
||||
typedef struct iodbpsd {
|
||||
short datano; // parameter number
|
||||
short type; // axis index (0 for non-axis parameters)
|
||||
union {
|
||||
char cdata;
|
||||
short idata;
|
||||
long ldata;
|
||||
char cdatas[MAX_AXIS]; // MAX_AXIS varies — 8 on 0i, 32 on 30i
|
||||
short idatas[MAX_AXIS];
|
||||
long ldatas[MAX_AXIS];
|
||||
} u;
|
||||
} IODBPSD;
|
||||
```
|
||||
|
||||
With `pack(4)` and `MAX_AXIS=8`, total size = 2 + 2 + 32 = **36 bytes**. Our
|
||||
`FwlibNative.cs` matches this (`[SizeConst = 32]` data buffer).
|
||||
|
||||
Our mock's current param handler doesn't return bytes in IODBPSD shape —
|
||||
response payload is just the raw value. Stream C fix: wrap in 4-byte header
|
||||
+ union-padded data.
|
||||
|
||||
### ODBST — status info
|
||||
|
||||
```c
|
||||
typedef struct odbst {
|
||||
short dummy; // reserved
|
||||
short tmmode; // Memory / Tape / MDI / EDIT / DNC
|
||||
short aut; // automatic mode
|
||||
short run; // running state
|
||||
short motion; // motion state
|
||||
short mstb; // M/S/T/B finish signal
|
||||
short emergency; // emergency stop
|
||||
short alarm; // alarm state
|
||||
short edit; // edit mode sub-state
|
||||
} ODBST;
|
||||
```
|
||||
|
||||
9 × short = **18 bytes**. Our mock already emits 18 bytes via
|
||||
`struct.Struct(">9h")`. ✓ correct.
|
||||
|
||||
### IODBPMC — PMC range read/write buffer
|
||||
|
||||
```c
|
||||
typedef struct iodbpmc {
|
||||
short type_a; // PMC address letter encoded as ADR_* numeric code
|
||||
short type_d; // data type: 0=byte, 1=word, 2=long, 4=float, 5=double
|
||||
unsigned short datano_s; // start address number
|
||||
unsigned short datano_e; // end address number
|
||||
union {
|
||||
char cdata[5];
|
||||
short idata[5];
|
||||
long ldata[5];
|
||||
float fdata[5];
|
||||
double dbdata[5];
|
||||
} u; // 40-byte union (widest = dbdata = 5×8 bytes)
|
||||
} IODBPMC;
|
||||
```
|
||||
|
||||
With `pack(4)` the union is 40 bytes; struct total = 8 + 40 = **48 bytes**.
|
||||
Our `FwlibNative.cs` matches this.
|
||||
|
||||
Our mock's PMC handler takes a different layout (uint16 handle + uint8 letter
|
||||
+ ...). Stream C fix: rewrite to IODBPMC shape.
|
||||
|
||||
## Reference trace findings (2026-04-23 dev-box reversing)
|
||||
|
||||
**Good news** — we don't need a bench CNC for first-pass reversing. Loading
|
||||
`Fwlib64.dll` in `otopcua-focas-cli` + pointing it at our Python simulator on
|
||||
`127.0.0.1:8193` + enabling `OTOPCUA_FOCAS_RAW_CAPTURE=1` on the sim lets us
|
||||
observe Fwlib's outbound bytes + iterate on reply shapes. Each cycle is ~5s;
|
||||
progress measure is "Fwlib sends more bytes before disconnecting".
|
||||
|
||||
### Confirmed wire facts
|
||||
|
||||
**Magic prefix** — every frame Fwlib sends begins with `0xA0 0xA0 0xA0 0xA0`
|
||||
(4 bytes). This is NOT a length prefix — our scaffold tried to decode it as
|
||||
uint32-big-endian = 2.7 GB and died. It's a fixed protocol marker.
|
||||
|
||||
**Handshake request** — `cnc_allclibhndl3` produces this 8-byte frame:
|
||||
|
||||
```
|
||||
a0 a0 a0 a0 00 01 01 01
|
||||
└─ magic ─┘ └── negotiation ──┘
|
||||
```
|
||||
|
||||
The 4-byte negotiation field is stable across our observations (always
|
||||
`00 01 01 01`). Interpretation TBD — possibly `(version_major=0x0001,
|
||||
version_minor=0x0101)` or `(protocol=0x01, subtype=0x010101)`.
|
||||
|
||||
**Handshake reply that Fwlib accepts** (empirically confirmed — doesn't
|
||||
disconnect):
|
||||
|
||||
```
|
||||
a0 a0 a0 a0 00 01 01 01 00 XX 00 YY
|
||||
└─ magic ─┘ └── echo ──┘ handle api_version
|
||||
```
|
||||
|
||||
12 bytes: magic + echoed negotiation + 2-byte handle + 2-byte api_version code.
|
||||
|
||||
### Post-handshake frame shape — decoded via drain mode
|
||||
|
||||
The simulator's `OTOPCUA_FOCAS_DRAIN_AFTER_HANDSHAKE=1` mode reads all inbound
|
||||
bytes for 1000 ms after the handshake reply without attempting any decode.
|
||||
Captured payload from `cnc_allclibhndl3`:
|
||||
|
||||
```
|
||||
00 02 00 02 a0 a0 a0 a0 00 01 21 01 00 00
|
||||
└── prefix ─┘ └── magic ─┘ └─── body ────┘
|
||||
4 bytes 4 bytes 6 bytes (total = 14 bytes)
|
||||
```
|
||||
|
||||
**Key discovery**: post-handshake frames have a **4-byte prefix BEFORE the
|
||||
magic**, not magic-first. Frame shape:
|
||||
|
||||
```
|
||||
uint16 msg_counter // starts at 2; handshake was #1 implicitly
|
||||
uint16 handle_echo // matches the handle our open reply returned
|
||||
4 bytes FOCAS_MAGIC // 0xA0A0A0A0
|
||||
N bytes body // function-specific
|
||||
```
|
||||
|
||||
Session 1's drain captured only the prefix (`00 02 00 01`) before timing
|
||||
out — TCP multiplexed the two test sessions's bytes differently. Session 2
|
||||
caught the full 14-byte frame.
|
||||
|
||||
### Body bytes — first post-handshake request
|
||||
|
||||
Body on `cnc_allclibhndl3` first post-handshake frame:
|
||||
|
||||
```
|
||||
00 01 21 01 00 00
|
||||
```
|
||||
|
||||
Informed guesses (unvalidated):
|
||||
|
||||
- `00 01` = body length (1 useful byte?) or sub-request count
|
||||
- `21 01` = function code / operation tag — `0x21` is seen in public FOCAS
|
||||
reverse-engineering notes associated with "system info" / "controller
|
||||
identification" queries
|
||||
- `00 00` = padding / reserved
|
||||
|
||||
Likely this is Fwlib's "tell me what CNC you are" query — part of
|
||||
`cnc_allclibhndl3`'s internal handshake continuation before the handle is
|
||||
fully established. Returning an empty or malformed response causes Fwlib
|
||||
to declare the far end "not a CNC" and error with `EW_FUNC` (16).
|
||||
|
||||
### Iteration 3 — echo response, error-code advances
|
||||
|
||||
Sending back `<prefix><magic><echoed body>` (14 bytes matching request shape)
|
||||
advances Fwlib's client-side error code from **`EW_-16` (socket-level)** to
|
||||
**`EW_-17` (protocol-level rejection)**. Fwlib reads our response in full
|
||||
before disconnecting with `peer closed mid-frame`.
|
||||
|
||||
Meaning: our **frame structure is correct enough** that Fwlib parses it as a
|
||||
valid FOCAS frame; the **body content** (the 6 bytes after magic) is where
|
||||
the semantic mismatch now lives. Fwlib expects specific bytes back for the
|
||||
`0x2101` system-info query and an echo doesn't match.
|
||||
|
||||
### Current iteration block
|
||||
|
||||
Going deeper without reference requires either:
|
||||
|
||||
- **A bench CNC** (#54) to capture a real response to the `0x2101` query.
|
||||
Stream C.2 Wireshark trace gives us the exact byte pattern Fwlib expects.
|
||||
- **Published FOCAS response specs** for sub-function `0x2101` — not present
|
||||
in `strangesast/fwlib` headers; likely only in the licensed Developer Kit
|
||||
binary docs.
|
||||
- **Blind enumeration** — try N variations of the 6-byte body response until
|
||||
Fwlib's error code changes again. High cost, low signal.
|
||||
|
||||
The first two are both blocked on resources we don't have. The third is
|
||||
~hundreds of cycles with no guarantee of convergence.
|
||||
|
||||
### Diminishing-returns checkpoint
|
||||
|
||||
**What we've proven without hardware**:
|
||||
1. Magic prefix `0xA0A0A0A0` confirmed
|
||||
2. Handshake request format decoded (`magic + 4-byte negotiation`)
|
||||
3. Handshake response format that Fwlib accepts (`magic + echo + handle + api`)
|
||||
4. Post-handshake frame format decoded (`prefix + magic + body`)
|
||||
5. First post-handshake function code observed (`0x2101` — likely system-info)
|
||||
6. Error code progression `EW_SOCKET` → `EW_PROTOCOL` confirms our framing is
|
||||
structurally correct
|
||||
|
||||
**What we can't prove without bench CNC or reference docs**:
|
||||
1. The exact 6-byte response body Fwlib expects for `0x2101`
|
||||
2. The full list of post-handshake function codes + their body shapes
|
||||
3. Whether subsequent frames use length prefixes or fixed body sizes
|
||||
|
||||
**Recommendation**: checkpoint here. The framing discoveries above are
|
||||
preserved in `server/frames.py` + `server/state.py` + `server/focas_server.py`
|
||||
+ `server/handlers/__init__.py`. When bench-CNC access unblocks Stream C.2's
|
||||
reference trace, the iteration loop (with the framing work already done)
|
||||
should converge in hours rather than days.
|
||||
|
||||
### Still unknown
|
||||
|
||||
- **Response shape** for the post-handshake body request — we can frame the
|
||||
prefix + magic correctly now, but what the 6-byte body response should
|
||||
carry (CNC series ID? version? capability flags?) needs further iteration.
|
||||
- **Function-id numeric values** for the 9 FWLIB calls our driver makes —
|
||||
one per call, need to be observed separately.
|
||||
- **Error encoding** on the wire.
|
||||
|
||||
### Next iteration cycles
|
||||
|
||||
With the handshake working, each subsequent function gets its own probe-and-observe
|
||||
loop. The simulator now has a `RAW_FRAME_MARKER = 0xFFFF` sentinel that lets a
|
||||
handler return exact wire bytes (bypassing the scaffold envelope) — use that to
|
||||
try different post-handshake replies and watch Fwlib's reaction.
|
||||
|
||||
## Stream C work order
|
||||
|
||||
Given what's authoritative vs. guessed, here's the most efficient path:
|
||||
|
||||
### Phase 1 — payload shapes (no hardware required)
|
||||
|
||||
- [ ] Rewrite `server/handlers/macro.py` response to return 10-byte ODBM:
|
||||
`short datano, short dummy, int32 mcr_val, short dec_val`
|
||||
- [ ] Rewrite `server/handlers/param.py` response to return 36-byte IODBPSD:
|
||||
`short datano, short type, bytes[32] u`
|
||||
- [ ] Rewrite `server/handlers/pmc.py` response to return 48-byte IODBPMC:
|
||||
`short type_a, short type_d, uint16 datano_s, uint16 datano_e, bytes[40] u`
|
||||
- [ ] Add unit tests asserting byte-exact sizes
|
||||
- [ ] Update validate_harness.py to match the new shapes
|
||||
|
||||
Effect: when Stream C gets its first Wireshark trace, the payload-layer of the
|
||||
mock is already correct. Only the framing layer needs iteration.
|
||||
|
||||
### Phase 2 — framing (requires hardware)
|
||||
|
||||
This is the iterative Wireshark loop — no point starting until the Windows rig
|
||||
+ licensed Fwlib64.dll + real CNC are all available. See the implementer's
|
||||
checklist in
|
||||
[`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md`](../../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/Docker/README.md).
|
||||
|
||||
### Phase 3 — flip the C# test gate
|
||||
|
||||
Once Phase 2 proves Fwlib64 can talk to the mock:
|
||||
|
||||
- [ ] Flip `OTOPCUA_FOCAS_SIM_WIRE_COMPAT=1` in the CI env
|
||||
- [ ] Expand `tests/.../IntegrationTests/Series/WireCompatGatedTests.cs` with
|
||||
real per-series assertions
|
||||
- [ ] Update `scripts/e2e/test-focas.ps1` to accept `-ProfileName`
|
||||
- [ ] Close Stream D
|
||||
|
||||
## References
|
||||
|
||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibNative.cs) — P/Invoke surface, authoritative struct layouts
|
||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FwlibFocasClient.cs) — reference C# implementation of each FWLIB call
|
||||
- [`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs`](../../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs) — EW_* → OPC UA status mapping
|
||||
- Fanuc FOCAS Developer Kit (licensed, not in repo) — ultimate source of truth
|
||||
- `strangesast/fwlib` on GitHub — redistributes `fwlib32.h` + runtime binaries; no wire protocol docs
|
||||
Reference in New Issue
Block a user