Files
DD3-LoRa-Bridge-MultiSender/test/test_payload_codec/test_payload_codec.cpp
T
acidburns def09160d0 refactor lora payload timing
Bump the batch payload codec to schema v4 with separate meter-time and UTC anchors, then use meter seconds for sparse batch slotting and receiver reconstruction.

Update the current 868 MHz bench configuration, allow ACKs from configured receiver short IDs, improve AP-to-STA recovery, quiet the test build, and document the changed protocol in the README.
2026-06-30 12:19:27 +02:00

294 lines
10 KiB
C++

#include <Arduino.h>
#include <unity.h>
#include "dd3_legacy_core.h"
#include "payload_codec.h"
static constexpr uint8_t kMaxSamples = 30;
static void fill_sparse_batch(BatchInput &in) {
memset(&in, 0, sizeof(in));
in.sender_id = 1;
in.batch_id = 42;
in.meter_t_last = 123456789;
in.ts_utc_last = 1700000000;
in.present_mask = (1UL << 0) | (1UL << 2) | (1UL << 3) | (1UL << 10) | (1UL << 29);
in.n = 5;
in.battery_mV = 3750;
in.err_m = 2;
in.err_d = 1;
in.err_tx = 3;
in.err_last = 2;
in.err_rx_reject = 1;
in.energy_wh[0] = 100000;
in.energy_wh[1] = 100001;
in.energy_wh[2] = 100050;
in.energy_wh[3] = 100050;
in.energy_wh[4] = 100200;
in.p1_w[0] = -120;
in.p1_w[1] = -90;
in.p1_w[2] = 1910;
in.p1_w[3] = -90;
in.p1_w[4] = 500;
in.p2_w[0] = 50;
in.p2_w[1] = -1950;
in.p2_w[2] = 60;
in.p2_w[3] = 2060;
in.p2_w[4] = -10;
in.p3_w[0] = 0;
in.p3_w[1] = 10;
in.p3_w[2] = -1990;
in.p3_w[3] = 10;
in.p3_w[4] = 20;
}
static void fill_full_batch(BatchInput &in) {
memset(&in, 0, sizeof(in));
in.sender_id = 1;
in.batch_id = 0xBEEF;
in.meter_t_last = 1769904999 - 29;
in.ts_utc_last = 1769904999;
in.present_mask = 0x3FFFFFFFUL;
in.n = kMaxSamples;
in.battery_mV = 4095;
in.err_m = 10;
in.err_d = 20;
in.err_tx = 30;
in.err_last = 3;
in.err_rx_reject = 6;
for (uint8_t i = 0; i < kMaxSamples; ++i) {
in.energy_wh[i] = 500000UL + static_cast<uint32_t>(i) * static_cast<uint32_t>(i) * 3UL;
in.p1_w[i] = static_cast<int16_t>(-1000 + static_cast<int16_t>(i) * 25);
in.p2_w[i] = static_cast<int16_t>(500 - static_cast<int16_t>(i) * 30);
in.p3_w[i] = static_cast<int16_t>(((i % 2) == 0 ? 100 : -100) + static_cast<int16_t>(i) * 5);
}
}
static void assert_batch_equals(const BatchInput &expected, const BatchInput &actual) {
TEST_ASSERT_EQUAL_UINT16(expected.sender_id, actual.sender_id);
TEST_ASSERT_EQUAL_UINT16(expected.batch_id, actual.batch_id);
TEST_ASSERT_EQUAL_UINT32(expected.meter_t_last, actual.meter_t_last);
TEST_ASSERT_EQUAL_UINT32(expected.ts_utc_last, actual.ts_utc_last);
TEST_ASSERT_EQUAL_UINT32(expected.present_mask, actual.present_mask);
TEST_ASSERT_EQUAL_UINT8(expected.n, actual.n);
TEST_ASSERT_EQUAL_UINT16(expected.battery_mV, actual.battery_mV);
TEST_ASSERT_EQUAL_UINT8(expected.err_m, actual.err_m);
TEST_ASSERT_EQUAL_UINT8(expected.err_d, actual.err_d);
TEST_ASSERT_EQUAL_UINT8(expected.err_tx, actual.err_tx);
TEST_ASSERT_EQUAL_UINT8(expected.err_last, actual.err_last);
TEST_ASSERT_EQUAL_UINT8(expected.err_rx_reject, actual.err_rx_reject);
for (uint8_t i = 0; i < expected.n; ++i) {
TEST_ASSERT_EQUAL_UINT32(expected.energy_wh[i], actual.energy_wh[i]);
TEST_ASSERT_EQUAL_INT16(expected.p1_w[i], actual.p1_w[i]);
TEST_ASSERT_EQUAL_INT16(expected.p2_w[i], actual.p2_w[i]);
TEST_ASSERT_EQUAL_INT16(expected.p3_w[i], actual.p3_w[i]);
}
for (uint8_t i = expected.n; i < kMaxSamples; ++i) {
TEST_ASSERT_EQUAL_UINT32(0, actual.energy_wh[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p1_w[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p2_w[i]);
TEST_ASSERT_EQUAL_INT16(0, actual.p3_w[i]);
}
}
static void test_encode_decode_roundtrip_schema_v4() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
TEST_ASSERT_TRUE(encoded_len > 28);
BatchInput out = {};
TEST_ASSERT_TRUE(decode_batch(encoded, encoded_len, &out));
assert_batch_equals(in, out);
}
static void test_decode_rejects_bad_magic_schema_flags() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
uint8_t bad_magic[256] = {};
memcpy(bad_magic, encoded, encoded_len);
bad_magic[0] = 0x00;
TEST_ASSERT_FALSE(decode_batch(bad_magic, encoded_len, &out));
uint8_t bad_schema[256] = {};
memcpy(bad_schema, encoded, encoded_len);
bad_schema[2] = 0x02;
TEST_ASSERT_FALSE(decode_batch(bad_schema, encoded_len, &out));
uint8_t bad_flags[256] = {};
memcpy(bad_flags, encoded, encoded_len);
bad_flags[3] = 0x00;
TEST_ASSERT_FALSE(decode_batch(bad_flags, encoded_len, &out));
}
static void test_decode_rejects_truncated_and_length_mismatch() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(encoded, encoded_len - 1, &out));
TEST_ASSERT_FALSE(decode_batch(encoded, 12, &out));
uint8_t with_tail[257] = {};
memcpy(with_tail, encoded, encoded_len);
with_tail[encoded_len] = 0xAA;
TEST_ASSERT_FALSE(decode_batch(with_tail, encoded_len + 1, &out));
}
static void test_encode_and_decode_reject_invalid_present_mask() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
in.present_mask = 0x40000000UL;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
TEST_ASSERT_TRUE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
BatchInput out = {};
uint8_t invalid_bits[256] = {};
memcpy(invalid_bits, encoded, encoded_len);
invalid_bits[15] |= 0x40;
TEST_ASSERT_FALSE(decode_batch(invalid_bits, encoded_len, &out));
uint8_t bitcount_mismatch[256] = {};
memcpy(bitcount_mismatch, encoded, encoded_len);
bitcount_mismatch[16] = 0x01; // n=1 while mask has 5 bits set
TEST_ASSERT_FALSE(decode_batch(bitcount_mismatch, encoded_len, &out));
}
static void test_encode_rejects_invalid_n_and_regression_cases() {
BatchInput in = {};
fill_sparse_batch(in);
uint8_t encoded[256] = {};
size_t encoded_len = 0;
in.n = 31;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
in.n = 0;
in.present_mask = 1;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
in.n = 2;
in.present_mask = 0x00000003UL;
in.energy_wh[1] = in.energy_wh[0] - 1;
TEST_ASSERT_FALSE(encode_batch(in, encoded, sizeof(encoded), &encoded_len));
fill_sparse_batch(in);
TEST_ASSERT_FALSE(encode_batch(in, encoded, 10, &encoded_len));
}
static const uint8_t VECTOR_SYNC_EMPTY[] = {
0xB3, 0xDD, 0x04, 0x01, 0x01, 0x00, 0x34, 0x12,
0x15, 0xCD, 0x5B, 0x07,
0xE4, 0x97, 0x7E, 0x69,
0x00, 0x00, 0x00, 0x00,
0x00,
0xA6, 0x0E,
0x00, 0x00, 0x00, 0x00, 0x00};
static const uint8_t VECTOR_SPARSE_5[] = {
0xB3, 0xDD, 0x04, 0x01, 0x01, 0x00, 0x2A, 0x00,
0x15, 0xCD, 0x5B, 0x07,
0x00, 0xF1, 0x53, 0x65,
0x0D, 0x04, 0x00, 0x20,
0x05, 0xA6, 0x0E,
0x02, 0x01, 0x03, 0x02, 0x01,
0xA0, 0x86, 0x01, 0x00, 0x01, 0x31, 0x00, 0x96, 0x01, 0x88, 0xFF, 0x3C, 0xA0, 0x1F,
0x9F, 0x1F, 0x9C, 0x09, 0x32, 0x00, 0x9F, 0x1F, 0xB4, 0x1F, 0xA0, 0x1F, 0xAB, 0x20, 0x00, 0x00, 0x14, 0x9F, 0x1F,
0xA0, 0x1F, 0x14};
static const uint8_t VECTOR_FULL_30[] = {
0xB3, 0xDD, 0x03, 0x01, 0x01, 0x00, 0xEF, 0xBE, 0x67, 0x9B, 0x7E, 0x69, 0xFF, 0xFF, 0xFF, 0x3F, 0x1E, 0xFF, 0x0F,
0x0A, 0x14, 0x1E, 0x03, 0x06, 0x20, 0xA1, 0x07, 0x00, 0x03, 0x09, 0x0F, 0x15, 0x1B, 0x21, 0x27, 0x2D, 0x33, 0x39,
0x3F, 0x45, 0x4B, 0x51, 0x57, 0x5D, 0x63, 0x69, 0x6F, 0x75, 0x7B, 0x81, 0x01, 0x87, 0x01, 0x8D, 0x01, 0x93, 0x01,
0x99, 0x01, 0x9F, 0x01, 0xA5, 0x01, 0xAB, 0x01, 0x18, 0xFC, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0xF4, 0x01, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B,
0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x64, 0x00, 0x85, 0x03, 0x9A, 0x03,
0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A,
0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03,
0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03, 0x9A, 0x03, 0x85, 0x03};
static void test_payload_golden_vectors() {
BatchInput expected_sync = {};
expected_sync.sender_id = 1;
expected_sync.batch_id = 0x1234;
expected_sync.meter_t_last = 123456789;
expected_sync.ts_utc_last = 1769904100;
expected_sync.present_mask = 0;
expected_sync.n = 0;
expected_sync.battery_mV = 3750;
expected_sync.err_m = 0;
expected_sync.err_d = 0;
expected_sync.err_tx = 0;
expected_sync.err_last = 0;
expected_sync.err_rx_reject = 0;
BatchInput expected_sparse = {};
fill_sparse_batch(expected_sparse);
BatchInput expected_full = {};
fill_full_batch(expected_full);
struct VectorCase {
const char *name;
const uint8_t *bytes;
size_t len;
const BatchInput *expected;
} cases[] = {
{"sync_empty", VECTOR_SYNC_EMPTY, sizeof(VECTOR_SYNC_EMPTY), &expected_sync},
{"sparse_5", VECTOR_SPARSE_5, sizeof(VECTOR_SPARSE_5), &expected_sparse},
{"full_30", VECTOR_FULL_30, sizeof(VECTOR_FULL_30), &expected_full},
};
for (size_t i = 0; i < (sizeof(cases) / sizeof(cases[0])); ++i) {
BatchInput decoded = {};
TEST_ASSERT_TRUE_MESSAGE(decode_batch(cases[i].bytes, cases[i].len, &decoded), cases[i].name);
assert_batch_equals(*cases[i].expected, decoded);
uint8_t reencoded[512] = {};
size_t reencoded_len = 0;
TEST_ASSERT_TRUE_MESSAGE(encode_batch(*cases[i].expected, reencoded, sizeof(reencoded), &reencoded_len), cases[i].name);
TEST_ASSERT_EQUAL_UINT_MESSAGE(cases[i].len, reencoded_len, cases[i].name);
TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(cases[i].bytes, reencoded, cases[i].len, cases[i].name);
}
}
void setup() {
dd3_legacy_core_force_link();
UNITY_BEGIN();
RUN_TEST(test_encode_decode_roundtrip_schema_v4);
RUN_TEST(test_decode_rejects_bad_magic_schema_flags);
RUN_TEST(test_decode_rejects_truncated_and_length_mismatch);
RUN_TEST(test_encode_and_decode_reject_invalid_present_mask);
RUN_TEST(test_encode_rejects_invalid_n_and_regression_cases);
RUN_TEST(test_payload_golden_vectors);
UNITY_END();
}
void loop() {}