FOCAS version-matrix stabilization (PR 1 of #220 split) — ship the cheap half of the hardware-free stability gap ahead of the Tier-C out-of-process split. Without any CNC or simulator on the bench, the highest-leverage move is to catch operator config errors at init time instead of at steady-state per-read. Adds FocasCncSeries enum (Unknown/16i/0i-D/0i-F family/30i family/PowerMotion-i) + FocasCapabilityMatrix static class that encodes the per-series documented ranges for macro variables (cnc_rdmacro/wrmacro), parameters (cnc_rdparam/wrparam), and PMC letters + byte ceilings (pmc_rdpmcrng/wrpmcrng) straight from the Fanuc FOCAS Developer Kit. FocasDeviceOptions gains a Series knob (defaults Unknown = permissive so pre-matrix configs don't break on upgrade). FocasDriver.InitializeAsync now calls FocasAddress.TryParse on every tag + runs FocasCapabilityMatrix.Validate against the owning device's declared series, throwing InvalidOperationException with a reason string that names both the series and the documented limit ("Parameter #30000 is outside the documented range [0, 29999] for Thirty_i") so an operator can tell whether the mismatch is in the config or in their declared CNC model. Unknown series skips validation entirely. Ships 46 new theory cases in FocasCapabilityMatrixTests.cs — covering every boundary in the matrix (widen 16i->0i-F: macro ceiling 999->9999, param 9999->14999; widen 0i-F->30i: PMC letters +K+T; PMC-number 16i=999/0i-D=1999/0i-F=9999/30i=59999), permissive Unknown-series behavior, rejection-message content, and case-insensitive PMC-letter matching. Widening a range without updating docs/v2/focas-version-matrix.md fails a test because every InlineData cites the row it reflects. Full FOCAS test suite stays at 165/165 passing (119 existing + 46 new). Also authors docs/v2/focas-version-matrix.md as the authoritative range reference with per-function citations, CNC-series era context, error-surface shape, and the link back to the matrix code; docs/v2/implementation/focas-isolation-plan.md as the multi-PR plan for #220 Tier-C isolation (Shared contracts -> Host skeleton -> move Fwlib32 calls -> Supervisor+respawn -> MMF+ops glue, 2200-3200 LOC across 5 PRs mirroring the Galaxy Tier-C topology); and promotes docs/drivers/FOCAS-Test-Fixture.md from "version-matrix coverage = no" to explicit coverage via the new test file + cross-links to the matrix and isolation-plan docs. Leaves task #220 open since isolation itself (the expensive half) is still ahead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
docs/v2/focas-version-matrix.md
Normal file
145
docs/v2/focas-version-matrix.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# FOCAS version / capability matrix
|
||||
|
||||
Authoritative source for the per-CNC-series ranges that
|
||||
[`FocasCapabilityMatrix`](../../src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs)
|
||||
enforces at driver init time. Every row cites the Fanuc FOCAS Developer
|
||||
Kit function whose documented input range determines the ceiling.
|
||||
|
||||
**Why this exists** — we have no FOCAS hardware on the bench and no
|
||||
working simulator. Fwlib32 returns `EW_NUMBER` / `EW_PARAM` when you
|
||||
hand it an address outside the controller's supported range; the
|
||||
driver would map that to a per-read `BadOutOfRange` at steady state.
|
||||
Catching at `InitializeAsync` with this matrix surfaces operator
|
||||
typos + mismatched series declarations as config errors before any
|
||||
session is opened, which is the only feedback loop available without
|
||||
a live CNC to read against.
|
||||
|
||||
**Who declares the series** — `FocasDeviceOptions.Series` in
|
||||
`appsettings.json`. Defaults to `Unknown`, which is permissive — every
|
||||
address passes validation. Pre-matrix configs don't break on upgrade.
|
||||
|
||||
---
|
||||
|
||||
## Series covered
|
||||
|
||||
| Enum value | Controller family | Typical era |
|
||||
| --- | --- | --- |
|
||||
| `Unknown` | (legacy / not declared) | permissive fallback |
|
||||
| `Sixteen_i` | 16i / 18i / 21i | 1997-2008 |
|
||||
| `Zero_i_D` | 0i-D | 2008-2013 |
|
||||
| `Zero_i_F` | 0i-F | 2013-present, general-purpose |
|
||||
| `Zero_i_MF` | 0i-MF | 0i-F lathe variant |
|
||||
| `Zero_i_TF` | 0i-TF | 0i-F turning variant |
|
||||
| `Thirty_i` | 30i-A / 30i-B | 2007-present, high-end |
|
||||
| `ThirtyOne_i` | 31i-A / 31i-B | 30i simpler variant |
|
||||
| `ThirtyTwo_i` | 32i-A / 32i-B | 30i compact |
|
||||
| `PowerMotion_i` | Power Motion i-A / i-MODEL A | motion-only controller |
|
||||
|
||||
## Macro variable range (`cnc_rdmacro` / `cnc_wrmacro`)
|
||||
|
||||
Common macros `1-33` + `100-199` + `500-999` are universal across all
|
||||
series. Extended macros (`#10000+`) exist only on higher-end series.
|
||||
The numbers below reflect the extended ceiling per series per the
|
||||
DevKit range tables.
|
||||
|
||||
| Series | Min | Max | Notes |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `Sixteen_i` | 0 | 999 | legacy ceiling — no extended |
|
||||
| `Zero_i_D` | 0 | 999 | 0i-D still at legacy ceiling |
|
||||
| `Zero_i_F` / `Zero_i_MF` / `Zero_i_TF` | 0 | 9999 | extended added on 0i-F |
|
||||
| `Thirty_i` / `ThirtyOne_i` / `ThirtyTwo_i` | 0 | 99999 | full extended set |
|
||||
| `PowerMotion_i` | 0 | 999 | atypical — limited macro coverage |
|
||||
|
||||
## Parameter range (`cnc_rdparam` / `cnc_wrparam`)
|
||||
|
||||
| Series | Min | Max |
|
||||
| --- | ---: | ---: |
|
||||
| `Sixteen_i` | 0 | 9999 |
|
||||
| `Zero_i_D` / `Zero_i_F` / `Zero_i_MF` / `Zero_i_TF` | 0 | 14999 |
|
||||
| `Thirty_i` / `ThirtyOne_i` / `ThirtyTwo_i` | 0 | 29999 |
|
||||
| `PowerMotion_i` | 0 | 29999 |
|
||||
|
||||
## PMC letters (`pmc_rdpmcrng` / `pmc_wrpmcrng`)
|
||||
|
||||
Addresses are letter + number (e.g. `R100`, `F50.3`). Legacy
|
||||
controllers omit the `F`/`G` signal groups that 30i-family ladder
|
||||
programs use, and only the 30i-family exposes `K` (keep-relay) +
|
||||
`T` (timer).
|
||||
|
||||
| Letter | 16i | 0i-D | 0i-F family | 30i family | Power Motion-i |
|
||||
| --- | :-: | :-: | :-: | :-: | :-: |
|
||||
| `X` | yes | yes | yes | yes | yes |
|
||||
| `Y` | yes | yes | yes | yes | yes |
|
||||
| `R` | yes | yes | yes | yes | yes |
|
||||
| `D` | yes | yes | yes | yes | yes |
|
||||
| `E` | — | yes | yes | yes | — |
|
||||
| `A` | — | yes | yes | yes | — |
|
||||
| `F` | — | — | yes | yes | — |
|
||||
| `G` | — | — | yes | yes | — |
|
||||
| `M` | — | — | yes | yes | — |
|
||||
| `C` | — | — | yes | yes | — |
|
||||
| `K` | — | — | — | yes | — |
|
||||
| `T` | — | — | — | yes | — |
|
||||
|
||||
Letter match is case-insensitive. `FocasAddress.PmcLetter` is carried
|
||||
as a string (not char) so the matrix can do ordinal-ignore-case
|
||||
comparison.
|
||||
|
||||
## PMC address-number ceiling
|
||||
|
||||
PMC addresses are byte-addressed on read + bit-addressed on write;
|
||||
`FocasAddress` carries the bit index separately, so these are byte
|
||||
ceilings.
|
||||
|
||||
| Series | Max byte | Notes |
|
||||
| --- | ---: | --- |
|
||||
| `Sixteen_i` | 999 | legacy |
|
||||
| `Zero_i_D` | 1999 | doubled since 16i |
|
||||
| `Zero_i_F` family | 9999 | |
|
||||
| `Thirty_i` family | 59999 | highest density |
|
||||
| `PowerMotion_i` | 1999 | |
|
||||
|
||||
## Error surface
|
||||
|
||||
When a tag fails validation, `FocasDriver.InitializeAsync` throws
|
||||
`InvalidOperationException` with a message of the form:
|
||||
|
||||
```
|
||||
FOCAS tag '<name>' (<address>) rejected by capability matrix: <reason>
|
||||
```
|
||||
|
||||
`<reason>` is the verbatim string from `FocasCapabilityMatrix.Validate`
|
||||
and always names the series + the documented limit so the operator
|
||||
can either raise the limit (if wrong) or correct the CNC series they
|
||||
declared (if mismatched). Sample:
|
||||
|
||||
```
|
||||
FOCAS tag 'X_axis_macro_ext' (MACRO:50000) rejected by capability
|
||||
matrix: Macro variable #50000 is outside the documented range
|
||||
[0, 9999] for Zero_i_F.
|
||||
```
|
||||
|
||||
## How this matrix stays honest
|
||||
|
||||
- Every row is covered by a parameterized test in
|
||||
[`FocasCapabilityMatrixTests.cs`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityMatrixTests.cs)
|
||||
— 46 cases across macro / parameter / PMC-letter / PMC-number
|
||||
boundaries + unknown-series permissiveness + rejection-message
|
||||
content + case-insensitivity.
|
||||
- Widening or narrowing a range in the matrix without updating this
|
||||
doc will fail a test, because the theories cite the specific row
|
||||
they reflect in their `InlineData`.
|
||||
- The matrix is not comprehensive — it encodes only the subset of
|
||||
FOCAS surface the driver currently exposes (Macro / Parameter /
|
||||
PMC). When the driver gains a new capability (e.g. tool management,
|
||||
alarm history), add its series-specific range tables here + matching
|
||||
tests at the same time.
|
||||
|
||||
## Follow-up
|
||||
|
||||
This validation closes the cheap half of the FOCAS hardware-free
|
||||
stability gap — config errors now fail at load instead of per-read.
|
||||
The expensive half is Tier-C process isolation so that a crashing
|
||||
`Fwlib32.dll` doesn't take the main OPC UA server down with it. See
|
||||
[`docs/v2/implementation/focas-isolation-plan.md`](implementation/focas-isolation-plan.md)
|
||||
for that plan (task #220).
|
||||
163
docs/v2/implementation/focas-isolation-plan.md
Normal file
163
docs/v2/implementation/focas-isolation-plan.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# FOCAS Tier-C isolation — plan for task #220
|
||||
|
||||
> **Status**: DRAFT — not yet started. Tracks the multi-PR work to
|
||||
> move `Fwlib32.dll` behind an out-of-process host, mirroring the
|
||||
> Galaxy Tier-C split in [`phase-2-galaxy-out-of-process.md`](phase-2-galaxy-out-of-process.md).
|
||||
>
|
||||
> **Pre-reqs shipped** (this PR): version matrix + pre-flight
|
||||
> validation + unit tests. Those close the cheap half of the
|
||||
> hardware-free stability gap. Tier-C closes the expensive half.
|
||||
|
||||
## Why isolate
|
||||
|
||||
`Fwlib32.dll` is a proprietary Fanuc library with no source, no
|
||||
symbols, and a documented habit of crashing the hosting process on
|
||||
network errors, malformed responses, and during handle recycling.
|
||||
Today the FOCAS driver runs in-process with the OPC UA server —
|
||||
a crash inside the Fanuc DLL takes every driver down with it,
|
||||
including ones that have nothing to do with FOCAS. Galaxy has the
|
||||
same class of problem and solved it with the Tier-C pattern (host
|
||||
service + proxy driver + named-pipe IPC); FOCAS should follow that
|
||||
playbook.
|
||||
|
||||
## Topology (target)
|
||||
|
||||
```
|
||||
+-------------------------------------+ +--------------------------+
|
||||
| OtOpcUa.Server (.NET 10 x64) | | OtOpcUaFocasHost |
|
||||
| | pipe | (.NET 4.8 x86 Windows |
|
||||
| ZB.MOM.WW.OtOpcUa.Driver.FOCAS | <-----> | service) |
|
||||
| - FocasProxyDriver (in-proc) | | |
|
||||
| - supervisor / respawn / BackPr | | Fwlib32.dll + session |
|
||||
| | | handles + STA thread |
|
||||
+-------------------------------------+ +--------------------------+
|
||||
```
|
||||
|
||||
Why .NET 4.8 x86 for the host: `Fwlib32.dll` ships as 32-bit only.
|
||||
The Galaxy.Host is already .NET 4.8 x86 for the same reason
|
||||
(MXAccess COM bitness), so the NSSM wrapper pattern transfers
|
||||
directly.
|
||||
|
||||
## Three new projects
|
||||
|
||||
| Project | TFM | Role |
|
||||
| --- | --- | --- |
|
||||
| `ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared` | `netstandard2.0` | MessagePack DTOs — `FocasReadRequest`, `FocasReadResponse`, `FocasSubscribeRequest`, `FocasPmcBitWriteRequest`, etc. Same assembly referenced by .NET 10 + .NET 4.8 so the wire format stays identical. |
|
||||
| `ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host` | `net48` x86 | Windows service. Owns the Fwlib32 session handles + STA thread + handle-recycling loop. Pipe server + per-call auth (same ACL + caller SID + shared secret pattern as Galaxy.Host). |
|
||||
| `ZB.MOM.WW.OtOpcUa.Driver.FOCAS` (existing) | `net10.0` | Collapses to a proxy that forwards each `IReadable` / `IWritable` / `ISubscribable` call over the pipe. `FocasCapabilityMatrix` + `FocasAddress` stay here — pre-flight runs before any IPC. |
|
||||
|
||||
## Supervisor responsibilities (in the Proxy)
|
||||
|
||||
Mirrors Galaxy.Proxy 1:1:
|
||||
|
||||
1. Start the Host process on first `InitializeAsync` (NSSM-wrapped
|
||||
service in production, direct spawn in dev) + heartbeat every
|
||||
5s.
|
||||
2. If heartbeat misses 3× in a row, fan out `BadCommunicationError`
|
||||
to every subscription and respawn with exponential backoff
|
||||
(1s / 2s / 4s / max 30s).
|
||||
3. Crash-loop circuit breaker: 5 respawns in 60s → drop to
|
||||
`BadDeviceFailure` steady state until operator resets.
|
||||
4. Post-mortem MMF: on Host exit, Host writes its last-N operations
|
||||
+ session state to an MMF the Proxy reads to log context.
|
||||
|
||||
## IPC surface (approximate)
|
||||
|
||||
Every `FocasDriver` method that today calls into Fwlib32 directly
|
||||
becomes an `ExecuteAsync` call with a typed request:
|
||||
|
||||
| Today (in-process) | Tier-C (IPC) |
|
||||
| --- | --- |
|
||||
| `FocasTagReader.Read(tag)` | `client.Execute(new FocasReadRequest(session, address))` |
|
||||
| `FocasTagWriter.Write(tag, value)` | `client.Execute(new FocasWriteRequest(...))` |
|
||||
| `FocasPmcBitRmw.Write(tag, bit, value)` | `client.Execute(new FocasPmcBitWriteRequest(...))` — RMW happens in Host so the critical section stays on one process |
|
||||
| `FocasConnectivityProbe.ProbeAsync` | `client.Execute(new FocasProbeRequest())` |
|
||||
| `FocasSubscriber.Subscribe(tags)` | `client.Execute(new FocasSubscribeRequest(tags))` — Host owns the poll loop + streams changes back as `FocasDataChangedNotification` over the pipe |
|
||||
|
||||
Subscription streaming is the non-obvious piece: the Host polls on
|
||||
its own timer + pushes change notifications so the Proxy doesn't
|
||||
round-trip per poll. Matches `Driver.Galaxy.Host` subscription
|
||||
forwarding.
|
||||
|
||||
## PR sequence (proposed)
|
||||
|
||||
1. **PR A — shared contracts**
|
||||
Create `Driver.FOCAS.Shared` with the MessagePack DTOs. No
|
||||
behaviour change. ~200 LOC + round-trip tests for each DTO.
|
||||
2. **PR B — Host project skeleton**
|
||||
Create `Driver.FOCAS.Host` .NET 4.8 x86 project, NSSM wrapper,
|
||||
pipe server scaffold with the same ACL + caller-SID + shared
|
||||
secret plumbing as Galaxy.Host. No Fwlib32 wiring yet — returns
|
||||
`NotImplemented` for everything. ~400 LOC.
|
||||
3. **PR C — Move Fwlib32 calls into Host**
|
||||
Move `FocasNativeSession`, `FocasTagReader`, `FocasTagWriter`,
|
||||
`FocasPmcBitRmw` + the STA thread into the Host. Proxy forwards
|
||||
over IPC. This is the biggest PR — probably 800-1500 LOC of
|
||||
move-with-translation. Existing unit tests keep passing because
|
||||
`IFocasTagFactory` is the DI seam the tests inject against.
|
||||
4. **PR D — Supervisor + respawn**
|
||||
Proxy-side heartbeat + respawn + crash-loop circuit breaker +
|
||||
BackPressure fan-out on Host death. ~500 LOC + chaos tests.
|
||||
5. **PR E — Post-mortem MMF + operational glue**
|
||||
MMF writer in Host, reader in Proxy. Install scripts for the
|
||||
new `OtOpcUaFocasHost` Windows service. Docs. ~300 LOC.
|
||||
|
||||
Total estimate: 2200-3200 LOC across 5 PRs. Consistent with Galaxy
|
||||
Tier-C but narrower since FOCAS has no Historian + no alarm
|
||||
history.
|
||||
|
||||
## Testing without hardware
|
||||
|
||||
Same constraint as today: no CNC, no simulator. The isolation work
|
||||
itself is verifiable without Fwlib32 actually being called:
|
||||
|
||||
- **Pipe contract**: PR A's MessagePack round-trip tests cover every
|
||||
DTO.
|
||||
- **Supervisor**: PR D uses a `FakeFocasHost` stub that can be told
|
||||
to crash, hang, or miss heartbeats. The supervisor's respawn +
|
||||
circuit-breaker behaviour is fully testable against the stub.
|
||||
- **IPC ACL + auth**: reuse the Galaxy.Host's existing test harness
|
||||
pattern — negative tests attempt to connect as the wrong user and
|
||||
assert rejection.
|
||||
- **Fwlib32 integration itself**: still untestable without hardware.
|
||||
When a real CNC becomes available, the smoke tests already
|
||||
scaffolded in `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/`
|
||||
run against it via `FOCAS_ENDPOINT`.
|
||||
|
||||
## Decisions to confirm before starting
|
||||
|
||||
- **Sharing transport code with Galaxy.Host** — should the pipe
|
||||
server + ACL + shared-secret + MMF plumbing go into a common
|
||||
`Core.Hosting.Tier-C` project both hosts reference? Probably yes;
|
||||
deferred until PR B is drafted because the right abstraction only
|
||||
becomes visible after two uses.
|
||||
- **Handle-recycling cadence** — Fwlib32 session handles leak
|
||||
memory over weeks per the Fanuc-published defect list. Galaxy
|
||||
recycles MXAccess handles on a 24h timer; FOCAS should mirror but
|
||||
the trigger point (idle vs scheduled) needs operator input.
|
||||
- **Per-CNC Host process vs one Host serving N CNCs** — one-per-CNC
|
||||
isolates blast radius but scales poorly past ~20 machines; shared
|
||||
Host scales but one bad CNC can wedge the lot. Start with shared
|
||||
Host + document the blast-radius trade; revisit if operators hit
|
||||
it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Simulator work. `open_focas` + other OSS FOCAS simulators are
|
||||
untested + not maintained; not worth chasing vs. waiting for real
|
||||
hardware.
|
||||
- Changing the public `FocasDriverOptions` shape beyond what
|
||||
already shipped (the `Series` knob). Operator config continues to
|
||||
look the same after the split — the Tier-C topology is invisible
|
||||
from `appsettings.json`.
|
||||
- Historian / long-term history integration. FOCAS driver doesn't
|
||||
implement `IHistoryProvider` + there's no plan to add it.
|
||||
|
||||
## References
|
||||
|
||||
- [`docs/v2/implementation/phase-2-galaxy-out-of-process.md`](phase-2-galaxy-out-of-process.md)
|
||||
— the working Tier-C template this plan follows.
|
||||
- [`docs/drivers/FOCAS-Test-Fixture.md`](../../drivers/FOCAS-Test-Fixture.md)
|
||||
— what's covered today + what stays blocked on hardware.
|
||||
- [`docs/v2/focas-version-matrix.md`](../focas-version-matrix.md) —
|
||||
the capability matrix that pre-flights configs before IPC runs.
|
||||
Reference in New Issue
Block a user