Merge feat/830-heidenhain-followups
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Heidenhain LSV2 driver (read-only)
|
||||
|
||||
`DriverType = "Heidenhain"` (`DriverTypeNames.Heidenhain`) · projects `Driver.Heidenhain` (+ `.Contracts`) ·
|
||||
Gitea **#803**.
|
||||
Gitea **#803** (native alarms + rediscovery: **#831**; authoring UX: **#830**).
|
||||
|
||||
A native, **read-only** Heidenhain LSV2 client over TCP (default port **19000**) for iTNC/TNC controls. No
|
||||
Java/Python runtime and no vendor SDK (RemoTools/DNC) — a managed `System.Net.Sockets` client. It is a port of the
|
||||
@@ -21,9 +21,12 @@ Three independent layers, each sufficient on its own for its concern:
|
||||
manual-entry grid also fix the access level at Read — and again by the deploy gate `HeidenhainTagWritable`, so no
|
||||
node is ever advertised writable. A `"writable": true` TagConfig key is rejected by the tag reader and dropped by
|
||||
the typed editor. The same paths tie `Tag.DataType` to what the driver publishes (the catalog type for a fixed
|
||||
address — `Float64` ⇒ `Double` — or the binding's declared type): the modals default it from the address, the
|
||||
save path refuses a mismatch, and the deploy gate `HeidenhainTagDataTypeMismatch` catches anything older
|
||||
(one rule, `HeidenhainTagRules`).
|
||||
address — `Float64` ⇒ `Double` — or the binding's declared type; `Boolean` for an `ALARM:` condition): the tag
|
||||
modal and the manual-entry grid **lock** the access level and data type to the driver's values (#830 — the grid
|
||||
used to offer a choice it silently replaced on commit), the CSV import's staged review grid flags a ReadWrite or
|
||||
mistyped row **in the preview**, the save path refuses a mismatch, and the deploy gate
|
||||
`HeidenhainTagDataTypeMismatch` catches anything older. One rule (`HeidenhainTagRules`), reached by every AdminUI
|
||||
path through the driver-dispatched `RawTagRowRules` (no call site names a driver).
|
||||
3. **Payload-aware outbound allowlist** (`Lsv2ReadOnlyPolicy`). `Lsv2Codec.Frame` — the only frame builder — checks
|
||||
command **and** payload immediately before any byte is written. There is no raw-command API (`Lsv2Client`'s
|
||||
public surface is pinned by a reflection test) and no configuration knob that widens it.
|
||||
@@ -66,6 +69,7 @@ enums, so the numeric-enum form/factory trap cannot occur):
|
||||
| `Access.DataEnabled` | false | Opt-in DATA login, attempted only when a `DATA:` binding exists (iTNC only) |
|
||||
| `Access.InspectPassword` / `DncPassword` / `DataPassword` | — | **References only** (below) |
|
||||
| `Polling.StatusMs` / `CustomMs` / `SlowMs` | 1000 / 1000 / 10000 | each 100–3600000 |
|
||||
| `Polling.AlarmMs` | 2000 | 100–3600000; the alarm walk behind `ALARM:` conditions — only for a control carrying an `ALARM:` tag ([Alarm conditions](#alarm-conditions-alarm-gitea-831)) |
|
||||
| `Polling.MaxAlarms` | 64 | 1–256 |
|
||||
| `Polling.TimersEnabled` | **false** | Experimental selectors 30–34 |
|
||||
| `Reconnect.InitialDelayMs` / `MaxDelayMs` | 1000 / 30000 | doubling backoff, bounded |
|
||||
@@ -90,8 +94,9 @@ Host at their own machine. Anything outside the namespace is refused by **every*
|
||||
(`HeidenhainOptionsParser`, so the factory and the probe), the resolver **again at resolve time**
|
||||
(`HeidenhainCredentials`, fail-closed independently of earlier validation), the AdminUI **save path**
|
||||
(`RawTreeService` create/update driver + device refuse the config server-side via `DriverConfigSaveGate`, and the
|
||||
driver/device modals disable Save while it is invalid — so a literal never reaches the config database), and the
|
||||
deploy gate `HeidenhainCredentialPersisted`. Messages never echo the value.
|
||||
driver/device modals disable Save while it is invalid and show the refusal **once**, below the form — the typed
|
||||
forms report only ranges/endpoints, #830 — so a literal never reaches the config database), and the deploy gate
|
||||
`HeidenhainCredentialPersisted`. Messages never echo the value.
|
||||
|
||||
Resolved **at each login**, never held in options or session state. Fail-closed: an absent secret or unset
|
||||
variable fails the login rather than sending the reference text. An unusable **INSPECT** credential fails the
|
||||
@@ -147,6 +152,70 @@ truncates or wraps — otherwise `BadTypeMismatch`.
|
||||
|
||||
At most 256 PLC/DATA bindings per control (excess are skipped and reported on `/hosts`).
|
||||
|
||||
## Alarm conditions (`ALARM:`, Gitea #831)
|
||||
|
||||
The control exposes one flat active-alarm list (`R_RI 27` first, `28` next — class, group, number, text; only refusal
|
||||
57 ends it). Besides the `Alarms/*` typed arrays (unchanged), the list can drive **native OPC UA Part 9 conditions**:
|
||||
a raw tag whose address is an `ALARM:` selector **and** whose `TagConfig` carries the cross-driver `alarm` object is
|
||||
exactly one condition, `ConditionId` = the tag's RawPath (the key the host routes and fans on — raw device folder +
|
||||
every referencing equipment folder, per the v3 native-alarm model). Same opt-in-per-tag shape as FOCAS #647 and
|
||||
MTConnect #626.
|
||||
|
||||
```jsonc
|
||||
{ "address": "ALARM:CLASS=7", "dataType": "Boolean",
|
||||
"alarm": { "alarmType": "AlarmCondition", "severity": 800 } }
|
||||
```
|
||||
|
||||
| Address | Condition is active while… |
|
||||
|---|---|
|
||||
| `ALARM:*` | any alarm is active on the control (summary) |
|
||||
| `ALARM:OTHER` | an alarm is active that no **filter** selector on the same control claims |
|
||||
| `ALARM:CLASS=<r>[/GROUP=<r>][/NUMBER=<r>]` (any subset, any order, each key once) | an alarm matches every named field |
|
||||
|
||||
`<r>` is `n` or `lo..hi` (inclusive; `..` because numbers can be negative — a real iTNC 530 reports `-1`). CLASS/GROUP
|
||||
are 0–65535 (wire uint16), NUMBER is int32. **No semantic class/group names**: their meaning is undocumented in the
|
||||
reference (pyLSV2 / the Java port keep them raw), so nothing is guessed — observe `Alarms/Classes` / `Alarms/Groups` or
|
||||
a condition message, then filter. One parser (`HeidenhainAlarmSelector`) backs the editor, the deploy inspector and the
|
||||
driver.
|
||||
|
||||
- **Opt-in is the tag — nothing is on by default.** An existing config gains no condition and loses nothing; the
|
||||
`Alarms/*` arrays keep publishing. This follows FOCAS #647 (`AlarmProjection.Enabled` became a no-op: the authored
|
||||
tag is the switch) and MTConnect #626 (opt-in because alarm tags leave the value subscription). There is no
|
||||
driver-level enable flag.
|
||||
- **Pairing is enforced** (`HeidenhainTagDefinitionFactory`, so the editor, the save path, CSV and the deploy inspector
|
||||
agree): an `ALARM:` address without the `alarm` object, or an `alarm` object on any other address, is refused. The
|
||||
tag's data type is fixed at `Boolean`; it is never a value (`ReadAsync` answers `BadNotSupported`), never in a data
|
||||
subscription, never writable (the `HeidenhainTagWritable` gate still applies). The typed editor seeds
|
||||
`{"alarmType":"AlarmCondition","severity":700}` when you pick an `ALARM:` selector and drops it when you leave one.
|
||||
- **Identity** = (class, group, number, **text**) — the number alone is not unique (`-1` above). **Displayed alarm** is
|
||||
deterministic (ascending class, group, number, text), never wire order. Message:
|
||||
`-1 Run test of cutout channels (class 7, group 4) [+N more]`.
|
||||
- **Transitions** (the FOCAS #647 rules): inactive → active `Raise`; a key not in the told set appears, or the displayed
|
||||
alarm leaves while others remain → `Retrigger` (un-acknowledges — a statement the operator has not seen); a
|
||||
non-displayed member leaves → nothing; nothing matches → `Clear`.
|
||||
- **Severity**: an authored integer `alarm.severity` is the exact Part 9 severity; otherwise High (700). No
|
||||
class→severity table is invented.
|
||||
- **Diffed against what the server was TOLD**, recorded only while a handler is attached; the record lives on the
|
||||
driver instance (survives a reconnect / same-set re-subscribe / in-place re-init). A deploy respawns the driver and its
|
||||
first walk re-states still-active alarms with `Raise`, which the host treats as a re-statement (ack kept,
|
||||
`NativeAlarmProjector` #647).
|
||||
- **A failed walk is not "no alarms."** A refusal (e.g. 23/49 access denied → `BadUserAccessDenied`, 22/24 →
|
||||
`BadNotSupported`), a list overflow (`MaxAlarms` → `BadOutOfRange`), a decode error, telemetry disabled, or comms loss
|
||||
(`BadNoCommunication`) keeps every condition's Active/Acked state and annotates its **quality** once (quality-only
|
||||
event, no `/alerts` row, one Warning per episode); the next good walk lifts it to Good before diffing. Only a down
|
||||
session degrades health (the existing rule) — a refusal never does.
|
||||
- **Acknowledgement is local-only.** LSV2 has no acknowledge request and the read-only allowlist is **not** widened:
|
||||
`AcknowledgeAsync` is a no-op success, so the Part 9 Acknowledge / AddComment the host routes to it is applied (and
|
||||
pair-replicated) server-side — the FOCAS / MTConnect posture. Confirm, the shelve methods and Enable/Disable are
|
||||
refused `BadNotSupported` by the host for every native condition. The control clears an alarm when its cause is reset;
|
||||
an operator ack never reaches it.
|
||||
- **Traffic:** the walk is the same allowlisted `R_RI 27/28` the arrays use, on the control's single acquisition loop
|
||||
every `Polling.AlarmMs`, and only for a control that carries an `ALARM:` tag. No new command type exists
|
||||
(`HeidenhainAlarmTests` asserts the wire log is a subset of the read set and `Lsv2ReadOnlyPolicy` is unchanged).
|
||||
- **Authoring:** the `/raw` tag modal (`Native alarm` section + the typed editor), manual entry (the editor seeds the
|
||||
alarm object) and CSV (`Alarm.AlarmType` / `Alarm.Severity` columns). The browse picker offers only catalog leaves.
|
||||
- E2E: `Runtime.Tests/Drivers/HeidenhainNativeAlarmEndToEndTests` (real driver → simulator → real `DriverHostActor`).
|
||||
|
||||
## Quality
|
||||
|
||||
| Condition | Value status / capability |
|
||||
@@ -183,6 +252,11 @@ overrides, axes, tool, the whole alarm walk) and aliased custom addresses are re
|
||||
- Subscriptions: the initial push and later publications are ordered under one lock, so a subscriber never
|
||||
receives an older value after a newer one. A re-init disposes every control loop first; a late start of a
|
||||
disposed loop is a no-op.
|
||||
- **Rediscovery signal (`IRediscoverable`, #831):** every session already reads `R_VR` at connect; when a control
|
||||
reconnects reporting a **different** identity (model, NC/PLC software version or option string) than the previous
|
||||
session on this driver instance, the driver raises `OnRediscoveryNeeded` once with both identities in the reason —
|
||||
the `/hosts` "re-browse" chip. Advisory only (drivers.md §Change Detection): nothing is re-materialised. The first
|
||||
session records the baseline; a deploy respawn forgets it (the next session is a new baseline). No extra traffic.
|
||||
- **Health:** `Degraded` only while a control's session is down (connect failing / lost), with
|
||||
`LastSuccessfulRead` frozen — the conjunction the #527 comms-loss watchdog needs. Per-tag refusals, denials and
|
||||
decode errors are quality, not health (else the watchdog would reconnect-loop a control that merely denies one
|
||||
@@ -202,7 +276,13 @@ session to the first device: INSPECT login + `R_VR`. A bare TCP listener does no
|
||||
any non-allowlisted telegram. The Java fixtures' captured iTNC 530 replies (`Fixtures/*.properties`) are replayed
|
||||
as golden vectors (inspect-only telemetry on two controls; DATA boundary replies on three).
|
||||
- `Runtime.Tests/Drivers/HeidenhainDriverEndToEndTests` — the real driver inside `DriverInstanceActor`.
|
||||
- `Configuration.Tests/DraftValidatorHeidenhainTests`, `AdminUI.Tests/Uns/HeidenhainAuthoringTests` + the fleet
|
||||
- `HeidenhainAlarmTests` (#831) — selector grammar, pairing, transition rules, and against the simulator: the captured
|
||||
iTNC 530 alarm record raising/clearing conditions, local-only ack (zero bytes sent), quality-only comms-loss/refusal,
|
||||
handler-detached recording, identity-change rediscovery; every case asserts the wire log stays inside the read set.
|
||||
- `Runtime.Tests/Drivers/HeidenhainNativeAlarmEndToEndTests` — driver → simulator → real `DriverHostActor` condition.
|
||||
- `Configuration.Tests/DraftValidatorHeidenhainTests`, `AdminUI.Tests/Uns/HeidenhainAuthoringTests`,
|
||||
`AdminUI.Tests/Uns/HeidenhainAuthoringUxTests` (#830 CSV preview ⇔ commit parity, data-type lock, single
|
||||
credential notice) + the fleet
|
||||
parity suites.
|
||||
- **LiveIntegration** (`--filter Category=LiveIntegration`, skipped unless `HEIDENHAIN_LSV2_HOST` is set; optional
|
||||
`HEIDENHAIN_LSV2_PORT`, `OTOPCUA_HEIDENHAIN_INSPECT_PASSWORD`): read-only INSPECT identity/selector/layout survey and a
|
||||
@@ -223,5 +303,5 @@ No real control was available for this port. Still open:
|
||||
|
||||
Writes of any kind, file upload/download, remote input/keyboard/motion, parameter edits, PLCDEBUG/MONITOR/FILE
|
||||
roles, scope setup, arbitrary requests, buffer-size negotiation, vendor SDK, serial transport, built-in SSH,
|
||||
PLC-wide symbol discovery / memory scanning, OEE mapping, and native alarms as Part 9 conditions (alarms are
|
||||
published as typed arrays).
|
||||
PLC-wide symbol discovery / memory scanning, OEE mapping, and any controller-side alarm acknowledgement (native alarm
|
||||
conditions are acknowledged server-side only — see [Alarm conditions](#alarm-conditions-alarm-gitea-831)).
|
||||
|
||||
Reference in New Issue
Block a user