From 5d182583307653bcf81875d80b11dd6f67d8768b Mon Sep 17 00:00:00 2001 From: portersky <24420859+portersky@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:50:12 +0200 Subject: [PATCH] feat: implement cel_crsf_param_parse Parse PARAM_ENTRY payload into cel_crsf_param struct. Handles TEXT_SELECT with options string and UINT8/INT8 with min/max/default/ value fields. Respects hidden flag (bit 7 of type byte). Truncates name and options to buffer limits. --- celrs/crsf_param.c | 79 ++++++++++++++---- tests/CMakeLists.txt | 8 ++ tests/test_crsf_param.c | 176 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 tests/test_crsf_param.c diff --git a/celrs/crsf_param.c b/celrs/crsf_param.c index 18a0e6e..babaf92 100644 --- a/celrs/crsf_param.c +++ b/celrs/crsf_param.c @@ -59,22 +59,65 @@ int cel_crsf_param_set_power(cel_serial_port* port, int mw, int cel_crsf_param_parse(cel_crsf_param* out, uint8_t const* payload, size_t len) { - /* TODO: parse PARAM_ENTRY payload into cel_crsf_param. - * Layout: - * [0] dest (uint8) - * [1] src (uint8) - * [2] index (uint8) - * [3] chunks_remaining (uint8) - * [4] parent (uint8) - * [5] type (uint8, bit 7 = hidden flag) - * [6..] name (null-terminated ASCII string) - * Then type-specific data after name: - * TEXT_SELECT: options (null-terminated, semicolon-separated), - * then [value][min][max][default] (1 byte each) - * UINT8/INT8: [min][max][default][value] (1 byte each) - * Minimum payload length: 6 bytes. */ - (void)out; - (void)payload; - (void)len; - return -1; + if (out == NULL || payload == NULL) return -1; + if (len < 6) return -1; + + memset(out, 0, sizeof(*out)); + + size_t offset = 0; + (void)payload[offset++]; /* dest */ + (void)payload[offset++]; /* src */ + out->index = payload[offset++]; + (void)payload[offset++]; /* chunks_remaining */ + (void)payload[offset++]; /* parent */ + uint8_t raw_type = payload[offset++]; + out->hidden = (raw_type & 0x80) ? 1 : 0; + out->type = raw_type & 0x7F; + + /* Parse name (null-terminated) */ + size_t name_start = offset; + while (offset < len && payload[offset] != '\0') offset++; + size_t name_len = offset - name_start; + if (name_len >= sizeof(out->name)) name_len = sizeof(out->name) - 1; + memcpy(out->name, payload + name_start, name_len); + out->name[name_len] = '\0'; + if (offset < len) offset++; /* skip null terminator */ + + /* Parse type-specific data */ + switch (out->type) { + case CEL_PARAM_TEXT_SELECT: { + /* Options: null-terminated, semicolon-separated string */ + size_t opts_start = offset; + while (offset < len && payload[offset] != '\0') offset++; + size_t opts_len = offset - opts_start; + if (opts_len >= sizeof(out->options)) opts_len = sizeof(out->options) - 1; + memcpy(out->options, payload + opts_start, opts_len); + out->options[opts_len] = '\0'; + if (offset < len) offset++; /* skip null terminator */ + + /* [value][min][max][default] */ + if (offset + 4 <= len) { + out->value = payload[offset++]; + out->min_val = payload[offset++]; + out->max_val = payload[offset++]; + out->default_val = payload[offset++]; + } + break; + } + case CEL_PARAM_UINT8: + case CEL_PARAM_INT8: + /* [min][max][default][value] */ + if (offset + 4 <= len) { + out->min_val = payload[offset++]; + out->max_val = payload[offset++]; + out->default_val = payload[offset++]; + out->value = payload[offset++]; + } + break; + default: + /* Other types have no simple value representation */ + break; + } + + return 0; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fd57629..c9d93b5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,6 +55,14 @@ target_compile_features(test_crsf_telemetry PRIVATE c_std_23) add_test(NAME test_crsf_telemetry COMMAND test_crsf_telemetry) list(APPEND TEST_TARGETS test_crsf_telemetry) +# CRSF param tests — pure functions (parse), no mock needed +add_executable(test_crsf_param test_crsf_param.c) +target_include_directories(test_crsf_param PRIVATE "${CMAKE_SOURCE_DIR}") +target_link_libraries(test_crsf_param PRIVATE celrs_crsf Unity::Unity) +target_compile_features(test_crsf_param PRIVATE c_std_23) +add_test(NAME test_crsf_param COMMAND test_crsf_param) +list(APPEND TEST_TARGETS test_crsf_param) + # 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}") diff --git a/tests/test_crsf_param.c b/tests/test_crsf_param.c new file mode 100644 index 0000000..a5c85ae --- /dev/null +++ b/tests/test_crsf_param.c @@ -0,0 +1,176 @@ +#include "unity.h" +#include "celrs/crsf_param.h" +#include + +void setUp(void) {} +void tearDown(void) {} + +/* cel_crsf_param_parse tests */ + +void test_param_parse_null_args(void) { + uint8_t payload[16] = {0}; + TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(NULL, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(NULL, NULL, 0)); +} + +void test_param_parse_too_short(void) { + cel_crsf_param param; + uint8_t payload[5] = {0}; + TEST_ASSERT_EQUAL_INT(-1, cel_crsf_param_parse(¶m, payload, sizeof(payload))); +} + +void test_param_parse_text_select(void) { + cel_crsf_param param; + /* dest=0x10, src=0xEE, index=5, chunks=0, parent=0, type=TEXT_SELECT */ + /* name="TX Power", options="10 mW;25 mW;100 mW;500 mW;1000 mW", value=2, min=0, max=4, default=0 */ + uint8_t payload[] = { + 0x10, /* dest */ + 0xEE, /* src */ + 0x05, /* index */ + 0x00, /* chunks_remaining */ + 0x00, /* parent */ + 0x09, /* type = CEL_PARAM_TEXT_SELECT */ + 'T', 'X', ' ', 'P', 'o', 'w', 'e', 'r', '\0', /* name */ + '1', '0', ' ', 'm', 'W', ';', /* options */ + '2', '5', ' ', 'm', 'W', ';', + '1', '0', '0', ' ', 'm', 'W', ';', + '5', '0', '0', ' ', 'm', 'W', ';', + '1', '0', '0', '0', ' ', 'm', 'W', '\0', /* end of options */ + 0x02, /* value */ + 0x00, /* min */ + 0x04, /* max */ + 0x00, /* default */ + }; + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(5, param.index); + TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_TEXT_SELECT, param.type); + TEST_ASSERT_EQUAL_UINT8(0, param.hidden); + TEST_ASSERT_EQUAL_STRING("TX Power", param.name); + TEST_ASSERT_EQUAL_STRING("10 mW;25 mW;100 mW;500 mW;1000 mW", param.options); + TEST_ASSERT_EQUAL_UINT8(2, param.value); + TEST_ASSERT_EQUAL_UINT8(0, param.min_val); + TEST_ASSERT_EQUAL_UINT8(4, param.max_val); + TEST_ASSERT_EQUAL_UINT8(0, param.default_val); +} + +void test_param_parse_hidden_flag(void) { + cel_crsf_param param; + uint8_t payload[] = { + 0x10, 0xEE, 0x00, 0x00, 0x00, + 0x88, /* type with hidden bit set (0x80 | 0x08) */ + 'H', 'i', 'd', 'd', 'e', 'n', '\0', + 'A', ';', 'B', '\0', + 0x00, 0x00, 0x01, 0x00, + }; + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_FLOAT, param.type); + TEST_ASSERT_EQUAL_UINT8(1, param.hidden); +} + +void test_param_parse_uint8_type(void) { + cel_crsf_param param; + uint8_t payload[] = { + 0x10, 0xEE, 0x10, 0x00, 0x00, + 0x00, /* type = CEL_PARAM_UINT8 */ + 'V', 'a', 'l', '\0', /* name */ + 0x00, /* min */ + 0xFF, /* max */ + 0x80, /* default */ + 0x42, /* value */ + }; + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(0x10, param.index); + TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_UINT8, param.type); + TEST_ASSERT_EQUAL_STRING("Val", param.name); + TEST_ASSERT_EQUAL_UINT8(0x42, param.value); + TEST_ASSERT_EQUAL_UINT8(0x00, param.min_val); + TEST_ASSERT_EQUAL_UINT8(0xFF, param.max_val); + TEST_ASSERT_EQUAL_UINT8(0x80, param.default_val); +} + +void test_param_parse_int8_type(void) { + cel_crsf_param param; + uint8_t payload[] = { + 0x10, 0xEE, 0x01, 0x00, 0x00, + 0x01, /* type = CEL_PARAM_INT8 */ + 'S', '\0', + 0x80, /* min (-128) */ + 0x7F, /* max (127) */ + 0x00, /* default */ + 0x10, /* value */ + }; + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_INT8, param.type); + TEST_ASSERT_EQUAL_UINT8(0x10, param.value); +} + +void test_param_parse_folder(void) { + cel_crsf_param param; + uint8_t payload[] = { + 0x10, 0xEE, 0xFF, 0x00, 0x00, + 0x0B, /* type = CEL_PARAM_FOLDER */ + 'F', 'o', 'l', 'd', 'e', 'r', '\0', + }; + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(CEL_PARAM_FOLDER, param.type); + TEST_ASSERT_EQUAL_STRING("Folder", param.name); +} + +void test_param_parse_name_truncation(void) { + cel_crsf_param param; + /* Name longer than 63 chars (should be truncated) */ + uint8_t payload[128] = {0}; + payload[0] = 0x10; /* dest */ + payload[1] = 0xEE; /* src */ + payload[2] = 0x00; /* index */ + payload[3] = 0x00; /* chunks */ + payload[4] = 0x00; /* parent */ + payload[5] = 0x0B; /* type = FOLDER */ + /* Fill name with 70 'A' chars */ + for (int i = 0; i < 70; i++) payload[6 + i] = 'A'; + payload[6 + 70] = '\0'; + + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(63, strlen(param.name)); /* truncated to 63 */ +} + +void test_param_parse_options_truncation(void) { + cel_crsf_param param; + /* Options longer than 255 chars (should be truncated) */ + uint8_t payload[400] = {0}; + payload[0] = 0x10; /* dest */ + payload[1] = 0xEE; /* src */ + payload[2] = 0x00; /* index */ + payload[3] = 0x00; /* chunks */ + payload[4] = 0x00; /* parent */ + payload[5] = 0x09; /* type = TEXT_SELECT */ + strcpy((char*)(payload + 6), "Name"); + payload[6 + 4] = '\0'; + /* Fill options with 300 'O' chars */ + size_t opts_start = 6 + 5; /* after name */ + for (int i = 0; i < 299; i++) payload[opts_start + i] = 'O'; + payload[opts_start + 299] = '\0'; + /* Add type-specific data after options */ + size_t after_opts = opts_start + 300; + payload[after_opts] = 0x00; /* value */ + payload[after_opts + 1] = 0x00; /* min */ + payload[after_opts + 2] = 0x00; /* max */ + payload[after_opts + 3] = 0x00; /* default */ + + TEST_ASSERT_EQUAL_INT(0, cel_crsf_param_parse(¶m, payload, sizeof(payload))); + TEST_ASSERT_EQUAL_UINT8(255, strlen(param.options)); /* truncated to 255 */ +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_param_parse_null_args); + RUN_TEST(test_param_parse_too_short); + RUN_TEST(test_param_parse_text_select); + RUN_TEST(test_param_parse_hidden_flag); + RUN_TEST(test_param_parse_uint8_type); + RUN_TEST(test_param_parse_int8_type); + RUN_TEST(test_param_parse_folder); + RUN_TEST(test_param_parse_name_truncation); + RUN_TEST(test_param_parse_options_truncation); + return UNITY_END(); +}