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

@@ -29,6 +29,8 @@ command ids (and their request/response payloads) don't drift between the
| `0x0010` | `pmc_rdpmcrng` | reads PMC byte ranges |
| `0x0020` | `cnc_modal` | reads cached modal MSTB per profile |
| ... | ... | ... |
| **`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** |
| **`0x0F1A`** | **`cnc_rdalmhistry`** | **dumps the per-profile alarm-history ring buffer (issue #267, plan PR F3-a)** |
## `cnc_rdalmhistry` mock behaviour
@@ -100,3 +102,94 @@ Integration test `Series/AlarmHistoryProjectionTests.cs` will assert:
These tests are blocked on the focas-mock + integration-test project
landing; the unit-test coverage in `FocasAlarmProjectionTests` already
exercises every same-process invariant.
## `cnc_wrparam` / `cnc_wrmacro` mock behaviour — issue #269, plan PR F4-b
When the focas-mock fixture lands, it MUST implement the contract below.
The .NET side already ships against this contract (`FwlibFocasClient.cs`
write helpers, `FakeFocasClient` round-trip support); writing the simulator
to the same shape lets the existing integration-test scaffolds at
`tests/.../IntegrationTests/Series/ParameterWriteTests.cs` and
`MacroWriteTests.cs` (when they materialise) light up without driver
changes.
### Per-profile state
Each profile owns:
- `parameters: Dict[int, int]` — map from parameter number to current value.
- `macros: Dict[int, int]` — map from macro number to current scaled-int
value (decimal-point count fixed at 0 for F4-b).
- `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`.
- `last_write: Optional[LastWrite]` — most-recent successful
`(kind, number, value, ts)` tuple, surfaced via the admin endpoint
below for audit-log assertions.
### `cnc_wrparam` request decode
```
[int16 LE datano][int16 LE axis][int8|int16|int32 LE value]
```
Width of the value field is determined by the request frame trailer
length per the table in
[`focas-wire-protocol.md`](./focas-wire-protocol.md). On
`unlock_state == False` short-circuit to `[int16 LE 11]` (`EW_PASSWD`).
Otherwise mutate `parameters[datano] = value`, set `last_write`, return
`[int16 LE 0]`.
### `cnc_wrmacro` request decode
```
[int16 LE number][int16 LE length=8][int32 LE mcr_val][int16 LE dec_val]
```
Always accept (no `unlock_state` gate). Mutate
`macros[number] = mcr_val` (we ignore `dec_val` for F4-b — integer-only).
Return `[int16 LE 0]`. Round-trip: a subsequent `cnc_rdmacro(number)`
returns `(mcr_val, 0)`.
### Admin endpoint — `POST /admin/mock_set_unlock_state`
Toggles `unlock_state` for the F4-d unlock workflow tests. Without this,
F4-b parameter-write integration tests can't reproduce the
`EW_PASSWD``BadUserAccessDenied` mapping.
```
POST /admin/mock_set_unlock_state
{ "profile": "Series30i", "unlocked": true }
```
### Admin endpoint — `GET /admin/mock_get_last_write`
Returns the simulator's view of the most-recent successful write, used by
F4-b audit-log integration assertions ("did the write actually reach the
fixture, and is the audit log capturing the right kind/number/value?").
```
GET /admin/mock_get_last_write?profile=Series30i
->
{
"kind": "param", // "param" | "macro"
"number": 1815,
"value": 100,
"writtenAt": "2026-04-25T13:30:00Z"
}
```
When no write has happened the endpoint returns `null` rather than 404 so
the test helper can assert "no writes since fixture reset" without
exception handling.
### Status
focas-mock simulator has not landed yet (tracked separately from F4-b).
F4-b lands the .NET-side wire encoders + dispatch + status mapping
unconditionally; the integration-test scaffolds at
`tests/.../IntegrationTests/Series/ParameterWriteTests.cs` and
`MacroWriteTests.cs` are deferred until the simulator + integration-test
project land. Until then unit-test coverage in
`FocasWriteParameterTests` / `FocasWriteMacroTests` exercises every
same-process invariant against the in-memory `FakeFocasClient`.

View File

@@ -19,6 +19,8 @@ Each FOCAS-equivalent call gets a stable wire-protocol command id. Ids are
| `0x0003` | `cnc_rdmacro` | macro variable value |
| `0x0004` | `cnc_rddiag` | diagnostic value |
| ... | ... | ... |
| **`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)** |
| `0x0F1A` | **`cnc_rdalmhistry`** | **ODBALMHIS alarm-history ring-buffer dump (issue #267, plan PR F3-a)** |
## ODBALMHIS — alarm history (`cnc_rdalmhistry`, command `0x0F1A`)
@@ -74,3 +76,81 @@ DST transitions. The .NET decoder
unstable anyway.
- `msg_len` overrunning the payload truncates the entry list at the
malformed entry rather than throwing.
## IODBPSD — parameter write (`cnc_wrparam`, command `0x0102`)
Issue #269, plan PR F4-b. The write-side payload is the **byte-symmetric
inverse of the `cnc_rdparam` read** — the same `IODBPSD` struct shape, and
the .NET wire client uses the read-side decoder reversed (`EncodeParamValue`
in `FwlibFocasClient.cs`) so the encoder/decoder are guaranteed to stay in
lock-step.
### Request
| Offset | Width | Field |
| --- | --- | --- |
| 0 | `int16 LE` | `datano` — parameter number (e.g. `1815`) |
| 2 | `int16 LE` | `type` — axis index (1-based; `0` = whole-CNC parameter) |
| 4 | `length` | `data` payload — width depends on parameter type |
`length` (request frame trailer, drives `data` width):
| FocasDataType | `length` | Payload encoding |
| --- | --- | --- |
| `Byte` | `4 + 1` | one signed byte at offset 4 |
| `Int16` | `4 + 2` | int16 LE at offset 4 |
| `Int32` | `4 + 4` | int32 LE at offset 4 |
Bit-addressed parameters (`PARAM:1815/0` form) are not supported by F4-b
and surface as `BadNotSupported`; F4-c will land the read-modify-write
helper alongside the PMC bit RMW path.
### Response
Single `int16 LE` return code per the standard FWLIB convention:
- `0``Good`
- `11` (`EW_PASSWD`) → **`BadUserAccessDenied`** (was `BadNotWritable`
pre-F4-b — see `FocasStatusMapper`). Means the parameter-write switch is
off or the CNC isn't in MDI mode; the F4-d unlock workflow will close
the loop on this from the OPC UA side.
- Other `EW_*` codes map per
[`FocasStatusMapper.MapFocasReturn`](../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasStatusMapper.cs).
## ODBM — macro write (`cnc_wrmacro`, command `0x0103`)
Issue #269, plan PR F4-b. The write-side payload mirrors the
`cnc_rdmacro` read shape: the same `(mcr_val, dec_val)` (integer +
decimal-point count) split, but emitted from the .NET side rather than
decoded.
### Request
| Offset | Width | Field |
| --- | --- | --- |
| 0 | `int16 LE` | `number` — macro variable number (e.g. `500`) |
| 2 | `int16 LE` | `length` — fixed at `8` for ODBM |
| 4 | `int32 LE` | `mcr_val` — scaled integer value |
| 8 | `int16 LE` | `dec_val` — decimal-point count |
F4-b ships **integer-only writes** (`dec_val = 0`) to match the most
common HMI pattern; a future `WriteMacroScaled` overload will land if the
field calls for fractional macro setpoints. Read-side decoders apply
`mcr_val / 10^dec_val`, so a `dec_val = 0` write surfaces back as the
integer it was emitted as.
### Response
Same single-int16 envelope as `cnc_wrparam`. `EW_PASSWD` is rare on macro
writes (the gate-switch protection is parameter-specific) but the mapper
treats both kinds identically.
### Symmetry note
The plan carries a "byte layout symmetry" requirement — the encoder for
each kind is the read-side decoder reversed. Adding a new parameter type
(e.g. `Int64` parameters, when they ship) means extending both sides in
the same PR; the unit test
`FocasWriteParameterTests.ParameterWrite_round_trip_stores_value_visible_to_subsequent_read`
exercises encode → store → decode with the fake wire client and is the
canary for symmetry regressions.