Files
lmxopcua/docs/v2/implementation/focas-wire-protocol.md
2026-04-26 05:15:52 -04:00

9.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
... ... ...
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:

  • 0Good
  • 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.

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_status0 = 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.