142 lines
5.9 KiB
C#
142 lines
5.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ModbusBitRmwTests
|
|
{
|
|
/// <summary>Fake transport capturing each PDU so tests can assert on the read + write sequence.</summary>
|
|
private sealed class RmwTransport : IModbusTransport
|
|
{
|
|
public readonly ushort[] HoldingRegisters = new ushort[256];
|
|
public readonly List<byte[]> Pdus = new();
|
|
|
|
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
|
|
|
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
|
{
|
|
Pdus.Add(pdu);
|
|
if (pdu[0] == 0x03)
|
|
{
|
|
// FC03 Read Holding Registers.
|
|
var addr = (ushort)((pdu[1] << 8) | pdu[2]);
|
|
var qty = (ushort)((pdu[3] << 8) | pdu[4]);
|
|
var resp = new byte[2 + qty * 2];
|
|
resp[0] = 0x03;
|
|
resp[1] = (byte)(qty * 2);
|
|
for (var i = 0; i < qty; i++)
|
|
{
|
|
resp[2 + i * 2] = (byte)(HoldingRegisters[addr + i] >> 8);
|
|
resp[3 + i * 2] = (byte)(HoldingRegisters[addr + i] & 0xFF);
|
|
}
|
|
return Task.FromResult(resp);
|
|
}
|
|
if (pdu[0] == 0x06)
|
|
{
|
|
// FC06 Write Single Register.
|
|
var addr = (ushort)((pdu[1] << 8) | pdu[2]);
|
|
var v = (ushort)((pdu[3] << 8) | pdu[4]);
|
|
HoldingRegisters[addr] = v;
|
|
return Task.FromResult(new byte[] { 0x06, pdu[1], pdu[2], pdu[3], pdu[4] });
|
|
}
|
|
return Task.FromException<byte[]>(new NotSupportedException($"FC 0x{pdu[0]:X2} not supported by fake"));
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|
|
|
|
private static (ModbusDriver drv, RmwTransport fake) NewDriver(params ModbusTagDefinition[] tags)
|
|
{
|
|
var fake = new RmwTransport();
|
|
var opts = new ModbusDriverOptions
|
|
{
|
|
Host = "fake",
|
|
Tags = tags,
|
|
Probe = new ModbusProbeOptions { Enabled = false },
|
|
};
|
|
return (new ModbusDriver(opts, "modbus-1", _ => fake), fake);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_set_reads_current_register_ORs_bit_writes_back()
|
|
{
|
|
var (drv, fake) = NewDriver(
|
|
new ModbusTagDefinition("Flag3", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 3));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
fake.HoldingRegisters[10] = 0b0000_0001; // bit 0 already set
|
|
|
|
var results = await drv.WriteAsync([new WriteRequest("Flag3", true)], CancellationToken.None);
|
|
|
|
results.Single().StatusCode.ShouldBe(0u);
|
|
fake.HoldingRegisters[10].ShouldBe((ushort)0b0000_1001); // bit 3 now set, bit 0 preserved
|
|
// Two PDUs: FC03 read then FC06 write.
|
|
fake.Pdus.Count.ShouldBe(2);
|
|
fake.Pdus[0][0].ShouldBe((byte)0x03);
|
|
fake.Pdus[1][0].ShouldBe((byte)0x06);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_clear_reads_current_register_ANDs_bit_off_writes_back()
|
|
{
|
|
var (drv, fake) = NewDriver(
|
|
new ModbusTagDefinition("Flag3", ModbusRegion.HoldingRegisters, 10, ModbusDataType.BitInRegister, BitIndex: 3));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
fake.HoldingRegisters[10] = 0xFFFF; // all bits set
|
|
|
|
await drv.WriteAsync([new WriteRequest("Flag3", false)], CancellationToken.None);
|
|
|
|
fake.HoldingRegisters[10].ShouldBe((ushort)0b1111_1111_1111_0111); // bit 3 cleared, rest preserved
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Concurrent_bit_writes_to_same_register_preserve_all_updates()
|
|
{
|
|
// Serialization test — 8 writers target different bits in register 20. Without the RMW
|
|
// lock, concurrent reads interleave + last-to-commit wins so some bits get lost.
|
|
var tags = Enumerable.Range(0, 8)
|
|
.Select(b => new ModbusTagDefinition($"Bit{b}", ModbusRegion.HoldingRegisters, 20, ModbusDataType.BitInRegister, BitIndex: (byte)b))
|
|
.ToArray();
|
|
var (drv, fake) = NewDriver(tags);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
fake.HoldingRegisters[20] = 0;
|
|
|
|
await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
|
|
drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));
|
|
|
|
fake.HoldingRegisters[20].ShouldBe((ushort)0xFF); // all 8 bits set
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_write_on_different_registers_proceeds_in_parallel_without_contention()
|
|
{
|
|
var tags = Enumerable.Range(0, 4)
|
|
.Select(i => new ModbusTagDefinition($"Bit{i}", ModbusRegion.HoldingRegisters, (ushort)(50 + i), ModbusDataType.BitInRegister, BitIndex: 0))
|
|
.ToArray();
|
|
var (drv, fake) = NewDriver(tags);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await Task.WhenAll(Enumerable.Range(0, 4).Select(i =>
|
|
drv.WriteAsync([new WriteRequest($"Bit{i}", true)], CancellationToken.None)));
|
|
|
|
for (var i = 0; i < 4; i++)
|
|
fake.HoldingRegisters[50 + i].ShouldBe((ushort)0x01);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_write_preserves_other_bits_in_the_same_register()
|
|
{
|
|
var (drv, fake) = NewDriver(
|
|
new ModbusTagDefinition("BitA", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 5),
|
|
new ModbusTagDefinition("BitB", ModbusRegion.HoldingRegisters, 30, ModbusDataType.BitInRegister, BitIndex: 10));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.WriteAsync([new WriteRequest("BitA", true)], CancellationToken.None);
|
|
await drv.WriteAsync([new WriteRequest("BitB", true)], CancellationToken.None);
|
|
|
|
fake.HoldingRegisters[30].ShouldBe((ushort)((1 << 5) | (1 << 10)));
|
|
}
|
|
}
|