@@ -109,4 +109,103 @@ public static class AbLegacyDataTypeExtensions
|
|||||||
AbLegacyDataType.MicroLogixFunctionFile => DriverDataType.Int32,
|
AbLegacyDataType.MicroLogixFunctionFile => DriverDataType.Int32,
|
||||||
_ => DriverDataType.Int32,
|
_ => DriverDataType.Int32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub-element-aware driver type. Timer/Counter/Control elements expose Boolean status
|
||||||
|
/// bits (<c>.DN</c>, <c>.EN</c>, <c>.TT</c>, <c>.CU</c>, <c>.CD</c>, <c>.OV</c>,
|
||||||
|
/// <c>.UN</c>, <c>.ER</c>, etc.) and Int32 word members (<c>.PRE</c>, <c>.ACC</c>,
|
||||||
|
/// <c>.LEN</c>, <c>.POS</c>). Unknown sub-elements fall back to
|
||||||
|
/// <see cref="ToDriverDataType"/> so the driver remains permissive.
|
||||||
|
/// </summary>
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bit position within the parent control word for Timer/Counter/Control status bits.
|
||||||
|
/// Returns <c>null</c> if the sub-element is not a known bit member of the given element
|
||||||
|
/// type. Bit numbering follows Rockwell DTAM / PCCC documentation.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PLC-set status bits — read-only from the OPC UA side. Operator-controllable bits
|
||||||
|
/// (e.g. <c>.EN</c> on a timer/counter, <c>.CU</c>/<c>.CD</c> rung-driven inputs) are
|
||||||
|
/// omitted so they keep default writable behaviour.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,12 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
|
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);
|
results[i] = new DataValueSnapshot(value, AbLegacyStatusMapper.Good, now, now);
|
||||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
_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);
|
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
|
// 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
|
// 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
|
// 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));
|
string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase));
|
||||||
foreach (var tag in tagsForDevice)
|
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(
|
deviceFolder.Variable(tag.Name, tag.Name, new DriverAttributeInfo(
|
||||||
FullName: tag.Name,
|
FullName: tag.Name,
|
||||||
DriverDataType: tag.DataType.ToDriverDataType(),
|
DriverDataType: effectiveType,
|
||||||
IsArray: false,
|
IsArray: false,
|
||||||
ArrayDim: null,
|
ArrayDim: null,
|
||||||
SecurityClass: tag.Writable
|
SecurityClass: tag.Writable && !plcSetBit
|
||||||
? SecurityClassification.Operate
|
? SecurityClassification.Operate
|
||||||
: SecurityClassification.ViewOnly,
|
: SecurityClassification.ViewOnly,
|
||||||
IsHistorized: false,
|
IsHistorized: false,
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
|||||||
AbLegacyDataType.Long => _tag.GetInt32(0),
|
AbLegacyDataType.Long => _tag.GetInt32(0),
|
||||||
AbLegacyDataType.Float => _tag.GetFloat32(0),
|
AbLegacyDataType.Float => _tag.GetFloat32(0),
|
||||||
AbLegacyDataType.String => _tag.GetString(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
|
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,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,4 +102,96 @@ public sealed class AbLegacyDriverTests
|
|||||||
AbLegacyDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
|
AbLegacyDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
|
||||||
AbLegacyDataType.TimerElement.ToDriverDataType().ShouldBe(DriverDataType.Int32);
|
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;
|
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 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 EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value;
|
||||||
public virtual void Dispose() => Disposed = true;
|
public virtual void Dispose() => Disposed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user