using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests; /// /// Review 07 U3: String[] and BitInRegister[] tags read fine; their writes used to fail /// BadInternalError (no element width for String, Convert.ToBoolean(bool[]) for the bit /// array). Both now encode count elements at the tag's element width; a length mismatch is /// BadOutOfRange. /// [Trait("Category", "Unit")] public sealed class ModbusStringAndBitArrayWriteTests { private static async Task<(ModbusDriver, ScriptedTransport)> StartAsync(params ModbusTagDefinition[] tags) { var t = new ScriptedTransport(); var d = new ModbusDriver(new ModbusDriverOptions { Host = "f", RawTags = ModbusRawTags.Entries(tags), Probe = new ModbusProbeOptions { Enabled = false }, }, "m1", _ => t); await d.InitializeAsync("{}", TestContext.Current.CancellationToken); return (d, t); } [Fact] public async Task String_array_write_encodes_each_element_at_its_register_width() { var tag = new ModbusTagDefinition("S", ModbusRegion.HoldingRegisters, 10, ModbusDataType.String, StringLength: 3, ArrayCount: 2); // 2 registers per element var (drv, t) = await StartAsync(tag); await using var _ = drv; var w = await drv.WriteAsync([new WriteRequest("S", new[] { "AB", "XYZ" })], TestContext.Current.CancellationToken); w[0].StatusCode.ShouldBe(0u); t.Bank.HoldingRegisters[10].ShouldBe((ushort)(('A' << 8) | 'B')); t.Bank.HoldingRegisters[11].ShouldBe((ushort)0); t.Bank.HoldingRegisters[12].ShouldBe((ushort)(('X' << 8) | 'Y')); t.Bank.HoldingRegisters[13].ShouldBe((ushort)('Z' << 8)); // Round trip through the read path. (await drv.ReadAsync(["S"], TestContext.Current.CancellationToken))[0].Value.ShouldBe(new[] { "AB", "XYZ" }); } [Fact] public async Task BitInRegister_array_write_sets_only_the_addressed_bit_of_each_register() { var tag = new ModbusTagDefinition("B", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 4, ArrayCount: 3); var (drv, t) = await StartAsync(tag); await using var _ = drv; t.Bank.HoldingRegisters[30] = 0xFFFF; t.Bank.HoldingRegisters[31] = 0x0001; t.Bank.HoldingRegisters[32] = 0x8000; var w = await drv.WriteAsync([new WriteRequest("B", new object[] { false, true, 1 })], TestContext.Current.CancellationToken); w[0].StatusCode.ShouldBe(0u); t.Bank.HoldingRegisters[30].ShouldBe((ushort)0xFFEF, "bit 4 cleared, neighbours kept"); t.Bank.HoldingRegisters[31].ShouldBe((ushort)0x0011); t.Bank.HoldingRegisters[32].ShouldBe((ushort)0x8010); (await drv.ReadAsync(["B"], TestContext.Current.CancellationToken))[0].Value.ShouldBe(new[] { false, true, true }); } [Theory] [InlineData("S")] [InlineData("B")] public async Task A_length_mismatch_is_BadOutOfRange_and_sends_nothing(string tagName) { var (drv, t) = await StartAsync( new ModbusTagDefinition("S", ModbusRegion.HoldingRegisters, 10, ModbusDataType.String, StringLength: 2, ArrayCount: 2), new ModbusTagDefinition("B", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 1, ArrayCount: 3)); await using var _ = drv; object value = tagName == "S" ? new[] { "only one" } : new[] { true }; var w = await drv.WriteAsync([new WriteRequest(tagName, value)], TestContext.Current.CancellationToken); w[0].StatusCode.ShouldBe(0x803C0000u); t.Sent.Where(s => s.Pdu[0] is 0x06 or 0x10).ShouldBeEmpty(); } }