281 lines
9.9 KiB
Markdown
281 lines
9.9 KiB
Markdown
# FOCAS simulator (focas-mock) plan
|
|
|
|
Notes on the focas-mock simulator that the FOCAS driver's integration
|
|
tests will eventually talk to. Today there is no FOCAS integration-test
|
|
project; this doc is the contract the future fixture will be built
|
|
against. Keeping the contract tracked in repo means the wire-protocol
|
|
command ids (and their request/response payloads) don't drift between the
|
|
.NET wire client and a future Python implementation.
|
|
|
|
## Ground rules
|
|
|
|
- Append-only command ids. Mirror
|
|
[`focas-wire-protocol.md`](./focas-wire-protocol.md) verbatim.
|
|
- Per-profile state. The simulator hosts N CNC profiles concurrently
|
|
(`Series0i`, `Series30i`, `PowerMotion`, ...). Each profile has its own
|
|
alarm-history ring buffer + its own override map.
|
|
- Admin endpoints under `POST /admin/...` mutate state without going
|
|
through the wire protocol; integration tests use these to seed canned
|
|
inputs.
|
|
|
|
## Protocol surface (current scope)
|
|
|
|
| Cmd | API | State impact |
|
|
| --- | --- | --- |
|
|
| `0x0001` | `cnc_rdcncstat` | reads cached ODBST per profile |
|
|
| `0x0002` | `cnc_rdparam` | reads parameter map per profile |
|
|
| `0x0003` | `cnc_rdmacro` | reads macro variables per profile |
|
|
| `0x0004` | `cnc_rddiag` | reads diagnostic map per profile |
|
|
| `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** |
|
|
| **`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** |
|
|
| **`0x0F1A`** | **`cnc_rdalmhistry`** | **dumps the per-profile alarm-history ring buffer (issue #267, plan PR F3-a)** |
|
|
|
|
## `cnc_rdalmhistry` mock behaviour
|
|
|
|
The simulator keeps a per-profile ring buffer of alarm-history entries.
|
|
Default fixture seeds 5 profiles with 10 canned entries each (per the F3-a
|
|
plan).
|
|
|
|
### Request decode
|
|
|
|
```
|
|
[int16 LE depth]
|
|
```
|
|
|
|
### Response encode
|
|
|
|
Use `FocasAlarmHistoryDecoder.Encode` semantics in reverse: emit the
|
|
count followed by `ALMHIS_data` blocks padded to 4-byte boundaries. The
|
|
.NET-side decoder consumes the same format verbatim, so a Python encoder
|
|
written against the table in
|
|
[`focas-wire-protocol.md`](./focas-wire-protocol.md) interoperates without
|
|
extra glue.
|
|
|
|
### Admin endpoint — `POST /admin/mock_patch_alarmhistory`
|
|
|
|
Replaces the alarm-history ring buffer for a profile.
|
|
|
|
```
|
|
POST /admin/mock_patch_alarmhistory
|
|
{
|
|
"profile": "Series30i",
|
|
"entries": [
|
|
{
|
|
"occurrenceTime": "2025-04-01T09:30:00Z",
|
|
"axisNo": 1,
|
|
"alarmType": 2,
|
|
"alarmNumber": 100,
|
|
"message": "Spindle overload"
|
|
},
|
|
...
|
|
]
|
|
}
|
|
```
|
|
|
|
`entries` order is interpreted as ring-buffer order (most-recent first to
|
|
match FANUC's natural surface).
|
|
|
|
### `FocasSimFixture.SeedAlarmHistoryAsync`
|
|
|
|
The future test-support helper wraps the admin endpoint:
|
|
|
|
```csharp
|
|
await fixture.SeedAlarmHistoryAsync(
|
|
profile: "Series30i",
|
|
entries: new []
|
|
{
|
|
new FocasAlarmHistoryEntry(
|
|
new DateTimeOffset(2025, 4, 1, 9, 30, 0, TimeSpan.Zero),
|
|
AxisNo: 1, AlarmType: 2, AlarmNumber: 100, Message: "Spindle overload"),
|
|
});
|
|
```
|
|
|
|
Integration test `Series/AlarmHistoryProjectionTests.cs` will assert:
|
|
|
|
- historic events fire once with the seeded timestamps
|
|
- second poll yields zero new events (dedup honoured end-to-end)
|
|
- active-alarm raise/clear still works alongside the history poll
|
|
|
|
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.
|
|
|
|
## `pmc_wrpmcrng` mock behaviour — issue #270, plan PR F4-c
|
|
|
|
The simulator keeps a per-profile PMC byte table keyed by `(addr_type,
|
|
byte_address)` — the same map the existing `pmc_rdpmcrng` handler reads
|
|
from. The write handler mutates the same map so a subsequent read sees
|
|
the written bytes.
|
|
|
|
### Per-profile state
|
|
|
|
Each profile carries:
|
|
|
|
```python
|
|
pmc: Dict[int, bytearray] # addr_type -> bytearray (one per PMC letter, default 256 bytes each)
|
|
```
|
|
|
|
`addr_type` is the PMC area code (R=5, G=4, F=3, D=8, X=1, Y=2, K=10,
|
|
A=11, E=12, T=6, C=7); the existing `pmc_rdpmcrng` fixture seeds the
|
|
defaults (zeros + a few canned bits per the dl205-style profile fixtures).
|
|
|
|
### `pmc_wrpmcrng` request decode
|
|
|
|
| Offset | Width | Field |
|
|
| --- | --- | --- |
|
|
| 0 | int16 LE | `addr_type` |
|
|
| 2 | int16 LE | `data_type` (must be `0` = byte; the driver only emits byte writes) |
|
|
| 4 | uint16 LE | `datano_s` |
|
|
| 6 | uint16 LE | `datano_e` |
|
|
| 8 | bytes | `data[]` — `(datano_e - datano_s + 1)` bytes |
|
|
|
|
Handler steps:
|
|
|
|
1. Look up the per-profile bytearray for `addr_type` (allocate on first
|
|
write, default 256 zeros).
|
|
2. **Validate** `0 <= datano_s <= datano_e < len(bytearray)` — otherwise
|
|
return `EW_NUMBER` (`4`).
|
|
3. **Validate** `len(data) == datano_e - datano_s + 1` — otherwise
|
|
return `EW_LENGTH` (`14`).
|
|
4. **Validate** `data_type == 0` — otherwise return `EW_DATA` (`9`)
|
|
because the driver only ever emits byte writes (bit writes wrap with
|
|
driver-side RMW so they reach the simulator as 1-byte writes).
|
|
5. Copy `data[]` into `bytearray[datano_s:datano_e+1]`. Other bytes
|
|
in the array are untouched.
|
|
6. Update `last_write` admin-endpoint state (kind=`pmc`, address-type,
|
|
start byte, length, bytes).
|
|
7. Return `ew_status = 0`.
|
|
|
|
### Round-trip invariant
|
|
|
|
The simulator MUST satisfy:
|
|
|
|
```
|
|
write(R, [10..12], [0xAA, 0xBB, 0xCC]); read(R, [10..12]) == [0xAA, 0xBB, 0xCC]
|
|
```
|
|
|
|
and the **byte-isolation invariant**:
|
|
|
|
```
|
|
write(R, [11], [0xFF]); bytes[10] == prior bytes[10] && bytes[12] == prior bytes[12]
|
|
```
|
|
|
|
The integration tests `Series/PmcRangeWriteTests.cs` and
|
|
`Series/PmcBitRmwIntegrationTests.cs` assert both shapes.
|
|
|
|
### Admin endpoint — `GET /admin/mock_get_last_write` extension
|
|
|
|
The `last_write` payload gains a `kind: "pmc"` variant:
|
|
|
|
```
|
|
{
|
|
"kind": "pmc",
|
|
"addr_type": 5, // R
|
|
"datano_s": 100,
|
|
"datano_e": 100,
|
|
"bytes": "0x08", // hex-encoded
|
|
"writtenAt": "2026-04-25T13:30:00Z"
|
|
}
|
|
```
|
|
|
|
Bit-level writes never appear here as a separate kind — they reach the
|
|
simulator as 1-byte writes after the driver's RMW wrapper, so the audit
|
|
shape is identical to a byte write at the same address.
|
|
|
|
### Status
|
|
|
|
focas-mock simulator has not landed yet (tracked separately from F4-b /
|
|
F4-c). F4-b + F4-c land the .NET-side wire encoders + dispatch + status
|
|
mapping unconditionally; the integration-test scaffolds at
|
|
`tests/.../IntegrationTests/Series/ParameterWriteTests.cs`,
|
|
`MacroWriteTests.cs`, `PmcRangeWriteTests.cs`, and
|
|
`PmcBitRmwIntegrationTests.cs` are deferred until the simulator +
|
|
integration-test project land. Until then unit-test coverage in
|
|
`FocasWriteParameterTests` / `FocasWriteMacroTests` /
|
|
`FocasWritePmcTests` exercises every same-process invariant against the
|
|
in-memory `FakeFocasClient`.
|