Auto: focas-f4d — password / unlock parameter

Closes #271
This commit is contained in:
Joseph Doherty
2026-04-26 05:45:13 -04:00
parent d676b4056d
commit 86f3fc2733
16 changed files with 1016 additions and 40 deletions

View File

@@ -22,6 +22,7 @@ Each FOCAS-equivalent call gets a stable wire-protocol command id. Ids are
| **`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)** |
| **`0x0105`** | **`cnc_wrunlockparam`** | **4-byte password buffer for the parameter-protect / read-protect unlock (issue #271, plan PR F4-d)** |
| `0x0F1A` | **`cnc_rdalmhistry`** | **ODBALMHIS alarm-history ring-buffer dump (issue #267, plan PR F3-a)** |
## ODBALMHIS — alarm history (`cnc_rdalmhistry`, command `0x0F1A`)
@@ -214,3 +215,53 @@ 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.
## cnc_wrunlockparam — connection-level password unlock (command `0x0105`)
Issue #271, plan PR F4-d. Some controllers (notably 16i + certain 30i
firmwares with parameter-protect on) gate `cnc_wrparam` and selected
reads behind a connection-level password switch. The driver emits this
frame on connect when `FocasDeviceOptions.Password` is configured, and
re-emits it on any read/write that returns `EW_PASSWD` (then retries the
gated call once).
### Request
| Offset | Width | Field |
| --- | --- | --- |
| 0 | `byte[4]` | `password[4]` — 4-byte password buffer. ASCII-encoded from `FocasDeviceOptions.Password`, right-padded with `0x00`, truncated at 4 bytes. |
The 4-byte fixed slot matches the FANUC published shape — the controller
compares byte-for-byte. Longer / shorter source strings are normalised at
the driver layer before they hit this frame so the wire surface stays
canonical.
### Response
Same single-int16 envelope as the write frames:
| Offset | Width | Field |
| --- | --- | --- |
| 0 | `int16 LE` | `ew_status``0` = success (gate now lifted for the lifetime of this FWLIB handle), `EW_PASSWD` = supplied password did not match the controller's slot, `EW_HANDLE` = handle invalid. |
### Lifetime
Unlock is bound to the FWLIB handle: it persists until the handle closes
(disconnect / reconnect). The driver reinvokes unlock on every
`EnsureConnectedAsync` reconnect path so a planned or unplanned wire
restart self-heals without operator intervention. A `BadUserAccessDenied`
on a read/write triggers a single-shot retry: re-emit unlock + redispatch
the gated call once. A second `EW_PASSWD` propagates unchanged so a
mismatched password doesn't loop forever on the wire.
### No-log invariant
The password is a secret. Wire-client implementations MUST NOT log the
password on either request or response. The current
`FwlibFocasClient.UnlockAsync` constructs an exception that includes
only the `EW_*` return code; the `FocasDeviceOptions` record overrides
its auto-generated `ToString` so any Serilog destructure renders
`Password = ***`. See
[`docs/v2/focas-deployment.md`](../focas-deployment.md)
§ "FOCAS password handling" for the deployment-side guarantees +
rotation runbook.