Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
T
2026-04-25 23:36:01 -04:00

178 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// Maximum payload length for an ST (string) file element on SLC / MicroLogix / PLC-5.
/// The on-wire layout is a 1-word length prefix followed by 82 ASCII bytes — libplctag's
/// <c>SetString</c> handles the framing internally, but it does NOT validate length, so a
/// 93-byte source string would silently truncate. We reject up-front so the OPC UA client
/// gets a clean <c>BadOutOfRange</c> rather than a corrupted PLC value.
/// </summary>
internal const int StFileMaxStringLength = 82;
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,
};
// PR 7 — array contiguous-block reads. Setting ElementCount tells libplctag to allocate
// a buffer covering N consecutive PCCC words (one frame, up to ~120 elements). The
// driver decodes element-by-element through DecodeArrayElement after a single ReadAsync.
if (p.ElementCount > 1)
_tag.ElementCount = p.ElementCount;
}
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:
{
var s = Convert.ToString(value) ?? string.Empty;
if (s.Length > StFileMaxStringLength)
throw new ArgumentOutOfRangeException(
nameof(value),
$"ST string write exceeds {StFileMaxStringLength}-byte file element capacity (was {s.Length}).");
_tag.SetString(0, s);
}
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.");
}
}
/// <summary>
/// PR 7 — decode element <paramref name="elementIndex"/> of an N-element contiguous
/// PCCC block read. Element width is fixed per data type: Int / AnalogInt / Bit-as-word
/// are 16-bit (2 bytes/element), Long / Float are 32-bit (4 bytes/element). Mirrors the
/// non-array decoder shape but at byte offset <c>elementIndex × elementBytes</c>.
/// </summary>
public object? DecodeArrayElement(AbLegacyDataType type, int elementIndex)
{
if (elementIndex < 0) throw new ArgumentOutOfRangeException(nameof(elementIndex));
return type switch
{
// Bit / N-array reads — Rockwell convention is one BOOL per word (e.g. `B3:0,10`
// returns 10 BOOLs, not 160 individual bits). Each word is non-zero → true.
AbLegacyDataType.Bit => _tag.GetInt16(elementIndex * 2) != 0,
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => (int)_tag.GetInt16(elementIndex * 2),
AbLegacyDataType.Long => _tag.GetInt32(elementIndex * 4),
AbLegacyDataType.Float => _tag.GetFloat32(elementIndex * 4),
// String + element types are out-of-scope for PR 7 array reads — the PCCC layer's
// 240-byte frame ceiling means an ST array would only fit a couple of strings, and
// sub-element arrays (`T4:0,5.ACC`) are rejected at parse time. Surface a clear
// error if the driver mis-routes us here.
_ => throw new NotSupportedException(
$"AbLegacyDataType {type} cannot be decoded as a contiguous array element."),
};
}
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);
}