Files
lmxopcua/docs/v2/implementation/focas-simulator-plan.md
2026-04-26 04:54:28 -04:00

7.0 KiB

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 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
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 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:

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. 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_PASSWDBadUserAccessDenied 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.