test: add lora frame and chunk reassembly logic suite
This commit is contained in:
28
lib/dd3_transport_logic/include/batch_reassembly_logic.h
Normal file
28
lib/dd3_transport_logic/include/batch_reassembly_logic.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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);
|
||||||
19
lib/dd3_transport_logic/include/lora_frame_logic.h
Normal file
19
lib/dd3_transport_logic/include/lora_frame_logic.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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);
|
||||||
75
lib/dd3_transport_logic/src/batch_reassembly_logic.cpp
Normal file
75
lib/dd3_transport_logic/src/batch_reassembly_logic.cpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#include "batch_reassembly_logic.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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<uint16_t>(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;
|
||||||
|
}
|
||||||
88
lib/dd3_transport_logic/src/lora_frame_logic.cpp
Normal file
88
lib/dd3_transport_logic/src/lora_frame_logic.cpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "lora_frame_logic.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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<uint16_t>(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<uint8_t>(device_id_short >> 8);
|
||||||
|
out_frame[idx++] = static_cast<uint8_t>(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<uint8_t>(crc >> 8);
|
||||||
|
out_frame[idx++] = static_cast<uint8_t>(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<uint16_t>(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<uint16_t>(frame[1] << 8) | frame[2];
|
||||||
|
if (payload_len > 0) {
|
||||||
|
memcpy(out_payload, &frame[3], payload_len);
|
||||||
|
}
|
||||||
|
*out_payload_len = payload_len;
|
||||||
|
return LoraFrameDecodeStatus::Ok;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "lora_transport.h"
|
#include "lora_transport.h"
|
||||||
|
#include "lora_frame_logic.h"
|
||||||
#include <LoRa.h>
|
#include <LoRa.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
@@ -35,21 +36,6 @@ bool lora_get_last_rx_signal(int16_t &rssi_dbm, float &snr_db) {
|
|||||||
return true;
|
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<uint16_t>(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() {
|
void lora_init() {
|
||||||
SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS);
|
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);
|
LoRa.setPins(PIN_LORA_NSS, PIN_LORA_RST, PIN_LORA_DIO0);
|
||||||
@@ -70,32 +56,26 @@ bool lora_send(const LoraPacket &pkt) {
|
|||||||
t0 = millis();
|
t0 = millis();
|
||||||
}
|
}
|
||||||
LoRa.idle();
|
LoRa.idle();
|
||||||
uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2];
|
|
||||||
size_t idx = 0;
|
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.msg_kind);
|
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short >> 8);
|
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short & 0xFF);
|
|
||||||
|
|
||||||
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(&buffer[idx], pkt.payload, pkt.payload_len);
|
uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2];
|
||||||
idx += pkt.payload_len;
|
size_t frame_len = 0;
|
||||||
|
if (!lora_build_frame(static_cast<uint8_t>(pkt.msg_kind), pkt.device_id_short, pkt.payload, pkt.payload_len,
|
||||||
uint16_t crc = crc16_ccitt(buffer, idx);
|
buffer, sizeof(buffer), frame_len)) {
|
||||||
buffer[idx++] = static_cast<uint8_t>(crc >> 8);
|
return false;
|
||||||
buffer[idx++] = static_cast<uint8_t>(crc & 0xFF);
|
}
|
||||||
|
|
||||||
LoRa.beginPacket();
|
LoRa.beginPacket();
|
||||||
LoRa.write(buffer, idx);
|
LoRa.write(buffer, frame_len);
|
||||||
int result = LoRa.endPacket(false);
|
int result = LoRa.endPacket(false);
|
||||||
bool ok = result == 1;
|
bool ok = result == 1;
|
||||||
if (SERIAL_DEBUG_MODE) {
|
if (SERIAL_DEBUG_MODE) {
|
||||||
uint32_t tx_ms = millis() - t0;
|
uint32_t tx_ms = millis() - t0;
|
||||||
if (!ok || tx_ms > 2000) {
|
if (!ok || tx_ms > 2000) {
|
||||||
Serial.printf("lora_tx: len=%u total=%lums ok=%u\n",
|
Serial.printf("lora_tx: len=%u total=%lums ok=%u\n",
|
||||||
static_cast<unsigned>(idx),
|
static_cast<unsigned>(frame_len),
|
||||||
static_cast<unsigned long>(tx_ms),
|
static_cast<unsigned long>(tx_ms),
|
||||||
ok ? 1U : 0U);
|
ok ? 1U : 0U);
|
||||||
}
|
}
|
||||||
@@ -141,26 +121,33 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t crc_calc = crc16_ccitt(buffer, len - 2);
|
uint8_t msg_kind = 0;
|
||||||
uint16_t crc_rx = static_cast<uint16_t>(buffer[len - 2] << 8) | buffer[len - 1];
|
uint16_t device_id_short = 0;
|
||||||
if (crc_calc != crc_rx) {
|
size_t payload_len = 0;
|
||||||
|
LoraFrameDecodeStatus status = lora_parse_frame(
|
||||||
|
buffer, len, static_cast<uint8_t>(LoraMsgKind::AckDown), &msg_kind, &device_id_short,
|
||||||
|
pkt.payload, sizeof(pkt.payload), &payload_len);
|
||||||
|
|
||||||
|
if (status == LoraFrameDecodeStatus::CrcFail) {
|
||||||
note_reject(RxRejectReason::CrcFail);
|
note_reject(RxRejectReason::CrcFail);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uint8_t msg_kind = buffer[0];
|
if (status == LoraFrameDecodeStatus::InvalidMsgKind) {
|
||||||
if (msg_kind > static_cast<uint8_t>(LoraMsgKind::AckDown)) {
|
|
||||||
note_reject(RxRejectReason::InvalidMsgKind);
|
note_reject(RxRejectReason::InvalidMsgKind);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (status == LoraFrameDecodeStatus::LengthMismatch) {
|
||||||
|
note_reject(RxRejectReason::LengthMismatch);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
pkt.msg_kind = static_cast<LoraMsgKind>(msg_kind);
|
pkt.msg_kind = static_cast<LoraMsgKind>(msg_kind);
|
||||||
pkt.device_id_short = static_cast<uint16_t>(buffer[1] << 8) | buffer[2];
|
pkt.device_id_short = device_id_short;
|
||||||
pkt.payload_len = len - 5;
|
pkt.payload_len = payload_len;
|
||||||
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
||||||
note_reject(RxRejectReason::LengthMismatch);
|
note_reject(RxRejectReason::LengthMismatch);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memcpy(pkt.payload, &buffer[3], pkt.payload_len);
|
|
||||||
pkt.rssi_dbm = g_last_rx_rssi_dbm;
|
pkt.rssi_dbm = g_last_rx_rssi_dbm;
|
||||||
pkt.snr_db = g_last_rx_snr_db;
|
pkt.snr_db = g_last_rx_snr_db;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "batch_reassembly_logic.h"
|
||||||
#include "display_ui.h"
|
#include "display_ui.h"
|
||||||
#include "json_codec.h"
|
#include "json_codec.h"
|
||||||
#include "lora_transport.h"
|
#include "lora_transport.h"
|
||||||
@@ -90,19 +91,8 @@ static bool mqtt_publish_sample(const MeterData &data) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BatchRxState {
|
static BatchReassemblyState g_batch_rx = {};
|
||||||
bool active;
|
static uint8_t g_batch_rx_buffer[BATCH_MAX_COMPRESSED] = {};
|
||||||
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 void init_sender_statuses() {
|
static void init_sender_statuses() {
|
||||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
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() {
|
static void reset_batch_rx() {
|
||||||
g_batch_rx.active = false;
|
batch_reassembly_reset(g_batch_rx);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool process_batch_packet(const LoraPacket &pkt, BatchInput &out_batch, bool &decode_error, uint16_t &out_batch_id) {
|
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;
|
size_t chunk_len = pkt.payload_len - BATCH_HEADER_SIZE;
|
||||||
uint32_t now_ms = millis();
|
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)) {
|
uint16_t complete_len = 0;
|
||||||
if (chunk_index != 0) {
|
BatchReassemblyStatus reassembly_status = batch_reassembly_push(
|
||||||
reset_batch_rx();
|
g_batch_rx, batch_id, chunk_index, chunk_count, total_len, chunk_data, chunk_len, now_ms,
|
||||||
return false;
|
compute_batch_rx_timeout_ms(total_len, chunk_count), BATCH_MAX_COMPRESSED, g_batch_rx_buffer,
|
||||||
}
|
sizeof(g_batch_rx_buffer), complete_len);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!g_batch_rx.active || chunk_index != g_batch_rx.next_index || chunk_count != g_batch_rx.expected_chunks) {
|
if (reassembly_status == BatchReassemblyStatus::ErrorReset) {
|
||||||
reset_batch_rx();
|
return false;
|
||||||
|
}
|
||||||
|
if (reassembly_status == BatchReassemblyStatus::InProgress) {
|
||||||
return false;
|
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) {
|
if (reassembly_status == BatchReassemblyStatus::Complete) {
|
||||||
reset_batch_rx();
|
if (!decode_batch(g_batch_rx_buffer, complete_len, &out_batch)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(&g_batch_rx.buffer[g_batch_rx.received_len], chunk_data, chunk_len);
|
|
||||||
g_batch_rx.received_len += static_cast<uint16_t>(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)) {
|
|
||||||
decode_error = true;
|
decode_error = true;
|
||||||
reset_batch_rx();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out_batch_id = batch_id;
|
out_batch_id = batch_id;
|
||||||
reset_batch_rx();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
131
test/test_lora_transport/test_lora_transport.cpp
Normal file
131
test/test_lora_transport/test_lora_transport.cpp
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#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<uint8_t>(LoraFrameDecodeStatus::Ok), static_cast<uint8_t>(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<uint8_t>(LoraFrameDecodeStatus::CrcFail), static_cast<uint8_t>(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<uint8_t>(LoraFrameDecodeStatus::InvalidMsgKind), static_cast<uint8_t>(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<uint8_t>(LoraFrameDecodeStatus::LengthMismatch), static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::Complete),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::InProgress),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(BatchReassemblyStatus::ErrorReset),
|
||||||
|
static_cast<uint8_t>(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() {}
|
||||||
Reference in New Issue
Block a user