# FOCAS deployment guide Per-driver runbook for deploying the FANUC FOCAS driver. See [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) for the per-feature reference and [`focas-version-matrix.md`](./focas-version-matrix.md) for the per-CNC-series capability surface. ## Operator config-knob cheat sheet | Knob | Where | Default | Notes | | --- | --- | --- | --- | | `Devices[].HostAddress` | `FocasDriverOptions.Devices` | — | `focas://{ip}[:{port}]` | | `Devices[].Series` | `FocasDriverOptions.Devices` | `Unknown` | Drives per-series range validation in `FocasCapabilityMatrix`. | | `Devices[].OverrideParameters` | `FocasDriverOptions.Devices` | `null` | MTB-specific parameter numbers for Feed/Rapid/Spindle/Jog overrides. `null` suppresses the `Override/` subtree. | | `Probe.Enabled` | `FocasDriverOptions.Probe` | `true` | Background reachability probe. | | `Probe.Interval` | `FocasDriverOptions.Probe` | `00:00:05` | Probe cadence. | | `FixedTree.ApplyFigureScaling` | `FocasDriverOptions.FixedTree` | `true` | Divide position values by 10^decimal-places (issue #262). | | **`AlarmProjection.Mode`** | **`FocasDriverOptions.AlarmProjection`** | **`ActiveOnly`** | **`ActiveOnly` keeps today's behaviour. `ActivePlusHistory` polls `cnc_rdalmhistry` on connect + on `HistoryPollInterval` ticks (issue #267, plan PR F3-a).** | | **`AlarmProjection.HistoryPollInterval`** | **`FocasDriverOptions.AlarmProjection`** | **`00:05:00`** | **Cadence of the history poll. Operator dashboards run the default; high-frequency rigs can drop to 30 s.** | | **`AlarmProjection.HistoryDepth`** | **`FocasDriverOptions.AlarmProjection`** | **`100`** | **Most-recent-N ring-buffer entries pulled per poll. Hard-capped at `250` so misconfigured values can't blast the wire session.** | ## Sample `appsettings.json` snippet for `ActivePlusHistory` ```jsonc { "Drivers": { "FOCAS": { "Devices": [ { "HostAddress": "focas://10.0.0.5:8193", "Series": "Series30i" } ], "AlarmProjection": { "Mode": "ActivePlusHistory", "HistoryPollInterval": "00:05:00", "HistoryDepth": 100 } } } } ``` The history projection emits each unseen entry through `IAlarmSource.OnAlarmEvent` with `SourceTimestampUtc` set from the CNC's reported wall-clock — keep CNC clocks on UTC so the dedup key `(OccurrenceTime, AlarmNumber, AlarmType)` stays stable across DST transitions. ## Write safety — issue #269 (PARAM/MACRO, F4-b) + issue #270 (PMC, F4-c) The FOCAS driver supports `cnc_wrparam`, `cnc_wrmacro`, and `pmc_wrpmcrng` writes behind multiple independent opt-ins. A misdirected parameter write can put the CNC in a bad state; a misdirected PMC write can move motion or latch a feedhold. The runbook below MUST be followed before flipping any of the granular kill switches on. ### Operator pre-checks (every deployment, every change) 1. **CNC must be in MDI mode.** Most parameter writes fail with `EW_PASSWD` (surfaces as `BadUserAccessDenied`) unless the CNC is in MDI. The server-side write returns immediately with the access-denied status; no value reaches the wire. 2. **Parameter-write switch enabled on the CNC pendant.** Even in MDI mode protected parameters require the operator to physically enable the parameter-write switch. Without it `cnc_wrparam` returns `EW_PASSWD`. Plan PR F4-d will land an OPC UA-side unlock workflow; today the only path is the pendant. 3. **Verify each tag's address against the FANUC manual.** Ranges vary per CNC series; the [`focas-version-matrix`](./focas-version-matrix.md) capability matrix rejects out-of-range numbers at startup, but address-vs-meaning is the operator's job. 4. **Dry run with `Writable = true` but `Writes.AllowParameter = false`.** Staged opt-in catches mis-mapped tags: every PARAM write returns `BadNotWritable` until you flip the granular flag, so you can confirm the tag list before any wire write fires. ### PMC pre-checks (in addition to the above) — F4-c PMC writes have a higher blast radius than PARAM/MACRO writes because PMC is the ladder's working memory — bits in R/G/F/D directly drive servo enables, feedhold latches, and safety interlocks. Before flipping `Writes.AllowPmc` on: 1. **E-stop verified live + reachable.** The first PMC write of a session should be issued with the operator's hand on the e-stop. PMC writes bypass the ladder's normal MDI-mode protections; a misdirected bit can move motion the moment it lands on the wire. 2. **Machine in JOG mode (or equivalent low-energy mode).** Auto / MEM modes interpret PMC state immediately; JOG / MDI surface symptoms slowly enough that the e-stop is the recovery path. **Never issue the first PMC write of a deployment in Auto.** 3. **Audit the PMC tag list against the ladder print-out.** `R100.3` on one machine is "homing complete"; on another it's "feedhold released". The driver has no way to distinguish — the ladder source is the only ground truth. 4. **Bit writes are read-modify-write — see [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) "PMC bit-write read-modify-write semantics".** `pmc_wrpmcrng` is byte-addressed; the driver reads the parent byte first, masks the target bit, and writes the byte back. Concurrent ladder writes to the same byte create a small race window. Coordinate through a ladder-side handshake when this matters. 5. **Dry run with `Writable = true` but `Writes.AllowPmc = false`.** Same staged-opt-in pattern as PARAM/MACRO — confirm tag mapping before any PMC byte hits the wire. ### LDAP group requirements Per [`docs/security.md`](../security.md) the server-layer ACL maps `SecurityClassification` to LDAP groups. Post-F4-b: | Tag kind | LDAP group required | | --- | --- | | `PARAM:N` (writable) | **`WriteConfigure`** — heaviest write tier; matches commissioning roles | | `MACRO:N` (writable) | `WriteOperate` — standard HMI recipe / setpoint group | | PMC R/G/F (writable) | `WriteOperate` | | Read-only | `ReadOnly` | Per the `feedback_acl_at_server_layer` design note, the FOCAS driver declares the classification but does NOT enforce it; `DriverNodeManager` applies the gate before the driver's `WriteAsync` ever runs. A user without `WriteConfigure` who attempts a `PARAM:` write gets `BadUserAccessDenied` from the server with no driver-level audit entry — the OPC UA layer's audit log catches it. ### Audit-log expectations Every successful write produces: - An OPC UA AuditWriteEvent (server layer — see [`docs/security.md`](../security.md) "Audit logging"). - A FOCAS driver-level Serilog entry tagged `Driver=FOCAS DriverInstanceId=... TagName=... Address=... ResultStatus=...`. - A `Writes/LastWriteAt` and `Writes/LastWriteStatus` diagnostic counter refresh on the device's `Diagnostics/` fixed-tree node (planned; populated as F4-c lands). Failures to write (`BadUserAccessDenied`, `BadCommunicationError`, etc.) produce the same audit entries with the failure status code so a post-incident reviewer sees the same shape regardless of whether the write succeeded. **Audit PMC writes specifically.** Because PMC writes have the highest blast radius of the three write kinds, ops should set up a saved-search / dashboard query for `Driver=FOCAS` + `Address` matching the PMC letter prefixes (`R*`, `G*`, `F*`, `D*`, `Y*`, etc.) and review on the same cadence as ladder change reviews. A spike in PMC write rate or a write to an address outside the audited tag list is the leading indicator of a misconfigured client or compromised credential. ### Granular config example ```jsonc { "Drivers": { "FOCAS": { "Devices": [ { "HostAddress": "focas://10.0.0.5:8193", "Series": "Series30i" } ], "Writes": { "Enabled": true, "AllowMacro": true, // recipe / setpoint writes — operator role "AllowParameter": false, // commissioning only — keep locked except during planned work "AllowPmc": false // PMC writes — keep locked unless the deployment specifically needs them }, "Tags": [ { "Name": "Recipe.PartCount", "DeviceHostAddress": "focas://10.0.0.5:8193", "Address": "MACRO:500", "DataType": "Int32", "Writable": true, "WriteIdempotent": true }, { "Name": "MaxFeedrate", "DeviceHostAddress": "focas://10.0.0.5:8193", "Address": "PARAM:1815", "DataType": "Int32", "Writable": false /* keep read-only until commissioning window */ }, { "Name": "OperatorRequest", "DeviceHostAddress": "focas://10.0.0.5:8193", "Address": "R100.3", "DataType": "Bit", "Writable": false /* keep PMC read-only until ladder handshake reviewed */ } ] } } } ``` Flipping `AllowParameter` / `AllowPmc` on for the commissioning window (and back off afterward) is the recommended deployment cadence — the granular kill switches are lightweight runtime toggles, not config-DB redeploys. PMC in particular should default OFF in production and only flip on for windows where the ladder team has signed off on the write path. ## FOCAS password handling — issue #271 (F4-d) Some controllers (16i + certain 30i firmwares with parameter-protect on) gate `cnc_wrparam` and selected reads behind a connection-level password. The driver supports this via the `Password` field on `FocasDeviceOptions` which is emitted via `cnc_wrunlockparam` on connect and re-emitted on any `EW_PASSWD` read/write retry path. See [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) § "FOCAS password" for the driver-side behaviour; this section covers the deployment side. ### Storage in `appsettings.json` ```jsonc { "Drivers": { "Focas01": { "DriverConfigJson": { "Backend": "fwlib", "Series": "Sixteen_i", "Devices": [ { "HostAddress": "focas://10.0.0.5:8193", "Password": "1234" } ] } } } } ``` For dev environments, the password is materialised under `.local/focas-passwords.txt` (or whichever .local subkey the deployment team prefers); production deployments use the same secrets-store / KeyVault pattern the LDAP `Authentication.Ldap.Password` field follows. **The `.local/` directory is .gitignore'd** — this is the same posture as `.local/galaxy-host-secret.txt` and other dev secrets in this repo. ### No-log invariant The driver guarantees the password is **never logged**: 1. **`FocasDeviceOptions` ToString redaction.** The record overrides `PrintMembers` so any Serilog destructure of the device options renders `Password = ***` when the field is non-null. This catches the most common leak path — a structured-log statement that included `{@Device}` for diagnostic context. 2. **No password in exception messages.** `FwlibFocasClient.UnlockAsync` omits the password from its `InvalidOperationException` text — only the FWLIB error code (`EW_PASSWD`, `EW_HANDLE`, etc.) makes it through. 3. **Driver log line uses host only.** When unlock succeeds the driver updates `DriverHealth.StatusText` to `"FOCAS unlock applied for {host}"` — no password. 4. **CLI flag covered by the same choke point.** The `Driver.FOCAS.Cli --cnc-password` flag flows through `FocasDeviceOptions.Password`, so its redaction is identical to the server's. The PowerShell e2e harness (`scripts/e2e/test-focas.ps1 -CncPassword`) follows the same path. Any new logging surface that touches `FocasDeviceOptions` MUST continue to use the record's `ToString` (or otherwise omit `Password`). A code review checklist item: "no log statement contains `device.Options.Password` or `device.Password` directly." ### Password-rotation runbook When the CNC password rotates (operator team flipped a parameter-protect gate, or your security policy requires periodic rotation): 1. **Update the password on the controller** (CNC pendant or vendor's admin tool). The exact path varies by series — Fanuc service manual page reference depends on the MTB. 2. **Update `appsettings.json`** in place with the new value. - Production: bump the secrets-store entry that backs the `Devices[*].Password` config-DB column. Same workflow as rotating the LDAP service-account password. - Dev: update `.local/focas-passwords.txt` (or wherever the dev deployment sources the secret). 3. **Restart the OtOpcUa server** (or trigger a config-DB bump that forces driver reinitialise). The driver picks up the new password on the next `EnsureConnectedAsync` call. **No need to manually reconnect each device** — `cnc_wrunlockparam` emits on the next wire-call boundary. 4. **Verify**. The first wire call after restart logs `"FOCAS unlock applied for focas://{host}:{port}"` at info. A wrong password surfaces as `BadUserAccessDenied` on the next gated read or write. 5. **Audit.** OPC UA wrote-event entries (per [`audit-log-rules.md`](audit-log-rules.md)) cover the parameter/macro write paths. Password rotation itself is NOT logged beyond "unlock applied" — same posture as LDAP service-account rotation, where the password change is logged out-of-band by the IAM system. ### Cross-references - [`docs/Security.md`](../Security.md) — server-wide secrets handling + the same `.local/` pattern used for LDAP and the Galaxy.Host pipe secret. The FOCAS password follows the same posture. - [`docs/drivers/FOCAS.md`](../drivers/FOCAS.md) § "FOCAS password" — driver-side behaviour, EW_PASSWD retry semantics, status-code surface. - [`docs/v2/implementation/focas-wire-protocol.md`](implementation/focas-wire-protocol.md) § "cnc_wrunlockparam" — wire-frame layout for the password buffer.