# 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)** | | `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.