From b09466ce102107787a6c8bb2d27ddf29dbf84a66 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Mon, 15 Jun 2026 23:33:02 -0400 Subject: [PATCH 1/6] feat: serial-port hardware-ID classifier with unit tests --- CMakeLists.txt | 4 ++++ src/serial_enum.c | 26 ++++++++++++++++++++++++ src/serial_enum.h | 37 +++++++++++++++++++++++++++++++++ tests/serial_enum_test.c | 44 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/serial_enum.c create mode 100644 src/serial_enum.h create mode 100644 tests/serial_enum_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fdf5e7..01a8ddd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,10 @@ add_executable(ui_util_test tests/ui_util_test.c) target_include_directories(ui_util_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_test(NAME ui_util COMMAND ui_util_test) +add_executable(serial_enum_test tests/serial_enum_test.c src/serial_enum.c) +target_include_directories(serial_enum_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +add_test(NAME serial_enum COMMAND serial_enum_test) + # Ferrum Software-API wire framing: verbatim echo, ">>> " prompt, async # callback format, NUL-safe button byte, case-insensitive bool (ferrumSpec.md). add_executable(ferrum_framing_test tests/ferrum_framing_test.c src/ferrum_parser.c) diff --git a/src/serial_enum.c b/src/serial_enum.c new file mode 100644 index 0000000..4a6937f --- /dev/null +++ b/src/serial_enum.c @@ -0,0 +1,26 @@ +/* serial_enum.c — pure hardware-ID classifier (always compiled). */ +#include "serial_enum.h" +#include + +/* Case-insensitive substring search. Returns 1 if `needle` is in `hay`. */ +static int ci_contains(const char *hay, const char *needle) { + if (!hay || !needle || !*needle) return 0; + for (; *hay; hay++) { + const char *h = hay, *n = needle; + while (*h && *n && + tolower((unsigned char)*h) == tolower((unsigned char)*n)) { + h++; n++; + } + if (!*n) return 1; + } + return 0; +} + +port_class_t serial_classify_hwid(const char *hwid) { + if (!hwid || !*hwid) return PORT_OTHER; + if (ci_contains(hwid, "VID_1A86")) return PORT_FIRMWARE; + if (ci_contains(hwid, "COM0COM") || + ci_contains(hwid, "CNCA") || + ci_contains(hwid, "CNCB")) return PORT_COM0COM; + return PORT_OTHER; +} diff --git a/src/serial_enum.h b/src/serial_enum.h new file mode 100644 index 0000000..7255cde --- /dev/null +++ b/src/serial_enum.h @@ -0,0 +1,37 @@ +/* + * serial_enum.h — enumerate serial/COM ports and classify them. + * + * The classifier (serial_classify_hwid) is pure and always compiled, so it can + * be unit-tested on any host. The enumerator (serial_enum) is Windows-only in + * practice; a stub returns 0 on other platforms. + */ +#ifndef HURRA_SERIAL_ENUM_H +#define HURRA_SERIAL_ENUM_H + +#include + +typedef enum { + PORT_OTHER = 0, /* unknown / unrelated COM port */ + PORT_FIRMWARE, /* WCH CH343 (VID_1A86) — the Hurra device */ + PORT_COM0COM /* com0com virtual pair end */ +} port_class_t; + +typedef struct { + char name[32]; /* "COM5" */ + char friendly[128]; /* "USB-SERIAL CH343 (COM5)" */ + port_class_t klass; +} serial_cand_t; + +/* Classify a Win32 hardware-ID string. Pure; NULL or "" -> PORT_OTHER. + * Match is case-insensitive substring: + * "VID_1A86" -> PORT_FIRMWARE + * "COM0COM" | "CNCA" | "CNCB" -> PORT_COM0COM + * otherwise -> PORT_OTHER + */ +port_class_t serial_classify_hwid(const char *hwid); + +/* Enumerate present COM ports, newest API available per platform. Fills up to + * `max` candidates, returns the count. Returns 0 on failure or non-Windows. */ +size_t serial_enum(serial_cand_t *out, size_t max); + +#endif /* HURRA_SERIAL_ENUM_H */ diff --git a/tests/serial_enum_test.c b/tests/serial_enum_test.c new file mode 100644 index 0000000..17f8cf2 --- /dev/null +++ b/tests/serial_enum_test.c @@ -0,0 +1,44 @@ +/* Unit tests for src/serial_enum.c classifier. */ +#include "serial_enum.h" +#include + +static int g_fail = 0; +#define CHECK_CLASS(hwid, want) do { \ + port_class_t got_ = serial_classify_hwid(hwid); \ + if (got_ != (want)) { \ + printf("FAIL %s:%d classify(%s) == %d, want %d\n", \ + __FILE__, __LINE__, (hwid) ? (hwid) : "(null)", \ + (int)got_, (int)(want)); \ + g_fail = 1; \ + } \ +} while (0) + +static void test_firmware(void) { + CHECK_CLASS("USB\\VID_1A86&PID_55D3", PORT_FIRMWARE); + CHECK_CLASS("usb\\vid_1a86&pid_7523", PORT_FIRMWARE); /* lowercase */ + CHECK_CLASS("FTDIBUS\\VID_1A86+ABC", PORT_FIRMWARE); + CHECK_CLASS("VID_1A86\\COM0COM\\CNCA0", PORT_FIRMWARE); /* firmware wins priority */ +} + +static void test_com0com(void) { + CHECK_CLASS("com0com\\port\\CNCA0", PORT_COM0COM); + CHECK_CLASS("COM0COM\\PORT\\CNCB0", PORT_COM0COM); + CHECK_CLASS("something-CNCA-here", PORT_COM0COM); + CHECK_CLASS("something-cncb-here", PORT_COM0COM); +} + +static void test_other(void) { + CHECK_CLASS("USB\\VID_0403&PID_6001", PORT_OTHER); /* FTDI */ + CHECK_CLASS("ACPI\\PNP0501", PORT_OTHER); + CHECK_CLASS("", PORT_OTHER); + CHECK_CLASS(NULL, PORT_OTHER); +} + +int main(void) { + test_firmware(); + test_com0com(); + test_other(); + if (g_fail) { printf("TESTS FAILED\n"); return 1; } + printf("ALL TESTS PASSED\n"); + return 0; +} From 6e96887d0685312e6e9bc360d6cbc15a4cc166fc Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Mon, 15 Jun 2026 23:38:25 -0400 Subject: [PATCH 2/6] feat: SetupAPI COM-port enumerator (Windows) + non-Windows stub --- CMakeLists.txt | 5 ++- src/serial_enum_stub.c | 7 ++++ src/serial_enum_win32.c | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/serial_enum_stub.c create mode 100644 src/serial_enum_win32.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 01a8ddd..fe38fd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,12 +53,15 @@ set(BRIDGE_SOURCES src/frontend_kmbox.c src/kmbox_codec.c ) +list(APPEND BRIDGE_SOURCES src/serial_enum.c) if(WIN32) list(APPEND BRIDGE_SOURCES src/virtual_port_win32.c) list(APPEND BRIDGE_SOURCES src/udp_socket_win32.c) + list(APPEND BRIDGE_SOURCES src/serial_enum_win32.c) else() list(APPEND BRIDGE_SOURCES src/virtual_port_unix.c) list(APPEND BRIDGE_SOURCES src/udp_socket_unix.c) + list(APPEND BRIDGE_SOURCES src/serial_enum_stub.c) endif() add_executable(hurra-bridge ${BRIDGE_SOURCES}) @@ -69,7 +72,7 @@ target_include_directories(hurra-bridge ) if(WIN32) - target_link_libraries(hurra-bridge PRIVATE ws2_32) + target_link_libraries(hurra-bridge PRIVATE ws2_32 setupapi) endif() # ── examples ────────────────────────────────────────────────────────────── diff --git a/src/serial_enum_stub.c b/src/serial_enum_stub.c new file mode 100644 index 0000000..406ab2b --- /dev/null +++ b/src/serial_enum_stub.c @@ -0,0 +1,7 @@ +/* serial_enum_stub.c — serial_enum for non-Windows builds (no enumeration). */ +#include "serial_enum.h" + +size_t serial_enum(serial_cand_t *out, size_t max) { + (void)out; (void)max; + return 0; /* Unix uses glob-based discover_devices() in bridge.c instead. */ +} diff --git a/src/serial_enum_win32.c b/src/serial_enum_win32.c new file mode 100644 index 0000000..de7a441 --- /dev/null +++ b/src/serial_enum_win32.c @@ -0,0 +1,81 @@ +/* + * serial_enum_win32.c — enumerate COM ports via SetupAPI. + * + * Walks GUID_DEVCLASS_PORTS for present devices, reads each port's COM name + * (Device Parameters\PortName), friendly name, and hardware ID, and classifies + * it with serial_classify_hwid. Any per-device failure skips that device; a + * whole-enumeration failure returns 0 so callers fall back to explicit flags. + */ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include "serial_enum.h" + +#include +#include + +/* Read the "PortName" value (e.g. "COM5") from a device's hardware reg key. */ +static int read_port_name(HDEVINFO set, SP_DEVINFO_DATA *dev, + char *out, size_t cap) { + HKEY k = SetupDiOpenDevRegKey(set, dev, DICS_FLAG_GLOBAL, 0, + DIREG_DEV, KEY_READ); + if (k == INVALID_HANDLE_VALUE) return 0; + DWORD type = 0, len = (DWORD)cap; + LONG r = RegQueryValueExA(k, "PortName", NULL, &type, + (LPBYTE)out, &len); + RegCloseKey(k); + if (r != ERROR_SUCCESS || type != REG_SZ) return 0; + /* out[cap-1]='\0' covers both a non-terminated REG_SZ value and exact-fill + * truncation — fine here since COM port names are only a few chars. */ + out[cap - 1] = '\0'; + return out[0] != '\0'; +} + +/* Read a string device property. SPDRP_FRIENDLYNAME is REG_SZ; SPDRP_HARDWAREID + * is REG_MULTI_SZ (a \0-separated list). In both cases the first C-string in the + * buffer is what we want, and serial_classify_hwid only needs that first ID + * (the most-specific one, which carries the VID). Reject other types. */ +static int get_str_prop(HDEVINFO set, SP_DEVINFO_DATA *dev, DWORD prop, + char *out, size_t cap) { + DWORD type = 0; + out[0] = '\0'; + if (!SetupDiGetDeviceRegistryPropertyA(set, dev, prop, &type, + (PBYTE)out, (DWORD)cap, NULL)) + return 0; + if (type != REG_SZ && type != REG_MULTI_SZ) { out[0] = '\0'; return 0; } + out[cap - 1] = '\0'; /* guarantee termination of the first string */ + return out[0] != '\0'; +} + +size_t serial_enum(serial_cand_t *out, size_t max) { + if (!out || max == 0) return 0; + HDEVINFO set = SetupDiGetClassDevsA(&GUID_DEVCLASS_PORTS, NULL, NULL, + DIGCF_PRESENT); + if (set == INVALID_HANDLE_VALUE) return 0; + + size_t n = 0; + SP_DEVINFO_DATA dev; + dev.cbSize = sizeof(dev); + for (DWORD i = 0; n < max && + SetupDiEnumDeviceInfo(set, i, &dev); i++) { + char name[32]; + if (!read_port_name(set, &dev, name, sizeof name)) continue; + + char hwid[256]; + get_str_prop(set, &dev, SPDRP_HARDWAREID, hwid, sizeof hwid); + + char friendly[128]; + if (!get_str_prop(set, &dev, SPDRP_FRIENDLYNAME, + friendly, sizeof friendly)) + snprintf(friendly, sizeof friendly, "%s", name); + + snprintf(out[n].name, sizeof out[n].name, "%s", name); + snprintf(out[n].friendly, sizeof out[n].friendly, "%s", friendly); + out[n].klass = serial_classify_hwid(hwid); + n++; + } + SetupDiDestroyDeviceInfoList(set); + return n; +} From 3ec0de70ae4fd083e42ce5e42791b15b70d91ddd Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Mon, 15 Jun 2026 23:57:17 -0400 Subject: [PATCH 3/6] feat: auto-select Hurra firmware COM port on Windows --- src/bridge.c | 62 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/bridge.c b/src/bridge.c index c9ab317..515642d 100644 --- a/src/bridge.c +++ b/src/bridge.c @@ -15,6 +15,7 @@ #include "frontend_kmbox.h" #include "input_core.h" #include "selector.h" +#include "serial_enum.h" #include "hurra.h" #include "hurra_types.h" #include "ui_util.h" @@ -253,15 +254,7 @@ static int parse_args(int argc, char **argv, args_t *out) { else { fprintf(stderr, "%s%s unknown option: %s%s\n", c_red(), g_bad(), a, c_rst()); usage(argv[0]); return -1; } } -#ifdef _WIN32 - if (!out->device) { - fprintf(stderr, "%s%s --device is required on Windows%s\n", - c_red(), g_bad(), c_rst()); - usage(argv[0]); - return -1; - } -#endif - /* On Unix, a missing --device triggers auto-discovery in main() (Task 7). */ + /* A missing --device triggers auto-discovery in main() (Unix: glob; Windows: SetupAPI). */ return 0; } @@ -309,6 +302,7 @@ static size_t discover_devices(dev_cand_t *out, size_t max) { } return n; } +#endif /* !_WIN32 */ /* Append formatted text to buf[cap] starting at *off, clamping so *off never * exceeds cap-1. No-op once the buffer is full. Keeps multi-append loops safe. */ @@ -323,7 +317,6 @@ static void str_appendf(char *buf, size_t cap, int *off, const char *fmt, ...) { *off += w; if ((size_t)*off >= cap) *off = (int)cap - 1; /* clamp to last valid index */ } -#endif /* !_WIN32 */ /* ── Sleep ──────────────────────────────────────────────────────────────── */ @@ -423,6 +416,50 @@ int main(int argc, char **argv) { } #endif +#ifdef _WIN32 + char auto_dev[256] = {0}; + bool device_auto = false; + if (!args.device) { + serial_cand_t cands[16]; + size_t nc = serial_enum(cands, 16); + /* Collect firmware candidates. */ + size_t fw_idx[16]; size_t nfw = 0; + for (size_t i = 0; i < nc; i++) + if (cands[i].klass == PORT_FIRMWARE) fw_idx[nfw++] = i; + + if (nfw == 1) { + snprintf(auto_dev, sizeof auto_dev, "%s", cands[fw_idx[0]].name); + args.device = auto_dev; + device_auto = true; + } else if (nfw == 0) { + char body[700]; int o = 0; + str_appendf(body, sizeof body, &o, + " Couldn't find a Hurra device (WCH CH343, VID_1A86).\n"); + if (nc) { + str_appendf(body, sizeof body, &o, " Serial ports present:\n"); + for (size_t i = 0; i < nc; i++) + str_appendf(body, sizeof body, &o, " %s %s\n", + cands[i].name, cands[i].friendly); + } + str_appendf(body, sizeof body, &o, + " -> Plug the device in (and install the WCH CH343 driver),\n" + " or force a port with: hurra-bridge.exe --device COMx"); + return bridge_fail(2, "No Hurra device found", body); + } else { + char body[700]; int o = 0; + str_appendf(body, sizeof body, &o, + " Found several Hurra-like ports:\n"); + for (size_t i = 0; i < nfw; i++) + str_appendf(body, sizeof body, &o, " %s %s\n", + cands[fw_idx[i]].name, cands[fw_idx[i]].friendly); + str_appendf(body, sizeof body, &o, + " -> Pick one, e.g.: hurra-bridge.exe --device %s", + cands[fw_idx[0]].name); + return bridge_fail(2, "Multiple Hurra devices found", body); + } + } +#endif + br.hc = hurra_open(args.device, args.baud); if (!br.hc) { char head[320], body[640]; @@ -534,14 +571,9 @@ int main(int argc, char **argv) { { char baudbuf[32]; ui_humanize_baud(args.baud, baudbuf, sizeof baudbuf); -#ifndef _WIN32 fprintf(stderr, " %s%s%s Serial device %s @ %s%s\n", c_grn(), g_ok(), c_rst(), args.device, baudbuf, device_auto ? " (auto-detected)" : ""); -#else - fprintf(stderr, " %s%s%s Serial device %s @ %s\n", - c_grn(), g_ok(), c_rst(), args.device, baudbuf); -#endif fprintf(stderr, " %s%s%s Endpoint %s\n", c_grn(), g_ok(), c_rst(), fe.describe(&fe)); } From 250202589e6298e64dfae61323c2739ace9e033b Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Tue, 16 Jun 2026 00:19:24 -0400 Subject: [PATCH 4/6] feat: auto-select com0com virtual port on Windows VCOM endpoint --- src/bridge.c | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/bridge.c b/src/bridge.c index 515642d..8f32882 100644 --- a/src/bridge.c +++ b/src/bridge.c @@ -537,12 +537,31 @@ int main(int argc, char **argv) { #ifdef _WIN32 const char *link = NULL; const char *vp_arg = args.virtual_port; + char auto_vp[32] = {0}; if (!vp_arg) { - hurra_close(br.hc); - return bridge_fail(1, "No --virtual-port given (required on Windows)", - " hurra-bridge needs a com0com virtual COM pair.\n" - " -> Install com0com, create a pair with setupc.exe (e.g. CNCA0 <-> CNCB0),\n" - " then run: hurra-bridge.exe --device COM5 --virtual-port CNCA0"); + serial_cand_t cands[16]; + size_t nc = serial_enum(cands, 16); + /* Lowest-numbered com0com end (ends are interchangeable). */ + int best = -1; long best_num = -1; + for (size_t i = 0; i < nc; i++) { + if (cands[i].klass != PORT_COM0COM) continue; + long num = -1; + for (const char *p = cands[i].name; *p; p++) + if (*p >= '0' && *p <= '9') { num = strtol(p, NULL, 10); break; } + if (best < 0 || (num >= 0 && (best_num < 0 || num < best_num))) { + best = (int)i; best_num = num; + } + } + if (best < 0) { + hurra_close(br.hc); + return bridge_fail(1, "No com0com virtual port found", + " hurra-bridge needs a com0com virtual COM pair.\n" + " -> Install com0com, create a pair with setupc.exe (e.g. CNCA0 <-> CNCB0),\n" + " then re-run, or pass --virtual-port NAME explicitly."); + } + snprintf(auto_vp, sizeof auto_vp, "%s", cands[best].name); + vp_arg = auto_vp; + blog("auto-selected com0com port %s", vp_arg); } #else const char *vp_arg = NULL; @@ -556,8 +575,14 @@ int main(int argc, char **argv) { #ifndef _WIN32 free(owned_link); #endif +#ifdef _WIN32 + return bridge_fail(1, "Can't open the com0com virtual port", + " The port may already be in use, or the name is wrong.\n" + " -> Check the pair in Device Manager, or pass --virtual-port NAME explicitly."); +#else return bridge_fail(1, "Can't create the virtual serial port (PTY)", " The kernel refused to allocate a pseudo-terminal."); +#endif } #ifndef _WIN32 /* vp_open strdups link_path internally; owned_link no longer needed. */ From 2feaffd4a7dc8faa484dca45adc8e679dc3e3425 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Tue, 16 Jun 2026 00:30:58 -0400 Subject: [PATCH 5/6] docs: document Windows COM-port auto-detection --- README.md | 14 ++++++++++---- src/bridge.c | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d9ad9d2..361e9b7 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,10 @@ Flags: | Flag | Default | Description | |---|---|---| -| `--device PATH` | _auto on Unix_ | Real serial device (e.g. `/dev/cu.usbmodem01`, `COM5`). Auto-detected on Unix when exactly one serial port is present; required on Windows. | +| `--device PATH` | _auto_ | Real serial device (e.g. `/dev/cu.usbmodem01`, `COM5`). Auto-detected when exactly one port is present (Unix: any serial port; Windows: a WCH CH343 device); pass to override. | | `--baud N` | `4000000` | Real-link baud rate. | | `--link PATH` | `$HOME/.hurra-bridge.tty` | Symlink to the PTY slave (Unix only). | -| `--virtual-port NAME` | _required on Win32_ | com0com COM name the bridge will open. | +| `--virtual-port NAME` | _auto on Win32_ | com0com COM name the bridge will open. Auto-detected when a com0com pair is present; pass to override. | | `--timeout-ms N` | `250` | Per-request timeout for get-style commands. | | `--no-color` | _off_ | Disable colored output (also honors the `NO_COLOR` env var). | | `--km-port N` | `16896` | UDP port for the KMBox Net endpoint. | @@ -143,10 +143,16 @@ Windows has no PTY equivalent, so the bridge requires a pre-configured [com0com](https://com0com.sourceforge.net/) virtual COM port pair. 1. Install com0com and run `setupc.exe` to create a pair, e.g. `CNCA0 ↔ CNCB0`. -2. Run the bridge against one end: +2. Plug in the Hurra device and run the bridge — both COM ports auto-detect: ```cmd - hurra-bridge.exe --device COM5 --baud 4000000 --virtual-port CNCA0 + hurra-bridge.exe + ``` + + To override either, pass the flag explicitly: + + ```cmd + hurra-bridge.exe --device COM5 --virtual-port CNCA0 ``` 3. Point your Ferrum-speaking tool at the other end (`CNCB0`). diff --git a/src/bridge.c b/src/bridge.c index 8f32882..0f6f51a 100644 --- a/src/bridge.c +++ b/src/bridge.c @@ -199,13 +199,13 @@ static void usage(const char *prog) { "\n" " --device PATH Real serial device (e.g. /dev/cu.usbmodem01, COM5).\n" #ifdef _WIN32 - " Required on Windows.\n" + " Optional: auto-detected (WCH CH343) when one is present.\n" #else " Optional: auto-detected when exactly one port is found.\n" #endif " --baud N Real-link baud rate. Default 4000000 (4 Mbaud).\n" " --link PATH Symlink to the PTY slave (Unix). Default $%s/.hurra-bridge.tty.\n" - " --virtual-port NAME com0com COM name to open (Windows; required there).\n" + " --virtual-port NAME com0com COM name (Windows). Optional: auto-detected.\n" " --timeout-ms N Per-request timeout for get-style commands. Default 250.\n" " --km-port N KMBox Net UDP port to listen on. Default %d.\n" " --km-bind ADDR KMBox Net bind address. Default 0.0.0.0.\n" From 1ea3a296ebc1d497ec9fd5137483486d7ee0d036 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Tue, 16 Jun 2026 01:20:17 -0400 Subject: [PATCH 6/6] fix: skip LPT ports in Windows COM enumeration GUID_DEVCLASS_PORTS covers both COM and LPT devices; filter out LPT PortNames so parallel ports don't clutter error listings or consume candidate slots. --- src/serial_enum_win32.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/serial_enum_win32.c b/src/serial_enum_win32.c index de7a441..217169d 100644 --- a/src/serial_enum_win32.c +++ b/src/serial_enum_win32.c @@ -62,6 +62,11 @@ size_t serial_enum(serial_cand_t *out, size_t max) { SetupDiEnumDeviceInfo(set, i, &dev); i++) { char name[32]; if (!read_port_name(set, &dev, name, sizeof name)) continue; + /* GUID_DEVCLASS_PORTS also covers LPT (parallel) ports; skip them so + * they neither clutter error listings nor consume candidate slots. */ + if ((name[0] == 'L' || name[0] == 'l') && + (name[1] == 'P' || name[1] == 'p') && + (name[2] == 'T' || name[2] == 't')) continue; char hwid[256]; get_str_prop(set, &dev, SPDRP_HARDWAREID, hwid, sizeof hwid);