using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// /// Translates gateway-side instances into the boxed CLR objects /// DataValueSnapshot.Value carries. Mirrors the seven Galaxy data types in /// DataTypeMap (Boolean, Int32, Int64, Float32, Float64, String, DateTime), plus /// the array variants exposed by . Unknown / awkward values fall /// back to the raw_value bytes so a forward-compatible MXAccess deployment /// doesn't lose data on the wire — the consumer can opt to deserialise the bytes. /// /// /// ⚠️ Every method here must be total — it must never throw. The only caller is /// 's dispatch loop, and an exception escaping into that loop /// terminates it permanently: the pump keeps reading the gateway stream while nothing /// is ever dispatched again. So a malformed payload returns false from /// rather than raising, and every bound/shape check below is a /// guard rather than an assertion. /// internal static class MxValueDecoder { /// Decodes a gateway MxValue into a boxed CLR object. /// The MxValue to decode, or null. /// /// The decoded boxed CLR value, or null when is null, carries a /// null MxValue, or cannot be represented. Prefer when the caller /// needs to tell a genuine null apart from an unrepresentable value. /// public static object? Decode(MxValue? value) { TryDecode(value, out var decoded); return decoded; } /// /// Decodes a gateway MxValue, reporting whether it was representable at all. /// /// The MxValue to decode, or null. /// /// The decoded boxed CLR value. Null both for a genuine null and for a value this driver /// cannot represent — alone cannot distinguish the two. /// /// /// true when the value was decoded, including the genuine-null cases (a null /// reference or ). false when the payload carries a /// kind this driver has no CLR representation for — a null published under a /// Good quality would otherwise be indistinguishable from a real null, which is /// how a new wire type would go silently missing. /// public static bool TryDecode(MxValue? value, out object? decoded) { decoded = null; if (value is null) return true; if (value.IsNull) return true; switch (value.KindCase) { case MxValue.KindOneofCase.BoolValue: decoded = value.BoolValue; return true; case MxValue.KindOneofCase.Int32Value: decoded = value.Int32Value; return true; case MxValue.KindOneofCase.Int64Value: decoded = value.Int64Value; return true; case MxValue.KindOneofCase.FloatValue: decoded = value.FloatValue; return true; case MxValue.KindOneofCase.DoubleValue: decoded = value.DoubleValue; return true; case MxValue.KindOneofCase.StringValue: decoded = value.StringValue; return true; case MxValue.KindOneofCase.TimestampValue: decoded = DecodeTimestamp(value.TimestampValue); return true; case MxValue.KindOneofCase.RawValue: decoded = value.RawValue.ToByteArray(); return true; case MxValue.KindOneofCase.ArrayValue: return TryDecodeArray(value.ArrayValue, out decoded); case MxValue.KindOneofCase.SparseArrayValue: return TryDecodeSparseArray(value.SparseArrayValue, out decoded); default: return false; } } private static DateTime? DecodeTimestamp(Timestamp? ts) => ts?.ToDateTime(); private static bool TryDecodeArray(MxArray? array, out object? decoded) { decoded = null; if (array is null) return false; switch (array.ValuesCase) { case MxArray.ValuesOneofCase.BoolValues: decoded = array.BoolValues.Values.ToArray(); return true; case MxArray.ValuesOneofCase.Int32Values: decoded = array.Int32Values.Values.ToArray(); return true; case MxArray.ValuesOneofCase.Int64Values: decoded = array.Int64Values.Values.ToArray(); return true; case MxArray.ValuesOneofCase.FloatValues: decoded = array.FloatValues.Values.ToArray(); return true; case MxArray.ValuesOneofCase.DoubleValues: decoded = array.DoubleValues.Values.ToArray(); return true; case MxArray.ValuesOneofCase.StringValues: decoded = array.StringValues.Values.ToArray(); return true; case MxArray.ValuesOneofCase.TimestampValues: decoded = array.TimestampValues.Values.Select(t => t.ToDateTime()).ToArray(); return true; default: return false; } } /// /// Expands a sparse array into the dense CLR array it denotes: every index the payload /// names carries its value, and every index it does not carries the element type's /// default. /// /// /// /// ⚠️ Per the gateway contract this cannot arrive inbound. MxSparseArray is /// documented write-only — the gateway expands it into a full MxArray before /// forwarding, and "the worker never receives or produces it". This branch therefore /// exists as contract defence, not as an expected path: if a future gateway ever emits /// one, the alternative is a silent null published under the event's own (likely Good) /// quality, which loses the reading with nothing to show for it. /// /// /// The fill semantics deliberately mirror the gateway's own SparseArrayExpander /// rather than inventing a reading: unmentioned indices are reset to the element /// default, not preserved (the gateway cannot read-modify-write without racing the /// provider), Time defaults to the Unix epoch because the expander fills /// Timestamp{0,0}, and Integer widens to long[] if any element /// carries an int64 — matching the expander's own Int32/Int64 sub-array choice. /// /// /// Malformed payloads the gateway would have rejected outright (zero/oversized length, /// out-of-range index) are tolerated here rather than thrown, because this side is a /// consumer: an out-of-range index is skipped, a duplicate index is last-wins. /// /// private static bool TryDecodeSparseArray(MxSparseArray? sparse, out object? decoded) { decoded = null; if (sparse is null) return false; if (sparse.TotalLength == 0 || sparse.TotalLength > (uint)Array.MaxLength) return false; var length = (int)sparse.TotalLength; var elements = sparse.Elements; switch (sparse.ElementDataType) { case MxDataType.Boolean: return Fill(new bool[length], elements, v => v.BoolValue, out decoded); // The expander picks its Integer sub-array width from the element kinds, since a // sparse payload carries no CLR array to infer it from. Mirror that exactly, or a // round-trip through the gateway would change the published CLR element type. case MxDataType.Integer when UsesInt64(elements): return Fill(new long[length], elements, ReadInt64, out decoded); case MxDataType.Integer: return Fill(new int[length], elements, v => v.Int32Value, out decoded); case MxDataType.Float: return Fill(new float[length], elements, v => v.FloatValue, out decoded); case MxDataType.Double: return Fill(new double[length], elements, v => v.DoubleValue, out decoded); case MxDataType.String: var strings = new string[length]; Array.Fill(strings, string.Empty); return Fill(strings, elements, v => v.StringValue ?? string.Empty, out decoded); case MxDataType.Time: var times = new DateTime[length]; Array.Fill(times, DateTime.UnixEpoch); return Fill(times, elements, v => v.TimestampValue?.ToDateTime() ?? DateTime.UnixEpoch, out decoded); default: // Structs, enums, references and the rest are not scalar element types the // expander supports either — there is nothing meaningful to build. return false; } } private static bool Fill( T[] target, IReadOnlyList elements, Func read, out object? decoded) { foreach (var element in elements) { if (element.Index >= (uint)target.Length) continue; // out of range — skip, never throw if (element.Value is null) continue; // leave the default in place target[(int)element.Index] = read(element.Value); } decoded = target; return true; } private static bool UsesInt64(IReadOnlyList elements) { foreach (var element in elements) { if (element.Value?.KindCase == MxValue.KindOneofCase.Int64Value) return true; } return false; } private static long ReadInt64(MxValue value) => value.KindCase switch { MxValue.KindOneofCase.Int64Value => value.Int64Value, MxValue.KindOneofCase.Int32Value => value.Int32Value, _ => 0L, }; }