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 { /// /// Register-count lookup is per-tag now (strings need StringLength; Int64/Float64 need 4). /// [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(() => 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"); } }