Recover stashed driver-gaps work from pre-v2-mxgw-merge working tree

Captures uncommitted work that lived in the working tree on
v2-mxgw-integration but was orthogonal to the migration. Stashed
during the v2-mxgw merge to master (2026-04-30) and replanted here on
a feature branch off master so it's git-visible rather than living in
the stash list.

Two distinct buckets:

1. Tracked fixture/config refinements (10 files, ~36 lines):
   - scripts/e2e/test-opcuaclient.ps1
   - src/ZB.MOM.WW.OtOpcUa.Admin/appsettings.json
   - 5 docker-compose.yml under tests/.../IntegrationTests/Docker/
     (AbCip, Modbus, OpcUaClient, S7)
   - 4 fixture .cs files (AbServerFixture, ModbusSimulatorFixture,
     OpcPlcFixture, Snap7ServerFixture)

2. Untracked driver-gaps queue artifacts (~8000 lines):
   - docs/plans/{abcip,ablegacy,focas,opcuaclient,s7,twincat}-plan.md
     — per-driver gap plans
   - docs/featuregaps.md — cross-cutting analysis
   - docs/v2/focas-deployment.md, docs/v2/implementation/focas-simulator-plan.md
   - followup.md — auto/driver-gaps queue follow-ups
   - scripts/queue/ — PR-queue automation tooling (12 files including
     pr-manifest.yaml at 1473 lines)

This commit is a snapshot for recoverability — review and split into
focused PRs (or discard) before merging anywhere downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 08:28:01 -04:00
parent ae7106dfce
commit 2d07d716dc
33 changed files with 8074 additions and 14 deletions

807
docs/plans/s7-plan.md Normal file
View File

@@ -0,0 +1,807 @@
# S7 Driver — Implementation Plan
> Source of gap analysis: [featuregaps.md → S7](../featuregaps.md#s7-siemens-s7-3004001200--1500)
>
> Covers Build = Yes items only. Skip-rated rows are noted at the end for context.
## Summary
The S7 driver (`src/ZB.MOM.WW.OtOpcUa.Driver.S7/`) ships a working scaffold over
**S7netplus 0.20**: ISO-on-TCP / S7comm, single-connection-per-PLC (`SemaphoreSlim`),
DB / M / I / Q / T / C address parsing, atomic scalar reads/writes for Bool / Byte
/ I16 / U16 / I32 / U32 / F32, polled `ISubscribable` overlay, `IHostConnectivityProbe`
via `ReadStatusAsync`, and a Snap7-server-backed CI fixture on `localhost:1102`.
The 16 Build = Yes gaps fall into six tractable phases. **The hard one is gap #1
(S7-1500 Optimized DB / Symbolic addressing)** — S7netplus speaks classic S7comm
only and cannot reach optimized DBs at all. Phase 6 calls that out as an explicit
architectural decision: ship the constraint as documentation and the rest as
S7netplus-compatible features, *or* fork to a library that supports S7Plus
(Sharp7-fork, Snap7 v2, custom S7Plus). Phases 1-5 do not depend on that decision
and are landable on the current S7netplus base.
Every PR ships unit-test coverage and — where wire semantics matter — extends the
Snap7-server profile in `Docker/server.py` so the integration fixture exercises
the new path. PRs that need real S7-1500 firmware features the simulator doesn't
mimic (PUT/GET protection, password-tier auth, SZL diagnostic buffer) call that
out and gate the live-firmware test on the dev-box S7-1500 lab rig.
Architectural invariants we explicitly preserve:
- Single connection per PLC; `_gate` (SemaphoreSlim) serializes every PDU.
- Strict address-parse-at-init; bad config fails fast with `FormatException`.
- PUT/GET-disabled mapped to sticky `BadDeviceFailure`, not Polly-retried.
- 100 ms minimum publishing interval (matches CPU mailbox scan reality).
- `WriteIdempotent` per-tag flag is the only retry-policy lever.
## Phased delivery
| Phase | Theme | PRs | Gaps closed |
|------:|-------|-----|-------------|
| 1 | Data-type correctness | PR-S7-A1..A5 | #7, #8, #9, #19 |
| 2 | Performance — multi-tag PDU packing | PR-S7-B1..B2 | #3, #22 |
| 3 | Operability knobs | PR-S7-C1..C5 | #2, #4, #20, #21, #24 |
| 4 | Workflow — symbol import + UDTs | PR-S7-D1..D3 | #5, #6, #10 |
| 5 | Diagnostics & security | PR-S7-E1..E2 | #11, #14 |
| 6 | S7-1500 Optimized DB / Symbolic | PR-S7-F (decision) | #1 |
Phases 1-3 run sequentially because Phase 2 packing and Phase 3 deadbands are
both keyed off the type-decode work in Phase 1. Phase 4 (UDT/symbol import) is
parallelizable with Phase 5; Phase 6 is gated on the library-choice decision
in Open Questions (a).
---
## Per-PR detail
### Phase 1 — Data-type correctness
#### PR-S7-A1 — 64-bit scalar types (LInt / ULInt / LReal / LWord)
Closes gap #9. `Float64`/`Int64`/`UInt64` cases in `S7Driver.ReadOneAsync`/
`WriteOneAsync` currently throw `NotSupportedException`.
- **Files**: `S7Driver.cs` (read + write switch), `S7DriverOptions.cs` (extend
`S7Size` with `LWord` for 8-byte access), `S7AddressParser.cs` (accept `DBL` /
`LD` size suffix; S7netplus encodes 8-byte access via byte-array reads, so the
parser converts `DB1.LD0` to a byte-range read internally).
- **Tests**: unit decode tests for the byte-pattern → `long` / `ulong` / `double`
conversion; Snap7-server profile gets `f64` and `i64` seed types.
- **Risks**: S7netplus's `ReadAsync(string)` does not accept `LD` natively;
fallback path is `Plc.ReadBytes(DataType.DataBlock, db, byteOffset, 8)` then
`BitConverter` with explicit endian flip (S7 is big-endian on the wire,
`BitConverter` is little-endian on x86/x64).
- **Effort**: M (3-4 days incl. tests).
- **Deps**: none.
- **Docs / fixture / e2e**: extends the type-mapping table in `docs/v2/s7.md`
with `LInt` / `ULInt` / `LReal` / `LWord` rows; adds the new sizes
(`LInt`, `ULInt`, `LReal`) to the `read` / `write` cookbook in
`docs/Driver.S7.Cli.md`; updates `docs/drivers/S7-Test-Fixture.md`
§"What it actually covers" to list the new 64-bit types and removes them
from §5 "Data types beyond the scalars"; extends the snap7 seed-type set
in `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/server.py`
with `i64`, `u64`, `f64` cases; adds seeds at known offsets
(e.g. `DB1.DBL40` for i64, `DB1.DBL48` for f64) to
`Docker/profiles/s7_1500.json`; adds `S7_1500Profile` constants for the
new tags + a `Driver_reads_seeded_64bit_batch` smoke test in
`S7_1500SmokeTests`; adds an LInt loopback assertion to
`scripts/e2e/test-s7.ps1`.
#### PR-S7-A2 — STRING / WSTRING / CHAR / WCHAR
Closes gap #8 (string portion). S7 `STRING(n)` is `[max-len][actual-len][bytes...]`
(2-byte header + ASCII). `WSTRING(n)` is 4-byte header + UTF-16BE bytes. `CHAR`
is 1 byte; `WCHAR` is 2 bytes UTF-16BE.
- **Files**: `S7Driver.cs` (new `ReadStringAsync` / `WriteStringAsync` private
helpers using `Plc.ReadBytes` for raw byte-range fetch), `S7DriverOptions.cs`
(already has `StringLength`; add `S7DataType.WString`, `Char`, `WChar`).
- **Tests**: unit tests for header parsing including the "actual-len > max-len"
PLC bug case (clamp on read, reject on write); Snap7 `ascii` seed type already
exists, add `wstring` seed.
- **Risks**: write must respect the configured `StringLength` to avoid overrunning
the DB; mismatched max-len is a common field bug.
- **Effort**: M.
- **Deps**: PR-S7-A1 (byte-range read helper lands there).
- **Docs / fixture / e2e**: extends the type-mapping section in
`docs/v2/s7.md` with `STRING(n)` / `WSTRING(n)` / `CHAR` / `WCHAR`
layouts (2-byte vs 4-byte header, UTF-16BE encoding, the "actual-len >
max-len" PLC bug); extends the `read` / `write` cookbook in
`docs/Driver.S7.Cli.md` with `--type WString` / `--type Char` / `--type
WChar` examples and the `--string-length` flag for WString; updates
`docs/drivers/S7-Test-Fixture.md` §"What it actually covers" to list
ascii/wstring/char/wchar; adds `wstring`, `char`, `wchar` seed types to
`Docker/server.py` (existing `ascii` covers STRING); seeds a
`DB1.WSTRING[256]` and a `DB1.CHAR[300]` in
`Docker/profiles/s7_1500.json`; adds `Driver_round_trips_string_types`
smoke test exercising read + write of every variant; adds a string
round-trip assertion to `scripts/e2e/test-s7.ps1`.
#### PR-S7-A3 — DTL / DATE_AND_TIME / S5TIME / TIME / TOD / DATE
Closes gap #8 (date/time portion).
- DTL is 12 bytes: year(u16) / month / day / weekday / hour / minute / second / nanos(u32).
- DATE_AND_TIME (DT) is 8 bytes BCD: yy mm dd hh mm ss msH msL+dow.
- S5TIME is 16-bit BCD with a 2-bit time-base.
- TIME is `Int32` ms since 1972 (S7-300/400) or signed-ms duration (S7-1200/1500).
- TOD is `UInt32` ms since midnight; DATE is `UInt16` days since 1990-01-01.
- **Files**: `S7Driver.cs` + new `S7DateTimeCodec.cs` static class encapsulating
every encode/decode (keep the driver lean; codec is unit-testable in isolation).
- **Tests**: round-trip tests per type with golden byte vectors taken from the
Siemens "STEP 7 V18 — Programming Reference" document. Snap7-server seed
profile gains `dtl`, `dt`, `s5time`, `time` types.
- **Risks**: BCD parsing must reject invalid month/day combinations; PLC programs
occasionally write 0x00 0x00 ... when uninitialized — surface as `BadOutOfRange`
rather than parsing to year 0.
- **Effort**: L (4-5 days incl. all six types and the golden-vector suite).
- **Deps**: PR-S7-A1.
- **Docs / fixture / e2e**: extends `docs/v2/s7.md` with a new "Date / time
types" subsection documenting DTL / DT (BCD) / S5TIME / TIME / TOD /
DATE byte layouts and the S7-300/400 vs S7-1200/1500 TIME-encoding
split; adds `--type Dtl` / `--type DateAndTime` / `--type S5Time` /
`--type Time` / `--type TimeOfDay` / `--type Date` to the
`docs/Driver.S7.Cli.md` cookbook; updates
`docs/drivers/S7-Test-Fixture.md` §"What it actually covers" with the
new datetime types and removes "DTL / DATE_AND_TIME" from §5 "Data
types beyond the scalars"; adds `dtl`, `dt`, `s5time`, `time`, `tod`,
`date` seed types to `Docker/server.py` with golden-byte vectors
documented in comments; seeds `DB1.DTL[260]`, `DB1.DT[272]`,
`DB1.S5TIME[280]`, `DB1.TIME[284]`, `DB1.TOD[288]`, `DB1.DATE[292]` in
`Docker/profiles/s7_1500.json`; adds
`S7DateTimeCodecTests` (unit) + `Driver_round_trips_datetime_types`
smoke test; no `scripts/e2e/test-s7.ps1` change required (CLI cookbook
examples cover the manual surface).
#### PR-S7-A4 — Array tags (ValueRank=1)
Closes gap #7. `S7TagDefinition` currently has no array dimension; `MapDataType`
hard-codes `IsArray: false`.
- **Files**: `S7DriverOptions.cs` (extend `S7TagDefinition` with `ArrayDim` int?
and `ElementCount` int?), `S7Driver.cs` (read path: detect array tag, issue
one byte-range read covering N elements, slice client-side; write path: same
in reverse), `DiscoverAsync` reports `IsArray: true, ArrayDim: [N]`.
- **Tests**: unit tests for `Array[0..9] of Int` and `Array[0..9] of Real`;
Snap7-server profile adds an array seed type. Round-trip array-write test
proves slice ordering.
- **Risks**: S7-1500 supports multi-dim arrays; declare ValueRank=1 only and
document multi-dim as a follow-up. Array-of-UDT lands with PR-S7-D2.
- **Effort**: M.
- **Deps**: PR-S7-A1 (byte-range reads).
- **Docs / fixture / e2e**: adds an "Array tags (ValueRank=1)" subsection
to `docs/v2/s7.md` documenting `Array[0..N]` syntax + the multi-dim
follow-up note; extends `docs/Driver.S7.Cli.md` with an
`--array-count N` flag in the `read` / `write` cookbook and worked
examples for `Array[0..9] of Int` and `Array[0..9] of Real`; updates
`docs/drivers/S7-Test-Fixture.md` §"What it actually covers" to list
array round-trips and removes "arrays of structs" from §5 (struct
arrays land in PR-S7-D2); extends `Docker/server.py` with an `array`
meta-seed-type that takes an inner-type + count and lays out N elements
contiguously; seeds `DB1.ArrayInt[300]` (10×Int) and
`DB1.ArrayReal[320]` (10×Real) in `Docker/profiles/s7_1500.json`;
adds `Driver_round_trips_array_int10` + `Driver_round_trips_array_real10`
smoke tests proving slice ordering; adds an array round-trip assertion
to `scripts/e2e/test-s7.ps1`.
#### PR-S7-A5 — LOGO! 8 + S7-200 V-memory area
Closes gap #19. `S7AddressParser` currently rejects the `V` area letter.
- **Files**: `S7AddressParser.cs` (add `V` case → maps to `S7Area.DataBlock` with
`DbNumber=1` for S7-200 / DbNumber per LOGO! VM-mapping table; document the
conversion), `S7DriverOptions.cs` (note CpuType-dependent meaning of V).
- **Tests**: unit tests for `VW0` / `VD4` / `V0.0` parsing, both S7-200 and
LOGO! conventions; document caller responsibility to set `CpuType.S7200` or
`S7200Smart`.
- **Risks**: LOGO! VM base address differs by firmware (V0=0 vs V0=1024 depending
on block); document the offset table rather than auto-detecting.
- **Effort**: S (1-2 days, mostly parser + tests; no wire changes).
- **Deps**: none.
- **Docs / fixture / e2e**: adds a "LOGO! 8 / S7-200 V-memory" subsection
to `docs/v2/s7.md` covering the `V` area letter, the `S7200` /
`S7200Smart` CpuType pre-requisite, the LOGO! VM-mapping table by
firmware band, and the "V0 = DB1.DBX0.0" semantic; extends the address
grammar cheat sheet in `docs/Driver.S7.Cli.md` with `VW0` / `VD4` /
`V0.0` rows and a `-c S7200Smart` worked example; updates
`docs/drivers/S7-Test-Fixture.md` §"What it does NOT cover" item 4 to
note S7-200 / LOGO! parser coverage now exists at unit level; adds
unit-only `S7AddressParserTests` cases — no Snap7 fixture change
(server.py already exposes DB1, which is where V-memory aliases land);
no `scripts/e2e/test-s7.ps1` change required (live-LOGO! testing is
documented as field-only).
### Phase 2 — Performance (multi-tag PDU packing + block coalescing)
#### PR-S7-B1 — Multi-variable PDU packing
Closes gap #3. `ReadAsync(IReadOnlyList<string>)` currently issues one
`plc.ReadAsync` per tag inside the semaphore — N PDUs for N tags.
- **Files**: `S7Driver.cs` (replace per-tag loop with a packer that builds a
list of `S7.Net.Types.DataItem`, calls `plc.ReadMultipleVarsAsync`, then
fans the results back to the per-tag decoder). Keep the existing per-tag
decode switch — only the wire fetch becomes batched.
- **Tests**: integration test that subscribes to 100 tags and asserts the
packet count seen by the Snap7 server is 1 (or N / packing-budget) rather
than 100. Unit-level test covers packer chunking when the negotiated PDU
size won't fit all items.
- **Risks**: `ReadMultipleVarsAsync` errors are per-item; we must surface
per-tag StatusCodes correctly rather than failing the whole batch on one
bad tag. Packing budget = `negotiatedPduSize - 18 (header) - per_item(12)`,
conservatively cap at 19 items per PDU on a 240-byte PDU.
- **Effort**: L (5-6 days incl. the per-item-error fan-out semantics).
- **Deps**: Phase 1 PRs do not block this — but conflicts in `S7Driver.cs`
are likely, so land Phase 1 first.
- **Docs / fixture / e2e**: adds a "Performance — multi-variable PDU
packing" subsection to `docs/v2/s7.md` describing
`ReadMultipleVarsAsync`, the negotiated-PDU packing budget formula
(`pdu - 18 - 12·N`), the 19-items-per-240-byte-PDU rule of thumb, and
the per-item-error semantics; no `docs/Driver.S7.Cli.md` change (CLI
is single-tag); no Snap7-server seed change required (existing seeds
cover the wire path); adds
`S7MultiVarPduPackingTests` to the unit suite (planner chunking when
items don't fit) + a 100-tag perf integration test
`Driver_packs_100_tags_into_minimum_pdus` that asserts request-count
reduction; no `scripts/e2e/test-s7.ps1` change required.
#### PR-S7-B2 — Block-read coalescing for contiguous DBs
Closes gap #22. Reading `DB1.DBW0`, `DB1.DBW2`, `DB1.DBW4` should issue one
6-byte byte-range read against DB1 starting at offset 0, sliced client-side.
- **Files**: `S7Driver.cs` adds a planner pass: group same-DB tags by
contiguous byte ranges (gap-merge threshold = configurable, default 16
bytes; over-fetching 16 bytes is cheaper than one extra PDU). Merged ranges
become a single `Plc.ReadBytes` call; the result is sliced per-tag.
- **Tests**: unit tests for the merge planner (input list → expected ranges);
integration test with 50 contiguous DB words proves wire-level reduction.
- **Risks**: STRINGs / arrays should opt out of merging because the per-tag
byte size is variable. Add an "opaque-size" flag so the planner skips them.
- **Effort**: M.
- **Deps**: PR-S7-B1 (the multi-var packer). The two interact: the planner
emits sum-reads, then the packer puts multiple sum-reads on one PDU.
- **Docs / fixture / e2e**: extends the §"Performance" section in
`docs/v2/s7.md` with a "Block-read coalescing" subsection — the
default 16-byte gap-merge threshold, the opaque-size opt-out for
STRINGs / arrays, and operator guidance for tuning the threshold per
DB; no CLI doc change; no Snap7-server seed change (existing
contiguous DB1 seeds — DBW0 / DBW10 / DBD20 — already exercise
contiguous-merge); adds
`S7BlockCoalescingPlannerTests` (unit) covering the merge planner +
opaque opt-out; adds a 50-contiguous-DBW integration test
`Driver_coalesces_contiguous_DBWs_into_single_byte_range_read` that
asserts wire-level reduction; no `scripts/e2e/test-s7.ps1` change.
### Phase 3 — Operability
#### PR-S7-C1 — PDU size negotiation surfaced
Closes gap #2. S7netplus's `Plc` instance exposes the negotiated PDU size after
`OpenAsync` via `Plc.MaxPDUSize`.
- **Files**: `S7Driver.cs` (read `Plc.MaxPDUSize` after open, store on
`_health`; expose via `GetHealth().Diagnostics["NegotiatedPduSize"]`
this requires adding a `Diagnostics` dictionary to `DriverHealth`, which
is a Core change). Operator-visible via the Admin UI driver-diagnostics
panel that already renders Modbus diagnostic stats.
- **Tests**: integration test asserts the value is non-zero after init.
- **Risks**: `DriverHealth` extension must be backward-compatible — existing
drivers should still compile against the unchanged record. Make the new
property nullable with a default of `null`.
- **Effort**: S.
- **Deps**: Core `DriverHealth` shape change (single PR coordinated with
the Modbus diagnostic surface).
- **Docs / fixture / e2e**: adds a "Diagnostics surfacing" subsection to
`docs/v2/s7.md` documenting the `Diagnostics["NegotiatedPduSize"]`
surface + how it renders in the Admin UI driver-diagnostics panel;
no CLI doc change (CLI doesn't expose diagnostics); updates
`docs/drivers/S7-Test-Fixture.md` §"What it actually covers" with a
"negotiated PDU size surfaces in driver health" line; no Snap7
seed-type change (snap7's PDU negotiation is fixed at 240 bytes —
document the fixture's negotiated size in the README); adds
`Driver_exposes_negotiated_pdu_size_post_init` smoke test asserting
the value is non-zero; no `scripts/e2e/test-s7.ps1` change.
#### PR-S7-C2 — TSAP / Connection Type selector
Closes gap #4. S7netplus picks PG-class TSAPs by default; hardened CPUs may
require OP / S7-Basic / Other.
- **Files**: `S7DriverOptions.cs` (new `TsapMode` enum: `Auto` / `Pg` / `Op` /
`S7Basic` / `Other`; `Auto` preserves current behavior. Optional
`LocalTsap` / `RemoteTsap` `ushort?` for explicit override). `S7Driver.cs`
branches on the mode to pick the S7netplus `Plc(CpuType, ...)` constructor
vs the `Plc(string ip, byte rack, byte slot, ushort localTsap, ushort remoteTsap)`
raw-TSAP overload. Document the raw-TSAP table in `docs/v2/s7.md`.
- **Tests**: unit test on the mode → TSAP-byte mapping; live-firmware test
documented but only runnable against the dev-box S7-1500 lab rig.
- **Risks**: wrong TSAP causes connection refused at handshake — same failure
shape as wrong slot. Document the mapping prominently.
- **Effort**: M.
- **Deps**: none.
- **Docs / fixture / e2e**: adds a "TSAP / Connection Type" section to
`docs/v2/s7.md` covering the `TsapMode` enum, the raw-TSAP table
(PG = 0x0100/0x0102, OP = 0x0200/0x0202, S7-Basic = 0x0300/0x0302,
Other = caller-supplied), and the hardened-CPU motivation; adds
`--tsap-mode` and `--local-tsap` / `--remote-tsap` flags to
`docs/Driver.S7.Cli.md`'s common-flags table with a worked example
hitting an OP-class TSAP; no Snap7 seed change (snap7 accepts any
TSAP from the CLI, so the unit-level mapping test is sufficient); no
smoke test change (live-firmware-only); no `scripts/e2e/test-s7.ps1`
change.
#### PR-S7-C3 — Per-tag scan group / publish rate
Closes gap #20. `SubscribeAsync` takes one publishing interval for the whole
list; mixed 100 ms / 1 s / 10 s tags need three subscribe calls today.
- **Files**: `S7DriverOptions.cs` (extend `S7TagDefinition` with optional
`ScanGroup` string). `S7Driver.cs` (`SubscribeAsync` partitions the input
list into one poll loop per distinct interval; `PollGroupEngine`-style
internal group, but driver-local — same engine the TwinCAT driver uses).
- **Tests**: unit test with three tags at three rates asserts three independent
poll-tick streams; integration test asserts no group starves the others.
- **Risks**: the `_gate` semaphore still serializes — three poll loops can
contend. Document the contention as part of the "1 connection / 1 mailbox"
invariant; if it bites, follow-up adds a fairness queue.
- **Effort**: M.
- **Deps**: none.
- **Docs / fixture / e2e**: adds a "Per-tag scan groups" subsection to
`docs/v2/s7.md` documenting `S7TagDefinition.ScanGroup`, the multi-rate
partitioning semantics, and the `_gate` contention caveat; no CLI doc
change (CLI is single-tag); no Snap7 seed change required (existing
scalar seeds suffice); adds `S7ScanGroupPartitioningTests` (unit) +
`Driver_three_scan_groups_publish_independently` smoke test that
subscribes 3 tags at 100 ms / 1 s / 10 s rates and asserts
independent tick streams; no `scripts/e2e/test-s7.ps1` change
(subscribe assertion already covers the polling path).
#### PR-S7-C4 — Deadband / on-change with thresholds
Closes gap #21. `PollOnceAsync` currently does `!Equals(prev, current)` only —
no analog deadband.
- **Files**: `S7DriverOptions.cs` (extend `S7TagDefinition` with
`DeadbandAbsolute double?` and `DeadbandPercent double?`). `S7Driver.cs`
(`PollOnceAsync` evaluates per-tag deadband for numeric types; non-numeric
types fall through to exact equality).
- **Tests**: unit tests for absolute and percent deadbands at edge cases
(NaN, ±Infinity, sign flip, near-zero percent).
- **Risks**: percent deadband against a zero baseline diverges; document and
fall back to absolute when |baseline| < 1e-6.
- **Effort**: S.
- **Deps**: PR-S7-C3 helpful but not required.
- **Docs / fixture / e2e**: adds a "Deadband / on-change" subsection to
`docs/v2/s7.md` documenting `DeadbandAbsolute` / `DeadbandPercent` per
tag, NaN / ±Infinity / sign-flip / near-zero-percent edge cases, and
the |baseline| < 1e-6 fallback; no CLI doc change (CLI's `subscribe`
already polls on change); no Snap7 seed change; adds
`S7DeadbandTests` (unit) covering all edge cases — no integration test
required since deadband is pre-publish filtering inside the polling
loop; no `scripts/e2e/test-s7.ps1` change.
#### PR-S7-C5 — Pre-flight PUT/GET enablement test
Closes gap #24. We currently surface `BadDeviceFailure` only at first read.
Add a pre-flight check during `InitializeAsync` (after `OpenAsync`) that issues
one trivial read (`MW0` or the configured `Probe.ProbeAddress`) and surfaces
the dedicated diagnostic message before declaring `DriverState.Healthy`.
- **Files**: `S7Driver.cs` (`InitializeAsync` adds the probe read; on
`S7.Net.PlcException` with the PUT/GET-disabled error code, throw a
typed `S7PutGetDisabledException` with a configuration-fix hint).
- **Tests**: integration test toggles a Snap7 simulator quirk that mimics
the PUT/GET-disabled response (Snap7 doesn't model this; gate the test
on a `--with-real-plc` opt-in or document as live-firmware-only).
- **Risks**: pre-flight against a real `Probe.ProbeAddress` requires the
address to exist in the PLC; document that the default `MW0` is fine for
most installs but allow `null` / "skip" for sites that haven't wired one.
- **Effort**: S.
- **Deps**: none.
- **Docs / fixture / e2e**: extends the "PUT/GET must be enabled" section
of `docs/Driver.S7.Cli.md` with the new typed
`S7PutGetDisabledException` message + the "skip pre-flight" knob;
adds the same content as a "Pre-flight PUT/GET enablement" subsection
in `docs/v2/s7.md`; no Snap7 seed change (snap7 doesn't model
PUT/GET-disabled — the test for the success path uses the existing
MW0 seed); adds `Driver_preflight_passes_when_probe_address_seeded`
smoke test; documents the live-firmware test as gated on a
`--with-real-plc` opt-in flag in `docs/drivers/S7-Test-Fixture.md`
§"Follow-up candidates"; no `scripts/e2e/test-s7.ps1` change (probe
test already runs first).
### Phase 4 — Workflow (symbol import + UDTs + instance DBs)
#### PR-S7-D1 — Symbol-table / TIA Portal export browse
Closes gap #5. Operators currently hand-edit `S7TagDefinition` JSON. TIA Portal
exports symbols as **`.s7p` archive → External tags → CSV / SDF**. The lighter
target is the CSV format used by the "Generate source from blocks" exporter.
- **Files**: new `src/ZB.MOM.WW.OtOpcUa.Driver.S7/SymbolImport/` directory:
- `TiaCsvImporter.cs` — parses TIA Portal "Show all tags" CSV (`Name`,
`Address`, `Data type`, `Comment`, `Visible in HMI`). Output: list of
`S7TagDefinition`.
- `AwlImporter.cs` — best-effort AWL `VAR_GLOBAL` / `DATA_BLOCK` parser
for legacy STEP 7 Classic projects.
- **Files (Admin UI)**: a "Import S7 symbols" button on the Driver Tags tab
that POSTs the file to a new `POST /api/drivers/{id}/import-s7-symbols`
endpoint and reports the diff.
- **Tests**: unit tests with golden-input CSV / AWL fixtures; round-trip
test that imports → produces tags → reads against simulator.
- **Risks**: TIA Portal CSV is locale-dependent (decimal-comma in DE locale).
Detect from the header row and accept both. UDT-typed symbols import as
a placeholder until PR-S7-D2.
- **Effort**: L (5-7 days incl. the Admin UI flow).
- **Deps**: see Open Question (c) — confirm CSV+AWL is the right scope, or
whether `.s7p` / `.zip` archive parsing is required.
- **Docs / fixture / e2e**: adds new doc
`docs/drivers/S7-TIA-Import.md` documenting the supported TIA Portal
CSV format (column names, locale-comma detection, UDT-typed
placeholders) and the AWL `VAR_GLOBAL` / `DATA_BLOCK` parser scope;
cross-links it from `docs/v2/s7.md`'s new "Symbol import" section
and from `docs/Driver.S7.Cli.md` with a future `import` subcommand
hook; adds golden-input fixtures
`tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Fixtures/sample_tia_export.csv`,
`sample_tia_export_de_locale.csv`, and `sample_step7_classic.awl`;
no Snap7 seed change required (existing DB1 seeds support
the import-then-read round-trip); adds `TiaCsvImporterTests` and
`AwlImporterTests` (unit) + `Driver_imports_csv_then_reads_seeded_tags`
integration test that imports the sample CSV → reads via Snap7;
no `scripts/e2e/test-s7.ps1` change (Admin-UI flow has its own
end-to-end coverage in the Admin UI test suite).
#### PR-S7-D2 — UDT / STRUCT / nested-DB handling
Closes gap #6. Today's tag map is flat scalar-only; UDT-typed DBs are
unusable without hand-flattening every member.
- **Files**: `S7DriverOptions.cs` (extend `S7TagDefinition` with `UdtName string?`;
alongside, a new `IReadOnlyList<S7UdtDefinition> Udts` on the options that
declares the layout: name, ordered members `(Name, Offset, S7DataType, ArrayDim?)`).
`S7Driver.cs` fans a UDT-typed tag into per-member sub-tags at `InitializeAsync`,
so the read/write path stays scalar-only.
- **Tests**: unit tests for fan-out with nested UDTs (UDT-of-UDT); integration
test with a Snap7 DB seeded as a UDT-shape byte array proves the fan-out
decodes correctly.
- **Risks**: UDT-of-UDT arbitrary nesting depth — cap at 4 levels and reject
deeper with a clear error. Optimized DBs would let TIA reorder members,
re-introducing gap #1; document that user-defined UDTs require "Optimized
block access" off, same as the general DB rule.
- **Effort**: L (1-2 weeks).
- **Deps**: PR-S7-D1 (symbol importer drops UDT-typed entries with a
placeholder; D2 makes those usable).
- **Docs / fixture / e2e**: adds a "UDT / STRUCT support" section to
`docs/v2/s7.md` documenting `S7UdtDefinition`, the fan-out
semantics, the 4-level nesting cap, and the "Optimized block access
must be off" prerequisite; extends `docs/drivers/S7-TIA-Import.md`
(created in PR-S7-D1) with a UDT-typed-entry section showing how
the importer + `Udts` declaration cooperate; updates
`docs/drivers/S7-Test-Fixture.md` §"What it does NOT cover" item 5 to
remove "UDT fan-out"; extends `Docker/server.py` with a
`udt_layout` meta-seed-type that lays out per-member offsets within
a DB byte range; seeds a `DB1.MyUdt[400]` (e.g. Real + Int + Bool)
in `Docker/profiles/s7_1500.json`; adds `S7UdtFanOutTests` (unit) +
`Driver_fans_out_udt_into_member_tags` integration test covering a
nested-UDT case; adds a UDT-member round-trip assertion to
`scripts/e2e/test-s7.ps1`.
#### PR-S7-D3 — Instance-DB / FB parameter access
Closes gap #10. Multi-instance FBs are addressed symbolically (`MyFB_Instance.MyParam`)
with no fixed absolute DB byte offset visible without a TIA project export.
- **Files**: extends PR-S7-D1's importer to recognize "instance DB" entries
(TIA export shows them with a different "DB type" column value); the
importer translates `MyFB_Instance.MyParam` to the resolved
`DBn.DBW_offset` based on the FB's interface declaration in the export.
- **Tests**: golden-input test with an FB-instance DB export; resolved
addresses match Siemens reference.
- **Risks**: when the FB interface changes (TIA "online change"), instance-DB
layouts shift. Document that re-import is required after any FB-interface
edit. Eventually surface this as a startup warning when the symbol-table
hash differs from the imported snapshot — out of scope for this PR.
- **Effort**: M.
- **Deps**: PR-S7-D1, PR-S7-D2.
- **Docs / fixture / e2e**: extends `docs/drivers/S7-TIA-Import.md` with
an "Instance DBs / FB parameters" section covering the importer's
`MyFB_Instance.MyParam``DBn.DBW_offset` resolution, the "DB type"
column convention, and the "re-import on FB-interface edit" caveat;
adds the same caveat as a paragraph in `docs/v2/s7.md`'s "UDT /
STRUCT" section; adds a golden-input fixture
`Fixtures/sample_tia_export_with_fb_instance.csv` to the integration
tests; no Snap7 seed change required (resolved addresses land in DB1
which the existing seeds back); adds
`InstanceDbResolverTests` (unit) +
`Driver_resolves_fb_instance_then_reads_seeded_member` integration
test; no `scripts/e2e/test-s7.ps1` change (FB-instance lookup is an
import-time concern).
### Phase 5 — Diagnostics & security
#### PR-S7-E1 — CPU diagnostic buffer / SZL reads
Closes gap #11. SZL (System Status List) IDs surface CPU type, firmware
version, cycle-time min/avg/max, and the diagnostic-buffer entries.
- **Files**: `S7Driver.cs` exposes a small set of "system tags" alongside
`Tags` — virtual addresses prefixed `@System.` that the read path
recognizes and dispatches to S7netplus's `ReadSzlAsync` (or, if not
exposed, a raw `Plc.ReadBytes` against the SZL-via-S7comm sub-protocol):
- `@System.CpuType`, `@System.Firmware`, `@System.OrderNo` — SZL 0x0011
- `@System.CycleMs.Min` / `.Max` / `.Avg` — SZL 0x0132 / 0x0432
- `@System.DiagBuffer[0..N]` — SZL 0x00A0 ring-buffer entries
- **Files (discovery)**: `DiscoverAsync` adds a `Diagnostics/` subfolder
with the system-tag set when `S7DriverOptions.ExposeSystemTags = true`.
- **Tests**: unit tests for the SZL response parser (golden bytes); live-
firmware test against the dev-box S7-1500.
- **Risks**: S7netplus's SZL surface is incomplete; may need a raw
`Plc.ReadBytes` against `0x84` register or a small SZL-PDU helper.
- **Effort**: M-L.
- **Deps**: PR-S7-C1 (`DriverHealth.Diagnostics` dictionary already there).
- **Docs / fixture / e2e**: adds a "CPU diagnostics (SZL)" section to
`docs/v2/s7.md` listing the exposed `@System.*` virtual addresses, the
underlying SZL IDs, and the `ExposeSystemTags` opt-in; extends
`docs/Driver.S7.Cli.md` with a worked `read -a @System.CpuType` example
in the cookbook; updates `docs/drivers/S7-Test-Fixture.md` §"What it
does NOT cover" with a note that snap7 does not implement SZL — golden-
byte unit tests cover the parser, live SZL is gated on a real S7-1500;
no Snap7 seed change (snap7 returns a fixed handshake banner that the
test checks for "SZL not supported on simulator" branch); adds
`S7SzlParserTests` (unit) with golden bytes; documents the live SZL
test in `docs/drivers/S7-Test-Fixture.md` §"Follow-up candidates"; no
`scripts/e2e/test-s7.ps1` change.
#### PR-S7-E2 — PLC password / protection-level handling
Closes gap #14. S7-300/400 protection levels 1-3 and S7-1200/1500 connection
mechanisms can require a password on connect.
- **Files**: `S7DriverOptions.cs` (new `Password string?` and `ProtectionLevel`
enum). `S7Driver.cs` calls S7netplus's `SetPassword` (if the API surfaces it
— newer S7netplus versions ship `Plc.SendPassword(string)`; if not, raw-PDU
fallback per Siemens "Communication Function Manual" §5.2).
- **Tests**: live-firmware-gated; password-tier failure modes don't reproduce
in Snap7. Unit-level coverage for the options-binding shape only.
- **Risks**: S7netplus may not expose password auth — fallback is to call into
the lower-level `S7.Net.S7Protocol` types or to fork. Land the options
surface unconditionally, gate the wire path on library support, document
the limitation if the library doesn't oblige.
- **Effort**: M (S if S7netplus ships it; L if we need a fallback path).
- **Deps**: none.
- **Docs / fixture / e2e**: adds a "PLC password / protection levels"
section to `docs/v2/s7.md` documenting the `Password` /
`ProtectionLevel` options + the S7-300/400 levels 1-3 vs S7-1200/1500
connection-mechanism semantics + the "limitation if S7netplus
doesn't ship `SendPassword`" note; adds a `--password` flag to
`docs/Driver.S7.Cli.md`'s common-flags table with a hardened-CPU
worked example; updates `docs/drivers/S7-Test-Fixture.md` §"What it
does NOT cover" with a "password / protection levels not modelled by
snap7" note; no Snap7 seed change (snap7 doesn't enforce protection
levels); adds options-binding unit tests only — no integration test
(live-firmware-only); no `scripts/e2e/test-s7.ps1` change.
### Phase 6 — S7-1500 Optimized DB / Symbolic addressing (decision PR)
#### PR-S7-F — Optimized DB / S7Plus
Closes gap #1. **This is an architectural decision PR, not a code PR.**
S7netplus speaks classic S7comm only. Optimized DBs on S7-1500 (default for
new TIA projects) reorder fields and have no fixed byte offsets — absolute
`DB1.DBW0` reads return `BadDeviceFailure`. Three tracks:
1. **Document the constraint and stay on S7netplus.** Operators must uncheck
"Optimized block access" in TIA Portal for any DB the driver reads. This
is what the test fixture already documents. Effort: S (docs only).
2. **Migrate to a library that supports S7Plus.**
- **Snap7 v2 / `Snap7Net`** — C-library wrapper, supports classic S7comm
only (same limitation as S7netplus). Not a fix.
- **Sharp7 fork** — community fork of Snap7 with **partial** S7-1200/1500
PUT/GET semantics. Still classic S7comm.
- **Custom S7Plus implementation** — Wireshark dissector exists; reverse
engineering is substantial. Effort: ≥ 4 weeks; ongoing protocol-version
maintenance. Risk: Siemens has not published S7Plus.
3. **Embed an OPC UA → OPC UA bridge to the S7-1500's onboard OPC UA server.**
The S7-1500 V2.5+ exposes its own OPC UA server with full symbolic access.
Our `OPC UA Client driver` (already shipping per memory) could read the
target CPU's OPC UA server and re-publish — sidesteps S7Plus entirely.
Effort: S; semantics: requires the customer to license Siemens OPC UA
on the CPU. Most modern S7-1500 deployments already license it.
**Recommendation**: ship Track 1 docs immediately (closes the operator
expectation gap) and Track 3 as the Optimized-DB workflow path (re-uses
existing OPC UA Client driver). Track 2 (S7Plus reverse-engineering) is
out of scope unless a customer pays for it.
- **Files**: `docs/v2/s7.md` (Optimized DB section + how to disable),
`docs/featuregaps.md` row #1 updated to reflect the Track 1+3 decision.
- **Tests**: live-firmware test against the dev-box S7-1500 with optimized
block access toggled both ways, asserting `BadDeviceFailure` vs
successful read.
- **Risks**: Track 3's OPC-UA-Client-bridging needs Admin UI plumbing to
configure; that's a larger workstream tracked separately.
- **Effort**: S (docs + decision); L if Track 2 is taken.
- **Deps**: Open Question (a) below.
- **Docs / fixture / e2e**: rewrites `docs/v2/s7.md` to land a
prominent "Optimized DB constraint" section at the top — explicitly
documents the S7-1200 V4.0+ / S7-1500 default, the
`BadDeviceFailure` shape on absolute `DB1.DBW0` reads against an
optimized DB, the "Uncheck Optimized block access in TIA Portal"
fix, and the recommended **bridge-via-OpcUaClient** pattern with a
worked example (Siemens S7-1500 V2.5+ onboard OPC UA server →
`OpcUaClient` driver → re-publish on the OtOpcUa server's address
space); updates `docs/featuregaps.md` row #1 to reflect the
Track 1+3 decision; updates the "Optimized-DB" line of
`docs/drivers/S7-Test-Fixture.md` §"What it does NOT cover" item 4
to point at the new doc; no CLI doc change (CLI is a probe tool, not
the bridging path); no Snap7 fixture change (snap7 has no Optimized-
DB mode); the live-firmware test toggling Optimized block access on
/ off is recorded as a manual checklist in
`docs/drivers/S7-Test-Fixture.md` §"Follow-up candidates" and gated
behind `--with-real-plc`; if Track 2 is taken later, this PR's doc
surface becomes the migration baseline; no `scripts/e2e/test-s7.ps1`
change.
---
## Documentation, fixture, and e2e impact
Consolidated view of every per-PR `Docs / fixture / e2e` line above, so a
reviewer can see the cross-cutting churn at a glance and so the doc /
fixture / e2e maintainers can sequence their work alongside the code PRs.
### User-facing documentation churn
| PR | `docs/v2/s7.md` | `docs/Driver.S7.Cli.md` | `docs/drivers/S7-Test-Fixture.md` | New / cross-cut docs |
|----|-----------------|-------------------------|------------------------------------|----------------------|
| PR-S7-A1 (LInt/ULInt/LReal/LWord) | extend type-mapping table | new sizes in cookbook | remove "no 64-bit types" | — |
| PR-S7-A2 (STRING/WSTRING/CHAR/WCHAR) | string layout subsection | `--type WString` / `--string-length` | list new types | — |
| PR-S7-A3 (DTL/DT/S5TIME/TIME/TOD/DATE) | "Date / time types" subsection | datetime cookbook entries | list new types | — |
| PR-S7-A4 (arrays) | "Array tags (ValueRank=1)" subsection | `--array-count` flag + examples | list array round-trips | — |
| PR-S7-A5 (V-memory) | "LOGO! 8 / S7-200 V-memory" subsection | grammar table + S7200Smart example | parser coverage note | — |
| PR-S7-B1 (PDU packing) | "Performance — multi-variable PDU packing" subsection | — | — | — |
| PR-S7-B2 (block coalescing) | "Block-read coalescing" subsection | — | — | — |
| PR-S7-C1 (negotiated PDU diag) | "Diagnostics surfacing" subsection | — | "negotiated PDU size" line | — |
| PR-S7-C2 (TSAP) | "TSAP / Connection Type" section | `--tsap-mode` / `--local-tsap` / `--remote-tsap` flags | — | — |
| PR-S7-C3 (scan groups) | "Per-tag scan groups" subsection | — | — | — |
| PR-S7-C4 (deadband) | "Deadband / on-change" subsection | — | — | — |
| PR-S7-C5 (PUT/GET pre-flight) | "Pre-flight PUT/GET enablement" subsection | extend "PUT/GET must be enabled" | mark live-firmware test | — |
| PR-S7-D1 (TIA CSV / AWL import) | "Symbol import" cross-link | future `import` subcommand stub | — | **new `docs/drivers/S7-TIA-Import.md`** |
| PR-S7-D2 (UDT / STRUCT) | "UDT / STRUCT support" section | — | remove "UDT fan-out" | extend `S7-TIA-Import.md` |
| PR-S7-D3 (instance DB) | re-import-on-FB-edit caveat | — | — | extend `S7-TIA-Import.md` |
| PR-S7-E1 (SZL diagnostics) | "CPU diagnostics (SZL)" section | `read -a @System.CpuType` example | "SZL not modelled by snap7" + Follow-up | — |
| PR-S7-E2 (PLC password) | "PLC password / protection levels" section | `--password` flag | "password not modelled by snap7" | — |
| PR-S7-F (Optimized DB / S7Plus) | top-level "Optimized DB constraint" + bridge-via-OpcUaClient worked example | — | point §"What it does NOT cover" at new doc | also updates `docs/featuregaps.md` row #1 |
### Snap7-server fixture seed-type additions per PR
The snap7 simulator at `localhost:1102` (driven by
`tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/server.py` +
`Docker/profiles/s7_1500.json`) has a `seed_buffer` pump with a fixed type
set — `u8 / i8 / u16 / i16 / u32 / i32 / f32 / bool / ascii`. New PRs need
new seed-type cases in `server.py`, new offsets in `s7_1500.json`, and
matching constants in `S7_1500Profile.cs`. The table below names the
delta for each Build-Yes PR:
| PR | New `server.py` seed types | New `s7_1500.json` seed offsets | `S7_1500Profile.cs` additions |
|----|----------------------------|----------------------------------|-------------------------------|
| PR-S7-A1 | `i64`, `u64`, `f64` | `DB1.DBL40` (i64), `DB1.DBL48` (f64), `DB1.DBL56` (u64) | `SmokeI64Tag` / `SmokeU64Tag` / `SmokeF64Tag` |
| PR-S7-A2 | `wstring`, `char`, `wchar` (existing `ascii` covers STRING) | `DB1.WSTRING[256]`, `DB1.CHAR[300]` | `SmokeWStringTag` / `SmokeCharTag` |
| PR-S7-A3 | `dtl`, `dt`, `s5time`, `time`, `tod`, `date` (golden-byte vectors in comments) | `DB1.DTL[260]`, `DB1.DT[272]`, `DB1.S5TIME[280]`, `DB1.TIME[284]`, `DB1.TOD[288]`, `DB1.DATE[292]` | `SmokeDtl` / `SmokeDt` / `SmokeS5Time` / `SmokeTime` / `SmokeTod` / `SmokeDate` |
| PR-S7-A4 | `array` meta-seed (inner-type + count) | `DB1.ArrayInt[300]` 10×Int, `DB1.ArrayReal[320]` 10×Real | `ArrayInt10Tag` / `ArrayReal10Tag` |
| PR-S7-A5 | none (V-memory aliases land in DB1, which `server.py` already exposes) | none | unit-only — no profile change |
| PR-S7-B1 | none | none (existing scalar seeds suffice for packing) | none — perf integration test reuses scalar tags |
| PR-S7-B2 | none | none (existing contiguous DBW0 / DBW10 / DBD20 already test merge) | none |
| PR-S7-C1 | none | none | none |
| PR-S7-C2 | none (snap7 accepts any TSAP) | none | none |
| PR-S7-C3 | none | none | none |
| PR-S7-C4 | none | none | none |
| PR-S7-C5 | none (existing `MK0` MW0 seed covers success path) | none | none |
| PR-S7-D1 | none (CSV import lands tags pointing at existing seeds) | none | possibly add fixture-pointer constants |
| PR-S7-D2 | `udt_layout` meta-seed (per-member offsets) | `DB1.MyUdt[400]` (Real + Int + Bool layout) | `MyUdtTag` + member tags |
| PR-S7-D3 | none (resolved addresses land in DB1) | none | none |
| PR-S7-E1 | none — snap7 doesn't model SZL; unit-level golden bytes cover the parser | none | none |
| PR-S7-E2 | none — snap7 doesn't enforce protection levels; options-binding unit tests only | none | none |
| PR-S7-F | none — snap7 has no Optimized-DB mode; live-firmware checklist instead | none | none |
### E2E `scripts/e2e/test-s7.ps1` impact
`scripts/e2e/test-s7.ps1` runs the five-assertion CLI loopback (probe /
driver-loopback / forward-bridge / reverse-bridge / subscribe-sees-change)
against `DB1.DBW0` Int16. Build-Yes PRs that add CLI surface get a
matching loopback assertion; PRs that touch only internals or admin-UI
flows do not.
| PR | E2E script change |
|----|-------------------|
| PR-S7-A1 | add LInt loopback assertion (write 0x7FFFFFFFFFFFFFFF, read back) |
| PR-S7-A2 | add string round-trip assertion |
| PR-S7-A3 | none (CLI cookbook covers manual surface) |
| PR-S7-A4 | add array round-trip assertion |
| PR-S7-A5 | none (live-LOGO! field-only) |
| PR-S7-B1 | none |
| PR-S7-B2 | none |
| PR-S7-C1 | none |
| PR-S7-C2 | none (live-firmware-only) |
| PR-S7-C3 | none (subscribe assertion already covers polling) |
| PR-S7-C4 | none |
| PR-S7-C5 | none (probe runs first today) |
| PR-S7-D1 | none (Admin UI has its own e2e) |
| PR-S7-D2 | add UDT-member round-trip assertion |
| PR-S7-D3 | none (import-time concern) |
| PR-S7-E1 | none |
| PR-S7-E2 | none (live-firmware-only) |
| PR-S7-F | none (decision PR; live-firmware checklist instead) |
---
## Skip-rated items (for context)
| # | Gap | Skip rationale |
|---|-----|---------------|
| 12 | AS-Alarms / Alarm_S / ProDiag | Alarms are a separate workstream; no `IAlarmSource` shipped on this driver yet, and the gap analysis flags it as a deferred topic. |
| 13 | CPU Run / Stop control / block download | Security and safety risk. PG-class writes that change CPU state are explicitly out of scope. |
| 15 | S7-1500 Secure Communication / TLS | Significant work; S7netplus has no TLS surface. Reconsider when S7Plus track is taken. |
| 16 | S7-400H redundant H-system support | Rare in our deployment scope. Server-level redundancy (`docs/Redundancy.md`) covers the OPC UA layer; H-system driver-level failover is a separate axis. |
| 17 | Multi-CPU rack parallel sessions | One session per CPU works for the deployments we target; multi-CPU racks are an S7-400 niche. |
| 18 | MPI / Profibus / RFC1006-routed transports | Declining use; brownfield only. S7netplus is Ethernet-only. |
| 23 | Connection-resource budget / parallel jobs | One connection works; premature optimization until a deployment hits the cap. |
---
## Open questions
### (a) Library choice for S7Plus
PR-S7-F gates on this decision. Options:
1. **Stay on S7netplus + document Optimized-DB constraint** (preferred default).
2. **Fork to Sharp7 / Snap7 v2** — does *not* solve the S7Plus / Optimized-DB
problem; both are classic S7comm only. Adopting them buys nothing for this
gap. Reject unless we want it for unrelated reasons.
3. **Custom S7Plus client over Wireshark-dissected protocol** — large effort,
ongoing maintenance risk. Only if a customer is paying.
4. **OPC UA → OPC UA bridge via existing OPC UA Client driver** — sidesteps
S7Plus by re-using Siemens's onboard OPC UA server. Recommended secondary
track.
Decision needed before Phase 6 PR-S7-F kicks off.
### (b) `WriteIdempotent` semantics for new types
The `WriteIdempotent` per-tag flag (decisions #44, #45, #143) governs replay-
safe writes. New types from Phase 1:
- **STRING / WSTRING** — typically idempotent (recipe / message text).
Replay-safe by default? **Need confirmation.** Risk: PLC programs that
treat a new string write as a "new message" event would double-fire.
- **DTL / DT** — usually written from a clock master; replay-safe.
- **Arrays of UDT** — depends on the UDT semantics (recipe = safe, command
block = unsafe). Inherit `WriteIdempotent` from the parent tag, do not
add a per-member flag.
- **64-bit types** — same rule as 32-bit equivalents.
Default: keep `WriteIdempotent = false` for everything. Operators flip per
tag based on PLC program semantics. **No semantic extension needed**, but
document the per-type guidance in `docs/v2/s7.md`.
### (c) Symbol-import file format(s)
PR-S7-D1 ships an importer. Which formats?
- **TIA Portal CSV** (Show all tags / Export) — preferred entry point;
most common. **Confirm.**
- **TIA Portal SDF / Excel** — same data; harder to parse. Skip unless
customer demand emerges.
- **STEP 7 Classic AWL / SCL `.AWL`** — secondary. Useful for legacy
S7-300/400 sites still on Classic. **Include in D1?**
- **`.s7p` / `.zap` project archive** — full TIA project. ZIP-shaped;
symbol export would require unpacking and parsing internal XML. Large
scope. **Defer.**
- **`.udt` / `.SDF` external tag library** — niche; defer unless asked.
Recommendation: PR-S7-D1 ships **TIA CSV** + **AWL** only. Anything else is
a follow-up. Decision needed before Phase 4 work begins.