cb16ebc75e
H17: the .n read-modify-write hard-coded a DINT parent. On an INT or SINT host (both legal in Logix, and reads of Flags.3 there already worked) the DINT decode of a 2-byte buffer threw OutOfBounds, the generic catch evicted the BIT tag's (non-existent) runtime and reported BadCommunicationError + Degraded. The parent width now comes from the parent read's size (1 SINT / 2 INT / 4 DINT / 8 LINT; unknown keeps the DINT assumption). Narrow hosts use unsigned arithmetic so a set top bit encodes without overflow; a bit past the host's width is BadOutOfRange and a non-integer width BadTypeMismatch, both refused before anything is written, warned once, no health write. A transport failure in the RMW now evicts the parent runtime it actually used. Native end-to-end: an INT host is written as ONE INT (type 0xC3, 2 bytes). Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
128 lines
6.0 KiB
C#
128 lines
6.0 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 07 H17: Logix allows a <c>.n</c> bit selector on SINT / INT / DINT (and LINT) hosts, and reads of
|
|
/// <c>Flags.3</c> on an INT worked — but the bit write's read-modify-write hard-coded a DINT parent, so its
|
|
/// <c>DecodeValue(DInt)</c> on a 2-byte buffer threw OutOfBounds and the write was reported as a comms fault
|
|
/// (BadCommunicationError + Degraded). The parent width now comes from the parent read's size.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbCipBitInWordWidthTests
|
|
{
|
|
private const string Device = "ab://10.0.0.5/1,0";
|
|
|
|
private static async Task<(AbCipDriver Driver, FakeAbCipTagFactory Factory)> BuildAsync(int parentSize, object parentValue, string bitPath)
|
|
{
|
|
var factory = new FakeAbCipTagFactory
|
|
{
|
|
Customise = p => new FakeAbCipTag(p) { Value = parentValue, Size = parentSize },
|
|
};
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(Device)],
|
|
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Bit", Device, bitPath, AbCipDataType.Bool)),
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
}, "abcip-bit", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
return (drv, factory);
|
|
}
|
|
|
|
/// <summary>SINT / INT / DINT hosts: the bit is set, every other bit is preserved, nothing degrades.</summary>
|
|
/// <param name="size">The parent's wire width.</param>
|
|
/// <param name="before">The parent's value before the write.</param>
|
|
/// <param name="bit">The bit written true.</param>
|
|
/// <param name="after">The value the parent must be written back with.</param>
|
|
[Theory]
|
|
[InlineData(1, 0x81, 3, 0x89)] // SINT 0x81 (-127): bit 7 set stays set, written as its unsigned byte
|
|
[InlineData(1, 0x00, 7, 0x80)] // SINT top bit: 128 would overflow an sbyte encode
|
|
[InlineData(2, 0x8001, 3, 0x8009)] // INT top bit preserved
|
|
[InlineData(2, 0x0000, 15, 0x8000)] // INT top bit: 32768 would overflow a short encode
|
|
[InlineData(4, 0x00000001, 3, 0x00000009)]
|
|
public async Task A_bit_write_uses_the_parent_width(int size, int before, int bit, int after)
|
|
{
|
|
var (drv, factory) = await BuildAsync(size, before, $"Motor.Flags.{bit}");
|
|
|
|
var r = await drv.WriteAsync([new WriteRequest("Bit", true)], CancellationToken.None);
|
|
|
|
r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
Convert.ToInt64(factory.Tags["Motor.Flags"].Value).ShouldBe(after);
|
|
factory.Tags["Motor.Flags"].WriteCount.ShouldBe(1);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
|
|
/// <summary>A bit past the host's width is refused before anything is written, with no health write.</summary>
|
|
[Fact]
|
|
public async Task A_bit_past_the_host_width_is_BadOutOfRange_and_nothing_is_written()
|
|
{
|
|
var (drv, factory) = await BuildAsync(2, 0, "Motor.Flags.20");
|
|
|
|
var r = await drv.WriteAsync([new WriteRequest("Bit", true)], CancellationToken.None);
|
|
|
|
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
|
|
factory.Tags["Motor.Flags"].WriteCount.ShouldBe(0);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
|
|
/// <summary>A host whose width is no integer's is a type mismatch, refused before writing, no health write.</summary>
|
|
[Fact]
|
|
public async Task A_host_of_no_integer_width_is_BadTypeMismatch()
|
|
{
|
|
var (drv, factory) = await BuildAsync(3, 0, "Motor.Flags.1");
|
|
|
|
var r = await drv.WriteAsync([new WriteRequest("Bit", true)], CancellationToken.None);
|
|
|
|
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadTypeMismatch);
|
|
factory.Tags["Motor.Flags"].WriteCount.ShouldBe(0);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End to end over the REAL native runtime against a fake CIP target whose <c>Flags</c> is an INT: the write
|
|
/// goes out as ONE INT (type 0xC3, two data bytes) with the bit set and the other bits preserved.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task A_native_bit_write_on_an_INT_host_writes_an_INT()
|
|
{
|
|
await using var target = new FakeCipTarget();
|
|
// Read Tag Fragmented (0x52) → INT (0x00C3) 0x8001; Write Tag (0x4D) → success.
|
|
target.ConnectedHandler = req => Task.FromResult<byte[]?>(req[0] switch
|
|
{
|
|
0x52 => [0xD2, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x01, 0x80],
|
|
0x4D => [0xCD, 0x00, 0x00, 0x00],
|
|
_ => target.ServiceReply(req),
|
|
});
|
|
var host = $"ab://127.0.0.1:{target.Port}/1,0";
|
|
var drv = new AbCipDriver(new AbCipDriverOptions
|
|
{
|
|
Devices = [new AbCipDeviceOptions(host, AbCipPlcFamily.ControlLogix)],
|
|
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Bit", host, "Flags.3", AbCipDataType.Bool)),
|
|
Probe = new AbCipProbeOptions { Enabled = false },
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
}, "abcip-bit-native", new NativeAbCipTagFactory(new DeviceSessionPool()));
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
try
|
|
{
|
|
var r = await drv.WriteAsync([new WriteRequest("Bit", true)], CancellationToken.None);
|
|
|
|
r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
|
var write = target.ConnectedCips.Single(c => c[0] == 0x4D);
|
|
var typeAt = 2 + write[1] * 2;
|
|
write[typeAt].ShouldBe((byte)0xC3); // INT, not DINT
|
|
write.Length.ShouldBe(typeAt + 2 + 2 + 2); // type, element count, ONE 2-byte element
|
|
write[^2].ShouldBe((byte)0x09); // 0x8001 | bit 3
|
|
write[^1].ShouldBe((byte)0x80);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
finally
|
|
{
|
|
await drv.DisposeAsync();
|
|
}
|
|
}
|
|
}
|