40c7493829
The bit write read and wrote the parent word as a 32-bit UDInt with no parent check: a bit beyond a 16-bit parent was written into neighbouring bytes, and a signed parent with its sign bit set read back >= 2^31, so the checked conversion turned any bit write into BadOutOfRange. The write now goes through ITwinCATClient.WriteBitAsync: the ADS client resolves the parent symbol, checks the bit against it (TwinCATBitAccess. CheckParent, as the notification path already did), reads exactly parent.ByteSize raw bytes, flips one bit (TwinCATBitAccess.ApplyBitWrite) and writes the bytes back by index group/offset or variable handle. A bit beyond the parent is BadOutOfRange, a non-integer parent BadTypeMismatch, a short parent read BadCommunicationError (never written back), all without a health write. The per-parent RMW lock stays in the driver. Refs #739 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
105 lines
5.4 KiB
C#
105 lines
5.4 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();
|
|
}
|
|
|
|
// ---- #739 H18: the bit WRITE is a read-modify-write at the parent's own width ----
|
|
|
|
/// <summary>A bit write flips exactly one bit of the parent's raw bytes at the parent's width.</summary>
|
|
[Fact]
|
|
public void ApplyBitWrite_flips_one_bit_at_the_parent_width()
|
|
{
|
|
var word = TwinCATPlcSymbolShape.Classify("WORD", DataTypeCategory.Primitive, 2);
|
|
TwinCATBitAccess.ApplyBitWrite(word, [0x01, 0x00], 9, true, out var set).ShouldBe(TwinCATStatusMapper.Good);
|
|
set.ShouldBe(new byte[] { 0x01, 0x02 });
|
|
TwinCATBitAccess.ApplyBitWrite(word, [0xFF, 0xFF], 3, false, out var cleared).ShouldBe(TwinCATStatusMapper.Good);
|
|
cleared.ShouldBe(new byte[] { 0xF7, 0xFF });
|
|
}
|
|
|
|
/// <summary>
|
|
/// A signed parent with its sign bit set is plain bytes — the old UDInt round-trip read it back as
|
|
/// a value ≥ 2³¹ and a checked conversion turned ANY bit write into BadOutOfRange.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ApplyBitWrite_on_a_negative_signed_parent_preserves_the_sign_bit()
|
|
{
|
|
var intWord = TwinCATPlcSymbolShape.Classify("INT", DataTypeCategory.Primitive, 2);
|
|
TwinCATBitAccess.ApplyBitWrite(intWord, [0x00, 0x80], 0, true, out var updated).ShouldBe(TwinCATStatusMapper.Good);
|
|
updated.ShouldBe(new byte[] { 0x01, 0x80 });
|
|
}
|
|
|
|
/// <summary>A bit beyond the parent is BadOutOfRange; a non-integer parent BadTypeMismatch; nothing is produced.</summary>
|
|
[Fact]
|
|
public void ApplyBitWrite_refuses_a_bit_the_parent_does_not_have()
|
|
{
|
|
var word = TwinCATPlcSymbolShape.Classify("UINT", DataTypeCategory.Primitive, 2);
|
|
TwinCATBitAccess.ApplyBitWrite(word, [0, 0], 16, true, out var none).ShouldBe(TwinCATStatusMapper.BadOutOfRange);
|
|
none.ShouldBeEmpty();
|
|
var real = TwinCATPlcSymbolShape.Classify("REAL", DataTypeCategory.Primitive, 4);
|
|
TwinCATBitAccess.ApplyBitWrite(real, [0, 0, 0, 0], 1, true, out _).ShouldBe(TwinCATStatusMapper.BadTypeMismatch);
|
|
}
|
|
|
|
/// <summary>A parent read shorter than the parent's width is never written back (it would zero bytes).</summary>
|
|
[Fact]
|
|
public void ApplyBitWrite_refuses_a_short_parent_buffer()
|
|
{
|
|
var dword = TwinCATPlcSymbolShape.Classify("DWORD", DataTypeCategory.Primitive, 4);
|
|
TwinCATBitAccess.ApplyBitWrite(dword, [0xFF, 0xFF], 1, true, out var none)
|
|
.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
|
|
none.ShouldBeEmpty();
|
|
}
|
|
}
|