From c89f5bb3b9d7df14e81eb521454cc3aea18eb23e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 25 Apr 2026 13:41:52 -0400 Subject: [PATCH] =?UTF-8?q?Auto:=20ablegacy-3=20=E2=80=94=20sub-element=20?= =?UTF-8?q?bit=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #246 --- .../AbLegacyDataType.cs | 99 ++++++++++++++++ .../AbLegacyDriver.cs | 27 ++++- .../LibplctagLegacyTagRuntime.cs | 8 +- .../AbLegacyDriverTests.cs | 92 +++++++++++++++ .../AbLegacyReadWriteTests.cs | 109 ++++++++++++++++++ .../FakeAbLegacyTag.cs | 20 +++- 6 files changed, 350 insertions(+), 5 deletions(-) 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; }