docs(s7-fixture): recapture the TM/CT addressing probe against the shipped profile; note CountingPlcFactory thread-safety

This commit is contained in:
Joseph Doherty
2026-08-17 05:47:43 -04:00
parent fd202d81ce
commit f797df5187
2 changed files with 19 additions and 7 deletions
@@ -142,17 +142,22 @@ counter to an `int` count (`Counter.FromByteArray`, raw word — **not** BCD).
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`:
real S7 CPU, put the device **number** in that field. Measured with S7netplus
0.20.0 against `10.100.0.35:1102` running **the profile above**:
```
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
ReadBytes(Timer,db=0,start=0,count=8) => [24 56 00 00 00 00 00 00] # TM bytes 0..7: T0 = 0x2456
ReadBytes(Timer,db=0,start=0,count=2) => [24 56] # T0 -> byte 0. 0>>3 == 2*0, so BOTH conventions agree here
ReadBytes(Timer,db=0,start=5,count=2) => [24 56] # T5 -> 5>>3 = byte 0. WRONG cell: T5 lives at bytes 10..11 (0x1123)
ReadBytes(Timer,db=0,start=80,count=2) => [11 23] # byte 10 needs start 80 (= 10*8): the T5 cell, reached the bit-address way
ReadBytes(Counter,db=0,start=0,count=2) => [12 67] # C0 -> byte 0 = 4711. Both conventions agree
ReadBytes(Counter,db=0,start=7,count=2) => [12 67] # C7 -> 7>>3 = byte 0. WRONG cell: C7 lives at bytes 14..15 (0x7A69 = 31337)
ReadBytes(Counter,db=0,start=112,count=2) => [7A 69] # byte 14 needs start 112 (= 14*8): the C7 cell
```
Note lines 2 and 3: asking for `T0` and asking for `T5` return the **same bytes**
— every device 0..7 collapses onto TM byte 0. Same for `C0` / `C7`.
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
@@ -488,6 +488,13 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
/// test can assert the wire shape rather than infer it. Delegates to the real factory, so
/// every counted call is the one production would have issued.
/// </summary>
/// <remarks>
/// <b>Not thread-safe, by design.</b> Every <c>ReadBatchAsync</c> builds its own factory and
/// the driver serialises all wire calls on its <c>_gate</c>, so nothing here is ever touched
/// concurrently today. A future caller that shared one factory across parallel reads would
/// silently corrupt the counts and shape lists rather than fail loudly — give it a lock, or
/// a factory per batch, before doing that.
/// </remarks>
/// <param name="inner">The factory producing the real connection.</param>
private sealed class CountingPlcFactory(IS7PlcFactory inner) : IS7PlcFactory
{