Auto: focas-f4c — pmc_wrpmcrng with bit-level RMW

Closes #270
This commit is contained in:
Joseph Doherty
2026-04-26 05:15:52 -04:00
parent 0c967af645
commit 54c09d4d5d
17 changed files with 837 additions and 101 deletions

View File

@@ -54,7 +54,7 @@ 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 (F4-a) + #269 (F4-b)
## 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
@@ -66,22 +66,54 @@ choice. Decision record: [`docs/v2/decisions.md`](../v2/decisions.md) →
| `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. |
### Config shape — F4-b
> **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
"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 }
"Writable": true, "WriteIdempotent": false },
{ "Name": "StartFlag", "Address": "R100.3", "DataType": "Bit",
"Writable": true, "WriteIdempotent": true }
]
}
```
@@ -119,9 +151,9 @@ value that the operator simply wants forced to a target).
- `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.
(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