Merge branch 'master' into remed/lane-G2
This commit is contained in:
@@ -512,16 +512,16 @@ same pass (a UNS reference variable, say) are visible through the index with no
|
||||
|
||||
⚠️ **The value fan is now a single message.** It sends one
|
||||
`DriverInstanceActor.AttributeValueBatchPublished` carrying one item per RawPath, where it previously sent
|
||||
one `AttributeValuePublished` per ref — 5 000 messages onto the mux that also carries the node's steady
|
||||
one per-value message per ref — 5 000 messages onto the mux that also carries the node's steady
|
||||
value ingress, every time one driver flapped. Delivery is unchanged: the mux filters the batch per item
|
||||
against the same registered-interest map the per-value receive consulted, so only the framing moved. The
|
||||
**alarm** fan is indexed but deliberately not batched — one driver ref can back several condition NodeIds,
|
||||
so it still emits one `AlarmQualityUpdate` per condition.
|
||||
|
||||
**Consequence worth knowing: `AttributeValuePublished` now has no production sender at all.** This fan was
|
||||
its last one — everything on the subscription path was already wrapped in `AttributeValueBatchPublished`.
|
||||
`DependencyMuxActor` still registers the per-value receive, but in production it is dead code; it stays
|
||||
for tests and direct senders.
|
||||
**Consequence: the per-value `AttributeValuePublished` record is deleted.** This fan was its last
|
||||
production sender (everything on the subscription path was already wrapped in
|
||||
`AttributeValueBatchPublished`), so the record and `DependencyMuxActor`'s per-value receive were removed
|
||||
(2026-10-22). A single value is a one-item batch; tests build one with `ValuePublishes.Single`.
|
||||
|
||||
#### The third fan: the value nodes (archreview 03/S15)
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ This was not always true. Until #519 the publish path projected the driver's `ui
|
||||
`Good`/`Uncertain`/`Bad` enum in `DriverInstanceActor`, and `OtOpcUaNodeManager` re-expanded `Bad` to
|
||||
a bare `StatusCodes.Bad` (`0x80000000`). Every driver's diagnostic intent survived in logs only —
|
||||
including the 16 status constants #497 had just corrected across six drivers. The value path now
|
||||
carries the raw `uint` end to end (`AttributeValuePublished` → `AttributeValueUpdate` →
|
||||
`IOpcUaAddressSpaceSink.WriteValue`), and severity classification is a helper
|
||||
carries the raw `uint` end to end (`AttributeValueBatchPublished` → `AttributeValueBatchUpdate` →
|
||||
`IOpcUaAddressSpaceSink.WriteValues`), and severity classification is a helper
|
||||
(`OpcUaStatus.IsGood`/`IsUncertain`/`IsBad`) rather than a lossy projection.
|
||||
|
||||
**Writing a status code in engine code:** use a named constant on
|
||||
|
||||
+23
-1
@@ -198,10 +198,32 @@ scripted-alarm predicates) runs scripts through the shared `ScriptRunGuard`:
|
||||
2 s default) quarantines immediately, through the same path (Error log, metric
|
||||
`reason=stuck`, Bad). A short overrun (a young orphan) stays a plain refusal and
|
||||
the script resumes once the orphan completes.
|
||||
- **Cooperative cancellation (`ctx.CancellationToken`).** The guard cancels the run
|
||||
token when a run times out, and scripts see it as `ctx.CancellationToken`. A
|
||||
long-running loop that polls `ctx.CancellationToken.IsCancellationRequested` (or calls
|
||||
`ctx.CancellationToken.ThrowIfCancellationRequested()`) ends its own abandoned run
|
||||
and frees the slot, instead of holding a pool thread until it returns. A script that
|
||||
ignores the token is still an orphan. The editor completes the member, and
|
||||
`CancellationToken` is on the sandbox allow-list.
|
||||
- **Allocation budget.** A single run allocating more than
|
||||
`Scripting:MaxAllocBytesPerRun` (64 MB) is quarantined. This is sampled after the
|
||||
run returns, so a run that OOMs the process first is not stopped — that needs an
|
||||
out-of-process runner.
|
||||
- **Configuration keys and their ranges.** All three knobs are node-wide (the
|
||||
`Scripting` appsettings section, shared by virtual tags, Calculation tags and
|
||||
scripted alarms) and are clamped when bound, with one startup Warning per
|
||||
clamped value:
|
||||
|
||||
| Key | Default | Range after the clamp |
|
||||
|---|---|---|
|
||||
| `Scripting:MaxAllocBytesPerRun` | `67108864` (64 MB) | `0` (or negative) disables the budget; a positive value below `1048576` (1 MB) becomes 1 MB |
|
||||
| `Scripting:QuarantineAfterConsecutiveTimeouts` | `3` | `1`–`1000` |
|
||||
| `Scripting:StuckOrphanQuarantineFactor` | `10` | `0` (or negative) disables the rule; above `1000` becomes `1000`; `NaN` becomes the default |
|
||||
|
||||
A Calculation driver may override `maxAllocBytesPerRun` and
|
||||
`quarantineAfterConsecutiveTimeouts` in its own driver config. Those keys are
|
||||
layered on the node values and clamped to the same ranges, and an out-of-range
|
||||
value logs a Warning naming the driver. The stuck factor has no per-driver key.
|
||||
- **Lifting.** A quarantine lifts when the script source changes or on redeploy,
|
||||
never by itself.
|
||||
- **Script-log rate limit.** The cluster-wide script-log fan-out admits 20 entries
|
||||
@@ -243,7 +265,7 @@ prefix. The concrete implementation `ScriptTagCatalog` reads `Tag` +
|
||||
| Virtual tag | Leaf `Name` (best-effort) |
|
||||
|
||||
**Why only resolvable keys?** The live runtime resolves a `ctx.GetTag("X")`
|
||||
literal against `DriverInstanceActor.AttributeValuePublished.FullReference`,
|
||||
literal against `DriverInstanceActor.AttributeValueItem.FullReference`,
|
||||
which is the `FullName` field from `Tag.TagConfig`
|
||||
(see `ScriptTagCatalog.ExtractFullNameFromTagConfig` + `EquipmentNodeWalker.ExtractFullName`).
|
||||
The former UNS-path cascade engine (`Core.VirtualTags.VirtualTagEngine`, keyed by the
|
||||
|
||||
+16
-5
@@ -327,12 +327,23 @@ carried on the plan records (`RawTagPlan.ArrayWritable`, mirrored onto every UNS
|
||||
| **S7** | Yes | `Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Float32` (one contiguous block). Not `Bool` (would clobber neighbouring bits) nor the wide / String / DateTime types |
|
||||
| **OpcUaClient** | Yes | All — forwarded upstream unchanged; the upstream server decides (shape included) |
|
||||
| **Galaxy** | Yes | OPC UA `Boolean`/`Int32`/`Int64`/`Float`/`Double`/`String`/`DateTime` (the `MxValueEncoder` array variants) — **only with a declared `arrayLength`**: an undeclared length materialises an unbounded node, and a short whole-array write would *resize* the Galaxy attribute (mxaccessgw contract), so such a node stays read-only |
|
||||
| AB CIP, AB Legacy, TwinCAT, FOCAS, MQTT, MTConnect, Sql, Calculation | **No** — scalar-only write encoders; array nodes stay read-only and a client write is `BadNotWritable` | — |
|
||||
| **AB CIP** | Yes (#737) | `SInt`/`USInt`/`Int`/`UInt`/`DInt`/`UDInt`/`Real`/`Dt`/`LInt`/`ULInt`/`LReal` (`AbCipArrayCodec`, both transports). Not `Bool` (bit-packed DWORDs), `String` or `Structure`, nor a `safetyTag` |
|
||||
| **AB Legacy** | Yes (#738) | `Int`/`AnalogInt` on an N/B/A file, `Long` on an L file, `Float` on an F file — the encode width is the data file's, so a type that does not match its file stays read-only — on a plain `<file><number>:<word>` address, ≤ 256 elements. Not Bit / String / Timer / Counter / Control arrays, `/bit` or `.sub` addresses, indirect addresses, the `S:`/`I:`/`O:` files, nor any PLC-5 on the native transport |
|
||||
| **TwinCAT** | Yes (#739) | Every element type `TwinCATArrayCodec` lays out (numeric, `Bool`, `String`/`WString`, temporal); multi-dimensional PLC arrays are written row-major from the flat value. Not a BOOL bit-in-word path (`MAIN.Word.3`) |
|
||||
| FOCAS, MQTT, MTConnect, Sql, Calculation | **No** — scalar-only write encoders; array nodes stay read-only and a client write is `BadNotWritable` | — |
|
||||
|
||||
Modbus, S7 and Galaxy additionally need a **declared length** — `isArray: true` *and* an integer
|
||||
`arrayLength >= 1`. The Modbus and S7 tag factories read anything else as a **scalar** tag (whose
|
||||
scalar encoder cannot take an array), even though the composer still materialises an array node;
|
||||
that node stays read-only.
|
||||
Every "Yes" driver but OpcUaClient additionally needs a **declared length** — `isArray: true` *and* an
|
||||
integer `arrayLength >= 1`. The tag factories read anything else as a **scalar** tag (whose scalar
|
||||
encoder cannot take an array), even though the composer still materialises an array node; that node
|
||||
stays read-only.
|
||||
|
||||
**Elements convert; they are not exact-matched.** ScadaBridge writes CLR `int[]` / `List<int>` to Int16
|
||||
nodes: the node manager converts a numeric array to the node's element type, and the AB CIP, AB Legacy
|
||||
and TwinCAT encoders convert each element again to the controller's type (an element that does not fit
|
||||
is `BadOutOfRange`). Those rows therefore do not key on an exact node `DataType`; they leave out only
|
||||
node types whose elements never convert — a `DateTime` or `String` node over a numeric element, and a
|
||||
`Boolean` node on AB Legacy. An array tag deployed read-only before its driver gained an encoder turns
|
||||
writable on the next deploy (a surgical `ArrayWritable` update, no rebuild).
|
||||
|
||||
Each "Yes" row is pinned against the driver's real encoder by a parity test in that driver's
|
||||
test project (`*ArrayWriteSupportParityTests`), so a driver gaining or losing an array encoder
|
||||
|
||||
+3
-3
@@ -93,7 +93,7 @@ Virtual-tag nodes are materialised by `AddressSpaceApplier` and fed by `VirtualT
|
||||
|
||||
`ITagUpstreamSource` and `IHistoryWriter` are the two ports value consumers require from their host. Both live in `Core.VirtualTags`. In the v2 actor system:
|
||||
|
||||
- **Upstream-tag feed.** `DependencyMuxActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs`) routes `DriverInstanceActor.AttributeValuePublished` events to the `VirtualTagActor` instances that declared interest in those tag refs. Each `VirtualTagActor` holds the in-memory per-tag dependency map; the `IVirtualTagEvaluator` (`RoslynVirtualTagEvaluator`) receives the dependency snapshot synchronously on the actor message thread. Reads of never-pushed dependency refs return `null` values in the dependency snapshot.
|
||||
- **Upstream-tag feed.** `DependencyMuxActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs`) routes the items of each `DriverInstanceActor.AttributeValueBatchPublished` to the `VirtualTagActor` instances that declared interest in those tag refs. Each `VirtualTagActor` holds the in-memory per-tag dependency map; the `IVirtualTagEvaluator` (`RoslynVirtualTagEvaluator`) receives the dependency snapshot synchronously on the actor message thread. Reads of never-pushed dependency refs return `null` values in the dependency snapshot.
|
||||
- **`IHistoryWriter`** — the equipment-namespace path threads `Historize` end-to-end and `VirtualTagHostActor` invokes the injected writer on historized results (H5); the writer is resolved through `DriverHostActor` DI with a `NullHistoryWriter` default. No *durable* writer ships on this seam — durable continuous historization now flows through the separate `ContinuousHistorizationRecorder` → HistorianGateway `WriteLiveValues` path (see the `IHistoryWriter` section above and [Historian.md](Historian.md)).
|
||||
|
||||
## Composition
|
||||
@@ -104,7 +104,7 @@ In the v2 actor system, virtual-tag engine composition is owned by the driver-ro
|
||||
|
||||
- `AddressSpaceComposer.Compose` emits `DriverInstancePlan` / `ScriptedAlarmPlan` records; the driver-role `DriverHostActor` spawns one `VirtualTagActor` per virtual-tag expression and one `ScriptedAlarmActor` per scripted alarm.
|
||||
- `RoslynVirtualTagEvaluator` (`src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs`) is injected into each `VirtualTagActor` as its `IVirtualTagEvaluator`. It holds a per-source `CompiledScriptCache` keyed by script source and compiles on first use.
|
||||
- `DependencyMuxActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs`) receives every `DriverInstanceActor.AttributeValuePublished` event and routes it to the `VirtualTagActor` instances that registered interest in that tag ref.
|
||||
- `DependencyMuxActor` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs`) receives every `DriverInstanceActor.AttributeValueBatchPublished` and routes each item to the `VirtualTagActor` instances that registered interest in that tag ref.
|
||||
|
||||
`ITagUpstreamSource` and `IHistoryWriter` remain available as `Core.VirtualTags` ports.
|
||||
|
||||
@@ -125,7 +125,7 @@ In the v2 actor system, virtual-tag engine composition is owned by the driver-ro
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs` — driver-tag read + subscribe port
|
||||
- `src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs` — historize sink port + `NullHistoryWriter`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs` — actor that receives `DependencyValueChanged` from the mux and invokes `IVirtualTagEvaluator` per expression
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs` — routes `DriverInstanceActor.AttributeValuePublished` to interested `VirtualTagActor` subscribers
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyMuxActor.cs` — routes `DriverInstanceActor.AttributeValueBatchPublished` items to interested `VirtualTagActor` subscribers
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs` — production `IVirtualTagEvaluator` binding; holds a per-source `CompiledScriptCache`
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs` — pure data composer: config-DB entities → `AddressSpaceComposition` (UNS topology + raw subtree + driver/alarm plans)
|
||||
- `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs` — applies the composed plan into the SDK node manager
|
||||
|
||||
+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