# FOCAS Driver Getting-started guide for the FANUC FOCAS2 driver. This is the short path — for the exhaustive per-node mapping read [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md), for the test-harness map read [FOCAS-Test-Fixture.md](FOCAS-Test-Fixture.md). ## What it talks to FANUC CNCs (0i-D / 0i-F / 0i-MF / 0i-TF / 16i / 30i / 31i / 32i / Power Motion i) over the proprietary FOCAS2 protocol on TCP port 8193. The wire is spoken directly by the pure-managed [`Focas.Wire`](https://github.com/Ladder99/focas-mock) client — no Fwlib64.dll, no P/Invoke, no out-of-process isolation needed. OtOpcUa is **read-only** against FOCAS; all reads go over the native wire protocol using the documented command IDs. Writes return `BadNotWritable` by design. ## Project split | Project | Target | Role | |---------|--------|------| | `src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/` | net10.0 | In-process driver — hosts `WireFocasClient` which speaks FOCAS2 over TCP directly | Previous `Driver.FOCAS.Host` / `Driver.FOCAS.Shared` Tier-C split has been retired — the managed wire client removes the native-crash blast radius that justified the out-of-process service. ## Minimum deployment Register the driver instance in the main server's `appsettings.json`. No separate service, no DLL deployment, no shared-secret handshake: ```jsonc "Drivers": { "focas-cnc-1": { "Type": "FOCAS", "Config": { "Backend": "wire", "Devices": [ { "HostAddress": "focas://10.20.30.40:8193", "Series": "ThirtyOne_i" } ], "Tags": [ { "Name": "Mode", "DeviceHostAddress": "focas://10.20.30.40:8193", "Address": "PARAM:3402", "DataType": "Int32", "Writable": false }, { "Name": "SpndLoad", "DeviceHostAddress": "focas://10.20.30.40:8193", "Address": "MACRO:500", "DataType": "Float64", "Writable": false } ] } } } ``` The main server opens two TCP sockets per configured device and speaks the FOCAS2 binary protocol directly. No local privileged components, no platform bitness constraint — the driver runs on every host OtOpcUa runs on. ## Address forms | Form | Example | Meaning | |------|---------|---------| | `X0.0` / `R100` / `R100.3` | PMC bit or byte | Letter + number; optional `.bit` for bit access | | `PARAM:1815` / `PARAM:1815/0` | CNC parameter | Number + optional axis index | | `MACRO:500` | Custom macro variable | System / user macro variable number | Addresses are validated against the per-device `Series` at `InitializeAsync` — a config referencing a number outside the documented range for that series fails at load time with an error message naming the limit. See [`docs/v2/focas-version-matrix.md`](../v2/focas-version-matrix.md) for the authoritative range table. ## Backend selection The driver picks its client from `Config.Backend`: | Value | Client | Use it for | |-------|--------|------------| | `wire` (default) | `WireFocasClient` | Production — pure-managed FOCAS2 over TCP | | `unimplemented` / `none` / `stub` | `UnimplementedFocasClientFactory` | Scaffolding a DriverInstance row before the CNC endpoint is reachable | Previous backends (`fwlib`, `fwlib32`, `ipc`) have been retired along with `Driver.FOCAS.Host` and the Fwlib P/Invoke path. Configs that still reference them will throw at startup with a message pointing here. ## Capability surface | Capability | Wire path | Notes | |------------|-----------|-------| | `IReadable` | `ReadAsync` → `cnc_rdpmcrng` / `cnc_rdparam` / `cnc_rdmacro` | One TCP request/response per read; `Focas.Wire` serializes requests on socket 2 internally | | `IWritable` | returns `BadNotWritable` | OtOpcUa is read-only against FOCAS by design — no `cnc_wrparam` / `pmc_wrpmcrng` / `cnc_wrmacro` path is implemented | | `ITagDiscovery` | `DiscoverAsync` | Emits `FOCAS/{device}/{tag}` folders per configured device | | `ISubscribable` | polled via shared `PollGroupEngine` | FOCAS has no push model — subscriptions turn into per-tag polling groups | | `IHostConnectivityProbe` | periodic `cnc_rdcncstat` | Probe cadence is `Probe.Interval`; transitions fire `OnHostStatusChanged` | | `IPerCallHostResolver` | lookup in `_tagsByName` | Each call routes to the device of the referenced tag | | `IAlarmSource` | polled `cnc_rdalmmsg2` via `FocasAlarmProjection` | Opt-in — set `AlarmProjection.Enabled=true`; diffs `(AlarmNumber, Type)` between ticks | Ack is a no-op — FANUC clears alarms on its own once the underlying condition resolves, so `AcknowledgeAsync` swallows the batch rather than surfacing `BadNotSupported`. ## Fixed node tree Enable a pre-defined hierarchy of CNC nodes populated automatically from `cnc_sysinfo` + `cnc_rdaxisname` + `cnc_rddynamic2` + related FWLIB calls, so operators get an out-of-the-box view of identity / axes / program / timers without declaring per-address tags. ```jsonc "Config": { "Devices": [ ... ], "Tags": [ ... ], "FixedTree": { "Enabled": true, "PollInterval": "00:00:00.250", // fast — per-axis dynamic reads "ProgramPollInterval": "00:00:01", // medium — program + mode changes "TimerPollInterval": "00:00:30" // slow — cumulative counters } } ``` What gets populated (all under `FOCAS/{deviceHostAddress}/`): | Subtree | Nodes | Source call | |---------|-------|-------------| | `Identity/` | `SeriesNumber`, `Version`, `MaxAxes`, `CncType`, `MtType`, `AxisCount` | `cnc_sysinfo` once at bootstrap | | `Axes/{name}/` | `AbsolutePosition`, `MachinePosition`, `RelativePosition`, `DistanceToGo`, `ServoLoad` — one folder per discovered axis | `cnc_rdaxisname` once + `cnc_rddynamic2` + `cnc_rdsvmeter` per tick | | `Axes/FeedRate/Actual`, `Axes/SpindleSpeed/Actual` | Current feed + spindle RPM | `cnc_rddynamic2` | | `Spindle/{name}/` | `Load` (percentage), `MaxRpm` — one folder per discovered spindle | `cnc_rdspdlname` once + `cnc_rdspload` + `cnc_rdspmaxrpm` | | `Program/` | `Name` (filename), `ONumber`, `Number`, `MainNumber`, `Sequence`, `BlockCount` | `cnc_exeprgname2` + `cnc_rdblkcount` + cached `cnc_rddynamic2` | | `OperationMode/` | `Mode` (int), `ModeText` ("AUTO", "MDI", "EDIT", …) | `cnc_rdopmode` | | `Timers/` | `PowerOnSeconds`, `OperatingSeconds`, `CuttingSeconds`, `CycleSeconds` | `cnc_rdtimer` × 4 | ### Per-series node suppression The driver probes each optional call once at bootstrap. If the target CNC returns `EW_FUNC` / `EW_NOOPT` / `EW_VERSION` on the wire, the corresponding subtree is **not emitted** — the operator doesn't see nodes that will only ever return `BadDeviceFailure`. Capability suppression covers `Spindle/`, `Program/` + `OperationMode/`, `Timers/`, and per-axis `ServoLoad` independently. Identity + `Axes/*` position reads (which every Fanuc CNC supports) are always emitted. Position values are scaled integers (matching FOCAS's convention). The managed side exposes them as `Float64` OPC UA nodes; a future `cnc_getfigure` integration will add per-axis decimal scaling. Until then, treat the raw integer as the value the CNC reports and scale on the client side if decimal precision matters. **Still user-authored**: `PARAM:6711`, `MACRO:500`, `R100` etc. — specific numbers whose meaning is MTB-specific. Those go under the device folder alongside the fixed subtree. ## Alarm projection Alarm surfacing is **disabled by default** because the polling cost is wasted on sites that don't consume CNC alarms. Opt in per driver instance: ```jsonc "Config": { "Devices": [ ... ], "Tags": [ ... ], "AlarmProjection": { "Enabled": true, "PollInterval": "00:00:02" } } ``` Every alarm transition fires `OnAlarmEvent` with: - `SourceNodeId` = the device host address (FOCAS has no per-node alarm model; the CNC exposes a single flat active-alarm list per session) - `ConditionId` = `"{host}#{Type}:{AlarmNumber}"` - `AlarmType` = projected from FANUC's `ALM_TYPE_*` (e.g. `Overtravel`, `Servo`, `Parameter`, `MacroAlarm`) - `Severity` = Overtravel / Servo / PulseCode → `Critical`; Parameter / Macro → `Medium`; everything else → `High` Cleared alarms fire a second event with `" (cleared)"` appended to the message so downstream consumers can ignore the clear if they only care about raises. ## Handle recycling FANUC CNCs have a finite FWLIB handle pool (~5–10 concurrent connections) and certain series have documented handle-leak bugs that manifest after long uptime. The driver can proactively close + reopen each device's session on a cadence to release its slot back to the pool: ```jsonc "Config": { "Devices": [ ... ], "HandleRecycle": { "Enabled": true, "Interval": "01:00:00" } } ``` Disabled by default — a healthy CNC + driver doesn't need it. Enable when field experience shows handle exhaustion. Typical tuning: 30 min for sites running multiple OtOpcUa instances against the same CNC (they share the pool); 6 h for a single-client deployment. Reads / writes during recycle simply wait for the reconnect rather than failing — worst case, an operator sees a brief read latency spike once per cadence. ## Testing - **Unit tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/` cover the driver surface via `FakeFocasClient`. Includes the alarm-projection raise / clear diffing tests. - **Integration tests** — `tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/` hold the Docker simulator scaffold; see [`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md) for what the simulator emits vs. real CNC behaviour. - **E2E script** — `scripts/e2e/test-focas.ps1` stages Host + Proxy + a real CNC (or the simulator) and exercises connect → read → write → subscribe round-trips. See [`docs/drivers/FOCAS-Test-Fixture.md`](FOCAS-Test-Fixture.md) for the coverage map. ## Troubleshooting | Symptom | Likely cause | Fix | |---------|--------------|-----| | `BadCommunicationError` on every read | CNC unreachable on TCP:8193 | Check firewall / LAN reachability; FOCAS Ethernet option must be licensed on the CNC side | | Every read returns `BadNotWritable` on writes | Expected — OtOpcUa is read-only against FOCAS | If you actually need writes, open a feature request — the driver's managed wire client doesn't expose the write commands | | `BadOutOfRange` on reads for a macro/parameter | Config address outside the declared `Series` range | Check `docs/v2/focas-version-matrix.md` — either fix the address or widen the `Series` | | Alarm events never fire | `AlarmProjection.Enabled` left at default (false) | Set it to `true` in the driver config | ## Further reading - [`docs/v2/driver-specs.md §7`](../v2/driver-specs.md) — full OPC UA node mapping, pre-defined tag set, per-API notes - [`docs/v2/focas-version-matrix.md`](../v2/focas-version-matrix.md) — per-series macro / parameter / PMC range table - [`docs/v2/implementation/focas-wire-protocol.md`](../v2/implementation/focas-wire-protocol.md) — captured FOCAS2 wire semantics (magic prefix, handshake, command-id table) - [upstream `Focas.Wire`](https://github.com/Ladder99/focas-mock/tree/main/dotnet/Focas.Wire) — the managed client implementation OtOpcUa consumes as a NuGet dependency