Auto: focas-f4b — cnc_wrmacro + cnc_wrparam writes

Closes #269
This commit is contained in:
Joseph Doherty
2026-04-26 04:54:28 -04:00
parent 71af554497
commit f48f31cfc7
15 changed files with 1066 additions and 36 deletions

View File

@@ -54,9 +54,9 @@ giant request. Typical FANUC ring buffers cap at ~100 entries; the default
surfacing the FWLIB struct fields directly into
`FocasAlarmHistoryEntry`.
## Writes (opt-in, off by default) — issue #268, plan PR F4-a
## Writes (opt-in, off by default) — issue #268 (F4-a) + #269 (F4-b)
Writes ship behind two independent opt-ins. Both default off so a freshly
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".
@@ -64,20 +64,49 @@ choice. Decision record: [`docs/v2/decisions.md`](../v2/decisions.md) →
| 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. |
| `FocasTagDefinition.Writable` *(per-tag opt-in)* | `false` | The per-tag check returns `BadNotWritable` for that tag even when the driver-level flag is on. |
| **`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.** |
| `FocasTagDefinition.Writable` *(per-tag opt-in)* | `false` | The per-tag check returns `BadNotWritable` for that tag even when the driver-level flags are on. |
### Config shape
### Config shape — F4-b
```jsonc
{
"Writes": { "Enabled": true },
"Writes": {
"Enabled": true,
"AllowParameter": true, // F4-b — opt into cnc_wrparam
"AllowMacro": true // F4-b — opt into cnc_wrmacro
},
"Tags": [
{ "Name": "RPM", "Address": "PARAM:1815", "DataType": "Int32",
"Writable": true, "WriteIdempotent": false },
{ "Name": "Recipe", "Address": "MACRO:500", "DataType": "Int32",
"Writable": true, "WriteIdempotent": false }
]
}
```
### 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
@@ -86,14 +115,23 @@ 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-a
### Status-code semantics post-F4-b
- `BadNotWritable` — driver-level `Writes.Enabled = false`, OR per-tag
`Writable = false`. Two distinct paths, same status code.
- `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)**. Same
status code, four 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
yet implement the kind being written. F4-a wires the dispatch surface;
F4-b/c land the actual macro / parameter / PMC writes for unimplemented
kinds, replacing those `BadNotSupported` responses with real wire calls.
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,