Merge branch 'master' into remed/lane-G2
This commit is contained in:
+16
-5
@@ -252,7 +252,7 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
|
||||
write, read back, restored byte-exact). A user-approved **native array-element write** followed: `Program:EM01_PowerUnit._SEQ_EP[31]`
|
||||
0→4242→0 (the array's only alias targets `[1]`, no routine indexes or block-copies it; all 32 elements snapshotted
|
||||
twice before, after the write and after the restore — only `[31]` changed). Still ab_server-only: whole-array
|
||||
writes (the driver has none), libplctag member writes, and fragmented writes. #566 is answered (the controller drops TCP; see "A target-closed CIP connection"). Mixed STRING/array/scalar
|
||||
writes (#737; see [1-D array support](#1-d-array-support)), libplctag member writes, and fragmented writes. #566 is answered (the controller drops TCP; see "A target-closed CIP connection"). Mixed STRING/array/scalar
|
||||
Multiple Service Packet reads (#582) are proven on the press (one 5-read MSP, values equal to single reads). Results:
|
||||
[native-ab-transport-gate-results](../plans/2026-10-07-native-ab-transport-gate-results.md).
|
||||
- **libplctag cannot be deleted yet.** Scalar, UDT-member and STRING native writes are proven on a CompactLogix
|
||||
@@ -262,7 +262,7 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
|
||||
libplctag member-write path is `ab_server`-only. See [Member writes](#member-writes).
|
||||
- ⚠️ RESIDUAL (#567): wide-struct **fragmented writes** (8-byte alignment) are unit-tested only. A write fragments only
|
||||
above `MaxRequestBytes` (4000 B on a Large Forward Open), so it needs a ~4 KB value; Z-35162 has no safe one (a DINT[32]
|
||||
or a STRING is a single Write Tag, and AbCip has no whole-array write anyway). It stays open until a bench controller
|
||||
or a STRING is a single Write Tag — and a whole DINT[32] array write, #737, is 128 B). It stays open until a bench controller
|
||||
or an approved large scratch tag exists.
|
||||
- Native browse (#572, resolved in code): listing paging no longer assumes ascending instance order, pages
|
||||
are paced, and a browse's template reads share one lease. Observed on Z-35162 (2026-10-09, 41
|
||||
@@ -361,7 +361,17 @@ integration fixture is down during normal dev (Docker host fixture currently off
|
||||
|
||||
**Live-verify** — integration-fixture-gated (requires `ab_server` Docker fixture on `10.100.0.35:44818`).
|
||||
|
||||
**Deferrals** — array *writes*, multi-dimensional arrays, per-element historization. Safety-partition array tags (`SafetyTag: true`) are excluded (GuardLogix write constraints extend to array writes).
|
||||
**Array writes (#737, review 07 U10)** — a whole-array write of exactly `arrayLength` elements, encoded by
|
||||
`AbCipArrayCodec` little-endian at the element width on both transports. Element values **convert**
|
||||
(ScadaBridge writes `int[]` to INT arrays); a value that does not fit is `BadOutOfRange`, one that cannot convert
|
||||
`BadTypeMismatch`, a different length `BadOutOfRange`, all before anything is sent. The controller buffer must be
|
||||
width × count (else `BadTypeMismatch`, checked before sending). Writable element types: `SInt`/`USInt`/`Int`/`UInt`/
|
||||
`DInt`/`UDInt`/`Real`/`Dt`/`LInt`/`ULInt`/`LReal`. **BOOL arrays** (bit-packed DWORDs), **STRING** and **structure**
|
||||
arrays are refused `BadNotSupported`, so their nodes stay read-only (`DriverArrayWriteSupport`); so does a
|
||||
safety-partition array tag (`safetyTag: true`, `BadNotWritable`). An array deployed read-only before #737 turns
|
||||
writable on the next deploy (a surgical `ArrayWritable` update). Proven on `ab_server` only until the hardware gate runs.
|
||||
|
||||
**Deferrals** — multi-dimensional arrays, per-element historization, whole-array writes on hardware.
|
||||
|
||||
See [Uns.md §Array tags](../Uns.md#array-tags-1-d) for the cross-driver coverage matrix
|
||||
and the UI authoring flow.
|
||||
@@ -454,8 +464,9 @@ when it is browse-committed in `/raw`, and that tag writes like any other atomic
|
||||
`writable` key. The node is writable only once the operator sets the raw tag to `ReadWrite` (the
|
||||
node-manager gate); an authored `"writable": false` still refuses at the driver (`BadNotWritable`). The
|
||||
browse's `SecurityClass` (members inherit the parent symbol's read-only bit) is not consulted by the
|
||||
commit. An element of a member array is authored by hand as `Seg.Points[2]`; a whole member array is
|
||||
not writable (AbCip has no array encoder, `DriverArrayWriteSupport`).
|
||||
commit. An element of a member array is authored by hand as `Seg.Points[2]`; a whole member array of an
|
||||
atomic type is writable as an array tag (`isArray` + `arrayLength`, #737), a BOOL / STRING / structure member
|
||||
array is not (`DriverArrayWriteSupport`).
|
||||
- **The request names the member.** One CIP symbolic segment per member (plus an element segment), the
|
||||
member's own CIP type, the member's own bytes — never the parent UDT, never a hidden host. Pinned at the
|
||||
request-byte level by `NativeAbCipTagRuntimeTests.A_member_path_write_names_the_member_and_sends_only_its_own_bytes`
|
||||
|
||||
@@ -304,8 +304,18 @@ N values of the declared `AbLegacyDataType`. Maximum element count is 256 (PCCC
|
||||
limit).
|
||||
|
||||
This closes the previously-noted limitation ("single-element addressing today") for the
|
||||
array read path. The opt-in native transport also writes multi-element arrays (chunked); on libplctag
|
||||
array *writes* remain a follow-up.
|
||||
array read path.
|
||||
|
||||
**Array writes (#738, review 07 U10 / r0 I3)** — on both transports, a whole-array write encodes exactly
|
||||
`arrayLength` elements at the **data file's** element width (N / B / A words, L longs, F floats). Element values
|
||||
**convert** (ScadaBridge writes `int[]` to Int16 nodes); a length mismatch or an element that overflows is
|
||||
`BadOutOfRange`, a non-numeric value `BadTypeMismatch`, before any runtime exists. Writable: `Int` / `AnalogInt` on
|
||||
an N / B / A file, `Long` on an L file, `Float` on an F file, on a plain `<file><number>:<word>` address of at most
|
||||
256 elements. These nodes stay **read-only** (`DriverArrayWriteSupport`): Bit arrays (bit-packed words), String /
|
||||
Timer / Counter / Control arrays (rejected at `FromTagConfig`), `/bit` and `.sub` addresses, a type that does not
|
||||
match its file (`BadTypeMismatch`), any PLC-5 on the native transport (writes refused before the wire), the
|
||||
`S:` / `I:` / `O:` files (libplctag refuses their translated names), and — deliberately — indirect addresses, whose
|
||||
element moves with the index word. An array deployed read-only before #738 turns writable on the next deploy.
|
||||
|
||||
**Unit test coverage** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/` covers
|
||||
the multi-element read path via the fake tag runtime. The AB Legacy integration fixture is
|
||||
@@ -313,8 +323,10 @@ down during normal dev.
|
||||
|
||||
**Live-verify** — integration-fixture-gated (requires the AB Legacy Docker fixture).
|
||||
|
||||
**Deferrals** — array *writes* on libplctag (the opt-in native transport writes them), multi-dimensional arrays, per-element historization.
|
||||
`arrayLength > 256` is rejected at init with an explicit error (PCCC payload cap).
|
||||
**Deferrals** — multi-dimensional arrays, per-element historization, array writes on hardware
|
||||
(`AbLegacyArrayWriteIntegrationTests` is green on `ab_server` and ready for a real controller via
|
||||
`AB_LEGACY_ARRAY_WRITE_BASE`).
|
||||
`arrayLength > 256` is clamped to 256 by the tag factory (PCCC payload cap), so such a node stays read-only.
|
||||
|
||||
See [Uns.md §Array tags](../Uns.md#array-tags-1-d) for the cross-driver coverage matrix
|
||||
and the UI authoring flow.
|
||||
|
||||
+50
-8
@@ -88,6 +88,9 @@ After the handshake, Test Connect reads 2 bytes from `MB0`. A CPU with PUT/GET d
|
||||
default) completes the handshake but denies every read, so the probe fails with an actionable "enable
|
||||
PUT/GET communication" message instead of a false green tick. A CPU that merely rejects that address (no
|
||||
`MB0`) still passes — the connection is fine; any other read fault fails the probe with its message.
|
||||
Both are recognised in the bare-`Exception` shape S7.Net's read path really raises. The handshake and
|
||||
test read are bounded by the config's `timeoutMs` (the driver's own per-operation timeout), capped by the
|
||||
Test Connect timeout — before R07:H7 the config value was ignored.
|
||||
|
||||
## Address forms
|
||||
|
||||
@@ -103,9 +106,18 @@ Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
|
||||
| Timer | `T0` / `T15` | Timer area (read-only — see Wide types section) |
|
||||
| Counter | `C0` / `C10` | Counter area (read-only — see Wide types section) |
|
||||
|
||||
Parsing is strict and runs once at `InitializeAsync` so a config typo fails fast
|
||||
at load instead of surfacing as `BadInternalError` on every read. Bit offsets
|
||||
must be 0-7, byte offsets non-negative, DB numbers >= 1.
|
||||
Parsing is strict. Bit offsets must be 0-7, byte offsets non-negative, DB numbers >= 1.
|
||||
|
||||
**Tag shape rules are judged per tag, at every gate** (`S7TagShapeRules`, called by
|
||||
`S7TagDefinitionFactory.FromTagConfig`): the AdminUI editor refuses the tag, the deploy gate
|
||||
(`Inspect`) warns, and the driver's init **skips** it (reported through `ISkippedTagReporter`; it reads
|
||||
`BadNodeIdUnknown`) while every other tag keeps working. Refused: an address that does not parse, a
|
||||
wide/structured type that is an array or is not byte-addressed (`DBB`/`MB`/`IB`/`QB`), a narrow type at the
|
||||
wrong access width (`Int32` on `DBW0`, `Bool` on `DBB0`...), a Timer not typed `Float64` or a Counter not typed
|
||||
`Int32`, a writable or array Timer/Counter, a writable `Bool` array, and a `Bool` array that does not start
|
||||
at bit 0 (a Bool array reads bit 0 of each byte, so `DB1.DBX0.3` used to silently return bit 0 of bytes
|
||||
0..n). These used to be init guards that THREW, so one typo faulted every tag on the PLC and the actor
|
||||
retried the same init every 10 s forever (R07:H3).
|
||||
|
||||
## Data types
|
||||
|
||||
@@ -277,7 +289,7 @@ AdminUI `/hosts` page (Gitea #640). See `docs/Telemetry.md` ("Driver diagnostics
|
||||
| `IWritable` | `WriteAsync` → `S7.Net.Plc.WriteAsync` | Read-only tags (`Writable=false`) return `BadNotWritable` |
|
||||
| `ITagDiscovery` | `DiscoverAsync` | Emits a flat `S7/` folder of the configured tags — no live browse |
|
||||
| `ISubscribable` | per-tag poll loop with capped exponential backoff | S7 has no push model; floor is 100 ms (the CPU services the comms mailbox once per scan) |
|
||||
| `IHostConnectivityProbe` | periodic `S7.Net.Plc.ReadStatusAsync` (CPU-status PDU) | `host:port` host key; `Running`/`Stopped` transitions raise `OnHostStatusChanged` |
|
||||
| `IHostConnectivityProbe` | periodic `S7.Net.Plc.ReadStatusAsync` (CPU-status PDU) | `host:port` host key; `Running`/`Stopped` transitions raise `OnHostStatusChanged`. A tick that finds the gate held past the probe deadline (a long read pass) gives **no verdict** — it no longer reports `Stopped` for a connection that is busy, not lost (R07:H7) |
|
||||
|
||||
### Single-connection policy
|
||||
|
||||
@@ -302,8 +314,9 @@ Portal under *Protection & Security* for the CPU.
|
||||
| Tag not in config | `BadNodeIdUnknown` | unchanged |
|
||||
| Read-only tag written | `BadNotWritable` | unchanged |
|
||||
| Unimplemented data type | `BadNotSupported` | unchanged |
|
||||
| PUT/GET denied | `BadNotSupported` | `Faulted` (config alert) |
|
||||
| CPU / hardware fault | `BadDeviceFailure` | `Degraded` |
|
||||
| PUT/GET denied | `BadNotSupported` | `Faulted` (config alert) — also counts as *answered*, advances `LastSuccessfulRead` (no watchdog reconnect loop) |
|
||||
| CPU refusal of the item (object does not exist, address out of range, type inconsistent, hardware fault) | `BadDeviceFailure` | unchanged — counts as *answered*, advances `LastSuccessfulRead`; one Warning per tag |
|
||||
| Other device-layer fault (`PlcException` that is neither a refusal nor connection-fatal) | `BadDeviceFailure` | `Degraded` |
|
||||
| Socket / timeout | `BadCommunicationError` | `Degraded` |
|
||||
| Reconnect suppressed (backoff window) | `BadCommunicationError` | `Degraded` |
|
||||
| Array write of the wrong shape / length / element type | `BadTypeMismatch` | unchanged |
|
||||
@@ -314,7 +327,36 @@ accumulate their outcomes and write health once (`Healthy < Degraded < Faulted`)
|
||||
`Faulted` item is not erased by a later `Degraded` or successful sibling. The write-side `Faulted` is
|
||||
**sticky against reads**: a clean read pass (reads often work against a write-protected CPU) leaves
|
||||
`Faulted` in place and only advances `LastSuccessfulRead`; the next write that succeeds clears it. A
|
||||
transient `Degraded` write failure is still cleared by a clean read, as before.
|
||||
transient `Degraded` write failure is cleared by a clean read, or by the next write batch the CPU
|
||||
answers without a failure.
|
||||
|
||||
**A refusal is an answer (R07:H4).** S7.Net's read paths raise the CPU's item error as a bare
|
||||
`Exception` ("Received error from PLC: Object does not exist." — live-probed on the snap7 fixture), its
|
||||
write paths as a `PlcException(WriteData)`; both are classified over the whole exception chain, and the
|
||||
access-denied text inside either (S7.Net's exact "Accessing object not allowed", and never when the chain
|
||||
also carries a transport fault) is the `Faulted` alert — which is ALSO an answer: it advances
|
||||
`LastSuccessfulRead`, so a factory-default CPU with PUT/GET disabled stays `Faulted` instead of being
|
||||
reconnected by the comms-loss watchdog every 90 s. A multi-var chunk that falls back per tag memoises the
|
||||
members refused on their own, like a refused span. A refusal is deterministic, so it neither
|
||||
degrades health nor reopens the connection, and because it proves the CPU is reachable it advances
|
||||
`LastSuccessfulRead` and restores `Healthy` after an earlier comms fault. Before this, a driver whose
|
||||
every tag was refused (a wrong DB number) sat `Degraded` with a frozen timestamp and the comms-loss
|
||||
watchdog reconnected it every 90 s against a perfectly reachable PLC.
|
||||
|
||||
**A dead handle ends the pass's wire traffic (R07:P1).** Once a read pass or write batch marks the
|
||||
connection dead (a wire op blew its deadline on a frozen-but-established peer, a socket dropped), the
|
||||
remaining spans / multi-var chunks / tags / writes of that pass are stamped `BadCommunicationError`
|
||||
without I/O; the next call reopens. A multi-var chunk that dies on a transport loss no longer falls back
|
||||
to one doomed per-tag read per member. Without this an op that ignores its cancellation token would cost
|
||||
one full deadline per remaining item under the gate, and `Degraded` (and so the comms-loss watchdog's
|
||||
clock) only became visible after the whole pass.
|
||||
|
||||
**A refused span falls back per member (R07:P2).** A grouped block read covers the union of its members'
|
||||
ranges, so one member the CPU refuses (running past the DB's real end, an over-declared `stringLength`)
|
||||
used to make every neighbour in the block Bad on every poll. When the CPU refuses a multi-member span the
|
||||
driver re-reads its members one at a time for that pass and remembers which tags must not be grouped —
|
||||
the members refused on their own, or every member when none was — so later passes group only the rest.
|
||||
The memo lasts until the next initialize. A span failure that is not a refusal still stamps every member.
|
||||
|
||||
**Connect backoff.** A dead PLC costs one connect attempt per backoff window (1 s doubling to a 30 s
|
||||
cap), not one per read / poll tick / probe tick: inside the window `EnsureConnectedAsync` fails fast
|
||||
@@ -359,7 +401,7 @@ normal dev (the S7 sim is on the shared Docker host but currently offline).
|
||||
**Array writes (S7-016)** — a writable `Byte` / `Int16` / `UInt16` / `Int32` / `UInt32` / `Float32` array
|
||||
is written as ONE contiguous `WriteBytesAsync` of `count x element-bytes` (the inverse of the read
|
||||
decode), and the value must be an array of exactly `arrayLength` elements (else `BadTypeMismatch`, no
|
||||
health impact). **Writable `Bool` arrays are refused at init** (`NotSupportedException`): the array
|
||||
health impact). **Writable `Bool` arrays are refused per tag** (see the tag shape rules above): the array
|
||||
read is byte-granular, so a matching write would overwrite the 7 neighbouring bits of every byte it
|
||||
touches, and a safe bit-packed write needs read-modify-write. Mark Bool arrays `writable: false`. A `null` array element is rejected (`BadTypeMismatch`, nothing written)
|
||||
rather than coerced to 0. An array write larger than one PDU is split by S7.Net into several writes and is
|
||||
|
||||
@@ -362,6 +362,20 @@ element-typed CLR array (`bool[]`, `sbyte[]`, `byte[]`, `short[]`, `ushort[]`, `
|
||||
down (found live, below). A bit outside the parent's width, or a bit of a non-integer parent,
|
||||
is refused with `BadTypeMismatch`.
|
||||
|
||||
### Array writes (#739, review 07 U9)
|
||||
|
||||
An array node is **writable** (`DriverArrayWriteSupport`) for every element type the codec lays out —
|
||||
numeric, `Bool` (one byte per element, as TwinCAT stores a BOOL array), `STRING`/`WSTRING` and the temporal
|
||||
types. The write takes exactly `arrayLength` elements and **converts** each one (`ConvertForWrite`; ScadaBridge
|
||||
writes `int[]` to an Int16 node); a length mismatch or an out-of-range element is `BadOutOfRange` and a scalar
|
||||
value `BadTypeMismatch`, before any wire traffic. `WriteArrayAsync` then plans the layout from the resolved PLC
|
||||
array with the same `CheckArray` the read uses and encodes with `TwinCATArrayCodec.Encode` (the inverse of the
|
||||
decode); a shorter `arrayLength` writes the leading sub-range, and a multi-dimensional PLC array is written
|
||||
row-major from the flat 1-D value. A BOOL **bit-in-word** path (`MAIN.Word.3`) stays read-only as an array: it is a
|
||||
scalar bit write. Under a `DateTime` node only `DATE`/`DT`/`WSTRING` elements are writable, and under a `String`
|
||||
node only `STRING`/`WSTRING` (other conversions would depend on the text). An array deployed read-only before #739
|
||||
turns writable on the next deploy. Unit- and XAR-gate-tested; real-hardware proof is pending.
|
||||
|
||||
### Notification size cap
|
||||
|
||||
ADS documents no notification size limit, and a live TwinCAT 2.11 target registered and
|
||||
|
||||
Reference in New Issue
Block a user