docs: clarify S7 counter evidence and fixture limits

This commit is contained in:
Joseph Doherty
2026-10-13 11:34:59 -04:00
parent 76bdca90e9
commit cd50db136e
3 changed files with 78 additions and 25 deletions
@@ -27,7 +27,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
/// areas: <b>PASSED</b> — 4 passed, 0 failed, 1 skipped (the device number-addressing test,
/// see below). The Timer/Counter batch was served by 4 requests for 7 tags: one DB1 span,
/// one 3-item multi-var carrying only the scalars, and one 2-byte area read apiece for
/// <c>T0</c> (decoded 456.0 s from S5TIME <c>0x2456</c>) and <c>C0</c> (decoded count 4711).
/// <c>T0</c> (decoded 456.0 s from S5TIME <c>0x2456</c>) and <c>C0</c> (surfaced raw word 4711).
/// </para>
/// <para>
/// <b>Counter evidence limit.</b> The profile's C0=4711 and C7=31337 are synthetic raw
/// words, outside classic SIMATIC counts 0..999. The device-0 record proves the fixture's
/// raw-word decode and batching exclusion, not classic count semantics or nonzero
/// addressing. BuildOptions retains S71500/rack 0/slot 0; a target-specific semantic gate
/// still needs independent PLC counts and binary/BCD references paired with captured
/// C-area bytes, CPU model/firmware and connection settings (docs/drivers/S7.md, #616).
/// </para>
/// <para>
/// <b>What the layout buys.</b> The ten tags are positioned so the planner's rules produce a
@@ -73,11 +81,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
/// returns TM bytes 0..1, and it takes <c>start 80</c> (= 10 × 8) to reach the timer seeded
/// at TM byte 10. Device <b>0</b> is the single fixed point of the two conventions
/// (<c>0 &gt;&gt; 3 == 2 × 0 == 0</c>), so it is the only device this fixture can pin a
/// VALUE for, and that is what the mixed batch uses. The spec-correct <c>2 × N</c> cells
/// VALUE for, and that is what the mixed batch uses. The profile's <c>2 × N</c> cells
/// are seeded too and
/// <see cref="Timer_and_counter_decode_the_seeded_values_at_their_device_numbers"/> asserts
/// them — skipped unless <c>S7_TIMER_NUMBER_ADDRESSING=1</c> declares the endpoint a real
/// CPU. The device-number offset arithmetic itself stays unit-covered
/// CPU. This flag is an operator declaration, not hardware evidence.
/// The device-number offset arithmetic itself stays unit-covered
/// (<c>S7ScalarBlockTests</c>, <c>S7SpanReadParityTests</c>).
/// </para>
/// <para>
@@ -240,7 +249,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
snapshot.StatusCode.ShouldBe(0u, $"'{name}' must be Good");
// (a) Correct decode off real wire. A Timer surfaces as the S5TIME word's duration in
// seconds (double) and a Counter as the raw count (int) — two different areas, two
// seconds (double) and a Counter as the raw word (int) — two different areas, two
// different values, two different CLR types, so a read that reached the wrong area or
// decoded through the wrong arm cannot pass.
byName["TC_Timer"].Value.ShouldBe(TimerCounterLayout.Device0TimerSeconds);
@@ -280,15 +289,19 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
/// <summary>
/// Verifies a Timer and a Counter at NON-zero device numbers decode their seeded values —
/// the assertion the shared fixture cannot serve.
/// the raw-word assertion the shared fixture cannot serve.
/// </summary>
/// <remarks>
/// Skipped unless <c>S7_TIMER_NUMBER_ADDRESSING=1</c> declares the endpoint a device
/// number-addressing CPU, because python-snap7's server is not one: it resolves the TM/CT
/// request address as a bit address, so <c>T5</c> reads TM byte 0 rather than TM byte 10
/// (measured 2026-08-17 — see the type remarks). Set the variable alongside
/// <c>S7_SIM_ENDPOINT</c> when pointing this suite at a real S7 CPU seeded to match
/// <c>Docker/profiles/s7_1500.json</c>'s TM/CT areas.
/// (measured 2026-08-17 — see the type remarks). The flag only enables this test;
/// BuildOptions still selects S71500, rack 0, slot 0 and the profile's raw C7 word 31337.
/// It does not configure a classic CPU or prove classic 0..999 count semantics. A separate
/// target-specific gate needs valid counts, independent PLC binary/BCD references and
/// captured C-area bytes, plus CPU model/firmware and connection settings (#616).
/// See docs/drivers/S7.md for the evidence checklist. The skip message below retains the
/// historical fixture instructions; enabling the flag alone does not satisfy this gate.
/// </remarks>
[Fact]
public async Task Timer_and_counter_decode_the_seeded_values_at_their_device_numbers()
@@ -408,12 +421,13 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
/// <summary>
/// The Timer/Counter layouts. Two sets: a mixed batch that provokes all three read shapes at
/// once (block span, multi-var, per-tag Timer/Counter), and a device-numbered pair only a
/// real CPU can serve. Addresses map 1:1 to the TM0 / CT0 seeds in
/// device number-addressing endpoint can serve. These synthetic raw-word expectations
/// do not prove classic counter semantics. Addresses map 1:1 to the TM0 / CT0 seeds in
/// <c>Docker/profiles/s7_1500.json</c>.
/// </summary>
private static class TimerCounterLayout
{
/// <summary>Declares the endpoint a real S7 CPU, which addresses TM/CT by device number.</summary>
/// <summary>Operator opt-in for nonzero TM/CT addressing; does not change CPU options or prove encoding.</summary>
public const string RealCpuEnvVar = "S7_TIMER_NUMBER_ADDRESSING";
/// <summary>
@@ -425,7 +439,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
/// <summary>T0's seed 0x2456 — S5TIME base 2 (×1 s) over BCD 456.</summary>
public const double Device0TimerSeconds = 456.0d;
/// <summary>C0's seed — the raw big-endian count word.</summary>
/// <summary>C0's synthetic raw big-endian word 4711; not a classic 0..999 count reference.</summary>
public const int Device0CounterCount = 4711;
/// <summary>
@@ -441,7 +455,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
new("TC_Lone_I16", "DB1.DBW600", S7DataType.Int16),
new("TC_Lone_Mk", "MW10", S7DataType.UInt16),
// Timer tags are typed Float64 (decoded to seconds) and Counter tags Int32 (decoded to
// a count) — the init guard rejects any other typing. Writable:false is mandatory, not
// a raw word) — the init guard rejects any other typing. Writable:false is mandatory, not
// stylistic: S7TagDefinition defaults Writable to TRUE and the same guard rejects a
// writable Timer/Counter outright (they are read-only this phase).
new("TC_Timer", "T0", S7DataType.Float64, Writable: false),