test(mqtt): Sparkplug array metrics in the simulator + live gate (lmxopcua#770)
The simulator now packs Int32/Boolean/Float/Double/String arrays per the Sparkplug B spec (independent of the driver codec) and seeds Profile, Flags and Curve in the standalone catalog. SparkplugLiveTests gains a live test that asserts typed int[]/bool[]/float[] values from the birth, a changed vector (including a different Boolean length) from alias-only DATA, the read path, and BadTypeMismatch for an array metric bound without isArray. Run against a throwaway local Mosquitto; mutation-checked (scalar binding / seed removed). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+6
-1
@@ -75,7 +75,12 @@ foreach (var node in nodes)
|
||||
new SparkplugMetricSeed("Pressure", 2, DataType.Double, 101.325d),
|
||||
new SparkplugMetricSeed("Count", 3, DataType.Int32, -42),
|
||||
new SparkplugMetricSeed("Running", 4, DataType.Boolean, true),
|
||||
new SparkplugMetricSeed("Serial", 5, DataType.String, $"{node}-001"));
|
||||
new SparkplugMetricSeed("Serial", 5, DataType.String, $"{node}-001"),
|
||||
|
||||
// Array metrics (lmxopcua#770): bind with "isArray": true.
|
||||
new SparkplugMetricSeed("Profile", 6, DataType.Int32Array, new[] { 1, -2, 300000 }),
|
||||
new SparkplugMetricSeed("Flags", 7, DataType.BooleanArray, new[] { true, false, true, true, false, false, true, false, true }),
|
||||
new SparkplugMetricSeed("Curve", 8, DataType.FloatArray, new[] { 0.5f, 1.25f, -2.5f }));
|
||||
|
||||
edge.SetDeviceMetrics(
|
||||
"Filler1",
|
||||
|
||||
+84
@@ -757,6 +757,17 @@ public sealed class SparkplugEdgeNode : IAsyncDisposable
|
||||
metric.BytesValue = ByteString.CopyFrom((byte[])value);
|
||||
break;
|
||||
|
||||
// Array metrics (lmxopcua#770, driver side #754): the whole vector rides packed in bytes_value.
|
||||
// Packed HERE, from the spec text, not through the driver's codec — an encoder that shared
|
||||
// the decoder's assumptions could not falsify them.
|
||||
case DataType.Int32Array:
|
||||
case DataType.BooleanArray:
|
||||
case DataType.FloatArray:
|
||||
case DataType.DoubleArray:
|
||||
case DataType.StringArray:
|
||||
metric.BytesValue = ByteString.CopyFrom(PackArray(dataType, value));
|
||||
break;
|
||||
|
||||
// Structured values (Gitea #515). The seed carries the already-built protobuf message
|
||||
// rather than a scalar, because a Template's shape IS its value — there is nothing simpler
|
||||
// to convert from, and inventing a mini-DSL here would put a second, divergent notion of
|
||||
@@ -775,6 +786,74 @@ public sealed class SparkplugEdgeNode : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs an array per Sparkplug B 3.0 "Datatype details": fixed-width elements little-endian, no
|
||||
/// header; BooleanArray = uint32 LE count then bits MSB-first (last byte zero padded);
|
||||
/// StringArray = each UTF-8 string NUL-terminated.
|
||||
/// </summary>
|
||||
private static byte[] PackArray(DataType dataType, object value)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
switch (dataType)
|
||||
{
|
||||
case DataType.Int32Array:
|
||||
foreach (var v in (int[])value)
|
||||
{
|
||||
ms.Write(BitConverter.GetBytes(v));
|
||||
}
|
||||
|
||||
break;
|
||||
case DataType.FloatArray:
|
||||
foreach (var v in (float[])value)
|
||||
{
|
||||
ms.Write(BitConverter.GetBytes(v));
|
||||
}
|
||||
|
||||
break;
|
||||
case DataType.DoubleArray:
|
||||
foreach (var v in (double[])value)
|
||||
{
|
||||
ms.Write(BitConverter.GetBytes(v));
|
||||
}
|
||||
|
||||
break;
|
||||
case DataType.BooleanArray:
|
||||
{
|
||||
var bools = (bool[])value;
|
||||
ms.Write(BitConverter.GetBytes((uint)bools.Length));
|
||||
var bits = new byte[(bools.Length + 7) / 8];
|
||||
for (var i = 0; i < bools.Length; i++)
|
||||
{
|
||||
if (bools[i])
|
||||
{
|
||||
bits[i >> 3] |= (byte)(0x80 >> (i & 7));
|
||||
}
|
||||
}
|
||||
|
||||
ms.Write(bits);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.StringArray:
|
||||
foreach (var v in (string[])value)
|
||||
{
|
||||
ms.Write(System.Text.Encoding.UTF8.GetBytes(v));
|
||||
ms.WriteByte(0);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Cannot pack {dataType}.");
|
||||
}
|
||||
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
throw new PlatformNotSupportedException("The simulator packs arrays with host byte order = little-endian.");
|
||||
}
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private Payload NewSequencedPayload()
|
||||
{
|
||||
var payload = new Payload { Timestamp = NowMs(), Seq = NextSeq() };
|
||||
@@ -837,6 +916,11 @@ public sealed class SparkplugEdgeNode : IAsyncDisposable
|
||||
DataType.Boolean => tick % 2 == 0,
|
||||
DataType.String or DataType.Text or DataType.Uuid => $"tick-{tick}",
|
||||
|
||||
// Arrays move too (a fixed length, so a tag declaring arrayLength stays valid).
|
||||
DataType.Int32Array => new[] { tick, -tick, tick * 2 },
|
||||
DataType.BooleanArray => new[] { tick % 2 == 0, true, false, tick % 2 != 0, true },
|
||||
DataType.FloatArray => new[] { tick + 0.5f, tick + 1.5f },
|
||||
|
||||
// Structured values move too, or the live gate could not tell a flattened member that is being
|
||||
// fed from one that was merely published once at birth and then went quiet.
|
||||
DataType.Template => AdvanceTemplate((Payload.Types.Template)seed.Value!, tick),
|
||||
|
||||
Reference in New Issue
Block a user