using System.Reflection; using libplctag; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Default libplctag-backed . Wraps a /// instance + translates our enum into the /// GetInt32 / GetFloat32 / GetString / GetBit calls libplctag /// exposes. One runtime instance per (device, tag path); lifetime is owned by the /// driver's per-device state dict. /// 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 connection_size as a public Tag // property, so we reach into the internal NativeTagWrapper's // SetIntAttribute (mirroring libplctag's plc_tag_set_int_attribute). // libplctag native parses connection_size 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); /// /// Best-effort propagation of connection_size to libplctag native. Reflects into /// the wrapper's internal NativeTagWrapper.SetIntAttribute(string, int); isolated /// in a static helper so the lookup costs run once + the failure path is one line. /// 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. } } /// /// PR abcip-3.2 — best-effort propagation of CIP logical-segment / instance-ID /// addressing to libplctag native. Two attributes are forwarded: /// /// use_connected_msg=1 — instance-ID addressing only works over a /// connected CIP session; switch the tag to use Forward Open + Class3 messaging. /// cip_addr=0x6B,N — replace the ASCII Symbol Object lookup with a /// direct logical segment reference, where N is the resolved instance ID /// from the driver's one-time @tags walk. /// /// Same reflection-via-NativeTagWrapper.SetAttributeString shape as /// — the 1.5.x .NET wrapper does not expose a /// public knob, so we degrade gracefully when the internal API is not present. /// 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, }; } /// /// Default — creates a fresh /// per call. Stateless; safe to share across devices. /// internal sealed class LibplctagTagFactory : IAbCipTagFactory { public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) => new LibplctagTagRuntime(createParams); }