test(s7): live-pin the Timer/Counter multi-var exclusion against a TM/CT-seeded s7_1500 fixture

This commit is contained in:
Joseph Doherty
2026-08-17 05:39:58 -04:00
parent 68461fbfb6
commit fd202d81ce
3 changed files with 353 additions and 21 deletions
@@ -76,7 +76,8 @@ without Docker running.
## What's encoded in `profiles/s7_1500.json`
DB1 (1024 bytes) + MB (256 bytes) with typed seeds at known offsets:
DB1 (1024 bytes) + MB (256 bytes) + TM0/CT0 (64 bytes each) with typed seeds at
known offsets:
| Address | Type | Seed | Purpose |
|---|---|---|---|
@@ -116,6 +117,58 @@ inside the holes cluster A's span reads through. No tag maps to them; they are
there so a mis-computed slice offset returns garbage instead of a zero that could
match by luck. Every seed above is distinct and non-zero for the same reason.
### Timer / Counter (TM / CT) areas
`TM0` and `CT0` are 64 bytes each — **32 device cells**, since a timer is a 2-byte
S5TIME word and a counter a 2-byte count word, with device number `N` at byte
offset `2 × N`. They exist so `S7_1500SpanReadTests` can pin, against real wire,
that `S7ReadSpanPlanner` excludes Timer/Counter from every span and the multi-var
pass excludes them again on `IsBufferType` — each takes its own area-typed
`ReadBytes` round-trip instead.
| Address | Byte offset | Seed | Decodes to |
|---|---|---|---|
| `T0` | 0 | `0x2456` | `456.0` s (S5TIME base 2 = ×1 s, BCD 456) |
| `T5` | 10 | `0x1123` | `12.3` s (base 1 = ×0.1 s, BCD 123) |
| `T9` | 18 | `0x0250` | `2.5` s (base 0 = ×0.01 s, BCD 250) |
| `C0` | 0 | `4711` | `4711` (raw big-endian count word) |
| `C7` | 14 | `31337` | `31337` |
| `C11` | 22 | `1234` | `1234` |
A timer decodes to seconds as a `double` (S7.Net `Timer.FromByteArray`) and a
counter to an `int` count (`Counter.FromByteArray`, raw word — **not** BCD).
#### ⚠️ Only device **0** is addressable on this fixture
Live-probed 2026-08-17: snap7's server resolves the TM/CT request address as a
**bit address** — it serves byte `addr >> 3` of the area — whereas S7.Net, and a
real S7 CPU, put the device **number** in that field. Measured directly against
`10.100.0.35:1102`:
```
ReadBytes(Timer,db=0,start=0,count=8) => [00 00 00 00 24 56 00 00] # bytes 0..7 (T2 seeded at byte 4)
ReadBytes(Timer,db=0,start=2,count=2) => [00 00] # 2>>3 = byte 0, NOT timer 2 at byte 4
ReadBytes(Timer,db=0,start=4,count=2) => [00 00] # 4>>3 = byte 0
ReadBytes(Timer,db=0,start=32,count=2) => [24 56] # 32>>3 = byte 4 ← the timer-2 cell
ReadBytes(Counter,db=0,start=48,count=2) => [12 67] # 48>>3 = byte 6 ← the counter-3 cell
```
No error is raised — the reads succeed, they just land on the wrong cell. Device
**0** is the single fixed point of the two conventions (`0 >> 3 == 2 × 0 == 0`),
so `T0` / `C0` are the only cells whose *value* the fixture can pin; that is what
`Timer_and_counter_tags_never_enter_a_multi_var_request` asserts, and the
wire-shape half of that test is unaffected by the quirk. The `2 × N` cells above
are seeded for a real CPU: run
`Timer_and_counter_decode_the_seeded_values_at_their_device_numbers` against one
with
```bash
S7_SIM_ENDPOINT=<cpu>:102 S7_TIMER_NUMBER_ADDRESSING=1 \
dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests
```
It skips (naming this limitation) whenever that variable is unset.
## Known limitations
From the `snap7.server.Server` docstring upstream:
@@ -130,6 +183,9 @@ Not exercised here — needs a lab rig:
- S7-1500 Optimized-DB symbolic access
- PG / OP / S7-Basic session-type differentiation
- PUT/GET-disabled-by-default enforcement
- Device-number addressing of the TM / CT areas (see the ⚠️ above — the reads are
served, but resolved as bit addresses, so only device 0 lands where a CPU
would put it)
See [`docs/drivers/S7-Test-Fixture.md`](../../../docs/drivers/S7-Test-Fixture.md)
for the full coverage map.