diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
index f675234e..0a7eb3b0 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -1150,6 +1150,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// A deterministic local refusal — not a comms fault: BadNotSupported, keep the handle, no
// health signal.
results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNotSupported, null, now);
+ WarnRefusalOnce(def.Name, "read", reference, def.DeviceHostAddress, ate.Message);
return ReadOutcome.None;
}
catch (AbCipTagConfigurationException ace)
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
index e2ff94c9..2e6cd7fc 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
@@ -29,8 +29,19 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
public LibplctagTagRuntime(AbCipTagCreateParams p)
{
_tag = BuildTag(p);
+ _boolArrayElement = !p.IsArray && p.AuthoredType == AbCipDataType.Bool
+ && NativeAbCipTagRuntime.TrySplitBoolArrayElement(p.TagName, out _, out _);
}
+ ///
+ /// A scalar BOOL authored over a BOOL-array element (Bits[n]). libplctag reads it as whatever the
+ /// controller returns for that element segment — a packed DWORD — and exposes no type code, so which DWORD
+ /// came back (and so which bit is element n) cannot be checked here. Review 07 U11: such a read used to decode
+ /// "low byte != 0" as Good; it is refused (BadNotSupported) when a DWORD came back. The native transport
+ /// (the default) addresses the right DWORD and decodes the bit.
+ ///
+ private readonly bool _boolArrayElement;
+
/// Builds the (uninitialized) libplctag tag for .
/// The tag creation parameters.
/// The new, uninitialised libplctag tag.
@@ -120,7 +131,10 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
{
AbCipDataType.Bool => bitIndex is int bit
? _tag.GetBit(bit)
- : _tag.GetInt8(offset) != 0,
+ : _boolArrayElement && offset == 0 && _tag.GetSize() == 4
+ ? throw new AbTransportException(AbStatus.Unsupported,
+ $"'{_tag.Name}' is a BOOL-array element read as a packed DWORD; libplctag cannot say which DWORD it is — use the native transport.")
+ : _tag.GetInt8(offset) != 0,
AbCipDataType.SInt => (int)(sbyte)_tag.GetInt8(offset),
AbCipDataType.USInt => (int)_tag.GetUInt8(offset),
AbCipDataType.Int => (int)_tag.GetInt16(offset),
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs
index 8bf65cca..917f205b 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/NativeAbCipTagRuntime.cs
@@ -41,6 +41,10 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
private int? _implicitBit;
private byte[]? _path;
+ // U11: a scalar BOOL authored over a BOOL-array element "Bits[n]" — the bit (n % 32) of the DWORD the request
+ // addresses (n / 32). Null for every other tag.
+ private int? _boolElementBit;
+
private ConnectionLease? _lease;
private RequestScheduler? _privateScheduler; // ForTest only — production always uses the lease's
private LogixTagClient? _client;
@@ -115,7 +119,14 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
try
{
split = CipPath.SplitBitSelector(_p.TagName);
- path = CipPath.EncodeSymbolic(split.Name);
+ var name = split.Name;
+ if (split.Bit is null && !_p.IsArray && _p.AuthoredType == AbCipDataType.Bool
+ && TrySplitBoolArrayElement(name, out var dwordName, out var bit))
+ {
+ name = dwordName;
+ _boolElementBit = bit;
+ }
+ path = CipPath.EncodeSymbolic(name);
}
catch (Exception ex) when (ex is ArgumentException or FormatException or OverflowException)
{
@@ -275,7 +286,9 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
return type switch
{
- AbCipDataType.Bool => bitIndex is int bit ? GetBit(bit) : Span(offset, 1)[0] != 0,
+ AbCipDataType.Bool => bitIndex is int bit ? GetBit(bit)
+ : offset == 0 && _boolElementBit is int element && _typeCode == LogixTypeCode.BoolArray ? GetBit(element)
+ : Span(offset, 1)[0] != 0,
AbCipDataType.SInt => (int)(sbyte)Span(offset, 1)[0],
AbCipDataType.USInt => (int)Span(offset, 1)[0],
AbCipDataType.Int => (int)BinaryPrimitives.ReadInt16LittleEndian(Span(offset, 2)),
@@ -409,6 +422,31 @@ internal sealed class NativeAbCipTagRuntime : IAbCipTagRuntime
catch (Exception) { /* ForTest only; Dispose must not throw */ }
}
+ ///
+ /// Splits a BOOL-array element reference Name[n] (one non-negative subscript, last segment) into the
+ /// DWORD it lives in (Name[n / 32]) and its bit within that DWORD (n % 32). A Logix BOOL array
+ /// is stored as DWORDs (type 0xD3) and its element segment addresses those DWORDs.
+ ///
+ /// The tag name, bit selector already split off.
+ /// The name addressing the containing DWORD.
+ /// The element's bit within that DWORD.
+ /// when ends in a single subscript.
+ internal static bool TrySplitBoolArrayElement(string name, out string dwordName, out int bit)
+ {
+ dwordName = name;
+ bit = 0;
+ if (!name.EndsWith(']')) return false;
+ var open = name.LastIndexOf('[');
+ if (open <= 0) return false;
+ var inner = name[(open + 1)..^1];
+ if (inner.Length == 0 || !inner.All(char.IsAsciiDigit)
+ || !int.TryParse(inner, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var n))
+ return false;
+ dwordName = $"{name[..open]}[{n / 32}]";
+ bit = n % 32;
+ return true;
+ }
+
private byte[] Path() =>
_path ?? throw new InvalidOperationException("InitializeAsync has not been called.");
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs
index 35f42bd3..9d013613 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/NativeAbCipTagRuntimeTests.cs
@@ -112,6 +112,37 @@ public sealed class NativeAbCipTagRuntimeTests
rt.DecodeValue(AbCipDataType.Bool, null).ShouldBe(true);
}
+ ///
+ /// Review 07 U11: a scalar BOOL authored over a BOOL-array element Bits[n] reads DWORD n / 32
+ /// (a Logix BOOL array is stored as DWORDs and its CIP element index addresses them) and decodes bit
+ /// n % 32 of it — it used to request element n and report "any of bits 0-7 of the reply set".
+ ///
+ /// The authored BOOL-array element.
+ /// The DWORD the request must address.
+ /// The DWORD the controller returns (little-endian hex).
+ /// The element's value.
+ [Theory]
+ [InlineData(37, 1, "20000000", true)] // bit 5 of DWORD 1
+ [InlineData(36, 1, "20000000", false)] // bit 4 of the same DWORD — the old "low byte != 0" read true
+ [InlineData(5, 0, "20000000", true)]
+ [InlineData(1, 0, "01000000", false)] // bit 1, not bit 0
+ [InlineData(31, 0, "00000080", true)] // top bit of DWORD 0 — outside the low byte entirely
+ public async Task A_BOOL_array_element_decodes_its_bit_of_its_DWORD(int n, int dword, string reply, bool expected)
+ {
+ var ch = Replying("D300", reply);
+ using var rt = await ReadAsync(ch, Params($"Bits[{n}]", AbCipDataType.Bool));
+ rt.DecodeValue(AbCipDataType.Bool, null).ShouldBe(expected);
+ ch.Sent[0].ShouldBe(Convert.FromHexString("5204" + "910442697473" + "28" + dword.ToString("X2") + "0100" + "00000000"));
+ }
+
+ /// Positive control: a BOOL answered as a plain BOOL (0xC1) still decodes its byte.
+ [Fact]
+ public async Task A_BOOL_element_answered_as_BOOL_decodes_the_byte()
+ {
+ using var rt = await ReadAsync(Replying("C100", "01"), Params("Flag", AbCipDataType.Bool));
+ rt.DecodeValue(AbCipDataType.Bool, null).ShouldBe(true);
+ }
+
[Fact]
public async Task Real_returned_for_a_DInt_authored_tag_is_TypeMismatch()
{