diff --git a/lib/dd3_transport_logic/include/batch_reassembly_logic.h b/lib/dd3_transport_logic/include/batch_reassembly_logic.h new file mode 100644 index 0000000..3c67142 --- /dev/null +++ b/lib/dd3_transport_logic/include/batch_reassembly_logic.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +struct BatchReassemblyState { + bool active; + uint16_t batch_id; + uint8_t next_index; + uint8_t expected_chunks; + uint16_t total_len; + uint16_t received_len; + uint32_t last_rx_ms; + uint32_t timeout_ms; +}; + +enum class BatchReassemblyStatus : uint8_t { + InProgress = 0, + Complete = 1, + ErrorReset = 2 +}; + +void batch_reassembly_reset(BatchReassemblyState &state); + +BatchReassemblyStatus batch_reassembly_push(BatchReassemblyState &state, uint16_t batch_id, uint8_t chunk_index, + uint8_t chunk_count, uint16_t total_len, const uint8_t *chunk_data, + size_t chunk_len, uint32_t now_ms, uint32_t timeout_ms_for_new_batch, + uint16_t max_total_len, uint8_t *buffer, size_t buffer_cap, + uint16_t &out_complete_len); diff --git a/lib/dd3_transport_logic/include/lora_frame_logic.h b/lib/dd3_transport_logic/include/lora_frame_logic.h new file mode 100644 index 0000000..0d64abe --- /dev/null +++ b/lib/dd3_transport_logic/include/lora_frame_logic.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +enum class LoraFrameDecodeStatus : uint8_t { + Ok = 0, + LengthMismatch = 1, + CrcFail = 2, + InvalidMsgKind = 3 +}; + +uint16_t lora_crc16_ccitt(const uint8_t *data, size_t len); + +bool lora_build_frame(uint8_t msg_kind, uint16_t device_id_short, const uint8_t *payload, size_t payload_len, + uint8_t *out_frame, size_t out_cap, size_t &out_len); + +LoraFrameDecodeStatus lora_parse_frame(const uint8_t *frame, size_t frame_len, uint8_t max_msg_kind, uint8_t *out_msg_kind, + uint16_t *out_device_id_short, uint8_t *out_payload, size_t payload_cap, + size_t *out_payload_len); diff --git a/lib/dd3_transport_logic/src/batch_reassembly_logic.cpp b/lib/dd3_transport_logic/src/batch_reassembly_logic.cpp new file mode 100644 index 0000000..1181086 --- /dev/null +++ b/lib/dd3_transport_logic/src/batch_reassembly_logic.cpp @@ -0,0 +1,75 @@ +#include "batch_reassembly_logic.h" + +#include + +void batch_reassembly_reset(BatchReassemblyState &state) { + state.active = false; + state.batch_id = 0; + state.next_index = 0; + state.expected_chunks = 0; + state.total_len = 0; + state.received_len = 0; + state.last_rx_ms = 0; + state.timeout_ms = 0; +} + +BatchReassemblyStatus batch_reassembly_push(BatchReassemblyState &state, uint16_t batch_id, uint8_t chunk_index, + uint8_t chunk_count, uint16_t total_len, const uint8_t *chunk_data, + size_t chunk_len, uint32_t now_ms, uint32_t timeout_ms_for_new_batch, + uint16_t max_total_len, uint8_t *buffer, size_t buffer_cap, + uint16_t &out_complete_len) { + out_complete_len = 0; + if (!buffer || !chunk_data) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + if (chunk_len > 0 && total_len == 0) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + + bool expired = state.timeout_ms > 0 && (now_ms - state.last_rx_ms > state.timeout_ms); + if (!state.active || batch_id != state.batch_id || expired) { + if (chunk_index != 0) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + if (total_len == 0 || total_len > max_total_len || chunk_count == 0) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + state.active = true; + state.batch_id = batch_id; + state.expected_chunks = chunk_count; + state.total_len = total_len; + state.received_len = 0; + state.next_index = 0; + state.last_rx_ms = now_ms; + state.timeout_ms = timeout_ms_for_new_batch; + } + + if (!state.active || chunk_index != state.next_index || chunk_count != state.expected_chunks) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + + if (state.received_len + chunk_len > state.total_len || + state.received_len + chunk_len > max_total_len || + state.received_len + chunk_len > buffer_cap) { + batch_reassembly_reset(state); + return BatchReassemblyStatus::ErrorReset; + } + + memcpy(&buffer[state.received_len], chunk_data, chunk_len); + state.received_len += static_cast(chunk_len); + state.next_index++; + state.last_rx_ms = now_ms; + + if (state.next_index == state.expected_chunks && state.received_len == state.total_len) { + out_complete_len = state.received_len; + batch_reassembly_reset(state); + return BatchReassemblyStatus::Complete; + } + + return BatchReassemblyStatus::InProgress; +} diff --git a/lib/dd3_transport_logic/src/lora_frame_logic.cpp b/lib/dd3_transport_logic/src/lora_frame_logic.cpp new file mode 100644 index 0000000..17ed10e --- /dev/null +++ b/lib/dd3_transport_logic/src/lora_frame_logic.cpp @@ -0,0 +1,88 @@ +#include "lora_frame_logic.h" + +#include + +uint16_t lora_crc16_ccitt(const uint8_t *data, size_t len) { + if (!data && len > 0) { + return 0; + } + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; ++i) { + crc ^= static_cast(data[i]) << 8; + for (uint8_t b = 0; b < 8; ++b) { + if (crc & 0x8000) { + crc = (crc << 1) ^ 0x1021; + } else { + crc <<= 1; + } + } + } + return crc; +} + +bool lora_build_frame(uint8_t msg_kind, uint16_t device_id_short, const uint8_t *payload, size_t payload_len, + uint8_t *out_frame, size_t out_cap, size_t &out_len) { + out_len = 0; + if (!out_frame) { + return false; + } + if (payload_len > 0 && !payload) { + return false; + } + if (payload_len > (SIZE_MAX - 5)) { + return false; + } + size_t needed = payload_len + 5; + if (needed > out_cap) { + return false; + } + + size_t idx = 0; + out_frame[idx++] = msg_kind; + out_frame[idx++] = static_cast(device_id_short >> 8); + out_frame[idx++] = static_cast(device_id_short & 0xFF); + if (payload_len > 0) { + memcpy(&out_frame[idx], payload, payload_len); + idx += payload_len; + } + uint16_t crc = lora_crc16_ccitt(out_frame, idx); + out_frame[idx++] = static_cast(crc >> 8); + out_frame[idx++] = static_cast(crc & 0xFF); + out_len = idx; + return true; +} + +LoraFrameDecodeStatus lora_parse_frame(const uint8_t *frame, size_t frame_len, uint8_t max_msg_kind, uint8_t *out_msg_kind, + uint16_t *out_device_id_short, uint8_t *out_payload, size_t payload_cap, + size_t *out_payload_len) { + if (!frame || !out_msg_kind || !out_device_id_short || !out_payload_len) { + return LoraFrameDecodeStatus::LengthMismatch; + } + if (frame_len < 5) { + return LoraFrameDecodeStatus::LengthMismatch; + } + + size_t payload_len = frame_len - 5; + if (payload_len > payload_cap || (payload_len > 0 && !out_payload)) { + return LoraFrameDecodeStatus::LengthMismatch; + } + + uint16_t crc_calc = lora_crc16_ccitt(frame, frame_len - 2); + uint16_t crc_rx = static_cast(frame[frame_len - 2] << 8) | frame[frame_len - 1]; + if (crc_calc != crc_rx) { + return LoraFrameDecodeStatus::CrcFail; + } + + uint8_t msg_kind = frame[0]; + if (msg_kind > max_msg_kind) { + return LoraFrameDecodeStatus::InvalidMsgKind; + } + + *out_msg_kind = msg_kind; + *out_device_id_short = static_cast(frame[1] << 8) | frame[2]; + if (payload_len > 0) { + memcpy(out_payload, &frame[3], payload_len); + } + *out_payload_len = payload_len; + return LoraFrameDecodeStatus::Ok; +} diff --git a/src/lora_transport.cpp b/src/lora_transport.cpp index 9ccde41..be36277 100644 --- a/src/lora_transport.cpp +++ b/src/lora_transport.cpp @@ -1,4 +1,5 @@ #include "lora_transport.h" +#include "lora_frame_logic.h" #include #include #include @@ -35,21 +36,6 @@ bool lora_get_last_rx_signal(int16_t &rssi_dbm, float &snr_db) { return true; } -static uint16_t crc16_ccitt(const uint8_t *data, size_t len) { - uint16_t crc = 0xFFFF; - for (size_t i = 0; i < len; ++i) { - crc ^= static_cast(data[i]) << 8; - for (uint8_t b = 0; b < 8; ++b) { - if (crc & 0x8000) { - crc = (crc << 1) ^ 0x1021; - } else { - crc <<= 1; - } - } - } - return crc; -} - void lora_init() { SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS); LoRa.setPins(PIN_LORA_NSS, PIN_LORA_RST, PIN_LORA_DIO0); @@ -70,32 +56,26 @@ bool lora_send(const LoraPacket &pkt) { t0 = millis(); } LoRa.idle(); - uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2]; - size_t idx = 0; - buffer[idx++] = static_cast(pkt.msg_kind); - buffer[idx++] = static_cast(pkt.device_id_short >> 8); - buffer[idx++] = static_cast(pkt.device_id_short & 0xFF); - if (pkt.payload_len > LORA_MAX_PAYLOAD) { return false; } - memcpy(&buffer[idx], pkt.payload, pkt.payload_len); - idx += pkt.payload_len; - - uint16_t crc = crc16_ccitt(buffer, idx); - buffer[idx++] = static_cast(crc >> 8); - buffer[idx++] = static_cast(crc & 0xFF); + uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2]; + size_t frame_len = 0; + if (!lora_build_frame(static_cast(pkt.msg_kind), pkt.device_id_short, pkt.payload, pkt.payload_len, + buffer, sizeof(buffer), frame_len)) { + return false; + } LoRa.beginPacket(); - LoRa.write(buffer, idx); + LoRa.write(buffer, frame_len); int result = LoRa.endPacket(false); bool ok = result == 1; if (SERIAL_DEBUG_MODE) { uint32_t tx_ms = millis() - t0; if (!ok || tx_ms > 2000) { Serial.printf("lora_tx: len=%u total=%lums ok=%u\n", - static_cast(idx), + static_cast(frame_len), static_cast(tx_ms), ok ? 1U : 0U); } @@ -141,26 +121,33 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { return false; } - uint16_t crc_calc = crc16_ccitt(buffer, len - 2); - uint16_t crc_rx = static_cast(buffer[len - 2] << 8) | buffer[len - 1]; - if (crc_calc != crc_rx) { + uint8_t msg_kind = 0; + uint16_t device_id_short = 0; + size_t payload_len = 0; + LoraFrameDecodeStatus status = lora_parse_frame( + buffer, len, static_cast(LoraMsgKind::AckDown), &msg_kind, &device_id_short, + pkt.payload, sizeof(pkt.payload), &payload_len); + + if (status == LoraFrameDecodeStatus::CrcFail) { note_reject(RxRejectReason::CrcFail); return false; } - uint8_t msg_kind = buffer[0]; - if (msg_kind > static_cast(LoraMsgKind::AckDown)) { + if (status == LoraFrameDecodeStatus::InvalidMsgKind) { note_reject(RxRejectReason::InvalidMsgKind); return false; } + if (status == LoraFrameDecodeStatus::LengthMismatch) { + note_reject(RxRejectReason::LengthMismatch); + return false; + } pkt.msg_kind = static_cast(msg_kind); - pkt.device_id_short = static_cast(buffer[1] << 8) | buffer[2]; - pkt.payload_len = len - 5; + pkt.device_id_short = device_id_short; + pkt.payload_len = payload_len; if (pkt.payload_len > LORA_MAX_PAYLOAD) { note_reject(RxRejectReason::LengthMismatch); return false; } - memcpy(pkt.payload, &buffer[3], pkt.payload_len); pkt.rssi_dbm = g_last_rx_rssi_dbm; pkt.snr_db = g_last_rx_snr_db; return true; diff --git a/src/receiver_pipeline.cpp b/src/receiver_pipeline.cpp index 4b3162e..0ef466c 100644 --- a/src/receiver_pipeline.cpp +++ b/src/receiver_pipeline.cpp @@ -5,6 +5,7 @@ #include #include "config.h" +#include "batch_reassembly_logic.h" #include "display_ui.h" #include "json_codec.h" #include "lora_transport.h" @@ -90,19 +91,8 @@ static bool mqtt_publish_sample(const MeterData &data) { #endif } -struct BatchRxState { - bool active; - uint16_t batch_id; - uint8_t next_index; - uint8_t expected_chunks; - uint16_t total_len; - uint16_t received_len; - uint32_t last_rx_ms; - uint32_t timeout_ms; - uint8_t buffer[BATCH_MAX_COMPRESSED]; -}; - -static BatchRxState g_batch_rx = {}; +static BatchReassemblyState g_batch_rx = {}; +static uint8_t g_batch_rx_buffer[BATCH_MAX_COMPRESSED] = {}; static void init_sender_statuses() { for (uint8_t i = 0; i < NUM_SENDERS; ++i) { @@ -272,14 +262,7 @@ static void send_batch_ack(uint16_t batch_id, uint8_t sample_count) { } static void reset_batch_rx() { - g_batch_rx.active = false; - g_batch_rx.batch_id = 0; - g_batch_rx.next_index = 0; - g_batch_rx.expected_chunks = 0; - g_batch_rx.total_len = 0; - g_batch_rx.received_len = 0; - g_batch_rx.last_rx_ms = 0; - g_batch_rx.timeout_ms = 0; + batch_reassembly_reset(g_batch_rx); } static bool process_batch_packet(const LoraPacket &pkt, BatchInput &out_batch, bool &decode_error, uint16_t &out_batch_id) { @@ -295,47 +278,25 @@ static bool process_batch_packet(const LoraPacket &pkt, BatchInput &out_batch, b size_t chunk_len = pkt.payload_len - BATCH_HEADER_SIZE; uint32_t now_ms = millis(); - if (!g_batch_rx.active || batch_id != g_batch_rx.batch_id || (now_ms - g_batch_rx.last_rx_ms > g_batch_rx.timeout_ms)) { - if (chunk_index != 0) { - reset_batch_rx(); - return false; - } - if (total_len == 0 || total_len > BATCH_MAX_COMPRESSED || chunk_count == 0) { - reset_batch_rx(); - return false; - } - g_batch_rx.active = true; - g_batch_rx.batch_id = batch_id; - g_batch_rx.expected_chunks = chunk_count; - g_batch_rx.total_len = total_len; - g_batch_rx.received_len = 0; - g_batch_rx.next_index = 0; - g_batch_rx.timeout_ms = compute_batch_rx_timeout_ms(total_len, chunk_count); - } + uint16_t complete_len = 0; + BatchReassemblyStatus reassembly_status = batch_reassembly_push( + g_batch_rx, batch_id, chunk_index, chunk_count, total_len, chunk_data, chunk_len, now_ms, + compute_batch_rx_timeout_ms(total_len, chunk_count), BATCH_MAX_COMPRESSED, g_batch_rx_buffer, + sizeof(g_batch_rx_buffer), complete_len); - if (!g_batch_rx.active || chunk_index != g_batch_rx.next_index || chunk_count != g_batch_rx.expected_chunks) { - reset_batch_rx(); + if (reassembly_status == BatchReassemblyStatus::ErrorReset) { + return false; + } + if (reassembly_status == BatchReassemblyStatus::InProgress) { return false; } - if (g_batch_rx.received_len + chunk_len > g_batch_rx.total_len || g_batch_rx.received_len + chunk_len > BATCH_MAX_COMPRESSED) { - reset_batch_rx(); - return false; - } - - memcpy(&g_batch_rx.buffer[g_batch_rx.received_len], chunk_data, chunk_len); - g_batch_rx.received_len += static_cast(chunk_len); - g_batch_rx.next_index++; - g_batch_rx.last_rx_ms = now_ms; - - if (g_batch_rx.next_index == g_batch_rx.expected_chunks && g_batch_rx.received_len == g_batch_rx.total_len) { - if (!decode_batch(g_batch_rx.buffer, g_batch_rx.received_len, &out_batch)) { + if (reassembly_status == BatchReassemblyStatus::Complete) { + if (!decode_batch(g_batch_rx_buffer, complete_len, &out_batch)) { decode_error = true; - reset_batch_rx(); return false; } out_batch_id = batch_id; - reset_batch_rx(); return true; } diff --git a/test/test_lora_transport/test_lora_transport.cpp b/test/test_lora_transport/test_lora_transport.cpp new file mode 100644 index 0000000..0a9a7e1 --- /dev/null +++ b/test/test_lora_transport/test_lora_transport.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include "batch_reassembly_logic.h" +#include "lora_frame_logic.h" + +static void test_crc16_known_vectors() { + const uint8_t canonical[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}; + TEST_ASSERT_EQUAL_HEX16(0x29B1, lora_crc16_ccitt(canonical, sizeof(canonical))); + + const uint8_t binary[] = {0x00, 0x01, 0x02, 0x03, 0x04}; + TEST_ASSERT_EQUAL_HEX16(0x1C0F, lora_crc16_ccitt(binary, sizeof(binary))); +} + +static void test_frame_encode_decode_and_crc_reject() { + const uint8_t payload[] = {0x01, 0x02, 0xA5}; + uint8_t frame[64] = {}; + size_t frame_len = 0; + TEST_ASSERT_TRUE(lora_build_frame(0, 0xF19C, payload, sizeof(payload), frame, sizeof(frame), frame_len)); + TEST_ASSERT_EQUAL_UINT(8, frame_len); + + uint8_t out_kind = 0xFF; + uint16_t out_device_id = 0; + uint8_t out_payload[16] = {}; + size_t out_payload_len = 0; + LoraFrameDecodeStatus ok = lora_parse_frame(frame, frame_len, 1, &out_kind, &out_device_id, out_payload, + sizeof(out_payload), &out_payload_len); + TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::Ok), static_cast(ok)); + TEST_ASSERT_EQUAL_UINT8(0, out_kind); + TEST_ASSERT_EQUAL_UINT16(0xF19C, out_device_id); + TEST_ASSERT_EQUAL_UINT(sizeof(payload), out_payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, out_payload, sizeof(payload)); + + frame[frame_len - 1] ^= 0x01; + LoraFrameDecodeStatus bad_crc = lora_parse_frame(frame, frame_len, 1, &out_kind, &out_device_id, out_payload, + sizeof(out_payload), &out_payload_len); + TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::CrcFail), static_cast(bad_crc)); +} + +static void test_frame_rejects_invalid_msg_kind_and_short_length() { + const uint8_t payload[] = {0x42}; + uint8_t frame[32] = {}; + size_t frame_len = 0; + TEST_ASSERT_TRUE(lora_build_frame(2, 0xF19C, payload, sizeof(payload), frame, sizeof(frame), frame_len)); + + uint8_t out_kind = 0; + uint16_t out_device_id = 0; + uint8_t out_payload[8] = {}; + size_t out_payload_len = 0; + LoraFrameDecodeStatus invalid_msg = lora_parse_frame(frame, frame_len, 1, &out_kind, &out_device_id, out_payload, + sizeof(out_payload), &out_payload_len); + TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::InvalidMsgKind), static_cast(invalid_msg)); + + LoraFrameDecodeStatus short_len = lora_parse_frame(frame, 4, 1, &out_kind, &out_device_id, out_payload, + sizeof(out_payload), &out_payload_len); + TEST_ASSERT_EQUAL_UINT8(static_cast(LoraFrameDecodeStatus::LengthMismatch), static_cast(short_len)); +} + +static void test_chunk_reassembly_in_order_success() { + BatchReassemblyState state = {}; + batch_reassembly_reset(state); + + const uint8_t payload[] = {1, 2, 3, 4, 5, 6, 7}; + uint8_t buffer[32] = {}; + uint16_t complete_len = 0; + + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::InProgress), + static_cast(batch_reassembly_push(state, 77, 0, 3, 7, &payload[0], 3, 1000, 5000, 32, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::InProgress), + static_cast(batch_reassembly_push(state, 77, 1, 3, 7, &payload[3], 2, 1100, 5000, 32, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::Complete), + static_cast(batch_reassembly_push(state, 77, 2, 3, 7, &payload[5], 2, 1200, 5000, 32, buffer, sizeof(buffer), complete_len))); + + TEST_ASSERT_EQUAL_UINT16(7, complete_len); + TEST_ASSERT_FALSE(state.active); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, buffer, sizeof(payload)); +} + +static void test_chunk_reassembly_missing_or_out_of_order_fails_deterministically() { + BatchReassemblyState state = {}; + batch_reassembly_reset(state); + + const uint8_t payload[] = {9, 8, 7, 6, 5, 4}; + uint8_t buffer[32] = {}; + uint16_t complete_len = 0; + + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::InProgress), + static_cast(batch_reassembly_push(state, 10, 0, 3, 6, &payload[0], 2, 1000, 5000, 32, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::ErrorReset), + static_cast(batch_reassembly_push(state, 10, 2, 3, 6, &payload[4], 2, 1100, 5000, 32, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_FALSE(state.active); + + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::ErrorReset), + static_cast(batch_reassembly_push(state, 11, 1, 3, 6, &payload[2], 2, 1200, 5000, 32, buffer, sizeof(buffer), complete_len))); +} + +static void test_chunk_reassembly_wrong_total_length_fails() { + BatchReassemblyState state = {}; + batch_reassembly_reset(state); + + const uint8_t payload[] = {1, 2, 3, 4, 5, 6}; + uint8_t buffer[8] = {}; + uint16_t complete_len = 0; + + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::InProgress), + static_cast(batch_reassembly_push(state, 55, 0, 2, 5, &payload[0], 3, 1000, 5000, 8, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_EQUAL_UINT8( + static_cast(BatchReassemblyStatus::ErrorReset), + static_cast(batch_reassembly_push(state, 55, 1, 2, 5, &payload[3], 3, 1100, 5000, 8, buffer, sizeof(buffer), complete_len))); + TEST_ASSERT_FALSE(state.active); +} + +void setup() { + UNITY_BEGIN(); + RUN_TEST(test_crc16_known_vectors); + RUN_TEST(test_frame_encode_decode_and_crc_reject); + RUN_TEST(test_frame_rejects_invalid_msg_kind_and_short_length); + RUN_TEST(test_chunk_reassembly_in_order_success); + RUN_TEST(test_chunk_reassembly_missing_or_out_of_order_fails_deterministically); + RUN_TEST(test_chunk_reassembly_wrong_total_length_fails); + UNITY_END(); +} + +void loop() {}