217 lines
9.0 KiB
Markdown
217 lines
9.0 KiB
Markdown
# FOCAS wire protocol — packed-buffer surface
|
|
|
|
Notes on the language-neutral packed-buffer encoding the FOCAS driver +
|
|
focas-mock simulator share. This format is **not** the FWLIB native struct
|
|
layout — Tier-C Fwlib32 backends marshal directly from the FANUC C struct.
|
|
The packed surface exists so the simulator (Python / FastAPI) and the .NET
|
|
wire client can speak a common format over IPC without piping a Win32 DLL
|
|
through both ends.
|
|
|
|
## Command id table
|
|
|
|
Each FOCAS-equivalent call gets a stable wire-protocol command id. Ids are
|
|
**append-only** — never renumber, never reuse.
|
|
|
|
| Id | FOCAS API | Surface |
|
|
| --- | --- | --- |
|
|
| `0x0001` | `cnc_rdcncstat` | ODBST 9-field status struct |
|
|
| `0x0002` | `cnc_rdparam` | parameter value (one number) |
|
|
| `0x0003` | `cnc_rdmacro` | macro variable value |
|
|
| `0x0004` | `cnc_rddiag` | diagnostic value |
|
|
| ... | ... | ... |
|
|
| **`0x0102`** | **`cnc_wrparam`** | **IODBPSD parameter-write packet (issue #269, plan PR F4-b)** |
|
|
| **`0x0103`** | **`cnc_wrmacro`** | **ODBM macro-write packet (issue #269, plan PR F4-b)** |
|
|
| **`0x0104`** | **`pmc_wrpmcrng`** | **IODBPMC PMC range-write packet (issue #270, plan PR F4-c)** |
|
|
| `0x0F1A` | **`cnc_rdalmhistry`** | **ODBALMHIS alarm-history ring-buffer dump (issue #267, plan PR F3-a)** |
|
|
|
|
## ODBALMHIS — alarm history (`cnc_rdalmhistry`, command `0x0F1A`)
|
|
|
|
Issued by `FocasAlarmProjection` when
|
|
`FocasDriverOptions.AlarmProjection.Mode == ActivePlusHistory`. Returns up
|
|
to `depth` most-recent ring-buffer entries.
|
|
|
|
### Request
|
|
|
|
| Offset | Width | Field | Notes |
|
|
| --- | --- | --- | --- |
|
|
| 0 | `int16 LE` | `depth` | clamped client-side to `[1..250]` (`FocasAlarmProjectionOptions.MaxHistoryDepth`) |
|
|
|
|
### Response (packed buffer, little-endian)
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `num_alm` — number of entries that follow. `< 0` indicates CNC error. |
|
|
| 2 | repeated | `ALMHIS_data alm[num_alm]` (see below) |
|
|
|
|
Each entry block:
|
|
|
|
| Offset (rel.) | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `year` |
|
|
| 2 | `int16 LE` | `month` |
|
|
| 4 | `int16 LE` | `day` |
|
|
| 6 | `int16 LE` | `hour` |
|
|
| 8 | `int16 LE` | `minute` |
|
|
| 10 | `int16 LE` | `second` |
|
|
| 12 | `int16 LE` | `axis_no` (1-based; 0 = whole-CNC) |
|
|
| 14 | `int16 LE` | `alm_type` (P/S/OT/SV/SR/MC/SP/PW/IO encoded numerically) |
|
|
| 16 | `int16 LE` | `alm_no` |
|
|
| 18 | `int16 LE` | `msg_len` (0..32 typical) |
|
|
| 20 | `msg_len` | ASCII message (no null terminator) |
|
|
| `20 + msg_len` | 0..3 | pad to 4-byte boundary so per-entry blocks stay self-delimiting |
|
|
|
|
The CNC stamps `year..second` in **its own local time**. The deployment
|
|
guide instructs operators to keep CNC clocks on UTC so the projection's
|
|
dedup key `(OccurrenceTime, AlarmNumber, AlarmType)` stays stable across
|
|
DST transitions. The .NET decoder
|
|
(`Wire/FocasAlarmHistoryDecoder.Decode`) constructs each
|
|
`DateTimeOffset` with `TimeSpan.Zero` (UTC) on that assumption.
|
|
|
|
### Error handling
|
|
|
|
- A negative `num_alm` short-circuits decode to an empty list — the
|
|
projection treats it as "no history this tick" and the next poll
|
|
retries.
|
|
- Malformed timestamps (e.g. month=0) are skipped per-entry instead of
|
|
faulting the whole decode; the dedup key for malformed entries would be
|
|
unstable anyway.
|
|
- `msg_len` overrunning the payload truncates the entry list at the
|
|
malformed entry rather than throwing.
|
|
|
|
## IODBPSD — parameter write (`cnc_wrparam`, command `0x0102`)
|
|
|
|
Issue #269, plan PR F4-b. The write-side payload is the **byte-symmetric
|
|
inverse of the `cnc_rdparam` read** — the same `IODBPSD` struct shape, and
|
|
the .NET wire client uses the read-side decoder reversed (`EncodeParamValue`
|
|
in `FwlibFocasClient.cs`) so the encoder/decoder are guaranteed to stay in
|
|
lock-step.
|
|
|
|
### Request
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `datano` — parameter number (e.g. `1815`) |
|
|
| 2 | `int16 LE` | `type` — axis index (1-based; `0` = whole-CNC parameter) |
|
|
| 4 | `length` | `data` payload — width depends on parameter type |
|
|
|
|
`length` (request frame trailer, drives `data` width):
|
|
|
|
| FocasDataType | `length` | Payload encoding |
|
|
| --- | --- | --- |
|
|
| `Byte` | `4 + 1` | one signed byte at offset 4 |
|
|
| `Int16` | `4 + 2` | int16 LE at offset 4 |
|
|
| `Int32` | `4 + 4` | int32 LE at offset 4 |
|
|
|
|
Bit-addressed parameters (`PARAM:1815/0` form) are not supported by F4-b
|
|
and surface as `BadNotSupported`; F4-c will land the read-modify-write
|
|
helper alongside the PMC bit RMW path.
|
|
|
|
### Response
|
|
|
|
Single `int16 LE` return code per the standard FWLIB convention:
|
|
|
|
- `0` → `Good`
|
|
- `11` (`EW_PASSWD`) → **`BadUserAccessDenied`** (was `BadNotWritable`
|
|
pre-F4-b — see `FocasStatusMapper`). Means the parameter-write switch is
|
|
off or the CNC isn't in MDI mode; the F4-d unlock workflow will close
|
|
the loop on this from the OPC UA side.
|
|
- Other `EW_*` codes map per
|
|
[`FocasStatusMapper.MapFocasReturn`](../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs).
|
|
|
|
## ODBM — macro write (`cnc_wrmacro`, command `0x0103`)
|
|
|
|
Issue #269, plan PR F4-b. The write-side payload mirrors the
|
|
`cnc_rdmacro` read shape: the same `(mcr_val, dec_val)` (integer +
|
|
decimal-point count) split, but emitted from the .NET side rather than
|
|
decoded.
|
|
|
|
### Request
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `number` — macro variable number (e.g. `500`) |
|
|
| 2 | `int16 LE` | `length` — fixed at `8` for ODBM |
|
|
| 4 | `int32 LE` | `mcr_val` — scaled integer value |
|
|
| 8 | `int16 LE` | `dec_val` — decimal-point count |
|
|
|
|
F4-b ships **integer-only writes** (`dec_val = 0`) to match the most
|
|
common HMI pattern; a future `WriteMacroScaled` overload will land if the
|
|
field calls for fractional macro setpoints. Read-side decoders apply
|
|
`mcr_val / 10^dec_val`, so a `dec_val = 0` write surfaces back as the
|
|
integer it was emitted as.
|
|
|
|
### Response
|
|
|
|
Same single-int16 envelope as `cnc_wrparam`. `EW_PASSWD` is rare on macro
|
|
writes (the gate-switch protection is parameter-specific) but the mapper
|
|
treats both kinds identically.
|
|
|
|
### Symmetry note
|
|
|
|
The plan carries a "byte layout symmetry" requirement — the encoder for
|
|
each kind is the read-side decoder reversed. Adding a new parameter type
|
|
(e.g. `Int64` parameters, when they ship) means extending both sides in
|
|
the same PR; the unit test
|
|
`FocasWriteParameterTests.ParameterWrite_round_trip_stores_value_visible_to_subsequent_read`
|
|
exercises encode → store → decode with the fake wire client and is the
|
|
canary for symmetry regressions.
|
|
|
|
## IODBPMC — PMC range write (`pmc_wrpmcrng`, command `0x0104`)
|
|
|
|
Issue #270, plan PR F4-c. The write-side payload is the read-side
|
|
`pmc_rdpmcrng` IODBPMC packet with the data direction inverted: the
|
|
caller fills the `data[]` byte run and the simulator / Fwlib32 stores
|
|
it; the response is the small status envelope rather than the populated
|
|
data buffer the read side returns.
|
|
|
|
### Request
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `type_a` — PMC address-type code (R=5, G=4, F=3, D=8, X=1, Y=2, K=10, A=11, E=12, T=6, C=7) |
|
|
| 2 | `int16 LE` | `type_d` — data type (`0` = byte; only byte writes are issued — bit writes wrap the byte path with a read-modify-write helper) |
|
|
| 4 | `uint16 LE` | `datano_s` — first byte address (inclusive) |
|
|
| 6 | `uint16 LE` | `datano_e` — last byte address (inclusive) — `(datano_e - datano_s + 1)` is the byte count |
|
|
| 8 | `bytes` | `data[]` — payload, exactly `(datano_e - datano_s + 1)` bytes |
|
|
|
|
The header is 8 bytes; the FWLIB `IODBPMC.data` field caps at 32 bytes
|
|
(40-byte total per call), so larger ranges are chunked into 32-byte
|
|
sub-calls by the wire client. The simulator MUST honour the same chunk
|
|
ceiling so chunked-vs-single round-trips produce the same final bytes.
|
|
|
|
### Response
|
|
|
|
Same single-int16 envelope as `cnc_wrparam` / `cnc_wrmacro`:
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | `int16 LE` | `ew_status` — `0` = success, non-zero = FANUC `EW_*` |
|
|
|
|
`EW_NOOPT` (option not installed), `EW_NUMBER` (out-of-range address),
|
|
`EW_LENGTH` (chunk size mismatch) are the typical failures the simulator
|
|
reproduces; the mapper translates them to OPC UA status codes the same
|
|
way the read-side does.
|
|
|
|
### Bit-level RMW (driver-side, no extra wire op)
|
|
|
|
`pmc_wrpmcrng` is **byte-addressed** — there is no sub-byte write op on
|
|
the wire. Bit writes go through `IFocasClient.WritePmcBitAsync` which:
|
|
|
|
1. Issues a 1-byte `pmc_rdpmcrng` to fetch the parent byte.
|
|
2. Masks the target bit (set: OR; clear: AND-NOT).
|
|
3. Issues a 1-byte `pmc_wrpmcrng` with the modified byte.
|
|
|
|
A per-byte semaphore in `FwlibFocasClient` serialises concurrent bit
|
|
writes against the same byte so two updates that race never lose one
|
|
another's bit. The simulator's handler implements the same byte-aligned
|
|
semantics — bit writes never reach it as a separate frame.
|
|
|
|
### Symmetry note
|
|
|
|
The encoder is the `pmc_rdpmcrng` decoder reversed: the read side parses
|
|
`(type_a, type_d, datano_s, datano_e)` from the request and emits the
|
|
data buffer in the response; the write side parses all five fields plus
|
|
the data buffer from the request and emits a status int16 in the
|
|
response. Tests `FocasWritePmcTests.PMC_*` exercise the round-trip on
|
|
the fake wire client.
|