docs(phase4): Modbus Int64/UInt64, FOCAS fail-fast+scaling, Historian Total+dead-letter cap

This commit is contained in:
Joseph Doherty
2026-06-16 05:47:04 -04:00
parent 88d9e3b1a8
commit f9b38c0a61
4 changed files with 58 additions and 8 deletions
+7 -1
View File
@@ -111,7 +111,12 @@ collision waits out the file lock instead of failing fast.
4. The remaining batch is handed to `IAlarmHistorianWriter.WriteBatchAsync`, and
each outcome is applied in one transaction: `Ack` deletes the row,
`PermanentFail` flips its `DeadLettered` flag, `RetryPlease` bumps its attempt
count and leaves it queued.
count and leaves it queued. A row whose `AttemptCount` has reached the configured
**`MaxAttempts`** cap (default 10) is dead-lettered automatically on the next drain
tick rather than retried — this breaks infinite retry loops for poison events whose
payload the historian will always reject (e.g. a malformed alarm record that triggers
a permanent SDK error on every attempt). The dead-lettered row remains inspectable
via `RetryDeadLettered()` for the configured retention window.
5. The timer re-arms its next due-time to `max(tickInterval, currentBackoff)`.
**Backoff ladder** (applied to the timer's next due-time, so a historian outage
@@ -196,6 +201,7 @@ When `Enabled` is `false` (the default), `AddAlarmHistorian` registers
| `DatabasePath` | string | — | Absolute path to the SQLite queue file. Created on first use (WAL mode). Required when `Enabled`. |
| `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. |
| `BatchSize` | int | `100` | Max rows per drain cycle handed to `IAlarmHistorianWriter.WriteBatchAsync`. |
| `MaxAttempts` | int | `10` | Maximum delivery attempts before a poison (perpetually-retrying) row is dead-lettered automatically. Must be > 0. |
| `AlarmHistorian:Host` | string | `localhost` | DNS name or IP of the machine running the historian sidecar. |
| `AlarmHistorian:Port` | int | `32569` | TCP port the sidecar listens on (`OTOPCUA_HISTORIAN_TCP_PORT`). |
| `AlarmHistorian:UseTls` | bool | `false` | Wrap the TCP stream in TLS before the Hello handshake. |
+21 -2
View File
@@ -154,6 +154,23 @@ are disposed when the session closes). Resuming an unknown / evicted / released
`BadContinuationPointInvalid`. `releaseContinuationPoints` drops the stored cursors without reading
data.
### Total aggregate derivation
The OPC UA `Total` aggregate is **supported** over the Wonderware backend. Because the
Wonderware `AnalogSummary` query exposes no `Total` column, the value is derived client-side
using the time-integral identity:
> **Total = time-weighted Average × interval-seconds**
The wire request is issued with the `Average` column; each returned bucket's value is
multiplied by `interval.TotalSeconds` before the result is returned to the OPC UA client.
Bucket status codes and timestamps are preserved unchanged. Null (unavailable) Average
buckets produce a null Total (`BadNoData` downstream) — the scaling is not applied.
This derivation is exact for piecewise-constant (step) signals. For continuously varying
signals it is an approximation identical to the one Wonderware would apply internally, so
the result is consistent with what AVEVA Historian reports for the same window.
### Known limitations
- **Processed and AtTime are single-shot** (no continuation points). Unlike Raw, neither
@@ -225,9 +242,11 @@ otopcua-cli historyread \
-U reader -P password
```
Supported `--aggregate` values: `Average`, `Minimum`, `Maximum`, `Count`, `Start`, `End`,
`StandardDeviation` (aliases: `avg`, `min`, `max`, `stddev`/`stdev`, `first`, `last`).
Supported `--aggregate` values: `Average`, `Minimum`, `Maximum`, `Total`, `Count`, `Start`, `End`,
`StandardDeviation` (aliases: `avg`, `min`, `max`, `total`, `stddev`/`stdev`, `first`, `last`).
`--interval` is the processing interval in milliseconds (default 3600000 = 1 hour).
`Total` is derived client-side as time-weighted Average × interval-seconds (see "Total aggregate
derivation" above).
---
+29 -5
View File
@@ -78,6 +78,13 @@ The driver picks its client from `Config.Backend`:
| `wire` (default) | `WireFocasClient` | Production — pure-managed FOCAS2 over TCP |
| `unimplemented` / `none` / `stub` | `UnimplementedFocasClientFactory` | Scaffolding a DriverInstance row before the CNC endpoint is reachable |
**Fail-fast on `unimplemented` / `none` / `stub`:** a driver instance configured with any of
these backends now **faults immediately at `InitializeAsync`** with a clear error message rather
than reporting healthy and then failing on the first read/write/subscribe. The driver moves to
`Faulted` state and the error is visible on the Admin UI driver-status panel. This is intentional
— an operator who forgets to switch to `"wire"` before deploying to production sees a driver
fault at startup, not a phantom-healthy driver that silently rejects every request.
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.
@@ -140,11 +147,26 @@ 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.
Position values (`AbsolutePosition`, `MachinePosition`, `RelativePosition`, `DistanceToGo`)
are CNC-internal scaled integers exposed as `Float64` OPC UA nodes. The driver converts them
to engineering units using the per-device `PositionDecimalPlaces` config field (default `0`
= no scaling). When set to a positive integer *d*, each position value is divided by `10^d`
before publishing, so a CNC that reports millimetres × 1000 is corrected by setting
`PositionDecimalPlaces: 3`.
```jsonc
"Devices": [
{
"HostAddress": "focas://10.20.30.40:8193",
"Series": "ThirtyOne_i",
"PositionDecimalPlaces": 3 // 123456 → 123.456 mm
}
]
```
Auto-fetching the decimal-place count via `cnc_getfigure` is deferred (wire-gated). Until
that lands, the config field is the authoritative source — consult the MTB / machine
parameter sheets for the correct value. Negative values are clamped to `0`.
**Still user-authored**: `PARAM:6711`, `MACRO:500`, `R100` etc. — specific
numbers whose meaning is MTB-specific. Those go under the device folder
@@ -221,10 +243,12 @@ latency spike once per cadence.
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Driver faults immediately at startup with "unimplemented" in the error | `Backend` is `"unimplemented"` / `"none"` / `"stub"` | Change `Backend` to `"wire"` and supply the real CNC endpoint in `Devices[]` |
| `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 |
| Axis position values seem 1000× too large | `PositionDecimalPlaces` not set | Add `"PositionDecimalPlaces": 3` (or the MTB-specific value) to the device entry in `Devices[]` |
## Further reading
+1
View File
@@ -116,3 +116,4 @@ is reproduced in [docs/v2/driver-specs.md §2](../v2/driver-specs.md).
- **Wrong-endian readings are silently plausible.** A byte-order misconfiguration produces a wrong number, not a Bad quality code — surface byte-order mismatches as data-validation alerts, not status codes (see [docs/v2/driver-specs.md §2](../v2/driver-specs.md)).
- **`WriteOnChangeOnly` + write-only tags** — the suppression cache is only invalidated by a read that returns a divergent value. A tag that is never subscribed/polled never refreshes its cache entry, so a re-asserted value can be suppressed indefinitely. Subscribe every tag that needs deterministic re-writes, or leave the option off.
- **Auto-prohibited ranges** are visible via `GetAutoProhibitedRanges` and logged on first occurrence / on clear — use them to find protected register holes in a device's map.
- **Int64 / UInt64 OPC UA node DataType** — tags declared with `DataType: Int64` or `DataType: UInt64` advertise the correct OPC UA scalar type (`Int64` / `UInt64`) on their node. Values outside the 32-bit range are preserved end-to-end; the wire codec (4-register read/write) was already correct before this fix.