Files
lmxopcua/docs/drivers/FOCAS.md
Joseph Doherty 33054c3275 docs: drop dangling FOCAS refs + link unreferenced v2 design docs
- 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>
2026-04-30 09:42:28 -04:00

11 KiB
Raw Blame History

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 ReadAsynccnc_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'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 (~510 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 teststests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/ cover the driver surface via FakeFocasClient. Includes the alarm-projection raise / clear diffing tests.
  • Integration teststests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests/ hold the Docker simulator scaffold; see docs/v2/implementation/focas-wire-protocol.md for what the simulator emits vs. real CNC behaviour.
  • E2E scriptscripts/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 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