Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
2026-04-25 22:58:33 -04:00

252 lines
12 KiB
C#

using System.Reflection;
using libplctag;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Default libplctag-backed <see cref="IAbCipTagRuntime"/>. Wraps a <see cref="Tag"/>
/// instance + translates our <see cref="AbCipDataType"/> enum into the
/// <c>GetInt32</c> / <c>GetFloat32</c> / <c>GetString</c> / <c>GetBit</c> calls libplctag
/// exposes. One runtime instance per <c>(device, tag path)</c>; lifetime is owned by the
/// driver's per-device state dict.
/// </summary>
internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
{
private readonly Tag _tag;
private readonly int _connectionSize;
private readonly AddressingMode _addressingMode;
private readonly uint? _logicalInstanceId;
public LibplctagTagRuntime(AbCipTagCreateParams p)
{
_tag = new Tag
{
Gateway = p.Gateway,
Path = p.CipPath,
PlcType = MapPlcType(p.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = p.TagName,
Timeout = p.Timeout,
};
// PR abcip-1.2 — Logix STRINGnn variant decoding. When the caller pins a non-default
// DATA-array capacity (STRING_20 / STRING_40 / STRING_80 etc.), forward it to libplctag
// via the StringMaxCapacity attribute so GetString / SetString truncate at the right
// boundary. Null leaves libplctag at its default 82-byte STRING for back-compat.
if (p.StringMaxCapacity is int cap && cap > 0)
_tag.StringMaxCapacity = (uint)cap;
// PR abcip-1.3 — slice reads. Setting ElementCount tells libplctag to allocate a buffer
// covering N consecutive elements; the array-read planner pairs this with TagName=Tag[N]
// to issue one Rockwell array read for a [N..M] slice.
if (p.ElementCount is int n && n > 0)
_tag.ElementCount = n;
_connectionSize = p.ConnectionSize;
_addressingMode = p.AddressingMode;
_logicalInstanceId = p.LogicalInstanceId;
}
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
// PR abcip-3.1 — propagate the configured CIP connection size to the native libplctag
// handle. The 1.5.x C# wrapper does not expose <c>connection_size</c> as a public Tag
// property, so we reach into the internal <c>NativeTagWrapper</c>'s
// <c>SetIntAttribute</c> (mirroring libplctag's <c>plc_tag_set_int_attribute</c>).
// libplctag native parses <c>connection_size</c> at create time, so this best-effort
// call lights up automatically when a future wrapper release exposes the attribute or
// when libplctag native gains post-create hot-update support — until then it falls back
// to the wrapper default. Failures (older / patched wrappers without the internal API)
// are intentionally swallowed so the driver keeps initialising.
TrySetConnectionSize(_tag, _connectionSize);
// PR abcip-3.2 — propagate the addressing mode + (when known) the resolved Symbol
// Object instance ID. Same reflection-fallback shape as ConnectionSize: the libplctag
// .NET wrapper (1.5.x) doesn't expose a public knob for instance-ID addressing, so
// we forward the relevant attribute string through NativeTagWrapper.SetAttributeString.
// Logical mode lights up only when the driver has populated LogicalInstanceId via the
// one-time @tags walk; first reads on a Logical device + every Symbolic-mode read take
// the libplctag default ASCII-symbolic path.
if (_addressingMode == AddressingMode.Logical)
TrySetLogicalAddressing(_tag, _logicalInstanceId);
}
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
/// <summary>
/// Best-effort propagation of <c>connection_size</c> to libplctag native. Reflects into
/// the wrapper's internal <c>NativeTagWrapper.SetIntAttribute(string, int)</c>; isolated
/// in a static helper so the lookup costs run once + the failure path is one line.
/// </summary>
private static void TrySetConnectionSize(Tag tag, int connectionSize)
{
try
{
var wrapperField = typeof(Tag).GetField("_tag", BindingFlags.NonPublic | BindingFlags.Instance);
var wrapper = wrapperField?.GetValue(tag);
if (wrapper is null) return;
var setInt = wrapper.GetType().GetMethod(
"SetIntAttribute",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
types: [typeof(string), typeof(int)],
modifiers: null);
setInt?.Invoke(wrapper, ["connection_size", connectionSize]);
}
catch
{
// Wrapper internals shifted (newer libplctag.NET) — drop quietly. Either the new
// wrapper exposes ConnectionSize directly (our reflection no-ops) or operators must
// upgrade to a known-good version per docs/drivers/AbCip-Performance.md.
}
}
/// <summary>
/// PR abcip-3.2 — best-effort propagation of CIP logical-segment / instance-ID
/// addressing to libplctag native. Two attributes are forwarded:
/// <list type="bullet">
/// <item><c>use_connected_msg=1</c> — instance-ID addressing only works over a
/// connected CIP session; switch the tag to use Forward Open + Class3 messaging.</item>
/// <item><c>cip_addr=0x6B,N</c> — replace the ASCII Symbol Object lookup with a
/// direct logical segment reference, where <c>N</c> is the resolved instance ID
/// from the driver's one-time <c>@tags</c> walk.</item>
/// </list>
/// Same reflection-via-<c>NativeTagWrapper.SetAttributeString</c> shape as
/// <see cref="TrySetConnectionSize"/> — the 1.5.x .NET wrapper does not expose a
/// public knob, so we degrade gracefully when the internal API is not present.
/// </summary>
private static void TrySetLogicalAddressing(Tag tag, uint? logicalInstanceId)
{
try
{
var wrapperField = typeof(Tag).GetField("_tag", BindingFlags.NonPublic | BindingFlags.Instance);
var wrapper = wrapperField?.GetValue(tag);
if (wrapper is null) return;
var setStr = wrapper.GetType().GetMethod(
"SetAttributeString",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
types: [typeof(string), typeof(string)],
modifiers: null);
if (setStr is null) return;
setStr.Invoke(wrapper, ["use_connected_msg", "1"]);
if (logicalInstanceId is uint id)
setStr.Invoke(wrapper, ["cip_addr", $"0x6B,{id}"]);
}
catch
{
// Wrapper internals not present / shifted — fall back to symbolic addressing on
// the wire. Driver-level logical-mode bookkeeping (the @tags map) is still useful
// because future wrapper releases may expose this attribute publicly + the
// reflection lights up cleanly then.
}
}
public int GetStatus() => (int)_tag.GetStatus();
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch
{
AbCipDataType.Bool => bitIndex is int bit
? _tag.GetBit(bit)
: _tag.GetInt8(offset) != 0,
AbCipDataType.SInt => (int)(sbyte)_tag.GetInt8(offset),
AbCipDataType.USInt => (int)_tag.GetUInt8(offset),
AbCipDataType.Int => (int)_tag.GetInt16(offset),
AbCipDataType.UInt => (int)_tag.GetUInt16(offset),
AbCipDataType.DInt => _tag.GetInt32(offset),
AbCipDataType.UDInt => (int)_tag.GetUInt32(offset),
AbCipDataType.LInt => _tag.GetInt64(offset),
AbCipDataType.ULInt => (long)_tag.GetUInt64(offset),
AbCipDataType.Real => _tag.GetFloat32(offset),
AbCipDataType.LReal => _tag.GetFloat64(offset),
AbCipDataType.String => _tag.GetString(offset),
AbCipDataType.Dt => _tag.GetInt64(offset),
AbCipDataType.Structure => null,
_ => null,
};
public void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
{
switch (type)
{
case AbCipDataType.Bool:
if (bitIndex is int)
{
// BOOL-within-DINT writes are routed at the driver level (AbCipDriver.
// WriteBitInDIntAsync) via a parallel parent-DINT runtime so the RMW stays
// serialised. If one reaches here it means the driver dispatch was bypassed —
// throw so the error surfaces loudly rather than clobbering the whole DINT.
throw new NotSupportedException(
"BOOL-with-bitIndex writes must go through AbCipDriver.WriteBitInDIntAsync, not LibplctagTagRuntime.");
}
_tag.SetInt8(0, Convert.ToBoolean(value) ? (sbyte)1 : (sbyte)0);
break;
case AbCipDataType.SInt:
_tag.SetInt8(0, Convert.ToSByte(value));
break;
case AbCipDataType.USInt:
_tag.SetUInt8(0, Convert.ToByte(value));
break;
case AbCipDataType.Int:
_tag.SetInt16(0, Convert.ToInt16(value));
break;
case AbCipDataType.UInt:
_tag.SetUInt16(0, Convert.ToUInt16(value));
break;
case AbCipDataType.DInt:
_tag.SetInt32(0, Convert.ToInt32(value));
break;
case AbCipDataType.UDInt:
_tag.SetUInt32(0, Convert.ToUInt32(value));
break;
case AbCipDataType.LInt:
_tag.SetInt64(0, Convert.ToInt64(value));
break;
case AbCipDataType.ULInt:
_tag.SetUInt64(0, Convert.ToUInt64(value));
break;
case AbCipDataType.Real:
_tag.SetFloat32(0, Convert.ToSingle(value));
break;
case AbCipDataType.LReal:
_tag.SetFloat64(0, Convert.ToDouble(value));
break;
case AbCipDataType.String:
_tag.SetString(0, Convert.ToString(value) ?? string.Empty);
break;
case AbCipDataType.Dt:
_tag.SetInt64(0, Convert.ToInt64(value));
break;
case AbCipDataType.Structure:
throw new NotSupportedException("Whole-UDT writes land in PR 6.");
default:
throw new NotSupportedException($"AbCipDataType {type} not writable.");
}
}
public void Dispose() => _tag.Dispose();
private static PlcType MapPlcType(string attribute) => attribute switch
{
"controllogix" => PlcType.ControlLogix,
"compactlogix" => PlcType.ControlLogix, // libplctag treats CompactLogix under ControlLogix family
"micro800" => PlcType.Micro800,
"micrologix" => PlcType.MicroLogix,
"slc500" => PlcType.Slc500,
"plc5" => PlcType.Plc5,
"omron-njnx" => PlcType.Omron,
_ => PlcType.ControlLogix,
};
}
/// <summary>
/// Default <see cref="IAbCipTagFactory"/> — creates a fresh <see cref="LibplctagTagRuntime"/>
/// per call. Stateless; safe to share across devices.
/// </summary>
internal sealed class LibplctagTagFactory : IAbCipTagFactory
{
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) =>
new LibplctagTagRuntime(createParams);
}