@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user