feat(heidenhain): R_RI runtime-info parity with pyLSV2 — state names, refusal codes, timer/unit docs
Compared every R_RI decoder we use (22-28, 30-34, 51) byte-for-byte with pyLSV2 (MIT, 97a3d691) and ran the captured iTNC 530 replies through pyLSV2 itself: no decode disagreement, so no published value changes. - Additive read-only String leaves Status/ExecutionStateName, Status/ProgramStateName and Tool/AxisName (pyLSV2 ExecState / PgmState / decode_tool_info names; out-of-table code = "Unknown"). They ride the raw leaf's feature, so no extra wire read; raw Int32 leaves unchanged. - Refusals: 52/72 -> BadOutOfRange, 58 -> BadTimeout, 56 -> BadInvalidState, 57 (outside the alarm walk) -> BadNoData. IsStable and the 57-only alarm-walk end are unchanged. Lsv2RefusalCodes names every code (pyLSV2 LSV2StatusCode) in exception/log/probe text. - Timers 30-34 documented as uint32be seconds (lifetime counters) per pyLSV2, hardware-unproven; TimersEnabled stays false. Axis/tool units: none on the wire, none assumed. - Lsv2ReadOnlyPolicy untouched (pinned by a test); R_RI 20/21 left out. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -121,13 +121,13 @@ SSH subsystem.
|
||||
| Folder | Leaves (type) | Source | Group |
|
||||
|---|---|---|---|
|
||||
| `Controller` | `Model`, `NcVersion`, `PlcVersion`, `Options` (String) | `R_VR` (4 or 5 fields; SPLC skipped; empty options preserved) | Slow |
|
||||
| `Status` | `ExecutionState` (Int32), `ProgramState` (Int32) — raw native enums | `R_RI 23`, `26` | Status |
|
||||
| `Status` | `ExecutionState` (Int32), `ProgramState` (Int32) — raw native enums, unchanged; `ExecutionStateName`, `ProgramStateName` (String) — their names per pyLSV2 ([below](#runtime-info-decoding--parity-with-pylsv2)) | `R_RI 23`, `26` (one read serves raw + name) | Status |
|
||||
| `Program` | `Main`, `Current` (String), `Line` (Int64) | `R_RI 24` | Status |
|
||||
| `Overrides` | `Feed`, `Spindle`, `Rapid` (Float64, percent) | `R_RI 25` (uint32 hundredths) | Status |
|
||||
| `Axes` | `Names` (String[]), `Positions` (Float64[]) — aligned, controller units | `R_RI 22` | Status |
|
||||
| `Tool` | `Number` (Int64), `Index`, `AxisCode` (Int32), `Length`, `Radius` (Float64) | `R_RI 51` (8-byte reply ⇒ geometry `BadNotSupported`, never 0) | Slow |
|
||||
| `Axes` | `Names` (String[]), `Positions` (Float64[]) — aligned; no unit on the wire (pyLSV2 assumes none either) | `R_RI 22` | Status |
|
||||
| `Tool` | `Number` (Int64), `Index`, `AxisCode` (Int32), `AxisName` (String, `X`/`Y`/`Z`/`Unknown` per pyLSV2), `Length`, `Radius` (Float64, no unit on the wire) | `R_RI 51` (8-byte reply ⇒ geometry `BadNotSupported`, never 0; `AxisName` still Good) | Slow |
|
||||
| `Alarms` | `Classes`, `Groups`, `Numbers` (Int32[]), `Messages` (String[]), `Count` (Int32) | `R_RI 27` then `28`; only refusal 57 ends the list | Slow |
|
||||
| `Timers` | `NcUptime`, `MachineUptime`, `MachineRuntime`, `Spindle0Runtime`, `Spindle1Runtime` (Int64, raw) | `R_RI 30–34`, **disabled by default** | Slow |
|
||||
| `Timers` | `NcUptime`, `MachineUptime`, `MachineRuntime`, `Spindle0Runtime`, `Spindle1Runtime` (Int64, raw uint32 **seconds** per pyLSV2 — lifetime counters, hardware-unproven here) | `R_RI 30–34`, **disabled by default** | Slow |
|
||||
| `Capabilities` | one String per feature + `PlcLayout`, `DncLogin`, `DataLogin` | local, never a wire read | Slow |
|
||||
|
||||
Array leaves require `"isArray": true` (variable length); the typed editor and browse commit write it. Capability
|
||||
@@ -156,6 +156,33 @@ truncates or wraps — otherwise `BadTypeMismatch`.
|
||||
|
||||
At most 256 PLC/DATA bindings per control (excess are skipped and reported on `/hosts`).
|
||||
|
||||
## Runtime-info decoding — parity with pyLSV2
|
||||
|
||||
Every `R_RI` decoder was compared byte-for-byte with pyLSV2 (MIT, commit `97a3d691`: `client.py` `axes_location`,
|
||||
`execution_state`, `program_status`, `program_stack`, `override_state`, `get_error_messages`, `spindle_tool_status`;
|
||||
`misc.py` decoders; `const.py` `ParRRI`, `ExecState`, `PgmState`, `LSV2StatusCode`) and the captured iTNC 530
|
||||
replies were run through pyLSV2 itself — our decoders produce the same values (`HeidenhainPyLsv2ParityTests`).
|
||||
**No decode disagreement was found**, so no published value changed.
|
||||
|
||||
| Selector | Layout (ours = pyLSV2) | Notes |
|
||||
|---|---|---|
|
||||
| 22 axes | byte 0 opaque, count byte, `count` value strings, `count` names | We are stricter: unique non-blank names, finite values, ≤ 64 axes, no trailing bytes |
|
||||
| 23 / 26 | uint16be | Names: `Manual, Mdi, PassReferences, SingleStep, Automatic, Undefined` / `Started, Stopped, Finished, Cancelled, Interrupted, Error, ErrorCleared, Idle, Undefined`; any other code ⇒ `Unknown` (pyLSV2 raises) |
|
||||
| 24 | uint32be line, main, current | Strict on NUL padding. pyLSV2 notes a TNC 640 serves 24 without DNC but with line 0 |
|
||||
| 25 | 3 × uint32be hundredths of percent: feed, spindle, rapid | — |
|
||||
| 27 / 28 | uint16be class, uint16be group, int32be number, text | Only 57 ends the walk (pyLSV2 too). Trailing blanks in the text are **kept** (pyLSV2 strips them — see [Hardware-gated](#hardware-gated--not-yet-proven)) |
|
||||
| 51 | uint32be number, uint16be index, uint16be axis (0 X, 1 Y, 2 Z), 8 or 24 bytes; geometry float64 **little**-endian | Live-proven (L 138.1506 on Hermle70) |
|
||||
| 30–34 | uint32be, seconds | pyLSV2: NC uptime (+1/s), machine uptime, machine runtime (grows only while a program runs), spindle 0 / 1 runtime (0 if unused); observed there on iTNC 530 340490 02/04 and 606421 01, refused `T_ER_UNKNOWN_TELE` on a TNC 640. `TimersEnabled` stays **false** by default |
|
||||
|
||||
Refusal codes now map to the most specific status (table under [Quality](#quality)) and every refusal log line and
|
||||
Test-Connect message names the code (`T_ER_NO_PRIV`, …) from pyLSV2's `LSV2StatusCode` table
|
||||
(`Lsv2RefusalCodes`). Stability is unchanged — only 22/24/23/49 (and a credential problem) suppress a feature for
|
||||
the session; every newly-named code is retried, as `BadUnexpectedError` was.
|
||||
|
||||
**Not adopted (outside the read-only allowlist):** pyLSV2 also reads `R_RI 20/21` (axis configuration / physical
|
||||
names, layouts only partly known), `R_DT` (control clock, DIAGNOSTICS login) and the scope/file/key functions. None is
|
||||
added; the allowlist is unchanged.
|
||||
|
||||
## 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
|
||||
@@ -259,8 +286,12 @@ class of the alarm it last showed (and a rebuild carries it). The condition mess
|
||||
|
||||
| Condition | Value status / capability |
|
||||
|---|---|
|
||||
| Refusal 22/24 | `BadNotSupported` / `Unsupported` — suppressed for the session |
|
||||
| Refusal 22/24 (`T_ER_UNKNOWN_TELE` / `T_ER_WRONG_PARA`) | `BadNotSupported` / `Unsupported` — suppressed for the session |
|
||||
| Refusal 23/49 | `BadUserAccessDenied` / `AccessDenied` — suppressed for the session, except a provisional denial while the optional DNC login is temporarily failing (retried) |
|
||||
| Refusal 52/72 (`T_ER_BAD_MEMADR` / `T_ER_OUT_OF_RANGE`) | `BadOutOfRange` / `InvalidData` — retried next poll |
|
||||
| Refusal 58 (`T_ER_ACCESS_TIMEOUT`) | `BadTimeout` / `Unknown` — retried next poll |
|
||||
| Refusal 56 (`T_ER_OPERATING_MODE`) | `BadInvalidState` / `Unknown` — retried next poll |
|
||||
| Refusal 57 (`T_ER_NO_NEXT_ERROR`) outside the alarm walk | `BadNoData` / `Unknown` — retried (inside the walk it is the end of the list) |
|
||||
| Other refusal | `BadUnexpectedError` / `Unknown` — retried next poll |
|
||||
| Malformed complete record (length, terminator, NaN, duplicate axis, …) | `BadDecodingError` / `InvalidData` |
|
||||
| Binding type mismatch | `BadTypeMismatch` |
|
||||
@@ -335,8 +366,14 @@ No real control was available for this port. Still open:
|
||||
real OPC UA client, OPC write refusal, network interruption + recovery quality, bounded traffic under load.
|
||||
- Controller families/versions other than iTNC 530 (TNC 320/430/640) — unproven even in the Java reference.
|
||||
- Successful raw **`R_MB`** decoding and its Boolean encoding (denied on all four tested controls).
|
||||
- Physical units of axis positions / tool geometry vs. the HMI; experimental timer semantics (selectors 30–34).
|
||||
- Physical units of axis positions / tool geometry vs. the HMI — the wire carries no unit and pyLSV2 assumes none, so
|
||||
only a control can settle it. Timer semantics (selectors 30–34) are now **per pyLSV2** (uint32be seconds,
|
||||
lifetime counters) but still unproven by this driver on hardware.
|
||||
- Execution / program-state **names** are per pyLSV2 (its `PgmState` table comes from a public forum thread); the
|
||||
2026-11-11 iTNC 530 values (running = 4 Automatic + 0 Started; idle = 7 Idle) are consistent with them.
|
||||
- DNC/DATA permission matrices per control; sustained fleet/load operation.
|
||||
- Whether any control pads `R_RI 22/24/27` strings with trailing blanks. pyLSV2 strips them (`ba_to_ustr`); this
|
||||
driver keeps them, because an alarm text is part of a condition's identity and the captured records carry none.
|
||||
|
||||
## Deliberate exclusions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user