ae20519e9d
H2: a Bad/null publish drops the deadband baseline and any status change publishes, so a device that returns holding the identical value is no longer swallowed by |new - old| < deadband (the #527 value-only-dedup trap). Integration case drives a real drop/restore through a loopback relay. Refs #741 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
54 lines
2.4 KiB
C#
54 lines
2.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 07 H2: the deadband baseline is keyed on (value, status). A Good-after-Bad recovery carrying
|
|
/// the IDENTICAL value must publish — the value-only dedup left the node Bad until the value drifted.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ModbusDeadbandRecoveryTests
|
|
{
|
|
private const uint Good = 0u;
|
|
private const uint Uncertain = 0x40000000u;
|
|
private const uint BadComm = 0x80050000u;
|
|
|
|
private static async Task<ModbusDriver> NewDriverAsync()
|
|
{
|
|
var tag = new ModbusTagDefinition("Temp", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int16, Deadband: 5.0);
|
|
var opts = new ModbusDriverOptions
|
|
{
|
|
Host = "f", RawTags = ModbusRawTags.Entries([tag]), Probe = new ModbusProbeOptions { Enabled = false },
|
|
};
|
|
var drv = new ModbusDriver(opts, "m1", _ => new ModbusDriverTests.FakeTransport());
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
return drv;
|
|
}
|
|
|
|
private static DataValueSnapshot Snap(object? v, uint status) => new(v, status, DateTime.UtcNow, DateTime.UtcNow);
|
|
|
|
[Fact]
|
|
public async Task Good_after_Bad_with_the_same_value_publishes()
|
|
{
|
|
await using var drv = await NewDriverAsync();
|
|
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue(); // baseline
|
|
drv.ShouldPublish("Temp", Snap(null, BadComm)).ShouldBeTrue(); // comms drop
|
|
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue("the recovery publish must not be swallowed");
|
|
// Positive control: the deadband is still active after the recovery re-seeded the baseline.
|
|
drv.ShouldPublish("Temp", Snap((short)11, Good)).ShouldBeFalse();
|
|
drv.ShouldPublish("Temp", Snap((short)16, Good)).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_status_change_with_the_same_value_publishes()
|
|
{
|
|
await using var drv = await NewDriverAsync();
|
|
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue();
|
|
drv.ShouldPublish("Temp", Snap((short)10, Uncertain)).ShouldBeTrue("a quality change must publish");
|
|
drv.ShouldPublish("Temp", Snap((short)12, Uncertain)).ShouldBeFalse("within deadband at the same quality");
|
|
drv.ShouldPublish("Temp", Snap((short)10, Good)).ShouldBeTrue("back to Good must publish");
|
|
}
|
|
}
|