Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATBitAccessTests.cs
T
Joseph Doherty 1e68121b17 fix(twincat): register a bit-indexed BOOL notification on its parent word (live TC2 gate)
A native notification for MAIN.Flags.3 / .ADSSTATE_RUN.0 was registered on the
literal WORD.N path, which ADS has no symbol for: live on the TC 2.11 Elma
cleaner Z33856 it failed with DeviceSymbolNotFound, and that registration
error tore down the whole subscribe call. The read path already reads the
parent word; the notification now does the same - it registers the parent as
raw bytes at its resolved width and reads the bit (TwinCATBitAccess), refusing
a bit outside the parent or a non-integer parent with BadTypeMismatch.
2026-10-07 14:50:39 -04:00

59 lines
3.0 KiB
C#

using Shouldly;
using TwinCAT.TypeSystem;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
/// <summary>
/// Pins <see cref="TwinCATBitAccess"/> — how a bit-indexed BOOL tag (<c>.ADSSTATE_RUN.0</c>) is served by a
/// native notification. ADS has no symbol for <c>WORD.N</c>: live on the TwinCAT 2.11 Elma cleaner Z33856
/// (2026-10-07) registering the notification on <c>.ADSSTATE_RUN.0</c> failed with
/// <c>DeviceSymbolNotFound</c> — and that registration error tore down the WHOLE subscribe call. The
/// notification now registers the PARENT word as raw bytes (its resolved width) and extracts the bit.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATBitAccessTests
{
/// <summary>The parent path is the bit path without its trailing <c>.N</c>; a non-bit path is unchanged.</summary>
[Fact]
public void ParentPath_strips_the_trailing_bit_index()
{
TwinCATBitAccess.ParentPath(".ADSSTATE_RUN.0").ShouldBe(".ADSSTATE_RUN");
TwinCATBitAccess.ParentPath("MAIN.Status.Flags.15").ShouldBe("MAIN.Status.Flags");
TwinCATBitAccess.ParentPath("MAIN.Flags").ShouldBe("MAIN.Flags");
}
/// <summary>Bits are numbered little-endian across the parent's bytes (bit 9 = byte 1, bit 1).</summary>
[Fact]
public void ReadBit_numbers_bits_little_endian_across_the_parent_bytes()
{
byte[] word = [0b0000_0101, 0b0000_0010]; // .ADSSTATE_RUN = 5 → bits 0 and 2; plus bit 9
TwinCATBitAccess.ReadBit(word, 0).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 1).ShouldBeFalse();
TwinCATBitAccess.ReadBit(word, 2).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 9).ShouldBeTrue();
TwinCATBitAccess.ReadBit(word, 15).ShouldBeFalse();
}
/// <summary>A bit past the parent's width cannot be read — refused instead of publishing FALSE at Good.</summary>
[Fact]
public void CheckParent_refuses_a_bit_outside_the_parent_width()
{
var word = TwinCATPlcSymbolShape.Classify("UINT16", DataTypeCategory.Primitive, 2);
TwinCATBitAccess.CheckParent(15, word).ShouldBeNull();
TwinCATBitAccess.CheckParent(16, word).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(-1, word).ShouldNotBeNull();
}
/// <summary>Only an integer word has addressable bits — a string, a float or a container is refused.</summary>
[Fact]
public void CheckParent_refuses_a_parent_that_is_not_an_integer_word()
{
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("STRING(80)", DataTypeCategory.String, 81)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("REAL", DataTypeCategory.Primitive, 4)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(0, TwinCATPlcSymbolShape.Classify("ST_X", DataTypeCategory.Struct, 8)).ShouldNotBeNull();
TwinCATBitAccess.CheckParent(31, TwinCATPlcSymbolShape.Classify("DWORD", DataTypeCategory.Primitive, 4)).ShouldBeNull();
}
}