dabd8eb955
MAJOR-1: the SDK skips TypeInfo.IsInstanceOfDataType on any node with an OnWriteValue handler (every writable equipment node), and the node manager's fail-fast treated numeric<->numeric as compatible, so a UInt32[] to a Galaxy Int32 array matched MxValueEncoder's int[] arm by CLR covariance and was written bit-reinterpreted with a Good result. EvaluateEquipmentWriteStructure now matches an ARRAY write's element type exactly (Byte[]/ByteString the only equivalence; Variant[]/object[] refused) -> BadTypeMismatch, nothing dispatched. MxValueEncoder's int[]/long[] arms are exact-type guarded too, and the Galaxy parity test's false "the SDK refuses" premise is corrected (the UInt32/UInt64 exclusion is now the encoder's own answer). Scalar writes keep numeric widening by decision: consumers write CLR-typed values (Double to a Float node) and every driver's scalar encoder range-checks. MINOR: Modbus row honours maxRegistersPerWrite (smallest of DriverConfig / the tag's DeviceConfig, default 123) and FC15's 1968-coil ceiling, and requires region + numeric address; Modbus/S7/Galaxy require isArray + integer arrayLength >= 1 (Galaxy: an unbounded node would let a short write resize the attribute); an array value to a scalar node is refused inline. Docs: Uns.md array-write rules + upgrade note (ReadWrite array tags become client-writable on first boot), Modbus.md, Galaxy.md. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
95 lines
5.4 KiB
C#
95 lines
5.4 KiB
C#
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
/// <summary>
|
|
/// Translates boxed CLR values from <c>WriteRequest.Value</c> into gateway-side
|
|
/// <see cref="MxValue"/> instances. Inverse of <see cref="MxValueDecoder"/>.
|
|
/// Handles the seven Galaxy data types — Boolean, Int32, Int64, Float32, Float64,
|
|
/// String, DateTime — and their array variants. Null + unsupported types throw
|
|
/// <see cref="ArgumentException"/> so the IWritable caller can fail the write with a
|
|
/// clear status code rather than silently mis-typing the wire payload.
|
|
/// </summary>
|
|
internal static class MxValueEncoder
|
|
{
|
|
/// <summary>Encodes a CLR value as an MxValue for transmission to the gateway.</summary>
|
|
/// <param name="value">The value to encode, or null for a null MxValue.</param>
|
|
/// <returns>An MxValue instance representing the encoded value.</returns>
|
|
/// <exception cref="ArgumentException">Thrown if the value type is not supported.</exception>
|
|
public static MxValue Encode(object? value)
|
|
{
|
|
if (value is null) return new MxValue { IsNull = true };
|
|
|
|
switch (value)
|
|
{
|
|
case bool b: return new MxValue { BoolValue = b };
|
|
case sbyte i8: return new MxValue { Int32Value = i8 };
|
|
case short i16: return new MxValue { Int32Value = i16 };
|
|
case int i32: return new MxValue { Int32Value = i32 };
|
|
case byte u8: return new MxValue { Int32Value = u8 };
|
|
case ushort u16: return new MxValue { Int32Value = u16 };
|
|
case uint u32 when u32 <= int.MaxValue: return new MxValue { Int32Value = (int)u32 };
|
|
case long i64: return new MxValue { Int64Value = i64 };
|
|
case ulong u64 when u64 <= long.MaxValue: return new MxValue { Int64Value = (long)u64 };
|
|
case float f32: return new MxValue { FloatValue = f32 };
|
|
case double f64: return new MxValue { DoubleValue = f64 };
|
|
case string s: return new MxValue { StringValue = s };
|
|
case DateTime dt: return new MxValue { TimestampValue = Timestamp.FromDateTime(EnsureUtc(dt)) };
|
|
case DateTimeOffset dto: return new MxValue { TimestampValue = Timestamp.FromDateTimeOffset(dto) };
|
|
|
|
case bool[] arr: return EncodeArray(arr, (mx, vs) => mx.BoolValues = ToBoolArray(vs));
|
|
// Exact-type guards: CLR array covariance makes `uint[] is int[]` and `ulong[] is long[]` true, so
|
|
// without them an unsigned array would be encoded by REINTERPRETING its bits (uint.MaxValue → -1) —
|
|
// unlike the range-checked scalar arms above. An unsigned array is refused instead (Gitea #610 review).
|
|
case int[] arr when arr.GetType() == typeof(int[]):
|
|
return EncodeArray(arr, (mx, vs) => mx.Int32Values = ToInt32Array(vs));
|
|
case long[] arr when arr.GetType() == typeof(long[]):
|
|
return EncodeArray(arr, (mx, vs) => mx.Int64Values = ToInt64Array(vs));
|
|
case float[] arr: return EncodeArray(arr, (mx, vs) => mx.FloatValues = ToFloatArray(vs));
|
|
case double[] arr: return EncodeArray(arr, (mx, vs) => mx.DoubleValues = ToDoubleArray(vs));
|
|
case string[] arr: return EncodeArray(arr, (mx, vs) => mx.StringValues = ToStringArray(vs));
|
|
case DateTime[] arr: return EncodeArray(arr, (mx, vs) => mx.TimestampValues = ToTimestampArray(vs));
|
|
|
|
default:
|
|
throw new ArgumentException(
|
|
$"Cannot encode value of type {value.GetType()} as MxValue. Supported: " +
|
|
"bool, int / long (and their unsigned variants), float, double, string, DateTime, " +
|
|
"and their 1-D array variants.",
|
|
nameof(value));
|
|
}
|
|
}
|
|
|
|
private static MxValue EncodeArray<T>(T[] values, Action<MxArray, T[]> populate)
|
|
{
|
|
var array = new MxArray();
|
|
populate(array, values);
|
|
array.Dimensions.Add((uint)values.Length);
|
|
return new MxValue { ArrayValue = array };
|
|
}
|
|
|
|
private static BoolArray ToBoolArray(bool[] vs) { var a = new BoolArray(); a.Values.AddRange(vs); return a; }
|
|
private static Int32Array ToInt32Array(int[] vs) { var a = new Int32Array(); a.Values.AddRange(vs); return a; }
|
|
private static Int64Array ToInt64Array(long[] vs) { var a = new Int64Array(); a.Values.AddRange(vs); return a; }
|
|
private static FloatArray ToFloatArray(float[] vs) { var a = new FloatArray(); a.Values.AddRange(vs); return a; }
|
|
private static DoubleArray ToDoubleArray(double[] vs) { var a = new DoubleArray(); a.Values.AddRange(vs); return a; }
|
|
private static StringArray ToStringArray(string[] vs) { var a = new StringArray(); a.Values.AddRange(vs); return a; }
|
|
private static TimestampArray ToTimestampArray(DateTime[] vs)
|
|
{
|
|
var a = new TimestampArray();
|
|
foreach (var dt in vs) a.Values.Add(Timestamp.FromDateTime(EnsureUtc(dt)));
|
|
return a;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="Timestamp.FromDateTime"/> requires UTC. Convert non-UTC inputs
|
|
/// explicitly so a caller passing local time gets predictable wire bytes.
|
|
/// </summary>
|
|
private static DateTime EnsureUtc(DateTime dt) => dt.Kind switch
|
|
{
|
|
DateTimeKind.Utc => dt,
|
|
DateTimeKind.Local => dt.ToUniversalTime(),
|
|
_ => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
|
|
};
|
|
}
|