using Google.Protobuf;
using Org.Eclipse.Tahu.Protobuf;
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;
///
/// Builds the two Sparkplug-B wire vectors committed under Golden/ — an NBIRTH and the
/// NDATA that follows it — and names the files they live in.
///
///
///
/// Why a generator instead of bytes captured off a real edge node. A capture would pin
/// one vendor's encoder rather than the schema, could not be regenerated when the vendored
/// proto is re-vendored, and would drag an unaudited third-party payload into the repo. These
/// payloads are hand-built from the generated types, serialized once, and committed; the
/// SparkplugGoldenVectorTests drift guard re-runs this builder on every test run and
/// fails if the committed bytes no longer match, which is what makes the files an actual
/// regression pin rather than decoration.
///
///
/// Every field in here is load-bearing. The NBIRTH carries seq = 0 (present and
/// zero — the case a Seq != 0 presence check gets wrong), a negative Int32 encoded the
/// Sparkplug way (two's complement in an unsigned int_value), an explicit
/// is_null metric, and a DataSet metric that v1 does not support. The NDATA
/// carries metrics with no name and no datatype — alias only — which is how every real
/// Sparkplug DATA message after a birth looks, and the case an "absent means empty string"
/// decoder gets wrong. Do not "tidy" these payloads.
///
///
internal static class SparkplugGoldenPayloads
{
/// Repo-relative directory the committed vectors live in, inside the test project.
public const string Directory = "Golden";
/// File name of the NBIRTH vector.
public const string NbirthFile = "nbirth.bin";
/// File name of the NDATA vector.
public const string NdataFile = "ndata.bin";
/// NBIRTH payload timestamp — 2024-07-24T12:00:00Z, in Sparkplug's epoch milliseconds.
public const ulong NbirthTimestampMs = 1721822400000UL;
/// NDATA payload timestamp — five seconds after the birth.
public const ulong NdataTimestampMs = 1721822405000UL;
/// The alias the NBIRTH binds to Temperature, reused by the NDATA.
public const ulong TemperatureAlias = 5UL;
/// The alias the NBIRTH binds to Count — the negative-Int32 case.
public const ulong CountAlias = 9UL;
/// The alias the NBIRTH binds to Missing — the explicit-null case.
public const ulong MissingAlias = 11UL;
/// The Count metric's value in the NBIRTH — negative, to pin the wire encoding.
public const int CountBirthValue = -42;
/// The Count metric's value in the NDATA.
public const int CountDataValue = -43;
///
/// Builds the NBIRTH: seq = 0, a full metric catalog with names, aliases and datatypes.
///
/// The payload; serialize with .
public static Payload BuildNbirth()
{
var payload = new Payload
{
Timestamp = NbirthTimestampMs,
// Present AND zero. Sparkplug REQUIRES an NBIRTH to carry seq = 0, so any decoder that
// infers absence from a zero value reports "no sequence" for every birth it ever sees.
Seq = 0UL,
};
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "bdSeq",
Datatype = (uint)DataType.Uint64,
LongValue = 7UL,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Node Control/Rebirth",
Datatype = (uint)DataType.Boolean,
BooleanValue = false,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Temperature",
Alias = TemperatureAlias,
Timestamp = NbirthTimestampMs,
Datatype = (uint)DataType.Float,
FloatValue = 21.5f,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Pressure",
Alias = 6UL,
Datatype = (uint)DataType.Double,
DoubleValue = 101.325d,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Running",
Alias = 7UL,
Datatype = (uint)DataType.Boolean,
BooleanValue = true,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Serial",
Alias = 8UL,
Datatype = (uint)DataType.String,
StringValue = "EDGE-A-001",
});
// Sparkplug carries every signed integer up to 32 bits in the UNSIGNED int_value field as
// two's complement, so -42 goes on the wire as 4294967254. A decoder that hands the raw
// uint32 straight to a consumer publishes 4294967254 for a tag whose value is -42.
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Count",
Alias = CountAlias,
Datatype = (uint)DataType.Int32,
IntValue = unchecked((uint)CountBirthValue),
});
// DataSet is explicitly out of scope for v1 — present here so "unsupported" is a decoded,
// testable outcome rather than an exception on the MQTT dispatcher thread.
var dataSet = new Payload.Types.DataSet { NumOfColumns = 1UL };
dataSet.Columns.Add("col0");
dataSet.Types_.Add((uint)DataType.Int32);
var row = new Payload.Types.DataSet.Types.Row();
row.Elements.Add(new Payload.Types.DataSet.Types.DataSetValue { IntValue = 1U });
dataSet.Rows.Add(row);
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Config",
Alias = 10UL,
Datatype = (uint)DataType.DataSet,
DatasetValue = dataSet,
});
// is_null = true with NO value in the oneof: "this metric exists and its value is null",
// which is a different fact from "this metric carried no value field".
payload.Metrics.Add(new Payload.Types.Metric
{
Name = "Missing",
Alias = MissingAlias,
Datatype = (uint)DataType.Float,
IsNull = true,
});
return payload;
}
///
/// Builds the NDATA that follows : seq = 1 and metrics carrying
/// alias only — no name, no datatype — exactly as a real edge node publishes them.
///
/// The payload; serialize with .
public static Payload BuildNdata()
{
var payload = new Payload
{
Timestamp = NdataTimestampMs,
Seq = 1UL,
};
payload.Metrics.Add(new Payload.Types.Metric
{
Alias = TemperatureAlias,
Timestamp = NdataTimestampMs,
FloatValue = 22.25f,
});
payload.Metrics.Add(new Payload.Types.Metric
{
Alias = CountAlias,
IntValue = unchecked((uint)CountDataValue),
});
payload.Metrics.Add(new Payload.Types.Metric
{
Alias = MissingAlias,
IsNull = true,
});
return payload;
}
/// Reads a committed vector from the test binary's output directory.
/// or .
/// The committed wire bytes.
public static byte[] Read(string fileName) => File.ReadAllBytes(PathTo(fileName));
/// Resolves a vector's path under the test binary's output directory.
/// or .
/// The absolute path.
///
/// Rooted at rather than the process working directory:
/// the runner's cwd is not guaranteed to be the output folder, and a relative read that
/// happens to work under one runner is exactly how a fixture stops being copied without
/// anyone noticing.
///
public static string PathTo(string fileName) =>
Path.Combine(AppContext.BaseDirectory, Directory, fileName);
}