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