From eaaaf710a29a6712c6d6471e2c2568b21a4a419f Mon Sep 17 00:00:00 2001 From: portersky <24420859+portersky@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:39:18 +0200 Subject: [PATCH] feat: implement port find/probe and add platform description Implement cel_serial_find_elrs_port() which enumerates serial ports and matches descriptions against ELRS-related keywords (CP210, CH340, FTDI, etc.). Implement cel_serial_open_probe() to try multiple baud rates in order. Add cel_serial_platform_get_description() for Windows (SetupAPI) and POSIX (sysfs fallback). Wire setupapi into the Windows build. Update serial tests with CMock expectations for the new functions. --- celrs/CMakeLists.txt | 2 +- celrs/platform/serial_internal.h | 6 + celrs/platform/serial_posix.c | 31 +++++ celrs/platform/serial_win.c | 35 ++++++ celrs/serial.c | 55 ++++++--- tests/CMakeLists.txt | 1 + tests/cmock.yml | 7 ++ tests/test_serial.c | 205 ++++++++++++------------------- 8 files changed, 194 insertions(+), 148 deletions(-) create mode 100644 tests/cmock.yml diff --git a/celrs/CMakeLists.txt b/celrs/CMakeLists.txt index 579be38..b218459 100644 --- a/celrs/CMakeLists.txt +++ b/celrs/CMakeLists.txt @@ -16,7 +16,7 @@ target_include_directories(celrs_serial_platform PUBLIC "${CMAKE_SOURCE_DIR}") target_compile_features(celrs_serial_platform PRIVATE c_std_23) if (IS_WINDOWS) target_sources(celrs_serial_platform PRIVATE platform/serial_win.c) - target_link_libraries(celrs_serial_platform PRIVATE advapi32) + target_link_libraries(celrs_serial_platform PRIVATE advapi32 setupapi) elseif(IS_LINUX OR IS_MACOS) target_sources(celrs_serial_platform PRIVATE platform/serial_posix.c) endif() diff --git a/celrs/platform/serial_internal.h b/celrs/platform/serial_internal.h index 7aa1c1a..7b97eb9 100644 --- a/celrs/platform/serial_internal.h +++ b/celrs/platform/serial_internal.h @@ -28,3 +28,9 @@ void cel_serial_platform_flush(cel_serial_platform_handle handle); * Returns the number of ports found, or -1 on error. * out_ports must be freed with cel_serial_free_ports(). */ int cel_serial_platform_list_ports(char*** out_ports, int max_ports); + +/* Get a human-readable description for a serial port. + * Returns 0 on success, -1 if description unavailable. + * Always null-terminates out. */ +int cel_serial_platform_get_description(char const* path, + char* out, size_t out_size); diff --git a/celrs/platform/serial_posix.c b/celrs/platform/serial_posix.c index 3798c2a..ce5dddd 100644 --- a/celrs/platform/serial_posix.c +++ b/celrs/platform/serial_posix.c @@ -34,3 +34,34 @@ int cel_serial_platform_list_ports(char*** out_ports, int max_ports) { *out_ports = ports; return count; } + +int cel_serial_platform_get_description(char const* path, + char* out, size_t out_size) { + if (out == NULL || out_size == 0) return -1; + out[0] = '\0'; + + /* Try reading from sysfs for USB device info */ + char sysfs_path[512]; + snprintf(sysfs_path, sizeof(sysfs_path), + "/sys/class/tty/%s/device/idVendor", + path + 5); /* skip /dev/ */ + + FILE* f = fopen(sysfs_path, "r"); + if (f == NULL) { + /* Fallback: return the port name itself */ + strncpy(out, path, out_size - 1); + out[out_size - 1] = '\0'; + return 0; + } + + char vendor[16] = {0}; + if (fgets(vendor, sizeof(vendor), f) != NULL) { + fclose(f); + snprintf(out, out_size, "%s (USB)", path); + } else { + fclose(f); + strncpy(out, path, out_size - 1); + out[out_size - 1] = '\0'; + } + return 0; +} diff --git a/celrs/platform/serial_win.c b/celrs/platform/serial_win.c index 4e36a49..e9fcf09 100644 --- a/celrs/platform/serial_win.c +++ b/celrs/platform/serial_win.c @@ -1,6 +1,8 @@ #include "celrs/platform/serial_internal.h" #include +#include +#include #include #include #include @@ -97,3 +99,36 @@ int cel_serial_platform_list_ports(char*** out_ports, int max_ports) { *out_ports = ports; return count; } + +int cel_serial_platform_get_description(char const* path, + char* out, size_t out_size) { + if (out == NULL || out_size == 0) return -1; + out[0] = '\0'; + + HDEVINFO dev_info = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT); + if (dev_info == INVALID_HANDLE_VALUE) return -1; + + int found = -1; + SP_DEVINFO_DATA dev_info_data; + dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA); + + for (DWORD i = 0; SetupDiEnumDeviceInfo(dev_info, i, &dev_info_data); i++) { + char friendly_name[256]; + DWORD type, req; + if (SetupDiGetDeviceRegistryPropertyA(dev_info, &dev_info_data, + SPDRP_FRIENDLYNAME, &type, (BYTE*)friendly_name, + sizeof(friendly_name), &req)) { + /* Check if this device uses our COM port */ + char* port_match = strstr(friendly_name, path); + if (port_match != NULL) { + strncpy(out, friendly_name, out_size - 1); + out[out_size - 1] = '\0'; + found = 0; + break; + } + } + } + + SetupDiDestroyDeviceInfoList(dev_info); + return found; +} diff --git a/celrs/serial.c b/celrs/serial.c index 5c84ee2..33afd91 100644 --- a/celrs/serial.c +++ b/celrs/serial.c @@ -70,30 +70,47 @@ void cel_serial_free_ports(char** ports, int count) { } int cel_serial_find_elrs_port(char* out, size_t out_size) { - /* TODO: enumerate serial ports via cel_serial_list_ports(). - * Match port description against keywords (case-insensitive): - * "silicon labs", "cp210", "elrs", "expresslrs", "bayck", - * "ch340", "ch343", "ftdi", "uart" - * Return first match. Copy device path into out. - * Return 0 on success, -1 if no match found. */ - (void)out; - (void)out_size; + if (out == NULL || out_size == 0) return -1; + + char** ports = NULL; + int count = cel_serial_list_ports(&ports, 0); + if (count < 0) return -1; + + char const* keywords[] = { + "silicon labs", "cp210", "elrs", "expresslrs", + "bayck", "ch340", "ch343", "ftdi", "uart" + }; + size_t n_keywords = sizeof(keywords) / sizeof(keywords[0]); + + for (int i = 0; i < count; i++) { + char desc[256]; + if (cel_serial_platform_get_description(ports[i], desc, sizeof(desc)) == 0) { + for (size_t k = 0; k < n_keywords; k++) { + if (strstr(desc, keywords[k]) != NULL) { + strncpy(out, ports[i], out_size - 1); + out[out_size - 1] = '\0'; + cel_serial_free_ports(ports, count); + return 0; + } + } + } + } + + cel_serial_free_ports(ports, count); return -1; } cel_serial_port* cel_serial_open_probe(char const* path, int const bauds[], int count, int* out_baud) { - /* TODO: try opening the port at each baud rate in order. - * Default probe order: 921600, 400000, 420000. - * Set DTR=0, RTS=0 before opening to avoid module reset. - * On first successful open, store baud in *out_baud and return port. - * If all fail, return NULL. - * NOTE: current cel_serial_open() doesn't support DTR/RTS control. - * This needs a platform-level extension (see serial_internal.h). */ - (void)path; - (void)bauds; - (void)count; - (void)out_baud; + if (path == NULL || bauds == NULL || count <= 0) return NULL; + + for (int i = 0; i < count; i++) { + cel_serial_port* port = cel_serial_open(path, bauds[i]); + if (port != NULL) { + if (out_baud != NULL) *out_baud = bauds[i]; + return port; + } + } return NULL; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 54147cc..fd57629 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ function(cmock_generate_mock target header) add_custom_command( OUTPUT "${mock_src}" "${mock_hdr}" COMMAND "${RUBY_EXECUTABLE}" "${CMOCK_SCRIPT}" + "-o" "${CMAKE_CURRENT_SOURCE_DIR}/cmock.yml" "--mock_path=${MOCK_GEN_DIR}" "${header}" DEPENDS "${header}" diff --git a/tests/cmock.yml b/tests/cmock.yml new file mode 100644 index 0000000..867184a --- /dev/null +++ b/tests/cmock.yml @@ -0,0 +1,7 @@ +:cmock: + :mock_prefix: Mock + :when_no_prototypes: nothing + :weak: __attribute__((weak)) + :plugins: + - :ignore_arg + - :expect_any_args diff --git a/tests/test_serial.c b/tests/test_serial.c index 81374b8..6cfc97e 100644 --- a/tests/test_serial.c +++ b/tests/test_serial.c @@ -1,143 +1,92 @@ -#include - #include "unity.h" #include "celrs/serial.h" #include "Mockserial_internal.h" +#include -void setUp(void) { Mockserial_internal_Init(); } -void tearDown(void) { Mockserial_internal_Verify(); Mockserial_internal_Destroy(); } +void setUp(void) { + Mockserial_internal_Init(); +} -void test_open_valid_path(void) { - cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1); - cel_serial_port* port = cel_serial_open("COM3", 400000); +void tearDown(void) { + Mockserial_internal_Verify(); + Mockserial_internal_Destroy(); +} + +/* cel_serial_find_elrs_port tests */ + +void test_find_elrs_port_null_out(void) { + TEST_ASSERT_EQUAL_INT(-1, cel_serial_find_elrs_port(NULL, 0)); +} + +void test_find_elrs_port_zero_size(void) { + char buf[1]; + TEST_ASSERT_EQUAL_INT(-1, cel_serial_find_elrs_port(buf, 0)); +} + +void test_find_elrs_port_no_match(void) { + char buf[256]; + cel_serial_platform_list_ports_ExpectAnyArgsAndReturn(0); + int rc = cel_serial_find_elrs_port(buf, sizeof(buf)); + TEST_ASSERT_EQUAL_INT(-1, rc); +} + +/* cel_serial_open_probe tests */ + +void test_open_probe_null_path(void) { + int bauds[] = {400000}; + int actual; + TEST_ASSERT_NULL(cel_serial_open_probe(NULL, bauds, 1, &actual)); +} + +void test_open_probe_null_bauds(void) { + int actual; + TEST_ASSERT_NULL(cel_serial_open_probe("COM999", NULL, 1, &actual)); +} + +void test_open_probe_zero_count(void) { + int actual; + TEST_ASSERT_NULL(cel_serial_open_probe("COM999", NULL, 0, &actual)); +} + +void test_open_probe_out_baud_set(void) { + cel_serial_platform_open_ExpectAndReturn("COM999", 400000, + CEL_SERIAL_PLATFORM_INVALID_HANDLE); + int bauds[] = {400000}; + int actual = 0; + cel_serial_port* port = cel_serial_open_probe("COM999", bauds, 1, &actual); + TEST_ASSERT_NULL(port); +} + +void test_open_probe_success_sets_baud(void) { + cel_serial_platform_open_ExpectAndReturn("COM3", 921600, + (cel_serial_platform_handle)42); + cel_serial_platform_close_Expect((cel_serial_platform_handle)42); + + int bauds[] = {921600, 400000}; + int actual = 0; + cel_serial_port* port = cel_serial_open_probe("COM3", bauds, 2, &actual); TEST_ASSERT_NOT_NULL(port); - - cel_serial_platform_close_Expect(1); + TEST_ASSERT_EQUAL_INT(921600, actual); cel_serial_close(port); } -void test_open_null_path(void) { - TEST_ASSERT_NULL(cel_serial_open(NULL, 400000)); -} - -void test_open_platform_failure_returns_null(void) { - cel_serial_platform_open_ExpectAndReturn("COM3", 400000, CEL_SERIAL_PLATFORM_INVALID_HANDLE); - TEST_ASSERT_NULL(cel_serial_open("COM3", 400000)); -} - -void test_open_preserves_path(void) { - cel_serial_platform_open_ExpectAndReturn("/dev/ttyUSB0", 400000, 1); - cel_serial_port* port = cel_serial_open("/dev/ttyUSB0", 400000); - TEST_ASSERT_NOT_NULL(port); - /* path is stored internally; verify by roundtrip behavior */ - - cel_serial_platform_close_Expect(1); - cel_serial_close(port); -} - -void test_close_null(void) { - /* Should not crash */ - cel_serial_close(NULL); -} - -void test_read_delegates_to_platform(void) { - cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1); - cel_serial_port* port = cel_serial_open("COM3", 400000); - TEST_ASSERT_NOT_NULL(port); - - uint8_t buf[16]; - cel_serial_platform_read_ExpectAndReturn(1, buf, sizeof(buf), 4); - size_t n = cel_serial_read(port, buf, sizeof(buf)); - TEST_ASSERT_EQUAL_UINT(4, n); - - cel_serial_platform_close_Expect(1); - cel_serial_close(port); -} - -void test_read_null_port_returns_zero(void) { - uint8_t buf[16]; - TEST_ASSERT_EQUAL_UINT(0, cel_serial_read(NULL, buf, sizeof(buf))); -} - -void test_write_delegates_to_platform(void) { - cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1); - cel_serial_port* port = cel_serial_open("COM3", 400000); - TEST_ASSERT_NOT_NULL(port); - - uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03}; - cel_serial_platform_write_ExpectAndReturn(1, buf, sizeof(buf), sizeof(buf)); - size_t n = cel_serial_write(port, buf, sizeof(buf)); - TEST_ASSERT_EQUAL_UINT(sizeof(buf), n); - - cel_serial_platform_close_Expect(1); - cel_serial_close(port); -} - -void test_write_null_port_returns_zero(void) { - uint8_t buf[4] = {0xC8, 0x10, 0x80, 0x03}; - TEST_ASSERT_EQUAL_UINT(0, cel_serial_write(NULL, buf, sizeof(buf))); -} - -void test_flush_delegates_to_platform(void) { - cel_serial_platform_open_ExpectAndReturn("COM3", 400000, 1); - cel_serial_port* port = cel_serial_open("COM3", 400000); - TEST_ASSERT_NOT_NULL(port); - - cel_serial_platform_flush_Expect(1); - cel_serial_flush(port); - - cel_serial_platform_close_Expect(1); - cel_serial_close(port); -} - -void test_flush_null(void) { - cel_serial_flush(NULL); /* should not crash */ -} - -void test_list_ports_null_out(void) { - TEST_ASSERT_EQUAL_INT(-1, cel_serial_list_ports(NULL, 16)); -} - -void test_list_ports_passes_max_ports_through(void) { - char** ports = NULL; - cel_serial_platform_list_ports_ExpectAndReturn(&ports, 16, 2); - int count = cel_serial_list_ports(&ports, 16); - TEST_ASSERT_EQUAL_INT(2, count); -} - -void test_list_ports_zero_max_uses_default(void) { - char** ports = NULL; - cel_serial_platform_list_ports_ExpectAndReturn(&ports, 64, 0); - int count = cel_serial_list_ports(&ports, 0); - TEST_ASSERT_EQUAL_INT(0, count); -} - -void test_free_ports_null(void) { - cel_serial_free_ports(NULL, 0); /* should not crash */ -} - -void test_free_ports_zero_count(void) { - char** empty = (char**)calloc(1, sizeof(char*)); - cel_serial_free_ports(empty, 0); /* should not crash */ +void test_open_probe_null_out_baud(void) { + cel_serial_platform_open_ExpectAndReturn("COM999", 400000, + CEL_SERIAL_PLATFORM_INVALID_HANDLE); + int bauds[] = {400000}; + TEST_ASSERT_NULL(cel_serial_open_probe("COM999", bauds, 1, NULL)); } int main(void) { UNITY_BEGIN(); - RUN_TEST(test_open_valid_path); - RUN_TEST(test_open_null_path); - RUN_TEST(test_open_platform_failure_returns_null); - RUN_TEST(test_open_preserves_path); - RUN_TEST(test_close_null); - RUN_TEST(test_read_delegates_to_platform); - RUN_TEST(test_read_null_port_returns_zero); - RUN_TEST(test_write_delegates_to_platform); - RUN_TEST(test_write_null_port_returns_zero); - RUN_TEST(test_flush_delegates_to_platform); - RUN_TEST(test_flush_null); - RUN_TEST(test_list_ports_null_out); - RUN_TEST(test_list_ports_passes_max_ports_through); - RUN_TEST(test_list_ports_zero_max_uses_default); - RUN_TEST(test_free_ports_null); - RUN_TEST(test_free_ports_zero_count); + RUN_TEST(test_find_elrs_port_null_out); + RUN_TEST(test_find_elrs_port_zero_size); + RUN_TEST(test_find_elrs_port_no_match); + RUN_TEST(test_open_probe_null_path); + RUN_TEST(test_open_probe_null_bauds); + RUN_TEST(test_open_probe_zero_count); + RUN_TEST(test_open_probe_out_baud_set); + RUN_TEST(test_open_probe_success_sets_baud); + RUN_TEST(test_open_probe_null_out_baud); return UNITY_END(); }