feat(s7): Timer/Counter read (read-only) + route Timer/Counter through buffer path
This commit is contained in:
@@ -14,8 +14,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||
/// String (S7 classic STRING) decode/encode is proven here via
|
||||
/// <see cref="S7.Net.Types.S7String"/>; DateTime (S7 classic DATE_AND_TIME / DT, 8-byte BCD)
|
||||
/// decode/encode is proven via <see cref="S7.Net.Types.DateTime"/> (the 8-byte DT helper —
|
||||
/// NOT DTL). Timer/Counter decode/encode are still deferred stubs (T5) and this file pins the
|
||||
/// NotSupportedException contract they land against.
|
||||
/// NOT DTL). Timer (S5TIME → seconds <c>double</c>) and Counter (word → <c>int</c> count)
|
||||
/// decode is proven via <see cref="S7.Net.Types.Timer"/> / <see cref="S7.Net.Types.Counter"/>
|
||||
/// and is routed by the parsed AREA (so a Timer/Float64 tag reads 2 bytes, not 8); both are
|
||||
/// read-only this phase, so their encode throws <see cref="NotSupportedException"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class S7ScalarBlockTests
|
||||
@@ -29,6 +31,21 @@ public sealed class S7ScalarBlockTests
|
||||
private static S7ParsedAddress Addr() =>
|
||||
new(S7Area.DataBlock, DbNumber: 1, S7Size.Byte, ByteOffset: 0, BitOffset: 0);
|
||||
|
||||
// Timer / Counter addresses: the parser stores the timer/counter NUMBER in ByteOffset,
|
||||
// DbNumber 0, Size Word (see S7AddressParser.ParseTimerOrCounter). A Timer tag is typed
|
||||
// Float64 (decoded to seconds) and a Counter tag Int32 (decoded to count) — T1 guard-c.
|
||||
private static S7TagDefinition TimerTag(int number = 5, bool writable = false) =>
|
||||
new("TimerTag", $"T{number}", S7DataType.Float64, Writable: writable);
|
||||
|
||||
private static S7ParsedAddress TimerAddr(int number = 5) =>
|
||||
new(S7Area.Timer, DbNumber: 0, S7Size.Word, ByteOffset: number, BitOffset: 0);
|
||||
|
||||
private static S7TagDefinition CounterTag(int number = 3, bool writable = false) =>
|
||||
new("CounterTag", $"C{number}", S7DataType.Int32, Writable: writable);
|
||||
|
||||
private static S7ParsedAddress CounterAddr(int number = 3) =>
|
||||
new(S7Area.Counter, DbNumber: 0, S7Size.Word, ByteOffset: number, BitOffset: 0);
|
||||
|
||||
// S7 is big-endian: most-significant byte first.
|
||||
private static byte[] BeUInt64(ulong v)
|
||||
{
|
||||
@@ -47,12 +64,24 @@ public sealed class S7ScalarBlockTests
|
||||
[InlineData(S7DataType.Float64, 8)]
|
||||
[InlineData(S7DataType.DateTime, 8)]
|
||||
public void ScalarByteWidth_fixed_width_types(S7DataType dt, int expected)
|
||||
=> S7Driver.ScalarByteWidth(Tag(dt)).ShouldBe(expected);
|
||||
=> S7Driver.ScalarByteWidth(Tag(dt), Addr()).ShouldBe(expected);
|
||||
|
||||
/// <summary>Verifies String width is StringLength + 2 (S7 STRING header: max-len + actual-len).</summary>
|
||||
[Fact]
|
||||
public void ScalarByteWidth_String_is_length_plus_two()
|
||||
=> S7Driver.ScalarByteWidth(Tag(S7DataType.String, stringLength: 10)).ShouldBe(12);
|
||||
=> S7Driver.ScalarByteWidth(Tag(S7DataType.String, stringLength: 10), Addr()).ShouldBe(12);
|
||||
|
||||
/// <summary>Headline correctness assertion: a Timer tag is typed Float64 (which would otherwise
|
||||
/// yield width 8), but the Timer AREA must take precedence and read exactly 2 bytes (one S5TIME
|
||||
/// word). Without area-precedence the codec would read 8 bytes and mis-frame the timer.</summary>
|
||||
[Fact]
|
||||
public void ScalarByteWidth_Timer_is_two_despite_Float64_datatype()
|
||||
=> S7Driver.ScalarByteWidth(TimerTag(), TimerAddr()).ShouldBe(2);
|
||||
|
||||
/// <summary>A Counter tag is typed Int32; the Counter AREA reads exactly 2 bytes (one counter word).</summary>
|
||||
[Fact]
|
||||
public void ScalarByteWidth_Counter_is_two()
|
||||
=> S7Driver.ScalarByteWidth(CounterTag(), CounterAddr()).ShouldBe(2);
|
||||
|
||||
// ── DecodeScalarBlock — Int64 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -368,4 +397,89 @@ public sealed class S7ScalarBlockTests
|
||||
var decoded = S7Driver.DecodeScalarBlock(tag, Addr(), S7Driver.EncodeScalarBlock(tag, value));
|
||||
decoded.ShouldBeOfType<System.DateTime>().ShouldBe(value);
|
||||
}
|
||||
|
||||
// ── DecodeScalarBlock — Timer (S5TIME → seconds, read-only) ───────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a 2-byte S5TIME block decodes to a duration in SECONDS as a <c>double</c>,
|
||||
/// routed by the Timer AREA (NOT the tag's Float64 DataType — that would mis-read the
|
||||
/// 2-byte block as an 8-byte LReal). The fixture is a hand-built S5TIME word: the high
|
||||
/// nibble of byte 0 carries the 2-bit time base (0=10 ms, 1=100 ms, 2=1 s, 3=10 s), the
|
||||
/// low nibble of byte 0 + the two nibbles of byte 1 carry a 3-digit BCD value. Here
|
||||
/// base bits = 2 (×1 s) and BCD = 100 → 100.0 seconds (matches S7.Net's
|
||||
/// <see cref="S7.Net.Types.Timer.FromByteArray"/> S5TIME decode).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Timer_decodes_s5time_to_seconds()
|
||||
{
|
||||
// [0x21,0x00]: byte0 high nibble 0x2 = base bits (×1 s), byte0 low nibble 0x1 = BCD hundreds,
|
||||
// byte1 0x00 = BCD tens+ones → value 100 × 1 s = 100.0 s.
|
||||
var block = new byte[] { 0x21, 0x00 };
|
||||
var result = S7Driver.DecodeScalarBlock(TimerTag(), TimerAddr(), block);
|
||||
result.ShouldBeOfType<double>().ShouldBe(100.0, tolerance: 1e-9);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a sub-second S5TIME decodes correctly (base = 100 ms, BCD = 250 → 25.0 s).</summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Timer_decodes_fractional_base()
|
||||
{
|
||||
// [0x12,0x50]: base bits 0x1 (×0.1 s), BCD 250 → 250 × 0.1 s = 25.0 s.
|
||||
var block = new byte[] { 0x12, 0x50 };
|
||||
var result = S7Driver.DecodeScalarBlock(TimerTag(), TimerAddr(), block);
|
||||
result.ShouldBeOfType<double>().ShouldBe(25.0, tolerance: 1e-9);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a zero S5TIME word decodes to 0.0 seconds.</summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Timer_zero()
|
||||
{
|
||||
var result = S7Driver.DecodeScalarBlock(TimerTag(), TimerAddr(), new byte[] { 0x00, 0x00 });
|
||||
result.ShouldBeOfType<double>().ShouldBe(0.0);
|
||||
}
|
||||
|
||||
// ── DecodeScalarBlock — Counter (BCD/raw word → count, read-only) ──────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a 2-byte counter block decodes to a COUNT as an <c>int</c>, routed by the
|
||||
/// Counter AREA (the tag is typed Int32). S7.Net's
|
||||
/// <see cref="S7.Net.Types.Counter.FromByteArray"/> reads the word big-endian; we surface
|
||||
/// it as a (non-negative) <c>int</c>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Counter_decodes_to_int_count()
|
||||
{
|
||||
// Big-endian word 0x0042 = 66.
|
||||
var block = new byte[] { 0x00, 0x42 };
|
||||
var result = S7Driver.DecodeScalarBlock(CounterTag(), CounterAddr(), block);
|
||||
result.ShouldBeOfType<int>().ShouldBe(66);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a larger counter value decodes (0x0123 = 291) — pins big-endian order.</summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Counter_decodes_big_endian()
|
||||
{
|
||||
var block = new byte[] { 0x01, 0x23 };
|
||||
var result = S7Driver.DecodeScalarBlock(CounterTag(), CounterAddr(), block);
|
||||
result.ShouldBeOfType<int>().ShouldBe(0x0123);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a zero counter decodes to 0.</summary>
|
||||
[Fact]
|
||||
public void DecodeScalarBlock_Counter_zero()
|
||||
{
|
||||
var result = S7Driver.DecodeScalarBlock(CounterTag(), CounterAddr(), new byte[] { 0x00, 0x00 });
|
||||
result.ShouldBeOfType<int>().ShouldBe(0);
|
||||
}
|
||||
|
||||
// ── EncodeScalarBlock — Timer/Counter are read-only this phase ─────────────────────────
|
||||
|
||||
/// <summary>Verifies a Timer write throws NotSupportedException — Timer/Counter are read-only.</summary>
|
||||
[Fact]
|
||||
public void EncodeScalarBlock_Timer_throws_read_only()
|
||||
=> Should.Throw<NotSupportedException>(() => S7Driver.EncodeScalarBlock(TimerTag(), 1.0));
|
||||
|
||||
/// <summary>Verifies a Counter write throws NotSupportedException — Timer/Counter are read-only.</summary>
|
||||
[Fact]
|
||||
public void EncodeScalarBlock_Counter_throws_read_only()
|
||||
=> Should.Throw<NotSupportedException>(() => S7Driver.EncodeScalarBlock(CounterTag(), 42));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user