test(s7): live-pin the Timer/Counter multi-var exclusion against a TM/CT-seeded s7_1500 fixture
This commit is contained in:
@@ -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.
|
||||
|
||||
+29
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_description": "S7-1500 profile — single DB1 (1024 bytes) + MB (256 bytes) with well-known seeds at named offsets for the smoke + byte-order + string tests. Big-endian Siemens wire order throughout.",
|
||||
"_description": "S7-1500 profile — DB1 (1024 bytes) + MB (256 bytes) + TM/CT (64 bytes each) with well-known seeds at named offsets for the smoke + byte-order + string + Timer/Counter tests. Big-endian Siemens wire order throughout.",
|
||||
"areas": [
|
||||
{
|
||||
"area": "DB",
|
||||
@@ -59,6 +59,34 @@
|
||||
{ "_desc": "MW10 — span-planner gate: a narrow scalar in a DIFFERENT area from the DB1 leftovers. A block read can never merge across (area, DB); the multi-var pass can, so this pins that the leftover batching crosses the boundary.",
|
||||
"offset": 10, "type": "u16", "value": 4097 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"_desc": "TM (timers) — 64 bytes = 32 timer cells. A timer is a 2-byte S5TIME word and timer number N lives at byte offset 2*N, so the offsets below ARE the timer numbers doubled. Values are S5TIME-encoded (bits 12..13 = time base, the remaining three nibbles = a BCD 0..999 count) and each decodes to a distinct NON-ZERO duration, because a zero-valued timer reads the same as a timer that was never served at all (the #536 trap). ⚠️ Only T0 is readable through this fixture — see the CT note below and Docker/README.md §'Timer / Counter (TM / CT) areas'.",
|
||||
"area": "TM",
|
||||
"index": 0,
|
||||
"size": 64,
|
||||
"seeds": [
|
||||
{ "_desc": "T0 @ byte 0 — 0x2456: base 2 (×1s), BCD 456 ⇒ 456.0 seconds. Device 0 is the one cell where snap7's address resolution and a real CPU's agree, so this is the seed S7_1500SpanReadTests asserts on.",
|
||||
"offset": 0, "type": "u16", "value": 9302 },
|
||||
{ "_desc": "T5 @ byte 10 — 0x1123: base 1 (×0.1s), BCD 123 ⇒ 12.3 seconds. Reachable only from a real CPU (S7_TIMER_NUMBER_ADDRESSING=1).",
|
||||
"offset": 10, "type": "u16", "value": 4387 },
|
||||
{ "_desc": "T9 @ byte 18 — 0x0250: base 0 (×0.01s), BCD 250 ⇒ 2.5 seconds. Real CPU only, as above.",
|
||||
"offset": 18, "type": "u16", "value": 592 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"_desc": "CT (counters) — 64 bytes = 32 counter cells, counter number N at byte offset 2*N. The driver surfaces a counter as the RAW big-endian word (S7Driver.DecodeScalarBlock via S7.Net's Counter.FromByteArray), so these seeds are plain u16 counts; distinct + non-zero for the same reason as the timers. ⚠️ snap7's server resolves the TM/CT request address as a BIT address (byte = addr>>3) instead of as a device NUMBER, while S7.Net (and a real CPU) put the device number there — so a read of C{n} lands on byte n>>3, and device 0 is the only n where the two conventions coincide. Live-probed 2026-08-17; see Docker/README.md.",
|
||||
"area": "CT",
|
||||
"index": 0,
|
||||
"size": 64,
|
||||
"seeds": [
|
||||
{ "_desc": "C0 @ byte 0 — count 4711. The device-0 cell both conventions agree on; asserted by S7_1500SpanReadTests.",
|
||||
"offset": 0, "type": "u16", "value": 4711 },
|
||||
{ "_desc": "C7 @ byte 14 — count 31337. Reachable only from a real CPU (S7_TIMER_NUMBER_ADDRESSING=1).",
|
||||
"offset": 14, "type": "u16", "value": 31337 },
|
||||
{ "_desc": "C11 @ byte 22 — count 1234. Real CPU only, as above.",
|
||||
"offset": 22, "type": "u16", "value": 1234 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user