c689ac58b1
Closes #250
178 lines
8.7 KiB
C#
178 lines
8.7 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;
|
||
|
||
/// <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);
|
||
}
|