diff --git a/lib/dd3_legacy_core/include/payload_codec.h b/lib/dd3_legacy_core/include/payload_codec.h new file mode 100644 index 0000000..1a60b02 --- /dev/null +++ b/lib/dd3_legacy_core/include/payload_codec.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +struct BatchInput { + uint16_t sender_id; + uint16_t batch_id; + uint32_t t_last; + uint32_t present_mask; + uint8_t n; + uint16_t battery_mV; + uint8_t err_m; + uint8_t err_d; + uint8_t err_tx; + uint8_t err_last; + uint8_t err_rx_reject; + uint32_t energy_wh[30]; + int16_t p1_w[30]; + int16_t p2_w[30]; + int16_t p3_w[30]; +}; + +bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *out_len); +bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out); + +size_t uleb128_encode(uint32_t v, uint8_t *out, size_t cap); +bool uleb128_decode(const uint8_t *in, size_t len, size_t *pos, uint32_t *v); + +uint32_t zigzag32(int32_t x); +int32_t unzigzag32(uint32_t u); + +size_t svarint_encode(int32_t x, uint8_t *out, size_t cap); +bool svarint_decode(const uint8_t *in, size_t len, size_t *pos, int32_t *x); + +#ifdef PAYLOAD_CODEC_TEST +bool payload_codec_self_test(); +#endif diff --git a/src/payload_codec.cpp b/lib/dd3_legacy_core/src/payload_codec.cpp similarity index 100% rename from src/payload_codec.cpp rename to lib/dd3_legacy_core/src/payload_codec.cpp diff --git a/test/test_payload_codec/test_payload_codec.cpp b/test/test_payload_codec/test_payload_codec.cpp new file mode 100644 index 0000000..e58a077 --- /dev/null +++ b/test/test_payload_codec/test_payload_codec.cpp @@ -0,0 +1,279 @@ +#include +#include + +#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.t_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.t_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(i) * static_cast(i) * 3UL; + in.p1_w[i] = static_cast(-1000 + static_cast(i) * 25); + in.p2_w[i] = static_cast(500 - static_cast(i) * 30); + in.p3_w[i] = static_cast(((i % 2) == 0 ? 100 : -100) + static_cast(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.t_last, actual.t_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_v3() { + 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 > 24); + + 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, 0x03, 0x01, 0x01, 0x00, 0x34, 0x12, 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, 0x03, 0x01, 0x01, 0x00, 0x2A, 0x00, 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.t_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_v3); + 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() {}