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.
This commit is contained in:
2026-06-30 12:19:27 +02:00
parent 664ff1d744
commit def09160d0
13 changed files with 242 additions and 133 deletions
+24 -10
View File
@@ -10,7 +10,8 @@ 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.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;
@@ -45,7 +46,8 @@ 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.meter_t_last = 1769904999 - 29;
in.ts_utc_last = 1769904999;
in.present_mask = 0x3FFFFFFFUL;
in.n = kMaxSamples;
in.battery_mV = 4095;
@@ -65,7 +67,8 @@ static void fill_full_batch(BatchInput &in) {
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.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);
@@ -89,14 +92,14 @@ static void assert_batch_equals(const BatchInput &expected, const BatchInput &ac
}
}
static void test_encode_decode_roundtrip_schema_v3() {
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 > 24);
TEST_ASSERT_TRUE(encoded_len > 28);
BatchInput out = {};
TEST_ASSERT_TRUE(decode_batch(encoded, encoded_len, &out));
@@ -199,12 +202,22 @@ static void test_encode_rejects_invalid_n_and_regression_cases() {
}
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,
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, 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,
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};
@@ -224,7 +237,8 @@ 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.meter_t_last = 123456789;
expected_sync.ts_utc_last = 1769904100;
expected_sync.present_mask = 0;
expected_sync.n = 0;
expected_sync.battery_mV = 3750;
@@ -267,7 +281,7 @@ static void test_payload_golden_vectors() {
void setup() {
dd3_legacy_core_force_link();
UNITY_BEGIN();
RUN_TEST(test_encode_decode_roundtrip_schema_v3);
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);
+59 -45
View File
@@ -17,8 +17,8 @@
static void test_decode_batch_null_args() {
uint8_t dummy[32] = {};
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(nullptr, 24, &out));
TEST_ASSERT_FALSE(decode_batch(dummy, 24, nullptr));
TEST_ASSERT_FALSE(decode_batch(nullptr, 28, &out));
TEST_ASSERT_FALSE(decode_batch(dummy, 28, nullptr));
TEST_ASSERT_FALSE(decode_batch(nullptr, 0, nullptr));
}
@@ -29,90 +29,103 @@ static void test_decode_batch_zero_length() {
}
static void test_decode_batch_minimal_valid_sync() {
// Sync-only (n=0) payload: 24 bytes header, no samples.
uint8_t buf[24] = {};
// Sync-only (n=0) payload: 28 bytes header, no samples.
uint8_t buf[28] = {};
// magic 0xDDB3 LE
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 3; // schema
buf[2] = 4; // schema
buf[3] = 0x01; // flags
// sender_id=1
buf[4] = 0x01; buf[5] = 0x00;
// batch_id=1
buf[6] = 0x01; buf[7] = 0x00;
// t_last=1769904000 LE
uint32_t t = 1769904000UL;
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
// meter_t_last=123456789 LE
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
// ts_utc_last=1769904000 LE
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
// present_mask=0
buf[12] = 0; buf[13] = 0; buf[14] = 0; buf[15] = 0;
buf[16] = 0; buf[17] = 0; buf[18] = 0; buf[19] = 0;
// n=0
buf[16] = 0;
buf[20] = 0;
// battery_mV=3750 LE
buf[17] = 0xA6; buf[18] = 0x0E;
buf[21] = 0xA6; buf[22] = 0x0E;
// err fields
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_TRUE(decode_batch(buf, 24, &out));
TEST_ASSERT_TRUE(decode_batch(buf, 28, &out));
TEST_ASSERT_EQUAL_UINT8(0, out.n);
TEST_ASSERT_EQUAL_UINT32(0, out.present_mask);
}
static void test_decode_batch_n_exceeds_30() {
// Forge a header with n=31, which should be rejected.
uint8_t buf[24] = {};
uint8_t buf[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 3; buf[3] = 0x01;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t t = 1769904000UL;
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
buf[12] = 0xFF; buf[13] = 0xFF; buf[14] = 0xFF; buf[15] = 0x3F; // all 30 bits set
buf[16] = 31; // n=31 → must reject
buf[17] = 0xA6; buf[18] = 0x0E;
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0xFF; buf[17] = 0xFF; buf[18] = 0xFF; buf[19] = 0x3F; // all 30 bits set
buf[20] = 31; // n=31 → must reject
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
}
static void test_decode_batch_present_mask_n_mismatch() {
// present_mask has 3 bits but n=5 → must reject.
uint8_t buf[24] = {};
uint8_t buf[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 3; buf[3] = 0x01;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t t = 1769904000UL;
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
buf[12] = 0x07; buf[13] = 0; buf[14] = 0; buf[15] = 0; // 3 bits
buf[16] = 5; // n=5 but only 3 mask bits
buf[17] = 0xA6; buf[18] = 0x0E;
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0x07; buf[17] = 0; buf[18] = 0; buf[19] = 0; // 3 bits
buf[20] = 5; // n=5 but only 3 mask bits
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
}
static void test_decode_batch_reserved_mask_bits() {
// Bit 30 or 31 set → must reject (only bits 0-29 valid).
uint8_t buf[24] = {};
uint8_t buf[28] = {};
buf[0] = 0xB3; buf[1] = 0xDD;
buf[2] = 3; buf[3] = 0x01;
buf[2] = 4; buf[3] = 0x01;
buf[4] = 0x01; buf[5] = 0x00;
buf[6] = 0x01; buf[7] = 0x00;
uint32_t t = 1769904000UL;
buf[8] = t & 0xFF; buf[9] = (t >> 8) & 0xFF;
buf[10] = (t >> 16) & 0xFF; buf[11] = (t >> 24) & 0xFF;
buf[12] = 0x01; buf[13] = 0; buf[14] = 0; buf[15] = 0x40; // bit 30
buf[16] = 1;
buf[17] = 0xA6; buf[18] = 0x0E;
buf[19] = 0; buf[20] = 0; buf[21] = 0; buf[22] = 0; buf[23] = 0;
uint32_t meter_t = 123456789UL;
buf[8] = meter_t & 0xFF; buf[9] = (meter_t >> 8) & 0xFF;
buf[10] = (meter_t >> 16) & 0xFF; buf[11] = (meter_t >> 24) & 0xFF;
uint32_t utc_t = 1769904000UL;
buf[12] = utc_t & 0xFF; buf[13] = (utc_t >> 8) & 0xFF;
buf[14] = (utc_t >> 16) & 0xFF; buf[15] = (utc_t >> 24) & 0xFF;
buf[16] = 0x01; buf[17] = 0; buf[18] = 0; buf[19] = 0x40; // bit 30
buf[20] = 1;
buf[21] = 0xA6; buf[22] = 0x0E;
buf[23] = 0; buf[24] = 0; buf[25] = 0; buf[26] = 0; buf[27] = 0;
BatchInput out = {};
TEST_ASSERT_FALSE(decode_batch(buf, 24, &out));
TEST_ASSERT_FALSE(decode_batch(buf, 28, &out));
}
// ---- uleb128_decode: negative tests ----
@@ -308,7 +321,8 @@ static void test_decode_batch_byte_flip_fuzz() {
BatchInput in = {};
in.sender_id = 1;
in.batch_id = 42;
in.t_last = 1769904000UL;
in.meter_t_last = 123456789UL;
in.ts_utc_last = 1769904000UL;
in.present_mask = 0x07; // bits 0-2
in.n = 3;
in.battery_mV = 3750;