using System.Buffers.Binary; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; /// /// Decodes a PLC array's raw bytes — laid out by — into the /// element-typed CLR array an array tag publishes (bool[], short[], float[], /// string[], …). Both the poll read and the native notification decode here, so the two paths /// publish identical values, element type and length by construction. /// /// /// The driver reads arrays as raw bytes rather than through the Beckhoff typed array read, which a live /// TwinCAT 2.11 target showed failing for most real arrays (2026-10-07, Elma cleaner Z33856): the CLR /// element type had to match the PLC's exactly (ARRAY OF TIME as uint[] threw), a /// multi-dimensional array demanded a rank-N CLR array, and a dotted MEMBER array was sliced by the /// under-reported symbol size (ArgumentOutOfRangeException). Elements are little-endian (every /// TwinCAT target is x86/x64/ARM-LE); a STRING / WSTRING element is decoded by /// within its fixed stride, so each ends at its own first NUL. /// internal static class TwinCATArrayCodec { /// /// Converts a client-supplied array value into exactly elements of the /// declared type, each converted the way a scalar write converts ( /// — so TIME/DATE elements become their encoded uint). Any CLR collection is accepted and the /// element type is CONVERTED, not exact-matched: ScadaBridge writes an int[] to an Int16 node. /// /// The tag's declared element type. /// The written value — an array or list. /// The tag's authored ArrayLength. /// The converted elements, in order. /// The value is not a collection (a scalar, a string, null), or /// an element cannot convert. /// The element count differs from , or an /// element is outside the declared type's range. public static object[] ToWriteElements(TwinCATDataType declared, object? value, int count) { if (value is null or string || value is not System.Collections.IEnumerable items) throw new InvalidCastException( $"an array tag of {count} {declared} takes an array value, not {value?.GetType().Name ?? "null"}"); var elements = new List(count); foreach (var item in items) elements.Add(AdsTwinCATClient.ConvertForWrite(declared, item)); if (elements.Count != count) throw new OverflowException($"wrote {elements.Count} elements to an array tag of arrayLength {count}"); return [.. elements]; } /// /// Encodes converted elements () into the PLC's raw bytes at the planned /// layout — the exact inverse of : little-endian at the element stride, a /// STRING / WSTRING element encoded by within its fixed stride. /// /// The tag's declared element type. /// The converted elements; exactly . /// The layout planned from the PLC array (). /// True when the target reports UTF-8 STRING data (else Windows-1252). /// Exactly bytes. /// The element count differs from the layout. /// A string element does not fit its stride. public static byte[] Encode(TwinCATDataType declared, IReadOnlyList elements, TwinCATArrayLayout layout, bool utf8) { if (layout.Stride <= 0 || elements.Count != layout.ElementCount) throw new ArgumentException( $"{elements.Count} elements do not match a layout of {layout.ElementCount} × {layout.Stride} B", nameof(elements)); var bytes = new byte[layout.ByteLength]; var stride = layout.Stride; for (var i = 0; i < elements.Count; i++) { var slot = bytes.AsSpan(i * stride, stride); var e = elements[i]; switch (declared) { case TwinCATDataType.Bool: slot[0] = Convert.ToBoolean(e) ? (byte)1 : (byte)0; break; case TwinCATDataType.SInt: slot[0] = unchecked((byte)Convert.ToSByte(e)); break; case TwinCATDataType.USInt: slot[0] = Convert.ToByte(e); break; case TwinCATDataType.Int: BinaryPrimitives.WriteInt16LittleEndian(slot, Convert.ToInt16(e)); break; case TwinCATDataType.UInt: BinaryPrimitives.WriteUInt16LittleEndian(slot, Convert.ToUInt16(e)); break; case TwinCATDataType.DInt: BinaryPrimitives.WriteInt32LittleEndian(slot, Convert.ToInt32(e)); break; case TwinCATDataType.UDInt or TwinCATDataType.Time or TwinCATDataType.TimeOfDay or TwinCATDataType.Date or TwinCATDataType.DateTime: BinaryPrimitives.WriteUInt32LittleEndian(slot, Convert.ToUInt32(e)); break; case TwinCATDataType.LInt: BinaryPrimitives.WriteInt64LittleEndian(slot, Convert.ToInt64(e)); break; case TwinCATDataType.ULInt: BinaryPrimitives.WriteUInt64LittleEndian(slot, Convert.ToUInt64(e)); break; case TwinCATDataType.Real: BinaryPrimitives.WriteSingleLittleEndian(slot, Convert.ToSingle(e)); break; case TwinCATDataType.LReal: BinaryPrimitives.WriteDoubleLittleEndian(slot, Convert.ToDouble(e)); break; case TwinCATDataType.String: TwinCATStringCodec.EncodeString(Convert.ToString(e) ?? string.Empty, stride, utf8).CopyTo(slot); break; case TwinCATDataType.WString: TwinCATStringCodec.EncodeWString(Convert.ToString(e) ?? string.Empty, stride).CopyTo(slot); break; default: throw new NotSupportedException($"TwinCATDataType {declared} has no array element encoding"); } } return bytes; } /// Decodes .ElementCount elements of . /// The tag's declared element type. /// The PLC bytes from the array's first element (at least ). /// The planned layout. /// True when the target reports UTF-8 STRING data (else Windows-1252). /// The element-typed CLR array. /// The buffer is shorter than the layout. public static Array Decode(TwinCATDataType declared, ReadOnlySpan bytes, TwinCATArrayLayout layout, bool utf8) { if (layout.ElementCount < 0 || layout.Stride <= 0 || bytes.Length < layout.ByteLength) throw new ArgumentException( $"An array buffer of {bytes.Length} B cannot hold {layout.ElementCount} × {layout.Stride} B elements", nameof(bytes)); var n = layout.ElementCount; var stride = layout.Stride; switch (declared) { case TwinCATDataType.Bool: { var a = new bool[n]; for (var i = 0; i < n; i++) a[i] = bytes[i * stride] != 0; return a; } case TwinCATDataType.SInt: { var a = new sbyte[n]; for (var i = 0; i < n; i++) a[i] = unchecked((sbyte)bytes[i * stride]); return a; } case TwinCATDataType.USInt: { var a = new byte[n]; for (var i = 0; i < n; i++) a[i] = bytes[i * stride]; return a; } case TwinCATDataType.Int: { var a = new short[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt16LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.UInt: { var a = new ushort[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt16LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.DInt: { var a = new int[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt32LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.UDInt: { var a = new uint[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt32LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.Time or TwinCATDataType.TimeOfDay: { // TIME / TOD publish milliseconds as double — the same CLR shape a scalar publishes. var a = new double[n]; for (var i = 0; i < n; i++) a[i] = (double)TwinCATTemporalCodec.Decode(declared, BinaryPrimitives.ReadUInt32LittleEndian(bytes[(i * stride)..])); return a; } case TwinCATDataType.Date or TwinCATDataType.DateTime: { // DATE / DT publish a UTC DateTime, as a scalar does. var a = new DateTime[n]; for (var i = 0; i < n; i++) a[i] = (DateTime)TwinCATTemporalCodec.Decode(declared, BinaryPrimitives.ReadUInt32LittleEndian(bytes[(i * stride)..])); return a; } case TwinCATDataType.LInt: { var a = new long[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadInt64LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.ULInt: { var a = new ulong[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadUInt64LittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.Real: { var a = new float[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadSingleLittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.LReal: { var a = new double[n]; for (var i = 0; i < n; i++) a[i] = BinaryPrimitives.ReadDoubleLittleEndian(bytes[(i * stride)..]); return a; } case TwinCATDataType.String: { var a = new string[n]; for (var i = 0; i < n; i++) a[i] = TwinCATStringCodec.DecodeString(bytes.Slice(i * stride, stride), utf8); return a; } case TwinCATDataType.WString: { var a = new string[n]; for (var i = 0; i < n; i++) a[i] = TwinCATStringCodec.DecodeWString(bytes.Slice(i * stride, stride)); return a; } default: throw new NotSupportedException($"TwinCATDataType {declared} has no array element decoding"); } } }