- docs/drivers/FOCAS.md and docs/v2/implementation/focas-wire-protocol.md pointed at focas-deployment.md and focas-simulator-plan.md, both of which were untracked drafts that have since been removed. Drop the refs (the wire-protocol companion now stands on its own; deployment guidance lives inline in the FOCAS driver doc). - Link the orphan v2 design docs from docs/README.md (multi-host dispatch, v2 release readiness, the historical lmx-followups tracker) and from modbus-test-plan.md (s7.md, mitsubishi.md per-family quirk catalogs, sibling to dl205.md). Surfaced by the doc audit; no content changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
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,
for the test-harness map read 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
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:
"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 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.
"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:
"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'sALM_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:
"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 viaFakeFocasClient. Includes the alarm-projection raise / clear diffing tests. - Integration tests —
tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/hold the Docker simulator scaffold; seedocs/v2/implementation/focas-wire-protocol.mdfor what the simulator emits vs. real CNC behaviour. - E2E script —
scripts/e2e/test-focas.ps1stages Host + Proxy + a real CNC (or the simulator) and exercises connect → read → write → subscribe round-trips. Seedocs/drivers/FOCAS-Test-Fixture.mdfor 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— full OPC UA node mapping, pre-defined tag set, per-API notesdocs/v2/focas-version-matrix.md— per-series macro / parameter / PMC range tabledocs/v2/implementation/focas-wire-protocol.md— captured FOCAS2 wire semantics (magic prefix, handshake, command-id table)- upstream
Focas.Wire— the managed client implementation OtOpcUa consumes as a NuGet dependency