test: add lora frame and chunk reassembly logic suite

This commit is contained in:
2026-02-20 21:26:51 +01:00
parent cef1d184ed
commit ca2cd1880a
7 changed files with 380 additions and 91 deletions

View File

@@ -1,4 +1,5 @@
#include "lora_transport.h"
#include "lora_frame_logic.h"
#include <LoRa.h>
#include <SPI.h>
#include <math.h>
@@ -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<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() {
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<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) {
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<uint8_t>(crc >> 8);
buffer[idx++] = static_cast<uint8_t>(crc & 0xFF);
uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2];
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,
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<unsigned>(idx),
static_cast<unsigned>(frame_len),
static_cast<unsigned long>(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<uint16_t>(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<uint8_t>(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<uint8_t>(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<LoraMsgKind>(msg_kind);
pkt.device_id_short = static_cast<uint16_t>(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;

View File

@@ -5,6 +5,7 @@
#include <stdarg.h>
#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<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)) {
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;
}