Adds FocasAlarmProjection with two modes (ActiveOnly default, ActivePlusHistory) that polls cnc_rdalmhistry on connect + on a configurable cadence (5 min default, HistoryDepth=100 capped at 250). Emits historic events via IAlarmSource with SourceTimestampUtc set from the CNC's reported timestamp; dedup keyed on (OccurrenceTime, AlarmNumber, AlarmType). Ships the ODBALMHIS packed-buffer decoder + encoder in Wire/FocasAlarmHistoryDecoder.cs and threads ReadAlarmHistoryAsync through IFocasClient (default no-op so existing transport variants stay back-compat). FocasDriver now implements IAlarmSource. 13 new unit tests cover: mode switch, dedup, distinct-timestamp emission, type-as-key behaviour, OccurrenceTime passthrough (not Now), HistoryDepth clamp/fallback, and decoder round-trip. All 341 FOCAS unit tests still pass. Docs: docs/drivers/FOCAS.md (new), docs/v2/focas-deployment.md (new), docs/v2/implementation/focas-wire-protocol.md (new), docs/v2/implementation/focas-simulator-plan.md (new), docs/drivers/FOCAS-Test-Fixture.md (alarm-history bullet appended). Closes #267
3.0 KiB
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_almshort-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_lenoverrunning the payload truncates the entry list at the malformed entry rather than throwing.