feat: implement frame parse and streaming reader

cel_crsf_frame_parse() parses ELRS USB format frames:
  [addr][length][type][payload...][crc]

cel_crsf_stream_* provides incremental parsing from a byte
stream: skips invalid sync bytes, discards bad CRC frames,
buffers partial frames across feed calls.
This commit is contained in:
2026-06-14 20:51:57 +02:00
parent dde27ab566
commit a846b063f9
5 changed files with 335 additions and 36 deletions
+8
View File
@@ -38,6 +38,14 @@ target_compile_features(test_crsf PRIVATE c_std_23)
add_test(NAME test_crsf COMMAND test_crsf)
list(APPEND TEST_TARGETS test_crsf)
# CRSF stream tests
add_executable(test_crsf_stream test_crsf_stream.c)
target_include_directories(test_crsf_stream PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_crsf_stream PRIVATE celrs_crsf Unity::Unity)
target_compile_features(test_crsf_stream PRIVATE c_std_23)
add_test(NAME test_crsf_stream COMMAND test_crsf_stream)
list(APPEND TEST_TARGETS test_crsf_stream)
# Serial tests — mocks the platform backend (serial_internal.h)
add_executable(test_serial test_serial.c)
target_include_directories(test_serial PRIVATE "${CMAKE_SOURCE_DIR}")
+84 -1
View File
@@ -18,7 +18,6 @@ void test_crc_single_byte(void) {
}
void test_crc_known_value(void) {
/* Verify CRC is deterministic */
uint8_t data[6] = {0x10, 0x80, 0x03, 0x02, 0x80, 0x01};
uint8_t crc = cel_crsf_crc(data, 6);
TEST_ASSERT_TRUE(crc != 0);
@@ -39,6 +38,83 @@ void test_channel_clamp_mid(void) {
TEST_ASSERT_EQUAL_INT16(CEL_CRSF_CH_MID, cel_crsf_channel_clamp(CEL_CRSF_CH_MID));
}
/* Frame parse tests — ELRS format: [addr][length][type][payload][crc] */
/* Build a valid test frame with known CRC */
static void build_test_frame(uint8_t* dst, uint8_t addr, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1; /* type + payload + crc */
dst[0] = addr;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
}
void test_parse_valid_frame(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
build_test_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_UINT8(0xC8, frame.addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frame.type);
TEST_ASSERT_EQUAL_UINT8(2, frame.payload_len);
TEST_ASSERT_EQUAL_UINT8(0x80, frame.payload[0]);
TEST_ASSERT_EQUAL_UINT8(0x01, frame.payload[1]);
}
void test_parse_null_frame(void) {
uint8_t buf[8];
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(NULL, buf, 8));
}
void test_parse_null_buf(void) {
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, NULL, 8));
}
void test_parse_too_short(void) {
cel_crsf_frame frame;
uint8_t buf[2] = {0xC8, 0x03};
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, 2));
}
void test_parse_bad_crc(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
build_test_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
buf[5] ^= 0xFF; /* corrupt the CRC */
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(-1, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
}
void test_parse_empty_payload(void) {
uint8_t buf[32];
build_test_frame(buf, 0xEE, CEL_CRSF_TYPE_HEARTBEAT, NULL, 0);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_UINT8(0xEE, frame.addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frame.type);
TEST_ASSERT_EQUAL_UINT8(0, frame.payload_len);
}
void test_parse_module_addr(void) {
uint8_t buf[32];
uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
build_test_frame(buf, 0xEE, CEL_CRSF_TYPE_GPS, payload, 4);
cel_crsf_frame frame;
TEST_ASSERT_EQUAL_INT(0, cel_crsf_frame_parse(&frame, buf, sizeof(buf)));
TEST_ASSERT_EQUAL_UINT8(0xEE, frame.addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_GPS, frame.type);
TEST_ASSERT_EQUAL_UINT8(4, frame.payload_len);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_crc_empty);
@@ -47,5 +123,12 @@ int main(void) {
RUN_TEST(test_channel_clamp_min);
RUN_TEST(test_channel_clamp_max);
RUN_TEST(test_channel_clamp_mid);
RUN_TEST(test_parse_valid_frame);
RUN_TEST(test_parse_null_frame);
RUN_TEST(test_parse_null_buf);
RUN_TEST(test_parse_too_short);
RUN_TEST(test_parse_bad_crc);
RUN_TEST(test_parse_empty_payload);
RUN_TEST(test_parse_module_addr);
return UNITY_END();
}
+169
View File
@@ -0,0 +1,169 @@
#include "unity.h"
#include "celrs/crsf.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
/* Helper: build a valid test frame, return total bytes */
static size_t build_frame(uint8_t* dst, uint8_t addr, uint8_t type,
uint8_t const* payload, uint8_t payload_len) {
uint8_t length = 1 + payload_len + 1; /* type + payload + crc */
dst[0] = addr;
dst[1] = length;
dst[2] = type;
memcpy(dst + 3, payload, payload_len);
uint8_t crc = cel_crsf_crc(dst + 2, 1 + payload_len);
dst[2 + length - 1] = crc;
return 2 + length;
}
/* Stream tests */
void test_stream_create_destroy(void) {
cel_crsf_stream* s = cel_crsf_stream_create();
TEST_ASSERT_NOT_NULL(s);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_empty(void) {
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, NULL, 0, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_single_frame(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
TEST_ASSERT_EQUAL_UINT8(2, frames[0].payload_len);
TEST_ASSERT_EQUAL_UINT8(0x80, frames[0].payload[0]);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_incremental(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t total = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
/* Feed first 3 bytes — not enough for a complete frame */
int n = cel_crsf_stream_feed(s, buf, 3, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n);
/* Feed remaining bytes — now complete */
n = cel_crsf_stream_feed(s, buf + 3, total - 3, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_multiple_frames(void) {
uint8_t buf[64];
uint8_t p1[2] = {0x80, 0x01};
uint8_t p2[3] = {0xAA, 0xBB, 0xCC};
size_t len1 = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, p1, 2);
size_t len2 = build_frame(buf + len1, 0xEE, CEL_CRSF_TYPE_GPS, p2, 3);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len1 + len2, frames, 4);
TEST_ASSERT_EQUAL_INT(2, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_HEARTBEAT, frames[0].type);
TEST_ASSERT_EQUAL_UINT8(0xEE, frames[1].addr);
TEST_ASSERT_EQUAL_UINT8(CEL_CRSF_TYPE_GPS, frames[1].type);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_skip_bad_sync(void) {
uint8_t buf[48];
/* Garbage bytes before valid frame */
buf[0] = 0xFF;
buf[1] = 0xFE;
buf[2] = 0xFD;
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf + 3, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, 3 + len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT8(0xC8, frames[0].addr);
cel_crsf_stream_destroy(s);
}
void test_stream_feed_discard_bad_crc(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
buf[len - 1] ^= 0xFF; /* corrupt CRC */
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[4];
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(0, n); /* bad frame discarded */
cel_crsf_stream_destroy(s);
}
void test_stream_feed_overflow(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
cel_crsf_frame frames[1];
/* Feed two frames but only have room for one */
size_t total = len * 2;
memcpy(buf + len, buf, len);
int n = cel_crsf_stream_feed(s, buf, total, frames, 1);
TEST_ASSERT_EQUAL_INT(1, n);
/* Second frame should still be in buffer */
n = cel_crsf_stream_feed(s, NULL, 0, frames, 1);
TEST_ASSERT_EQUAL_INT(1, n);
cel_crsf_stream_destroy(s);
}
void test_stream_reset(void) {
uint8_t buf[32];
uint8_t payload[2] = {0x80, 0x01};
size_t len = build_frame(buf, 0xC8, CEL_CRSF_TYPE_HEARTBEAT, payload, 2);
cel_crsf_stream* s = cel_crsf_stream_create();
/* Feed partial frame */
cel_crsf_frame frames[4];
cel_crsf_stream_feed(s, buf, 3, frames, 4);
/* Reset should discard partial */
cel_crsf_stream_reset(s);
/* Feed complete frame — should parse normally */
int n = cel_crsf_stream_feed(s, buf, len, frames, 4);
TEST_ASSERT_EQUAL_INT(1, n);
cel_crsf_stream_destroy(s);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_stream_create_destroy);
RUN_TEST(test_stream_feed_empty);
RUN_TEST(test_stream_feed_single_frame);
RUN_TEST(test_stream_feed_incremental);
RUN_TEST(test_stream_feed_multiple_frames);
RUN_TEST(test_stream_feed_skip_bad_sync);
RUN_TEST(test_stream_feed_discard_bad_crc);
RUN_TEST(test_stream_feed_overflow);
RUN_TEST(test_stream_reset);
return UNITY_END();
}