Captures uncommitted work that lived in the working tree on
v2-mxgw-integration but was orthogonal to the migration. Stashed
during the v2-mxgw merge to master (2026-04-30) and replanted here on
a feature branch off master so it's git-visible rather than living in
the stash list.
Two distinct buckets:
1. Tracked fixture/config refinements (10 files, ~36 lines):
- scripts/e2e/test-opcuaclient.ps1
- src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json
- 5 docker-compose.yml under tests/.../IntegrationTests/Docker/
(AbCip, Modbus, OpcUaClient, S7)
- 4 fixture .cs files (AbServerFixture, ModbusSimulatorFixture,
OpcPlcFixture, Snap7ServerFixture)
2. Untracked driver-gaps queue artifacts (~8000 lines):
- docs/plans/{abcip,ablegacy,focas,opcuaclient,s7,twincat}-plan.md
— per-driver gap plans
- docs/featuregaps.md — cross-cutting analysis
- docs/v2/focas-deployment.md, docs/v2/implementation/focas-simulator-plan.md
- followup.md — auto/driver-gaps queue follow-ups
- scripts/queue/ — PR-queue automation tooling (12 files including
pr-manifest.yaml at 1473 lines)
This commit is a snapshot for recoverability — review and split into
focused PRs (or discard) before merging anywhere downstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
808 lines
44 KiB
Markdown
808 lines
44 KiB
Markdown
# FOCAS Driver — Implementation Plan
|
||
|
||
> Source of gap analysis: [featuregaps.md → FOCAS](../featuregaps.md#focas-fanuc-cnc)
|
||
>
|
||
> Covers Build = Yes items only.
|
||
|
||
## Summary
|
||
|
||
The FOCAS driver today is a pure-managed, read-only FOCAS/2 wire client
|
||
(`src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/`) backing a fixed-tree projection
|
||
plus user-authored `PARAM:` / `MACRO:` / PMC tags. It exposes a thin set of
|
||
calls (`cnc_sysinfo`, `cnc_rdcncstat`, `cnc_rdaxisname`, `cnc_rdspdlname`,
|
||
`cnc_rddynamic2`, `cnc_rdsvmeter`, `cnc_rdspload`, `cnc_rdspmaxrpm`,
|
||
`cnc_exeprgname2`, `cnc_rdblkcount`, `cnc_rdopmode`, `cnc_rdtimer`,
|
||
`cnc_rdparam`, `cnc_rdmacro`, `pmc_rdpmcrng`, `cnc_rdalmmsg2`).
|
||
|
||
The featuregaps table marks **18** items as Build = Yes. They cluster into
|
||
five distinct workstreams:
|
||
|
||
1. **Phase 1 — fixed-tree expansion** (#6, #7, #8, #10, #11, #12, #13, #14,
|
||
#18, #20, #24, #27). These are mostly new wire calls plumbed into the
|
||
existing `FixedTree*` poll cadences; no architectural change.
|
||
2. **Phase 2 — addressing additions** (#4, #14 DIAG scheme, #15, #16). New
|
||
`FocasAreaKind` values, new capability-matrix entries, multi-path
|
||
`PathId`. Touches the parser + matrix + wire envelope; mostly additive.
|
||
3. **Phase 3 — alarm history** (#17). Extends the existing
|
||
`FocasAlarmProjection` with a one-shot history pull on connect plus
|
||
periodic delta polls.
|
||
4. **Phase 4 — write path** (#1, #3). The biggest behavioural change in
|
||
the driver's lifetime: removes the `BadNotWritable` short-circuit, adds
|
||
`cnc_wrparam` / `pmc_wrpmcrng` / `cnc_wrmacro` plus FOCAS password
|
||
handling. Material risk surface — see Risks.
|
||
5. **Phase 5 — derived telemetry** (#24 cycle-delta computation). Optional
|
||
companion to #24 raw cycle time; computes "last completed cycle" from
|
||
the existing cumulative `Cycle` timer.
|
||
|
||
DIAG (#14) is in Phase 2 (addressing) rather than Phase 1 because it
|
||
needs a new address scheme, but the fixed-tree status flag projection
|
||
(#12) is the cheapest item and should land first as a vertical slice.
|
||
|
||
The remaining 9 items in the featuregaps table (HSSB, Series 15 / 35i,
|
||
tool-offset write, program upload/download, DPRNT, deep servo info,
|
||
acceleration/jerk, operator preset commands, NTP) are scoped out as
|
||
Build = No; they appear in [Skip-rated items](#skip-rated-items-for-context)
|
||
for context only.
|
||
|
||
## Phased delivery
|
||
|
||
| Phase | Scope | Gaps closed | Approx PRs | Risk |
|
||
|-------|-------|-------------|------------|------|
|
||
| 1 | Fixed-tree expansion (read-only) | 12, 13, 7, 8, 10, 11, 20, 18, 6, 24, 27, 14 (read-only piece) | 6 | Low |
|
||
| 2 | Addressing additions | 4, 15, 16, 14 (DIAG: scheme) | 4 | Medium (multi-path) |
|
||
| 3 | Alarm history | 17 | 1 | Low |
|
||
| 4 | Write path + password | 1, 3 | 4 | High (read-only design choice removed) |
|
||
| 5 | Cycle-delta derived telemetry | 24 (delta companion) | 1 | Low |
|
||
|
||
Phases 1–3 are mutually independent and can ship in any order. Phase 4
|
||
deliberately follows Phase 2 so writes ride on top of the multi-path
|
||
addressing already in place. Phase 5 tags onto the cycle-time node from
|
||
Phase 1.
|
||
|
||
## Per-PR detail
|
||
|
||
### Phase 1 — fixed-tree expansion
|
||
|
||
Common shape: each PR adds one or more wire calls in
|
||
`Wire/FocasWireClient.cs`, surfaces them on `IFocasClient`, plumbs them
|
||
into `FocasDriver`'s `FixedTreeLoopAsync` cadences (axis 250 ms / program
|
||
1 s / timer 30 s) and the `TryReadFixedTree` synthesizer, then adds
|
||
fakes + assertions.
|
||
|
||
**PR F1-a — ODBST status flags as fixed-tree nodes (#12)**
|
||
- Scope: project the 9 fields of `cnc_rdcncstat` (`tmmode`, `aut`, `run`,
|
||
`motion`, `mstb`, `emergency`, `alarm`, `edit`, `dummy`) under
|
||
`Status/` per device. We already issue this call in `ProbeAsync`; this
|
||
PR keeps the boolean probe but additionally caches the full struct on
|
||
every poll tick.
|
||
- Files:
|
||
`Wire/FocasWireClient.cs` (extend `ReadStatusAsync` to return the
|
||
whole `WireStatus` rather than only `IsOk`), `IFocasClient.cs` (new
|
||
`GetStatusAsync`), `FocasDriver.cs` (new `Status/*` branch in
|
||
`TryReadFixedTree`, status cache on `DeviceState`).
|
||
- Tests:
|
||
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasFixedTreeStatusTests.cs`
|
||
(new) — `FakeFocasClient` returns canned ODBST, assert each field maps
|
||
to the expected `Status/*` browse name. Integration: extend
|
||
`FocasSimFixture` to seed the simulator's status response and assert
|
||
via the OPC UA client.
|
||
- **Docs / fixture / e2e**: extend `docs/drivers/FOCAS.md` fixed-tree
|
||
table with the 9 `Status/*` nodes; mention the boolean-probe →
|
||
full-struct change in `docs/drivers/FOCAS-Test-Fixture.md` integration
|
||
bullet list; teach `focas-mock` (under
|
||
`tests/.../IntegrationTests/Docker/focas-mock/`) the `cnc_rdcncstat`
|
||
payload shape per `docs/v2/implementation/focas-wire-protocol.md`
|
||
(add ODBST struct entry); extend `FocasSimFixture` with a helper to
|
||
patch the canned status payload; new
|
||
`Series/StatusFlagsPopulateTests.cs` integration test.
|
||
- Effort: small; one wire call already exists.
|
||
- Risk: Low.
|
||
|
||
**PR F1-b — parts count + cycle time (#13, #24 raw)**
|
||
- Scope: surface `cnc_rdparam(6711)` (parts produced), `6712` (parts
|
||
required), `6713` (parts total since power-on) under `Production/`,
|
||
plus `Production/CycleTimeSeconds` (already exposed as
|
||
`Timers/CycleSeconds` — promote to the `Production/` group too with
|
||
the same backing). The existing `cnc_rdtimer` call is sufficient.
|
||
- Files: `FocasDriver.cs` (`Production/*` branch, parameter-cached
|
||
reads on the timer poll cadence), `IFocasClient.cs` (no new call —
|
||
rides on `ReadParameterInt32Async`).
|
||
- Tests: `FakeFocasClient` returns canned parameter values; assert
|
||
`Production/PartsTotal` equals the canned value.
|
||
- **Docs / fixture / e2e**: add `Production/*` rows to the fixed-tree
|
||
table in `docs/drivers/FOCAS.md`; add `Production:` example to
|
||
`docs/Driver.FOCAS.Cli.md` (a `read -a PARAM:6711` snippet); the
|
||
parts-count parameters (6711/6712/6713) are already in the
|
||
simulator profile range, so only the `dl205`-style profile JSON
|
||
under `tests/.../Docker/focas-mock/profiles/` needs seeded values
|
||
added; extend `FocasSimFixture` with a `SeedPartsCount` helper;
|
||
integration test under `Series/ProductionPopulatesTests.cs`.
|
||
- Effort: small.
|
||
- Risk: Low.
|
||
|
||
**PR F1-c — modal G/M/T codes (#7) + override values (#11)**
|
||
- Scope: add `cnc_modal` (command id TBD per `fwlib32.h` — the wire
|
||
protocol uses the same numeric command convention seen in
|
||
`FocasWireClient`; capture during simulator iteration). Project:
|
||
`Modal/G_Group{n}` (groups 1..21), `Modal/MCode`, `Modal/SCode`,
|
||
`Modal/TCode`, `Modal/BCode`. Adds `Override/Feed`, `Override/Rapid`,
|
||
`Override/Spindle`, `Override/Jog` from `cnc_rdparam(...)` — the
|
||
override percent registers live at known parameter numbers; numbers
|
||
are MTB-specific so pull defaults from
|
||
`docs/v2/focas-version-matrix.md` and let operators override per device.
|
||
- Files: `Wire/FocasWireClient.cs` (new `ReadModalAsync`), new
|
||
`Wire/FocasWireModels.cs` records `WireModal` / `WireModalGroup`,
|
||
`IFocasClient.cs` (new `GetModalAsync`), `FocasDriver.cs` (new
|
||
poll-medium branches under the program-poll cadence).
|
||
- Tests: `FocasModalTests.cs` (unit), simulator handler returns canned
|
||
modal payload, integration asserts `Modal/G_Group1` text.
|
||
- **Docs / fixture / e2e**: add `Modal/*` and `Override/*` sections to
|
||
the fixed-tree table in `docs/drivers/FOCAS.md`, including the
|
||
G-group decode table for groups 01/03/06/07/14; add a `MODAL:`
|
||
address example row to `docs/Driver.FOCAS.Cli.md` (new `read -a
|
||
MODAL:G1` style — note: this PR does NOT add a new address scheme,
|
||
the modal data is fixed-tree only, so the CLI example reads via
|
||
`read -n "ns=2;s=Modal/G_Group1"` over the OPC UA endpoint);
|
||
document MTB-specific override register defaults in
|
||
`docs/v2/focas-version-matrix.md` (new `Override registers per
|
||
series` table); capture the `cnc_modal` command id resolved during
|
||
simulator iteration into `docs/v2/implementation/focas-wire-protocol.md`
|
||
(new struct entry — promote out of the open-questions list);
|
||
update `docs/v2/implementation/focas-simulator-plan.md` Stream C
|
||
protocol-surface table with the new `cnc_modal` handler;
|
||
extend focas-mock with a `cnc_modal` command-id handler + canned
|
||
modal payload per profile; integration test reading G54/G90 modal
|
||
state via `Series/ModalPopulatesTests.cs`.
|
||
- Effort: medium — `cnc_modal` returns a multi-group struct; encoding
|
||
needs care.
|
||
- Risk: Medium — modal-group numbering varies by series; treat the
|
||
raw integer as the value the CNC reports and surface a string
|
||
decode table only for the universally-present groups (G-group 01
|
||
motion, 03 absolute/incremental, 06 input units, 07 cutter comp,
|
||
14 work coordinate). Document MTB-specific groups as raw int.
|
||
|
||
**PR F1-d — tool number / tool life (#8) + work coordinate offsets (#10)**
|
||
- Scope: add `cnc_rdtofs` / `cnc_rdtlife*` / `cnc_rdzofs`. Project
|
||
`Tooling/CurrentTool`, `Tooling/CurrentOffset`,
|
||
`Tooling/Life/{group}/Remaining`, `Tooling/Life/{group}/Total`,
|
||
`Offsets/G54..G59[+ extended]/{X,Y,Z}`.
|
||
- Files: new wire calls in `Wire/FocasWireClient.cs` (`ReadToolOffsetAsync`,
|
||
`ReadToolLifeAsync`, `ReadWorkOffsetAsync`), `Wire/FocasWireModels.cs`
|
||
(records), `IFocasClient.cs`, `FocasDriver.cs` (new `Tooling/` and
|
||
`Offsets/` branches; both poll on the slow timer cadence — these
|
||
change at setup time, not per-cycle), capability matrix per-call
|
||
suppression like the existing `Spindle/` gating.
|
||
- Tests: unit + simulator. Tool-life is the largest payload; assert
|
||
array projection rather than per-tool nodes (one ValueRank=1 array
|
||
per group keeps the address-space size bounded on machines with
|
||
500+ tool slots).
|
||
- **Docs / fixture / e2e**: add `Tooling/*` and `Offsets/*` sections to
|
||
the fixed-tree table in `docs/drivers/FOCAS.md`, including the
|
||
ValueRank=1 array note for tool-life groups; add a per-series
|
||
capability-suppression row to `docs/v2/focas-version-matrix.md`
|
||
(which series support `cnc_rdtlife*` vs not); document the three
|
||
new structs (`ODBTOFS`, `ODBTLIFE5`, `IODBZOR`) in
|
||
`docs/v2/implementation/focas-wire-protocol.md`; add
|
||
`cnc_rdtofs` / `cnc_rdtlife*` / `cnc_rdzofs` rows to the protocol
|
||
surface table in `docs/v2/implementation/focas-simulator-plan.md`;
|
||
extend focas-mock with three new command-id handlers + per-profile
|
||
seed data (tool table + work-offset table); add a
|
||
`tools_per_series` matrix to the `focas-mock` per-series profile
|
||
JSON so 0i-D's small tool table differs from 30i's; new
|
||
`Series/ToolingPopulatesTests.cs` and `Series/OffsetsPopulatesTests.cs`
|
||
integration tests; update `docs/drivers/FOCAS-Test-Fixture.md`
|
||
coverage map with the three new wire calls.
|
||
- Effort: large — three new calls, each with its own struct; tool-life
|
||
is variable-length.
|
||
- Risk: Medium — payload shapes are series-specific; keep the
|
||
capability matrix as the authoritative gate.
|
||
|
||
**PR F1-e — operator messages (#18) + currently-executing block text (#20)**
|
||
- Scope: `cnc_rdopmsg3` (gives all four FANUC opmsg classes in one
|
||
call), `cnc_rdactpt` (current block text). Project `Messages/External`
|
||
(variable, last-N strings), `Program/CurrentBlock` (single string).
|
||
- Files: `Wire/FocasWireClient.cs` (`ReadOperatorMessagesAsync`,
|
||
`ReadCurrentBlockAsync`), `IFocasClient.cs`, `FocasDriver.cs` (new
|
||
branches under program-poll cadence).
|
||
- Tests: simulator returns canned ASCII; assert string round-trip is
|
||
trim-stable (FANUC right-pads with `\0` or space).
|
||
- **Docs / fixture / e2e**: add `Messages/External` and
|
||
`Program/CurrentBlock` rows to the fixed-tree table in
|
||
`docs/drivers/FOCAS.md`, including the ring-buffer / last-N
|
||
semantics for opmsg; document the `OPMSG3` and `ODBACT2`
|
||
payload shapes in `docs/v2/implementation/focas-wire-protocol.md`;
|
||
add `cnc_rdopmsg3` / `cnc_rdactpt` rows to the protocol surface
|
||
table in `docs/v2/implementation/focas-simulator-plan.md`; extend
|
||
focas-mock with the two new command-id handlers (per-profile
|
||
canned message text + canned current-block text); add a
|
||
`mock_patch_opmsg` admin endpoint hook on `FocasSimFixture` for
|
||
tests that need to push a canned message; integration test
|
||
`Series/OperatorMessagesPopulateTests.cs` asserts trim-stable
|
||
round-trip and last-N retention.
|
||
- Effort: medium.
|
||
- Risk: Low — ASCII-only payloads.
|
||
|
||
**PR F1-f — `cnc_getfigure` decimal scaling (#6) + connection statistics (#27)**
|
||
- Scope: `cnc_getfigure` returns per-axis decimal-place counts; cache
|
||
the result at bootstrap and divide each `AbsolutePosition` /
|
||
`MachinePosition` / `RelativePosition` / `DistanceToGo` /
|
||
`ActualFeedRate` value before publishing. Existing nodes already
|
||
carry `Float64`; the change is invisible to clients except that
|
||
values become real-world units. Adds `Diagnostics/` subtree:
|
||
`Diagnostics/ReadCount`, `Diagnostics/ReadFailureCount`,
|
||
`Diagnostics/LastErrorMessage`, `Diagnostics/LastSuccessfulRead`,
|
||
`Diagnostics/ReconnectCount` — driven by counters already maintained
|
||
on `DeviceState`.
|
||
- Files: `Wire/FocasWireClient.cs` (new `ReadFigureAsync`),
|
||
`IFocasClient.cs`, `FocasDriver.cs` (cache decimal places per axis,
|
||
multiply on the read path, expose counters under `Diagnostics/`).
|
||
- Tests: assert that with a canned `cnc_getfigure` returning 3, an
|
||
`AbsolutePosition` of 12345 becomes `12.345`. Connection-stat tests
|
||
assert counters increment under known conditions.
|
||
- **Docs / fixture / e2e**: significant `docs/drivers/FOCAS.md` change —
|
||
add a "Decimal-place scaling" subsection explaining the
|
||
`FixedTree.ApplyFigureScaling` flag (default true on new installs,
|
||
false on migrations) and the unit-correctness semantics it enforces;
|
||
add `Diagnostics/*` rows to the fixed-tree table; add a
|
||
Diagnostics-counters subsection to `docs/v2/focas-deployment.md`
|
||
for operator dashboards; document `cnc_getfigure` (`ODBAXDP` /
|
||
`ODBAXIS`) struct in `docs/v2/implementation/focas-wire-protocol.md`;
|
||
add `cnc_getfigure` to the protocol surface in
|
||
`docs/v2/implementation/focas-simulator-plan.md`; extend focas-mock
|
||
with the per-axis decimal-place command handler + a `decimal_places`
|
||
field on each profile JSON; update
|
||
`docs/drivers/FOCAS-Test-Fixture.md` "When to trust each layer"
|
||
table with a "Are axis values reported in real-world units?" row;
|
||
add an opt-in `-CheckDecimalScaling` switch to `scripts/e2e/test-focas.ps1`
|
||
that asserts AbsolutePosition is scaled when the flag is on;
|
||
integration test `Series/DecimalScalingTests.cs` and
|
||
`Series/DiagnosticsCountersTests.cs`.
|
||
- Effort: medium — touches every axis read.
|
||
- Risk: Medium — this is a behavioural change for any existing
|
||
consumer that was already dividing client-side. Surface as a
|
||
`FixedTree.ApplyFigureScaling` opt-in flag (default true on new
|
||
installs, false when migrating); document in `docs/drivers/FOCAS.md`.
|
||
|
||
### Phase 2 — addressing additions
|
||
|
||
**PR F2-a — DIAG: address scheme (#14)**
|
||
- Scope: new `FocasAreaKind.Diagnostic` parsed from `DIAG:nnn` /
|
||
`DIAG:nnn/axis`, dispatched to `cnc_rddiag` (or `cnc_rddiagdgn` for
|
||
series that support it).
|
||
- Files: `FocasAddress.cs` (new prefix branch), `FocasCapabilityMatrix.cs`
|
||
(new `DiagnosticRange` per series), `Wire/FocasWireClient.cs`
|
||
(`ReadDiagnosticAsync`), `WireFocasClient.ReadAsync` (new dispatch
|
||
branch).
|
||
- Tests: parser unit tests, capability matrix unit tests, simulator
|
||
read-round-trip.
|
||
- **Docs / fixture / e2e**: add a `DIAG:` row to the address-syntax
|
||
table in `docs/Driver.FOCAS.Cli.md` with `read -a DIAG:301` and
|
||
`DIAG:301/0` (axis-scoped) examples; add a `DIAG:` row to the
|
||
addressing table in `docs/drivers/FOCAS.md`; add per-series
|
||
`DiagnosticRange` columns to `docs/v2/focas-version-matrix.md`;
|
||
document the `ODBDGN` struct in
|
||
`docs/v2/implementation/focas-wire-protocol.md`; add `cnc_rddiag`
|
||
/ `cnc_rddiagdgn` to the protocol surface in
|
||
`docs/v2/implementation/focas-simulator-plan.md`; extend focas-mock
|
||
with the diagnostic-range command handler + per-profile seeded
|
||
diagnostic numbers; integration test
|
||
`Series/DiagAddressTests.cs` round-trips a seeded diagnostic
|
||
number; update `docs/drivers/FOCAS-Test-Fixture.md` capability list
|
||
with the new `Diagnostic` `FocasAreaKind`.
|
||
- Effort: medium.
|
||
- Risk: Low — additive.
|
||
|
||
**PR F2-b — Multi-path / multi-channel CNC (#4)**
|
||
- Scope: 30i/31i/32i can host 2–10 paths; today every request block is
|
||
built with `PathId = 1` (`Wire/FocasWireProtocol.cs:216`). Add
|
||
optional `Path` segment to `FocasAddress` (e.g. `PARAM:1815@2`,
|
||
`R100@3.0`, `MACRO:500@2`); thread it into the `RequestBlock.PathId`
|
||
field. Fixed-tree gets a `Paths/{n}/` folder pivot.
|
||
- Files: `FocasAddress.cs` (new `Path` field + parser), `IFocasClient.cs`
|
||
(every read call gains an optional `pathId` parameter, defaulting to
|
||
1 for backward compatibility), `Wire/FocasWireClient.cs`
|
||
(thread the param through every `RequestBlock` constructor),
|
||
`FocasDriver.cs` (per-device `PathCount` discovery via
|
||
`cnc_rdpathnum`; iterate fixed-tree per path).
|
||
- Tests: unit on the parser; simulator with two paths configured;
|
||
assert that a `PARAM:1815@2` read targets path 2.
|
||
- **Docs / fixture / e2e**: significant `docs/drivers/FOCAS.md`
|
||
update — new "Multi-path / multi-channel CNC" subsection explaining
|
||
the `@N` suffix syntax, `Paths/{n}/` browse pivot, and per-path
|
||
capability gating; add `@N` to every address row in the
|
||
addressing table in `docs/Driver.FOCAS.Cli.md`; document
|
||
`cnc_rdpathnum` (`ODBPATHNUM` struct) in
|
||
`docs/v2/implementation/focas-wire-protocol.md`, and update the
|
||
`RequestBlock.PathId` discussion (was hard-coded to 1 — now a
|
||
parameter); add `cnc_rdpathnum` to the protocol surface and the
|
||
per-profile `path_count` field to the profile schema in
|
||
`docs/v2/implementation/focas-simulator-plan.md`; extend focas-mock
|
||
with per-path state isolation (separate PMC / param / macro tables
|
||
per `path_id`) and a new `multi_path` profile (e.g.
|
||
`thirtyone_i_dual_path`); add a `-Paths` switch to
|
||
`scripts/e2e/test-focas.ps1` that runs the matrix once per
|
||
declared path; document the new compose profile in
|
||
`docs/drivers/FOCAS-Test-Fixture.md`; new
|
||
`Series/MultiPathTests.cs` integration test asserting independent
|
||
per-path reads.
|
||
- Effort: large — touches every wire call's `RequestBlock` shape.
|
||
- Risk: Medium — backward compatibility for existing single-path
|
||
configs. Default `PathId = 1` everywhere; only deviate when the
|
||
address explicitly carries a `@N` suffix or when the fixed-tree
|
||
loop is iterating discovered paths.
|
||
|
||
**PR F2-c — PMC F/G letters for 16i (#15)**
|
||
- Scope: capability matrix bug — `PmcLetters(Sixteen_i)` currently
|
||
returns `{X, Y, R, D}`; real 16i ladders use F/G for handshakes.
|
||
Widen the set; verify the address `pmc_rdpmcrng` numeric letter
|
||
codes match.
|
||
- Files: `FocasCapabilityMatrix.cs` (one-line fix to the 16i case),
|
||
`tests/.../FocasCapabilityMatrixTests.cs` (assert F0.0 and G50.5
|
||
parse against `Sixteen_i`).
|
||
- **Docs / fixture / e2e**: update the 16i row of the PMC-letters
|
||
column in `docs/v2/focas-version-matrix.md` (the row currently lists
|
||
X/Y/R/D — add F/G); add a one-line "fixed in v…" callout to the
|
||
changelog section of the same doc; no simulator change required (the
|
||
16i profile JSON in `tests/.../Docker/focas-mock/profiles/sixteen_i.json`
|
||
already has F/G ranges declared from Stream B); add F0.0 / G50.5
|
||
probes to the 16i row of the per-series matrix in
|
||
`scripts/e2e/test-focas.ps1`; no fixture-doc change needed.
|
||
- Effort: trivial.
|
||
- Risk: Low — correctness fix.
|
||
|
||
**PR F2-d — Bulk PMC range read (#16)**
|
||
- Scope: today the driver issues one `pmc_rdpmcrng` per tag (one TCP
|
||
RTT each). The wire call already supports a range `[start, end]`;
|
||
the missing piece is coalescing on the read side. Add a coalescer:
|
||
group same-letter contiguous (or near-contiguous within a small
|
||
gap budget) PMC bytes from the request batch into one wire call
|
||
per group, then slice client-side. Reuse the Modbus coalescing
|
||
infrastructure pattern (per-group-id ProhibitedRanges) where it
|
||
applies.
|
||
- Files: new `Wire/FocasPmcCoalescer.cs`, hook into
|
||
`FocasDriver.ReadAsync` between the per-tag path and the wire call
|
||
layer. Surface coalesce stats on the `Diagnostics/` subtree (PR F1-f).
|
||
- Tests: unit — given a request batch of `R100..R110`, assert that
|
||
the coalescer issues one call covering 100..110 and slices the
|
||
result. Integration — assert observed wire-call count drops with
|
||
coalescing on.
|
||
- **Docs / fixture / e2e**: add a "PMC range coalescing" subsection
|
||
to `docs/drivers/FOCAS.md` (wire-call reduction, gap budget,
|
||
per-series byte cap); document the new `Diagnostics/CoalesceStats/*`
|
||
counters added on top of PR F1-f's diagnostics tree; add a
|
||
PMC-byte-cap column to `docs/v2/focas-version-matrix.md`;
|
||
no new wire calls (`pmc_rdpmcrng` is already in the surface), but
|
||
document the supported max-bytes-per-call in
|
||
`docs/v2/implementation/focas-wire-protocol.md`; extend focas-mock
|
||
with a request-counter admin endpoint so integration tests can
|
||
assert the call-count reduction (counter visible via
|
||
`FocasSimFixture.GetWireCallCountAsync`); update
|
||
`docs/v2/implementation/focas-simulator-plan.md` Stream B
|
||
validation harness with the request-counter handler; integration
|
||
test `Series/PmcCoalescingTests.cs` asserts an `R100..R110` batch
|
||
produces exactly 1 wire call against the mock.
|
||
- Effort: medium.
|
||
- Risk: Medium — the FANUC max-bytes-per-`pmc_rdpmcrng` ceiling is
|
||
series-specific; cap conservatively (≤ 256 bytes per range) and
|
||
let operators raise it via config if their CNC accepts more.
|
||
|
||
### Phase 3 — alarm history
|
||
|
||
**PR F3-a — `cnc_rdalmhistry` extension to alarm projection (#17)**
|
||
- Scope: extend `FocasAlarmProjection` with two modes — `ActiveOnly`
|
||
(today's behaviour) and `ActivePlusHistory`. In the latter, on
|
||
connect (and on a configurable cadence — default 5 min, since the
|
||
CNC ring buffer changes only on alarm raise/clear) issue
|
||
`cnc_rdalmhistry` for the most-recent N entries; project as
|
||
historic events through `IAlarmSource` with `OccurrenceTime` from
|
||
the CNC's timestamp field.
|
||
- Files: new `Wire/FocasWireClient.ReadAlarmHistoryAsync`, new
|
||
`IFocasClient.ReadAlarmHistoryAsync`,
|
||
`FocasAlarmProjection.cs` (mode switch + history poll loop),
|
||
`FocasDriverOptions.cs` (`AlarmProjection.Mode` enum +
|
||
`HistoryPollInterval` + `HistoryDepth`).
|
||
- Tests: simulator returns canned history payload; assert events
|
||
fire with the timestamps from the canned data and don't re-fire
|
||
on every poll.
|
||
- **Docs / fixture / e2e**: add an "Alarm history" subsection to
|
||
`docs/drivers/FOCAS.md` documenting the `ActiveOnly` vs
|
||
`ActivePlusHistory` mode switch, the `HistoryDepth` cap, and the
|
||
dedup key; add a configuration-knob row to
|
||
`docs/v2/focas-deployment.md` for operator dashboards; document
|
||
`ODBALMHIS` struct in
|
||
`docs/v2/implementation/focas-wire-protocol.md`; add
|
||
`cnc_rdalmhistry` to the protocol surface in
|
||
`docs/v2/implementation/focas-simulator-plan.md`; extend focas-mock
|
||
with a ring-buffer alarm history (per profile) + `mock_patch_alarmhistory`
|
||
admin endpoint; expose a `SeedAlarmHistoryAsync` helper on
|
||
`FocasSimFixture`; add `Series/AlarmHistoryProjectionTests.cs`
|
||
asserting historic events fire once and active events still fire
|
||
raise/clear; update `docs/drivers/FOCAS-Test-Fixture.md` integration
|
||
bullet list with `cnc_rdalmhistry`.
|
||
- Effort: medium.
|
||
- Risk: Medium — duplicate-event suppression; key history events on
|
||
`(timestamp, alarmNumber, type)` to deduplicate against the active
|
||
list.
|
||
|
||
### Phase 4 — write path
|
||
|
||
This phase is the major behavioural change. The driver's read-only
|
||
contract has been the documented design choice in
|
||
`docs/drivers/FOCAS.md:14-18` and is reinforced by tests
|
||
(`FocasReadWriteTests.WriteAsync_ReturnsBadNotWritable`). Removing it
|
||
deserves a deliberate decision-record entry in the v2 decisions log
|
||
before any code lands.
|
||
|
||
**PR F4-a — write infrastructure + per-tag opt-in (no wire calls yet)**
|
||
- Scope: drop the `BadNotWritable` short-circuit in
|
||
`WireFocasClient.WriteAsync` and replace with a kind-based dispatch
|
||
that returns `BadNotWritable` only for kinds the wire client
|
||
doesn't yet implement. Honour `FocasTagDefinition.Writable` (already
|
||
present, default `true` — flip default to `false` per #1's safer
|
||
posture). Plumb `WriteIdempotent` through Polly retry.
|
||
- Files: `WireFocasClient.cs`, `FocasDriverOptions.cs`,
|
||
`FocasDriver.cs`, `docs/drivers/FOCAS.md` (rewrite the read-only
|
||
paragraph), new `docs/v2/decisions.md` entry.
|
||
- Tests: assert that with `Writable=false` the path still returns
|
||
`BadNotWritable`; with `Writable=true` and an unimplemented kind
|
||
the write returns `BadNotSupported` (distinct from the per-tag
|
||
policy denial).
|
||
- **Docs / fixture / e2e**: this is the heaviest doc PR in the plan.
|
||
- **`docs/drivers/FOCAS.md` lines 14–18** — revoke the unconditional
|
||
"OtOpcUa is read-only against FOCAS… Writes return BadNotWritable
|
||
by design" callout. Replace with a "Writes (opt-in, off by
|
||
default)" subsection that names `Writes.Enabled`, the per-tag
|
||
`Writable` flag (default flipped to `false`), and links to the
|
||
Phase 4 decision-record entry.
|
||
- **`docs/drivers/FOCAS-Test-Fixture.md` lines 42–43** — revoke the
|
||
"`IWritable` intentionally returns `BadNotWritable` — OtOpcUa is
|
||
read-only against FOCAS" callout. Replace with a qualified
|
||
"default behaviour" note plus a pointer to the new write-enabled
|
||
test profile.
|
||
- **`docs/Driver.FOCAS.Cli.md` lines 100–116** — the existing
|
||
`write` section already documents the CLI shape; expand the
|
||
"**Writes are non-idempotent by default**" warning with a
|
||
server-side note that the OtOpcUa endpoint enforces the
|
||
`Writes.Enabled` flag and rejects writes when off, and that
|
||
the CLI itself talks to the driver directly so its writes are
|
||
not gated by the server flag (operator must consciously use
|
||
the right tool).
|
||
- New `docs/v2/decisions.md` entry "FOCAS write-path opt-in"
|
||
capturing the design-choice reversal.
|
||
- Update `docs/featuregaps.md` row for #1 / #3 — flip Build = Yes
|
||
annotation to "shipping behind flag".
|
||
- Simulator: no new commands; existing read commands gain a
|
||
"writes when not unlocked" branch wired up here for symmetry
|
||
even though no write commands ship yet (returns
|
||
`BadNotSupported` until F4-b lands).
|
||
- E2E: add `-Write` switch (no-op stage in this PR; populated by
|
||
F4-b) to `scripts/e2e/test-focas.ps1`.
|
||
- Effort: medium.
|
||
- Risk: High — design-choice reversal. Mitigation: ship behind a
|
||
driver-level `Writes.Enabled` flag (default `false`); operators
|
||
must explicitly enable in `appsettings.json`.
|
||
|
||
**PR F4-b — `cnc_wrmacro` + `cnc_wrparam`**
|
||
- Scope: implement macro and parameter writes. Both have well-defined
|
||
payload shapes mirroring their read counterparts (IODBPSD for
|
||
parameters, ODBM for macros).
|
||
- Files: `Wire/FocasWireClient.cs` (new `WriteParameterAsync`,
|
||
`WriteMacroAsync`), `WireFocasClient.WriteAsync` (dispatch).
|
||
- Tests: simulator extension — accept writes and reflect them on
|
||
subsequent reads. ACL tests in
|
||
`tests/ZB.MOM.WW.OtOpcUa.IntegrationTests` to verify the
|
||
server-layer enforcement (per the memory entry: ACL decisions
|
||
happen in `DriverNodeManager`, never in driver-level code).
|
||
- **Docs / fixture / e2e**:
|
||
- `docs/drivers/FOCAS.md` — extend the "Writes" subsection
|
||
(introduced in F4-a) with the two new write kinds, the
|
||
`Writes.AllowParameter` and `Writes.AllowMacro` granular flags,
|
||
and a security note: parameter writes require LDAP group
|
||
`WriteConfigure`, macro writes require `WriteOperate` (cross-link
|
||
to `docs/Security.md`).
|
||
- `docs/v2/focas-deployment.md` — significant addition: a "Write
|
||
safety" section covering operator pre-checks (CNC in MDI mode,
|
||
parameter-write switch enabled), audit-log expectations, and the
|
||
LDAP group requirements.
|
||
- `docs/Driver.FOCAS.Cli.md` — populate the existing `write`
|
||
examples for `PARAM:` and `MACRO:` (already present at lines
|
||
105–108) with a "Server-enforced ACL" note linking to
|
||
`docs/Security.md`.
|
||
- Document `IODBPSD` (write side) and `ODBM` (write side) in
|
||
`docs/v2/implementation/focas-wire-protocol.md` (the read-side
|
||
structs are already there — flag the byte layout symmetry).
|
||
- `docs/v2/implementation/focas-simulator-plan.md` — add
|
||
`cnc_wrparam` / `cnc_wrmacro` to the protocol surface table
|
||
and update Stream C status accordingly.
|
||
- Extend focas-mock with `cnc_wrparam` / `cnc_wrmacro` handlers
|
||
that mutate the per-profile state and return
|
||
`EW_PASSWD` when the unlock state is off (sets up F4-d's
|
||
test path); add `mock_get_last_write` admin endpoint for
|
||
audit-log assertions.
|
||
- New `Series/ParameterWriteTests.cs` and `Series/MacroWriteTests.cs`
|
||
integration tests; ACL test under
|
||
`tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/Authz/FocasWriteAclTests.cs`
|
||
asserting `WriteConfigure` is required for `PARAM:` writes and
|
||
`WriteOperate` for `MACRO:` writes.
|
||
- `scripts/e2e/test-focas.ps1` — populate the `-Write` stage from
|
||
F4-a with macro and parameter round-trip writes against the
|
||
Docker mock.
|
||
- Effort: medium.
|
||
- Risk: High — a misdirected parameter write can put the CNC into a
|
||
bad state. Surface a `Writes.AllowParameter` flag separate from
|
||
`Writes.Enabled` so operators can grant macro writes without
|
||
parameter writes.
|
||
|
||
**PR F4-c — `pmc_wrpmcrng`**
|
||
- Scope: PMC range writes. Read-modify-write semantics for bit-level
|
||
writes (the wire call is byte-addressed). Existing tests
|
||
(`FocasPmcBitRmwTests.cs`) prove the read-modify-write pattern
|
||
shape that the write path needs.
|
||
- Files: `Wire/FocasWireClient.cs` (new `WritePmcRangeAsync`),
|
||
bit-level RMW helper in `WireFocasClient`.
|
||
- Tests: simulator round-trip on byte writes; bit-level write asserts
|
||
the unrelated bits in the same byte are preserved.
|
||
- **Docs / fixture / e2e**:
|
||
- `docs/drivers/FOCAS.md` — extend the "Writes" subsection with
|
||
PMC writes; loud safety callout block ("PMC is ladder working
|
||
memory — a mistargeted bit can move motion"); document the
|
||
read-modify-write semantics for bit-level writes; document the
|
||
new `Writes.AllowPmc` granular flag.
|
||
- `docs/v2/focas-deployment.md` — extend the "Write safety"
|
||
section with PMC-specific pre-checks (e-stop, jog mode); add an
|
||
ops-runbook bullet on auditing PMC writes from the
|
||
`Diagnostics/CoalesceStats/` (extended) tree.
|
||
- `docs/Driver.FOCAS.Cli.md` — the existing `write` example
|
||
`write -h … -a G50.3 -t Bit -v on` (line 107) is already PMC-bit;
|
||
update its surrounding warning to call out RMW behaviour.
|
||
- Document the `pmc_wrpmcrng` request frame in
|
||
`docs/v2/implementation/focas-wire-protocol.md` (the read frame
|
||
is already there — flag the inverted shape).
|
||
- `docs/v2/implementation/focas-simulator-plan.md` — add
|
||
`pmc_wrpmcrng` to the protocol surface table.
|
||
- Extend focas-mock with `pmc_wrpmcrng` handler that mutates
|
||
per-profile PMC tables; assert byte-aligned writes preserve
|
||
untouched bytes (mirrors the driver's RMW contract).
|
||
- New `Series/PmcRangeWriteTests.cs` and
|
||
`Series/PmcBitRmwIntegrationTests.cs` integration tests; ACL
|
||
test under
|
||
`tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/Authz/FocasPmcWriteAclTests.cs`
|
||
asserting `WriteOperate` is required.
|
||
- `scripts/e2e/test-focas.ps1` — extend the `-Write` stage with a
|
||
PMC bit round-trip.
|
||
- Effort: medium.
|
||
- Risk: High — PMC is the ladder logic's working memory; a
|
||
mistargeted write can move motion. Document loudly.
|
||
|
||
**PR F4-d — FOCAS password / unlock parameter (#3)**
|
||
- Scope: some controllers gate `cnc_wrparam` and certain reads behind
|
||
a connection-level password. Add `Password` to `FocasDeviceOptions`;
|
||
emit the FOCAS password block during connect (`cnc_wrunlockparam`
|
||
per FOCAS docs — confirm the exact command id during simulator
|
||
iteration). On any read/write returning `EW_PASSWD`
|
||
re-issue the password and retry once.
|
||
- Files: `Wire/FocasWireClient.cs` (`UnlockAsync`),
|
||
`FocasDriverOptions.cs` (`Password` field, treated as a secret —
|
||
redact in logs), `FocasDriver.cs` (call on connect).
|
||
- Tests: simulator extension — emit `EW_PASSWD` on writes when not
|
||
unlocked; assert the unlock+retry path.
|
||
- **Docs / fixture / e2e**:
|
||
- `docs/drivers/FOCAS.md` — new "FOCAS password" subsection under
|
||
Writes describing the optional `Password` device-option, when
|
||
the CNC requires it (16i + some 30i firmwares with parameter-
|
||
protect on), and the redaction guarantee.
|
||
- **Security-note in `docs/v2/focas-deployment.md`** — significant
|
||
addition: a "FOCAS password handling" subsection covering
|
||
storage in `appsettings.json` (and the dev redaction pattern at
|
||
`.local/`), the no-log invariant, and a runbook for password
|
||
rotation. Cross-link to `docs/Security.md`.
|
||
- `docs/Driver.FOCAS.Cli.md` — add a `--cnc-password` flag row to
|
||
the "Common flags" table with the redaction note.
|
||
- Document `cnc_wrunlockparam` (or the resolved command id) in
|
||
`docs/v2/implementation/focas-wire-protocol.md`; resolve the
|
||
open question raised by F4-d into the doc.
|
||
- `docs/v2/implementation/focas-simulator-plan.md` — add
|
||
`cnc_wrunlockparam` to the protocol surface; document the
|
||
per-profile `unlock_password` field on the JSON profile schema.
|
||
- Extend focas-mock with locked-state semantics on parameter
|
||
writes (already half-stubbed in F4-b's `EW_PASSWD` branch);
|
||
add `cnc_wrunlockparam` handler; add `mock_set_password`
|
||
admin endpoint so integration tests can pin the unlock value.
|
||
- New `Series/PasswordUnlockTests.cs` integration test asserts
|
||
a write returning `EW_PASSWD` triggers exactly one unlock
|
||
retry, and the second write succeeds.
|
||
- `scripts/e2e/test-focas.ps1` — add `-CncPassword` parameter,
|
||
threaded through to the CLI for the `-Write` stage.
|
||
- Effort: small — once Phase 4-a/b are in.
|
||
- Risk: Medium — password storage. Use the existing
|
||
`appsettings.json` redaction pattern (memory entry: `dohertj2`
|
||
AppData path); never log the password value.
|
||
|
||
### Phase 5 — derived telemetry
|
||
|
||
**PR F5-a — Cycle time per part / last cycle delta (#24 derivation)**
|
||
- Scope: with `Production/CycleTimeSeconds` in place from F1-b and
|
||
the parts-count from `cnc_rdparam`, compute "last completed cycle"
|
||
as the delta in `Timers/CycleSeconds` between successive
|
||
parts-count increments. Project `Production/LastCycleSeconds`,
|
||
`Production/LastCycleStartUtc`.
|
||
- Files: `FocasDriver.cs` only — pure derivation in the program-poll
|
||
cadence handler.
|
||
- Tests: simulate a parts-count increment from 5→6; assert
|
||
`LastCycleSeconds` equals the cycle-timer delta over the same
|
||
window.
|
||
- **Docs / fixture / e2e**: add `Production/LastCycleSeconds` and
|
||
`Production/LastCycleStartUtc` rows to the fixed-tree table in
|
||
`docs/drivers/FOCAS.md` with the rollover / counter-reset
|
||
behaviour documented; add a `Derived telemetry` callout in
|
||
`docs/v2/focas-deployment.md` explaining the derivation is
|
||
client-visible only (no new wire calls); no
|
||
`docs/v2/implementation/focas-wire-protocol.md` change (pure
|
||
derivation); no focas-mock change beyond `FocasSimFixture`'s
|
||
existing parameter-patch / timer-patch helpers — add a
|
||
`SimulateCycleCompletionAsync` convenience helper that increments
|
||
parts-count and advances the cycle timer atomically; new
|
||
`Series/CycleDeltaTests.cs` integration test simulates a 5→6
|
||
parts-count transition; no `scripts/e2e/test-focas.ps1` change.
|
||
- Effort: small.
|
||
- Risk: Low — pure derivation.
|
||
|
||
## Documentation, fixture, and e2e impact
|
||
|
||
Consolidated view of every doc, fixture, and e2e artefact this plan
|
||
touches. FOCAS has the largest doc surface of any driver in the v2
|
||
roadmap because Phase 4 reverses a long-standing read-only design
|
||
choice that is referenced from at least three user-facing docs and one
|
||
test-fixture doc.
|
||
|
||
### Docs touched (per file, with the heaviest PR called out)
|
||
|
||
| Doc | Touched by | Heaviest change |
|
||
| --- | --- | --- |
|
||
| `docs/drivers/FOCAS.md` | F1-a, F1-b, F1-c, F1-d, F1-e, F1-f, F2-a, F2-b, F2-d, F3-a, F4-a, F4-b, F4-c, F4-d, F5-a | **F4-a** revokes the read-only callout at lines 14–18; **F2-b** adds the multi-path subsection |
|
||
| `docs/drivers/FOCAS-Test-Fixture.md` | F1-a, F1-d, F1-f, F2-a, F2-b, F3-a, F4-a | **F4-a** revokes the "`IWritable` intentionally returns `BadNotWritable`" callout at lines 42–43 |
|
||
| `docs/Driver.FOCAS.Cli.md` | F1-b, F1-c, F2-a, F2-b, F4-a, F4-b, F4-c, F4-d | **F4-a** qualifies the read-only stance at lines 100–116; **F4-d** adds `--cnc-password` flag |
|
||
| `docs/v2/focas-deployment.md` | F1-f, F3-a, F4-a, F4-b, F4-c, F4-d | **F4-b** adds "Write safety" section; **F4-d** adds "FOCAS password handling" section |
|
||
| `docs/v2/focas-version-matrix.md` | F1-c, F1-d, F2-a, F2-c, F2-d | **F1-d** adds capability-suppression rows for tooling/offsets |
|
||
| `docs/v2/implementation/focas-wire-protocol.md` | F1-a, F1-c, F1-d, F1-e, F1-f, F2-a, F2-b, F2-d, F3-a, F4-b, F4-c, F4-d | **F1-d** documents three new structs (ODBTOFS, ODBTLIFE5, IODBZOR); **F4-d** resolves the `cnc_wrunlockparam` open question |
|
||
| `docs/v2/implementation/focas-simulator-plan.md` | F1-c, F1-d, F1-e, F1-f, F2-a, F2-b, F2-d, F3-a, F4-a, F4-b, F4-c, F4-d | Each PR appends to the protocol surface table; F4-* close out Stream C status |
|
||
| `docs/v2/decisions.md` (new entry) | F4-a | Net-new decision-record for the read-only reversal |
|
||
| `docs/featuregaps.md` | F4-a | Updates Build = Yes annotation for #1 / #3 with "shipping behind flag" |
|
||
|
||
### Fixture (focas-mock) extensions
|
||
|
||
The vendored Python `focas-mock` simulator under
|
||
`tests/.../IntegrationTests/Docker/focas-mock/` gains the following
|
||
new command-id handlers and per-profile state:
|
||
|
||
| PR | Mock extension |
|
||
| --- | --- |
|
||
| F1-a | `cnc_rdcncstat` full-struct response |
|
||
| F1-b | Seeded values for parameters 6711/6712/6713 in every profile JSON |
|
||
| F1-c | New `cnc_modal` handler + canned modal payload per profile |
|
||
| F1-d | `cnc_rdtofs` / `cnc_rdtlife*` / `cnc_rdzofs` handlers + per-profile tool/offset tables, plus a `tools_per_series` profile knob |
|
||
| F1-e | `cnc_rdopmsg3` / `cnc_rdactpt` handlers + `mock_patch_opmsg` admin endpoint |
|
||
| F1-f | `cnc_getfigure` handler + per-profile `decimal_places` field |
|
||
| F2-a | `cnc_rddiag` / `cnc_rddiagdgn` handlers + per-profile diagnostic numbers |
|
||
| F2-b | Per-path state isolation; new `path_count` profile field; new `thirtyone_i_dual_path` compose profile |
|
||
| F2-c | No mock change (16i profile already declares F/G ranges) |
|
||
| F2-d | Wire-call counter admin endpoint |
|
||
| F3-a | Ring-buffer alarm history + `mock_patch_alarmhistory` admin endpoint |
|
||
| F4-a | Stub branch returning `BadNotSupported` for write commands |
|
||
| F4-b | `cnc_wrparam` / `cnc_wrmacro` handlers (with `EW_PASSWD` when locked); `mock_get_last_write` admin endpoint |
|
||
| F4-c | `pmc_wrpmcrng` handler with byte-aligned write semantics |
|
||
| F4-d | `cnc_wrunlockparam` handler; `mock_set_password` admin endpoint; locked-state on the param-write path |
|
||
| F5-a | `SimulateCycleCompletionAsync` helper on `FocasSimFixture` (no new mock command) |
|
||
|
||
`FocasSimFixture` (in
|
||
`tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/FocasSimFixture.cs`)
|
||
gains corresponding admin-API client helpers for each new endpoint.
|
||
|
||
### Integration tests (per phase)
|
||
|
||
| Phase | New / extended integration tests under `tests/.../FOCAS.IntegrationTests/Series/` |
|
||
| --- | --- |
|
||
| Phase 1 | `StatusFlagsPopulateTests.cs`, `ProductionPopulatesTests.cs`, `ModalPopulatesTests.cs`, `ToolingPopulatesTests.cs`, `OffsetsPopulatesTests.cs`, `OperatorMessagesPopulateTests.cs`, `DecimalScalingTests.cs`, `DiagnosticsCountersTests.cs` |
|
||
| Phase 2 | `DiagAddressTests.cs`, `MultiPathTests.cs`, `PmcCoalescingTests.cs` (plus a 16i row in `FocasCapabilityMatrixTests.cs` for F2-c) |
|
||
| Phase 3 | `AlarmHistoryProjectionTests.cs` |
|
||
| Phase 4 | `ParameterWriteTests.cs`, `MacroWriteTests.cs`, `PmcRangeWriteTests.cs`, `PmcBitRmwIntegrationTests.cs`, `PasswordUnlockTests.cs` plus ACL tests under `tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/Authz/FocasWriteAclTests.cs` and `FocasPmcWriteAclTests.cs` |
|
||
| Phase 5 | `CycleDeltaTests.cs` |
|
||
|
||
### E2E script (`scripts/e2e/test-focas.ps1`) updates
|
||
|
||
| PR | Change |
|
||
| --- | --- |
|
||
| F1-f | New `-CheckDecimalScaling` switch |
|
||
| F2-b | New `-Paths` switch (matrix mode iterates per declared path) |
|
||
| F2-c | Adds F0.0 / G50.5 probes to the 16i row of the per-series matrix |
|
||
| F4-a | Adds `-Write` switch (no-op stage in F4-a; populated by F4-b/c) |
|
||
| F4-b | Populates `-Write` stage with macro + parameter round-trip writes |
|
||
| F4-c | Extends `-Write` stage with PMC bit round-trip |
|
||
| F4-d | Adds `-CncPassword` parameter, threaded through to the CLI |
|
||
|
||
`scripts/integration/run-focas.ps1` does not change shape across the
|
||
plan — it remains the compose up/test/compose down wrapper. New
|
||
profiles registered by F2-b are automatically picked up via the
|
||
existing `-Profile` switch.
|
||
|
||
### Read-only callouts requiring revocation in Phase 4
|
||
|
||
For reviewer benefit, the explicit read-only callouts that **F4-a
|
||
must revoke or qualify** in the same PR that flips the design choice:
|
||
|
||
- `docs/drivers/FOCAS.md` lines 14–18 ("OtOpcUa is **read-only**
|
||
against FOCAS… Writes return `BadNotWritable` by design.")
|
||
- `docs/drivers/FOCAS-Test-Fixture.md` lines 42–43 ("`IWritable`
|
||
intentionally returns `BadNotWritable` — OtOpcUa is read-only
|
||
against FOCAS.")
|
||
- `docs/Driver.FOCAS.Cli.md` lines 100–116 (write section is already
|
||
documented but predates the server-side flag; needs a
|
||
server-enforced-ACL note)
|
||
- `docs/featuregaps.md` (FOCAS row entries for #1 and #3 carry the
|
||
same read-only-by-design framing — flip annotation)
|
||
|
||
## Skip-rated items (for context)
|
||
|
||
These appear in the featuregaps recommendations table as Build = No;
|
||
recapped here so reviewers can confirm the scope decision rather than
|
||
re-deriving it from `featuregaps.md`:
|
||
|
||
- **#2 HSSB transport** — PCI hardware, declining install base,
|
||
reopens the Fwlib distribution problem the wire client deliberately
|
||
closed.
|
||
- **#5 Series 15 / Power Mate D-H / Series 35i** — very legacy; small
|
||
install base. Capability matrix already accepts `Unknown` as a
|
||
permissive escape hatch.
|
||
- **#9 Tool-offset write** — write-heavy; defer alongside the general
|
||
write decision (F4 covers reads via tool-life only).
|
||
- **#19 Program list / upload / download / delete** — DNC product
|
||
territory; significant scope; out of OtOpcUa's MES focus.
|
||
- **#21 DPRNT TCP listener** — significant scope; modern OPC UA
|
||
alarms / events supersede it.
|
||
- **#22 Servo / spindle deep info (`cnc_rdsvinfo` / `cnc_rdspinfo`)** —
|
||
specialty; load-percent already covers most needs.
|
||
- **#23 Per-axis acceleration / jerk / feed-per-rev** — niche
|
||
advanced telemetry.
|
||
- **#25 Operator write commands (preset, `cnc_setpath`, `cnc_wrabsmac`)** —
|
||
read-only-by-design covers it; parameter / PMC / macro writes from
|
||
Phase 4 are the supervisory writes operators actually need.
|
||
- **#26 CNC time / date sync** — rare ask; commonly handled by CNC NTP.
|
||
|
||
## Open questions
|
||
|
||
- **Modal command id** (PR F1-c): `cnc_modal` numeric command code is
|
||
not in the existing wire-protocol notes
|
||
(`docs/v2/implementation/focas-wire-protocol.md`). Capture during
|
||
the simulator iteration loop; if the simulator can't yet emit the
|
||
shape, gate F1-c behind a bench-CNC trace per the
|
||
diminishing-returns checkpoint.
|
||
- **Override parameter numbers** (PR F1-c): feedrate / rapid /
|
||
spindle override register numbers are MTB-specific. Default to the
|
||
documented Fanuc factory numbers and let operators override per
|
||
device (`Devices[].OverrideRegisters` map).
|
||
- **Multi-path discovery** (PR F2-b): does the simulator support
|
||
multi-path responses today? If not, F2-b lands gated behind the
|
||
`OTOPCUA_FOCAS_SIM_WIRE_COMPAT=1` flag the wire-protocol doc
|
||
describes.
|
||
- **Decimal-scaling migration** (PR F1-f): existing `Float64` axis
|
||
nodes are scaled integers today. Decision: ship F1-f with
|
||
scaling-on default, add a one-release deprecation window with the
|
||
flag default-off so existing dashboards don't silently scale by
|
||
10^N when the driver is upgraded. Need explicit operator opt-in.
|
||
- **Write security posture** (Phase 4): should writes require LDAP
|
||
group `WriteConfigure` (parameters) vs `WriteOperate` (macros /
|
||
PMC)? Per the memory entry on ACL-at-server-layer, the driver only
|
||
reports `SecurityClassification`; the server enforces. Need the
|
||
driver to surface the right classification per address kind:
|
||
`Configure` for `PARAM:`, `Operate` for `MACRO:` and PMC writes.
|
||
- **Phase 4 rollout**: ship behind a feature flag in `appsettings.json`
|
||
(`Drivers.{name}.Config.Writes.Enabled`) with `false` default for at
|
||
least one release before flipping the default. Update
|
||
`docs/drivers/FOCAS.md` and `docs/featuregaps.md` in the same PR
|
||
that flips the default.
|
||
- **Cycle-delta edge cases** (PR F5-a): parts-count rollover; counter
|
||
reset by the operator. Default behaviour: emit the delta only when
|
||
the counter strictly increments by 1; on any other transition emit
|
||
`Production/LastCycleSeconds` as `null` with `BadOutOfRange` and
|
||
let the operator interpret.
|