Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusDataTypeTests.cs
Joseph Doherty cd19022d19 Phase 3 PR 45 -- DL205 string byte-order quirk (low-byte-first ASCII packing). Adds ModbusStringByteOrder enum {HighByteFirst, LowByteFirst} + StringByteOrder field on ModbusTagDefinition (default HighByteFirst, the standard Modbus convention). DecodeRegister + EncodeRegister String branches now respect per-tag byte order. Under LowByteFirst each register packs the first char in the low byte instead of the high byte -- the AutomationDirect DirectLOGIC DL205/DL260/DL350 family's headline string quirk. Without the flag the driver decodes 'eHllo' garbage from HR[1040..1042] even though wire bytes are identical. Unit tests: String_LowByteFirst_decodes_DL205_packed_Hello (5 chars across 3 regs with nul pad), String_LowByteFirst_decode_truncates_at_first_nul, String_LowByteFirst_encode_round_trips_with_decode (asserts exact DL205-documented byte sequence {0x65,0x48,0x6C,0x6C,0x00,0x6F} + symmetric encode->decode), String_HighByteFirst_and_LowByteFirst_differ_on_same_wire (control: same wire, different flag => different decode). 56/56 Modbus.Tests pass. Integration test: DL205StringQuirkTests.DL205_string_low_byte_first_decodes_Hello_from_HR1040 against the dl205.json pymodbus profile; reads HR[1040..1042] with both flags on the same tag map and asserts LowByteFirst='Hello' + HighByteFirst!='Hello'. Gated on MODBUS_SIM_PROFILE=dl205 since the standard profile doesn't seed HR[1040..1042]. Verified 2/2 integration tests pass against running pymodbus dl205 simulator. Baseline for PR 46 (BCD decoder), PR 47 (V-memory octal helper), PR 48 (CDAB float order), PR 49 (FC03/FC16 per-device caps) -- each lands its own DL205_<behavior> test class in tests/.../DL205/.
2026-04-18 21:43:32 -04:00

223 lines
9.5 KiB
C#

using System.Buffers.Binary;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
[Trait("Category", "Unit")]
public sealed class ModbusDataTypeTests
{
/// <summary>
/// Register-count lookup is per-tag now (strings need StringLength; Int64/Float64 need 4).
/// </summary>
[Theory]
[InlineData(ModbusDataType.BitInRegister, 1)]
[InlineData(ModbusDataType.Int16, 1)]
[InlineData(ModbusDataType.UInt16, 1)]
[InlineData(ModbusDataType.Int32, 2)]
[InlineData(ModbusDataType.UInt32, 2)]
[InlineData(ModbusDataType.Float32, 2)]
[InlineData(ModbusDataType.Int64, 4)]
[InlineData(ModbusDataType.UInt64, 4)]
[InlineData(ModbusDataType.Float64, 4)]
public void RegisterCount_returns_correct_register_count_per_type(ModbusDataType t, int expected)
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, t);
ModbusDriver.RegisterCount(tag).ShouldBe((ushort)expected);
}
[Theory]
[InlineData(0, 1)] // 0 chars → still 1 byte / 1 register (pathological but well-defined: length 0 is 0 bytes)
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 2)]
[InlineData(10, 5)]
public void RegisterCount_for_String_rounds_up_to_register_pair(ushort chars, int expectedRegs)
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String, StringLength: chars);
// 0-char is encoded as 0 regs; the test case expects 1 for lengths 1-2, 2 for 3-4, etc.
if (chars == 0) ModbusDriver.RegisterCount(tag).ShouldBe((ushort)0);
else ModbusDriver.RegisterCount(tag).ShouldBe((ushort)expectedRegs);
}
// --- Int32 / UInt32 / Float32 with byte-order variants ---
[Fact]
public void Int32_BigEndian_decodes_ABCD_layout()
{
// Value 0x12345678 → bytes [0x12, 0x34, 0x56, 0x78] as PLC wrote them.
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int32,
ByteOrder: ModbusByteOrder.BigEndian);
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78 };
ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(0x12345678);
}
[Fact]
public void Int32_WordSwap_decodes_CDAB_layout()
{
// Siemens/AB PLC stored 0x12345678 as register[0] = 0x5678, register[1] = 0x1234.
// Wire bytes are [0x56, 0x78, 0x12, 0x34]; with ByteOrder=WordSwap we get 0x12345678 back.
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int32,
ByteOrder: ModbusByteOrder.WordSwap);
var bytes = new byte[] { 0x56, 0x78, 0x12, 0x34 };
ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(0x12345678);
}
[Fact]
public void Float32_WordSwap_encode_decode_roundtrips()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Float32,
ByteOrder: ModbusByteOrder.WordSwap);
var wire = ModbusDriver.EncodeRegister(25.5f, tag);
wire.Length.ShouldBe(4);
ModbusDriver.DecodeRegister(wire, tag).ShouldBe(25.5f);
}
// --- Int64 / UInt64 / Float64 ---
[Fact]
public void Int64_BigEndian_roundtrips()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int64);
var wire = ModbusDriver.EncodeRegister(0x0123456789ABCDEFL, tag);
wire.Length.ShouldBe(8);
BinaryPrimitives.ReadInt64BigEndian(wire).ShouldBe(0x0123456789ABCDEFL);
ModbusDriver.DecodeRegister(wire, tag).ShouldBe(0x0123456789ABCDEFL);
}
[Fact]
public void UInt64_WordSwap_reverses_four_words()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.UInt64,
ByteOrder: ModbusByteOrder.WordSwap);
var value = 0xAABBCCDDEEFF0011UL;
var wireBE = new byte[8];
BinaryPrimitives.WriteUInt64BigEndian(wireBE, value);
// Word-swap layout: [word3, word2, word1, word0] where each word keeps its bytes big-endian.
var wireWS = new byte[] { wireBE[6], wireBE[7], wireBE[4], wireBE[5], wireBE[2], wireBE[3], wireBE[0], wireBE[1] };
ModbusDriver.DecodeRegister(wireWS, tag).ShouldBe(value);
var roundtrip = ModbusDriver.EncodeRegister(value, tag);
roundtrip.ShouldBe(wireWS);
}
[Fact]
public void Float64_roundtrips_under_word_swap()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Float64,
ByteOrder: ModbusByteOrder.WordSwap);
var wire = ModbusDriver.EncodeRegister(3.14159265358979d, tag);
wire.Length.ShouldBe(8);
((double)ModbusDriver.DecodeRegister(wire, tag)!).ShouldBe(3.14159265358979d, tolerance: 1e-12);
}
// --- BitInRegister ---
[Theory]
[InlineData(0b0000_0000_0000_0001, 0, true)]
[InlineData(0b0000_0000_0000_0001, 1, false)]
[InlineData(0b1000_0000_0000_0000, 15, true)]
[InlineData(0b0100_0000_0100_0000, 6, true)]
[InlineData(0b0100_0000_0100_0000, 14, true)]
[InlineData(0b0100_0000_0100_0000, 7, false)]
public void BitInRegister_extracts_bit_at_index(ushort raw, byte bitIndex, bool expected)
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.BitInRegister,
BitIndex: bitIndex);
var bytes = new byte[] { (byte)(raw >> 8), (byte)(raw & 0xFF) };
ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(expected);
}
[Fact]
public void BitInRegister_write_is_not_supported_in_PR24()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.BitInRegister,
BitIndex: 5);
Should.Throw<InvalidOperationException>(() => ModbusDriver.EncodeRegister(true, tag))
.Message.ShouldContain("read-modify-write");
}
// --- String ---
[Fact]
public void String_decodes_ASCII_packed_two_chars_per_register()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 6);
// "HELLO!" = 0x48 0x45 0x4C 0x4C 0x4F 0x21 across 3 registers.
var bytes = "HELLO!"u8.ToArray();
ModbusDriver.DecodeRegister(bytes, tag).ShouldBe("HELLO!");
}
[Fact]
public void String_decode_truncates_at_first_nul()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 10);
var bytes = new byte[] { 0x48, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
ModbusDriver.DecodeRegister(bytes, tag).ShouldBe("Hi");
}
[Fact]
public void String_encode_nul_pads_remaining_bytes()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 8);
var wire = ModbusDriver.EncodeRegister("Hi", tag);
wire.Length.ShouldBe(8);
wire[0].ShouldBe((byte)'H');
wire[1].ShouldBe((byte)'i');
for (var i = 2; i < 8; i++) wire[i].ShouldBe((byte)0);
}
// --- DL205 low-byte-first strings (AutomationDirect DirectLOGIC quirk) ---
[Fact]
public void String_LowByteFirst_decodes_DL205_packed_Hello()
{
// HR[1040] = 0x6548 (wire BE bytes [0x65, 0x48]) decodes first char from low byte = 'H',
// second from high byte = 'e'. HR[1041] = 0x6C6C → 'l','l'. HR[1042] = 0x006F → 'o', nul.
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 5, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
var wire = new byte[] { 0x65, 0x48, 0x6C, 0x6C, 0x00, 0x6F };
ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hello");
}
[Fact]
public void String_LowByteFirst_decode_truncates_at_first_nul()
{
// Low-byte-first with only 2 real chars in register 0 (lo='H', hi='i') and the rest nul.
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 6, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
var wire = new byte[] { 0x69, 0x48, 0x00, 0x00, 0x00, 0x00 };
ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hi");
}
[Fact]
public void String_LowByteFirst_encode_round_trips_with_decode()
{
var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 5, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
var wire = ModbusDriver.EncodeRegister("Hello", tag);
// Expect exactly the DL205-documented byte sequence.
wire.ShouldBe(new byte[] { 0x65, 0x48, 0x6C, 0x6C, 0x00, 0x6F });
ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hello");
}
[Fact]
public void String_HighByteFirst_and_LowByteFirst_differ_on_same_wire()
{
// Same wire buffer, different byte order → first char switches 'H' vs 'e'.
var wire = new byte[] { 0x48, 0x65 };
var hi = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 2, StringByteOrder: ModbusStringByteOrder.HighByteFirst);
var lo = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
StringLength: 2, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
ModbusDriver.DecodeRegister(wire, hi).ShouldBe("He");
ModbusDriver.DecodeRegister(wire, lo).ShouldBe("eH");
}
}