Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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 ──────────────────────────────────────────────────────────────
Expand All @@ -91,6 +94,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)
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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`).
Expand Down
101 changes: 79 additions & 22 deletions src/bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -198,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"
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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. */
Expand All @@ -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 ──────────────────────────────────────────────────────────────── */

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -500,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;
Expand All @@ -519,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. */
Expand All @@ -534,14 +596,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));
}
Expand Down
26 changes: 26 additions & 0 deletions src/serial_enum.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* serial_enum.c — pure hardware-ID classifier (always compiled). */
#include "serial_enum.h"
#include <ctype.h>

/* 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;
}
37 changes: 37 additions & 0 deletions src/serial_enum.h
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>

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);
Comment on lines +33 to +35

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsing failure and empty into a single 0 return is intentional here, not an oversight — it's the documented design (spec: "SetupAPI / registry failures degrade to 'zero candidates' → existing explicit error paths fire").

The two states aren't actionably different at the call sites: in both cases the bridge cannot auto-select a port and must fall back to the explicit-flag error path. Distinguishing them would only change error wording, while complicating the API contract (out-param or sentinel) that every caller has to handle. That's the graceful degradation the summary refers to — a failed SetupAPI call lands on the same clean bridge_fail as 'no device plugged in', never a crash. Keeping the simple size_t count contract on purpose (YAGNI).


#endif /* HURRA_SERIAL_ENUM_H */
7 changes: 7 additions & 0 deletions src/serial_enum_stub.c
Original file line number Diff line number Diff line change
@@ -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. */
}
Loading
Loading