Adds PD (PID), MG (Message), PLS (Programmable Limit Switch) and BT
(Block Transfer) file types to the PCCC parser. New AbLegacyDataType
enum members (PidElement / MessageElement / PlsElement /
BlockTransferElement) plus per-type sub-element catalogue:
- PD: SP/PV/CV/KP/KI/KD/MAXS/MINS/DB/OUT as Float32; EN/DN/MO/PE/
AUTO/MAN/SP_VAL/SP_LL/SP_HL as Boolean (word-0 status bits).
- MG: RBE/MS/SIZE/LEN as Int32; EN/EW/ER/DN/ST/CO/NR/TO as Boolean.
- PLS: LEN as Int32 (bit table varies per PLC).
- BT: RLEN/DLEN as Int32; EN/ST/DN/ER/CO/EW/TO/NR as Boolean.
Per-family flags on AbLegacyPlcFamilyProfile gate availability:
- PD/MG: SLC500 + PLC-5 (operator + status bits both present).
- PLS/BT: PLC-5 only (chassis-IO block transfer is PLC-5-specific).
- MicroLogix + LogixPccc: rejected — no legacy file-letter form.
Status-bit indices match Rockwell DTAM / 1747-RM001 / 1785-6.5.12:
PD word 0 bits 0-8, MG/BT word 0 bits 8-15. PLC-set status bits
(PE/DN/SP_*; ST/DN/ER/CO/EW/NR/TO) are surfaced as ViewOnly via
IsPlcSetStatusBit, matching the Timer/Counter/Control pattern from
ablegacy-3.
LibplctagLegacyTagRuntime decodes PD non-bit members as Float32 and
MG/BT/PLS non-bit members as Int32; status bits route through GetBit
with the bit-position encoded by the driver via StatusBitIndex.
Tests: parser positive cases per family + negative cases per family,
catalogue + bit-index + read-only-bit assertions.
Closes #248
131 lines
5.8 KiB
C#
131 lines
5.8 KiB
C#
using libplctag;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
|
|
|
/// <summary>
|
|
/// Default libplctag-backed <see cref="IAbLegacyTagRuntime"/>. Uses <c>ab_pccc</c> protocol
|
|
/// on top of EtherNet/IP — libplctag's PCCC layer handles the file-letter + word + bit +
|
|
/// sub-element decoding internally, so our wrapper just has to forward the atomic type to
|
|
/// the right Get/Set call.
|
|
/// </summary>
|
|
internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
|
{
|
|
private readonly Tag _tag;
|
|
|
|
public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
|
|
{
|
|
_tag = new Tag
|
|
{
|
|
Gateway = p.Gateway,
|
|
Path = p.CipPath,
|
|
PlcType = MapPlcType(p.LibplctagPlcAttribute),
|
|
Protocol = Protocol.ab_eip, // PCCC-over-EIP; libplctag routes via the PlcType-specific PCCC layer
|
|
Name = p.TagName,
|
|
Timeout = p.Timeout,
|
|
};
|
|
}
|
|
|
|
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
|
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
|
|
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
|
|
|
|
public int GetStatus() => (int)_tag.GetStatus();
|
|
|
|
public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch
|
|
{
|
|
AbLegacyDataType.Bit => bitIndex is int bit
|
|
? _tag.GetBit(bit)
|
|
: _tag.GetInt8(0) != 0,
|
|
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => (int)_tag.GetInt16(0),
|
|
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 => bitIndex is int statusBit
|
|
? _tag.GetBit(statusBit)
|
|
: _tag.GetInt32(0),
|
|
// PD-file (PID): non-bit members (SP/PV/CV/KP/KI/KD/MAXS/MINS/DB/OUT) are 32-bit floats.
|
|
// Status bits (EN/DN/MO/PE/AUTO/MAN/SP_VAL/SP_LL/SP_HL) live in the parent control word
|
|
// and read through GetBit — the driver encodes the position via StatusBitIndex.
|
|
AbLegacyDataType.PidElement => bitIndex is int pidBit
|
|
? _tag.GetBit(pidBit)
|
|
: _tag.GetFloat32(0),
|
|
// MG/BT/PLS: non-bit members (RBE/MS/SIZE/LEN, RLEN/DLEN) are word-sized integers.
|
|
AbLegacyDataType.MessageElement or AbLegacyDataType.BlockTransferElement
|
|
or AbLegacyDataType.PlsElement => bitIndex is int statusBit2
|
|
? _tag.GetBit(statusBit2)
|
|
: _tag.GetInt32(0),
|
|
_ => null,
|
|
};
|
|
|
|
public void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value)
|
|
{
|
|
switch (type)
|
|
{
|
|
case AbLegacyDataType.Bit:
|
|
if (bitIndex is int)
|
|
// Bit-within-word writes are routed at the driver level
|
|
// (AbLegacyDriver.WriteBitInWordAsync) via a parallel parent-word runtime —
|
|
// this branch only fires if dispatch was bypassed. Throw loudly rather than
|
|
// silently clobbering the whole word.
|
|
throw new NotSupportedException(
|
|
"Bit-with-bitIndex writes must go through AbLegacyDriver.WriteBitInWordAsync.");
|
|
_tag.SetInt8(0, Convert.ToBoolean(value) ? (sbyte)1 : (sbyte)0);
|
|
break;
|
|
case AbLegacyDataType.Int:
|
|
case AbLegacyDataType.AnalogInt:
|
|
_tag.SetInt16(0, Convert.ToInt16(value));
|
|
break;
|
|
case AbLegacyDataType.Long:
|
|
_tag.SetInt32(0, Convert.ToInt32(value));
|
|
break;
|
|
case AbLegacyDataType.Float:
|
|
_tag.SetFloat32(0, Convert.ToSingle(value));
|
|
break;
|
|
case AbLegacyDataType.String:
|
|
_tag.SetString(0, Convert.ToString(value) ?? string.Empty);
|
|
break;
|
|
case AbLegacyDataType.TimerElement:
|
|
case AbLegacyDataType.CounterElement:
|
|
case AbLegacyDataType.ControlElement:
|
|
_tag.SetInt32(0, Convert.ToInt32(value));
|
|
break;
|
|
// PD-file non-bit writes route to the Float backing store. Status-bit writes within
|
|
// the parent word are blocked at the driver layer (PLC-set bits are read-only and
|
|
// operator-controllable bits go through the bit-RMW path with the parent word typed
|
|
// as Int).
|
|
case AbLegacyDataType.PidElement:
|
|
_tag.SetFloat32(0, Convert.ToSingle(value));
|
|
break;
|
|
case AbLegacyDataType.MessageElement:
|
|
case AbLegacyDataType.BlockTransferElement:
|
|
case AbLegacyDataType.PlsElement:
|
|
_tag.SetInt32(0, Convert.ToInt32(value));
|
|
break;
|
|
default:
|
|
throw new NotSupportedException($"AbLegacyDataType {type} not writable.");
|
|
}
|
|
}
|
|
|
|
public void Dispose() => _tag.Dispose();
|
|
|
|
private static PlcType MapPlcType(string attribute) => attribute switch
|
|
{
|
|
"slc500" => PlcType.Slc500,
|
|
"micrologix" => PlcType.MicroLogix,
|
|
"plc5" => PlcType.Plc5,
|
|
"logixpccc" => PlcType.LogixPccc,
|
|
_ => PlcType.Slc500,
|
|
};
|
|
}
|
|
|
|
internal sealed class LibplctagLegacyTagFactory : IAbLegacyTagFactory
|
|
{
|
|
public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams createParams) =>
|
|
new LibplctagLegacyTagRuntime(createParams);
|
|
}
|