f17ba8e258
Array tags were advertised writable but every write threw InvalidCastException in ConvertForWrite (reported BadTypeMismatch); STRING writes went through the Beckhoff marshaller with no length check against STRING(n) and the host's ANSI code page, which the read side deliberately avoids. - Array write: TwinCATArrayCodec.ToWriteElements converts exactly ArrayLength elements to the declared type (CLR lists convert - ScadaBridge writes int[] to Int16 nodes; a length mismatch or out-of-range element is BadOutOfRange, a scalar value BadTypeMismatch) before any wire traffic; the ADS client plans the layout from the resolved PLC array (TwinCATTypeGuard.CheckArray, as the read does), encodes with TwinCATArrayCodec.Encode - the exact inverse of Decode - and writes the raw bytes (new ITwinCATClient.WriteArrayAsync). - STRING / WSTRING write: encoded at the PLC's declared width by TwinCATStringCodec.EncodeString/EncodeWString (Windows-1252 or UTF-8 per target, NUL-padded, strict: an over-long value or unrepresentable character is OverflowException -> BadOutOfRange) and written as raw bytes. - TwinCAT CLI read/write gain --array-length (+ --separator) for the hardware gate. Refs #739 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
217 lines
12 KiB
C#
217 lines
12 KiB
C#
using System.Buffers.Binary;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||
|
||
/// <summary>
|
||
/// Decodes a PLC array's raw bytes — laid out by <see cref="TwinCATTypeGuard.CheckArray"/> — into the
|
||
/// element-typed CLR array an array tag publishes (<c>bool[]</c>, <c>short[]</c>, <c>float[]</c>,
|
||
/// <c>string[]</c>, …). Both the poll read and the native notification decode here, so the two paths
|
||
/// publish identical values, element type and length by construction.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// 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 (<c>ARRAY OF TIME</c> as <c>uint[]</c> threw), a
|
||
/// multi-dimensional array demanded a rank-N CLR array, and a dotted MEMBER array was sliced by the
|
||
/// under-reported symbol size (<c>ArgumentOutOfRangeException</c>). Elements are little-endian (every
|
||
/// TwinCAT target is x86/x64/ARM-LE); a STRING / WSTRING element is decoded by
|
||
/// <see cref="TwinCATStringCodec"/> within its fixed stride, so each ends at its own first NUL.
|
||
/// </remarks>
|
||
internal static class TwinCATArrayCodec
|
||
{
|
||
/// <summary>
|
||
/// Converts a client-supplied array value into exactly <paramref name="count"/> elements of the
|
||
/// declared type, each converted the way a scalar write converts (<see cref="AdsTwinCATClient.ConvertForWrite"/>
|
||
/// — so TIME/DATE elements become their encoded <c>uint</c>). Any CLR collection is accepted and the
|
||
/// element type is CONVERTED, not exact-matched: ScadaBridge writes an <c>int[]</c> to an Int16 node.
|
||
/// </summary>
|
||
/// <param name="declared">The tag's declared element type.</param>
|
||
/// <param name="value">The written value — an array or list.</param>
|
||
/// <param name="count">The tag's authored <c>ArrayLength</c>.</param>
|
||
/// <returns>The converted elements, in order.</returns>
|
||
/// <exception cref="InvalidCastException">The value is not a collection (a scalar, a string, null), or
|
||
/// an element cannot convert.</exception>
|
||
/// <exception cref="OverflowException">The element count differs from <paramref name="count"/>, or an
|
||
/// element is outside the declared type's range.</exception>
|
||
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<object>(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];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Encodes converted elements (<see cref="ToWriteElements"/>) into the PLC's raw bytes at the planned
|
||
/// layout — the exact inverse of <see cref="Decode"/>: little-endian at the element stride, a
|
||
/// STRING / WSTRING element encoded by <see cref="TwinCATStringCodec"/> within its fixed stride.
|
||
/// </summary>
|
||
/// <param name="declared">The tag's declared element type.</param>
|
||
/// <param name="elements">The converted elements; exactly <see cref="TwinCATArrayLayout.ElementCount"/>.</param>
|
||
/// <param name="layout">The layout planned from the PLC array (<see cref="TwinCATTypeGuard.CheckArray"/>).</param>
|
||
/// <param name="utf8">True when the target reports UTF-8 STRING data (else Windows-1252).</param>
|
||
/// <returns>Exactly <see cref="TwinCATArrayLayout.ByteLength"/> bytes.</returns>
|
||
/// <exception cref="ArgumentException">The element count differs from the layout.</exception>
|
||
/// <exception cref="OverflowException">A string element does not fit its stride.</exception>
|
||
public static byte[] Encode(TwinCATDataType declared, IReadOnlyList<object> 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;
|
||
}
|
||
|
||
/// <summary>Decodes <paramref name="layout"/>.ElementCount elements of <paramref name="declared"/>.</summary>
|
||
/// <param name="declared">The tag's declared element type.</param>
|
||
/// <param name="bytes">The PLC bytes from the array's first element (at least <see cref="TwinCATArrayLayout.ByteLength"/>).</param>
|
||
/// <param name="layout">The planned layout.</param>
|
||
/// <param name="utf8">True when the target reports UTF-8 STRING data (else Windows-1252).</param>
|
||
/// <returns>The element-typed CLR array.</returns>
|
||
/// <exception cref="ArgumentException">The buffer is shorter than the layout.</exception>
|
||
public static Array Decode(TwinCATDataType declared, ReadOnlySpan<byte> 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");
|
||
}
|
||
}
|
||
}
|