1e68121b17
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.
59 lines
3.0 KiB
C#
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();
|
|
}
|
|
}
|