feat(twincat): native ADS notifications for array tags, decoded from raw bytes
An array tag subscribed natively used to register only its declared scalar (publishing element 0 at Good) and was then refused wholesale with BadTypeMismatch. The driver now hands the authored ArrayLength to the client, which registers the whole array as raw bytes and decodes every notification. - TwinCATTypeGuard.CheckArray plans the layout from the RESOLVED array type: element checked like a scalar notification (width / float kind / string kind); STRING(n)/WSTRING(n) elements stride n+1 / 2(n+1); multi-dim arrays flattened in PLC memory order; an authored ArrayLength shorter than the PLC array reads the leading sub-range, a longer one is refused; struct / pointer / nested-array elements, non-array symbols and VAR_IN_OUT refs are refused. - CheckNotificationSize caps a native array at 128 KiB (largest registration proven on a live TC 2.11 target); above it the tag is refused BadOutOfRange. - TwinCATArrayCodec decodes the bytes into the element-typed CLR array. - The poll read uses the same plan + codec instead of the Beckhoff typed array read, which failed live for most real arrays (exact CLR element type, rank-N for multi-dim, member arrays sliced by the under-reported TC2 symbol size). - TwinCATNotificationRefusedException carries the refusal's status; the type mismatch derives from it and the driver isolates either per tag.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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>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 or TwinCATDataType.Time or TwinCATDataType.Date
|
||||
or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay:
|
||||
{
|
||||
// TIME / DATE / DT / TOD publish their raw 32-bit count — the same CLR shape (uint) a
|
||||
// scalar of those types publishes.
|
||||
var a = new uint[n];
|
||||
for (var i = 0; i < n; i++) a[i] = 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user