diff --git a/celrs/CMakeLists.txt b/celrs/CMakeLists.txt index 30397f1..4ba593f 100644 --- a/celrs/CMakeLists.txt +++ b/celrs/CMakeLists.txt @@ -5,6 +5,12 @@ target_compile_features(celrs_crsf PRIVATE c_std_23) add_library(celrs_serial STATIC serial.c) target_include_directories(celrs_serial PUBLIC "${CMAKE_SOURCE_DIR}") target_compile_features(celrs_serial PRIVATE c_std_23) +if (IS_WINDOWS) + target_sources(celrs_serial PRIVATE platform/serial_win.c) + target_link_libraries(celrs_serial PRIVATE advapi32) +elseif(IS_LINUX OR IS_MACOS) + target_sources(celrs_serial PRIVATE platform/serial_posix.c) +endif() # Level-filtering logger — calls log_write(); symbol resolved by the final binary add_library(celrs_logger STATIC logger.c) diff --git a/celrs/platform/serial_internal.h b/celrs/platform/serial_internal.h new file mode 100644 index 0000000..406a311 --- /dev/null +++ b/celrs/platform/serial_internal.h @@ -0,0 +1,7 @@ +#pragma once +#include + +/* Platform-specific port enumeration. + * 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); diff --git a/celrs/platform/serial_posix.c b/celrs/platform/serial_posix.c new file mode 100644 index 0000000..3798c2a --- /dev/null +++ b/celrs/platform/serial_posix.c @@ -0,0 +1,36 @@ +#include "celrs/platform/serial_internal.h" + +#include +#include +#include +#include + +int cel_serial_platform_list_ports(char*** out_ports, int max_ports) { + char** ports = (char**)calloc(max_ports, sizeof(char*)); + if (ports == NULL) return -1; + + int count = 0; + DIR* dir = opendir("/dev"); + if (dir != NULL) { + struct dirent* entry; + while ((entry = readdir(dir)) != NULL && count < max_ports) { + if (strncmp(entry->d_name, "tty", 3) == 0) { + char* rest = entry->d_name + 3; + if (strncmp(rest, "USB", 3) == 0 || + strncmp(rest, "ACM", 3) == 0) { + char path[256]; + snprintf(path, sizeof(path), "/dev/%s", entry->d_name); + + if (access(path, R_OK | W_OK) == 0) { + ports[count] = strdup(path); + if (ports[count] != NULL) count++; + } + } + } + } + closedir(dir); + } + + *out_ports = ports; + return count; +} diff --git a/celrs/platform/serial_win.c b/celrs/platform/serial_win.c new file mode 100644 index 0000000..0e85b34 --- /dev/null +++ b/celrs/platform/serial_win.c @@ -0,0 +1,41 @@ +#include "celrs/platform/serial_internal.h" + +#include +#include +#include + +/* Enumerate active COM ports via HKLM\HARDWARE\DEVICEMAP\SERIALCOMM. + * Windows keeps one value per active port in this key, so this is a + * single registry read instead of probing COM1..COM255 with CreateFile. */ +int cel_serial_platform_list_ports(char*** out_ports, int max_ports) { + char** ports = (char**)calloc(max_ports, sizeof(char*)); + if (ports == NULL) return -1; + + int count = 0; + + HKEY key; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", + 0, KEY_READ, &key) == ERROR_SUCCESS) { + for (DWORD index = 0; count < max_ports; index++) { + char value_name[256]; + char port_name[256]; + DWORD value_name_len = sizeof(value_name); + DWORD port_name_len = sizeof(port_name); + DWORD type; + + LONG result = RegEnumValueA(key, index, value_name, &value_name_len, + NULL, &type, (BYTE*)port_name, &port_name_len); + if (result != ERROR_SUCCESS) break; + + if (type == REG_SZ) { + ports[count] = _strdup(port_name); + if (ports[count] != NULL) count++; + } + } + + RegCloseKey(key); + } + + *out_ports = ports; + return count; +} diff --git a/celrs/serial.c b/celrs/serial.c index dcb89de..f0061c4 100644 --- a/celrs/serial.c +++ b/celrs/serial.c @@ -3,16 +3,7 @@ #endif #include "celrs/serial.h" - -/* - * Platform-agnostic serial port implementation. - * - * Windows uses Win32 CreateFile/ReadFile/WriteFile. - * POSIX uses open/read/write with termios. - * - * This is a stub implementation that compiles but does nothing. - * Real platform-specific code will be added when TDD tests drive it. - */ +#include "celrs/platform/serial_internal.h" #include #include @@ -67,3 +58,17 @@ void cel_serial_flush(cel_serial_port* port) { (void)port; /* TODO: platform-specific flush */ } + +int cel_serial_list_ports(char*** out_ports, int max_ports) { + if (out_ports == NULL) return -1; + if (max_ports <= 0) max_ports = 64; + return cel_serial_platform_list_ports(out_ports, max_ports); +} + +void cel_serial_free_ports(char** ports, int count) { + if (ports == NULL) return; + for (int i = 0; i < count; i++) { + free(ports[i]); + } + free(ports); +} diff --git a/celrs/serial.h b/celrs/serial.h index 8972b66..b4fa539 100644 --- a/celrs/serial.h +++ b/celrs/serial.h @@ -22,3 +22,11 @@ size_t cel_serial_write(cel_serial_port* port, unsigned char const* buf, size_t /* Flush output buffer */ void cel_serial_flush(cel_serial_port* port); + +/* List available serial ports. + * Returns the number of ports found, or -1 on error. + * Caller must free the returned array with cel_serial_free_ports(). */ +int cel_serial_list_ports(char*** out_ports, int max_ports); + +/* Free the port list returned by cel_serial_list_ports */ +void cel_serial_free_ports(char** ports, int count); diff --git a/tests/test_serial.c b/tests/test_serial.c index dc95205..d8697e0 100644 --- a/tests/test_serial.c +++ b/tests/test_serial.c @@ -1,3 +1,5 @@ +#include + #include "unity.h" #include "celrs/serial.h" #include "Mocklog_write.h" @@ -58,6 +60,34 @@ 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_runs_without_crash(void) { + char** ports = NULL; + int count = cel_serial_list_ports(&ports, 16); + /* count >= 0 is valid (may be 0 if no ports available) */ + TEST_ASSERT_GREATER_OR_EQUAL_INT(0, count); + cel_serial_free_ports(ports, count); +} + +void test_list_ports_zero_max_uses_default(void) { + char** ports = NULL; + int count = cel_serial_list_ports(&ports, 0); + TEST_ASSERT_GREATER_OR_EQUAL_INT(0, count); + cel_serial_free_ports(ports, 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 */ +} + int main(void) { UNITY_BEGIN(); RUN_TEST(test_open_valid_path); @@ -68,5 +98,10 @@ int main(void) { RUN_TEST(test_write_returns_zero_stub); RUN_TEST(test_flush_no_crash); RUN_TEST(test_flush_null); + RUN_TEST(test_list_ports_null_out); + RUN_TEST(test_list_ports_runs_without_crash); + RUN_TEST(test_list_ports_zero_max_uses_default); + RUN_TEST(test_free_ports_null); + RUN_TEST(test_free_ports_zero_count); return UNITY_END(); } diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index f705304..3816220 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable(tool_telemetry telemetry.c) -target_include_directories(tool_telemetry PRIVATE "${CMAKE_SOURCE_DIR}") -target_compile_features(tool_telemetry PRIVATE c_std_23) -target_link_libraries(tool_telemetry PRIVATE celrs_crsf celrs_serial celrs_logger celrs_log_write) +add_executable(telemetry telemetry.c) +target_include_directories(telemetry PRIVATE "${CMAKE_SOURCE_DIR}") +target_compile_features(telemetry PRIVATE c_std_23) +target_link_libraries(telemetry PRIVATE celrs_crsf celrs_serial celrs_logger celrs_log_write) diff --git a/tools/telemetry.c b/tools/telemetry.c index 5a22636..9cadc23 100644 --- a/tools/telemetry.c +++ b/tools/telemetry.c @@ -32,33 +32,63 @@ static int telemetry_parse_link(int16_t* rssi, uint8_t* link_quality, } static void print_usage(char const* prog) { - printf("Usage: %s [interval_ms]\n", prog); - printf(" serial_port : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n"); - printf(" interval_ms : poll interval in ms (default 200)\n"); + printf("Usage: %s --port [--baudrate ] [interval_ms]\n", prog); + printf(" %s --list\n", prog); + printf(" --port : COM3 (Windows) or /dev/ttyUSB0 (Linux)\n"); + printf(" --baudrate : baud rate (default 400000)\n"); + printf(" interval_ms : poll interval in ms (default 200)\n"); + printf(" --list : list available serial ports and exit\n"); +} + +static int list_ports(void) { + char** ports = NULL; + int count = cel_serial_list_ports(&ports, 0); + if (count < 0) { + cel_log_err("Failed to list serial ports"); + return 1; + } + + for (int i = 0; i < count; i++) { + printf("%s\n", ports[i]); + } + + cel_serial_free_ports(ports, count); + return 0; } int main(int argc, char const* argv[]) { - if (argc < 2) { + char const* port_path = NULL; + int baud_rate = 400000; + int interval_ms = 200; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--list") == 0) { + return list_ports(); + } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + port_path = argv[++i]; + } else if (strcmp(argv[i], "--baudrate") == 0 && i + 1 < argc) { + baud_rate = atoi(argv[++i]); + if (baud_rate <= 0) baud_rate = 400000; + } else { + interval_ms = atoi(argv[i]); + if (interval_ms <= 0) interval_ms = 200; + } + } + + if (port_path == NULL) { print_usage(argv[0]); return 1; } - char const* port_path = argv[1]; - int interval_ms = 200; - if (argc >= 3) { - interval_ms = atoi(argv[2]); - if (interval_ms <= 0) interval_ms = 200; - } - /* Open serial port */ - cel_serial_port* port = cel_serial_open(port_path, 400000); + cel_serial_port* port = cel_serial_open(port_path, baud_rate); if (port == NULL) { cel_log_err("Failed to open serial port"); return 1; } char msg[256]; - snprintf(msg, sizeof(msg), "Connected to %s (400000 baud)", port_path); + snprintf(msg, sizeof(msg), "Connected to %s (%d baud)", port_path, baud_rate); cel_log_info(msg); /* Send heartbeat to establish CRSF link */