From 2423ffc89557e06a4f63853e365dec94fe49eac6 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Sat, 21 Feb 2026 16:26:25 -0500 Subject: [PATCH 1/2] kmbox net binary --- .gitmodules | 3 + PIOKMbox.c | 99 +---- README.md | 5 +- bridge/CMakeLists.txt | 10 +- build.sh | 6 + defines.h | 25 +- state_management.c | 15 - state_management.h | 8 - tools/Makefile | 19 +- tools/kmbox_relay | Bin 0 -> 35096 bytes tools/kmbox_relay.c | 988 ++++++++++++++++++++++++++++++++++++++++++ usb_hid.c | 148 ++++++- watchdog.c | 99 +---- 13 files changed, 1198 insertions(+), 227 deletions(-) create mode 100644 .gitmodules create mode 100755 tools/kmbox_relay create mode 100644 tools/kmbox_relay.c diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..428e3a5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/Pico-PIO-USB"] + path = lib/Pico-PIO-USB + url = https://github.com/sekigon-gonnoc/Pico-PIO-USB.git diff --git a/PIOKMbox.c b/PIOKMbox.c index 2cac925..8ace3d5 100644 --- a/PIOKMbox.c +++ b/PIOKMbox.c @@ -39,30 +39,6 @@ #include "tusb.h" #endif -//--------------------------------------------------------------------+ -// Type Definitions and Structures -//--------------------------------------------------------------------+ - -typedef struct { - uint32_t watchdog_status_timer; - uint32_t last_button_press_time; - bool button_pressed_last; - bool usb_reset_cooldown; - uint32_t usb_reset_cooldown_start; -} main_loop_state_t; - -// Remove unused structures -//typedef struct { -// bool button_pressed; -// uint32_t current_time; -// uint32_t hold_duration; -//} button_state_t; - -//--------------------------------------------------------------------+ -// Constants and Configuration -//--------------------------------------------------------------------+ -static const uint32_t WATCHDOG_STATUS_INTERVAL_MS = WATCHDOG_STATUS_REPORT_INTERVAL_MS; - //--------------------------------------------------------------------+ // Global state for flash operation coordination //--------------------------------------------------------------------+ @@ -85,8 +61,6 @@ static void main_application_loop(void); // Button handling functions static void process_button_input(system_state_t* state, uint32_t current_time); -// Reporting functions -static void report_watchdog_status(uint32_t current_time, uint32_t* watchdog_status_timer); // Utility functions static inline bool is_time_elapsed(uint32_t current_time, uint32_t last_time, uint32_t interval); @@ -96,26 +70,6 @@ static inline bool is_time_elapsed(uint32_t current_time, uint32_t last_time, ui //--------------------------------------------------------------------+ #if PIO_USB_AVAILABLE -// Separate initialization concerns into focused functions - -typedef enum { - INIT_SUCCESS, - INIT_FAILURE, - INIT_RETRY_NEEDED -} init_result_t; - -typedef struct { - int attempt; - int max_attempts; - uint32_t base_delay_ms; - uint32_t last_heartbeat_time; -} init_context_t; - -typedef struct { - uint32_t last_heartbeat_ms; - uint32_t heartbeat_counter; -} core1_state_t; - static void core1_main(void) { // Small delay to let core0 stabilize @@ -245,8 +199,6 @@ static bool initialize_usb_device(void) { return device_init_success; } - - //--------------------------------------------------------------------+ // Button Handling Functions //--------------------------------------------------------------------+ @@ -271,11 +223,11 @@ static void process_button_input(system_state_t* state, uint32_t current_time) { state->last_button_press_time = current_time; } else { // Button being held - check for reset trigger - if (is_time_elapsed(current_time, state->last_button_press_time, BUTTON_HOLD_TRIGGER_MS)) { - usb_stacks_reset(); - state->usb_reset_cooldown = true; - state->usb_reset_cooldown_start = current_time; - } + if (is_time_elapsed(current_time, state->last_button_press_time, BUTTON_HOLD_TRIGGER_MS)) { + usb_stacks_reset(); + state->usb_reset_cooldown = true; + state->usb_reset_cooldown_start = current_time; + } } } else if (state->button_pressed_last) { // Button just released - check if it was a short press @@ -291,22 +243,10 @@ static void process_button_input(system_state_t* state, uint32_t current_time) { // Show mode with LED flash uint32_t mode_color; switch (new_mode) { - case HUMANIZATION_OFF: - mode_color = COLOR_HUMANIZATION_OFF; - - break; - case HUMANIZATION_MICRO: - mode_color = COLOR_HUMANIZATION_MICRO; - - break; - case HUMANIZATION_FULL: - mode_color = COLOR_HUMANIZATION_FULL; - - break; - default: - mode_color = COLOR_ERROR; - - break; + case HUMANIZATION_OFF: mode_color = COLOR_HUMANIZATION_OFF; break; + case HUMANIZATION_MICRO: mode_color = COLOR_HUMANIZATION_MICRO; break; + case HUMANIZATION_FULL: mode_color = COLOR_HUMANIZATION_FULL; break; + default: mode_color = COLOR_ERROR; break; } neopixel_set_color(mode_color); @@ -317,20 +257,6 @@ static void process_button_input(system_state_t* state, uint32_t current_time) { state->button_pressed_last = button_currently_pressed; } -//--------------------------------------------------------------------+ -// Reporting Functions -//--------------------------------------------------------------------+ - -static void report_watchdog_status(uint32_t current_time, uint32_t* watchdog_status_timer) { - if (!is_time_elapsed(current_time, *watchdog_status_timer, WATCHDOG_STATUS_INTERVAL_MS)) { - return; - } - - *watchdog_status_timer = current_time; - - -} - //--------------------------------------------------------------------+ // Utility Functions //--------------------------------------------------------------------+ @@ -343,7 +269,6 @@ static __force_inline bool is_time_elapsed(uint32_t current_time, uint32_t last_ // Main Application Loop //--------------------------------------------------------------------+ - static void main_application_loop(void) { system_state_t* state = get_system_state(); system_state_init(state); @@ -353,7 +278,7 @@ static void main_application_loop(void) { const uint32_t visual_interval = VISUAL_TASK_INTERVAL_MS; const uint32_t error_interval = ERROR_CHECK_INTERVAL_MS; const uint32_t button_interval = BUTTON_DEBOUNCE_MS; - const uint32_t status_report_interval = WATCHDOG_STATUS_REPORT_INTERVAL_MS; + const uint32_t status_interval = WATCHDOG_STATUS_REPORT_INTERVAL_MS; // Performance optimization: reduce time sampling frequency uint32_t current_time = to_ms_since_boot(get_absolute_time()); @@ -414,7 +339,7 @@ static void main_application_loop(void) { if ((current_time - state->last_button_time) >= button_interval) { task_flags |= BUTTON_FLAG; } - if ((current_time - state->watchdog_status_timer) >= status_report_interval) { + if ((current_time - state->watchdog_status_timer) >= status_interval) { task_flags |= STATUS_FLAG; } @@ -448,7 +373,7 @@ static void main_application_loop(void) { } if (task_flags & STATUS_FLAG) { - report_watchdog_status(current_time, &state->watchdog_status_timer); + state->watchdog_status_timer = current_time; // Send Xbox console mode status to bridge for TFT display if (g_xbox_mode) { kmbox_send_xbox_status_to_bridge(); diff --git a/README.md b/README.md index 4cc3953..244db48 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,12 @@ This allows bidirectional communication for KMBox commands and display data. ### 1. Clone and Build ```bash -git clone https://github.com/ramseymcgrath/RaspberryKMBox.git +git clone --recursive https://github.com/ramseymcgrath/RaspberryKMBox.git cd RaspberryKMBox +# If you already cloned without --recursive: +git submodule update --init --recursive + # Build options: ./build.sh metro # Main KMBox for Metro RP2350 ./build.sh bridge-metro # Bridge for Metro RP2350 with display diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index 0e8ef57..f956cbe 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -12,8 +12,14 @@ set(PICO_PLATFORM rp2350-arm-s) # Custom board headers (for adafruit_metro_rp2350) set(PICO_BOARD_HEADER_DIRS ${CMAKE_CURRENT_LIST_DIR}/../boards) -# Import Pico SDK -set(PICO_SDK_PATH $ENV{HOME}/.pico-sdk/sdk/2.2.0-fresh) +# Import Pico SDK — prefer cmake -D, then env var, then default local path +if(NOT DEFINED PICO_SDK_PATH) + if(DEFINED ENV{PICO_SDK_PATH}) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + else() + set(PICO_SDK_PATH $ENV{HOME}/.pico-sdk/sdk/2.2.0-fresh) + endif() +endif() include(${PICO_SDK_PATH}/pico_sdk_init.cmake) project(kmbox_bridge C CXX ASM) diff --git a/build.sh b/build.sh index 28cf4d5..868b729 100755 --- a/build.sh +++ b/build.sh @@ -241,6 +241,12 @@ if [ -z "$TARGET" ]; then TARGET="flash-metros" fi +# Ensure submodules are initialized +if [ ! -f "$SCRIPT_DIR/lib/Pico-PIO-USB/CMakeLists.txt" ]; then + echo -e "${YELLOW}Initializing git submodules...${NC}" + git -C "$SCRIPT_DIR" submodule update --init --recursive +fi + # Execute build cd "$SCRIPT_DIR" diff --git a/defines.h b/defines.h index 45bc5e8..42d931a 100644 --- a/defines.h +++ b/defines.h @@ -412,10 +412,7 @@ typedef struct __attribute__((packed)) { // Humanization mode colors (for button mode switching) #define COLOR_HUMANIZATION_OFF 0xFF0000 // Red - no humanization -#define COLOR_HUMANIZATION_LOW 0xFFAA00 // Orange - low humanization (deprecated, use MICRO) #define COLOR_HUMANIZATION_MICRO 0xFFFF00 // Yellow - micro-noise only (pre-humanized input) -#define COLOR_HUMANIZATION_MEDIUM 0xAAFF00 // Yellow-green - medium (deprecated, use FULL) -#define COLOR_HUMANIZATION_HIGH 0x00FF00 // Green - high (deprecated, use FULL) #define COLOR_HUMANIZATION_FULL 0x00FF00 // Green - full humanization (raw input) // Brightness constants @@ -509,22 +506,12 @@ typedef struct __attribute__((packed)) { // LOGGING CONFIGURATION //--------------------------------------------------------------------+ -#if BUILD_CONFIG == BUILD_CONFIG_PRODUCTION - #define ENABLE_VERBOSE_LOGGING 0 - #define ENABLE_INIT_LOGGING 0 - #define ENABLE_ERROR_LOGGING 0 - #define ENABLE_STATS_LOGGING 0 -#elif BUILD_CONFIG == BUILD_CONFIG_TESTING - #define ENABLE_VERBOSE_LOGGING 0 - #define ENABLE_INIT_LOGGING 0 - #define ENABLE_ERROR_LOGGING 0 - #define ENABLE_STATS_LOGGING 0 -#else // Development and Debug - #define ENABLE_VERBOSE_LOGGING 0 - #define ENABLE_INIT_LOGGING 0 - #define ENABLE_ERROR_LOGGING 0 - #define ENABLE_STATS_LOGGING 0 -#endif +// Logging flags — set to 1 to enable per-category printf output. +// All disabled by default to minimize code size and UART noise. +#define ENABLE_VERBOSE_LOGGING 0 +#define ENABLE_INIT_LOGGING 0 +#define ENABLE_ERROR_LOGGING 0 +#define ENABLE_STATS_LOGGING 0 //--------------------------------------------------------------------+ // CONDITIONAL COMPILATION MACROS diff --git a/state_management.c b/state_management.c index c12b1df..fe2c893 100644 --- a/state_management.c +++ b/state_management.c @@ -17,18 +17,3 @@ system_state_t* get_system_state(void) { return &g_system_state; } -// Performance-optimized inline time check function -inline bool system_state_should_run_task(const system_state_t* state, uint32_t current_time, - uint32_t last_run_time, uint32_t interval_ms) { - (void)state; // Suppress unused parameter warning - return (current_time - last_run_time) >= interval_ms; -} - -// Batch update function for performance - updates multiple timers at once -void system_state_batch_update_timers(system_state_t* state, uint32_t current_time, - uint8_t update_flags) { - if (update_flags & 0x01) state->last_watchdog_time = current_time; - if (update_flags & 0x02) state->last_visual_time = current_time; - if (update_flags & 0x04) state->last_button_time = current_time; - if (update_flags & 0x08) state->watchdog_status_timer = current_time; -} diff --git a/state_management.h b/state_management.h index f59aadd..272c5da 100644 --- a/state_management.h +++ b/state_management.h @@ -43,13 +43,5 @@ void system_state_init(system_state_t* state); // Get current system state (singleton pattern) system_state_t* get_system_state(void); -// State update helpers -void system_state_update_timing(system_state_t* state, uint32_t current_time); -bool system_state_should_run_task(const system_state_t* state, uint32_t current_time, - uint32_t last_run_time, uint32_t interval_ms); - -// Performance optimization: batch timer updates -void system_state_batch_update_timers(system_state_t* state, uint32_t current_time, - uint8_t update_flags); #endif // STATE_MANAGEMENT_H diff --git a/tools/Makefile b/tools/Makefile index 1f93d10..8ccbf85 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,16 +1,23 @@ -# Mouse Counteraction Tool - macOS Build +# Tools Build CC = clang CFLAGS = -Wall -Wextra -O2 FRAMEWORKS = -framework CoreGraphics -framework ApplicationServices -framework Foundation -TARGET = mouse_counteract +INCLUDES = -I../lib/wire-protocol/include -all: $(TARGET) +all: mouse_counteract kmbox_relay -$(TARGET): mouse_counteract.c +mouse_counteract: mouse_counteract.c $(CC) $(CFLAGS) -o $@ $< $(FRAMEWORKS) +kmbox_relay: kmbox_relay.c ../lib/wire-protocol/include/wire_protocol.h + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $< + +# Cross-compile for Windows (requires mingw-w64: brew install mingw-w64) +kmbox_relay.exe: kmbox_relay.c ../lib/wire-protocol/include/wire_protocol.h + x86_64-w64-mingw32-gcc $(CFLAGS) $(INCLUDES) -o $@ $< -lws2_32 + clean: - rm -f $(TARGET) + rm -f mouse_counteract kmbox_relay kmbox_relay.exe -.PHONY: all clean \ No newline at end of file +.PHONY: all clean diff --git a/tools/kmbox_relay b/tools/kmbox_relay new file mode 100755 index 0000000000000000000000000000000000000000..ed1fd88db6b64a04c69d9bd6461c9426aec7bc31 GIT binary patch literal 35096 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 V+p7kbe(kLP_a6-IdFrmS{|!FSdF}uJ literal 0 HcmV?d00001 diff --git a/tools/kmbox_relay.c b/tools/kmbox_relay.c new file mode 100644 index 0000000..8a5210d --- /dev/null +++ b/tools/kmbox_relay.c @@ -0,0 +1,988 @@ +/* + * KMBox Net UDP Relay — Host-Side Protocol Bridge + * + * Speaks KMBox Net protocol over UDP (compatible with KMBox B+ client apps) + * and translates commands to Wire Protocol v2 binary packets sent over + * CDC serial to the RaspberryKMBox bridge. + * + * Usage: + * ./kmbox_relay [options] + * + * Options: + * -p, --port PORT UDP listen port (default: 9346) + * -b, --baud RATE Serial baud rate (default: 3000000) + * -m, --mac MAC Expected MAC for auth (default: accept any) + * -v, --verbose Log all commands + * -h, --help Show help + * + * Build (macOS/Linux): + * cc -O2 -o kmbox_relay kmbox_relay.c + * + * Build (Windows, cross-compile with mingw-w64): + * x86_64-w64-mingw32-gcc -O2 -o kmbox_relay.exe kmbox_relay.c -lws2_32 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========================================================================== */ +/* Platform Abstraction */ +/* ========================================================================== */ + +#ifdef _WIN32 + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#pragma comment(lib, "ws2_32.lib") + +typedef SOCKET sock_t; +typedef HANDLE serial_t; +#define INVALID_SOCK INVALID_SOCKET +#define INVALID_SER INVALID_HANDLE_VALUE +#define sock_close(s) closesocket(s) +#define sock_errno WSAGetLastError() + +static int platform_init(void) { + WSADATA wsa; + return WSAStartup(MAKEWORD(2, 2), &wsa); +} +static void platform_cleanup(void) { WSACleanup(); } + +static serial_t serial_open(const char *port, int baud) { + HANDLE h = CreateFileA(port, GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, 0, NULL); + if (h == INVALID_HANDLE_VALUE) return INVALID_SER; + + DCB dcb = {0}; + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(h, &dcb)) { CloseHandle(h); return INVALID_SER; } + dcb.BaudRate = baud; + dcb.ByteSize = 8; + dcb.Parity = NOPARITY; + dcb.StopBits = ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fParity = FALSE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + if (!SetCommState(h, &dcb)) { CloseHandle(h); return INVALID_SER; } + + COMMTIMEOUTS to = {0}; + to.ReadIntervalTimeout = MAXDWORD; + to.ReadTotalTimeoutMultiplier = 0; + to.ReadTotalTimeoutConstant = 0; + to.WriteTotalTimeoutMultiplier = 0; + to.WriteTotalTimeoutConstant = 100; + SetCommTimeouts(h, &to); + return h; +} + +static void serial_close(serial_t h) { + if (h != INVALID_SER) CloseHandle(h); +} + +static int serial_write(serial_t h, const uint8_t *buf, int len) { + DWORD written; + if (!WriteFile(h, buf, len, &written, NULL)) return -1; + return (int)written; +} + +static int serial_read(serial_t h, uint8_t *buf, int maxlen) { + DWORD rd; + if (!ReadFile(h, buf, maxlen, &rd, NULL)) return -1; + return (int)rd; +} + +static void set_nonblocking(sock_t s) { + u_long mode = 1; + ioctlsocket(s, FIONBIO, &mode); +} + +static double time_sec(void) { + LARGE_INTEGER freq, now; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&now); + return (double)now.QuadPart / (double)freq.QuadPart; +} + +#else /* POSIX (macOS, Linux) */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +typedef int sock_t; +typedef int serial_t; +#define INVALID_SOCK (-1) +#define INVALID_SER (-1) +#define sock_close(s) close(s) +#define sock_errno errno + +static int platform_init(void) { return 0; } +static void platform_cleanup(void) {} + +static speed_t baud_to_speed(int baud) { + switch (baud) { + case 9600: return B9600; + case 19200: return B19200; + case 38400: return B38400; + case 57600: return B57600; + case 115200: return B115200; + case 230400: return B230400; +#ifdef B460800 + case 460800: return B460800; +#endif +#ifdef B921600 + case 921600: return B921600; +#endif +#ifdef B1000000 + case 1000000: return B1000000; +#endif +#ifdef B2000000 + case 2000000: return B2000000; +#endif +#ifdef B3000000 + case 3000000: return B3000000; +#endif + default: return B115200; + } +} + +static serial_t serial_open(const char *port, int baud) { + int fd = open(port, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd < 0) return INVALID_SER; + + struct termios tty; + memset(&tty, 0, sizeof(tty)); + if (tcgetattr(fd, &tty) != 0) { close(fd); return INVALID_SER; } + + speed_t spd = baud_to_speed(baud); + cfsetispeed(&tty, spd); + cfsetospeed(&tty, spd); + + tty.c_cflag &= ~(PARENB | CSTOPB | CSIZE | CRTSCTS); + tty.c_cflag |= CS8 | CLOCAL | CREAD; + tty.c_lflag = 0; + tty.c_iflag = 0; + tty.c_oflag = 0; + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 0; + + tcflush(fd, TCIOFLUSH); + if (tcsetattr(fd, TCSANOW, &tty) != 0) { close(fd); return INVALID_SER; } + +#ifdef __APPLE__ + /* macOS non-standard baud rate support */ + if (baud > 230400) { + speed_t custom = baud; + if (ioctl(fd, IOSSIOSPEED, &custom) == -1) { + fprintf(stderr, "[WARN] Could not set custom baud %d, using closest standard\n", baud); + } + } +#endif + + return fd; +} + +static void serial_close(serial_t fd) { + if (fd != INVALID_SER) close(fd); +} + +static int serial_write(serial_t fd, const uint8_t *buf, int len) { + return (int)write(fd, buf, len); +} + +static int serial_read(serial_t fd, uint8_t *buf, int maxlen) { + int n = (int)read(fd, buf, maxlen); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; + return n; +} + +static void set_nonblocking(sock_t s) { + int flags = fcntl(s, F_GETFL, 0); + fcntl(s, F_SETFL, flags | O_NONBLOCK); +} + +static double time_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec * 1e-9; +} + +#endif /* _WIN32 */ + +/* ========================================================================== */ +/* Wire Protocol v2 (subset needed for relay) */ +/* ========================================================================== */ + +#include "../lib/wire-protocol/include/wire_protocol.h" + +/* ========================================================================== */ +/* KMBox Net Protocol Structures */ +/* ========================================================================== */ + +#define KMNET_CMD_CONNECT 0xaf3c2828 +#define KMNET_CMD_MOUSE_MOVE 0xaede7345 +#define KMNET_CMD_MOUSE_LEFT 0x9823AE8D +#define KMNET_CMD_MOUSE_MIDDLE 0x97a3AE8D +#define KMNET_CMD_MOUSE_RIGHT 0x238d8212 +#define KMNET_CMD_MOUSE_WHEEL 0xffeead38 +#define KMNET_CMD_MOUSE_AUTOMOVE 0xaede7346 +#define KMNET_CMD_KEYBOARD_ALL 0x123c2c2f +#define KMNET_CMD_REBOOT 0xaa8855aa +#define KMNET_CMD_BAZER_MOVE 0xa238455a +#define KMNET_CMD_MONITOR 0x27388020 +#define KMNET_CMD_DEBUG 0x27382021 +#define KMNET_CMD_MASK_MOUSE 0x23234343 +#define KMNET_CMD_UNMASK_ALL 0x23344343 +#define KMNET_CMD_SETCONFIG 0x1d3d3323 +#define KMNET_CMD_SETVIDPID 0xffed3232 +#define KMNET_CMD_SHOWPIC 0x12334883 +#define KMNET_CMD_TRACE_ENABLE 0xbbcdddac + +#pragma pack(push, 1) + +typedef struct { + uint32_t mac; + uint32_t rand; + uint32_t indexpts; + uint32_t cmd; +} kmnet_head_t; + +typedef struct { + int32_t button; + int32_t x; + int32_t y; + int32_t wheel; + int32_t point[10]; +} kmnet_mouse_t; + +typedef struct { + char ctrl; + char resvel; + char button[10]; +} kmnet_keyboard_t; + +#pragma pack(pop) + +#define KMNET_HEAD_SIZE sizeof(kmnet_head_t) /* 16 */ +#define KMNET_MOUSE_SIZE sizeof(kmnet_mouse_t) /* 56 */ +#define KMNET_KEYBOARD_SIZE sizeof(kmnet_keyboard_t) /* 12 */ +#define KMNET_ENC_SIZE 128 /* encrypted packet */ + +/* ========================================================================== */ +/* XXTEA Encryption/Decryption */ +/* ========================================================================== */ + +#define XXTEA_DELTA 0x9E3779B9U +#define XXTEA_ROUNDS 6 +#define XXTEA_N 32 /* 128 bytes / 4 = 32 uint32_t words */ + +#define MX(e, p, y, z, sum, key) \ + (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ \ + (((sum) ^ (y)) + ((key)[(p & 3) ^ (e)] ^ (z))) + +__attribute__((unused)) +static void xxtea_encrypt(uint8_t *data, const uint8_t *key_bytes) { + uint32_t *v = (uint32_t *)data; + uint32_t *k = (uint32_t *)key_bytes; + uint32_t n = XXTEA_N; + uint32_t z = v[n - 1], y, sum = 0, e; + uint32_t p, q; + + q = XXTEA_ROUNDS; + while (q-- > 0) { + sum += XXTEA_DELTA; + e = (sum >> 2) & 3; + for (p = 0; p < n - 1; p++) { + y = v[p + 1]; + z = v[p] += MX(e, p, y, z, sum, k); + } + y = v[0]; + z = v[n - 1] += MX(e, p, y, z, sum, k); + } +} + +static void xxtea_decrypt(uint8_t *data, const uint8_t *key_bytes) { + uint32_t *v = (uint32_t *)data; + uint32_t *k = (uint32_t *)key_bytes; + uint32_t n = XXTEA_N; + uint32_t z, y = v[0], sum, e; + uint32_t p, q; + + q = XXTEA_ROUNDS; + sum = q * XXTEA_DELTA; + while (q-- > 0) { + e = (sum >> 2) & 3; + for (p = n - 1; p > 0; p--) { + z = v[p - 1]; + y = v[p] -= MX(e, p, y, z, sum, k); + } + z = v[n - 1]; + y = v[0] -= MX(e, p, y, z, sum, k); + sum -= XXTEA_DELTA; + } +} + +/* ========================================================================== */ +/* Relay State */ +/* ========================================================================== */ + +typedef struct { + /* CLI config */ + char serial_port[256]; + int udp_port; + int baud_rate; + uint32_t expected_mac; /* 0 = accept any */ + bool verbose; + + /* Runtime */ + serial_t ser; + sock_t udp; + volatile bool running; + + /* Client tracking */ + struct sockaddr_in client_addr; + bool client_connected; + uint32_t client_mac; + uint8_t enc_key[16]; + + /* Keyboard state tracking (for diffing) */ + kmnet_keyboard_t last_kb; + + /* Button state tracking */ + uint8_t last_buttons; + + /* Statistics */ + uint64_t cmds_received; + uint64_t cmds_translated; + uint64_t serial_errors; + uint64_t encrypted_cmds; +} relay_t; + +static relay_t g_relay; + +/* ========================================================================== */ +/* Logging */ +/* ========================================================================== */ + +static void log_timestamp(void) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + fprintf(stderr, "%02d:%02d:%02d ", t->tm_hour, t->tm_min, t->tm_sec); +} + +#define LOG_BRIDGE(fmt, ...) do { \ + log_timestamp(); \ + fprintf(stderr, "[BRIDGE] " fmt "\n", ##__VA_ARGS__); \ +} while(0) + +#define LOG_CLIENT(fmt, ...) do { \ + log_timestamp(); \ + fprintf(stderr, "[CLIENT] " fmt "\n", ##__VA_ARGS__); \ +} while(0) + +#define LOG_VERBOSE(fmt, ...) do { \ + if (g_relay.verbose) { log_timestamp(); fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__); } \ +} while(0) + +/* ========================================================================== */ +/* Command Name Lookup (for logging) */ +/* ========================================================================== */ + +static const char *cmd_name(uint32_t cmd) { + switch (cmd) { + case KMNET_CMD_CONNECT: return "connect"; + case KMNET_CMD_MOUSE_MOVE: return "mouse_move"; + case KMNET_CMD_MOUSE_LEFT: return "mouse_left"; + case KMNET_CMD_MOUSE_MIDDLE: return "mouse_middle"; + case KMNET_CMD_MOUSE_RIGHT: return "mouse_right"; + case KMNET_CMD_MOUSE_WHEEL: return "mouse_wheel"; + case KMNET_CMD_MOUSE_AUTOMOVE: return "mouse_automove"; + case KMNET_CMD_KEYBOARD_ALL: return "keyboard_all"; + case KMNET_CMD_REBOOT: return "reboot"; + case KMNET_CMD_BAZER_MOVE: return "bezier_move"; + case KMNET_CMD_MONITOR: return "monitor"; + case KMNET_CMD_DEBUG: return "debug"; + case KMNET_CMD_MASK_MOUSE: return "mask_mouse"; + case KMNET_CMD_UNMASK_ALL: return "unmask_all"; + case KMNET_CMD_SETCONFIG: return "setconfig"; + case KMNET_CMD_SETVIDPID: return "setvidpid"; + case KMNET_CMD_SHOWPIC: return "showpic"; + case KMNET_CMD_TRACE_ENABLE: return "trace_enable"; + default: return "unknown"; + } +} + +/* ========================================================================== */ +/* Serial Helpers */ +/* ========================================================================== */ + +static int send_wire(relay_t *r, const uint8_t *pkt, int len) { + int written = serial_write(r->ser, pkt, len); + if (written != len) { + r->serial_errors++; + LOG_BRIDGE("Serial write error: wrote %d/%d", written, len); + return -1; + } + return 0; +} + +/* ========================================================================== */ +/* UDP Helpers */ +/* ========================================================================== */ + +static void send_ack(relay_t *r, const kmnet_head_t *head) { + sendto(r->udp, (const char *)head, KMNET_HEAD_SIZE, 0, + (struct sockaddr *)&r->client_addr, sizeof(r->client_addr)); +} + +/* ========================================================================== */ +/* Encryption Key Derivation */ +/* ========================================================================== */ + +static void derive_key(relay_t *r, uint32_t mac) { + memset(r->enc_key, 0, 16); + r->enc_key[0] = (mac >> 24) & 0xFF; + r->enc_key[1] = (mac >> 16) & 0xFF; + r->enc_key[2] = (mac >> 8) & 0xFF; + r->enc_key[3] = (mac >> 0) & 0xFF; +} + +/* ========================================================================== */ +/* Command Translation: KMBox Net -> Wire Protocol v2 */ +/* ========================================================================== */ + +static void translate_mouse_move(relay_t *r, const kmnet_mouse_t *m) { + uint8_t pkt[WIRE_MAX_PACKET]; + int16_t x = (int16_t)m->x; + int16_t y = (int16_t)m->y; + size_t len = wire_build_move(pkt, x, y); + LOG_VERBOSE("mouse_move x=%d y=%d (wire=%s, %zu bytes)", + x, y, len == 3 ? "MOVE8" : "MOVE16", len); + send_wire(r, pkt, (int)len); + r->cmds_translated++; +} + +static void translate_mouse_button(relay_t *r, const kmnet_mouse_t *m) { + /* KMBox Net button bits: 0x01=left, 0x02=right, 0x04=middle, 0x08=side1, 0x10=side2 + * Wire protocol button bits match: 0x01=left, 0x02=right, 0x04=middle, 0x08=back, 0x10=forward */ + uint8_t buttons = (uint8_t)(m->button & 0x1F); + if (buttons != r->last_buttons) { + uint8_t pkt[WIRE_MAX_PACKET]; + size_t len = wire_build_buttons(pkt, buttons); + LOG_VERBOSE("buttons 0x%02X -> 0x%02X", r->last_buttons, buttons); + send_wire(r, pkt, (int)len); + r->last_buttons = buttons; + r->cmds_translated++; + } +} + +static void translate_mouse_wheel(relay_t *r, const kmnet_mouse_t *m) { + uint8_t pkt[WIRE_MAX_PACKET]; + int8_t delta = (int8_t)m->wheel; + size_t len = wire_build_wheel(pkt, delta); + LOG_VERBOSE("wheel delta=%d", delta); + send_wire(r, pkt, (int)len); + r->cmds_translated++; +} + +static void translate_mouse_automove(relay_t *r, const kmnet_mouse_t *m, + uint32_t duration_ms) { + uint8_t pkt[WIRE_MAX_PACKET]; + int16_t x = (int16_t)m->x; + int16_t y = (int16_t)m->y; + /* Use smooth injection mode; rand field carries duration */ + (void)duration_ms; + size_t len = wire_build_smooth16(pkt, x, y, WIRE_INJECT_SMOOTH); + LOG_VERBOSE("automove x=%d y=%d duration=%ums", x, y, duration_ms); + send_wire(r, pkt, (int)len); + r->cmds_translated++; +} + +static void translate_bezier_move(relay_t *r, const kmnet_mouse_t *m, + uint32_t duration_ms) { + uint8_t pkt[WIRE_MAX_PACKET]; + int16_t x = (int16_t)m->x; + int16_t y = (int16_t)m->y; + (void)duration_ms; + size_t len = wire_build_smooth16(pkt, x, y, WIRE_INJECT_SMOOTH); + LOG_VERBOSE("bezier x=%d y=%d duration=%ums", x, y, duration_ms); + send_wire(r, pkt, (int)len); + r->cmds_translated++; +} + +static void translate_keyboard(relay_t *r, const kmnet_keyboard_t *kb) { + uint8_t pkt[WIRE_MAX_PACKET]; + + /* Diff modifiers: send keydown/keyup for changed modifier bits */ + uint8_t old_ctrl = (uint8_t)r->last_kb.ctrl; + uint8_t new_ctrl = (uint8_t)kb->ctrl; + if (old_ctrl != new_ctrl) { + /* Modifier bits map to HID usage codes 0xE0..0xE7 */ + for (int bit = 0; bit < 8; bit++) { + uint8_t mask = 1 << bit; + bool was = (old_ctrl & mask) != 0; + bool now = (new_ctrl & mask) != 0; + if (!was && now) { + size_t len = wire_build_keydown(pkt, 0xE0 + bit, 0); + LOG_VERBOSE("keydown modifier 0x%02X", 0xE0 + bit); + send_wire(r, pkt, (int)len); + r->cmds_translated++; + } else if (was && !now) { + size_t len = wire_build_keyup(pkt, 0xE0 + bit); + LOG_VERBOSE("keyup modifier 0x%02X", 0xE0 + bit); + send_wire(r, pkt, (int)len); + r->cmds_translated++; + } + } + } + + /* Diff regular keys */ + /* Keys released: in old but not in new */ + for (int i = 0; i < 10; i++) { + uint8_t old_key = (uint8_t)r->last_kb.button[i]; + if (old_key == 0) continue; + bool found = false; + for (int j = 0; j < 10; j++) { + if ((uint8_t)kb->button[j] == old_key) { found = true; break; } + } + if (!found) { + size_t len = wire_build_keyup(pkt, old_key); + LOG_VERBOSE("keyup 0x%02X", old_key); + send_wire(r, pkt, (int)len); + r->cmds_translated++; + } + } + + /* Keys pressed: in new but not in old */ + for (int i = 0; i < 10; i++) { + uint8_t new_key = (uint8_t)kb->button[i]; + if (new_key == 0) continue; + bool found = false; + for (int j = 0; j < 10; j++) { + if ((uint8_t)r->last_kb.button[j] == new_key) { found = true; break; } + } + if (!found) { + /* Send keydown with current modifier state */ + size_t len = wire_build_keydown(pkt, new_key, new_ctrl); + LOG_VERBOSE("keydown 0x%02X (mods=0x%02X)", new_key, new_ctrl); + send_wire(r, pkt, (int)len); + r->cmds_translated++; + } + } + + memcpy(&r->last_kb, kb, sizeof(kmnet_keyboard_t)); +} + +/* ========================================================================== */ +/* Packet Handler */ +/* ========================================================================== */ + +static void handle_packet(relay_t *r, uint8_t *buf, int len, + struct sockaddr_in *from) { + bool encrypted = false; + uint8_t decrypted[KMNET_ENC_SIZE]; + + /* Detect encrypted packet: always exactly 128 bytes */ + if (len == KMNET_ENC_SIZE) { + /* Try decryption if we have a key */ + if (r->client_connected) { + memcpy(decrypted, buf, KMNET_ENC_SIZE); + xxtea_decrypt(decrypted, r->enc_key); + buf = decrypted; + encrypted = true; + r->encrypted_cmds++; + } + } + + if (len < (int)KMNET_HEAD_SIZE && !encrypted) { + LOG_VERBOSE("Packet too short: %d bytes", len); + return; + } + + kmnet_head_t head; + memcpy(&head, buf, KMNET_HEAD_SIZE); + + r->cmds_received++; + + /* Handle connect — always process regardless of MAC filter */ + if (head.cmd == KMNET_CMD_CONNECT) { + r->client_mac = head.mac; + memcpy(&r->client_addr, from, sizeof(*from)); + r->client_connected = true; + derive_key(r, head.mac); + memset(&r->last_kb, 0, sizeof(r->last_kb)); + r->last_buttons = 0; + + LOG_CLIENT("Connected from %s:%d (MAC: %08X)", + inet_ntoa(from->sin_addr), ntohs(from->sin_port), head.mac); + + if (r->expected_mac != 0 && head.mac != r->expected_mac) { + LOG_CLIENT("WARNING: MAC mismatch (expected %08X)", r->expected_mac); + } + + send_ack(r, &head); + return; + } + + /* For non-connect commands, must have a connected client */ + if (!r->client_connected) { + LOG_VERBOSE("Ignoring %s: no client connected", cmd_name(head.cmd)); + return; + } + + /* Update client address (in case port changed) */ + memcpy(&r->client_addr, from, sizeof(*from)); + + LOG_VERBOSE("cmd=%s (0x%08X) indexpts=%u rand=%u %s", + cmd_name(head.cmd), head.cmd, head.indexpts, head.rand, + encrypted ? "[encrypted]" : ""); + + /* Extract payloads */ + const uint8_t *payload = buf + KMNET_HEAD_SIZE; + int payload_len = (encrypted ? KMNET_ENC_SIZE : len) - (int)KMNET_HEAD_SIZE; + + switch (head.cmd) { + case KMNET_CMD_MOUSE_MOVE: + if (payload_len >= (int)KMNET_MOUSE_SIZE) { + kmnet_mouse_t m; + memcpy(&m, payload, KMNET_MOUSE_SIZE); + translate_mouse_move(r, &m); + } + break; + + case KMNET_CMD_MOUSE_LEFT: + case KMNET_CMD_MOUSE_RIGHT: + case KMNET_CMD_MOUSE_MIDDLE: + if (payload_len >= (int)KMNET_MOUSE_SIZE) { + kmnet_mouse_t m; + memcpy(&m, payload, KMNET_MOUSE_SIZE); + translate_mouse_button(r, &m); + } + break; + + case KMNET_CMD_MOUSE_WHEEL: + if (payload_len >= (int)KMNET_MOUSE_SIZE) { + kmnet_mouse_t m; + memcpy(&m, payload, KMNET_MOUSE_SIZE); + translate_mouse_wheel(r, &m); + } + break; + + case KMNET_CMD_MOUSE_AUTOMOVE: + if (payload_len >= (int)KMNET_MOUSE_SIZE) { + kmnet_mouse_t m; + memcpy(&m, payload, KMNET_MOUSE_SIZE); + translate_mouse_automove(r, &m, head.rand); + } + break; + + case KMNET_CMD_BAZER_MOVE: + if (payload_len >= (int)KMNET_MOUSE_SIZE) { + kmnet_mouse_t m; + memcpy(&m, payload, KMNET_MOUSE_SIZE); + translate_bezier_move(r, &m, head.rand); + } + break; + + case KMNET_CMD_KEYBOARD_ALL: + if (payload_len >= (int)KMNET_KEYBOARD_SIZE) { + kmnet_keyboard_t kb; + memcpy(&kb, payload, KMNET_KEYBOARD_SIZE); + translate_keyboard(r, &kb); + } + break; + + case KMNET_CMD_REBOOT: { + uint8_t pkt[WIRE_MAX_PACKET]; + size_t plen = wire_build_ping(pkt); + LOG_VERBOSE("reboot -> ping"); + send_wire(r, pkt, (int)plen); + r->cmds_translated++; + break; + } + + case KMNET_CMD_MONITOR: + LOG_VERBOSE("monitor command (port=%d) — not fully supported over serial", + head.rand & 0xFFFF); + break; + + default: + LOG_VERBOSE("unsupported command 0x%08X — ACK only", head.cmd); + break; + } + + /* Always ACK */ + send_ack(r, &head); +} + +/* ========================================================================== */ +/* Main Poll Loop */ +/* ========================================================================== */ + +static void signal_handler(int sig) { + (void)sig; + g_relay.running = false; +} + +static void run_relay(relay_t *r) { + uint8_t udp_buf[2048]; + uint8_t ser_buf[256]; + double last_stats = time_sec(); + + LOG_BRIDGE("Relay running — UDP port %d, serial %s @ %d baud", + r->udp_port, r->serial_port, r->baud_rate); + LOG_BRIDGE("Waiting for KMBox Net client connection..."); + + while (r->running) { + fd_set rfds; + struct timeval tv; + int maxfd = 0; + + FD_ZERO(&rfds); + +#ifdef _WIN32 + FD_SET(r->udp, &rfds); + maxfd = 0; /* Windows ignores nfds in select() */ +#else + FD_SET(r->udp, &rfds); + if (r->udp > maxfd) maxfd = r->udp; + FD_SET(r->ser, &rfds); + if (r->ser > maxfd) maxfd = r->ser; +#endif + + tv.tv_sec = 0; + tv.tv_usec = 1000; /* 1ms poll */ + + int ready = select(maxfd + 1, &rfds, NULL, NULL, &tv); + if (ready < 0) { + if (errno == EINTR) continue; + LOG_BRIDGE("select() error: %s", strerror(errno)); + break; + } + + /* Check UDP socket */ + if (FD_ISSET(r->udp, &rfds)) { + struct sockaddr_in from; + socklen_t fromlen = sizeof(from); + int n = recvfrom(r->udp, (char *)udp_buf, sizeof(udp_buf), 0, + (struct sockaddr *)&from, &fromlen); + if (n > 0) { + handle_packet(r, udp_buf, n, &from); + } + } + + /* Check serial port for responses (drain buffer) */ +#ifdef _WIN32 + { + int n = serial_read(r->ser, ser_buf, sizeof(ser_buf)); + if (n > 0) { + LOG_VERBOSE("Serial rx: %d bytes", n); + } + } +#else + if (FD_ISSET(r->ser, &rfds)) { + int n = serial_read(r->ser, ser_buf, sizeof(ser_buf)); + if (n > 0) { + LOG_VERBOSE("Serial rx: %d bytes", n); + } else if (n < 0) { + LOG_BRIDGE("Serial read error: %s", strerror(errno)); + } + } +#endif + + /* Periodic stats (every 30s) */ + double now = time_sec(); + if (now - last_stats > 30.0) { + LOG_BRIDGE("Stats: %llu cmds recv, %llu translated, %llu encrypted, %llu serial errors", + (unsigned long long)r->cmds_received, + (unsigned long long)r->cmds_translated, + (unsigned long long)r->encrypted_cmds, + (unsigned long long)r->serial_errors); + last_stats = now; + } + } + + LOG_BRIDGE("Shutting down..."); +} + +/* ========================================================================== */ +/* CLI Argument Parsing */ +/* ========================================================================== */ + +static void usage(const char *prog) { + fprintf(stderr, + "KMBox Net UDP Relay — Host-Side Protocol Bridge\n" + "\n" + "Usage: %s [options] \n" + "\n" + "Options:\n" + " -p, --port PORT UDP listen port (default: 9346)\n" + " -b, --baud RATE Serial baud rate (default: 3000000)\n" + " -m, --mac MAC Expected MAC for auth (hex, default: accept any)\n" + " -v, --verbose Log all commands\n" + " -h, --help Show help\n" + "\n" + "Example:\n" + " %s -v /dev/tty.usbmodem2101\n" + " %s -p 16896 -m AABBCCDD COM3\n" + "\n" + "Client connection:\n" + " kmNet_init(\"127.0.0.1\", \"9346\", \"AABBCCDD\")\n", + prog, prog, prog); +} + +static uint32_t parse_mac(const char *s) { + uint32_t mac = 0; + for (int i = 0; i < 8 && s[i]; i++) { + char c = s[i]; + uint8_t nibble; + if (c >= '0' && c <= '9') nibble = c - '0'; + else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10; + else break; + mac = (mac << 4) | nibble; + } + return mac; +} + +static int parse_args(relay_t *r, int argc, char **argv) { + r->udp_port = 9346; + r->baud_rate = 3000000; + r->expected_mac = 0; + r->verbose = false; + r->serial_port[0] = '\0'; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return -1; + } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + r->verbose = true; + } else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) && i + 1 < argc) { + r->udp_port = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--baud") == 0) && i + 1 < argc) { + r->baud_rate = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--mac") == 0) && i + 1 < argc) { + r->expected_mac = parse_mac(argv[++i]); + } else if (argv[i][0] != '-') { + strncpy(r->serial_port, argv[i], sizeof(r->serial_port) - 1); + r->serial_port[sizeof(r->serial_port) - 1] = '\0'; + } else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + usage(argv[0]); + return -1; + } + } + + if (r->serial_port[0] == '\0') { + fprintf(stderr, "Error: serial port required\n\n"); + usage(argv[0]); + return -1; + } + + return 0; +} + +/* ========================================================================== */ +/* Main */ +/* ========================================================================== */ + +int main(int argc, char **argv) { + relay_t *r = &g_relay; + memset(r, 0, sizeof(*r)); + r->ser = INVALID_SER; + r->udp = INVALID_SOCK; + + if (parse_args(r, argc, argv) != 0) + return 1; + + if (platform_init() != 0) { + fprintf(stderr, "Platform init failed\n"); + return 1; + } + + /* Open serial port */ + r->ser = serial_open(r->serial_port, r->baud_rate); + if (r->ser == INVALID_SER) { + fprintf(stderr, "Failed to open serial port %s: %s\n", + r->serial_port, strerror(errno)); + platform_cleanup(); + return 1; + } + LOG_BRIDGE("Connected to %s @ %d baud", r->serial_port, r->baud_rate); + + /* Create UDP socket */ + r->udp = socket(AF_INET, SOCK_DGRAM, 0); + if (r->udp == INVALID_SOCK) { + fprintf(stderr, "Failed to create UDP socket\n"); + serial_close(r->ser); + platform_cleanup(); + return 1; + } + + /* Allow address reuse */ + int reuse = 1; + setsockopt(r->udp, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)); + + /* Bind */ + struct sockaddr_in bind_addr; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = INADDR_ANY; + bind_addr.sin_port = htons((uint16_t)r->udp_port); + + if (bind(r->udp, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) { + fprintf(stderr, "Failed to bind UDP port %d: %s\n", + r->udp_port, strerror(errno)); + sock_close(r->udp); + serial_close(r->ser); + platform_cleanup(); + return 1; + } + + set_nonblocking(r->udp); + + /* Install signal handlers */ + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + r->running = true; + run_relay(r); + + /* Cleanup */ + sock_close(r->udp); + serial_close(r->ser); + platform_cleanup(); + + LOG_BRIDGE("Final stats: %llu cmds recv, %llu translated, %llu encrypted, %llu serial errors", + (unsigned long long)r->cmds_received, + (unsigned long long)r->cmds_translated, + (unsigned long long)r->encrypted_cmds, + (unsigned long long)r->serial_errors); + + return 0; +} diff --git a/usb_hid.c b/usb_hid.c index 879b10a..4bb9454 100644 --- a/usb_hid.c +++ b/usb_hid.c @@ -17,7 +17,8 @@ #include "xbox_gip.h" // Xbox mode flag #include "xbox_device.h" // Xbox device descriptors #include // For strcpy, strlen, memset -#include // For sqrtf +#include // For sqrtf, roundf +#include "pico/rand.h" // For get_rand_32() hardware TRNG uint16_t attached_vid = 0; uint16_t attached_pid = 0; @@ -292,6 +293,36 @@ static volatile uint8_t last_sent_buttons = 0; // final zero-delta "stop" report on the active→idle edge. static volatile bool was_active = false; +//--------------------------------------------------------------------+ +// Output-stage PRNG (xorshift32, independent from smooth_injection.c) +//--------------------------------------------------------------------+ +static uint32_t hid_rng_state = 0; + +static void hid_rng_seed(uint32_t seed) { + hid_rng_state = seed ? seed : 0xDEADBEEF; +} + +static inline uint32_t hid_rng_next(void) { + uint32_t x = hid_rng_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + hid_rng_state = x; + return x; +} + +// Gaussian approximation via CLT: sum of 4 uniform [0,1) values → ~N(2,1/√3) +// Normalized to ~N(0,1) by subtracting 2 and scaling. +static inline float hid_rng_gaussian(void) { + const float scale = 1.0f / 4294967296.0f; // 1/(2^32) + float sum = (float)hid_rng_next() * scale + + (float)hid_rng_next() * scale + + (float)hid_rng_next() * scale + + (float)hid_rng_next() * scale; + // sum ∈ [0,4), mean=2, stddev≈0.577; normalize to ~N(0,1) + return (sum - 2.0f) * 1.7320508f; // 1/0.577 ≈ √3 +} + // Device management helpers static void handle_device_disconnection(uint8_t dev_addr); static void handle_hid_device_connection(uint8_t dev_addr, uint8_t itf_protocol); @@ -304,6 +335,24 @@ static bool process_mouse_report_internal(const hid_mouse_report_t *report); static void apply_output_humanization(int16_t *x, int16_t *y, int16_t injected_x, int16_t injected_y); static inline int8_t clamp_i8(int32_t val); +// Stochastic rounding: a value of 0.3 rounds to 1 with 30% probability, 0 with 70%. +// This preserves the statistical mean while making sub-pixel tremor actually visible +// in the output — deterministic roundf() kills any tremor < 0.5px. +static inline int16_t stochastic_round(float v) { + if (v >= 0.0f) { + int16_t floor_v = (int16_t)v; + float frac = v - (float)floor_v; + float r = (float)(hid_rng_next() & 0xFFFF) * (1.0f / 65536.0f); + return floor_v + (r < frac ? 1 : 0); + } else { + float abs_v = -v; + int16_t floor_v = (int16_t)abs_v; + float frac = abs_v - (float)floor_v; + float r = (float)(hid_rng_next() & 0xFFFF) * (1.0f / 65536.0f); + return -(floor_v + (r < frac ? 1 : 0)); + } +} + // --- Runtime HID descriptor mirroring storage & helpers --- // Gaming mice (Razer, Logitech, SteelSeries) can have very large HID // report descriptors — 500-1000+ bytes with multiple collections for @@ -574,10 +623,12 @@ static void apply_output_humanization(int16_t *x, int16_t *y, int16_t injected_x float magnitude = total_mag; // Mode-dependent intensity scaling + // MICRO was 0.5x which, combined with 0.5px base jitter, gave only 0.25px + // effective tremor — too small to survive even stochastic rounding consistently. float mode_scale = 1.0f; switch (mode) { case HUMANIZATION_MICRO: - mode_scale = 0.5f; + mode_scale = 0.75f; break; case HUMANIZATION_FULL: mode_scale = 1.0f; @@ -590,7 +641,14 @@ static void apply_output_humanization(int16_t *x, int16_t *y, int16_t injected_x float movement_scale = humanization_jitter_scale(magnitude); float base_jitter = (float)jitter_amount_fp / 65536.0f; // Convert from 16.16 fixed-point float tremor_scale = base_jitter * movement_scale * mode_scale * blend; - + + // Scale noise DOWN at low velocities to prevent overwhelming the signal. + // At 1-2 count deltas, ±1 of tremor is 50-100% perturbation, creating + // chaotic scribble instead of smooth slow transitions. + // Ramp: 0 at 0px → full at 4px + float low_speed_scale = fminf(1.0f, magnitude / 4.0f); + tremor_scale *= low_speed_scale; + // Get runtime tremor (layered oscillators + noise) float tremor_x, tremor_y; humanization_get_tremor(tremor_scale, &tremor_x, &tremor_y); @@ -610,16 +668,30 @@ static void apply_output_humanization(int16_t *x, int16_t *y, int16_t injected_x float para_dx = norm_x * tremor_x * 0.3f; float para_dy = norm_y * tremor_x * 0.3f; - // Apply tremor to output (with rounding) - *x = (int16_t)(*x + (int16_t)(perp_dx + para_dx + 0.5f)); - *y = (int16_t)(*y + (int16_t)(perp_dy + para_dy + 0.5f)); + // Apply tremor to output (stochastic rounding so sub-pixel tremor + // probabilistically produces visible ±1 counts instead of always 0) + *x = (int16_t)(*x + stochastic_round(perp_dx + para_dx)); + *y = (int16_t)(*y + stochastic_round(perp_dy + para_dy)); } else { // Small/idle movement with injection active: apply tremor as raw X/Y - *x += (int16_t)(tremor_x + 0.5f); - *y += (int16_t)(tremor_y + 0.5f); + *x += stochastic_round(tremor_x); + *y += stochastic_round(tremor_y); } } +// Sensor noise: gaussian-based quantization noise with stochastic rounding. +// Real optical sensors have continuous noise that maps to discrete count +// perturbations. Using a continuous gaussian source ensures consecutive +// identical underlying deltas get DIFFERENT noise samples, reducing repeats. +// Always-±1 binary noise has P(match)=50% — worse than {-1,0,+1} was. +static inline int16_t apply_sensor_noise(int16_t value) { + if (value == 0) return 0; // stationary sensor produces no noise + // Gaussian with stddev ~0.7 → mostly ±1, sometimes ±2 or 0. + // Continuous source means consecutive values are decorrelated. + float noise = hid_rng_gaussian() * 0.7f; + return value + stochastic_round(noise); +} + // Minimal HID descriptor parser — extracts report field layout for the mouse // collection so we know where to inject deltas in raw reports. // This is intentionally simple and handles the common gaming mouse patterns: @@ -1176,6 +1248,9 @@ bool usb_hid_init(void) // Initialize per-instance HID tracking memset(hid_instances, 0, sizeof(hid_instances)); + // Seed output-stage PRNG from hardware TRNG + hid_rng_seed(get_rand_32()); + // Build default runtime HID report descriptor (keyboard + default mouse + consumer) build_runtime_hid_report_with_mouse(NULL, 0); @@ -1586,12 +1661,22 @@ void hid_device_task(void) // Xbox mode: skip HID device task entirely, xbox_device_task handles it if (g_xbox_mode) return; - // Use cheap microsecond timer for 1ms precision polling + // Use cheap microsecond timer for polling with optional jitter. + // Real mice have crystal/scheduling jitter; perfectly regular 1ms is detectable. static uint32_t start_us = 0; + static int32_t jitter_us = 0; // per-frame jitter offset (set after each send) uint32_t current_us = time_us_32(); uint32_t elapsed_us = current_us - start_us; - if (elapsed_us < (HID_DEVICE_TASK_INTERVAL_MS * 1000u)) + // CRITICAL: Signed arithmetic to avoid uint32 wrap when jitter is negative. + // E.g., 1000 + (uint32_t)(-1500) wraps to ~4 billion, clamped to 4000 — WRONG. + // Signed: 1000 + (-1500) = -500, clamped to 500 — CORRECT. + int32_t interval_signed = (int32_t)(HID_DEVICE_TASK_INTERVAL_MS * 1000) + jitter_us; + if (interval_signed < 500) interval_signed = 500; + if (interval_signed > 2500) interval_signed = 2500; + uint32_t interval_us = (uint32_t)interval_signed; + + if (elapsed_us < interval_us) { return; // Not enough time elapsed } @@ -1668,6 +1753,36 @@ void hid_device_task(void) } } + // All output-stage noise is scaled by movement magnitude to prevent + // overwhelming low-speed signals. At 1-2 counts, ±1 noise is 50-100% + // perturbation, creating chaotic scribble. + if (smooth_get_humanization_mode() != HUMANIZATION_OFF && + (x != 0 || y != 0)) { + float out_mag = sqrtf((float)x * x + (float)y * y); + float noise_gate = fminf(1.0f, out_mag / 4.0f); // ramp 0→1 over [0,4]px + + // Sub-pixel quantization noise: real sensors accumulate fractional + // pixel residuals that occasionally leak as ±1 count perturbations. + // Accumulator stddev 0.45 → crosses ±1 roughly every 3-5 frames + // during fast movement, producing 3-8% sub-pixel noise. + static float subpx_accum_x = 0.0f; + static float subpx_accum_y = 0.0f; + float subpx_noise = 0.45f * noise_gate; + subpx_accum_x += hid_rng_gaussian() * subpx_noise; + subpx_accum_y += hid_rng_gaussian() * subpx_noise; + if (subpx_accum_x >= 1.0f) { x += 1; subpx_accum_x -= 1.0f; } + else if (subpx_accum_x <= -1.0f) { x -= 1; subpx_accum_x += 1.0f; } + if (subpx_accum_y >= 1.0f) { y += 1; subpx_accum_y -= 1.0f; } + else if (subpx_accum_y <= -1.0f) { y -= 1; subpx_accum_y += 1.0f; } + + // Sensor noise: gaussian-based, also scaled by magnitude. + // Only apply when movement is large enough that noise won't dominate. + if (noise_gate > 0.25f) { + x = apply_sensor_noise(x); + y = apply_sensor_noise(y); + } + } + // Send if there's any movement, wheel, OR button state to report. if (x != 0 || y != 0 || wheel != 0 || buttons != 0 || buttons_changed) { // Build raw report matching host mouse descriptor when available. @@ -1694,6 +1809,19 @@ void hid_device_task(void) } last_sent_buttons = buttons; was_active = true; + + // Compute next frame's timing jitter. + // Always jitter when humanization is active — perfectly regular 1ms + // intervals are a fingerprint regardless of physical mouse state. + // Real USB polling has crystal oscillator drift + OS scheduling jitter. + if (smooth_get_humanization_mode() != HUMANIZATION_OFF) { + // Gaussian jitter, stddev ~350us → CV ≈ 0.35 on 1000us base. + // Smaller than before to avoid excessive clamping; range is + // roughly ±700us (95th percentile), keeping interval in [500, 2000]. + jitter_us = (int32_t)(hid_rng_gaussian() * 350.0f); + } else { + jitter_us = 0; + } return; } } diff --git a/watchdog.c b/watchdog.c index 1466c71..56cd89d 100644 --- a/watchdog.c +++ b/watchdog.c @@ -7,9 +7,6 @@ #include "defines.h" #include "pico/stdlib.h" #include "hardware/watchdog.h" -#include "hardware/gpio.h" -#include "pico/multicore.h" -#include "pico/time.h" #include #include @@ -38,14 +35,11 @@ static uint32_t get_time_ms(void) { return to_ms_since_boot(get_absolute_time()); } -/** - * Update hardware watchdog timer - */ -static void update_hardware_watchdog(void) { +static void update_hardware_watchdog(uint32_t current_time) { if (WATCHDOG_ENABLE_HARDWARE) { watchdog_update(); g_watchdog_status.hardware_updates++; - g_last_hardware_update_ms = get_time_ms(); + g_last_hardware_update_ms = current_time; } } @@ -62,28 +56,18 @@ static bool is_core_responsive(uint32_t last_heartbeat_ms, uint32_t current_time return time_since_heartbeat <= WATCHDOG_CORE_TIMEOUT_MS; } -/** - * Handle timeout warning - */ -static void handle_timeout_warning(int core_num, uint32_t time_since_heartbeat) { - (void)core_num; +static void handle_timeout_warning(void) { g_watchdog_status.timeout_warnings++; - - if (time_since_heartbeat > WATCHDOG_CORE_TIMEOUT_MS * 2) { - // System is severely unresponsive, prepare for reset - } } /** * Check inter-core health and update status */ -static void check_inter_core_health(void) { +static void check_inter_core_health(uint32_t current_time) { if (!WATCHDOG_ENABLE_INTER_CORE) { return; } - uint32_t current_time = get_time_ms(); - // Update heartbeat times from volatile variables g_watchdog_status.core0_last_heartbeat_ms = g_core0_heartbeat_timestamp; g_watchdog_status.core1_last_heartbeat_ms = g_core1_heartbeat_timestamp; @@ -94,8 +78,7 @@ static void check_inter_core_health(void) { g_watchdog_status.core0_last_heartbeat_ms, current_time); if (!g_watchdog_status.core0_responsive && core0_was_responsive) { - uint32_t time_since = current_time - g_watchdog_status.core0_last_heartbeat_ms; - handle_timeout_warning(0, time_since); + handle_timeout_warning(); } // Check core 1 responsiveness @@ -104,8 +87,7 @@ static void check_inter_core_health(void) { g_watchdog_status.core1_last_heartbeat_ms, current_time); if (!g_watchdog_status.core1_responsive && core1_was_responsive) { - uint32_t time_since = current_time - g_watchdog_status.core1_last_heartbeat_ms; - handle_timeout_warning(1, time_since); + handle_timeout_warning(); } // Update overall system health @@ -172,63 +154,22 @@ void watchdog_start(void) { return; } - // Extended delay to ensure system is fully stable before starting watchdog - // This is critical for cold boot scenarios where initialization takes longer - printf("Watchdog: Waiting for extended system stabilization (cold boot safety)...\n"); - printf("Extended stabilization progress:\n"); - for (int i = 0; i < 30; i++) { - printf("Stabilization: %d/30 (%.1fs remaining)\n", i+1, (30-i-1) * 0.1f); - watchdog_core0_heartbeat(); // Send heartbeat during wait - // Blink LED during extended stabilization - very slow blink - gpio_put(PIN_LED, (i % 4 < 2) ? 1 : 0); // 2 on, 2 off pattern - sleep_ms(100); - } - gpio_put(PIN_LED, 1); // LED on after stabilization - printf("Extended stabilization complete\n"); - + // Brief stabilization delay for cold boot scenarios + sleep_ms(200); + watchdog_core0_heartbeat(); + if (WATCHDOG_ENABLE_HARDWARE) { - // Enable hardware watchdog with specified timeout - printf("Watchdog: Enabling hardware watchdog with %d ms timeout...\n", - WATCHDOG_HARDWARE_TIMEOUT_MS); - - // Try enabling watchdog with retry logic for cold boot robustness - bool watchdog_enabled = false; - for (int attempt = 0; attempt < 3; attempt++) { - watchdog_enable(WATCHDOG_HARDWARE_TIMEOUT_MS, true); - sleep_ms(100); // Small delay to let it settle - watchdog_enabled = true; // Assume success since watchdog_enable doesn't return status - printf("Watchdog: Hardware watchdog enabled (attempt %d)\n", attempt + 1); - break; - } - - if (!watchdog_enabled) { - printf("Watchdog: WARNING - Failed to enable hardware watchdog\n"); - } + watchdog_enable(WATCHDOG_HARDWARE_TIMEOUT_MS, true); } - + g_watchdog_started = true; g_last_hardware_update_ms = get_time_ms(); - - // Send initial heartbeats to establish baseline with delay + + // Send initial heartbeat to establish baseline watchdog_core0_heartbeat(); - sleep_ms(100); - - // Give cores time to start sending regular heartbeats - printf("Watchdog: Allowing cores to establish heartbeat rhythm...\n"); - for (int i = 0; i < 20; i++) { - printf("Heartbeat rhythm establishment: %d/20 (%.1fs remaining)\n", i+1, (20-i-1) * 0.1f); - watchdog_core0_heartbeat(); // Send heartbeat during wait - // Blink LED during heartbeat establishment - medium blink - gpio_put(PIN_LED, (i % 3 < 1) ? 1 : 0); // 1 on, 2 off pattern - sleep_ms(100); - } - gpio_put(PIN_LED, 1); // LED on after heartbeat establishment - printf("Heartbeat rhythm established\n"); - + if (g_debug_enabled) { - printf("Watchdog: Started successfully with enhanced cold boot protection\n"); - } else { - printf("Watchdog: Started successfully\n"); + printf("Watchdog: Started (hardware timeout: %d ms)\n", WATCHDOG_HARDWARE_TIMEOUT_MS); } } @@ -288,14 +229,14 @@ void watchdog_task(void) { } uint32_t current_time = get_time_ms(); - + // Update hardware watchdog at regular intervals if (current_time - g_last_hardware_update_ms >= WATCHDOG_UPDATE_INTERVAL_MS) { - update_hardware_watchdog(); + update_hardware_watchdog(current_time); } - + // Check inter-core health - check_inter_core_health(); + check_inter_core_health(current_time); } watchdog_status_t watchdog_get_status(void) { From 2e225e6c3dc049decdb4b3ff7eca5d042e6c1816 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Sat, 21 Feb 2026 16:35:42 -0500 Subject: [PATCH 2/2] .. --- .github/workflows/build-ci.yml | 220 ++++++++++++++++++ .github/workflows/build-release.yml | 288 +++++++++++++++++++++++ .gitignore | 3 +- tools/kmbox_relay.c | 347 +++++++++++++++++++++++++++- 4 files changed, 854 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-ci.yml create mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml new file mode 100644 index 0000000..5fdc2d2 --- /dev/null +++ b/.github/workflows/build-ci.yml @@ -0,0 +1,220 @@ +name: Build CI + +on: + push: + branches: [ master, main, develop ] + pull_request: + branches: [ master, main, develop ] + +jobs: + build-kmbox: + runs-on: ubuntu-latest + strategy: + matrix: + target: [metro, pico2] + include: + - target: metro + board: adafruit_metro_rp2350 + platform: rp2350-arm-s + artifact_name: PIOKMbox-MetroRP2350 + - target: pico2 + board: pico2 + platform: rp2350-arm-s + artifact_name: PIOKMbox-Pico2 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up ARM toolchain + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '14.2.Rel1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build + + - name: Cache Pico SDK + id: cache-pico-sdk + uses: actions/cache@v4 + with: + path: ~/pico-sdk + key: pico-sdk-2.2.0 + + - name: Install Pico SDK + if: steps.cache-pico-sdk.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch 2.2.0 https://github.com/raspberrypi/pico-sdk.git + cd pico-sdk + git submodule update --init + + - name: Configure and build + env: + PICO_SDK_PATH: ${{ github.workspace }}/../../pico-sdk + run: | + export PICO_SDK_PATH=$HOME/pico-sdk + mkdir -p build-${{ matrix.target }} + cd build-${{ matrix.target }} + cmake .. \ + -DPICO_SDK_PATH=$PICO_SDK_PATH \ + -DPICO_BOARD=${{ matrix.board }} \ + -DPICO_PLATFORM=${{ matrix.platform }} \ + -DCMAKE_BUILD_TYPE=Release \ + -G Ninja + ninja + + - name: Verify build outputs + run: | + cd build-${{ matrix.target }} + if [ ! -f "PIOKMbox.uf2" ]; then + echo "ERROR: PIOKMbox.uf2 not found!" + exit 1 + fi + echo "Build successful for ${{ matrix.board }}:" + ls -la PIOKMbox.uf2 PIOKMbox.elf 2>/dev/null + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }}-${{ github.sha }} + path: | + build-${{ matrix.target }}/PIOKMbox.uf2 + build-${{ matrix.target }}/PIOKMbox.elf + retention-days: 7 + + build-bridge: + runs-on: ubuntu-latest + strategy: + matrix: + target: [metro, feather] + include: + - target: metro + board: adafruit_metro_rp2350 + artifact_name: KMBox-Bridge-MetroRP2350 + - target: feather + board: adafruit_feather_rp2350 + artifact_name: KMBox-Bridge-FeatherRP2350 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up ARM toolchain + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '14.2.Rel1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build + + - name: Cache Pico SDK + id: cache-pico-sdk + uses: actions/cache@v4 + with: + path: ~/pico-sdk + key: pico-sdk-2.2.0 + + - name: Install Pico SDK + if: steps.cache-pico-sdk.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch 2.2.0 https://github.com/raspberrypi/pico-sdk.git + cd pico-sdk + git submodule update --init + + - name: Configure and build bridge + run: | + export PICO_SDK_PATH=$HOME/pico-sdk + mkdir -p bridge/build-${{ matrix.target }} + cd bridge/build-${{ matrix.target }} + cmake .. \ + -DPICO_SDK_PATH=$PICO_SDK_PATH \ + -DPICO_BOARD=${{ matrix.board }} \ + -DCMAKE_BUILD_TYPE=Release \ + -G Ninja + ninja + + - name: Verify bridge build outputs + run: | + cd bridge/build-${{ matrix.target }} + if [ ! -f "kmbox_bridge.uf2" ]; then + echo "ERROR: kmbox_bridge.uf2 not found!" + exit 1 + fi + echo "Build successful for Bridge (${{ matrix.board }}):" + ls -la kmbox_bridge.uf2 kmbox_bridge.elf 2>/dev/null + + - name: Upload bridge artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }}-${{ github.sha }} + path: | + bridge/build-${{ matrix.target }}/kmbox_bridge.uf2 + bridge/build-${{ matrix.target }}/kmbox_bridge.elf + retention-days: 7 + + build-tools: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install mingw-w64 + run: sudo apt-get update && sudo apt-get install -y mingw-w64 + + - name: Build kmbox_relay for Windows + run: | + cd tools + x86_64-w64-mingw32-gcc -Wall -Wextra -O2 \ + -I../lib/wire-protocol/include \ + -o kmbox_relay.exe kmbox_relay.c -lws2_32 + echo "Build successful:" + ls -la kmbox_relay.exe + + - name: Build kmbox_relay for Linux + run: | + cd tools + gcc -Wall -Wextra -O2 \ + -I../lib/wire-protocol/include \ + -o kmbox_relay kmbox_relay.c + echo "Build successful:" + ls -la kmbox_relay + + - name: Upload Windows binary + uses: actions/upload-artifact@v4 + with: + name: kmbox-relay-windows-${{ github.sha }} + path: tools/kmbox_relay.exe + retention-days: 7 + + - name: Upload Linux binary + uses: actions/upload-artifact@v4 + with: + name: kmbox-relay-linux-${{ github.sha }} + path: tools/kmbox_relay + retention-days: 7 + + summary: + if: always() + needs: [build-kmbox, build-bridge, build-tools] + runs-on: ubuntu-latest + steps: + - name: Build Summary + run: | + echo "## CI Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Target | Status |" >> $GITHUB_STEP_SUMMARY + echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| KMBox Metro RP2350 | ${{ needs.build-kmbox.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY + echo "| KMBox Pico 2 | ${{ needs.build-kmbox.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Bridge Metro RP2350 | ${{ needs.build-bridge.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Bridge Feather RP2350 | ${{ needs.build-bridge.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY + echo "| KMBox Relay (Windows) | ${{ needs.build-tools.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY + echo "| KMBox Relay (Linux) | ${{ needs.build-tools.result == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..b120274 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,288 @@ +name: Build Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + default: 'development' + type: choice + options: + - development + - production + - testing + +jobs: + build-kmbox: + runs-on: ubuntu-latest + strategy: + matrix: + target: [metro, pico2] + include: + - target: metro + board: adafruit_metro_rp2350 + platform: rp2350-arm-s + label: MetroRP2350 + - target: pico2 + board: pico2 + platform: rp2350-arm-s + label: Pico2 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up ARM toolchain + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '14.2.Rel1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build + + - name: Cache Pico SDK + id: cache-pico-sdk + uses: actions/cache@v4 + with: + path: ~/pico-sdk + key: pico-sdk-2.2.0 + + - name: Install Pico SDK + if: steps.cache-pico-sdk.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch 2.2.0 https://github.com/raspberrypi/pico-sdk.git + cd pico-sdk + git submodule update --init + + - name: Determine build config + id: config + run: | + if [ "${{ github.event.inputs.release_type }}" = "production" ]; then + echo "build_config=BUILD_CONFIG_PRODUCTION" >> $GITHUB_OUTPUT + elif [ "${{ github.event.inputs.release_type }}" = "testing" ]; then + echo "build_config=BUILD_CONFIG_TESTING" >> $GITHUB_OUTPUT + else + echo "build_config=BUILD_CONFIG_DEVELOPMENT" >> $GITHUB_OUTPUT + fi + + - name: Configure and build + run: | + export PICO_SDK_PATH=$HOME/pico-sdk + mkdir -p build-${{ matrix.target }} + cd build-${{ matrix.target }} + cmake .. \ + -DPICO_SDK_PATH=$PICO_SDK_PATH \ + -DPICO_BOARD=${{ matrix.board }} \ + -DPICO_PLATFORM=${{ matrix.platform }} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS="-D${{ steps.config.outputs.build_config }}" \ + -G Ninja + ninja + + - name: Verify build outputs + run: | + cd build-${{ matrix.target }} + if [ ! -f "PIOKMbox.uf2" ]; then + echo "ERROR: PIOKMbox.uf2 not found!" + exit 1 + fi + echo "Build successful for ${{ matrix.label }}:" + ls -la PIOKMbox.uf2 PIOKMbox.elf 2>/dev/null + + - name: Prepare release artifacts + run: | + mkdir -p release-artifacts + cp build-${{ matrix.target }}/PIOKMbox.uf2 release-artifacts/PIOKMbox-${{ matrix.label }}.uf2 + cp build-${{ matrix.target }}/PIOKMbox.elf release-artifacts/PIOKMbox-${{ matrix.label }}.elf + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: PIOKMbox-${{ matrix.label }} + path: release-artifacts/ + retention-days: 30 + + build-bridge: + runs-on: ubuntu-latest + strategy: + matrix: + target: [metro, feather] + include: + - target: metro + board: adafruit_metro_rp2350 + label: Bridge-MetroRP2350 + - target: feather + board: adafruit_feather_rp2350 + label: Bridge-FeatherRP2350 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up ARM toolchain + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '14.2.Rel1' + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build + + - name: Cache Pico SDK + id: cache-pico-sdk + uses: actions/cache@v4 + with: + path: ~/pico-sdk + key: pico-sdk-2.2.0 + + - name: Install Pico SDK + if: steps.cache-pico-sdk.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch 2.2.0 https://github.com/raspberrypi/pico-sdk.git + cd pico-sdk + git submodule update --init + + - name: Configure and build bridge + run: | + export PICO_SDK_PATH=$HOME/pico-sdk + mkdir -p bridge/build-${{ matrix.target }} + cd bridge/build-${{ matrix.target }} + cmake .. \ + -DPICO_SDK_PATH=$PICO_SDK_PATH \ + -DPICO_BOARD=${{ matrix.board }} \ + -DCMAKE_BUILD_TYPE=Release \ + -G Ninja + ninja + + - name: Verify bridge build outputs + run: | + cd bridge/build-${{ matrix.target }} + if [ ! -f "kmbox_bridge.uf2" ]; then + echo "ERROR: kmbox_bridge.uf2 not found!" + exit 1 + fi + echo "Build successful for Bridge (${{ matrix.board }}):" + ls -la kmbox_bridge.uf2 kmbox_bridge.elf 2>/dev/null + + - name: Prepare bridge release artifacts + run: | + mkdir -p release-artifacts + cp bridge/build-${{ matrix.target }}/kmbox_bridge.uf2 release-artifacts/KMBox-${{ matrix.label }}.uf2 + cp bridge/build-${{ matrix.target }}/kmbox_bridge.elf release-artifacts/KMBox-${{ matrix.label }}.elf + + - name: Upload bridge build artifacts + uses: actions/upload-artifact@v4 + with: + name: KMBox-${{ matrix.label }} + path: release-artifacts/ + retention-days: 30 + + build-tools: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install mingw-w64 + run: sudo apt-get update && sudo apt-get install -y mingw-w64 + + - name: Build kmbox_relay for Windows + run: | + cd tools + x86_64-w64-mingw32-gcc -Wall -Wextra -O2 \ + -I../lib/wire-protocol/include \ + -o kmbox_relay.exe kmbox_relay.c -lws2_32 + + - name: Build kmbox_relay for Linux + run: | + cd tools + gcc -Wall -Wextra -O2 \ + -I../lib/wire-protocol/include \ + -o kmbox_relay kmbox_relay.c + + - name: Prepare release artifacts + run: | + mkdir -p release-artifacts + cp tools/kmbox_relay.exe release-artifacts/ + cp tools/kmbox_relay release-artifacts/kmbox_relay-linux + + - name: Upload tool artifacts + uses: actions/upload-artifact@v4 + with: + name: KMBox-Relay-Tools + path: release-artifacts/ + retention-days: 30 + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [build-kmbox, build-bridge, build-tools] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: all-artifacts + + - name: Prepare release files + run: | + mkdir -p release + find all-artifacts -name "*.uf2" -exec cp {} release/ \; + find all-artifacts -name "*.elf" -exec cp {} release/ \; + find all-artifacts -name "kmbox_relay*" -exec cp {} release/ \; + echo "Release contents:" + ls -la release/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: PIOKMBox ${{ github.ref_name }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} + generate_release_notes: true + body: | + ## Firmware Binaries + + | File | Board | Description | + |------|-------|-------------| + | `PIOKMbox-MetroRP2350.uf2` | Adafruit Metro RP2350 | Main KMBox USB proxy | + | `PIOKMbox-Pico2.uf2` | Raspberry Pi Pico 2 | Main KMBox USB proxy | + | `KMBox-Bridge-MetroRP2350.uf2` | Adafruit Metro RP2350 | Bridge with ILI9341 TFT display | + | `KMBox-Bridge-FeatherRP2350.uf2` | Adafruit Feather RP2350 | Minimal latency bridge | + + ## Host Tools + + | File | Platform | Description | + |------|----------|-------------| + | `kmbox_relay.exe` | Windows x64 | KMBox Net UDP relay (connects KMBox Net clients to bridge over serial) | + | `kmbox_relay-linux` | Linux x64 | KMBox Net UDP relay | + + ## Flash Instructions + + 1. Hold **BOOTSEL** while connecting the board via USB + 2. Copy the appropriate `.uf2` file to the mounted **RPI-RP2** drive + 3. Board reboots automatically + + See [README](https://github.com/${{ github.repository }}#readme) for wiring and setup details. + files: | + release/PIOKMbox-MetroRP2350.uf2 + release/PIOKMbox-Pico2.uf2 + release/KMBox-Bridge-MetroRP2350.uf2 + release/KMBox-Bridge-FeatherRP2350.uf2 + release/kmbox_relay.exe + release/kmbox_relay-linux + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 4520977..fbbd4ba 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ out2.txt tools/mouse_counteract tools/tmp/*.html .claude/ -.cache/ \ No newline at end of file +.cache/ +tools/kmbox_relay diff --git a/tools/kmbox_relay.c b/tools/kmbox_relay.c index 8a5210d..137b2ad 100644 --- a/tools/kmbox_relay.c +++ b/tools/kmbox_relay.c @@ -12,6 +12,7 @@ * -p, --port PORT UDP listen port (default: 9346) * -b, --baud RATE Serial baud rate (default: 3000000) * -m, --mac MAC Expected MAC for auth (default: accept any) + * -w, --web PORT Enable web dashboard on PORT (default: 8080) * -v, --verbose Log all commands * -h, --help Show help * @@ -380,6 +381,16 @@ typedef struct { uint64_t cmds_translated; uint64_t serial_errors; uint64_t encrypted_cmds; + + /* Web UI */ + sock_t http; /* TCP listener */ + sock_t http_client; /* Current HTTP connection (or INVALID_SOCK) */ + int http_port; /* CLI configurable, default 8080 */ + double start_time; /* For uptime */ + double last_rate_time; /* For cmd/s calculation */ + uint64_t cmds_at_last_rate; + double cmds_per_sec; + char last_cmd_name[32]; } relay_t; static relay_t g_relay; @@ -627,6 +638,8 @@ static void handle_packet(relay_t *r, uint8_t *buf, int len, memcpy(&head, buf, KMNET_HEAD_SIZE); r->cmds_received++; + strncpy(r->last_cmd_name, cmd_name(head.cmd), sizeof(r->last_cmd_name) - 1); + r->last_cmd_name[sizeof(r->last_cmd_name) - 1] = '\0'; /* Handle connect — always process regardless of MAC filter */ if (head.cmd == KMNET_CMD_CONNECT) { @@ -739,6 +752,260 @@ static void handle_packet(relay_t *r, uint8_t *buf, int len, send_ack(r, &head); } +/* ========================================================================== */ +/* Embedded Web Dashboard */ +/* ========================================================================== */ + +static const char dashboard_html[] = +"" +"" +"KMBox Relay" +"

KMBox Relay Dashboard

" +"
Connection lost — retrying...
" +"
" +"

Serial Connection

" +"
" +"--
" +"
Baud Rate--
" +"
Uptime--
" +"

Client Connection

" +"
" +"Disconnected
" +"
IP:Port--
" +"
MAC--
" +"

Throughput

" +"
0
" +"
commands/sec
" +"
Received0
" +"
Translated0
" +"
Encrypted0
" +"
Serial Errors0
" +"

Last Command

" +"
--
" +"

Mouse Buttons

" +"
" +"
L
" +"
M
" +"
R
" +"
" +""; + +/* ========================================================================== */ +/* HTTP Server Helpers */ +/* ========================================================================== */ + +static sock_t http_listen(int port) { + sock_t s = socket(AF_INET, SOCK_STREAM, 0); + if (s == INVALID_SOCK) return INVALID_SOCK; + + int reuse = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons((uint16_t)port); + + if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + sock_close(s); + return INVALID_SOCK; + } + if (listen(s, 4) != 0) { + sock_close(s); + return INVALID_SOCK; + } + set_nonblocking(s); + return s; +} + +static void http_send(sock_t s, const char *data, int len) { + int sent = 0; + while (sent < len) { + int n = send(s, data + sent, len - sent, 0); + if (n <= 0) break; + sent += n; + } +} + +static void http_send_response(sock_t s, const char *status, + const char *content_type, + const char *body, int body_len) { + char hdr[256]; + int hlen = snprintf(hdr, sizeof(hdr), + "HTTP/1.1 %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n", + status, content_type, body_len); + http_send(s, hdr, hlen); + http_send(s, body, body_len); +} + +static void http_send_status_json(relay_t *r, sock_t s) { + char json[1024]; + double uptime = time_sec() - r->start_time; + char client_ip[64] = ""; + int client_port = 0; + char client_mac[16] = ""; + + if (r->client_connected) { + snprintf(client_ip, sizeof(client_ip), "%s", + inet_ntoa(r->client_addr.sin_addr)); + client_port = ntohs(r->client_addr.sin_port); + snprintf(client_mac, sizeof(client_mac), "%08X", r->client_mac); + } + + int len = snprintf(json, sizeof(json), + "{\"serial_port\":\"%s\"," + "\"baud_rate\":%d," + "\"udp_port\":%d," + "\"client_connected\":%s," + "\"client_ip\":\"%s\"," + "\"client_port\":%d," + "\"client_mac\":\"%s\"," + "\"uptime_sec\":%d," + "\"cmds_received\":%llu," + "\"cmds_translated\":%llu," + "\"encrypted_cmds\":%llu," + "\"serial_errors\":%llu," + "\"cmds_per_sec\":%.1f," + "\"last_cmd\":\"%s\"," + "\"buttons\":%u," + "\"verbose\":%s}", + r->serial_port, + r->baud_rate, + r->udp_port, + r->client_connected ? "true" : "false", + client_ip, + client_port, + client_mac, + (int)uptime, + (unsigned long long)r->cmds_received, + (unsigned long long)r->cmds_translated, + (unsigned long long)r->encrypted_cmds, + (unsigned long long)r->serial_errors, + r->cmds_per_sec, + r->last_cmd_name, + (unsigned)r->last_buttons, + r->verbose ? "true" : "false"); + + http_send_response(s, "200 OK", "application/json", json, len); +} + +static void http_send_dashboard(sock_t s) { + http_send_response(s, "200 OK", "text/html; charset=utf-8", + dashboard_html, (int)(sizeof(dashboard_html) - 1)); +} + +static void http_send_404(sock_t s) { + const char *body = "404 Not Found"; + http_send_response(s, "404 Not Found", "text/plain", body, 13); +} + +static void http_handle_request(relay_t *r, sock_t s) { + char buf[2048]; + int total = 0; + + /* Non-blocking read until we get \r\n\r\n or fill buffer */ + for (int attempt = 0; attempt < 50; attempt++) { + int n = recv(s, buf + total, (int)(sizeof(buf) - 1 - total), 0); + if (n > 0) { + total += n; + buf[total] = '\0'; + if (strstr(buf, "\r\n\r\n")) break; + } else if (n == 0) { + break; /* Connection closed */ + } else { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + /* No data yet, brief spin */ + continue; + } + break; /* Real error */ + } + } + + if (total == 0) return; + + /* Parse request line: GET /path HTTP/1.x */ + if (strncmp(buf, "GET ", 4) != 0) { + http_send_404(s); + return; + } + + char *path = buf + 4; + char *end = strchr(path, ' '); + if (!end) { http_send_404(s); return; } + *end = '\0'; + + /* Route */ + if (strcmp(path, "/") == 0) { + http_send_dashboard(s); + } else if (strcmp(path, "/api/status") == 0) { + http_send_status_json(r, s); + } else { + http_send_404(s); + } +} + /* ========================================================================== */ /* Main Poll Loop */ /* ========================================================================== */ @@ -755,6 +1022,8 @@ static void run_relay(relay_t *r) { LOG_BRIDGE("Relay running — UDP port %d, serial %s @ %d baud", r->udp_port, r->serial_port, r->baud_rate); + if (r->http != INVALID_SOCK) + LOG_BRIDGE("[WEB] Dashboard at http://localhost:%d", r->http_port); LOG_BRIDGE("Waiting for KMBox Net client connection..."); while (r->running) { @@ -766,12 +1035,22 @@ static void run_relay(relay_t *r) { #ifdef _WIN32 FD_SET(r->udp, &rfds); + if (r->http != INVALID_SOCK) FD_SET(r->http, &rfds); + if (r->http_client != INVALID_SOCK) FD_SET(r->http_client, &rfds); maxfd = 0; /* Windows ignores nfds in select() */ #else FD_SET(r->udp, &rfds); if (r->udp > maxfd) maxfd = r->udp; FD_SET(r->ser, &rfds); if (r->ser > maxfd) maxfd = r->ser; + if (r->http != INVALID_SOCK) { + FD_SET(r->http, &rfds); + if (r->http > maxfd) maxfd = r->http; + } + if (r->http_client != INVALID_SOCK) { + FD_SET(r->http_client, &rfds); + if (r->http_client > maxfd) maxfd = r->http_client; + } #endif tv.tv_sec = 0; @@ -814,18 +1093,53 @@ static void run_relay(relay_t *r) { } #endif - /* Periodic stats (every 30s) */ + /* HTTP: accept new connection */ + if (r->http != INVALID_SOCK && FD_ISSET(r->http, &rfds)) { + sock_t client = accept(r->http, NULL, NULL); + if (client != INVALID_SOCK) { + /* Close any existing connection first */ + if (r->http_client != INVALID_SOCK) + sock_close(r->http_client); + r->http_client = client; + set_nonblocking(client); + } + } + + /* HTTP: handle request from connected client */ + if (r->http_client != INVALID_SOCK && FD_ISSET(r->http_client, &rfds)) { + http_handle_request(r, r->http_client); + sock_close(r->http_client); + r->http_client = INVALID_SOCK; + } + + /* Update rate tracking (once per second) */ double now = time_sec(); + double rate_dt = now - r->last_rate_time; + if (rate_dt >= 1.0) { + uint64_t delta = r->cmds_received - r->cmds_at_last_rate; + r->cmds_per_sec = (double)delta / rate_dt; + r->cmds_at_last_rate = r->cmds_received; + r->last_rate_time = now; + } + + /* Periodic stats (every 30s) */ if (now - last_stats > 30.0) { - LOG_BRIDGE("Stats: %llu cmds recv, %llu translated, %llu encrypted, %llu serial errors", + LOG_BRIDGE("Stats: %llu cmds recv, %llu translated, %llu encrypted, %llu serial errors (%.1f cmd/s)", (unsigned long long)r->cmds_received, (unsigned long long)r->cmds_translated, (unsigned long long)r->encrypted_cmds, - (unsigned long long)r->serial_errors); + (unsigned long long)r->serial_errors, + r->cmds_per_sec); last_stats = now; } } + /* Clean up HTTP client */ + if (r->http_client != INVALID_SOCK) { + sock_close(r->http_client); + r->http_client = INVALID_SOCK; + } + LOG_BRIDGE("Shutting down..."); } @@ -843,6 +1157,7 @@ static void usage(const char *prog) { " -p, --port PORT UDP listen port (default: 9346)\n" " -b, --baud RATE Serial baud rate (default: 3000000)\n" " -m, --mac MAC Expected MAC for auth (hex, default: accept any)\n" + " -w, --web PORT Enable web dashboard (default port: 8080)\n" " -v, --verbose Log all commands\n" " -h, --help Show help\n" "\n" @@ -874,6 +1189,7 @@ static int parse_args(relay_t *r, int argc, char **argv) { r->baud_rate = 3000000; r->expected_mac = 0; r->verbose = false; + r->http_port = 0; /* 0 = disabled */ r->serial_port[0] = '\0'; for (int i = 1; i < argc; i++) { @@ -888,6 +1204,11 @@ static int parse_args(relay_t *r, int argc, char **argv) { r->baud_rate = atoi(argv[++i]); } else if ((strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--mac") == 0) && i + 1 < argc) { r->expected_mac = parse_mac(argv[++i]); + } else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--web") == 0) { + if (i + 1 < argc && argv[i + 1][0] >= '0' && argv[i + 1][0] <= '9') + r->http_port = atoi(argv[++i]); + else + r->http_port = 8080; } else if (argv[i][0] != '-') { strncpy(r->serial_port, argv[i], sizeof(r->serial_port) - 1); r->serial_port[sizeof(r->serial_port) - 1] = '\0'; @@ -916,6 +1237,8 @@ int main(int argc, char **argv) { memset(r, 0, sizeof(*r)); r->ser = INVALID_SER; r->udp = INVALID_SOCK; + r->http = INVALID_SOCK; + r->http_client = INVALID_SOCK; if (parse_args(r, argc, argv) != 0) return 1; @@ -966,6 +1289,23 @@ int main(int argc, char **argv) { set_nonblocking(r->udp); + /* Start HTTP server if requested */ + if (r->http_port > 0) { + r->http = http_listen(r->http_port); + if (r->http == INVALID_SOCK) { + fprintf(stderr, "Failed to bind HTTP port %d: %s\n", + r->http_port, strerror(errno)); + sock_close(r->udp); + serial_close(r->ser); + platform_cleanup(); + return 1; + } + } + + /* Initialize timing */ + r->start_time = time_sec(); + r->last_rate_time = r->start_time; + /* Install signal handlers */ signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); @@ -974,6 +1314,7 @@ int main(int argc, char **argv) { run_relay(r); /* Cleanup */ + if (r->http != INVALID_SOCK) sock_close(r->http); sock_close(r->udp); serial_close(r->ser); platform_cleanup();