@@ -32,6 +32,7 @@ command ids (and their request/response payloads) don't drift between the
|
||||
| **`0x0102`** | **`cnc_wrparam`** | **mutates per-profile parameter map; returns `EW_PASSWD` (`11`) when the profile's `unlock_state` is off (sets up F4-d's unlock workflow) — issue #269, plan PR F4-b** |
|
||||
| **`0x0103`** | **`cnc_wrmacro`** | **mutates per-profile macro map; integer-only writes for now (decimalPointCount=0) — issue #269, plan PR F4-b** |
|
||||
| **`0x0104`** | **`pmc_wrpmcrng`** | **mutates per-profile PMC byte tables; byte-aligned writes preserve untouched bytes; bit-level writes never reach the simulator (driver wraps with RMW) — issue #270, plan PR F4-c** |
|
||||
| **`0x0105`** | **`cnc_wrunlockparam`** | **flips the per-profile `unlock_state` to true when the supplied 4-byte password buffer matches the profile's `unlock_password`; otherwise returns `EW_PASSWD`. State persists for the connection lifetime (per-session). — issue #271, plan PR F4-d** |
|
||||
| **`0x0F1A`** | **`cnc_rdalmhistry`** | **dumps the per-profile alarm-history ring buffer (issue #267, plan PR F3-a)** |
|
||||
|
||||
## `cnc_rdalmhistry` mock behaviour
|
||||
@@ -124,6 +125,12 @@ Each profile owns:
|
||||
- `unlock_state: bool` — defaults `False`. When `False`, every
|
||||
`cnc_wrparam` returns `EW_PASSWD` (numeric `11`) regardless of
|
||||
parameter. Macro writes are NOT gated by `unlock_state`.
|
||||
- `unlock_password: bytes` (4-byte buffer) — defaults to the profile's
|
||||
fixture default (e.g. `b"1234"` for Series30i). Compared byte-for-byte
|
||||
by the `cnc_wrunlockparam` handler; flips `unlock_state = True` on
|
||||
match, leaves it untouched on mismatch (and returns `EW_PASSWD`).
|
||||
Mutable via `POST /admin/mock_set_password` for tests that exercise
|
||||
rotation. Issue #271, plan PR F4-d.
|
||||
- `last_write: Optional[LastWrite]` — most-recent successful
|
||||
`(kind, number, value, ts)` tuple, surfaced via the admin endpoint
|
||||
below for audit-log assertions.
|
||||
@@ -163,6 +170,37 @@ POST /admin/mock_set_unlock_state
|
||||
{ "profile": "Series30i", "unlocked": true }
|
||||
```
|
||||
|
||||
### `cnc_wrunlockparam` request decode — issue #271, plan PR F4-d
|
||||
|
||||
```
|
||||
[byte[4] password]
|
||||
```
|
||||
|
||||
Match `password == profile.unlock_password` byte-for-byte. On match:
|
||||
flip `unlock_state = True`, return `[int16 LE 0]`. On mismatch: leave
|
||||
`unlock_state` untouched, return `[int16 LE 11]` (`EW_PASSWD`).
|
||||
|
||||
The simulator deliberately keeps unlock state per-session (per OpenSession
|
||||
handle) so a reconnect drops back to `unlock_state = False` — matching the
|
||||
FWLIB lifetime semantics described in
|
||||
[`focas-wire-protocol.md`](./focas-wire-protocol.md) § "cnc_wrunlockparam".
|
||||
|
||||
### Admin endpoint — `POST /admin/mock_set_password`
|
||||
|
||||
Rotates the per-profile `unlock_password` for tests that exercise the
|
||||
F4-d password-rotation runbook (`docs/v2/focas-deployment.md`
|
||||
§ "FOCAS password handling"). Idempotent — call again to revert.
|
||||
|
||||
```
|
||||
POST /admin/mock_set_password
|
||||
{ "profile": "Series30i", "password": "5678" }
|
||||
```
|
||||
|
||||
The endpoint accepts the password as a UTF-8/ASCII string and applies
|
||||
the same right-pad-to-4-bytes / truncate-to-4-bytes normalisation the
|
||||
driver does, so simulator-side matching is byte-symmetric with the
|
||||
production wire encoder.
|
||||
|
||||
### Admin endpoint — `GET /admin/mock_get_last_write`
|
||||
|
||||
Returns the simulator's view of the most-recent successful write, used by
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user