7cc8415310
U10 (user decision: implement array encode now): config, the AdminUI and discovery all advertised array tags Operate-writable, but both runtimes encoded a scalar - an int[] threw InvalidCastException (BadTypeMismatch AND Degraded), a scalar wrote element 0 natively, and libplctag re-sent its whole last-read buffer with element 0 patched (stale elements 1..n-1). - AbCipArrayCodec encodes count elements little-endian at the element width, CONVERTING each element (ScadaBridge writes int[] to INT arrays; overflow is BadOutOfRange, unconvertible BadTypeMismatch, both before anything is sent). - The driver checks the shape before any runtime exists: not a list -> BadTypeMismatch, length != elementCount -> BadOutOfRange, BOOL/STRING/ structure element types -> BadNotSupported; and, before sending, a controller buffer that is not width x count -> BadTypeMismatch. Refusals warn once and write no health. - Native: ONE Write Tag of count elements with the controller's type code (the client fragments a large one); libplctag: the codec's bytes copied into the elem_count-sized buffer, refused TooSmall if it does not fit. - IAbCipTagRuntime.EncodeArray (default: NotSupportedException). Real-controller proof is a hardware userGate (see the lane report recipe). Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
81 lines
4.7 KiB
C#
81 lines
4.7 KiB
C#
using System.Buffers.Binary;
|
||
using System.Collections;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||
|
||
/// <summary>
|
||
/// Encodes a whole-array write (review 07 U10): <c>count</c> elements at the element type's wire width,
|
||
/// little-endian, exactly the layout a Logix atomic array holds and the read path decodes. Shared by both
|
||
/// runtimes so the bytes on the wire are transport-independent.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para><b>Element values CONVERT, they are not exact-matched.</b> ScadaBridge (and any OPC UA client that
|
||
/// round-trips what it read) writes CLR lists such as <c>int[]</c> to an INT array — the read path itself
|
||
/// publishes INT/SINT elements as <c>int</c> — so each element goes through the same <see cref="Convert"/> the
|
||
/// scalar path uses. A value that does not fit is an <see cref="OverflowException"/> (BadOutOfRange) and one that
|
||
/// cannot convert a <see cref="FormatException"/> / <see cref="InvalidCastException"/> (BadTypeMismatch), all
|
||
/// thrown before any byte is sent.</para>
|
||
/// <para>BOOL arrays (bit-packed DWORDs — a byte-per-element encode would clobber neighbour bits), STRING arrays
|
||
/// (structures) and structure arrays are not encodable here; the driver refuses them <c>BadNotSupported</c>
|
||
/// before a runtime exists.</para>
|
||
/// </remarks>
|
||
internal static class AbCipArrayCodec
|
||
{
|
||
/// <summary>Wire width of one element of an encodable array type, or <c>null</c> when the type is not encodable.</summary>
|
||
/// <param name="type">The element type.</param>
|
||
/// <returns>The element width in bytes, or <c>null</c>.</returns>
|
||
public static int? ElementWidth(AbCipDataType type) => type switch
|
||
{
|
||
AbCipDataType.SInt or AbCipDataType.USInt => 1,
|
||
AbCipDataType.Int or AbCipDataType.UInt => 2,
|
||
AbCipDataType.DInt or AbCipDataType.UDInt or AbCipDataType.Real or AbCipDataType.Dt => 4,
|
||
AbCipDataType.LInt or AbCipDataType.ULInt or AbCipDataType.LReal => 8,
|
||
_ => null,
|
||
};
|
||
|
||
/// <summary>
|
||
/// The elements of an array write value, or <c>null</c> when the value is not a list (a scalar, a string, null).
|
||
/// Any non-string <see cref="IEnumerable"/> is accepted — <c>int[]</c>, <c>short[]</c>, <c>List<object></c> …
|
||
/// </summary>
|
||
/// <param name="value">The client's write value.</param>
|
||
/// <returns>The elements, or <c>null</c>.</returns>
|
||
public static object?[]? Elements(object? value) => value switch
|
||
{
|
||
null or string => null,
|
||
IEnumerable list => [.. list.Cast<object?>()],
|
||
_ => null,
|
||
};
|
||
|
||
/// <summary>Encodes <paramref name="values"/> as <paramref name="type"/> elements.</summary>
|
||
/// <param name="type">The element type (must have an <see cref="ElementWidth"/>).</param>
|
||
/// <param name="values">The element values; their count is the element count.</param>
|
||
/// <returns>The little-endian wire bytes, <c>values.Count × ElementWidth(type)</c> long.</returns>
|
||
/// <exception cref="NotSupportedException">The type is not encodable.</exception>
|
||
public static byte[] Encode(AbCipDataType type, IReadOnlyList<object?> values)
|
||
{
|
||
var width = ElementWidth(type)
|
||
?? throw new NotSupportedException($"Array writes of {type} elements are not supported.");
|
||
var buffer = new byte[values.Count * width];
|
||
for (var i = 0; i < values.Count; i++)
|
||
{
|
||
var v = values[i];
|
||
var span = buffer.AsSpan(i * width, width);
|
||
switch (type)
|
||
{
|
||
case AbCipDataType.SInt: span[0] = unchecked((byte)Convert.ToSByte(v)); break;
|
||
case AbCipDataType.USInt: span[0] = Convert.ToByte(v); break;
|
||
case AbCipDataType.Int: BinaryPrimitives.WriteInt16LittleEndian(span, Convert.ToInt16(v)); break;
|
||
case AbCipDataType.UInt: BinaryPrimitives.WriteUInt16LittleEndian(span, Convert.ToUInt16(v)); break;
|
||
case AbCipDataType.DInt:
|
||
case AbCipDataType.Dt: BinaryPrimitives.WriteInt32LittleEndian(span, Convert.ToInt32(v)); break;
|
||
case AbCipDataType.UDInt: BinaryPrimitives.WriteUInt32LittleEndian(span, Convert.ToUInt32(v)); break;
|
||
case AbCipDataType.LInt: BinaryPrimitives.WriteInt64LittleEndian(span, Convert.ToInt64(v)); break;
|
||
case AbCipDataType.ULInt: BinaryPrimitives.WriteUInt64LittleEndian(span, Convert.ToUInt64(v)); break;
|
||
case AbCipDataType.Real: BinaryPrimitives.WriteSingleLittleEndian(span, Convert.ToSingle(v)); break;
|
||
case AbCipDataType.LReal: BinaryPrimitives.WriteDoubleLittleEndian(span, Convert.ToDouble(v)); break;
|
||
}
|
||
}
|
||
return buffer;
|
||
}
|
||
}
|