# FOCAS driver Fanuc CNC driver for the FS 0i / 16i / 18i / 21i / 30i / 31i / 32i / 35i / Power Mate i families. Talks to the controller via the licensed `Fwlib32.dll` (Tier C, process-isolated per [`docs/v2/driver-stability.md`](../v2/driver-stability.md)). For range-validation and per-series capability surface see [`docs/v2/focas-version-matrix.md`](../v2/focas-version-matrix.md). ## Alarm history (`cnc_rdalmhistry`) — issue #267, plan PR F3-a `FocasAlarmProjection` exposes two modes via `FocasDriverOptions.AlarmProjection`: | Mode | Behaviour | | --- | --- | | `ActiveOnly` *(default)* | Subscribe / unsubscribe / acknowledge wire up so capability negotiation works, but no history poll runs. Back-compat with every pre-F3-a deployment. | | `ActivePlusHistory` | On subscribe (== "on connect") and on every `HistoryPollInterval` tick, the projection issues `cnc_rdalmhistry` for the most recent `HistoryDepth` entries. Each previously-unseen entry fires an `OnAlarmEvent` with `SourceTimestampUtc` set from the CNC's reported timestamp — OPC UA dashboards see the real occurrence time, not the moment the projection polled. | ### Config knobs ```jsonc { "AlarmProjection": { "Mode": "ActivePlusHistory", // "ActiveOnly" (default) | "ActivePlusHistory" "HistoryPollInterval": "00:05:00", // default 5 min "HistoryDepth": 100 // default 100, capped at 250 } } ``` ### Dedup key `(OccurrenceTime, AlarmNumber, AlarmType)`. The same triple across two polls only emits once. The dedup set is in-memory and **resets on reconnect** — first poll after reconnect re-emits everything in the ring buffer. OPC UA clients that need exactly-once semantics dedupe client-side on the same triple (the timestamp + type + number tuple is stable across the boundary). ### `HistoryDepth` cap Capped at `FocasAlarmProjectionOptions.MaxHistoryDepth = 250` so an operator who types `10000` by accident can't blast the wire session with a giant request. Typical FANUC ring buffers cap at ~100 entries; the default `HistoryDepth = 100` matches the most common ring-buffer size. ### Wire surface - Wire-protocol command id: `0x0F1A` (see [`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md)). - ODBALMHIS struct decoder: `Wire/FocasAlarmHistoryDecoder.cs`. - Tier-C Fwlib32 backend short-circuits the packed-buffer decoder by surfacing the FWLIB struct fields directly into `FocasAlarmHistoryEntry`. ## Writes (opt-in, off by default) — issue #268 (F4-a) + #269 (F4-b) + #270 (F4-c) Writes ship behind multiple independent opt-ins. All default off so a freshly deployed FOCAS driver is read-only until the deployment makes a deliberate choice. Decision record: [`docs/v2/decisions.md`](../v2/decisions.md) → "FOCAS write-path opt-in". | Knob | Default | Effect when off | | --- | --- | --- | | `FocasDriverOptions.Writes.Enabled` *(driver-level master switch)* | `false` | Every entry in a `WriteAsync` batch short-circuits to `BadNotWritable` with status text `writes disabled at driver level`. Wire client never gets touched. | | **`FocasDriverOptions.Writes.AllowParameter`** *(F4-b granular kill switch)* | **`false`** | **`PARAM:` writes return `BadNotWritable` with no wire client constructed. Defense in depth — even if `Enabled = true` an operator must explicitly opt into parameter writes per kind because a misdirected `cnc_wrparam` can put the CNC in a bad state.** | | **`FocasDriverOptions.Writes.AllowMacro`** *(F4-b granular kill switch)* | **`false`** | **`MACRO:` writes return `BadNotWritable` with no wire client constructed. Macro writes are the normal HMI-driven recipe / setpoint surface; gating them separately from `AllowParameter` lets a deployment open MACRO without exposing the heavier PARAM write surface.** | | **`FocasDriverOptions.Writes.AllowPmc`** *(F4-c granular kill switch)* | **`false`** | **PMC writes (R/G/F/D/X/Y/K/A/E/T/C letters, both Bit and Byte) return `BadNotWritable` with no wire client constructed. PMC is ladder working memory — a mistargeted bit can move motion, latch a feedhold, or flip a safety interlock, so PMC writes are gated separately from PARAM/MACRO so an operator team can open PARAM (commissioning) without exposing the much higher-blast-radius PMC surface.** | | `FocasTagDefinition.Writable` *(per-tag opt-in)* | `false` | The per-tag check returns `BadNotWritable` for that tag even when the driver-level flags are on. | > **PMC SAFETY CALLOUT** — PMC is the FANUC ladder's working memory. A > mistargeted bit can move motion (a Y-coil writing to a servo enable), > latch a feedhold (an internal R-relay the ladder ANDs with cycle-start), > or flip a safety interlock (an X-input shadow). **Treat PMC writes the > same way you'd treat editing a live ladder:** verify e-stop is live and > the machine is in jog mode before issuing the first write of a session. > The driver gates these writes behind THREE independent opt-ins > (`Writes.Enabled` + `Writes.AllowPmc` + per-tag `Writable`) precisely > because the blast radius is higher than parameter writes. ### PMC bit-write read-modify-write semantics — F4-c The FOCAS wire call `pmc_wrpmcrng` is **byte-addressed** — there is no sub-byte write primitive. When the driver receives a write request on a `Bit` tag (e.g. `R100.3`), it: 1. Reads the parent byte via `pmc_rdpmcrng` (1 byte at `R100`). 2. Masks the target bit (set: `current | (1 << bit)`; clear: `current & ~(1 << bit)`). 3. Writes the modified byte back via `pmc_wrpmcrng` (1 byte at `R100`). A **per-byte semaphore** serialises concurrent bit writes against the same byte so two updates that race never lose one another's bit. RMW means **a PMC bit write reads first, then writes back the whole byte** — if the ladder is also writing to that byte at the same instant, there is a small window where the driver's value can clobber the ladder's. Operators who care about this race must coordinate the write through a ladder-side handshake (e.g. the operator sets a request bit, the ladder reads + clears it). ### Config shape — F4-c ```jsonc { "Writes": { "Enabled": true, "AllowParameter": true, // F4-b — opt into cnc_wrparam "AllowMacro": true, // F4-b — opt into cnc_wrmacro "AllowPmc": true // F4-c — opt into pmc_wrpmcrng (incl. RMW bit writes) }, "Tags": [ { "Name": "RPM", "Address": "PARAM:1815", "DataType": "Int32", "Writable": true, "WriteIdempotent": false }, { "Name": "Recipe", "Address": "MACRO:500", "DataType": "Int32", "Writable": true, "WriteIdempotent": false }, { "Name": "StartFlag", "Address": "R100.3", "DataType": "Bit", "Writable": true, "WriteIdempotent": true } ] } ``` ### Server-layer ACL (LDAP groups) Per the [`docs/v2/acl-design.md`](../v2/acl-design.md) tier model, the FOCAS driver only declares per-tag `SecurityClassification`; `DriverNodeManager` applies the gate. The classification post-F4-b is: | Tag kind | Classification | LDAP group required (default mapping) | | --- | --- | --- | | `PARAM:N` writable | `Configure` | **`WriteConfigure`** | | `MACRO:N` writable | `Operate` | `WriteOperate` | | Other writable (PMC R/G/F/...) | `Operate` | `WriteOperate` | | Non-writable | `ViewOnly` | (no write permission) | Parameter writes need the heavier `WriteConfigure` group because they're mostly emergency commissioning territory; macro writes use `WriteOperate` because they're the normal HMI recipe surface. The driver-level `AllowParameter` / `AllowMacro` kill switches sit independently of ACL — an operator-team kill switch the deployment can flip without redeploying ACL group memberships. See [`docs/security.md`](../security.md) for the full group/permission map. `WriteIdempotent` is plumbed through Polly retry by the server-layer `CapabilityInvoker.ExecuteWriteAsync`. When `false` (default), failed writes are NOT auto-retried per plan decisions #44/#45 — a timeout that fires after the CNC already accepted the write would otherwise risk a duplicate non-idempotent action (alarm acks, M-code pulses, recipe steps). Flip `WriteIdempotent` on per tag for genuinely-idempotent writes (a parameter value that the operator simply wants forced to a target). ### Status-code semantics post-F4-b - `BadNotWritable` — one of: driver-level `Writes.Enabled = false`; per-tag `Writable = false`; **`Writes.AllowParameter = false` for a `PARAM:` tag (F4-b)**; **`Writes.AllowMacro = false` for a `MACRO:` tag (F4-b)**; **`Writes.AllowPmc = false` for a PMC tag (F4-c)**. Same status code, five distinct paths — operators distinguish by checking the knobs. - `BadUserAccessDenied` — **F4-b** — the CNC reported `EW_PASSWD` (parameter-write switch off / unlock required). F4-d will land the unlock workflow on top of this surface; today the deployment instructs the operator to flip the parameter-write switch on the CNC pendant. - `BadNotSupported` — both opt-ins flipped on, but the wire client doesn't implement the kind being written (e.g. older transport variant). F4-a wired the generic dispatch; F4-b adds typed `WriteParameterAsync` / `WriteMacroAsync` entry points whose default impls return `BadNotSupported` so transports compiled against a stale `IFocasClient` surface still build. - `BadNodeIdUnknown` — full-reference doesn't match any configured `FocasTagDefinition.Name`. - `BadCommunicationError` — wire failure (DLL not loaded, IPC peer dead, etc.). ### CLI bypass `otopcua-focas-cli write` ([`docs/Driver.FOCAS.Cli.md`](../Driver.FOCAS.Cli.md)) sets `Writes.Enabled=true` locally for the lifetime of one invocation because the CLI is a per-operator tool — not a long-lived process bound to the central config DB. The server-side flag is untouched; configure-the- server code paths remain safer-by-default.