using Shouldly; using TwinCAT.TypeSystem; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; /// /// Pins — how a bit-indexed BOOL tag (.ADSSTATE_RUN.0) is served by a /// native notification. ADS has no symbol for WORD.N: live on the TwinCAT 2.11 Elma cleaner Z33856 /// (2026-10-07) registering the notification on .ADSSTATE_RUN.0 failed with /// DeviceSymbolNotFound — 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. /// [Trait("Category", "Unit")] public sealed class TwinCATBitAccessTests { /// The parent path is the bit path without its trailing .N; a non-bit path is unchanged. [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"); } /// Bits are numbered little-endian across the parent's bytes (bit 9 = byte 1, bit 1). [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(); } /// A bit past the parent's width cannot be read — refused instead of publishing FALSE at Good. [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(); } /// Only an integer word has addressable bits — a string, a float or a container is refused. [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(); } }