diff --git a/docs/AlarmHistorian.md b/docs/AlarmHistorian.md index 5da3a023..4e9bc45b 100644 --- a/docs/AlarmHistorian.md +++ b/docs/AlarmHistorian.md @@ -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. | diff --git a/docs/Historian.md b/docs/Historian.md index 74f01acf..148bf0f5 100644 --- a/docs/Historian.md +++ b/docs/Historian.md @@ -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). --- diff --git a/docs/drivers/FOCAS.md b/docs/drivers/FOCAS.md index 14bc6117..cf9c17e2 100644 --- a/docs/drivers/FOCAS.md +++ b/docs/drivers/FOCAS.md @@ -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 diff --git a/docs/drivers/Modbus.md b/docs/drivers/Modbus.md index b15744ee..14e98aff 100644 --- a/docs/drivers/Modbus.md +++ b/docs/drivers/Modbus.md @@ -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.