using libplctag; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// /// Default libplctag-backed . Uses ab_pccc 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. /// internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime { private readonly Tag _tag; /// /// 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 /// SetString 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 BadOutOfRange rather than a corrupted PLC value. /// 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, }; } 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."); } } 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); }