diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs
index 48e253d..fc55970 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDataType.cs
@@ -109,4 +109,103 @@ public static class AbLegacyDataTypeExtensions
AbLegacyDataType.MicroLogixFunctionFile => DriverDataType.Int32,
_ => DriverDataType.Int32,
};
+
+ ///
+ /// Sub-element-aware driver type. Timer/Counter/Control elements expose Boolean status
+ /// bits (.DN, .EN, .TT, .CU, .CD, .OV,
+ /// .UN, .ER, etc.) and Int32 word members (.PRE, .ACC,
+ /// .LEN, .POS). Unknown sub-elements fall back to
+ /// so the driver remains permissive.
+ ///
+ public static DriverDataType EffectiveDriverDataType(AbLegacyDataType t, string? subElement)
+ {
+ if (subElement is null) return t.ToDriverDataType();
+ var key = subElement.ToUpperInvariant();
+ return t switch
+ {
+ AbLegacyDataType.TimerElement => key switch
+ {
+ "EN" or "TT" or "DN" => DriverDataType.Boolean,
+ "PRE" or "ACC" => DriverDataType.Int32,
+ _ => t.ToDriverDataType(),
+ },
+ AbLegacyDataType.CounterElement => key switch
+ {
+ "CU" or "CD" or "DN" or "OV" or "UN" => DriverDataType.Boolean,
+ "PRE" or "ACC" => DriverDataType.Int32,
+ _ => t.ToDriverDataType(),
+ },
+ AbLegacyDataType.ControlElement => key switch
+ {
+ "EN" or "EU" or "DN" or "EM" or "ER" or "UL" or "IN" or "FD" => DriverDataType.Boolean,
+ "LEN" or "POS" => DriverDataType.Int32,
+ _ => t.ToDriverDataType(),
+ },
+ _ => t.ToDriverDataType(),
+ };
+ }
+
+ ///
+ /// Bit position within the parent control word for Timer/Counter/Control status bits.
+ /// Returns null if the sub-element is not a known bit member of the given element
+ /// type. Bit numbering follows Rockwell DTAM / PCCC documentation.
+ ///
+ public static int? StatusBitIndex(AbLegacyDataType t, string? subElement)
+ {
+ if (subElement is null) return null;
+ var key = subElement.ToUpperInvariant();
+ return t switch
+ {
+ // T4 element word 0: bit 13=DN, 14=TT, 15=EN.
+ AbLegacyDataType.TimerElement => key switch
+ {
+ "DN" => 13,
+ "TT" => 14,
+ "EN" => 15,
+ _ => null,
+ },
+ // C5 element word 0: bit 10=UN, 11=OV, 12=DN, 13=CD, 14=CU.
+ AbLegacyDataType.CounterElement => key switch
+ {
+ "UN" => 10,
+ "OV" => 11,
+ "DN" => 12,
+ "CD" => 13,
+ "CU" => 14,
+ _ => null,
+ },
+ // R6 element word 0: bit 8=FD, 9=IN, 10=UL, 11=ER, 12=EM, 13=DN, 14=EU, 15=EN.
+ AbLegacyDataType.ControlElement => key switch
+ {
+ "FD" => 8,
+ "IN" => 9,
+ "UL" => 10,
+ "ER" => 11,
+ "EM" => 12,
+ "DN" => 13,
+ "EU" => 14,
+ "EN" => 15,
+ _ => null,
+ },
+ _ => null,
+ };
+ }
+
+ ///
+ /// PLC-set status bits — read-only from the OPC UA side. Operator-controllable bits
+ /// (e.g. .EN on a timer/counter, .CU/.CD rung-driven inputs) are
+ /// omitted so they keep default writable behaviour.
+ ///
+ public static bool IsPlcSetStatusBit(AbLegacyDataType t, string? subElement)
+ {
+ if (subElement is null) return false;
+ var key = subElement.ToUpperInvariant();
+ return t switch
+ {
+ AbLegacyDataType.TimerElement => key is "DN" or "TT",
+ AbLegacyDataType.CounterElement => key is "DN" or "OV" or "UN",
+ AbLegacyDataType.ControlElement => key is "DN" or "EM" or "ER" or "FD" or "UL" or "IN",
+ _ => false,
+ };
+ }
}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
index bef7957..86469b4 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
@@ -141,7 +141,12 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
}
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
- var value = runtime.DecodeValue(def.DataType, parsed?.BitIndex);
+ // Timer/Counter/Control status bits route through GetBit at the parent-word
+ // address — translate the .DN/.EN/etc. sub-element to its standard bit position
+ // and pass it down to the runtime as a synthetic bitIndex.
+ var decodeBit = parsed?.BitIndex
+ ?? AbLegacyDataTypeExtensions.StatusBitIndex(def.DataType, parsed?.SubElement);
+ var value = runtime.DecodeValue(def.DataType, decodeBit);
results[i] = new DataValueSnapshot(value, AbLegacyStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null);
}
@@ -188,6 +193,15 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
{
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
+ // Timer/Counter/Control PLC-set status bits (DN, TT, OV, UN, FD, ER, EM, UL,
+ // IN) are read-only — the PLC sets them; any client write would be silently
+ // overwritten on the next scan. Reject up front with BadNotWritable.
+ if (AbLegacyDataTypeExtensions.IsPlcSetStatusBit(def.DataType, parsed?.SubElement))
+ {
+ results[i] = new WriteResult(AbLegacyStatusMapper.BadNotWritable);
+ continue;
+ }
+
// PCCC bit-within-word writes — task #181 pass 2. RMW against a parallel
// parent-word runtime (strip the /N bit suffix). Per-parent-word lock serialises
// concurrent bit writers. Applies to N-file bit-in-word (N7:0/3) + B-file bits
@@ -247,12 +261,19 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase));
foreach (var tag in tagsForDevice)
{
+ var parsed = AbLegacyAddress.TryParse(tag.Address, device.PlcFamily);
+ // Timer/Counter/Control sub-elements (.DN/.EN/.TT/.PRE/.ACC/etc.) refine the
+ // base element's Int32 to Boolean for status bits and Int32 for word members.
+ var effectiveType = AbLegacyDataTypeExtensions.EffectiveDriverDataType(
+ tag.DataType, parsed?.SubElement);
+ var plcSetBit = AbLegacyDataTypeExtensions.IsPlcSetStatusBit(
+ tag.DataType, parsed?.SubElement);
deviceFolder.Variable(tag.Name, tag.Name, new DriverAttributeInfo(
FullName: tag.Name,
- DriverDataType: tag.DataType.ToDriverDataType(),
+ DriverDataType: effectiveType,
IsArray: false,
ArrayDim: null,
- SecurityClass: tag.Writable
+ SecurityClass: tag.Writable && !plcSetBit
? SecurityClassification.Operate
: SecurityClassification.ViewOnly,
IsHistorized: false,
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
index 3f8e1aa..556d3d7 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
@@ -40,8 +40,14 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
AbLegacyDataType.Long => _tag.GetInt32(0),
AbLegacyDataType.Float => _tag.GetFloat32(0),
AbLegacyDataType.String => _tag.GetString(0),
+ // Timer/Counter/Control sub-elements: bitIndex is the status bit position within the
+ // parent control word (encoded by AbLegacyDriver from the .DN / .EN / etc. sub-element
+ // name). Word members (.PRE / .ACC / .LEN / .POS) come through with bitIndex=null and
+ // decode as Int32 like before.
AbLegacyDataType.TimerElement or AbLegacyDataType.CounterElement
- or AbLegacyDataType.ControlElement => _tag.GetInt32(0),
+ or AbLegacyDataType.ControlElement => bitIndex is int statusBit
+ ? _tag.GetBit(statusBit)
+ : _tag.GetInt32(0),
_ => null,
};
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs
index 5935d22..86dc460 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs
@@ -102,4 +102,96 @@ public sealed class AbLegacyDriverTests
AbLegacyDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
AbLegacyDataType.TimerElement.ToDriverDataType().ShouldBe(DriverDataType.Int32);
}
+
+ [Theory]
+ [InlineData(AbLegacyDataType.TimerElement, "EN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.TimerElement, "TT", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.TimerElement, "DN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.TimerElement, "PRE", DriverDataType.Int32)]
+ [InlineData(AbLegacyDataType.TimerElement, "ACC", DriverDataType.Int32)]
+ [InlineData(AbLegacyDataType.CounterElement, "CU", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.CounterElement, "CD", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.CounterElement, "DN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.CounterElement, "OV", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.CounterElement, "UN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.CounterElement, "PRE", DriverDataType.Int32)]
+ [InlineData(AbLegacyDataType.CounterElement, "ACC", DriverDataType.Int32)]
+ [InlineData(AbLegacyDataType.ControlElement, "EN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "EU", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "DN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "EM", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "ER", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "UL", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "IN", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "FD", DriverDataType.Boolean)]
+ [InlineData(AbLegacyDataType.ControlElement, "LEN", DriverDataType.Int32)]
+ [InlineData(AbLegacyDataType.ControlElement, "POS", DriverDataType.Int32)]
+ public void EffectiveDriverDataType_resolves_subelements(
+ AbLegacyDataType dataType, string subElement, DriverDataType expected)
+ {
+ AbLegacyDataTypeExtensions.EffectiveDriverDataType(dataType, subElement).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void EffectiveDriverDataType_unknown_subelement_falls_back_to_base()
+ {
+ // Permissive — keeps the driver from refusing tags whose sub-element we don't catalogue.
+ AbLegacyDataTypeExtensions.EffectiveDriverDataType(AbLegacyDataType.TimerElement, "BOGUS")
+ .ShouldBe(DriverDataType.Int32);
+ AbLegacyDataTypeExtensions.EffectiveDriverDataType(AbLegacyDataType.TimerElement, null)
+ .ShouldBe(DriverDataType.Int32);
+ AbLegacyDataTypeExtensions.EffectiveDriverDataType(AbLegacyDataType.Int, "DN")
+ .ShouldBe(DriverDataType.Int32);
+ }
+
+ [Theory]
+ [InlineData(AbLegacyDataType.TimerElement, "DN", 13)]
+ [InlineData(AbLegacyDataType.TimerElement, "TT", 14)]
+ [InlineData(AbLegacyDataType.TimerElement, "EN", 15)]
+ [InlineData(AbLegacyDataType.CounterElement, "UN", 10)]
+ [InlineData(AbLegacyDataType.CounterElement, "OV", 11)]
+ [InlineData(AbLegacyDataType.CounterElement, "DN", 12)]
+ [InlineData(AbLegacyDataType.CounterElement, "CD", 13)]
+ [InlineData(AbLegacyDataType.CounterElement, "CU", 14)]
+ [InlineData(AbLegacyDataType.ControlElement, "FD", 8)]
+ [InlineData(AbLegacyDataType.ControlElement, "IN", 9)]
+ [InlineData(AbLegacyDataType.ControlElement, "UL", 10)]
+ [InlineData(AbLegacyDataType.ControlElement, "ER", 11)]
+ [InlineData(AbLegacyDataType.ControlElement, "EM", 12)]
+ [InlineData(AbLegacyDataType.ControlElement, "DN", 13)]
+ [InlineData(AbLegacyDataType.ControlElement, "EU", 14)]
+ [InlineData(AbLegacyDataType.ControlElement, "EN", 15)]
+ public void StatusBitIndex_maps_to_standard_pccc_positions(
+ AbLegacyDataType dataType, string subElement, int expectedBit)
+ {
+ AbLegacyDataTypeExtensions.StatusBitIndex(dataType, subElement).ShouldBe(expectedBit);
+ }
+
+ [Fact]
+ public void StatusBitIndex_for_word_subelements_is_null()
+ {
+ AbLegacyDataTypeExtensions.StatusBitIndex(AbLegacyDataType.TimerElement, "PRE").ShouldBeNull();
+ AbLegacyDataTypeExtensions.StatusBitIndex(AbLegacyDataType.CounterElement, "ACC").ShouldBeNull();
+ AbLegacyDataTypeExtensions.StatusBitIndex(AbLegacyDataType.ControlElement, "LEN").ShouldBeNull();
+ AbLegacyDataTypeExtensions.StatusBitIndex(AbLegacyDataType.TimerElement, null).ShouldBeNull();
+ AbLegacyDataTypeExtensions.StatusBitIndex(AbLegacyDataType.Int, "DN").ShouldBeNull();
+ }
+
+ [Theory]
+ [InlineData(AbLegacyDataType.TimerElement, "DN", true)]
+ [InlineData(AbLegacyDataType.TimerElement, "TT", true)]
+ [InlineData(AbLegacyDataType.TimerElement, "EN", false)] // operator-controllable
+ [InlineData(AbLegacyDataType.CounterElement, "DN", true)]
+ [InlineData(AbLegacyDataType.CounterElement, "OV", true)]
+ [InlineData(AbLegacyDataType.CounterElement, "UN", true)]
+ [InlineData(AbLegacyDataType.CounterElement, "CU", false)]
+ [InlineData(AbLegacyDataType.ControlElement, "DN", true)]
+ [InlineData(AbLegacyDataType.ControlElement, "ER", true)]
+ [InlineData(AbLegacyDataType.ControlElement, "EM", true)]
+ [InlineData(AbLegacyDataType.ControlElement, "EN", false)]
+ public void IsPlcSetStatusBit_classifies_writable_vs_status_bits(
+ AbLegacyDataType dataType, string subElement, bool expected)
+ {
+ AbLegacyDataTypeExtensions.IsPlcSetStatusBit(dataType, subElement).ShouldBe(expected);
+ }
}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
index b71ade6..c9eed25 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
@@ -256,4 +256,113 @@ public sealed class AbLegacyReadWriteTests
Value = value;
}
}
+
+ // ---- Timer / Counter / Control sub-element bit semantics (issue #246) ----
+
+ [Theory]
+ [InlineData("T4:0.DN", 13)]
+ [InlineData("T4:0.TT", 14)]
+ [InlineData("T4:0.EN", 15)]
+ public async Task Timer_status_bit_decodes_correct_position(string address, int bitPos)
+ {
+ var (drv, factory) = NewDriver(
+ new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", address, AbLegacyDataType.TimerElement));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ // Seed a parent-word with only the target bit set.
+ factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 << bitPos };
+
+ var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
+
+ snapshots.Single().Value.ShouldBe(true);
+ // The driver must have asked the runtime for the right bit position.
+ factory.Tags[address].LastDecodeBitIndex.ShouldBe(bitPos);
+ }
+
+ [Fact]
+ public async Task Timer_PRE_subelement_decodes_as_int_word()
+ {
+ var (drv, factory) = NewDriver(
+ new AbLegacyTagDefinition("Pre", "ab://10.0.0.5/1,0", "T4:0.PRE", AbLegacyDataType.TimerElement));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ factory.Customise = p => new FakeAbLegacyTag(p) { Value = 5000 };
+
+ var snapshots = await drv.ReadAsync(["Pre"], CancellationToken.None);
+
+ snapshots.Single().Value.ShouldBe(5000);
+ factory.Tags["T4:0.PRE"].LastDecodeBitIndex.ShouldBeNull();
+ }
+
+ [Theory]
+ [InlineData("C5:0.UN", 10)]
+ [InlineData("C5:0.OV", 11)]
+ [InlineData("C5:0.DN", 12)]
+ [InlineData("C5:0.CD", 13)]
+ [InlineData("C5:0.CU", 14)]
+ public async Task Counter_status_bit_decodes_correct_position(string address, int bitPos)
+ {
+ var (drv, factory) = NewDriver(
+ new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", address, AbLegacyDataType.CounterElement));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 << bitPos };
+
+ var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
+ snapshots.Single().Value.ShouldBe(true);
+ factory.Tags[address].LastDecodeBitIndex.ShouldBe(bitPos);
+ }
+
+ [Theory]
+ [InlineData("R6:0.FD", 8)]
+ [InlineData("R6:0.IN", 9)]
+ [InlineData("R6:0.UL", 10)]
+ [InlineData("R6:0.ER", 11)]
+ [InlineData("R6:0.EM", 12)]
+ [InlineData("R6:0.DN", 13)]
+ [InlineData("R6:0.EU", 14)]
+ [InlineData("R6:0.EN", 15)]
+ public async Task Control_status_bit_decodes_correct_position(string address, int bitPos)
+ {
+ var (drv, factory) = NewDriver(
+ new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", address, AbLegacyDataType.ControlElement));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 << bitPos };
+
+ var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
+ snapshots.Single().Value.ShouldBe(true);
+ factory.Tags[address].LastDecodeBitIndex.ShouldBe(bitPos);
+ }
+
+ [Fact]
+ public async Task Status_bit_returns_false_when_parent_word_bit_is_clear()
+ {
+ var (drv, factory) = NewDriver(
+ new AbLegacyTagDefinition("Done", "ab://10.0.0.5/1,0", "T4:0.DN", AbLegacyDataType.TimerElement));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ // Bit 14 (TT) set, bit 13 (DN) clear.
+ factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 << 14 };
+
+ var snapshots = await drv.ReadAsync(["Done"], CancellationToken.None);
+ snapshots.Single().Value.ShouldBe(false);
+ }
+
+ [Theory]
+ [InlineData("T4:0.DN", AbLegacyDataType.TimerElement)]
+ [InlineData("T4:0.TT", AbLegacyDataType.TimerElement)]
+ [InlineData("C5:0.DN", AbLegacyDataType.CounterElement)]
+ [InlineData("C5:0.OV", AbLegacyDataType.CounterElement)]
+ [InlineData("C5:0.UN", AbLegacyDataType.CounterElement)]
+ [InlineData("R6:0.ER", AbLegacyDataType.ControlElement)]
+ [InlineData("R6:0.EM", AbLegacyDataType.ControlElement)]
+ [InlineData("R6:0.DN", AbLegacyDataType.ControlElement)]
+ [InlineData("R6:0.FD", AbLegacyDataType.ControlElement)]
+ public async Task Writes_to_PLC_set_status_bits_return_BadNotWritable(
+ string address, AbLegacyDataType dataType)
+ {
+ var (drv, _) = NewDriver(
+ new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", address, dataType));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+
+ var results = await drv.WriteAsync(
+ [new WriteRequest("X", true)], CancellationToken.None);
+ results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
+ }
}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs
index 914fa8f..14b6a0e 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs
@@ -40,7 +40,25 @@ internal class FakeAbLegacyTag : IAbLegacyTagRuntime
}
public virtual int GetStatus() => Status;
- public virtual object? DecodeValue(AbLegacyDataType type, int? bitIndex) => Value;
+ public int? LastDecodeBitIndex { get; private set; }
+ public AbLegacyDataType? LastDecodeType { get; private set; }
+ public virtual object? DecodeValue(AbLegacyDataType type, int? bitIndex)
+ {
+ LastDecodeType = type;
+ LastDecodeBitIndex = bitIndex;
+ // If the test seeded a parent-word value (ushort/short/int) and the driver asked for a
+ // specific status bit, mask it out so we can assert the correct bit reaches the client.
+ if (bitIndex is int bit && Value is not null and not bool)
+ {
+ try
+ {
+ var word = Convert.ToInt32(Value);
+ return ((word >> bit) & 1) != 0;
+ }
+ catch (Exception ex) when (ex is FormatException or InvalidCastException) { }
+ }
+ return Value;
+ }
public virtual void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value;
public virtual void Dispose() => Disposed = true;
}