This commit is contained in:
+156
-11
@@ -66,7 +66,7 @@ enums, so the numeric-enum form/factory trap cannot occur):
|
||||
| `TimeoutMs` | 5000 | 100–60000; connect + per-transaction deadline |
|
||||
| `Access.TelemetryEnabled` | true | Off = no `R_RI` requests at all |
|
||||
| `Access.DncLoginEnabled` | true | Optional DNC login; a refusal never blocks reads permitted under INSPECT |
|
||||
| `Access.DataEnabled` | false | Opt-in DATA login, attempted only when a `DATA:` binding exists (iTNC only) |
|
||||
| `Access.DataEnabled` | false | Opt-in DATA login, attempted only when a `DATA:` binding exists, and only on an iTNC 530 ([Control families](#control-families)) |
|
||||
| `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)) |
|
||||
@@ -120,14 +120,14 @@ 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 |
|
||||
| `Controller` | `Model`, `NcVersion`, `PlcVersion`, `Options`, `Family` (String) | `R_VR` (4 or 5 fields; SPLC skipped; empty options preserved). `Family` is classified locally from `Model` — no extra request ([Control families](#control-families)) | Slow |
|
||||
| `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
|
||||
@@ -141,9 +141,10 @@ the next populated advertised base is a defensive cap. Bit and STRING counts are
|
||||
|
||||
| Area | Native | Allowed `dataType` |
|
||||
|---|---|---|
|
||||
| `MARKER`, `INPUT`, `OUTPUT`, `COUNTER`, `TIMER` | one byte per Boolean (0/1 only) | Boolean |
|
||||
| `MARKER`, `INPUT`, `OUTPUT`, `COUNTER`, `TIMER` | one byte per Boolean: `00` false, `01`/`FF` true (anything else ⇒ `BadDecodingError`) | Boolean |
|
||||
| `BYTE` / `WORD` / `DWORD` | signed int8 / int16le / int32le | Int16 (BYTE, WORD only), Int32, Int64 |
|
||||
| `INPUT_WORD`, `OUTPUT_WORD` | uint16le | Int32, Int64 |
|
||||
| `INPUT_DWORD`, `OUTPUT_DWORD` | signed int32le over the input/output-word pool (per pyLSV2, hardware-unproven) | Int32, Int64 |
|
||||
| `STRING` | advertised fixed width, terminated Latin-1 (too wide ⇒ error, never truncated) | String |
|
||||
|
||||
Example: `PLC:WORD:4999` → `R_MB` at `base + 4999×2`. Note that `DATA:/PLC/memory/W/9998` uses a native **byte
|
||||
@@ -156,6 +157,135 @@ truncates or wraps — otherwise `BadTypeMismatch`.
|
||||
|
||||
At most 256 PLC/DATA bindings per control (excess are skipped and reported on `/hosts`).
|
||||
|
||||
**PLC bit `FF`.** Accepting `FF` for a raw `R_MB` bit (since 2026-11-12) follows the evidence, not a capture of `R_MB` itself: the same one-byte
|
||||
operands read through `R_DP` return `FF` for a set input/output on a real iTNC 530 (`I.999`/`O.999` in
|
||||
`Fixtures/plc-count-units.properties`), and pyLSV2 decodes these areas as "any non-zero is true". `02`–`FE` stay
|
||||
malformed. Previously an `FF` raw bit read `BadDecodingError`.
|
||||
|
||||
### pyLSV2 correspondence (per pyLSV2, hardware-unproven)
|
||||
|
||||
[pyLSV2](https://github.com/drunsinn/pyLSV2) (MIT) is the other open implementation of these requests. Checked
|
||||
2026-11-12; where it disagrees with a real-control capture, the capture wins.
|
||||
|
||||
| Item | pyLSV2 | This driver | Verdict |
|
||||
|---|---|---|---|
|
||||
| `R_PR` field offsets (`misc.py:decode_system_parameters`, `!14L8B8L…`, 120 or 124 bytes) | (base, count) pairs at 0/8/16/24/32/40/48, string length byte at 56, input words 64, output words 72 | same; trailing version/buffer/key fields unused | agree |
|
||||
| `R_PR` numeric count **units** (`client.py:read_plc_memory`) | BYTE max = count×2, WORD/INPUT_WORD/OUTPUT_WORD max = count, DWORD and I/O DWORD max = count/4 | every numeric count is a byte capacity | **conflict — capture wins**: the three iTNC 530 controls refuse `B10000`/`W10000`/`IW1000`, and each base + count is exactly the next advertised base |
|
||||
| `R_MB` address | `start + first_element × width` | `base + INDEX × width` | agree |
|
||||
| `R_MB` count byte | `elements × width`, ≤ ⌊255/width⌋ − 1 elements per telegram | one element per request (`width` bytes, ≤ 247) | agree (STRING widths above 247 are refused rather than sent) |
|
||||
| Signedness / endianness | BYTE `<b`, WORD `<h`, DWORD `<l`, I/O WORD `<H`, I/O DWORD `<l`, bits `?` | same; bits `00`/`01`/`FF` only | agree (bits stricter) |
|
||||
| `INPUT_DWORD` / `OUTPUT_DWORD` (`MemoryType` 13/12) | supported | **added** 2026-11-12 | fixed |
|
||||
| `STRING` read | `max_string_length` bytes, Latin-1, all NULs stripped and trailing whitespace trimmed | `width` bytes, text up to the NUL, only NUL padding allowed after it | divergent — see open question below |
|
||||
| Roles | `R_MB` after a **`PLCDEBUG`** login; `R_DP` after `DATA` (iTNC only) | `R_MB` under INSPECT (+ optional DNC/DATA); `R_DP` under DATA, gated to the `ITnc530` control family ([Control families](#control-families)) | see [Why raw `R_MB` is refused](#why-raw-r_mb-is-refused-plcdebug) |
|
||||
| `R_DP` request | four `00` bytes + path (`/`→`\`, `"`→`'`) + NUL, no length check | same, ≤ 242 path characters | agree |
|
||||
| `R_DP` value types | 2 `!h`, 3 `!l`, 5 `<d`, 8 Latin-1 (NULs stripped, right-trimmed), 11 `?`, 16 `!b`, 17 `!B`; lengths unchecked | same types; exact lengths, finite floats, Boolean `00`/`01`/`FF`, text kept verbatim | agree (ours stricter; no trimming) |
|
||||
|
||||
**Address notation.** pyLSV2's `read_plc_address` takes HEIDENHAIN PLC-program operands: `M<n>`, `I<n>`, `O<n>`,
|
||||
`B<n>`, `S<n>` are entry/element numbers, while `W<n>`, `D<n>`, `IW<n>`, `OW<n>`, `ID<n>`, `OD<n>` are **byte**
|
||||
addresses (pyLSV2 divides by 2 or 4 and silently truncates an unaligned one). No `T`/`C` operands. The correspondence
|
||||
is: `M n` = `PLC:MARKER:n`, `I n` = `PLC:INPUT:n`, `O n` = `PLC:OUTPUT:n`, `B n` = `PLC:BYTE:n`, `S n` =
|
||||
`PLC:STRING:n`, `W 2k` = `PLC:WORD:k`, `D 4k` = `PLC:DWORD:k`, `IW 2k` = `PLC:INPUT_WORD:k`, `OW 2k` =
|
||||
`PLC:OUTPUT_WORD:k`, `ID 4k` = `PLC:INPUT_DWORD:k`, `OD 4k` = `PLC:OUTPUT_DWORD:k`. The `DATA:/PLC/memory/<op>/<n>`
|
||||
paths use the same byte numbering as the PLC program. The driver does **not** accept the native operands as aliases:
|
||||
a `PLC:` field in which `W8` means byte 8 but `WORD:8` means byte 16 is exactly the confusion the element-index rule
|
||||
exists to avoid.
|
||||
|
||||
**Open question — PLC strings.** pyLSV2 returns the whole fixed-width buffer with NULs stripped, so any bytes after
|
||||
the terminator stay in the text. This driver refuses anything other than NUL padding after the terminator
|
||||
(`BadDecodingError`). If a real control leaves stale bytes after a shorter string, the right fix is "text up to the
|
||||
first NUL", not pyLSV2's behaviour. Hardware-gated with raw `R_MB`.
|
||||
|
||||
### Why raw `R_MB` is refused (PLCDEBUG)
|
||||
|
||||
Raw `R_MB` was refused on every real iTNC 530 tried. pyLSV2 explains why: `read_plc_memory` and `read_plc_address`
|
||||
both send `A_LG PLCDEBUG` before `R_MB` (pyLSV2's `const.py` itself hedges: "requires INSPECT (PLCDEBUG??)"), and
|
||||
this driver never logs in as PLCDEBUG. pyLSV2 describes PLCDEBUG as "enables **write** access to PLC, requires
|
||||
password", and uses the same role for `C_MC` (`set_machine_parameter`, a machine-parameter **write**). So PLCDEBUG is
|
||||
a write-capable role, not a read-only one. pyLSV2's own default "safe mode" refuses the PLCDEBUG login (it allows
|
||||
INSPECT/FILE/MONITOR only), and its PLC-memory demo turns safe mode off. pyLSV2 sends PLCDEBUG with an empty password,
|
||||
which can only work on a control that has no PLCDEBUG password set (typically a programming station). A production
|
||||
control has a password that would have to be entered here. **Owner decision:** raw PLC memory stays unavailable
|
||||
unless the owner accepts a write-capable LSV2 session. `Lsv2ReadOnlyPolicy` would still refuse every write request,
|
||||
but the controller would no longer be enforcing read-only. Until then, use `DATA:/PLC/memory/...` bindings, which
|
||||
read the same operands under the DATA role. The decoders above are already pyLSV2-parity, so nothing else changes
|
||||
if a role is ever enabled. All of this is per pyLSV2 and has not been tested on hardware.
|
||||
|
||||
## Control families
|
||||
|
||||
`R_VR`'s model field is classified once, in one place — `HeidenhainControlFamilies.Classify` (Contracts project,
|
||||
`HeidenhainControlFamily`) — with pyLSV2's rule (MIT; `dat_cls.py` `VersionInfo.control`): strip spaces, compare
|
||||
case-insensitively by prefix, `ITNC530` first. The result is published on the **`Controller/Family`** String leaf
|
||||
(member name: `ITnc530`, `Tnc`, `Tnc7`, `Lathe`, `MillPlus`, `LegacyTnc`, `Unknown`), included in the Test-Connect
|
||||
probe message (`TNC640 (Tnc) NC …`) and the `/hosts` diagnostics bag (`<host>.family`). It costs no request: it is
|
||||
derived from the `R_VR` reply the session already reads. `Controller/Model` keeps the raw string.
|
||||
|
||||
The only code-level gate pyLSV2 applies to a request this driver makes is **DATA**: `read_data_path` returns early
|
||||
on anything but an iTNC (`client.py` "does not work on non iTNC controls"). So a `DATA:` binding on any other family
|
||||
— and on an `Unknown` model — is `BadNotSupported` with `Capabilities/DataLogin = Unsupported`, and **the DATA login
|
||||
is never sent**. (Scope is pyLSV2's other iTNC-only gate; this driver has no scope.) Every other feature is
|
||||
attempted on every family and settled by the control's own reply: `T_ER_UNKNOWN_TELE` (22) ⇒ `Unsupported`, asked
|
||||
once per session, then suppressed; an access refusal ⇒ `AccessDenied`. No family pre-gates a selector — pyLSV2
|
||||
records those as "observations, not guarantees" (`docs/protocol.rst`), and a pre-gate would hide data a newer NC
|
||||
version serves.
|
||||
|
||||
| Family (`Controller/Family`) | Models (`R_VR` prefix) | Evidence | What the reference says | Driver behaviour |
|
||||
|---|---|---|---|---|
|
||||
| `ITnc530` | `iTNC530` / `iTNC 530` | **Hardware-proven** — four Hermle iTNC 530 (NC 340490 03/04, 606420 03; `evidence/heidenhain-live-2026-11-11.md`) | pyLSV2 `MILL_OLD`; DATA + scope iTNC-only; tool (51), alarms (27/28) and timers (30–34) served | All features attempted; DATA allowed (opt-in) |
|
||||
| `Tnc` | `TNC640`, `TNC620`, `TNC320`, `TNC128` | Per pyLSV2, hardware-unproven | `MILL_NEW`. TNC 640 NC 340590 10 SP2: 27/28/51 and 30–34 → `T_ER_UNKNOWN_TELE`; 25/26 need DNC; 24 serves the program under INSPECT with block number 0 | `Tool/*`, `Alarms/*`, `ALARM:` conditions and `Timers/*` read `Unsupported` after one refused request; without DNC `Overrides/*` and `Status/ProgramState` are `AccessDenied` and `Program/Line` reads the control's raw 0. DATA `Unsupported`, not sent |
|
||||
| `Tnc7` | `TNC7` | Per pyLSV2, hardware-unproven | `TNC7` — "behaves just a bit different again"; only keyboard/key functions are flagged (not used here) | Reply-driven (no selector observations recorded); DATA `Unsupported` |
|
||||
| `Lathe` | `CNCPILOT640`, `MANUALPLUS` | Per pyLSV2, hardware-unproven | `LATHE_NEW`; protocol table: no tool (51), no error messages (27/28) | Those features settle `Unsupported` from the reply; DATA `Unsupported` |
|
||||
| `MillPlus` | `MILLPLUS` (e.g. `MillPlusIT V600`) | Per pyLSV2, hardware-unproven | `MILLPLUS`; "not officially supported by any other Heidenhain tool" | Reply-driven; DATA `Unsupported` |
|
||||
| `LegacyTnc` | `TNC426`, `TNC430` | **No reference** (pyLSV2 reports these `UNKNOWN`) | — | Named for the leaf only; reply-driven; DATA `Unsupported` |
|
||||
| `Unknown` | anything else | — | pyLSV2 `UNKNOWN` | Reply-driven; DATA `Unsupported` |
|
||||
|
||||
Decoding is family-independent per pyLSV2: `R_VR` is 4 or 5 NUL-separated fields on every family (`client.py`
|
||||
`_read_version`), `R_PR` is 120 or 124 bytes with one layout (`misc.py` `decode_system_parameters`), and the `R_RI`
|
||||
decoders for 22–28/51 carry no family branch. The simulator exercises every family in both `R_VR` shapes, both
|
||||
`R_PR` lengths and the read-only wire set (`Simulator/ControlProfiles.cs`, `HeidenhainControlFamilyTests`) — that is
|
||||
evidence for the classification and the gates, **not** for any non-iTNC control's real replies.
|
||||
|
||||
**Open, needs real hardware or a request this driver deliberately does not send:**
|
||||
|
||||
- **Buffer size.** pyLSV2 negotiates 512–4096-byte telegrams with `C_CC` from `R_PR`'s max block length; `C_CC` is
|
||||
a configuration command, outside the allowlist. We stay at the un-negotiated legacy frame on every family (pyLSV2's own default
|
||||
is 256 bytes, so a control is expected to fit replies to it); a reply over 255 bytes is a transport error
|
||||
(`Oversized LSV2 response`), never a truncated value.
|
||||
- **Encrypted passwords.** pyLSV2's `R_PR` exposes a password-encryption key and notes TNC 640 sends some login
|
||||
passwords encrypted (scope). Whether a password-protected DNC/DATA login on a `Tnc`/`Tnc7` needs that is unknown;
|
||||
INSPECT and DNC with an empty password are unaffected.
|
||||
- **Which family sends the 5-field `R_VR`**, and whether `R_RI 20/21` (TNC 640 axis config/physical names, outside
|
||||
our selector allowlist) would be a better axes source there.
|
||||
- **Selector availability per NC version** on `Tnc`/`Tnc7`/`Lathe`/`MillPlus` (the table above is one TNC 640
|
||||
observation plus pyLSV2's test-suite gating), the `R_RI 22` axis encoding, override/tool units, and whether
|
||||
`R_MB` is granted under INSPECT on any non-iTNC family.
|
||||
|
||||
## 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 +389,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` |
|
||||
@@ -333,10 +467,21 @@ No real control was available for this port. Still open:
|
||||
|
||||
- **.NET commissioning evidence** on a real control (the LiveIntegration gate) — browse/types/arrays through a
|
||||
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).
|
||||
- Controller families/versions other than iTNC 530 (TNC 640/620/320/128, TNC7, CNC PILOT/MANUALplus, MillPlus,
|
||||
TNC 426/430) — see the [Control families](#control-families) matrix; everything there except iTNC 530 is per
|
||||
pyLSV2 only. `R_DP` on non-iTNC families is gated off (pyLSV2 restricts `read_data_path` to iTNC).
|
||||
- Successful raw **`R_MB`** decoding and its Boolean encoding. It was denied on all four tested controls, and per
|
||||
pyLSV2 that is because it needs the write-capable PLCDEBUG role
|
||||
([why](#why-raw-r_mb-is-refused-plcdebug)) — which this read-only driver will not use. The `FF`-true bit,
|
||||
`INPUT_DWORD`/`OUTPUT_DWORD` and PLC-string trailing bytes are per pyLSV2 or `R_DP` evidence, not `R_MB` captures.
|
||||
- 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