1adc9d65e1
U3: both array shapes read fine but every write failed BadInternalError. String[] now encodes each element at its register width (StringLength chars, 2 per register); BitInRegister[] reads the span once, flips the addressed bit of each register and writes the span back with FC16 (per-register RMW locks taken in ascending order). A length mismatch is BadOutOfRange and sends nothing. DriverArrayWriteSupport (outside this lane) still advertises these nodes read-only - recorded as follow-up F1 in the parity test. Refs #741 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
83 lines
3.8 KiB
C#
83 lines
3.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 07 U3: <c>String[]</c> and <c>BitInRegister[]</c> tags read fine; their writes used to fail
|
|
/// <c>BadInternalError</c> (no element width for String, <c>Convert.ToBoolean(bool[])</c> for the bit
|
|
/// array). Both now encode <c>count</c> elements at the tag's element width; a length mismatch is
|
|
/// <c>BadOutOfRange</c>.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
}
|