From 2423ffc89557e06a4f63853e365dec94fe49eac6 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Sat, 21 Feb 2026 16:26:25 -0500 Subject: [PATCH 1/6] 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 zcmeHQdwf*Yoj-SGLS`lb5(o(|$qk53fCK{JF=%8aAp+s)WE2)_CzF{;X2@hFnFk~& zG6>Wl)p8Z9wYJ1ZYZ6QwTeXgT7;U#3yRHT7gRQ$uun!ZY)hb}3K=%8)_fB#dvC!Rq zcK^AT&&m1y&f|A}=llDe-#zyx_wdV4{(Lc=F^=KVAdN%Pr!cmc@t_!+gtQXL;n-kZ zQNFp{ww_8ay~NI!7JY^zImgk^;jou)vR_&)_P1PG#$+CF0YmX}I9$O%ctpWu zc@q~)^7L|h;u#HebN)@C9muB7dnp z4la}Q^U6YqPnIE9i5c1TQ6z_Bxz%oUl&xP^DOuMd$zjOFD=DWrQzBS1kQ|QM;0S7; zFHbMqf#fQ28wDA;NQvuGy%MF!V@Y}`s)$tTMZCjN9Uk$HgkDr5UolBuQ$jBkEA>(o ziycPR)0$Q5R+ledC85PwcamO^OV>n$l#?8#tYXZBaHILmgMk*v{JeX>qZmbOB9a&U z^~jI!Wvmqf(m-!OqDv%W)OKD+5uM*6|?>GM&>BS9rxBxS5fi}d%Y^xn99qxH zi^g4oM0GR|##SI)a3i`vpUUQN#8Y2bS_r*-)nX%d9Ws(%br0el@_d;myy&_HiRxqb z!oMXYsaU;K1XKi61XKi61XKi61XKi61XKi61XKi61XKi61XKi61VjYhoBdLK{|v7F zd{EQ=Nv*bhz@ckDw=t#t%=%I77w$}LzwlE|7}Cx@`jM9F!272U^rJ0~- zc33Ujum#wzh8?!R4y#}Xyzl;Cm4S=r!_HofWe~6ZKgiFt4DyHxeNVCWJ}ny**TKO& z;V#&L{1frQY|^n}y!-zkjCj$j9pCXP>||xFq1I7YKj{ODX{`^<;07s{43u#K+dZa} z?H=8EQmYq!h&EMeY+?Tt%itNUVOC%6jOgwMMS52X zvuA_Gd;F{k82^QFmcgf>H-g_0HZ@Nuh8|C7W47tY8!-;esTC=CF14tufQ&52I0zXr zv^k%c>l?h_ko!{kPk@b{mvpQG{(8%!HWTJbI^&hUT^~DO^UC}#;F}G8D|ld;m5sKG zK;!ouS?9D_9r)cxRyO9Pp&T-MfyYU#yJC!|r2EpWgNELR9^5zd%7=Q+aG#+{up#g0 zOXCKI#v-|N%|ofkwcKE@c0z{}^R0%AcOZ{=4!j$@1$H8SCTKm2jq{G<;(D?nY@7uf z8&Qtmu*BCQ|47T)MO(+FLPl1{G?WpZZM!!Ph<<^4MLzmq-tX=D7R8IK4%|zG3wN%- z{pDudXUcKES&sWoS^N25X?y<+YyEq(d;Hy6$8StBM(1Gj6B>QpY~XliHyiBJ>ICvJ z($N~b&&F|GAp^$E)v+Ruu$6UX82237iTcgU7d}p52cwt^ep7O+0PTB49Q48klut&V z&EqYDE47&4)0X@+%0M>FKPr(YxVqWm_@+k#|8 z(jX0?tSXy}lU}Xx(}gHIdjpIA{`X8cIdsT&5BNPu@1xz&&N1!yJ=q5bH9Fzky;^gN z#?q`ebZ^%0$f)f57p^&$ZHb@UtrawynC<&$-;MQO2|XUxSmNIXPxk0Xjx{9NBV!u$ z9pCXX%Bb)6C?97Lb$J39Jp%tMLw|7%JbsP126oA7pbKqH(1m4M#rClWnEgqV-G@Gr zY-@Z-#6C;(pF=3uvuX1sdt`O&!h9Y>8P>>=gU1X4;c*f+kE5)Yljs*gN2_&aTG!pE zZvlT+YaKk7YK1OX&IdhOj08CUzwGl*rcYO23ESY{HQM4qp=FnLDMbdTl&W)9@+*k>A(dQ(s1Nh0pxqr&{5UA2G<^WrKWMHspVbd&kaH z*0|cq>^rrZMp4h_mYa13%l3~`4^8HA4`=tT@9fu_1b(lkliR(aGjEr+lij&(wDoJ**V($L; z7h2=*bH<9(Tzc9YTzbW`ByYSip3hB43t;TSpA0R;{rSkMIO1*!6IR99-~jxN?jbvu zvBvgnW*-2COxs;@3U;#O#Y$$Qy{}Vh@9SiP{U({)16EEc!qcb9aR?z)kD?DYqk`NU*4cw#Aw8+J^reEoaOY(U(4{YA?4m9M|d z%soFro7c1hC&uXpXdV6=?B1(QK@2wu-vE8NHBRdgvAZh|v9|?nZb!W8{n!%U54!JN z>u`MRhi-jX@4aGNnj-Z%Jy*CB?RpR&X)lpjt0u%vYESoJJg?YE7u;Znb=eO3O1gN^ zhGLEAV+Jes@!~zEEAP>H!WU6@EpXbiK_|raSYA3=CdF*>(-in}BkG+RmPOcmN3&wW zQIu1p6Ie>DM2~&aO5A@*uRUm+z{n2zN_s6ro7YK$yqPVN4|^Z5 z{p;J3U!>ZHz z3sIi2Zr40!&pnB;O0ge0QJUYQ(PZ``E#dUD^vl?|Tu#?G{RU242<8?P+!{1$bP%2NccC@w8 z;iC`R>dt&m9J>|!S&`R$U*uoSlkB>B0kf~3BeD6+?u9Mev^h&& z9G|m9S`W+3JsL^R*A#j_coll?ko8QA(RaJJC+f=cnuV>XYc10pr9E0c=zOfzd~1rZ zh+}4x0q->sGff6I7{r`@q43Qr#7*Nf58Hlm;T6ouSeJ+Q3FNDX=fGEUE%BDi=-(#m zPdX=Xt_FQ2|5%GQgmcd{HhviAGiG50$`iVmA)brlo0vW4_oeygev23=>+l0$*mt+S zvTa7rl9v|bEYa@-T}HS9mdKLQ3l=bVoiH$cbW8?eZR_wKOHwX_v zw;sF?AbTcknZRPbVyxV&7|Vz@!+Wsv^94@cqo$&qu*k!letMIafQiE;g1M)0cB-vgLlh zklDwMWo`Tz);6!uz^hnJ8%`_-I*8ts?|O^*Q`mmAY;z~da?!r!DeC~mhL)$efkb<1E82cO`>O@- z7@F#~Ey>lI>Nwc_6ztA@Ij=AecF%*|^I-Scu={@4d<)X1i$le^+Wb0pamcQh?fw$% z{^C<3w%5zHzX#(dY>x-aE84yUZOHZmiMW?uA}m4K8?b$!wtP^Gd6_AKiNmvWT16c3 zE}pn|&&9lZ5Ibm}ytv5Nvn<=Iw?9zt46xn4nWGDP!)>Y^?LDsJa@uMFp zH+N#lh&5QT)+}JHb=5623tPDR9y=A}+WW4TW4C*WkcxifA186Ie3|>~nosOI{?`60 z6Ef%?PIH)o=NRQP&{4>Y=`a5*(v!*-Q6G9HK8g1a8XnK`Xiv`_Q?vt%-yX$^6S#Om z6E0a8E3yCh1KM6`&HfT?$Zo^?kJdQ?{!TdSJ1E!Fv*G=$F^2c4*r*Nj3_Oj64a@LY zuYypMx!~q)b6)6sgW10q~dq7Mv=7y^n^9IG3^p1Wp z_!lwOq)&%($Y=cE6aIPQ=g1Q=m*0I|k2dtq$%veMW-axX_R0tH=FAc3ebY3tE_Uoz zs+0HLGeKVtAEoRJw2|-g2iWOC!7AV9N0XdUmbiv;Jj>C$Gz<2Z7n$@vhvt}gSCj_& zO5>bEzt8op1Ji`Rpgs#WIr7O+&Rd|drn_>TIy1eG?ECh^wjS*Na+$s^9qXg$Eat4oo;ZO? z73S>C*k|+JEY-gXSz_N0q;cC6cvWEzJqq55zG7SZB5@qAV*XXok@%JxY5pAgiftn@ zR*LylL8p~?#?<2(Q`8^4*!E|V+&+c=RlwEjh7F#1N^ENdFSc!!$$_{D9*ID43RA+cWdZ%M}fr;-`8_=eAEMmNi;k_(7h~I^D z(eFaq@cW9(yz>_07QKT(JU!ToHfPq0_u>P2Z6yNku?H{Qxg2_x;T?WyJM!FLE< z8EDHixxW$VQ3~3qptBY9WCcAT-ptmdNyA||35`7Ph(f#uRcC*vx_qh2; zkPkL^0({sL@;Uu{LogKKXNH#|8QJZnwpGhll;6RZ1p@((D z4T|Q#3hF`CJ6-J7KwTiXEdW3wzFBy#$_+r!ndadE7=LTfMe4*u<~%M8vt- zJbq_0|4!^6zcLt(6jb`$9)3e87zw(9e!ev1bJu!|##_VAS`W$QZ3f9*<1MV>C>7{05CAkf{OUufZ zFXzkFuU%j?mic|2Km_B8PE3|Xm%4gb4c`j-BBtp@^A{J+!@r{GbNK0mA>|1n)AO+q zHd<}#Xc{3Kpn|#$g=VRnUiq2OPAa5lHBO@MW`FuW5f$x$x|&XEL5!Hu*VNen(}#Z zmB?#wd3ourEA9aBl7obrP>$8z9PubaK%&;;bYIdt5h5!8fopeol9k?QM4Y7?%Tk(3 zLfZ{a3NZ*@K|buovbjX*vNfy9*V&bEt0n|fA+BL&Cb%(L&s(yIESqRb7GAY(#ZuAl z>wV#RXT$|3_auBj(LOQ$s@g!1zFsAva0%F5>0}5F?Dj-cgt8J?(KN3~6v87ELTn^l zGBe5}s-ZFpDd;d9((Yg*y+fYrAUv+%CcXi~Da6(X;k>ZEyq%HdU^MJ;)I*v~`aLy~ zM5)j1_9x0izFKdhVw>0F@ym@6KZ0WSI!|+T5R1ftxWJ@|vTDzEpC^&;e2Mt8Xwis(YIrBUiRKBd`J2c@5~ToFoHnAQ zKjMURMMVMw#nA33;xfV${Gp!QA>ls~B3IE6aMD8I5gL6p0P(ZxphdLdiuIqFMMAJV zQ4svZ&k$aUoFTf(YNSy zS;U_sKj<6UbiBlvh(y=aDU5sk8tfjmcp}GVvM8loab@5!n68W%jX}B8Gb1K7%!tu9 z_*72c2~&>02@`X@oEzjE@0@UD;PFYyrSIZ1@MtFS^rbJ=kCk)mXGK0!&guLCsvj@s zS#pkjHLi?Uww&W(Tjc4xUn-v@=lGiwk*DvDsa(P@BSzoz5-%MKkP(x<70-zAVmXUR znfg;jKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`B zKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`BKt(`B zKt(`BKt(`BKt(`BKt(`BKt(`B;QtQ+9z$l z)VLiu`NBbmd(`-Y+o)bVdw~d?LW34AFIwTar%Ov}qd0j(DsrPk19LbdK_7F_QBBO@ zs)0~nxWVH|Fu?@l$LUUvS`SWUsrR5E9eBeWHLgGe1JpF&P?Sgw2=NpMh7)$6TObm2 zf(*K-3A#J|QeQfGiaBs@6vm*lp6HYohS|_5HIxV3ks##}I#3FyC4u(U;-o0#;t5m8 zBOzBkR7WmP8dSj5(99eWSB*a!_7VYQosme0NC=ZjQF}>CvN7WvOgy83VAitZg*Z>b zQSWtlnmn#(#N+TnOHYUy>Ycs-!=Vqy3m;#Uj{lGVXFszsND=&pG2|yZWgh2Li~K&B zKT|C62Z^UcCSnp#!;R&n(;}!podYoz1pR;T#M3zs$cYrM8}U=gmnZQXllU!3d^Cx_ zGl}1o#N&MCOUC@U%;EQv=*xTJn#5?`Ifdy{xNr-a;x&L^R>Na!3AI)jAH zAEC2H$dl;I5%Og^Ya|Pa&KM!TqO(OP*3p?Fbe;&EB|`p1XNb`GA#`>KkCcZr9cc#A zwMa9Ou0v9eP3NM}nJE9)I`9ZtpIw#?aapRN@gE)<<%sL>;-D%&;-3SjSvdkuiu@Fx z9duk0bJQYyf4nMrY>;?S3dOq?;7L3dh1)p z=?haHzG3Q%cirFd=Ed!N(`nn@y`O%2L(MLA3;Q40b=tE0z{~yV3o6YEJ~DQG_{WOB zJpEry#_4~_t*-Z^{gDrr^)} zYr1v6J$kfGvwl;`&|S~pwSDjJe!PF>zB$kQZ|gPpU!4Bd5BeV*{L;8nH+^ONukZNs 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/6] .. --- .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(); From 169b4d56fea45c9583a102f5771f4e4b3f1760b8 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Sun, 22 Feb 2026 23:19:09 -0500 Subject: [PATCH 3/6] TFT UI: improve visual design and rendering performance Performance: - Replace naive O(sqrt(n)) isqrt loops in circle rendering with fast Newton's method (~5x fewer iterations for r=50) - Add FNV-1a stats hash to skip re-rendering unchanged frames, saving CPU cycles in steady state when display content is static - Replace sparse pixel-by-pixel gauge arcs with proper filled arc sectors using cross-product angle tests for clean, thick arcs Visual design: - Add colored status indicator dots next to Host/KMBox connection text - Add visual progress bar for queue depth in detailed view (was text-only) - Color-code latency values: green <200us, yellow <500us, red >=500us - Color-code jitter values: green <50us, yellow <150us, red >=150us - Add small cyan accent bars before section header titles - Improve gauge view: thicker track ring, tick marks at 0/50/100%, white value text for better contrast, label below center - Remove strlen() call in gauge label rendering (manual length calc) - Redesign splash screen with bordered frame and accent divider line Both Metro RP2350 (TFT) and Feather RP2350 (no TFT) builds verified. Co-Authored-By: Claude Opus 4.6 --- bridge/tft_display.c | 293 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 233 insertions(+), 60 deletions(-) diff --git a/bridge/tft_display.c b/bridge/tft_display.c index 99c52ce..1867683 100644 --- a/bridge/tft_display.c +++ b/bridge/tft_display.c @@ -142,6 +142,22 @@ static const int16_t sin_lut[SINCOS_LUT_SIZE] = { static inline int16_t lut_sin(int angle_idx) { return sin_lut[angle_idx % SINCOS_LUT_SIZE]; } static inline int16_t lut_cos(int angle_idx) { return sin_lut[(angle_idx + 45) % SINCOS_LUT_SIZE]; } // +45 entries = +90° +// Fast integer square root (Newton's method, 4 iterations — good for r<=320) +static inline int fast_isqrt(int v) { + if (v <= 0) return 0; + int x = v; + // Initial estimate: highest bit / 2 + int s = 1; + while (s * s <= v) s <<= 1; + x = s; + x = (x + v / x) >> 1; + x = (x + v / x) >> 1; + x = (x + v / x) >> 1; + // Correct off-by-one + if ((x + 1) * (x + 1) <= v) x++; + return x; +} + // ============================================================================ // State — timer-driven background rendering // @@ -210,6 +226,38 @@ static volatile bool stats_pending = false; // Main loop set, timer clears // Frame ready flag: timer ISR sets after drawing, main loop clears after DMA static volatile bool frame_ready = false; +// Stats hash for frame-skip optimization: skip render if stats unchanged +static uint32_t last_stats_hash = 0; + +// Lightweight FNV-1a hash over the key volatile fields of stats +static uint32_t stats_quick_hash(const tft_stats_t *s) { + uint32_t h = 2166136261u; + #define HASH_MIX(val) do { \ + uint32_t v = (uint32_t)(val); \ + h ^= v; h *= 16777619u; \ + } while(0) + HASH_MIX(s->cdc_connected); + HASH_MIX(s->kmbox_connected); + HASH_MIX(s->humanization_mode); + HASH_MIX(s->inject_mode); + HASH_MIX(s->queue_depth); + HASH_MIX(s->tx_rate_bps); + HASH_MIX(s->rx_rate_bps); + HASH_MIX(s->mouse_moves); + HASH_MIX(s->commands_per_sec); + HASH_MIX(s->latency_avg_us); + HASH_MIX(s->latency_jitter_us); + HASH_MIX(s->uptime_sec); + HASH_MIX(s->button_presses); + HASH_MIX(s->total_injected); + HASH_MIX(s->uart_errors + s->frame_errors); + HASH_MIX((uint32_t)(s->bridge_temperature_c * 10)); + HASH_MIX(s->console_mode); + HASH_MIX(s->gamepad_buttons); + #undef HASH_MIX + return h; +} + // Repeating timer handle static repeating_timer_t tft_render_timer; @@ -293,6 +341,12 @@ static inline uint8_t hmode_color(uint8_t m) { #define TEMP_VALID(t) ((t) > -50.0f && (t) < 150.0f) +static inline uint8_t latency_color(uint32_t us) { + if (us < 200) return COL_GREEN; + if (us < 500) return COL_YELLOW; + return COL_RED; +} + // ============================================================================ // Touch Interrupt Callback // ============================================================================ @@ -569,11 +623,13 @@ static void format_stats(const tft_stats_t *stats) { } else { fmt_clear(&fmt_rx_peak); } } -// Helper to draw a section header label +// Helper to draw a section header label with accent bar static int draw_section_header(int y, const char *title) { hline(y, COL_DIM_LINE); y += SEP_GAP + 1; - tft_draw_string(MARGIN, y, COL_CYAN, title); + // Small accent bar before title + box(MARGIN, y + 4, 3, FONT_H - 8, COL_CYAN); + tft_draw_string(MARGIN + 6, y, COL_CYAN, title); y += LINE_H; return y; } @@ -665,22 +721,28 @@ static void draw_stats(const tft_stats_t *stats) { hline(y, COL_DIM_LINE); y += SEP_GAP + 2; - // === CONNECTION STATUS === + // === CONNECTION STATUS (with indicator dots) === { uint8_t cdc_col = stats->cdc_connected ? COL_GREEN : COL_RED; uint8_t km_col = stats->kmbox_connected ? COL_GREEN : COL_RED; - - tft_draw_string(MARGIN, y, COL_GRAY, "Host"); - tft_draw_string(MARGIN + 5 * FONT_W, y, cdc_col, + + // Status dot (filled 5x5 square) + label + box(MARGIN, y + 5, 5, 5, cdc_col); + tft_draw_string(MARGIN + 8, y, COL_GRAY, "Host"); + tft_draw_string(MARGIN + 8 + 5 * FONT_W, y, cdc_col, stats->cdc_connected ? "OK" : "--"); #if (TFT_RAW_WIDTH >= 240) - tft_draw_string(MARGIN + 10 * FONT_W, y, COL_GRAY, "KMBox"); - tft_draw_string(MARGIN + 16 * FONT_W, y, km_col, + int km_x = MARGIN + 14 * FONT_W; + box(km_x, y + 5, 5, 5, km_col); + tft_draw_string(km_x + 8, y, COL_GRAY, "KMBox"); + tft_draw_string(km_x + 8 + 6 * FONT_W, y, km_col, stats->kmbox_connected ? "OK" : "--"); #else - tft_draw_string(MARGIN + 8 * FONT_W, y, COL_GRAY, "KM"); - tft_draw_string(MARGIN + 11 * FONT_W, y, km_col, + int km_x = MARGIN + 9 * FONT_W; + box(km_x, y + 5, 5, 5, km_col); + tft_draw_string(km_x + 8, y, COL_GRAY, "KM"); + tft_draw_string(km_x + 8 + 3 * FONT_W, y, km_col, stats->kmbox_connected ? "OK" : "--"); #endif y += LINE_H; @@ -713,14 +775,30 @@ static void draw_stats(const tft_stats_t *stats) { } y += LINE_H; - // Queue bar + injection count + // Queue bar + injection count (with visual bar) if (fmt_queuebar.len) { tft_draw_string(MARGIN, y, COL_GRAY, "Queue"); + uint8_t q_col = COL_GREEN; if (stats->queue_depth > stats->queue_capacity * 3 / 4) q_col = COL_RED; else if (stats->queue_depth > stats->queue_capacity / 2) q_col = COL_YELLOW; - tft_draw_string(MARGIN + 6 * FONT_W, y, q_col, fmt_queuebar.str); - + + // Visual progress bar + int bar_x = MARGIN + 6 * FONT_W; + int bar_w = TFT_WIDTH - bar_x - MARGIN; + if (fmt_injcount.len) bar_w -= (fmt_injcount.len + 1) * FONT_W; + int bar_h = FONT_H - 4; + int bar_y = y + 2; + box(bar_x, bar_y, bar_w, bar_h, COL_DARK); + if (stats->queue_capacity > 0) { + int fill = (int)((float)stats->queue_depth / stats->queue_capacity * bar_w); + if (fill > bar_w) fill = bar_w; + if (fill > 0) box(bar_x, bar_y, fill, bar_h, q_col); + } + // Overlay text on bar + int txt_x = bar_x + (bar_w - fmt_queuebar.len * FONT_W) / 2; + tft_draw_string(txt_x, y, COL_WHITE, fmt_queuebar.str); + if (fmt_injcount.len) { int ix = TFT_WIDTH - MARGIN - fmt_injcount.len * FONT_W; tft_draw_string(ix, y, COL_GRAY, fmt_injcount.str); @@ -808,17 +886,21 @@ static void draw_stats(const tft_stats_t *stats) { y += SECTION_GAP; y = draw_section_header(y, "Latency"); - draw_row_lr(y, "Avg", COL_GRAY, fmt_lat_avg.str, fmt_lat_avg.len, COL_GREEN); + draw_row_lr(y, "Avg", COL_GRAY, fmt_lat_avg.str, fmt_lat_avg.len, + latency_color(stats->latency_avg_us)); y += LINE_H; - + #if (TFT_RAW_WIDTH >= 240) if (fmt_lat_range.len) { - draw_row_lr(y, "Range", COL_GRAY, fmt_lat_range.str, fmt_lat_range.len, COL_GRAY); + draw_row_lr(y, "Range", COL_GRAY, fmt_lat_range.str, fmt_lat_range.len, + latency_color(stats->latency_max_us)); y += LINE_H; } #endif if (fmt_lat_jitter.len) { - draw_row_lr(y, "Jitter", COL_GRAY, fmt_lat_jitter.str, fmt_lat_jitter.len, COL_YELLOW); + uint8_t jit_col = stats->latency_jitter_us < 50 ? COL_GREEN : + stats->latency_jitter_us < 150 ? COL_YELLOW : COL_RED; + draw_row_lr(y, "Jitter", COL_GRAY, fmt_lat_jitter.str, fmt_lat_jitter.len, jit_col); y += LINE_H; } } @@ -908,20 +990,15 @@ static void draw_circle_ring(int cx, int cy, int r_inner, int r_outer, uint8_t c if (py < 0 || py >= TFT_HEIGHT) continue; int dy2 = dy * dy; - // Solve for x range: r_inner^2 <= dx^2+dy^2 <= r_outer^2 int xo2 = r2_outer - dy2; if (xo2 < 0) continue; - // Outer x extent (integer sqrt via isqrt approximation) - int xo = 0; - while ((xo + 1) * (xo + 1) <= xo2) xo++; + int xo = fast_isqrt(xo2); - // Inner x extent (hollow center) int xi = 0; int xi2 = r2_inner - dy2; if (xi2 > 0) { - while ((xi + 1) * (xi + 1) <= xi2) xi++; - xi++; // inner boundary is exclusive + xi = fast_isqrt(xi2) + 1; // inner boundary exclusive } uint8_t *row = &tft_input[py * TFT_WIDTH]; @@ -942,40 +1019,108 @@ static void draw_circle_ring(int cx, int cy, int r_inner, int r_outer, uint8_t c } } +// Draw a filled arc ring between r_inner..r_outer, from start_deg to end_deg +// Uses angle test per-pixel within bounding ring — clean filled arcs +static void draw_arc_ring(int cx, int cy, int r_inner, int r_outer, + int start_deg, int end_deg, uint8_t color) { + int r2_inner = r_inner * r_inner; + int r2_outer = r_outer * r_outer; + + // Precompute angle boundary vectors (Q15) for start/end + int si_start = angle_to_idx(start_deg); + int si_end = angle_to_idx(end_deg); + int16_t sx = lut_cos(si_start), sy = lut_sin(si_start); + int16_t ex = lut_cos(si_end), ey = lut_sin(si_end); + + for (int dy = -r_outer; dy <= r_outer; dy++) { + int py = cy + dy; + if (py < 0 || py >= TFT_HEIGHT) continue; + + int dy2 = dy * dy; + int xo2 = r2_outer - dy2; + if (xo2 < 0) continue; + + int xo = fast_isqrt(xo2); + int xi = 0; + int xi2 = r2_inner - dy2; + if (xi2 > 0) xi = fast_isqrt(xi2) + 1; + + uint8_t *row = &tft_input[py * TFT_WIDTH]; + + for (int dx = -xo; dx <= xo; dx++) { + // Skip inner hollow + if (dx > -xi && dx < xi) { dx = xi - 1; continue; } + + int px = cx + dx; + if (px < 0 || px >= TFT_WIDTH) continue; + + // Angle test: is (dx,dy) between start_deg and end_deg? + // Cross product sign test against boundary vectors + // cross(start_vec, point) >= 0 AND cross(point, end_vec) >= 0 + // For arcs < 180°, both must be true + // For arcs >= 180°, either can be true + int32_t cross_start = (int32_t)sx * dy - (int32_t)sy * dx; + int32_t cross_end = (int32_t)dx * ey - (int32_t)dy * ex; + + int span = end_deg - start_deg; + if (span < 0) span += 360; + bool inside; + if (span <= 180) { + inside = (cross_start >= 0) && (cross_end >= 0); + } else { + inside = (cross_start >= 0) || (cross_end >= 0); + } + + if (inside) { + row[px] = color; + } + } + } +} + static void draw_circular_gauge(int cx, int cy, int radius, float value, float max_value, const char* label, uint8_t color) { - // Draw gauge outline ring (3px thick) - draw_circle_ring(cx, cy, radius - 2, radius, COL_DARK); + // Draw gauge background ring (track) + draw_circle_ring(cx, cy, radius - 8, radius, COL_DARK); - // Draw value arc using LUT (6px thick band) + // Draw value arc (filled thick ring sector) float percentage = (value / max_value); if (percentage > 1.0f) percentage = 1.0f; - int end_angle = (int)(percentage * 270.0f) - 135; + if (percentage < 0.0f) percentage = 0.0f; - for (int angle = -135; angle < end_angle && angle < 135; angle += 2) { - int ai = angle_to_idx(angle); + if (percentage > 0.01f) { + int start_deg = -135 + 360; // Normalize to positive: 225° + int end_deg = start_deg + (int)(percentage * 270.0f); + draw_arc_ring(cx, cy, radius - 7, radius - 1, + start_deg, end_deg, color); + } + + // Tick marks at 0%, 50%, 100% positions on the outer edge + for (int i = 0; i <= 2; i++) { + int tick_deg = -135 + i * 135; + int ai = angle_to_idx(tick_deg); int16_t s = lut_sin(ai); int16_t c = lut_cos(ai); - // Draw just inner and outer edge pixels of the arc band - for (int r = radius - 8; r <= radius - 3; r++) { - int x = cx + (r * c + 16384) / 32768; - int y = cy + (r * s + 16384) / 32768; - if ((unsigned)x < (unsigned)TFT_WIDTH && (unsigned)y < (unsigned)TFT_HEIGHT) { - tft_input[y * TFT_WIDTH + x] = color; - } + for (int r = radius + 1; r <= radius + 3; r++) { + int tx = cx + (r * c + 16384) / 32768; + int ty = cy + (r * s + 16384) / 32768; + if ((unsigned)tx < (unsigned)TFT_WIDTH && (unsigned)ty < (unsigned)TFT_HEIGHT) + tft_input[ty * TFT_WIDTH + tx] = COL_GRAY; } } - // Draw value text in center + // Draw value text in center (larger visual emphasis) char value_str[16]; char *p = u32_to_str(value_str, (uint32_t)(value + 0.5f)); *p = '\0'; int text_len = (int)(p - value_str); - tft_draw_string(cx - (text_len * FONT_W) / 2, cy - FONT_H / 2, color, value_str); + tft_draw_string(cx - (text_len * FONT_W) / 2, cy - FONT_H / 2, COL_WHITE, value_str); - // Draw label below - int label_len = strlen(label); - tft_draw_string(cx - (label_len * FONT_W) / 2, cy + radius + 4, COL_GRAY, label); + // Draw label below center + int label_len = 0; + const char *lp = label; + while (*lp++) label_len++; + tft_draw_string(cx - (label_len * FONT_W) / 2, cy + FONT_H / 2 + 2, COL_GRAY, label); } static void draw_bar_gauge(int x, int y, int width, int height, float value, float max_value, @@ -1054,12 +1199,16 @@ static void draw_gauge_view(const tft_stats_t *stats) { uint8_t cdc_col = stats->cdc_connected ? COL_GREEN : COL_RED; uint8_t km_col = stats->kmbox_connected ? COL_GREEN : COL_RED; - - tft_draw_string(MARGIN, y, COL_GRAY, "CDC:"); - tft_draw_string(MARGIN + 40, y, cdc_col, stats->cdc_connected ? "CONN" : "DISC"); - - tft_draw_string(TFT_WIDTH / 2, y, COL_GRAY, "KM:"); - tft_draw_string(TFT_WIDTH / 2 + 32, y, km_col, stats->kmbox_connected ? "CONN" : "DISC"); + + box(MARGIN, y + 5, 5, 5, cdc_col); + tft_draw_string(MARGIN + 8, y, COL_GRAY, "CDC"); + tft_draw_string(MARGIN + 8 + 4 * FONT_W, y, cdc_col, + stats->cdc_connected ? "OK" : "--"); + + box(TFT_WIDTH / 2, y + 5, 5, 5, km_col); + tft_draw_string(TFT_WIDTH / 2 + 8, y, COL_GRAY, "KM"); + tft_draw_string(TFT_WIDTH / 2 + 8 + 3 * FONT_W, y, km_col, + stats->kmbox_connected ? "OK" : "--"); y += LINE_H; @@ -1367,7 +1516,12 @@ static bool tft_render_timer_callback(repeating_timer_t *rt) { if (!stats_pending) return true; tft_stats_t local_stats = shared_stats; // Snapshot stats_pending = false; - + + // Skip render if stats haven't meaningfully changed (saves CPU in steady state) + uint32_t h = stats_quick_hash(&local_stats); + if (h == last_stats_hash && menu_highlight_item < 0) return true; + last_stats_hash = h; + // Render into the back buffer tft_fill(COL_BG); format_stats(&local_stats); @@ -1446,18 +1600,37 @@ void tft_display_refresh(const tft_stats_t *stats) { void tft_display_splash(void) { if (!initialized) return; - + tft_fill(COL_BG); - - int y = TFT_HEIGHT / 2 - LINE_H * 2; - box(10, y - 4, TFT_WIDTH - 20, LINE_H * 3 + 8, COL_DARK); - - tft_draw_string_center(TFT_WIDTH / 2, y, COL_CYAN, "KMBox Bridge"); - y += LINE_H; - tft_draw_string_center(TFT_WIDTH / 2, y, COL_WHITE, "Autopilot"); - y += LINE_H * 2; - tft_draw_string_center(TFT_WIDTH / 2, y, COL_GREEN, "Starting..."); - + + int cx = TFT_WIDTH / 2; + int cy = TFT_HEIGHT / 2; + + // Outer frame with double border + int bx = 8, by = cy - LINE_H * 3; + int bw = TFT_WIDTH - 16, bh = LINE_H * 5 + 12; + box(bx, by, bw, 2, COL_CYAN); // top + box(bx, by + bh - 2, bw, 2, COL_CYAN); // bottom + box(bx, by, 2, bh, COL_CYAN); // left + box(bx + bw - 2, by, 2, bh, COL_CYAN); // right + // Inner fill + box(bx + 4, by + 4, bw - 8, bh - 8, COL_DARK); + + int y = by + 8; + tft_draw_string_center(cx, y, COL_CYAN, "KMBox Bridge"); + y += LINE_H + 2; + + // Thin accent line + int line_x = cx - 40; + int line_w = 80; + if (line_x < bx + 6) { line_x = bx + 6; line_w = bw - 12; } + box(line_x, y, line_w, 1, COL_CYAN); + y += 6; + + tft_draw_string_center(cx, y, COL_WHITE, "Autopilot"); + y += LINE_H + LINE_H; + tft_draw_string_center(cx, y, COL_GREEN, "Starting..."); + tft_swap_sync(); } From 3cf5ceec5df35d61c087a8910c9f4325aeb7fb89 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Thu, 26 Feb 2026 01:48:53 -0500 Subject: [PATCH 4/6] .. --- PIOKMbox.c | 3 + bridge/main.c | 26 +- bridge/tft_display.c | 274 ++++++++---- defines.h | 3 + humanization_fpu.h | 36 -- humanization_lut.h | 36 -- smooth_injection.c | 253 +++++++---- smooth_injection.h | 29 +- tusb_config.h | 6 +- usb_hid.c | 968 ++++++++++++++++++++++++++++++------------- 10 files changed, 1075 insertions(+), 559 deletions(-) diff --git a/PIOKMbox.c b/PIOKMbox.c index 8ace3d5..4b9e869 100644 --- a/PIOKMbox.c +++ b/PIOKMbox.c @@ -119,6 +119,9 @@ static void core1_task_loop(void) { tuh_task(); + // Drain SET_REPORT passthrough queue (device→host vendor reports) + hid_host_task(); + // Xbox host task: forward console commands to controller, keepalive if (g_xbox_mode) { xbox_host_task(); diff --git a/bridge/main.c b/bridge/main.c index addf040..5c21852 100644 --- a/bridge/main.c +++ b/bridge/main.c @@ -250,7 +250,7 @@ static uint32_t uart_rx_bytes_total = 0; static uint32_t uart_tx_bytes_total = 0; static uint32_t uart_rx_overflows = 0; -// KMBox temperature (from 0x0C info packet) +// KMBox temperature (from 0x0D info packet) static float kmbox_temperature_c = -999.0f; // Sync stats from hw_uart module (called periodically) @@ -496,8 +496,8 @@ static void uart_rx_task(void) { binary_idx = 0; } - // Check for start of binary response packet (0xFF, 0xFE, 0x0C, or 0x0E from KMBox) - if (!in_binary_packet && (c == 0xFF || c == 0xFE || c == 0x0C || c == 0x0E)) { + // Check for start of binary response packet (0xFF, 0xFE, 0x0D, or 0x0E from KMBox) + if (!in_binary_packet && (c == 0xFF || c == 0xFE || c == 0x0D || c == 0x0E)) { in_binary_packet = true; binary_idx = 0; binary_packet[binary_idx++] = c; @@ -530,8 +530,8 @@ static void uart_rx_task(void) { if (kmbox_state != KMBOX_CONNECTED) { kmbox_state = KMBOX_CONNECTED; } - } else if (binary_packet[0] == 0x0C) { - // Info response: [0x0C][hmode][imode][max_per_frame][queue_count][temp_lo][temp_hi][flags] + } else if (binary_packet[0] == 0x0D) { + // Info response: [0x0D][hmode][imode][max_per_frame][queue_count][temp_lo][temp_hi][flags] // flags: [0]=jitter_en [1]=vel_match [2:4]=queue_depth_3bit kmbox_humanization_mode = binary_packet[1]; kmbox_inject_mode = binary_packet[2]; @@ -555,7 +555,7 @@ static void uart_rx_task(void) { // Extended stats: [0x0E][queue_count][queue_cap][hmode][total_lo][total_hi][ovf_lo][ovf_hi] kmbox_queue_depth = binary_packet[1]; kmbox_queue_capacity = binary_packet[2]; - // binary_packet[3] = hmode (redundant, but useful if 0x0C wasn't received) + // binary_packet[3] = hmode (redundant, but useful if 0x0D wasn't received) if (!kmbox_humanization_valid) { kmbox_humanization_mode = binary_packet[3]; } @@ -672,7 +672,7 @@ static void kmbox_connection_task(void) { last_humanization_request_ms = now - HUMANIZATION_REQUEST_INTERVAL_MS + HUMANIZATION_INITIAL_DELAY_MS; // Send initial info request right away (binary) - uint8_t info_req[8] = {0x0C, 0, 0, 0, 0, 0, 0, 0}; + uint8_t info_req[8] = {0x0D, 0, 0, 0, 0, 0, 0, 0}; send_uart_packet(info_req, 8); } } @@ -696,11 +696,11 @@ static void kmbox_connection_task(void) { if (now - last_humanization_request_ms >= HUMANIZATION_REQUEST_INTERVAL_MS) { last_humanization_request_ms = now; - // Alternate between 0x0C (basic info + temp) and 0x0E (extended stats) + // Alternate between 0x0D (basic info + temp) and 0x0E (extended stats) static uint8_t info_cycle = 0; if (info_cycle % 2 == 0) { // Primary: humanization mode, inject mode, queue depth, temperature, flags - uint8_t info_req[8] = {0x0C, 0, 0, 0, 0, 0, 0, 0}; + uint8_t info_req[8] = {0x0D, 0, 0, 0, 0, 0, 0, 0}; send_uart_packet(info_req, 8); } else { // Extended: total injected, queue overflows, queue capacity @@ -736,16 +736,16 @@ static void kmbox_connection_task(void) { } } - // TEMP: Send 0x0C request even when disconnected to test UART RX + // Send 0x0D info request even when disconnected to test UART RX if (now - last_humanization_request_ms >= HUMANIZATION_REQUEST_INTERVAL_MS) { last_humanization_request_ms = now; - uint8_t info_req[8] = {0x0C, 0, 0, 0, 0, 0, 0, 0}; + uint8_t info_req[8] = {0x0D, 0, 0, 0, 0, 0, 0, 0}; bool sent = send_uart_packet(info_req, 8); - + // Debug: confirm we're sending the request static uint32_t last_info_debug_disc = 0; if (now - last_info_debug_disc > 5000) { - printf("[Bridge TX DISC] 0x0C: %02X %02X %02X %02X %02X %02X %02X %02X (sent=%d)\n", + printf("[Bridge TX DISC] 0x0D: %02X %02X %02X %02X %02X %02X %02X %02X (sent=%d)\n", info_req[0], info_req[1], info_req[2], info_req[3], info_req[4], info_req[5], info_req[6], info_req[7], sent); last_info_debug_disc = now; diff --git a/bridge/tft_display.c b/bridge/tft_display.c index 1867683..0705b65 100644 --- a/bridge/tft_display.c +++ b/bridge/tft_display.c @@ -252,6 +252,7 @@ static uint32_t stats_quick_hash(const tft_stats_t *s) { HASH_MIX(s->total_injected); HASH_MIX(s->uart_errors + s->frame_errors); HASH_MIX((uint32_t)(s->bridge_temperature_c * 10)); + HASH_MIX((uint32_t)(s->kmbox_temperature_c * 10)); HASH_MIX(s->console_mode); HASH_MIX(s->gamepad_buttons); #undef HASH_MIX @@ -937,16 +938,27 @@ static void draw_stats(const tft_stats_t *stats) { y += LINE_H; } - // Temperatures + // Temperatures — with mini visual bar if (fmt_br_temp.len || fmt_km_temp.len) { tft_draw_string(MARGIN, y, COL_GRAY, "Temp"); int tx = MARGIN + 5 * FONT_W; if (fmt_br_temp.len) { - tft_draw_string(tx, y, temp_color(stats->bridge_temperature_c), fmt_br_temp.str); + uint8_t tcol = temp_color(stats->bridge_temperature_c); + tft_draw_string(tx, y, tcol, fmt_br_temp.str); + // Mini bar: 2px tall under the text, width proportional to temp (0-100C) + int bar_w = (int)(stats->bridge_temperature_c * (fmt_br_temp.len * FONT_W) / 100.0f); + if (bar_w < 0) bar_w = 0; + if (bar_w > fmt_br_temp.len * FONT_W) bar_w = fmt_br_temp.len * FONT_W; + box(tx, y + FONT_H, bar_w, 2, tcol); tx += (fmt_br_temp.len + 1) * FONT_W; } if (fmt_km_temp.len) { - tft_draw_string(tx, y, temp_color(stats->kmbox_temperature_c), fmt_km_temp.str); + uint8_t tcol = temp_color(stats->kmbox_temperature_c); + tft_draw_string(tx, y, tcol, fmt_km_temp.str); + int bar_w = (int)(stats->kmbox_temperature_c * (fmt_km_temp.len * FONT_W) / 100.0f); + if (bar_w < 0) bar_w = 0; + if (bar_w > fmt_km_temp.len * FONT_W) bar_w = fmt_km_temp.len * FONT_W; + box(tx, y + FONT_H, bar_w, 2, tcol); } } } @@ -1084,7 +1096,7 @@ static void draw_circular_gauge(int cx, int cy, int radius, float value, float m draw_circle_ring(cx, cy, radius - 8, radius, COL_DARK); // Draw value arc (filled thick ring sector) - float percentage = (value / max_value); + float percentage = (max_value > 0.0f) ? (value / max_value) : 0.0f; if (percentage > 1.0f) percentage = 1.0f; if (percentage < 0.0f) percentage = 0.0f; @@ -1123,6 +1135,54 @@ static void draw_circular_gauge(int cx, int cy, int radius, float value, float m tft_draw_string(cx - (label_len * FONT_W) / 2, cy + FONT_H / 2 + 2, COL_GRAY, label); } +// Draw a circular gauge with a unit suffix next to the value (e.g. "42C") +static void draw_circular_gauge_unit(int cx, int cy, int radius, float value, float max_value, + const char* label, const char* unit, uint8_t color) { + // Draw gauge background ring (track) + draw_circle_ring(cx, cy, radius - 8, radius, COL_DARK); + + // Draw value arc + float percentage = (max_value > 0.0f) ? (value / max_value) : 0.0f; + if (percentage > 1.0f) percentage = 1.0f; + if (percentage < 0.0f) percentage = 0.0f; + + if (percentage > 0.01f) { + int start_deg = -135 + 360; + int end_deg = start_deg + (int)(percentage * 270.0f); + draw_arc_ring(cx, cy, radius - 7, radius - 1, + start_deg, end_deg, color); + } + + // Tick marks at 0%, 50%, 100% + for (int i = 0; i <= 2; i++) { + int tick_deg = -135 + i * 135; + int ai = angle_to_idx(tick_deg); + int16_t s = lut_sin(ai); + int16_t c = lut_cos(ai); + for (int r = radius + 1; r <= radius + 3; r++) { + int tx = cx + (r * c + 16384) / 32768; + int ty = cy + (r * s + 16384) / 32768; + if ((unsigned)tx < (unsigned)TFT_WIDTH && (unsigned)ty < (unsigned)TFT_HEIGHT) + tft_input[ty * TFT_WIDTH + tx] = COL_GRAY; + } + } + + // Value text with unit suffix: e.g. "42C" + char value_str[16]; + char *p = u32_to_str(value_str, (uint32_t)(value + 0.5f)); + const char *up = unit; + while (*up) *p++ = *up++; + *p = '\0'; + int text_len = (int)(p - value_str); + tft_draw_string(cx - (text_len * FONT_W) / 2, cy - FONT_H / 2, COL_WHITE, value_str); + + // Label below center + int label_len = 0; + const char *lp = label; + while (*lp++) label_len++; + tft_draw_string(cx - (label_len * FONT_W) / 2, cy + FONT_H / 2 + 2, COL_GRAY, label); +} + static void draw_bar_gauge(int x, int y, int width, int height, float value, float max_value, const char* label, uint8_t color) { // Draw border @@ -1153,107 +1213,137 @@ static void draw_bar_gauge(int x, int y, int width, int height, float value, flo static void draw_gauge_view(const tft_stats_t *stats) { int y = MARGIN; - - // === HEADER === - tft_draw_string_center(TFT_WIDTH / 2, y, COL_CYAN, "KMBox Gauges"); + + // === HEADER with connection status inline === + { + uint8_t cdc_col = stats->cdc_connected ? COL_GREEN : COL_RED; + uint8_t km_col = stats->kmbox_connected ? COL_GREEN : COL_RED; + + // Left: connection dots + box(MARGIN, y + 5, 5, 5, cdc_col); + box(MARGIN + 8, y + 5, 5, 5, km_col); + + // Center: title + tft_draw_string_center(TFT_WIDTH / 2, y, COL_CYAN, "KMBox Gauges"); + + // Right: uptime + int ux = TFT_WIDTH - MARGIN - fmt_uptime.len * FONT_W; + tft_draw_string(ux, y, COL_DARK, fmt_uptime.str); + } y += LINE_H + SEP_GAP; hline(y, COL_DARK); - y += SEP_GAP + 10; - + y += SEP_GAP + 4; + #if (TFT_RAW_WIDTH >= 240) - // Large display: circular gauges in a grid - int gauge_radius = 50; - int gauge_spacing_x = TFT_WIDTH / 2; - int gauge_spacing_y = 120; - - // Row 1: Latency and Command Rate - if (stats->latency_samples > 0) { - draw_circular_gauge(gauge_spacing_x / 2, y + gauge_radius, gauge_radius, - stats->latency_avg_us, 1000.0f, "Lat us", - stats->latency_avg_us < 200 ? COL_GREEN : - stats->latency_avg_us < 500 ? COL_YELLOW : COL_RED); + // ── Large display (ILI9341 240x320): 3 rows of 2 circular gauges ── + // Row 1: Latency + Cmd/s (performance) + // Row 2: Bridge Temp + KMBox Temp (thermal) + // Row 3: TX Rate + RX Rate (throughput) + int gauge_radius = 42; + int col_cx_l = TFT_WIDTH / 4; // Left column center X + int col_cx_r = TFT_WIDTH * 3 / 4; // Right column center X + int row_h = 100; // Vertical spacing per row + + // ── Row 1: Latency + Command Rate ── + { + int row_cy = y + gauge_radius; + + // Latency gauge — always shown, color by threshold + uint8_t lat_col = latency_color(stats->latency_avg_us); + draw_circular_gauge(col_cx_l, row_cy, gauge_radius, + (float)stats->latency_avg_us, 1000.0f, "Lat us", lat_col); + + // Command rate gauge — always shown + uint8_t cmd_col = (stats->commands_per_sec > 500) ? COL_YELLOW : + (stats->commands_per_sec > 0) ? COL_GREEN : COL_DARK; + draw_circular_gauge(col_cx_r, row_cy, gauge_radius, + (float)stats->commands_per_sec, 1000.0f, "Cmd/s", cmd_col); + + y += row_h; } - - if (stats->commands_per_sec > 0) { - draw_circular_gauge(gauge_spacing_x + gauge_spacing_x / 2, y + gauge_radius, gauge_radius, - stats->commands_per_sec, 1000.0f, "Cmd/s", COL_GREEN); + + // ── Row 2: Temperature Gauges ── + { + int row_cy = y + gauge_radius; + + // Bridge temperature gauge (0-100C range) + if (TEMP_VALID(stats->bridge_temperature_c)) { + draw_circular_gauge_unit(col_cx_l, row_cy, gauge_radius, + stats->bridge_temperature_c, 100.0f, + "Bridge", "C", + temp_color(stats->bridge_temperature_c)); + } else { + // Show empty gauge with "--" placeholder + draw_circle_ring(col_cx_l, row_cy, gauge_radius - 8, gauge_radius, COL_DARK); + tft_draw_string(col_cx_l - FONT_W, row_cy - FONT_H / 2, COL_DARK, "--"); + tft_draw_string(col_cx_l - 3 * FONT_W, row_cy + FONT_H / 2 + 2, COL_GRAY, "Bridge"); + } + + // KMBox temperature gauge + if (stats->kmbox_connected && TEMP_VALID(stats->kmbox_temperature_c)) { + draw_circular_gauge_unit(col_cx_r, row_cy, gauge_radius, + stats->kmbox_temperature_c, 100.0f, + "KMBox", "C", + temp_color(stats->kmbox_temperature_c)); + } else { + draw_circle_ring(col_cx_r, row_cy, gauge_radius - 8, gauge_radius, COL_DARK); + tft_draw_string(col_cx_r - FONT_W, row_cy - FONT_H / 2, COL_DARK, "--"); + tft_draw_string(col_cx_r - 3 * FONT_W / 2, row_cy + FONT_H / 2 + 2, COL_GRAY, "KMBox"); + } + + y += row_h; } - - y += gauge_spacing_y; - - // Row 2: TX and RX rates - float tx_mbps = stats->tx_rate_bps / 1000.0f; - float rx_mbps = stats->rx_rate_bps / 1000.0f; - - draw_circular_gauge(gauge_spacing_x / 2, y + gauge_radius, gauge_radius, - tx_mbps, 100.0f, "TX KB/s", COL_CYAN); - - draw_circular_gauge(gauge_spacing_x + gauge_spacing_x / 2, y + gauge_radius, gauge_radius, - rx_mbps, 100.0f, "RX KB/s", COL_CYAN); - - y += gauge_spacing_y + 10; - - // Connection status row - hline(y, COL_DARK); - y += 4; - - uint8_t cdc_col = stats->cdc_connected ? COL_GREEN : COL_RED; - uint8_t km_col = stats->kmbox_connected ? COL_GREEN : COL_RED; - - box(MARGIN, y + 5, 5, 5, cdc_col); - tft_draw_string(MARGIN + 8, y, COL_GRAY, "CDC"); - tft_draw_string(MARGIN + 8 + 4 * FONT_W, y, cdc_col, - stats->cdc_connected ? "OK" : "--"); - - box(TFT_WIDTH / 2, y + 5, 5, 5, km_col); - tft_draw_string(TFT_WIDTH / 2 + 8, y, COL_GRAY, "KM"); - tft_draw_string(TFT_WIDTH / 2 + 8 + 3 * FONT_W, y, km_col, - stats->kmbox_connected ? "OK" : "--"); - - y += LINE_H; - - // Temperature - if (TEMP_VALID(stats->bridge_temperature_c)) { - char temp_str[16]; - char *tp = u32_to_str(temp_str, (uint32_t)(stats->bridge_temperature_c + 0.5f)); - *tp++ = 'C'; *tp = '\0'; - tft_draw_string(MARGIN, y, temp_color(stats->bridge_temperature_c), temp_str); + + // ── Row 3: TX + RX throughput ── + { + int row_cy = y + gauge_radius; + float tx_kbps = stats->tx_rate_bps / 1000.0f; + float rx_kbps = stats->rx_rate_bps / 1000.0f; + + uint8_t tx_col = (tx_kbps > 50.0f) ? COL_YELLOW : + (tx_kbps > 0.1f) ? COL_CYAN : COL_DARK; + uint8_t rx_col = (rx_kbps > 50.0f) ? COL_YELLOW : + (rx_kbps > 0.1f) ? COL_CYAN : COL_DARK; + + draw_circular_gauge(col_cx_l, row_cy, gauge_radius, + tx_kbps, 100.0f, "TX KB/s", tx_col); + draw_circular_gauge(col_cx_r, row_cy, gauge_radius, + rx_kbps, 100.0f, "RX KB/s", rx_col); } - - // Uptime (reuse pre-formatted buffer) - int uptime_x = TFT_WIDTH - MARGIN - fmt_uptime.len * FONT_W; - tft_draw_string(uptime_x, y, COL_CYAN, fmt_uptime.str); + #else - // Small display: horizontal bar gauges - int bar_height = 24; + // ── Small display (ST7735 128x160): horizontal bar gauges ── + int bar_height = 20; int bar_width = TFT_WIDTH - MARGIN * 2; - - // Latency bar - if (stats->latency_samples > 0) { - draw_bar_gauge(MARGIN, y, bar_width, bar_height, stats->latency_avg_us, 1000.0f, - "Latency (us)", stats->latency_avg_us < 200 ? COL_GREEN : COL_YELLOW); - y += bar_height + LINE_H + 4; + + // Latency bar (always shown) + { + uint8_t lat_col = latency_color(stats->latency_avg_us); + draw_bar_gauge(MARGIN, y, bar_width, bar_height, + (float)stats->latency_avg_us, 1000.0f, "Latency us", lat_col); + y += bar_height + LINE_H + 2; } - - // Command rate bar - if (stats->commands_per_sec > 0) { - draw_bar_gauge(MARGIN, y, bar_width, bar_height, stats->commands_per_sec, 500.0f, - "Commands/sec", COL_GREEN); - y += bar_height + LINE_H + 4; + + // Command rate bar (always shown) + { + uint8_t cmd_col = (stats->commands_per_sec > 0) ? COL_GREEN : COL_DARK; + draw_bar_gauge(MARGIN, y, bar_width, bar_height, + (float)stats->commands_per_sec, 500.0f, "Cmd/s", cmd_col); + y += bar_height + LINE_H + 2; + } + + // Temperature bar (bridge) + if (TEMP_VALID(stats->bridge_temperature_c)) { + draw_bar_gauge(MARGIN, y, bar_width, bar_height, + stats->bridge_temperature_c, 100.0f, "Temp C", + temp_color(stats->bridge_temperature_c)); + } else { + draw_bar_gauge(MARGIN, y, bar_width, bar_height, 0.0f, 100.0f, "Temp C", COL_DARK); } - - // TX rate bar - draw_bar_gauge(MARGIN, y, bar_width, bar_height, stats->tx_rate_bps / 1000.0f, 50.0f, - "TX (KB/s)", COL_CYAN); - y += bar_height + LINE_H + 4; - - // RX rate bar - draw_bar_gauge(MARGIN, y, bar_width, bar_height, stats->rx_rate_bps / 1000.0f, 50.0f, - "RX (KB/s)", COL_CYAN); #endif #if TOUCH_ENABLED - // Touch zone hints (right edge, centered in each zone — no hlines) + // Touch zone hints (right edge, centered in each zone) { int zone_x = TFT_WIDTH - MARGIN - FONT_W; int z1_y = (TOUCH_ZONE_TOP_END) / 2 - FONT_H / 2; diff --git a/defines.h b/defines.h index 42d931a..f0bbbd8 100644 --- a/defines.h +++ b/defines.h @@ -317,6 +317,9 @@ typedef struct __attribute__((packed)) { #define USB_STACK_ERROR_THRESHOLD 50 // Number of consecutive errors before reset // USB descriptor configuration +#define MAX_DEVICE_HID_INTERFACES 4 // Max HID interfaces to mirror (matches CFG_TUD_HID) +#define MIRROR_ITF_DESC_MAX 512 // Max HID report descriptor per non-mouse interface +#define DESC_CONFIG_RUNTIME_MAX 256 // Max runtime config descriptor (9 + 4*32 = 137 typical) #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) #define EPNUM_HID HID_ENDPOINT_ADDRESS diff --git a/humanization_fpu.h b/humanization_fpu.h index 9dae8e4..99c7c06 100644 --- a/humanization_fpu.h +++ b/humanization_fpu.h @@ -55,26 +55,6 @@ static inline float humanization_jitter_scale(float magnitude) { return fmaxf(0.4f, 0.7f - (magnitude - 120.0f) * 0.0015f); } -/** - * Ease-in-out cubic (FPU direct computation) - */ -static inline float ease_in_out_cubic(float t) { - if (t < 0.5f) { - return 4.0f * t * t * t; - } else { - float x = -2.0f * t + 2.0f; - return 1.0f - x * x * x * 0.5f; - } -} - -/** - * Ease-out quadratic (FPU direct computation) - */ -static inline float ease_out_quad(float t) { - float x = 1.0f - t; - return 1.0f - x * x; -} - /** * Minimum-jerk velocity profile * Natural acceleration/deceleration curve @@ -84,20 +64,4 @@ static inline float min_jerk_velocity(float t) { return 30.0f * t * t * one_minus_t * one_minus_t; } -/** - * Convert progress (0-1) to eased progress based on mode - */ -static inline float apply_easing_fpu(float t, uint8_t mode) { - switch (mode) { - case 0: // EASING_LINEAR - return t; - case 1: // EASING_EASE_IN_OUT - return ease_in_out_cubic(t); - case 2: // EASING_EASE_OUT - return ease_out_quad(t); - default: - return t; - } -} - #endif // HUMANIZATION_FPU_H diff --git a/humanization_lut.h b/humanization_lut.h index d0c3361..554e726 100644 --- a/humanization_lut.h +++ b/humanization_lut.h @@ -174,42 +174,6 @@ extern const int32_t g_frame_spread_by_movement_lut[FRAME_SPREAD_LUT_SIZE]; // Fast Lookup Functions (inline for zero overhead) //--------------------------------------------------------------------+ -/** - * Fast easing lookup with interpolation - * @param t Progress in 16.16 fixed-point [0, SMOOTH_FP_ONE] - * @param mode Easing mode - * @return Eased progress in 16.16 fixed-point - */ -static inline int32_t lut_apply_easing(int32_t t, easing_mode_t mode) { - // Clamp t to valid range - if (t <= 0) return 0; - if (t >= SMOOTH_FP_ONE) return SMOOTH_FP_ONE; - - // Convert t to table index (0-255) - // t is 16.16, we want 8-bit index - uint32_t index = (uint32_t)t >> (SMOOTH_FP_SHIFT - EASING_LUT_SHIFT); - if (index >= EASING_LUT_SIZE - 1) index = EASING_LUT_SIZE - 2; - - // Select table based on mode - const int32_t *table; - switch (mode) { - case EASING_LINEAR: table = g_ease_linear_lut; break; - case EASING_EASE_IN_OUT: table = g_ease_in_out_cubic_lut; break; - case EASING_EASE_OUT: table = g_ease_out_quad_lut; break; - default: table = g_ease_linear_lut; break; // Fallback for safety - } - - // Linear interpolation between table entries for smoothness - int32_t v0 = table[index]; - int32_t v1 = table[index + 1]; - - // Fractional part for interpolation (lower 8 bits of shifted t) - uint32_t frac = ((uint32_t)t >> (SMOOTH_FP_SHIFT - EASING_LUT_SHIFT - 8)) & 0xFF; - - // Interpolate: v0 + (v1 - v0) * frac / 256 - return v0 + (((v1 - v0) * (int32_t)frac) >> 8); -} - /** * Fast progress lookup (no division needed) * @param total_frames Total frames for movement diff --git a/smooth_injection.c b/smooth_injection.c index eaa61e0..dd14158 100644 --- a/smooth_injection.c +++ b/smooth_injection.c @@ -132,18 +132,59 @@ static __force_inline int16_t fp_to_int(int32_t fp_val) { return (int16_t)((fp_val + SMOOTH_FP_HALF) >> SMOOTH_FP_SHIFT); } -static __force_inline int8_t clamp_i8(int32_t val) { - if (val > 127) return 127; - if (val < -128) return -128; - return (int8_t)val; +static __force_inline int16_t clamp_i16(int32_t val) { + if (val > 32767) return 32767; + if (val < -32768) return -32768; + return (int16_t)val; } //--------------------------------------------------------------------+ -// Easing Curves - FPU direct computation (no LUTs on M33) +// Velocity IIR Filter Helpers //--------------------------------------------------------------------+ -// Easing is now computed directly using apply_easing_fpu() inline -// M33 FPU makes direct computation faster than LUT lookups +// Fixed-point square root using hardware VSQRT.F32 (14 cycles on M33) +static inline int32_t __not_in_flash_func(fp_sqrt)(int32_t x) { + if (x <= 0) return 0; + float f = (float)x / (float)SMOOTH_FP_ONE; + float r = sqrtf(f); + return (int32_t)(r * SMOOTH_FP_ONE); +} + +// Soft saturation using Padé approximant: max * x * (27 + x²) / (27 + 9x²) +// Provides smooth clamping instead of hard clamp — no sharp discontinuity +static inline int32_t __not_in_flash_func(soft_saturate_fp)(int32_t input, int32_t max_fp) { + if (max_fp <= 0) return input; + int32_t abs_input = input >= 0 ? input : -input; + if (abs_input <= max_fp / 2) return input; // Linear region, no saturation needed + + // x = abs_input / max_fp (normalized) + int32_t x = fp_div(abs_input, max_fp); + // x² in fixed-point + int32_t x2 = fp_mul(x, x); + // numerator = x * (27 + x²) + int32_t twenty_seven = 27 * SMOOTH_FP_ONE; + int32_t num = fp_mul(x, twenty_seven + x2); + // denominator = 27 + 9*x² + int32_t den = twenty_seven + 9 * x2; + if (den == 0) return input; + // result = max * num / den + int32_t result = fp_mul(max_fp, fp_div(num, den)); + return input >= 0 ? result : -result; +} + +// Adaptive alpha: high accel → alpha near alpha_max (responsive); +// low accel → alpha near alpha_min (smooth) +// Formula: alpha_min + range - range / (1 + accel_mag * sensitivity) +static inline int32_t __not_in_flash_func(compute_adaptive_alpha)( + int32_t accel_mag, int32_t alpha_min, int32_t alpha_max, int32_t sensitivity) { + int32_t range = alpha_max - alpha_min; + if (range <= 0) return alpha_max; + int32_t product = fp_mul(accel_mag, sensitivity); + int32_t denom = SMOOTH_FP_ONE + product; + if (denom <= 0) denom = 1; + int32_t decay = fp_div(range, denom); + return alpha_min + range - decay; +} //--------------------------------------------------------------------+ // Velocity Tracking @@ -374,23 +415,6 @@ static bool queue_single_substep(int32_t x_fp, int32_t y_fp, inject_mode_t mode, g_smooth.humanization.onset_jitter_max); } - // Fix #4: Fix easing curve selection bias - single RNG draw, correct distribution - // Old code: two draws created biased distribution (33% ease-out, then 25% of remaining) - // New code: one draw with explicit probability buckets - easing_mode_t easing = EASING_LINEAR; - if (max_component > int_to_fp(10)) { - // Larger sub-steps: smooth easing - easing = EASING_EASE_IN_OUT; - } else { - uint32_t r = rng_next() % 12; - if (r < 4) { - easing = EASING_EASE_OUT; // 33% chance - } else if (r < 7) { - easing = EASING_EASE_IN_OUT; // 25% chance - } - // else: 42% linear (default) - } - // For velocity-matched mode, adjust based on current velocity if (mode == INJECT_MODE_VELOCITY_MATCHED && g_smooth.velocity_matching_enabled) { int32_t vel_mag = g_smooth.velocity.avg_velocity_x_fp; @@ -423,7 +447,6 @@ static bool queue_single_substep(int32_t x_fp, int32_t y_fp, inject_mode_t mode, entry->frames_left = frames; entry->total_frames = frames; entry->mode = mode; - entry->easing = easing; entry->active = true; entry->onset_delay = onset_delay; // Fix #1: onset jitter entry->will_overshoot = false; @@ -454,8 +477,8 @@ static bool queue_single_substep(int32_t x_fp, int32_t y_fp, inject_mode_t mode, #define SUBSTEP_MIN_MOVEMENT_PX 3 // Number of sub-steps to split into (base value, randomized) -#define SUBSTEP_COUNT_BASE 4 -#define SUBSTEP_COUNT_EXTRA_MAX 4 // Up to +4 extra = 4-8 total +#define SUBSTEP_COUNT_BASE 2 +#define SUBSTEP_COUNT_EXTRA_MAX 2 // Up to +2 extra = 2-4 total // Frame delay between consecutive sub-steps (randomized) #define SUBSTEP_DELAY_MIN 1 @@ -540,7 +563,7 @@ bool smooth_inject_movement_fp(int32_t x_fp, int32_t y_fp, inject_mode_t mode) { //--------------------------------------------------------------------+ // Determine number of sub-steps (only reached in FULL mode) - uint8_t num_substeps = SUBSTEP_COUNT_BASE + (uint8_t)rng_range(0, 3); // 4-7 sub-steps + uint8_t num_substeps = SUBSTEP_COUNT_BASE + (uint8_t)rng_range(0, 2); // 2-4 sub-steps // Clamp to available queue space uint8_t available = SMOOTH_QUEUE_SIZE - g_smooth.queue_count; @@ -670,11 +693,13 @@ void smooth_record_physical_movement(int16_t x, int16_t y) { velocity_update(x, y); } -void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { - // Super-fast path for empty queue with no accumulator - if (g_smooth.queue_count == 0 && - g_smooth.x_accumulator_fp == 0 && - g_smooth.y_accumulator_fp == 0) { +void __not_in_flash_func(smooth_process_frame)(int16_t *out_x, int16_t *out_y) { + // Super-fast path for empty queue with no accumulator and no filter debt + if (g_smooth.queue_count == 0 && + g_smooth.x_accumulator_fp == 0 && + g_smooth.y_accumulator_fp == 0 && + g_smooth.filtered_vx_fp == 0 && + g_smooth.filtered_vy_fp == 0) { *out_x = 0; *out_y = 0; g_smooth.frames_processed++; @@ -692,17 +717,17 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { int32_t frame_x_fp = 0; int32_t frame_y_fp = 0; - // Early exit if no active entries + // Early exit if no active entries (still run IIR filter to release debt) if (g_smooth.queue_count == 0) { - goto apply_accumulator; + goto apply_vel_filter; } - + // Safety: if queue_count > 0 but linked list is empty, reset to prevent hang if (g_active_head == NULL) { g_smooth.queue_count = 0; g_free_bitmap = 0xFFFFFFFFFFFFFFFFULL; g_active_node_bitmap = 0ULL; - goto apply_accumulator; + goto apply_vel_filter; } // Process active entries using linked list (O(n) where n = active count, not queue size) @@ -722,22 +747,13 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { } if (entry->frames_left > 0) { - // Calculate progress using FPU (faster than LUT on M33) - // Offset by +1 so the first frame (frames_elapsed=0) produces - // non-zero output. Without this, progress_delta is always 0 on - // the first frame, which means no HID report is sent when the - // physical mouse is idle — the cursor won't move until frame 2. - uint8_t frames_elapsed = entry->total_frames - entry->frames_left; - float progress_flt = (float)(frames_elapsed + 1) / (float)(entry->total_frames + 1); - float prev_progress = (float)frames_elapsed / (float)(entry->total_frames + 1); - - // Apply easing curve using FPU (3 cycles for multiply on M33) - float eased_progress = apply_easing_fpu(progress_flt, entry->easing); - float prev_eased = apply_easing_fpu(prev_progress, entry->easing); - float progress_delta_flt = eased_progress - prev_eased; - - // Convert to fixed-point for accumulator - int32_t progress_delta = (int32_t)(progress_delta_flt * SMOOTH_FP_ONE); + // Linear progress: equal fraction per frame (IIR filter handles smoothing) + int32_t progress_delta; + if (entry->total_frames <= 1) { + progress_delta = SMOOTH_FP_ONE; + } else { + progress_delta = SMOOTH_FP_ONE / entry->total_frames; + } // === MOVEMENT DELTA (tracked - affects remaining) === int32_t movement_dx_fp = fp_mul(entry->x_fp, progress_delta); @@ -776,7 +792,6 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { correction->frames_left = (uint8_t)rng_range(2, 4); // Correct over 2-4 frames correction->total_frames = correction->frames_left; correction->mode = INJECT_MODE_SMOOTH; - correction->easing = EASING_EASE_OUT; // Quick correction correction->active = true; correction->will_overshoot = false; corrections_this_frame++; @@ -809,26 +824,73 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { frame_y_fp = 0; } -apply_accumulator: +apply_vel_filter: + // Velocity IIR filter: debt-based smoothing across command boundaries. + // Raw queue output accumulates as "debt"; each frame releases a fraction (alpha). + // Total movement is conserved — no tracking loss. + if (g_smooth.humanization.mode == HUMANIZATION_FULL) { + int32_t raw_vx = frame_x_fp, raw_vy = frame_y_fp; + + // Acceleration magnitude (for adaptive alpha) + int32_t ax = raw_vx - g_smooth.prev_raw_vx_fp; + int32_t ay = raw_vy - g_smooth.prev_raw_vy_fp; + int32_t accel_mag = fp_sqrt(fp_mul(ax, ax) + fp_mul(ay, ay)); + g_smooth.prev_raw_vx_fp = raw_vx; + g_smooth.prev_raw_vy_fp = raw_vy; + + // Adaptive alpha: high accel → fast release, low accel → slow release + int32_t alpha = compute_adaptive_alpha( + accel_mag, + g_smooth.humanization.vel_filter_alpha_min_fp, + g_smooth.humanization.vel_filter_alpha_max_fp, + g_smooth.humanization.vel_filter_accel_sens_fp); + + // Accumulate raw queue output into velocity debt + g_smooth.filtered_vx_fp += raw_vx; + g_smooth.filtered_vy_fp += raw_vy; + + // Release portion of debt (alpha controls release rate) + int32_t release_x = fp_mul(alpha, g_smooth.filtered_vx_fp); + int32_t release_y = fp_mul(alpha, g_smooth.filtered_vy_fp); + g_smooth.filtered_vx_fp -= release_x; + g_smooth.filtered_vy_fp -= release_y; + + frame_x_fp = release_x; + frame_y_fp = release_y; + + // Soft saturation (replaces hard clamp for FULL mode) + int32_t max_fp = int_to_fp(g_smooth.max_per_frame); + int32_t sat_x = soft_saturate_fp(frame_x_fp, max_fp); + int32_t sat_y = soft_saturate_fp(frame_y_fp, max_fp); + // Return any capped excess back to debt (conserves movement) + g_smooth.filtered_vx_fp += frame_x_fp - sat_x; + g_smooth.filtered_vy_fp += frame_y_fp - sat_y; + frame_x_fp = sat_x; + frame_y_fp = sat_y; + } + // Add sub-pixel accumulator frame_x_fp += g_smooth.x_accumulator_fp; frame_y_fp += g_smooth.y_accumulator_fp; - + // Convert to integer with sub-pixel tracking int16_t out_x_int = fp_to_int(frame_x_fp); int16_t out_y_int = fp_to_int(frame_y_fp); - - // Apply per-frame rate limiting - if (out_x_int > g_smooth.max_per_frame) { - out_x_int = g_smooth.max_per_frame; - } else if (out_x_int < -g_smooth.max_per_frame) { - out_x_int = -g_smooth.max_per_frame; - } - - if (out_y_int > g_smooth.max_per_frame) { - out_y_int = g_smooth.max_per_frame; - } else if (out_y_int < -g_smooth.max_per_frame) { - out_y_int = -g_smooth.max_per_frame; + + // Apply per-frame rate limiting (hard clamp for non-FULL modes; + // FULL mode uses soft saturation from IIR filter above) + if (g_smooth.humanization.mode != HUMANIZATION_FULL) { + if (out_x_int > g_smooth.max_per_frame) { + out_x_int = g_smooth.max_per_frame; + } else if (out_x_int < -g_smooth.max_per_frame) { + out_x_int = -g_smooth.max_per_frame; + } + + if (out_y_int > g_smooth.max_per_frame) { + out_y_int = g_smooth.max_per_frame; + } else if (out_y_int < -g_smooth.max_per_frame) { + out_y_int = -g_smooth.max_per_frame; + } } // Update sub-pixel accumulator with remainder @@ -850,8 +912,8 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { } // Output - *out_x = clamp_i8(out_x_int); - *out_y = clamp_i8(out_y_int); + *out_x = clamp_i16(out_x_int); + *out_y = clamp_i16(out_y_int); // Fix: When queue is fully drained and output rounds to zero, flush // sub-pixel accumulator residuals. Without this, tiny residuals (<1px) @@ -860,6 +922,20 @@ void __not_in_flash_func(smooth_process_frame)(int8_t *out_x, int8_t *out_y) { if (g_smooth.queue_count == 0 && out_x_int == 0 && out_y_int == 0) { g_smooth.x_accumulator_fp = 0; g_smooth.y_accumulator_fp = 0; + + // Flush velocity filter state to prevent residual drift + if (g_smooth.humanization.mode == HUMANIZATION_FULL) { + // Only flush if filtered velocity is sub-quarter-pixel + int32_t quarter_px = SMOOTH_FP_ONE / 4; + int32_t abs_fvx = g_smooth.filtered_vx_fp >= 0 ? g_smooth.filtered_vx_fp : -g_smooth.filtered_vx_fp; + int32_t abs_fvy = g_smooth.filtered_vy_fp >= 0 ? g_smooth.filtered_vy_fp : -g_smooth.filtered_vy_fp; + if (abs_fvx < quarter_px && abs_fvy < quarter_px) { + g_smooth.filtered_vx_fp = 0; + g_smooth.filtered_vy_fp = 0; + g_smooth.prev_raw_vx_fp = 0; + g_smooth.prev_raw_vy_fp = 0; + } + } } g_smooth.frames_processed++; @@ -892,6 +968,11 @@ void smooth_clear_queue(void) { // Also reset velocity tracking accumulators for consistency g_velocity_sum_x_fp = 0; g_velocity_sum_y_fp = 0; + // Reset velocity IIR filter state + g_smooth.filtered_vx_fp = 0; + g_smooth.filtered_vy_fp = 0; + g_smooth.prev_raw_vx_fp = 0; + g_smooth.prev_raw_vy_fp = 0; } void smooth_get_stats(uint32_t *total_injected, uint32_t *frames_processed, @@ -906,6 +987,9 @@ bool smooth_has_pending(void) { if (g_smooth.queue_count > 0) return true; if (g_smooth.x_accumulator_fp != 0) return true; if (g_smooth.y_accumulator_fp != 0) return true; + // Velocity filter debt: movement received but not yet output + if (g_smooth.filtered_vx_fp != 0) return true; + if (g_smooth.filtered_vy_fp != 0) return true; return false; } @@ -921,7 +1005,7 @@ static void smooth_set_humanization_mode_internal(humanization_mode_t mode, bool switch (mode) { case HUMANIZATION_OFF: // Disable all humanization - pure digital pass-through - g_smooth.max_per_frame = 16; // Fix #2: fixed + g_smooth.max_per_frame = 32767; // No artificial limit g_smooth.velocity_matching_enabled = true; g_smooth.humanization.jitter_enabled = false; g_smooth.humanization.jitter_amount_fp = 0; @@ -933,12 +1017,15 @@ static void smooth_set_humanization_mode_internal(humanization_mode_t mode, bool g_smooth.humanization.accum_clamp_fp = 0; // Fix #13: no clamp (unlimited) g_smooth.humanization.onset_jitter_min = 0; // Fix #1: no onset delay g_smooth.humanization.onset_jitter_max = 0; + g_smooth.humanization.vel_filter_alpha_min_fp = SMOOTH_FP_ONE; // Passthrough + g_smooth.humanization.vel_filter_alpha_max_fp = SMOOTH_FP_ONE; + g_smooth.humanization.vel_filter_accel_sens_fp = 0; break; - + case HUMANIZATION_MICRO: // Micro-noise only — for pre-humanized input // Only adds sub-pixel tremor + sensor noise below the PC's correction threshold - g_smooth.max_per_frame = 16; // Fixed — don't alter delivery rate + g_smooth.max_per_frame = 32767; // No artificial limit g_smooth.velocity_matching_enabled = false; // Input already has natural velocity g_smooth.humanization.jitter_enabled = true; g_smooth.humanization.jitter_amount_fp = int_to_fp(1) / 2; // 0.5px base tremor @@ -947,17 +1034,20 @@ static void smooth_set_humanization_mode_internal(humanization_mode_t mode, bool g_smooth.humanization.vel_slow_threshold_fp = int_to_fp(2); g_smooth.humanization.vel_fast_threshold_fp = int_to_fp(10); g_smooth.humanization.delivery_error_fp = SMOOTH_FP_ONE / 100; // ±1% sensor noise - g_smooth.humanization.accum_clamp_fp = int_to_fp(8); // ±8px — generous clamp prevents + g_smooth.humanization.accum_clamp_fp = int_to_fp(1000); // Generous clamp prevents // unbounded drift from delivery error // residuals while trusting input g_smooth.humanization.onset_jitter_min = 0; // No onset delay g_smooth.humanization.onset_jitter_max = 0; + g_smooth.humanization.vel_filter_alpha_min_fp = SMOOTH_FP_ONE; // Passthrough + g_smooth.humanization.vel_filter_alpha_max_fp = SMOOTH_FP_ONE; + g_smooth.humanization.vel_filter_accel_sens_fp = 0; break; case HUMANIZATION_FULL: // Full humanization — for raw/robotic input - // Subdivision, easing, onset delay, overshoot — the works - g_smooth.max_per_frame = (int16_t)rng_range(12, 20); // Per-session variation + // Subdivision, IIR velocity filter, onset delay, overshoot — the works + g_smooth.max_per_frame = (int16_t)rng_range(10000, 12000); // High-DPI: ~650 IPS at 26000 DPI g_smooth.velocity_matching_enabled = true; g_smooth.humanization.jitter_enabled = true; g_smooth.humanization.jitter_amount_fp = int_to_fp(1); // 1.0px base tremor @@ -966,11 +1056,14 @@ static void smooth_set_humanization_mode_internal(humanization_mode_t mode, bool g_smooth.humanization.vel_slow_threshold_fp = int_to_fp(rng_range(1, 4)); g_smooth.humanization.vel_fast_threshold_fp = int_to_fp(rng_range(8, 14)); g_smooth.humanization.delivery_error_fp = SMOOTH_FP_ONE / 50; // ±2% - g_smooth.humanization.accum_clamp_fp = int_to_fp(16); // ±16px — matches max_per_frame, + g_smooth.humanization.accum_clamp_fp = int_to_fp(12000); // Matches max_per_frame, // lets accumulator drain naturally // when queue overflow dumps to accum g_smooth.humanization.onset_jitter_min = 1; // 1-4 frames g_smooth.humanization.onset_jitter_max = 4; + g_smooth.humanization.vel_filter_alpha_min_fp = SMOOTH_FP_ONE * 50 / 100; // 0.50 — near-passthrough + g_smooth.humanization.vel_filter_alpha_max_fp = SMOOTH_FP_ONE * 90 / 100; // 0.90 — near-instant + g_smooth.humanization.vel_filter_accel_sens_fp = SMOOTH_FP_ONE * 25 / 100; // 0.25 break; default: @@ -978,6 +1071,12 @@ static void smooth_set_humanization_mode_internal(humanization_mode_t mode, bool return; } + // Reset velocity IIR filter state on mode change + g_smooth.filtered_vx_fp = 0; + g_smooth.filtered_vy_fp = 0; + g_smooth.prev_raw_vx_fp = 0; + g_smooth.prev_raw_vy_fp = 0; + // NOTE: Runtime flash saves are DISABLED to prevent device hangs. // flash_safe_execute() pauses Core1 (USB host) via multicore_lockout // during the ~100ms flash erase/program, which causes the USB host diff --git a/smooth_injection.h b/smooth_injection.h index 307ae1c..62fc29c 100644 --- a/smooth_injection.h +++ b/smooth_injection.h @@ -24,8 +24,8 @@ //--------------------------------------------------------------------+ // Maximum movement per HID frame (prevents jarring jumps) -// At 125Hz (8ms), 16 pixels/frame = ~2000 pixels/sec max smooth speed -#define SMOOTH_MAX_PER_FRAME 16 +// Mode configs override this default. Sized for high-DPI mice (26000+ DPI). +#define SMOOTH_MAX_PER_FRAME 127 // Movement queue size (number of pending inject operations) #define SMOOTH_QUEUE_SIZE 64 @@ -56,16 +56,6 @@ typedef enum { INJECT_MODE_MICRO, } inject_mode_t; -//--------------------------------------------------------------------+ -// Easing Modes for Natural Movement -//--------------------------------------------------------------------+ - -typedef enum { - EASING_LINEAR = 0, // No easing (constant velocity) - EASING_EASE_IN_OUT, // Slow start, fast middle, slow end (natural) - EASING_EASE_OUT, // Quick start, slow end (corrections) -} easing_mode_t; - //--------------------------------------------------------------------+ // Humanization Modes //--------------------------------------------------------------------+ @@ -87,9 +77,8 @@ typedef struct { int32_t x_remaining_fp; // Remaining X to inject int32_t y_remaining_fp; // Remaining Y to inject uint8_t frames_left; // Frames remaining for this movement - uint8_t total_frames; // Total frames for this movement (for easing calc) + uint8_t total_frames; // Total frames for this movement (for linear progress calc) inject_mode_t mode; // Injection mode - easing_mode_t easing; // Easing curve to apply bool active; // Is this entry in use? uint8_t onset_delay; // Frames to wait before starting delivery (onset jitter) @@ -143,6 +132,12 @@ typedef struct { // Configuration int16_t max_per_frame; bool velocity_matching_enabled; + + // Velocity IIR filter state (output-stage smoothing) + int32_t filtered_vx_fp; // Current filtered velocity X (16.16) + int32_t filtered_vy_fp; // Current filtered velocity Y (16.16) + int32_t prev_raw_vx_fp; // Previous raw velocity X for acceleration calc + int32_t prev_raw_vy_fp; // Previous raw velocity Y for acceleration calc // Humanization settings struct { @@ -157,6 +152,10 @@ typedef struct { int32_t accum_clamp_fp; // Max accumulator magnitude (mode-dependent) uint8_t onset_jitter_min; // Min onset delay frames uint8_t onset_jitter_max; // Max onset delay frames + // Velocity IIR filter parameters + int32_t vel_filter_alpha_min_fp; // Min alpha (responsive, during high accel) ~0.3 + int32_t vel_filter_alpha_max_fp; // Max alpha (smooth, steady state) ~0.85 + int32_t vel_filter_accel_sens_fp; // Acceleration sensitivity ~0.15 } humanization; } smooth_injection_state_t; @@ -205,7 +204,7 @@ void smooth_record_physical_movement(int16_t x, int16_t y); * @param out_x Output X movement for this frame * @param out_y Output Y movement for this frame */ -void smooth_process_frame(int8_t *out_x, int8_t *out_y); +void smooth_process_frame(int16_t *out_x, int16_t *out_y); /** * Get current average velocity (for velocity-matched injection) diff --git a/tusb_config.h b/tusb_config.h index a34767d..32a6c69 100644 --- a/tusb_config.h +++ b/tusb_config.h @@ -81,7 +81,11 @@ #endif //------------- CLASS -------------// -#define CFG_TUD_HID 1 +// Support up to 4 HID interfaces for faithful multi-interface device mirroring. +// Gaming mice expose 2-4 HID interfaces (mouse, keyboard-macros, vendor). +// We mirror all of them on the device side so the downstream PC sees an +// identical interface layout. +#define CFG_TUD_HID 4 // HID buffer size - should be sufficient to hold ID (if any) + Data #define CFG_TUD_HID_EP_BUFSIZE 64 diff --git a/usb_hid.c b/usb_hid.c index 4bb9454..5e5bda4 100644 --- a/usb_hid.c +++ b/usb_hid.c @@ -165,10 +165,117 @@ static struct { bool valid; } host_config_info = { .bmAttributes = TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, .bMaxPower = USB_CONFIG_POWER_MA / 2, .bInterfaceProtocol = HID_ITF_PROTOCOL_NONE, .bInterfaceSubClass = 0, .wMaxPacketSize = CFG_TUD_HID_EP_BUFSIZE, .bInterval = HID_POLLING_INTERVAL_MS, .valid = false }; -// Runtime configuration descriptor buffer (mutable so we can patch it) -static uint8_t desc_configuration_runtime[TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN]; +// Runtime configuration descriptor buffer (large enough for multi-interface) +static uint8_t desc_configuration_runtime[DESC_CONFIG_RUNTIME_MAX]; static bool desc_config_runtime_valid = false; +//--------------------------------------------------------------------+ +// Multi-interface mirroring infrastructure +//--------------------------------------------------------------------+ + +// Per-interface state captured from host device for faithful mirroring. +// Gaming mice expose 2-4 HID interfaces (mouse, keyboard-macros, vendor). +// We capture all of them and present matching interfaces on the device side. +typedef struct { + // Interface properties (from host config descriptor) + uint8_t itf_subclass; + uint8_t itf_protocol; + uint16_t ep_in_max_packet; + uint8_t ep_in_interval; + bool has_ep_out; + uint16_t ep_out_max_packet; + uint8_t ep_out_interval; + + // Runtime state (populated during tuh_hid_mount_cb) + uint8_t host_dev_addr; + uint8_t host_instance; + bool is_mouse; // Interface we inject mouse/keyboard/consumer into + + // HID report descriptor (non-mouse only; mouse uses desc_hid_report_runtime) + uint8_t report_desc[MIRROR_ITF_DESC_MAX]; + uint16_t report_desc_len; + + bool active; +} mirrored_interface_t; + +static mirrored_interface_t mirrored_itfs[MAX_DEVICE_HID_INTERFACES]; +static uint8_t mirrored_itf_count = 0; // Active mirrored interfaces +static uint8_t expected_hid_itf_count = 0; // Expected from config descriptor +static uint8_t mounted_hid_itf_count = 0; // Mounted so far + +// Which device-side HID instance carries the composite descriptor (keyboard + +// mouse + consumer). All mouse/keyboard/consumer reports must be sent on this +// instance. Defaults to 0 for single-interface mode. +static uint8_t mouse_device_instance = 0; + +// Vendor report passthrough queue (Core1 producer → Core0 consumer). +// When the host mouse sends vendor reports (e.g. Logitech HID++, Razer), +// Core1 queues them here and Core0 drains them via tud_hid_report(). +#define VENDOR_QUEUE_SIZE 8 +#define VENDOR_REPORT_MAX_LEN 64 + +typedef struct { + uint8_t device_instance; // Which device-side HID instance to send on + uint8_t report_id; + uint8_t data[VENDOR_REPORT_MAX_LEN]; + uint8_t len; +} vendor_report_entry_t; + +static struct { + vendor_report_entry_t entries[VENDOR_QUEUE_SIZE]; + volatile uint8_t head; // Written by Core1 (producer) + volatile uint8_t tail; // Read by Core0 (consumer) +} vendor_fwd_queue; + +// SET_REPORT passthrough queue (Core0 producer → Core1 consumer). +// When the downstream PC sends SET_REPORT to vendor interfaces, Core0 +// queues them here and Core1 forwards to the real mouse. +typedef struct { + uint8_t host_dev_addr; + uint8_t host_instance; + uint8_t report_id; + uint8_t report_type; + uint8_t data[VENDOR_REPORT_MAX_LEN]; + uint8_t len; +} set_report_entry_t; + +static struct { + set_report_entry_t entries[VENDOR_QUEUE_SIZE]; + volatile uint8_t head; // Written by Core0 (producer) + volatile uint8_t tail; // Read by Core1 (consumer) +} set_report_queue; + +// Extended string descriptor cache. +// Gaming mice may use string indices beyond the standard 1-3 (manufacturer, +// product, serial). Interface strings, HID class strings, etc. +#define MAX_CACHED_STRINGS 8 +#define CACHED_STRING_MAX_LEN 64 + +typedef struct { + uint8_t index; + char str[CACHED_STRING_MAX_LEN]; + bool valid; +} cached_string_t; + +static cached_string_t extra_strings[MAX_CACHED_STRINGS]; +static uint8_t extra_string_count = 0; +static uint8_t max_string_index_seen = 3; // Track highest string index from host + +// GET_REPORT cache: stores the last received report per (instance, report_id) +// so that tud_hid_get_report_cb can respond to the host (macOS IOKit sends +// GET_REPORT during device open to verify responsiveness). +// Written by Core1 (in queue_vendor_report), read by Core0 (in get_report_cb). +#define REPORT_CACHE_SLOTS_PER_ITF 8 + +typedef struct { + uint8_t report_id; + uint8_t data[VENDOR_REPORT_MAX_LEN]; + uint8_t len; + bool valid; +} cached_report_t; + +static cached_report_t report_cache[MAX_DEVICE_HID_INTERFACES][REPORT_CACHE_SLOTS_PER_ITF]; + // Function to fetch string descriptors from attached device static void fetch_device_string_descriptors(uint8_t dev_addr) { // Reset string descriptors @@ -516,7 +623,7 @@ static void reset_device_string_descriptors(void) { memset(attached_serial, 0, sizeof(attached_serial)); string_descriptors_fetched = false; attached_has_serial = false; - + // Reset cloned descriptor state host_device_info.valid = false; host_config_info.valid = false; @@ -525,11 +632,28 @@ static void reset_device_string_descriptors(void) { host_mouse_has_report_id = false; host_mouse_report_id = 0; cloned_dev_addr = 0; - + + // Reset multi-interface mirroring state + mirrored_itf_count = 0; + expected_hid_itf_count = 0; + mounted_hid_itf_count = 0; + mouse_device_instance = 0; + memset(mirrored_itfs, 0, sizeof(mirrored_itfs)); + + // Reset vendor report queues and GET_REPORT cache + vendor_fwd_queue.head = vendor_fwd_queue.tail = 0; + set_report_queue.head = set_report_queue.tail = 0; + memset(report_cache, 0, sizeof(report_cache)); + + // Reset extra string descriptor cache + extra_string_count = 0; + max_string_index_seen = 3; + memset(extra_strings, 0, sizeof(extra_strings)); + // Reset runtime report IDs to defaults runtime_kbd_report_id = REPORT_ID_KEYBOARD; runtime_consumer_report_id = REPORT_ID_CONSUMER_CONTROL; - + // Rebuild config descriptor with defaults build_runtime_hid_report_with_mouse(NULL, 0); rebuild_configuration_descriptor(); @@ -1248,6 +1372,17 @@ bool usb_hid_init(void) // Initialize per-instance HID tracking memset(hid_instances, 0, sizeof(hid_instances)); + // Initialize multi-interface mirroring state + memset(mirrored_itfs, 0, sizeof(mirrored_itfs)); + mirrored_itf_count = 0; + expected_hid_itf_count = 0; + mounted_hid_itf_count = 0; + mouse_device_instance = 0; + vendor_fwd_queue.head = vendor_fwd_queue.tail = 0; + set_report_queue.head = set_report_queue.tail = 0; + memset(report_cache, 0, sizeof(report_cache)); + extra_string_count = 0; + // Seed output-stage PRNG from hardware TRNG hid_rng_seed(get_rand_32()); @@ -1364,13 +1499,13 @@ static bool __not_in_flash_func(process_keyboard_report_internal)(const hid_keyb // CRITICAL FIX: Check readiness before attempting to send // If endpoint is busy, return true anyway to avoid blocking the HID report pipeline // The keyboard state will be sent with the next available opportunity - if (!tud_hid_ready()) + if (!tud_hid_n_ready(mouse_device_instance)) { return true; // Endpoint busy, continue processing without blocking } // Fast path: send report immediately if endpoint is ready - bool success = tud_hid_report(runtime_kbd_report_id, report, sizeof(hid_keyboard_report_t)); + bool success = tud_hid_n_report(mouse_device_instance, runtime_kbd_report_id, report, sizeof(hid_keyboard_report_t)); if (success) { // Skip error counter reset for performance @@ -1711,7 +1846,7 @@ void hid_device_task(void) // ARCHITECTURE: Core0 is the ONLY core that calls tud_hid_report(). // Core1 (physical mouse callbacks) accumulates into kmbox accumulators // and sets was_active=true. We drain everything here. - if (tud_hid_ready()) + if (tud_hid_n_ready(mouse_device_instance)) { bool has_kmbox = kmbox_has_pending_movement(); bool has_smooth = smooth_has_pending(); @@ -1729,13 +1864,10 @@ void hid_device_task(void) int8_t wheel, pan; kmbox_get_mouse_report_16(&buttons, &x, &y, &wheel, &pan); - // Process smooth injection + // Process smooth injection (int16_t for high-DPI support) int16_t smooth_x = 0, smooth_y = 0; if (has_smooth) { - int8_t sx8 = 0, sy8 = 0; - smooth_process_frame(&sx8, &sy8); - smooth_x = sx8; - smooth_y = sy8; + smooth_process_frame(&smooth_x, &smooth_y); x += smooth_x; y += smooth_y; } @@ -1790,7 +1922,7 @@ void hid_device_task(void) uint8_t raw[16]; build_raw_mouse_report(raw, sizeof(raw), &output_mouse_layout_16bit, buttons, x, y, wheel, pan); - tud_hid_report(REPORT_ID_MOUSE, raw, output_mouse_layout_16bit.report_size); + tud_hid_n_report(mouse_device_instance, REPORT_ID_MOUSE, raw, output_mouse_layout_16bit.report_size); } else if (host_mouse_layout.valid && host_mouse_desc_len > 0) { uint8_t raw[64]; uint8_t sz = host_mouse_layout.report_size; @@ -1800,12 +1932,12 @@ void hid_device_task(void) buttons, x, y, wheel, pan); uint8_t rid = host_mouse_layout.has_report_id ? host_mouse_layout.mouse_report_id : REPORT_ID_MOUSE; - tud_hid_report(rid, raw, sz); + tud_hid_n_report(mouse_device_instance, rid, raw, sz); } else { // Clamp to int8 for standard HID mouse report int8_t cx = (x > 127) ? 127 : ((x < -128) ? -128 : (int8_t)x); int8_t cy = (y > 127) ? 127 : ((y < -128) ? -128 : (int8_t)y); - tud_hid_mouse_report(REPORT_ID_MOUSE, buttons, cx, cy, wheel, pan); + tud_hid_n_mouse_report(mouse_device_instance, REPORT_ID_MOUSE, buttons, cx, cy, wheel, pan); } last_sent_buttons = buttons; was_active = true; @@ -1830,7 +1962,7 @@ void hid_device_task(void) // --- Active → idle edge: send one final zero-delta stop report --- // Real mice send a last report with zero deltas (confirming the stop) // before they begin NAKing idle polls. Mirror that behavior here. - if (was_active && tud_hid_ready()) + if (was_active && tud_hid_n_ready(mouse_device_instance)) { uint8_t current_buttons = kmbox_get_current_buttons(); if (host_mouse_layout.valid && host_mouse_desc_len > 0) { @@ -1842,9 +1974,9 @@ void hid_device_task(void) current_buttons, 0, 0, 0, 0); uint8_t rid = host_mouse_layout.has_report_id ? host_mouse_layout.mouse_report_id : REPORT_ID_MOUSE; - tud_hid_report(rid, raw, sz); + tud_hid_n_report(mouse_device_instance, rid, raw, sz); } else { - tud_hid_mouse_report(REPORT_ID_MOUSE, current_buttons, 0, 0, 0, 0); + tud_hid_n_mouse_report(mouse_device_instance, REPORT_ID_MOUSE, current_buttons, 0, 0, 0, 0); } last_sent_buttons = current_buttons; was_active = false; @@ -1863,6 +1995,19 @@ void hid_device_task(void) { send_hid_report(REPORT_ID_MOUSE); } + + // --- Drain vendor report queue (Core1 → Core0 passthrough) --- + // Forward vendor/non-mouse reports from the host device to the downstream PC. + // This enables software like Logitech G Hub / Razer Synapse to communicate + // through the proxy for battery status, DPI changes, lighting, etc. + while (vendor_fwd_queue.tail != vendor_fwd_queue.head) { + vendor_report_entry_t *e = &vendor_fwd_queue.entries[vendor_fwd_queue.tail]; + if (e->device_instance < CFG_TUD_HID && tud_hid_n_ready(e->device_instance)) { + tud_hid_n_report(e->device_instance, e->report_id, e->data, e->len); + } + __dmb(); + vendor_fwd_queue.tail = (vendor_fwd_queue.tail + 1) % VENDOR_QUEUE_SIZE; + } } void send_hid_report(uint8_t report_id) @@ -1891,11 +2036,11 @@ void send_hid_report(uint8_t report_id) if (!connection_state.keyboard_connected) { // Check device readiness before each report - if (tud_hid_ready()) + if (tud_hid_n_ready(mouse_device_instance)) { // Use static array to avoid stack allocation overhead static const uint8_t empty_keycode[HID_KEYBOARD_KEYCODE_COUNT] = {0}; - tud_hid_keyboard_report(runtime_kbd_report_id, 0, empty_keycode); + tud_hid_n_keyboard_report(mouse_device_instance, runtime_kbd_report_id, 0, empty_keycode); } } break; @@ -1905,7 +2050,7 @@ void send_hid_report(uint8_t report_id) if (!connection_state.mouse_connected) { // Check device readiness before each report - if (tud_hid_ready()) + if (tud_hid_n_ready(mouse_device_instance)) { static bool prev_button_state = true; // true = not pressed (active low) bool current_button_state = gpio_get(PIN_BUTTON); @@ -1913,14 +2058,14 @@ void send_hid_report(uint8_t report_id) if (!current_button_state) { // button pressed (active low) // Mouse move up (negative Y direction) - tud_hid_mouse_report(REPORT_ID_MOUSE, MOUSE_BUTTON_NONE, + tud_hid_n_mouse_report(mouse_device_instance, REPORT_ID_MOUSE, MOUSE_BUTTON_NONE, MOUSE_NO_MOVEMENT, MOUSE_BUTTON_MOVEMENT_DELTA, MOUSE_NO_MOVEMENT, MOUSE_NO_MOVEMENT); } else if (prev_button_state != current_button_state) { // Send stop movement when button is released - tud_hid_mouse_report(REPORT_ID_MOUSE, MOUSE_BUTTON_NONE, + tud_hid_n_mouse_report(mouse_device_instance, REPORT_ID_MOUSE, MOUSE_BUTTON_NONE, MOUSE_NO_MOVEMENT, MOUSE_NO_MOVEMENT, MOUSE_NO_MOVEMENT, MOUSE_NO_MOVEMENT); } @@ -1933,10 +2078,10 @@ void send_hid_report(uint8_t report_id) case REPORT_ID_CONSUMER_CONTROL: { // CRITICAL: Check device readiness before each report - if (tud_hid_ready()) + if (tud_hid_n_ready(mouse_device_instance)) { static const uint16_t empty_key = 0; - tud_hid_report(runtime_consumer_report_id, &empty_key, HID_CONSUMER_CONTROL_SIZE); + tud_hid_n_report(mouse_device_instance, runtime_consumer_report_id, &empty_key, HID_CONSUMER_CONTROL_SIZE); } break; } @@ -1948,8 +2093,16 @@ void send_hid_report(uint8_t report_id) void hid_host_task(void) { - // This function can be called from core0 if needed for additional host processing - // The main host task runs on core1 in PIOKMbox.c + // Drain SET_REPORT passthrough queue (Core0 → Core1). + // Forward vendor SET_REPORT requests from the downstream PC to the real mouse. + while (set_report_queue.tail != set_report_queue.head) { + set_report_entry_t *e = &set_report_queue.entries[set_report_queue.tail]; + tuh_hid_set_report(e->host_dev_addr, e->host_instance, + e->report_id, e->report_type, + (void*)e->data, e->len); + __dmb(); + set_report_queue.tail = (set_report_queue.tail + 1) % VENDOR_QUEUE_SIZE; + } } // Device callbacks with improved error handling @@ -2016,24 +2169,25 @@ void tuh_umount_cb(uint8_t dev_addr) neopixel_update_status(); } -// HID host callbacks with improved validation +// HID host callbacks — multi-interface mirroring with vendor report passthrough void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, const uint8_t *desc_report, uint16_t desc_len) { uint16_t vid, pid; tuh_vid_pid_get(dev_addr, &vid, &pid); // === DEVICE-LEVEL CLONING (once per physical device) === - // Composite devices (e.g. Razer Basilisk V3 = 4 HID interfaces) trigger - // tuh_hid_mount_cb once per interface. Device/config descriptors and strings - // are device-level, so we only need to fetch them once. bool first_interface_for_device = (cloned_dev_addr != dev_addr); - + if (first_interface_for_device) { cloned_dev_addr = dev_addr; - + mounted_hid_itf_count = 0; + mirrored_itf_count = 0; + mouse_device_instance = 0; + memset(mirrored_itfs, 0, sizeof(mirrored_itfs)); + // Fetch string descriptors from the attached device fetch_device_string_descriptors(dev_addr); - + // Capture full device descriptor for identity cloning tusb_desc_device_t host_dev_desc; if (tuh_descriptor_get_device_sync(dev_addr, &host_dev_desc, sizeof(host_dev_desc)) == XFER_RESULT_SUCCESS) { @@ -2044,159 +2198,169 @@ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, const uint8_t *desc_re host_device_info.bMaxPacketSize0 = host_dev_desc.bMaxPacketSize0; host_device_info.bcdDevice = host_dev_desc.bcdDevice; host_device_info.valid = true; + + // Track string indices from device descriptor + if (host_dev_desc.iManufacturer > max_string_index_seen) max_string_index_seen = host_dev_desc.iManufacturer; + if (host_dev_desc.iProduct > max_string_index_seen) max_string_index_seen = host_dev_desc.iProduct; + if (host_dev_desc.iSerialNumber > max_string_index_seen) max_string_index_seen = host_dev_desc.iSerialNumber; } - - // Capture configuration descriptor (contains all interfaces + endpoints) - uint8_t cfg_buf[256]; + + // Capture and parse full configuration descriptor (all interfaces + endpoints) + uint8_t cfg_buf[512]; if (tuh_descriptor_get_configuration_sync(dev_addr, 0, cfg_buf, sizeof(cfg_buf)) == XFER_RESULT_SUCCESS) { - parse_host_config_descriptor(cfg_buf, sizeof(cfg_buf)); + uint16_t cfg_total = cfg_buf[2] | (cfg_buf[3] << 8); + if (cfg_total > sizeof(cfg_buf)) cfg_total = sizeof(cfg_buf); + parse_host_config_descriptor(cfg_buf, cfg_total); + } + + // Fetch any extra string descriptors beyond the standard 3 (manufacturer, product, serial) + for (uint8_t si = 4; si <= max_string_index_seen && extra_string_count < MAX_CACHED_STRINGS; si++) { + uint16_t tmp_buf[48]; + memset(tmp_buf, 0, sizeof(tmp_buf)); + if (tuh_descriptor_get_string_sync(dev_addr, si, LANGUAGE_ID, tmp_buf, sizeof(tmp_buf)) == XFER_RESULT_SUCCESS) { + cached_string_t *cs = &extra_strings[extra_string_count]; + cs->index = si; + utf16_to_utf8(tmp_buf, sizeof(tmp_buf), cs->str, sizeof(cs->str)); + cs->valid = (strlen(cs->str) > 0); + if (cs->valid) extra_string_count++; + } } } + mounted_hid_itf_count++; + // === DETERMINE EFFECTIVE PROTOCOL === uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); hid_instance_info_t *inst_info = alloc_hid_instance(dev_addr, instance); uint8_t effective_protocol = itf_protocol; - + if (itf_protocol == HID_ITF_PROTOCOL_NONE && desc_report != NULL && desc_len > 0) { - // Non-boot protocol device — detect usage from report descriptor effective_protocol = detect_usage_from_report_descriptor(desc_report, desc_len, inst_info); } - if (inst_info) { inst_info->effective_protocol = effective_protocol; } - // === MOUSE INTERFACE: Capture HID report descriptor === - // CRITICAL: Only capture the mouse descriptor from the mouse interface! - // Composite devices have multiple interfaces with different descriptors. - // e.g. Razer Basilisk V3: - // Interface 0: Boot Mouse (protocol=2), 79 byte desc — THIS IS THE MOUSE - // Interface 1: Keyboard (protocol=1), 159 byte desc — macro keys - // Interface 2: Keyboard (protocol=1), 61 byte desc — more keys - // Interface 3: Vendor (protocol=0), 22 byte desc — lighting control - // We MUST NOT let interfaces 1-3 overwrite the mouse descriptor from interface 0. bool is_mouse_interface = (effective_protocol == HID_ITF_PROTOCOL_MOUSE); - - if (is_mouse_interface && desc_report != NULL && desc_len > 0) { - // Capture this interface's HID report descriptor as the mouse descriptor. - // Logitech Unifying receivers include HID++ vendor collections (Report IDs - // 0x10/0x11, Usage Page 0xFF00) alongside the mouse collection. Strip - // these before storing — they confuse host HID drivers and can trigger - // installation of vendor filter drivers that fight the proxy. - size_t copy_len = desc_len; - if (copy_len > sizeof(host_mouse_desc)) - copy_len = sizeof(host_mouse_desc); - host_mouse_desc_len = strip_vendor_collections(desc_report, copy_len, - host_mouse_desc, sizeof(host_mouse_desc)); - - // Parse the mouse report layout to discover field offsets for raw forwarding - parse_mouse_report_layout(host_mouse_desc, host_mouse_desc_len, &host_mouse_layout); - - // CRITICAL FIX: Detect OS descriptor mismatches (macOS/Windows sometimes expose - // 8-bit descriptors for 16-bit mice). Check if descriptor claims 8-bit X/Y but - // report is large enough for 16-bit data (8+ bytes = 2 btn + 2 X + 2 Y + wheel + pan). - if (host_mouse_layout.valid && - host_mouse_layout.x_bits == 8 && - host_mouse_layout.y_bits == 8 && - host_mouse_layout.report_size >= 8 && - host_mouse_layout.buttons_bits >= 8) { - // Descriptor says 8-bit but structure suggests 16-bit - // Override to 16-bit layout (common with Logitech Lightspeed, etc.) - host_mouse_layout.x_bits = 16; - host_mouse_layout.y_bits = 16; - host_mouse_layout.x_is_16bit = true; - host_mouse_layout.y_is_16bit = true; - host_mouse_layout.report_size = 8; + + // === ALLOCATE MIRRORED INTERFACE SLOT === + // Each host HID interface gets a corresponding device-side interface. + uint8_t mirror_idx = mirrored_itf_count; + if (mirror_idx < MAX_DEVICE_HID_INTERFACES) { + mirrored_interface_t *mitf = &mirrored_itfs[mirror_idx]; + mitf->active = true; + mitf->host_dev_addr = dev_addr; + mitf->host_instance = instance; + mitf->is_mouse = is_mouse_interface; + + // Endpoint config was already parsed from config descriptor; + // the Nth HID interface in the config maps to mirrored_itfs[N]. + // (parse_host_config_descriptor pre-populated subclass/protocol/ep_*) + + // Track which device-side instance is the mouse (composite descriptor) + if (is_mouse_interface) { + mouse_device_instance = mirror_idx; } - - // Use the layout-parsed report ID (extracted from the mouse collection context) - // instead of blindly scanning for the first 0x85 tag - if (host_mouse_layout.valid && host_mouse_layout.has_report_id) { - host_mouse_has_report_id = true; - host_mouse_report_id = host_mouse_layout.mouse_report_id; - } else { - // Fallback: scan for any Report ID tag in the descriptor - host_mouse_has_report_id = false; - host_mouse_report_id = 0; - for (size_t i = 0; i + 1 < host_mouse_desc_len; ++i) { - if (host_mouse_desc[i] == 0x85) { - host_mouse_has_report_id = true; - host_mouse_report_id = host_mouse_desc[i + 1]; - break; + + if (is_mouse_interface && desc_report != NULL && desc_len > 0) { + // --- MOUSE INTERFACE --- + // Store full descriptor (including vendor collections) for the mouse interface. + size_t copy_len = desc_len; + if (copy_len > sizeof(host_mouse_desc)) copy_len = sizeof(host_mouse_desc); + memcpy(host_mouse_desc, desc_report, copy_len); + host_mouse_desc_len = copy_len; + + // Parse mouse report layout for raw forwarding + parse_mouse_report_layout(host_mouse_desc, host_mouse_desc_len, &host_mouse_layout); + + // Detect OS descriptor mismatches (8-bit desc for 16-bit mouse) + if (host_mouse_layout.valid && + host_mouse_layout.x_bits == 8 && host_mouse_layout.y_bits == 8 && + host_mouse_layout.report_size >= 8 && host_mouse_layout.buttons_bits >= 8) { + host_mouse_layout.x_bits = 16; + host_mouse_layout.y_bits = 16; + host_mouse_layout.x_is_16bit = true; + host_mouse_layout.y_is_16bit = true; + host_mouse_layout.report_size = 8; + } + + // Extract mouse report ID + if (host_mouse_layout.valid && host_mouse_layout.has_report_id) { + host_mouse_has_report_id = true; + host_mouse_report_id = host_mouse_layout.mouse_report_id; + } else { + host_mouse_has_report_id = false; + host_mouse_report_id = 0; + for (size_t i = 0; i + 1 < host_mouse_desc_len; ++i) { + if (host_mouse_desc[i] == 0x85) { + host_mouse_has_report_id = true; + host_mouse_report_id = host_mouse_desc[i + 1]; + break; + } } } - } - - // Also update instance info with the correct mouse report ID - if (inst_info) { - inst_info->has_report_id = host_mouse_has_report_id; - inst_info->mouse_report_id = host_mouse_report_id; - } - // Build runtime HID report descriptor (keyboard + mouse + consumer) - build_runtime_hid_report_with_mouse(host_mouse_desc, host_mouse_desc_len); - rebuild_configuration_descriptor(); - - // CRITICAL: set_attached_device_vid_pid() triggers force_usb_reenumeration() - // which disconnects/reconnects the device stack. This MUST happen AFTER all - // descriptors are fully rebuilt. It also only triggers if VID/PID changed. - set_attached_device_vid_pid(vid, pid); - } - else if (first_interface_for_device && !is_mouse_interface) { - // First interface mounted but it's not the mouse — still need to set VID/PID - // so we present the correct identity. The mouse descriptor will be captured - // when the mouse interface mounts (if it exists). - // Don't overwrite host_mouse_desc or host_mouse_layout here! - - // If we haven't seen a mouse interface yet for this device, build - // descriptors with defaults. They'll be rebuilt when mouse mounts. - if (host_mouse_desc_len == 0) { - build_runtime_hid_report_with_mouse(NULL, 0); - rebuild_configuration_descriptor(); - set_attached_device_vid_pid(vid, pid); + if (inst_info) { + inst_info->has_report_id = host_mouse_has_report_id; + inst_info->mouse_report_id = host_mouse_report_id; + } + + // Build composite HID report descriptor (keyboard + mouse + consumer) + build_runtime_hid_report_with_mouse(host_mouse_desc, host_mouse_desc_len); + + } else if (desc_report != NULL && desc_len > 0) { + // --- NON-MOUSE INTERFACE (keyboard-macros, vendor, etc.) --- + // Store verbatim descriptor for faithful mirroring + size_t copy_len = desc_len; + if (copy_len > MIRROR_ITF_DESC_MAX) copy_len = MIRROR_ITF_DESC_MAX; + memcpy(mitf->report_desc, desc_report, copy_len); + mitf->report_desc_len = copy_len; } + + mirrored_itf_count++; } // Handle HID device connection using effective protocol handle_hid_device_connection(dev_addr, effective_protocol); - // Start receiving reports — but only from interfaces we actually want. - // - // Composite gaming mice (e.g. Razer Basilisk V3) expose multiple HID - // interfaces on a single dev_addr: - // Interface 0: Boot Mouse (protocol=2) — WE WANT THIS - // Interface 1: Keyboard (protocol=1) — macro keys, NOT a real keyboard - // Interface 2: Keyboard (protocol=1) — media keys, NOT a real keyboard - // Interface 3: Vendor (protocol=0) — lighting control - // - // If we receive reports from the non-mouse interfaces, their periodic - // status/idle reports get forwarded as keyboard input, causing garbage - // keypresses (e.g. '#' flood). - // - // Rule: Only receive from mouse interfaces, OR from keyboard/vendor - // interfaces on a DIFFERENT device (i.e. a standalone keyboard). + // === START RECEIVING REPORTS === + // Receive from ALL interfaces on the mouse device for vendor report passthrough. + // Standalone keyboards on a different device are also received. + // Filtering of composite macro-keyboard garbage happens in report_received_cb. bool should_receive = false; - + if (is_mouse_interface) { - // Always receive from mouse interfaces should_receive = true; } else if (dev_addr != connection_state.mouse_dev_addr) { - // This interface is on a different physical device than the mouse, - // so it's a standalone keyboard — receive its reports + // Standalone keyboard on different device + should_receive = true; + } else { + // Non-mouse interface on same device — receive for vendor report passthrough should_receive = true; } - // else: non-mouse interface on the same device as the mouse → skip if (should_receive) { if (!tuh_hid_receive_report(dev_addr, instance)) { neopixel_trigger_activity_flash_color(COLOR_USB_DISCONNECTION); - } else { - neopixel_update_status(); } - } else { - // Skip receiving from non-mouse interfaces on the same device } + + // === DEFERRED RE-ENUMERATION === + // Wait until all expected HID interfaces have mounted before triggering + // re-enumeration. This ensures the config descriptor includes ALL + // interfaces, not just the first one. + if (mounted_hid_itf_count >= expected_hid_itf_count) { + // All interfaces captured — build final descriptors + if (host_mouse_desc_len == 0) { + // No mouse interface found; use defaults + build_runtime_hid_report_with_mouse(NULL, 0); + } + rebuild_configuration_descriptor(); + + // Trigger re-enumeration (only fires if VID/PID actually changed) + set_attached_device_vid_pid(vid, pid); + } + neopixel_update_status(); } @@ -2307,6 +2471,61 @@ static void __not_in_flash_func(parse_and_forward_mouse_report)(const uint8_t *d process_mouse_report(&mouse_report_local); } +// Queue a vendor/non-mouse report for Core0 to send on the device side. +// Called from Core1 — must not call any tud_* functions. +static void __not_in_flash_func(queue_vendor_report)(uint8_t device_instance, uint8_t report_id, + const uint8_t *data, uint8_t data_len) +{ + uint8_t capped_len = (data_len > VENDOR_REPORT_MAX_LEN) ? VENDOR_REPORT_MAX_LEN : data_len; + + // Update GET_REPORT cache so tud_hid_get_report_cb can respond to macOS IOKit. + if (device_instance < MAX_DEVICE_HID_INTERFACES) { + cached_report_t *slots = report_cache[device_instance]; + int slot = -1; + int empty = -1; + for (int i = 0; i < REPORT_CACHE_SLOTS_PER_ITF; i++) { + if (slots[i].valid && slots[i].report_id == report_id) { slot = i; break; } + if (!slots[i].valid && empty < 0) empty = i; + } + if (slot < 0) slot = (empty >= 0) ? empty : 0; // Evict first slot if full + slots[slot].report_id = report_id; + slots[slot].len = capped_len; + memcpy(slots[slot].data, data, capped_len); + __dmb(); + slots[slot].valid = true; + } + + // Queue for Core0 to forward via interrupt IN endpoint + uint8_t next_head = (vendor_fwd_queue.head + 1) % VENDOR_QUEUE_SIZE; + if (next_head == vendor_fwd_queue.tail) return; // Queue full, drop + + vendor_report_entry_t *e = &vendor_fwd_queue.entries[vendor_fwd_queue.head]; + e->device_instance = device_instance; + e->report_id = report_id; + e->len = capped_len; + memcpy(e->data, data, capped_len); + + __dmb(); // Ensure data written before head advances + vendor_fwd_queue.head = next_head; +} + +// Find which mirrored interface slot corresponds to a host (dev_addr, instance). +static mirrored_interface_t* find_mirrored_interface(uint8_t dev_addr, uint8_t instance) { + for (uint8_t i = 0; i < mirrored_itf_count; i++) { + if (mirrored_itfs[i].active && + mirrored_itfs[i].host_dev_addr == dev_addr && + mirrored_itfs[i].host_instance == instance) { + return &mirrored_itfs[i]; + } + } + return NULL; +} + +// Get the device-side instance index for a mirrored interface. +static uint8_t mirrored_device_instance(const mirrored_interface_t *mitf) { + return (uint8_t)(mitf - mirrored_itfs); +} + void __not_in_flash_func(tuh_hid_report_received_cb)(uint8_t dev_addr, uint8_t instance, const uint8_t *report, uint16_t len) { if (report == NULL || len == 0) @@ -2315,45 +2534,46 @@ void __not_in_flash_func(tuh_hid_report_received_cb)(uint8_t dev_addr, uint8_t i return; } - // Look up effective protocol from our per-instance tracking - // This handles non-boot-protocol devices (Logitech, gaming mice, etc.) + // Look up effective protocol from per-instance tracking uint8_t effective_protocol = tuh_hid_interface_protocol(dev_addr, instance); hid_instance_info_t *inst_info = find_hid_instance(dev_addr, instance); bool has_report_id = false; - uint8_t mouse_report_id = 0; - + uint8_t mouse_report_id_local = 0; + if (inst_info) { effective_protocol = inst_info->effective_protocol; has_report_id = inst_info->has_report_id; - mouse_report_id = inst_info->mouse_report_id; + mouse_report_id_local = inst_info->mouse_report_id; } - // Direct processing without extra copying for better performance - // SAFETY: Only forward keyboard reports from standalone keyboard devices, - // not from keyboard interfaces on composite mouse devices (e.g. Razer - // Basilisk V3 exposes macro/media key interfaces that send garbage data). switch (effective_protocol) { case HID_ITF_PROTOCOL_KEYBOARD: { - // Only forward if this is a standalone keyboard (different device than mouse) + // Only forward keyboard from standalone devices (different device than mouse) if (dev_addr == connection_state.mouse_dev_addr) { - // This keyboard interface is on the same device as the mouse — - // it's a composite gaming mouse's macro/media keys, skip it + // Composite gaming mouse macro/media keys — queue for vendor passthrough + // instead of interpreting as keyboard input + mirrored_interface_t *mitf = find_mirrored_interface(dev_addr, instance); + if (mitf) { + uint8_t dev_inst = mirrored_device_instance(mitf); + uint8_t rid = (has_report_id && len > 0) ? report[0] : 0; + const uint8_t *data = (has_report_id && len > 0) ? report + 1 : report; + uint8_t dlen = (has_report_id && len > 0) ? (uint8_t)(len - 1) : (uint8_t)len; + queue_vendor_report(dev_inst, rid, data, dlen); + } break; } - + const uint8_t *kbd_data = report; uint16_t kbd_len = len; - - // Strip report ID prefix if present + if (has_report_id && kbd_len > 0) { kbd_data++; kbd_len--; } - - if (kbd_len >= (int)sizeof(hid_keyboard_report_t)) - { + + if (kbd_len >= (int)sizeof(hid_keyboard_report_t)) { process_kbd_report((const hid_keyboard_report_t*)kbd_data); } } @@ -2363,26 +2583,40 @@ void __not_in_flash_func(tuh_hid_report_received_cb)(uint8_t dev_addr, uint8_t i { const uint8_t *mouse_data = report; uint16_t mouse_len = len; - - // For composite/report-ID devices, the first byte is the report ID - // We need to strip it before parsing the mouse data + if (has_report_id && mouse_len > 0) { uint8_t received_id = mouse_data[0]; - // Only process if this report ID matches the mouse report ID - if (received_id != mouse_report_id) { - // Not a mouse report from this composite device - skip + if (received_id != mouse_report_id_local) { + // Not the mouse report — this is a vendor report on the mouse + // interface (e.g. Logitech HID++ on same interface as mouse). + // Queue for passthrough to device side. + mirrored_interface_t *mitf = find_mirrored_interface(dev_addr, instance); + if (mitf) { + uint8_t dev_inst = mirrored_device_instance(mitf); + queue_vendor_report(dev_inst, received_id, mouse_data + 1, (uint8_t)(mouse_len - 1)); + } break; } mouse_data++; mouse_len--; } - + parse_and_forward_mouse_report(mouse_data, mouse_len); } break; default: - // Unknown HID protocol - ignore + { + // Unknown/vendor protocol — forward entire report for passthrough + mirrored_interface_t *mitf = find_mirrored_interface(dev_addr, instance); + if (mitf) { + uint8_t dev_inst = mirrored_device_instance(mitf); + uint8_t rid = (has_report_id && len > 0) ? report[0] : 0; + const uint8_t *data = (has_report_id && len > 0) ? report + 1 : report; + uint8_t dlen = (has_report_id && len > 0) ? (uint8_t)(len - 1) : (uint8_t)len; + queue_vendor_report(dev_inst, rid, data, dlen); + } + } break; } @@ -2393,25 +2627,35 @@ void __not_in_flash_func(tuh_hid_report_received_cb)(uint8_t dev_addr, uint8_t i // HID device callbacks with improved validation uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) { - (void)instance; - (void)report_id; (void)report_type; - (void)buffer; - (void)reqlen; - return 0; + + if (instance >= MAX_DEVICE_HID_INTERFACES || !buffer || reqlen == 0) return 0; + + // Look up cached report from the real device + cached_report_t *slots = report_cache[instance]; + __dmb(); // Ensure we see Core1's latest cache writes + for (int i = 0; i < REPORT_CACHE_SLOTS_PER_ITF; i++) { + if (slots[i].valid && slots[i].report_id == report_id) { + uint16_t copy_len = (slots[i].len < reqlen) ? slots[i].len : reqlen; + memcpy(buffer, slots[i].data, copy_len); + return copy_len; + } + } + + // No cached data yet — return zeros so the host sees the device as responsive. + // macOS IOKit sends GET_REPORT during device open; returning 0 (no data) causes + // "open failed". Returning a zeroed buffer keeps the open path happy. + uint16_t fill_len = (reqlen > VENDOR_REPORT_MAX_LEN) ? VENDOR_REPORT_MAX_LEN : reqlen; + memset(buffer, 0, fill_len); + return fill_len; } void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, const uint8_t *buffer, uint16_t bufsize) { - (void)instance; - + // Handle keyboard LED output reports (caps lock, etc.) if (report_type == HID_REPORT_TYPE_OUTPUT && report_id == runtime_kbd_report_id) { - // Validate buffer - if (buffer == NULL || bufsize < MIN_BUFFER_SIZE) - { - return; - } + if (buffer == NULL || bufsize < MIN_BUFFER_SIZE) return; uint8_t const kbd_leds = buffer[0]; bool new_caps_state = (kbd_leds & KEYBOARD_LED_CAPSLOCK) != 0; @@ -2419,9 +2663,29 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_ if (new_caps_state != caps_lock_state) { caps_lock_state = new_caps_state; - // Indicate caps lock change with LED flash instead of console logging neopixel_trigger_caps_lock_flash(); } + return; + } + + // Forward all other SET_REPORT requests to the real mouse for vendor passthrough. + // This enables Logitech G Hub, Razer Synapse, etc. to configure DPI, lighting, + // macros, battery queries through the proxy. + if (buffer != NULL && bufsize > 0 && instance < mirrored_itf_count && mirrored_itfs[instance].active) { + mirrored_interface_t *mitf = &mirrored_itfs[instance]; + + uint8_t next_head = (set_report_queue.head + 1) % VENDOR_QUEUE_SIZE; + if (next_head != set_report_queue.tail) { + set_report_entry_t *e = &set_report_queue.entries[set_report_queue.head]; + e->host_dev_addr = mitf->host_dev_addr; + e->host_instance = mitf->host_instance; + e->report_id = report_id; + e->report_type = (uint8_t)report_type; + e->len = (bufsize > VENDOR_REPORT_MAX_LEN) ? VENDOR_REPORT_MAX_LEN : (uint8_t)bufsize; + memcpy(e->data, buffer, e->len); + __dmb(); + set_report_queue.head = next_head; + } } } @@ -2586,16 +2850,28 @@ uint8_t const * tud_descriptor_device_cb(void) return (uint8_t const *)&desc_device; } -// HID Report Descriptor +// HID Report Descriptor — per-instance for multi-interface mirroring. +// Instance 0 is typically the mouse interface (composite: keyboard + mouse + consumer). +// Other instances return verbatim cloned descriptors from the host device. uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { - (void)instance; - if (desc_hid_runtime_valid) - { + // Multi-interface mode: return the correct descriptor for each instance + if (mirrored_itf_count > 0 && instance < mirrored_itf_count && mirrored_itfs[instance].active) { + if (mirrored_itfs[instance].is_mouse) { + // Mouse interface returns composite descriptor (keyboard + mouse + consumer) + return desc_hid_runtime_valid ? desc_hid_report_runtime : desc_hid_report; + } + // Non-mouse interface returns verbatim host descriptor + if (mirrored_itfs[instance].report_desc_len > 0) { + return mirrored_itfs[instance].report_desc; + } + } + + // Single-interface fallback + if (instance == 0 && desc_hid_runtime_valid) { return desc_hid_report_runtime; } - // Fallback to static concatenation if runtime descriptor not ready - return desc_hid_report_runtime; // still points to buffer (may contain defaults) + return desc_hid_report; } // Configuration Descriptor - now dynamic for cloning @@ -2605,108 +2881,217 @@ enum ITF_NUM_TOTAL }; -// Offsets within the configuration descriptor for fields we patch -#define CFG_DESC_OFFSET_BMATTRIBUTES 7 -#define CFG_DESC_OFFSET_BMAXPOWER 8 -#define HID_ITF_OFFSET_SUBCLASS (TUD_CONFIG_DESC_LEN + 6) // bInterfaceSubClass -#define HID_ITF_OFFSET_PROTOCOL (TUD_CONFIG_DESC_LEN + 7) // bInterfaceProtocol -#define HID_DESC_OFFSET_REPORT_LEN_LO (TUD_CONFIG_DESC_LEN + 9 + 7) // wDescriptorLength low byte -#define HID_DESC_OFFSET_REPORT_LEN_HI (TUD_CONFIG_DESC_LEN + 9 + 8) // wDescriptorLength high byte -#define HID_EP_OFFSET_MAXPACKET_LO (TUD_CONFIG_DESC_LEN + 9 + 9 + 4) // wMaxPacketSize low byte -#define HID_EP_OFFSET_MAXPACKET_HI (TUD_CONFIG_DESC_LEN + 9 + 9 + 5) // wMaxPacketSize high byte -#define HID_EP_OFFSET_INTERVAL (TUD_CONFIG_DESC_LEN + 9 + 9 + 6) // bInterval - -// Build the configuration descriptor from current runtime state +// Helper: write one HID interface block (interface desc + HID desc + endpoint(s)) +// into a buffer. Returns number of bytes written. +static uint16_t write_hid_interface_desc(uint8_t *buf, uint16_t buf_max, + uint8_t itf_num, uint8_t subclass, + uint8_t protocol, uint16_t report_desc_len, + uint8_t ep_in_addr, uint16_t ep_in_size, + uint8_t ep_in_interval, + bool has_ep_out, uint8_t ep_out_interval) +{ + uint16_t pos = 0; + uint8_t num_eps = has_ep_out ? 2 : 1; + + // Interface descriptor (9 bytes) + if (pos + 9 > buf_max) return 0; + buf[pos++] = 9; + buf[pos++] = TUSB_DESC_INTERFACE; + buf[pos++] = itf_num; + buf[pos++] = 0; // bAlternateSetting + buf[pos++] = num_eps; + buf[pos++] = TUSB_CLASS_HID; + buf[pos++] = subclass; + buf[pos++] = protocol; + buf[pos++] = 0; // iInterface + + // HID descriptor (9 bytes) + if (pos + 9 > buf_max) return 0; + buf[pos++] = 9; + buf[pos++] = HID_DESC_TYPE_HID; + buf[pos++] = 0x11; // bcdHID low (1.11) + buf[pos++] = 0x01; // bcdHID high + buf[pos++] = 0; // bCountryCode + buf[pos++] = 1; // bNumDescriptors + buf[pos++] = HID_DESC_TYPE_REPORT; + buf[pos++] = (uint8_t)(report_desc_len & 0xFF); + buf[pos++] = (uint8_t)((report_desc_len >> 8) & 0xFF); + + // IN endpoint descriptor (7 bytes) + if (pos + 7 > buf_max) return 0; + buf[pos++] = 7; + buf[pos++] = TUSB_DESC_ENDPOINT; + buf[pos++] = ep_in_addr; + buf[pos++] = TUSB_XFER_INTERRUPT; + buf[pos++] = (uint8_t)(ep_in_size & 0xFF); + buf[pos++] = (uint8_t)((ep_in_size >> 8) & 0xFF); + buf[pos++] = ep_in_interval; + + // OUT endpoint descriptor (7 bytes, optional) + if (has_ep_out) { + if (pos + 7 > buf_max) return 0; + buf[pos++] = 7; + buf[pos++] = TUSB_DESC_ENDPOINT; + buf[pos++] = ep_in_addr & 0x0F; // Same EP number, OUT direction + buf[pos++] = TUSB_XFER_INTERRUPT; + buf[pos++] = (uint8_t)(ep_in_size & 0xFF); + buf[pos++] = (uint8_t)((ep_in_size >> 8) & 0xFF); + buf[pos++] = ep_out_interval; + } + + return pos; +} + +// Build the configuration descriptor from current runtime state. +// Supports 1-4 HID interfaces mirroring the host device's layout. static void rebuild_configuration_descriptor(void) { - // Determine actual report descriptor length - size_t report_desc_len = desc_hid_runtime_valid ? desc_hid_runtime_len : sizeof(desc_hid_report); - - // Clone ALL configuration descriptor fields from the host mouse. - // The goal is to present as the exact same device to the downstream PC. + uint8_t num_itfs = (mirrored_itf_count > 0) ? mirrored_itf_count : 1; + if (num_itfs > MAX_DEVICE_HID_INTERFACES) num_itfs = MAX_DEVICE_HID_INTERFACES; + uint8_t cfg_attributes = TU_BIT(7) | (host_config_info.valid ? host_config_info.bmAttributes : TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP); uint8_t cfg_max_power = host_config_info.valid ? host_config_info.bMaxPower : (USB_CONFIG_POWER_MA / 2); - uint8_t itf_sub_class = host_config_info.valid ? host_config_info.bInterfaceSubClass : 0; - uint8_t itf_protocol = host_config_info.valid ? host_config_info.bInterfaceProtocol : HID_ITF_PROTOCOL_NONE; - // CRITICAL: wMaxPacketSize must be our actual EP buffer size, NOT the host mouse's. - // We re-encode reports through tud_hid_mouse_report() / tud_hid_keyboard_report(), - // and our keyboard report (1 + 8 = 9 bytes) may exceed a small mouse EP size (e.g. 8). - // The downstream PC uses this to allocate its receive buffer — it must fit our largest report. - uint16_t ep_max_packet = CFG_TUD_HID_EP_BUFSIZE; - uint8_t ep_interval = host_config_info.valid ? host_config_info.bInterval : HID_POLLING_INTERVAL_MS; - - // Build using the TinyUSB macros as a base template, then patch - uint8_t template[] = { - TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, USB_CONFIG_POWER_MA), - TUD_HID_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, HID_POLLING_INTERVAL_MS) - }; - - _Static_assert(sizeof(template) == TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN, "config descriptor size mismatch"); - memcpy(desc_configuration_runtime, template, sizeof(template)); - - // Patch config descriptor fields - desc_configuration_runtime[CFG_DESC_OFFSET_BMATTRIBUTES] = cfg_attributes; - desc_configuration_runtime[CFG_DESC_OFFSET_BMAXPOWER] = cfg_max_power; - - // Patch HID interface descriptor fields - desc_configuration_runtime[HID_ITF_OFFSET_SUBCLASS] = itf_sub_class; - desc_configuration_runtime[HID_ITF_OFFSET_PROTOCOL] = itf_protocol; - - // Patch HID report descriptor length (critical — must match what tud_hid_descriptor_report_cb returns) - desc_configuration_runtime[HID_DESC_OFFSET_REPORT_LEN_LO] = (uint8_t)(report_desc_len & 0xFF); - desc_configuration_runtime[HID_DESC_OFFSET_REPORT_LEN_HI] = (uint8_t)((report_desc_len >> 8) & 0xFF); - - // Patch endpoint descriptor fields - desc_configuration_runtime[HID_EP_OFFSET_MAXPACKET_LO] = (uint8_t)(ep_max_packet & 0xFF); - desc_configuration_runtime[HID_EP_OFFSET_MAXPACKET_HI] = (uint8_t)((ep_max_packet >> 8) & 0xFF); - desc_configuration_runtime[HID_EP_OFFSET_INTERVAL] = ep_interval; - + + // Leave 9 bytes for config header, fill interfaces after + uint16_t pos = 9; + + for (uint8_t i = 0; i < num_itfs; i++) { + uint16_t report_len; + uint8_t subclass, protocol, ep_interval; + bool has_out = false; + uint8_t ep_out_interval_val = 1; + + if (mirrored_itf_count > 0 && i < mirrored_itf_count && mirrored_itfs[i].active) { + mirrored_interface_t *itf = &mirrored_itfs[i]; + if (itf->is_mouse) { + report_len = desc_hid_runtime_valid ? desc_hid_runtime_len : sizeof(desc_hid_report); + } else { + report_len = itf->report_desc_len; + } + subclass = itf->itf_subclass; + protocol = itf->itf_protocol; + ep_interval = itf->ep_in_interval ? itf->ep_in_interval : HID_POLLING_INTERVAL_MS; + has_out = itf->has_ep_out; + ep_out_interval_val = itf->ep_out_interval ? itf->ep_out_interval : ep_interval; + } else { + // Default single-interface fallback + report_len = desc_hid_runtime_valid ? desc_hid_runtime_len : sizeof(desc_hid_report); + subclass = host_config_info.valid ? host_config_info.bInterfaceSubClass : 0; + protocol = host_config_info.valid ? host_config_info.bInterfaceProtocol : HID_ITF_PROTOCOL_NONE; + ep_interval = host_config_info.valid ? host_config_info.bInterval : HID_POLLING_INTERVAL_MS; + } + + // CRITICAL: wMaxPacketSize must be our buffer size, not the host's. + // The downstream PC uses this to allocate receive buffers. + uint16_t written = write_hid_interface_desc( + &desc_configuration_runtime[pos], DESC_CONFIG_RUNTIME_MAX - pos, + i, // bInterfaceNumber + subclass, protocol, + report_len, + 0x81 + i, // EP IN address: 0x81, 0x82, 0x83, 0x84 + CFG_TUD_HID_EP_BUFSIZE, + ep_interval, + has_out, ep_out_interval_val + ); + if (written == 0) break; // Buffer full + pos += written; + } + + // Fill config descriptor header (first 9 bytes) + desc_configuration_runtime[0] = 9; // bLength + desc_configuration_runtime[1] = TUSB_DESC_CONFIGURATION; // bDescriptorType + desc_configuration_runtime[2] = (uint8_t)(pos & 0xFF); // wTotalLength low + desc_configuration_runtime[3] = (uint8_t)((pos >> 8) & 0xFF); // wTotalLength high + desc_configuration_runtime[4] = num_itfs; // bNumInterfaces + desc_configuration_runtime[5] = 1; // bConfigurationValue + desc_configuration_runtime[6] = 0; // iConfiguration + desc_configuration_runtime[7] = cfg_attributes; // bmAttributes + desc_configuration_runtime[8] = cfg_max_power; // bMaxPower + desc_config_runtime_valid = true; } -// Parse host configuration descriptor to extract endpoint size, interval, power, etc. +// Parse host configuration descriptor to extract ALL HID interfaces and their endpoints. +// Populates mirrored_itfs[] with per-interface endpoint configs, and tracks string indices. static void parse_host_config_descriptor(const uint8_t *cfg_desc, uint16_t cfg_len) { if (!cfg_desc || cfg_len < TUD_CONFIG_DESC_LEN) return; - + // Extract config-level fields host_config_info.bmAttributes = cfg_desc[7] & 0x7F; // Mask off reserved bit 7 (we add it back) host_config_info.bMaxPower = cfg_desc[8]; - - // Walk the descriptor chain to find HID interface + endpoint + + // Reset per-interface state + expected_hid_itf_count = 0; + + // Walk the descriptor chain to find ALL HID interfaces + their endpoints uint16_t offset = 0; - bool found_hid_interface = false; - + int current_hid_idx = -1; // Index into mirrored_itfs for current HID interface + while (offset + 1 < cfg_len) { uint8_t desc_length = cfg_desc[offset]; uint8_t desc_type = cfg_desc[offset + 1]; - + if (desc_length == 0) break; // Prevent infinite loop on malformed descriptors - + // Interface descriptor if (desc_type == TUSB_DESC_INTERFACE && desc_length >= 9 && offset + 8 < cfg_len) { uint8_t itf_class = cfg_desc[offset + 5]; - if (itf_class == TUSB_CLASS_HID) { - host_config_info.bInterfaceSubClass = cfg_desc[offset + 6]; - host_config_info.bInterfaceProtocol = cfg_desc[offset + 7]; - found_hid_interface = true; + if (itf_class == TUSB_CLASS_HID && expected_hid_itf_count < MAX_DEVICE_HID_INTERFACES) { + current_hid_idx = expected_hid_itf_count; + mirrored_itfs[current_hid_idx].itf_subclass = cfg_desc[offset + 6]; + mirrored_itfs[current_hid_idx].itf_protocol = cfg_desc[offset + 7]; + mirrored_itfs[current_hid_idx].has_ep_out = false; + + // Track interface string index for faithful string mirroring + uint8_t iInterface = cfg_desc[offset + 8]; + if (iInterface > 0 && iInterface > max_string_index_seen) + max_string_index_seen = iInterface; + + // Back-compat: populate legacy host_config_info from first HID interface + if (expected_hid_itf_count == 0) { + host_config_info.bInterfaceSubClass = cfg_desc[offset + 6]; + host_config_info.bInterfaceProtocol = cfg_desc[offset + 7]; + } + + expected_hid_itf_count++; + } else { + current_hid_idx = -1; // Non-HID interface, ignore endpoints } } - - // Endpoint descriptor (IN endpoint after HID interface) - if (found_hid_interface && desc_type == TUSB_DESC_ENDPOINT && desc_length >= 7 && offset + 6 < cfg_len) { + + // Endpoint descriptor (after a HID interface) + if (current_hid_idx >= 0 && desc_type == TUSB_DESC_ENDPOINT && desc_length >= 7 && offset + 6 < cfg_len) { uint8_t ep_addr = cfg_desc[offset + 2]; uint8_t ep_attr = cfg_desc[offset + 3]; - - // Only capture IN interrupt endpoint (direction bit 7 set, transfer type = interrupt) - if ((ep_addr & 0x80) && (ep_attr & 0x03) == TUSB_XFER_INTERRUPT) { - host_config_info.wMaxPacketSize = cfg_desc[offset + 4] | (cfg_desc[offset + 5] << 8); - host_config_info.bInterval = cfg_desc[offset + 6]; - host_config_info.valid = true; - break; // Found what we need + uint16_t ep_size = cfg_desc[offset + 4] | (cfg_desc[offset + 5] << 8); + uint8_t ep_interval = cfg_desc[offset + 6]; + + if ((ep_attr & 0x03) == TUSB_XFER_INTERRUPT) { + if (ep_addr & 0x80) { + // IN endpoint + mirrored_itfs[current_hid_idx].ep_in_max_packet = ep_size; + mirrored_itfs[current_hid_idx].ep_in_interval = ep_interval; + + // Back-compat: populate legacy host_config_info from first IN endpoint + if (!host_config_info.valid) { + host_config_info.wMaxPacketSize = ep_size; + host_config_info.bInterval = ep_interval; + host_config_info.valid = true; + } + } else { + // OUT endpoint + mirrored_itfs[current_hid_idx].has_ep_out = true; + mirrored_itfs[current_hid_idx].ep_out_max_packet = ep_size; + mirrored_itfs[current_hid_idx].ep_out_interval = ep_interval; + } } } - + offset += desc_length; } + + if (expected_hid_itf_count == 0) { + expected_hid_itf_count = 1; // At least 1 interface expected + } } // Static fallback (used only for initial sizeof reference) @@ -2791,21 +3176,9 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) } else { - // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. - // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors - - if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[BUFFER_FIRST_ELEMENT_INDEX]))) - { - return NULL; - } - - const char *str = string_desc_arr[index]; - if (str == NULL) - { - return NULL; - } + const char *str = NULL; - // Use dynamic string descriptors if available + // Standard indices 1-3: manufacturer, product, serial if (string_descriptors_fetched) { switch (index) { case STRING_DESC_MANUFACTURER_IDX: @@ -2822,17 +3195,34 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) } break; default: - // Use default for other indices break; } - } else { - // Fallback to default strings if not fetched yet - if (index == STRING_DESC_SERIAL_IDX) { - str = get_dynamic_serial_string(); + } + + // Check extra string cache for higher indices (interface strings, etc.) + if (str == NULL && index > STRING_DESC_SERIAL_IDX) { + for (uint8_t i = 0; i < extra_string_count; i++) { + if (extra_strings[i].valid && extra_strings[i].index == index) { + str = extra_strings[i].str; + break; + } } } - // Convert ASCII string to UTF-16 and get character count + // Fallback to static array or defaults + if (str == NULL) { + if (index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])) { + str = string_desc_arr[index]; + } + } + + // Final fallback for serial + if (str == NULL && index == STRING_DESC_SERIAL_IDX) { + str = get_dynamic_serial_string(); + } + + if (str == NULL) return NULL; + chr_count = convert_string_to_utf16(str, _desc_str); } From 46afbac29c7de35507d20fae61e885ed7b270307 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Thu, 26 Feb 2026 02:10:47 -0500 Subject: [PATCH 5/6] blocking operation tuning --- defines.h | 8 ++- humanization_fpu.c | 79 +++++++++++++-------- kmbox_serial_handler.c | 60 +++++++++------- lib/kmbox-commands/kmbox_commands.c | 65 +++++++++++++++++ lib/kmbox-commands/kmbox_commands.h | 8 +++ smooth_injection.c | 41 ++++++----- usb_hid.c | 104 ++++++++++++++++++++++------ 7 files changed, 274 insertions(+), 91 deletions(-) diff --git a/defines.h b/defines.h index f0bbbd8..14f4f7f 100644 --- a/defines.h +++ b/defines.h @@ -341,9 +341,11 @@ typedef struct __attribute__((packed)) { #define HID_KEYBOARD_KEYCODE_COUNT 6 // Number of simultaneous keycodes supported #define HID_CONSUMER_CONTROL_SIZE 2 // Consumer control report size in bytes -// Activity tracking -#define KEYBOARD_ACTIVITY_THROTTLE 50 // Trigger keyboard activity flash every 50 reports -#define MOUSE_ACTIVITY_THROTTLE 100 // Trigger mouse activity flash every 100 reports +// Activity tracking (power-of-2 for bitmask instead of modulo division) +#define KEYBOARD_ACTIVITY_THROTTLE 64 // Trigger keyboard activity flash every 64 reports +#define KEYBOARD_ACTIVITY_MASK (KEYBOARD_ACTIVITY_THROTTLE - 1) +#define MOUSE_ACTIVITY_THROTTLE 128 // Trigger mouse activity flash every 128 reports +#define MOUSE_ACTIVITY_MASK (MOUSE_ACTIVITY_THROTTLE - 1) //--------------------------------------------------------------------+ // MOUSE CONFIGURATION diff --git a/humanization_fpu.c b/humanization_fpu.c index df8f3a9..ae3eb1d 100644 --- a/humanization_fpu.c +++ b/humanization_fpu.c @@ -33,14 +33,45 @@ static inline float jitter_next(void) { g_jitter_lfsr ^= g_jitter_lfsr << 13; g_jitter_lfsr ^= g_jitter_lfsr >> 17; g_jitter_lfsr ^= g_jitter_lfsr << 5; - + // FIX: Balanced [-1.0, 1.0] conversion // Use top 24 bits mapped symmetrically to avoid DC bias - // Old code: (float)(int32_t)g_jitter_lfsr / (float)INT32_MAX had asymmetric range int32_t balanced = (int32_t)(g_jitter_lfsr >> 8) - 0x800000; return (float)balanced * (1.0f / 8388608.0f); // 1/2^23 } +//--------------------------------------------------------------------+ +// Fast sine approximation (~10 FPU cycles vs ~80 for libm sinf) +// +// Degree-5 minimax polynomial on [-pi, pi]: +// sin(x) ≈ x * (1 - x²/6 * (1 - x²/20 * (1 - x²/42))) +// Max error: ~2.5e-5 — more than adequate for tremor noise. +// +// Input is reduced to [-pi, pi] by subtracting multiples of 2*pi. +// Uses M33 VFMA.F32 for fused multiply-add chains. +//--------------------------------------------------------------------+ + +static const float TWO_PI = 6.28318530718f; +static const float INV_2PI = 0.15915494309f; // 1/(2*pi) +static const float PI_F = 3.14159265359f; + +static inline float fast_sinf(float x) { + // Range reduction to [-pi, pi] via round-to-nearest + // floorf(x * INV_2PI + 0.5f) gives nearest integer + float n = x * INV_2PI; + // Round to nearest integer: add 0.5 then truncate + // (faster than floorf on M33 which lacks VRINTM) + n = (float)(int32_t)(n + (n >= 0.0f ? 0.5f : -0.5f)); + x -= n * TWO_PI; + + // Horner form: x * (1 + x²*(-1/6 + x²*(1/120 + x²*(-1/5040)))) + float x2 = x * x; + float r = fmaf(x2, -1.984126984e-4f, 8.333333333e-3f); // -1/5040, 1/120 + r = fmaf(x2, r, -1.666666667e-1f); // -1/6 + r = fmaf(x2, r, 1.0f); // 1 + return x * r; +} + //--------------------------------------------------------------------+ // Public API //--------------------------------------------------------------------+ @@ -56,15 +87,12 @@ void humanization_fpu_init(uint32_t seed) { void humanization_get_tremor(float scale, float *perp_x, float *perp_y) { g_tremor_phase++; - - // FIX: Wrap phase to prevent sinf() precision loss at large arguments. - // After 100 seconds, t*19.1*2*PI = 11,999 — single-precision sinf loses - // fractional accuracy above ~4096. Wrapping at 100000 (~100s) keeps - // arguments well within float32 precision. + + // Wrap phase to prevent precision loss at large arguments. + // Wrapping at 100000 (~100s) keeps arguments within float32 precision. float t = (float)(g_tremor_phase % 100000u) * 0.001f; - + // Pre-computed angular frequency constants (2*PI*freq) - // Eliminates 6 runtime multiplications per call at 240MHz. // M33 FPU executes fmaf() as single VFMA.F32 instruction. static const float W_X1 = 8.7f * (2.0f * (float)M_PI); // ~54.67 rad/s static const float W_X2 = 12.3f * (2.0f * (float)M_PI); // ~77.28 rad/s @@ -72,36 +100,31 @@ void humanization_get_tremor(float scale, float *perp_x, float *perp_y) { static const float W_Y1 = 9.4f * (2.0f * (float)M_PI); // ~59.06 rad/s static const float W_Y2 = 13.7f * (2.0f * (float)M_PI); // ~86.08 rad/s static const float W_Y3 = 17.8f * (2.0f * (float)M_PI); // ~111.84 rad/s - - // FIX: Use fixed offsets for X/Y decorrelation instead of accumulating - // secondary phases. The old approach doubled the effective frequency - // because both t and g_tremor_phase_x advanced by 0.001 per call. - // + // X-axis tremor: Three incommensurate frequencies - // Physiological hand tremor is 8-25Hz, these ratios are irrational - // so the composite waveform has no clean repeat period + // Uses fast_sinf (~10 cycles) instead of libm sinf (~80 cycles) + // 6 calls: ~60 cycles total vs ~480 cycles before float tx = t + 0.7f; // Fixed offset for X channel - float tremor_x = sinf(tx * W_X1) * 0.40f // ~8.7Hz primary - + sinf(tx * W_X2) * 0.25f // ~12.3Hz secondary - + sinf(tx * W_X3) * 0.15f; // ~19.1Hz tertiary - + float tremor_x = fast_sinf(tx * W_X1) * 0.40f // ~8.7Hz primary + + fast_sinf(tx * W_X2) * 0.25f // ~12.3Hz secondary + + fast_sinf(tx * W_X3) * 0.15f; // ~19.1Hz tertiary + // Add LFSR noise component (breaks any remaining periodicity) - // Use fmaf for fused multiply-add -> single VFMA.F32 on M33 tremor_x = fmaf(jitter_next(), 0.3f, tremor_x); - + // Y-axis tremor: Different fixed offset for decorrelation float ty = t + 1.3f; // Different offset than X - float tremor_y = sinf(ty * W_Y1) * 0.40f // ~9.4Hz primary - + sinf(ty * W_Y2) * 0.25f // ~13.7Hz secondary - + sinf(ty * W_Y3) * 0.15f; // ~17.8Hz tertiary - + float tremor_y = fast_sinf(ty * W_Y1) * 0.40f // ~9.4Hz primary + + fast_sinf(ty * W_Y2) * 0.25f // ~13.7Hz secondary + + fast_sinf(ty * W_Y3) * 0.15f; // ~17.8Hz tertiary + // Add independent LFSR noise (fused multiply-add) tremor_y = fmaf(jitter_next(), 0.3f, tremor_y); - + // Apply scale and clamp to reasonable range *perp_x = tremor_x * scale; *perp_y = tremor_y * scale; - + // Clamp to ±3.0px to prevent extreme outliers if (*perp_x > 3.0f) *perp_x = 3.0f; if (*perp_x < -3.0f) *perp_x = -3.0f; diff --git a/kmbox_serial_handler.c b/kmbox_serial_handler.c index ae1518c..b3999e5 100644 --- a/kmbox_serial_handler.c +++ b/kmbox_serial_handler.c @@ -212,32 +212,29 @@ static void __not_in_flash_func(on_uart_irq)(void) { // Non-blocking TX: Use DMA if available, fall back to IRQ ring buffer static bool uart_send_bytes(const uint8_t *data, size_t len) { if (!data || len == 0) return true; - - // DMA TX path: zero-CPU transmission + + // DMA TX path: zero-CPU, zero-wait transmission if (uart_tx_dma_chan >= 0) { - // Wait for previous DMA transfer to complete (should be very fast at 3Mbaud) - // Reduced timeout to 500µs to prevent starving tud_task() under burst traffic - // At 3Mbaud, 256 bytes takes ~0.85ms max; most packets are 8 bytes (~27µs) - uint32_t timeout = time_us_32() + 500; // 500µs safety timeout - while (dma_tx_busy && time_us_32() < timeout) { - tight_loop_contents(); - } + // Non-blocking: if DMA is still sending the previous packet, drop this one. + // At 3Mbaud, an 8-byte packet takes ~27µs — the previous DMA is almost + // always done by the next main loop iteration. The old 500µs spin-wait + // blocked tud_task() and hid_device_task(), adding up to 500µs of input latency. + // Bridge retries on missed responses, so dropping is safe. if (dma_tx_busy) { - // DMA still busy after timeout — drop the data to avoid blocking g_uart_tx_dropped += len; return false; } - + // Copy data to DMA TX staging buffer size_t to_send = (len > DMA_TX_BUFFER_SIZE) ? DMA_TX_BUFFER_SIZE : len; memcpy(dma_tx_buffer, data, to_send); dma_tx_len = to_send; dma_tx_busy = true; - + // Fire DMA transfer dma_channel_set_read_addr(uart_tx_dma_chan, dma_tx_buffer, false); dma_channel_set_trans_count(uart_tx_dma_chan, to_send, true); - + if (to_send < len) { g_uart_tx_dropped += (len - to_send); return false; @@ -395,14 +392,25 @@ static uint8_t __not_in_flash_func(process_bridge_packet)(const uint8_t *data, s extern void process_mouse_report(const hid_mouse_report_t *report); extern void process_kbd_report(const hid_keyboard_report_t *report); +// 256-bit bitmap: O(1) single-load lookup replaces chained range comparisons. +// Bit N is set if byte value N is a valid fast command start byte. +// Excludes 0x0A (\n) and 0x0D (\r) to avoid text protocol conflict. +static const uint32_t g_fast_cmd_bitmap[8] = { + // Bits 0-31: commands 0x01-0x0F (excluding 0x0A, 0x0D) + // 0x01 MOUSE_MOVE, 0x02 MOUSE_CLICK, 0x07 SMOOTH_MOVE, 0x08 SMOOTH_CONFIG, + // 0x09 SMOOTH_CLEAR, 0x0B MULTI_MOVE, 0x0C KEY_COMBO, 0x0E INFO_EXT, 0x0F CYCLE_HUMAN + (1u << 0x01) | (1u << 0x02) | (1u << 0x07) | (1u << 0x08) | + (1u << 0x09) | (1u << 0x0B) | (1u << 0x0C) | (1u << 0x0E) | (1u << 0x0F), + // Bits 32-63: Xbox commands 0x20-0x28 + (1u << (0x20 - 32)) | (1u << (0x22 - 32)) | (1u << (0x23 - 32)) | + (1u << (0x27 - 32)) | (1u << (0x28 - 32)), + 0, 0, 0, 0, 0, // Bits 64-223: none + // Bits 224-255: 0xFC SYNC, 0xFE PING + (1u << (0xFC - 224)) | (1u << (0xFE - 224)), +}; + static __force_inline bool is_fast_cmd_start(uint8_t byte) { - // Exclude 0x0A (\n) and 0x0D (\r) — these overlap with FAST_CMD_TIMED_MOVE - // and FAST_CMD_INFO but the text protocol uses them as line terminators. - // TODO: Reassign FAST_CMD_TIMED_MOVE to 0x10+ to avoid this conflict. - if (__builtin_expect(byte == 0x0A || byte == 0x0D, 0)) return false; - return (byte >= FAST_CMD_MOUSE_MOVE && byte <= FAST_CMD_CYCLE_HUMAN) || - (byte >= FAST_CMD_XBOX_INPUT && byte <= FAST_CMD_XBOX_STATUS) || - byte == FAST_CMD_PING; + return (g_fast_cmd_bitmap[byte >> 5] >> (byte & 31)) & 1; } static bool __not_in_flash_func(process_fast_command)(const uint8_t *pkt) { @@ -463,11 +471,15 @@ static bool __not_in_flash_func(process_fast_command)(const uint8_t *pkt) { } case FAST_CMD_MULTI_MOVE: { + // Direct accumulator path — same as FAST_CMD_MOUSE_MOVE. + // Bridge moves are pre-transformed, so skip transform/velocity tracking + // that process_mouse_report() would apply. Sum all 3 sub-moves into + // a single kmbox_add_mouse_movement() call (1 spinlock instead of 3). const fast_cmd_multi_t *m = (const fast_cmd_multi_t *)pkt; - hid_mouse_report_t report = {0}; - if (m->x1 || m->y1) { report.x = m->x1; report.y = m->y1; process_mouse_report(&report); } - if (m->x2 || m->y2) { report.x = m->x2; report.y = m->y2; process_mouse_report(&report); } - if (m->x3 || m->y3) { report.x = m->x3; report.y = m->y3; process_mouse_report(&report); } + int16_t sum_x = m->x1 + m->x2 + m->x3; + int16_t sum_y = m->y1 + m->y2 + m->y3; + if (sum_x || sum_y) kmbox_add_mouse_movement(sum_x, sum_y); + neopixel_signal_activity(COLOR_BRIDGE_ACTIVE); fast_cmd_count++; return true; } diff --git a/lib/kmbox-commands/kmbox_commands.c b/lib/kmbox-commands/kmbox_commands.c index 77f0bf4..dc9e8fd 100644 --- a/lib/kmbox-commands/kmbox_commands.c +++ b/lib/kmbox-commands/kmbox_commands.c @@ -1363,6 +1363,71 @@ uint8_t kmbox_get_current_buttons(void) (g_kmbox_state.buttons[KMBOX_BUTTON_SIDE2].is_pressed ? 0x10 : 0); } +bool kmbox_try_drain_mouse_16(uint8_t last_sent_buttons, + uint8_t *buttons, int16_t *x, int16_t *y, + int8_t *wheel, int8_t *pan) +{ + // Build button byte (no lock needed — single-core writes) + uint8_t button_byte = + (g_kmbox_state.buttons[KMBOX_BUTTON_LEFT].is_pressed ? 0x01 : 0) | + (g_kmbox_state.buttons[KMBOX_BUTTON_RIGHT].is_pressed ? 0x02 : 0) | + (g_kmbox_state.buttons[KMBOX_BUTTON_MIDDLE].is_pressed ? 0x04 : 0) | + (g_kmbox_state.buttons[KMBOX_BUTTON_SIDE1].is_pressed ? 0x08 : 0) | + (g_kmbox_state.buttons[KMBOX_BUTTON_SIDE2].is_pressed ? 0x10 : 0); + + *buttons = button_byte; + bool buttons_changed = (button_byte != last_sent_buttons); + + // Single spinlock: check pending + drain in one shot + uint32_t irq = spin_lock_blocking(g_acc_spinlock); + + bool pending = (g_kmbox_state.mouse_x_accumulator != 0 || + g_kmbox_state.mouse_y_accumulator != 0 || + g_kmbox_state.wheel_accumulator != 0); + + if (!pending && !buttons_changed) { + // Nothing to do — fast unlock + spin_unlock(g_acc_spinlock, irq); + *x = 0; *y = 0; *wheel = 0; *pan = 0; + return false; + } + + // Drain movement accumulators + *x = (int16_t)g_kmbox_state.mouse_x_accumulator; + g_kmbox_state.mouse_x_accumulator = 0; + *y = (int16_t)g_kmbox_state.mouse_y_accumulator; + g_kmbox_state.mouse_y_accumulator = 0; + + // Drain wheel (clamp to int8 range, keep remainder) + int16_t w_acc = g_kmbox_state.wheel_accumulator; + if (w_acc > 127) { + *wheel = 127; + g_kmbox_state.wheel_accumulator = w_acc - 127; + } else if (w_acc < -128) { + *wheel = -128; + g_kmbox_state.wheel_accumulator = w_acc + 128; + } else { + *wheel = (int8_t)w_acc; + g_kmbox_state.wheel_accumulator = 0; + } + + // Drain pan (clamp to int8 range, keep remainder) + int16_t p_acc = g_kmbox_state.pan_accumulator; + if (p_acc > 127) { + *pan = 127; + g_kmbox_state.pan_accumulator = p_acc - 127; + } else if (p_acc < -128) { + *pan = -128; + g_kmbox_state.pan_accumulator = p_acc + 128; + } else { + *pan = (int8_t)p_acc; + g_kmbox_state.pan_accumulator = 0; + } + + spin_unlock(g_acc_spinlock, irq); + return true; +} + void kmbox_set_axis_lock(bool lock_x, bool lock_y) { g_kmbox_state.lock_mx = lock_x; diff --git a/lib/kmbox-commands/kmbox_commands.h b/lib/kmbox-commands/kmbox_commands.h index 3220b90..e7bf8d0 100644 --- a/lib/kmbox-commands/kmbox_commands.h +++ b/lib/kmbox-commands/kmbox_commands.h @@ -152,6 +152,14 @@ void kmbox_start_button_click(kmbox_button_t button, uint32_t current_time_ms); // Used to detect button-only state changes that need an immediate report. uint8_t kmbox_get_current_buttons(void); +// Atomic check-and-drain: single spinlock acquire to test pending + drain accumulators. +// Returns true if any movement/wheel/pan/button-change was pending and writes drained values. +// Replaces separate kmbox_has_pending_movement() + kmbox_get_mouse_report_16() calls +// to eliminate double spinlock acquisition in the hot path. +bool kmbox_try_drain_mouse_16(uint8_t last_sent_buttons, + uint8_t *buttons, int16_t *x, int16_t *y, + int8_t *wheel, int8_t *pan); + // Get button name string for debugging const char* kmbox_get_button_name(kmbox_button_t button); diff --git a/smooth_injection.c b/smooth_injection.c index dd14158..d556fd1 100644 --- a/smooth_injection.c +++ b/smooth_injection.c @@ -113,9 +113,12 @@ static inline int32_t __not_in_flash_func(fp_mul)(int32_t a, int32_t b) { return (int32_t)((uint32_t)(hi << SMOOTH_FP_SHIFT) | (lo >> SMOOTH_FP_SHIFT)); } +// Fixed-point division via M33 FPU float path (~14 cycles VDIV.F32) +// instead of 64-bit software division (__aeabi_ldivmod, ~60-100 cycles). +// Precision: 24-bit mantissa → sufficient for 16.16 fixed-point work. static __force_inline int32_t fp_div(int32_t a, int32_t b) { if (b == 0) return 0; - return (int32_t)(((int64_t)a << SMOOTH_FP_SHIFT) / b); + return (int32_t)((float)a / (float)b * (float)SMOOTH_FP_ONE); } static __force_inline int32_t int_to_fp(int16_t val) { @@ -194,26 +197,32 @@ static inline int32_t __not_in_flash_func(compute_adaptive_alpha)( static int32_t g_velocity_sum_x_fp = 0; static int32_t g_velocity_sum_y_fp = 0; +// SMOOTH_VELOCITY_WINDOW must be power of 2 for bitmask; use shift for division +_Static_assert((SMOOTH_VELOCITY_WINDOW & (SMOOTH_VELOCITY_WINDOW - 1)) == 0, + "SMOOTH_VELOCITY_WINDOW must be power of 2"); +#define VELOCITY_WINDOW_MASK (SMOOTH_VELOCITY_WINDOW - 1) +#define VELOCITY_WINDOW_SHIFT 3 // log2(8) = 3 + static void velocity_update(int16_t x, int16_t y) { velocity_tracker_t *v = &g_smooth.velocity; - + const uint8_t idx = v->history_index; + // Get old value that will be replaced - int16_t old_x = v->x_history[v->history_index]; - int16_t old_y = v->y_history[v->history_index]; - + int16_t old_x = v->x_history[idx]; + int16_t old_y = v->y_history[idx]; + // Store new values in history - v->x_history[v->history_index] = x; - v->y_history[v->history_index] = y; - v->history_index = (v->history_index + 1) % SMOOTH_VELOCITY_WINDOW; - + v->x_history[idx] = x; + v->y_history[idx] = y; + v->history_index = (idx + 1) & VELOCITY_WINDOW_MASK; // Bitmask instead of modulo + // Update running sum in fixed-point (remove old, add new) - O(1) - // Keep accumulators in fixed-point to preserve precision across cycles - g_velocity_sum_x_fp = g_velocity_sum_x_fp - int_to_fp(old_x) + int_to_fp(x); - g_velocity_sum_y_fp = g_velocity_sum_y_fp - int_to_fp(old_y) + int_to_fp(y); - - // Store as fixed-point average (no precision loss) - v->avg_velocity_x_fp = g_velocity_sum_x_fp / SMOOTH_VELOCITY_WINDOW; - v->avg_velocity_y_fp = g_velocity_sum_y_fp / SMOOTH_VELOCITY_WINDOW; + g_velocity_sum_x_fp += int_to_fp(x) - int_to_fp(old_x); + g_velocity_sum_y_fp += int_to_fp(y) - int_to_fp(old_y); + + // Arithmetic right shift by 3 = divide by 8 (power of 2, no division instruction) + v->avg_velocity_x_fp = g_velocity_sum_x_fp >> VELOCITY_WINDOW_SHIFT; + v->avg_velocity_y_fp = g_velocity_sum_y_fp >> VELOCITY_WINDOW_SHIFT; } // smooth state accessor for external use diff --git a/usb_hid.c b/usb_hid.c index 5e5bda4..a271388 100644 --- a/usb_hid.c +++ b/usb_hid.c @@ -212,6 +212,7 @@ static uint8_t mouse_device_instance = 0; // When the host mouse sends vendor reports (e.g. Logitech HID++, Razer), // Core1 queues them here and Core0 drains them via tud_hid_report(). #define VENDOR_QUEUE_SIZE 8 +#define VENDOR_QUEUE_MASK (VENDOR_QUEUE_SIZE - 1) #define VENDOR_REPORT_MAX_LEN 64 typedef struct { @@ -541,6 +542,16 @@ static uint8_t host_mouse_report_id = 0; // incoming reports with kmbox/smooth deltas injected in-place. // // Layout populated by parse_mouse_report_layout() during tuh_hid_mount_cb. + +// Fast-path classification for forward_raw_mouse_report(): +// Most gaming mice use byte-aligned 8-bit or 16-bit XY. Classifying the +// layout at parse time lets the hot path skip complex bitwise extraction. +typedef enum { + LAYOUT_GENERIC, // Arbitrary bit-width / non-aligned — full extraction needed + LAYOUT_FAST_8BIT, // buttons[1] + X[i8] + Y[i8] + optional wheel/pan — all byte-aligned + LAYOUT_FAST_16BIT, // buttons[1-2] + X[i16 LE] + Y[i16 LE] — all byte-aligned +} layout_class_t; + typedef struct { // Total expected report size (excluding report-ID prefix byte) uint8_t report_size; @@ -578,6 +589,7 @@ typedef struct { bool has_report_id; bool valid; // true once successfully parsed + layout_class_t layout_class; // fast-path classification (set by classify_layout) } mouse_report_layout_t; static mouse_report_layout_t host_mouse_layout = { .valid = false }; @@ -1582,6 +1594,26 @@ static inline void build_raw_mouse_report(uint8_t *buf, uint8_t sz, buf[L->pan_offset] = (uint8_t)pan; } +// Classify a parsed layout for fast-path dispatch in forward_raw_mouse_report(). +// Called once after parse (+ any fixups), not on the hot path. +static void classify_mouse_layout(mouse_report_layout_t *L) { + if (!L->valid) { L->layout_class = LAYOUT_GENERIC; return; } + + // Check byte-alignment: all axes must start on byte boundary + bool x_aligned = (L->x_bit_in_byte == 0); + bool y_aligned = (L->y_bit_in_byte == 0); + + if (x_aligned && y_aligned && L->x_bits == 16 && L->y_bits == 16 && + L->x_is_16bit && L->y_is_16bit && L->buttons_bits <= 8) { + L->layout_class = LAYOUT_FAST_16BIT; + } else if (x_aligned && y_aligned && L->x_bits == 8 && L->y_bits == 8 && + !L->x_is_16bit && !L->y_is_16bit && L->buttons_bits <= 8) { + L->layout_class = LAYOUT_FAST_8BIT; + } else { + L->layout_class = LAYOUT_GENERIC; + } +} + /** * Core1 accumulate-only mouse report handler. * @@ -1596,11 +1628,36 @@ static void __not_in_flash_func(forward_raw_mouse_report)(const uint8_t *raw, ui { const mouse_report_layout_t *L = &host_mouse_layout; - // --- Extract physical movement from the raw report --- int16_t phys_x = 0, phys_y = 0; int8_t phys_wheel = 0, phys_pan = 0; uint8_t phys_buttons = 0; + // --- Fast paths for common byte-aligned layouts (95%+ of gaming mice) --- + // Avoids all the bitwise extraction, bounds checks, and branches below. + if (__builtin_expect(L->layout_class == LAYOUT_FAST_16BIT, 1)) { + // 16-bit XY, byte-aligned, <=8-bit buttons + if (__builtin_expect(raw_len >= L->y_offset + 2, 1)) { + phys_buttons = raw[L->buttons_offset] & ((L->buttons_bits >= 8) ? 0xFF : ((1u << L->buttons_bits) - 1)); + phys_x = (int16_t)(raw[L->x_offset] | (raw[L->x_offset + 1] << 8)); + phys_y = (int16_t)(raw[L->y_offset] | (raw[L->y_offset + 1] << 8)); + if (L->has_wheel && L->wheel_offset < raw_len) phys_wheel = (int8_t)raw[L->wheel_offset]; + if (L->has_pan && L->pan_offset < raw_len) phys_pan = (int8_t)raw[L->pan_offset]; + goto accumulate; + } + } else if (L->layout_class == LAYOUT_FAST_8BIT) { + // 8-bit XY, byte-aligned, <=8-bit buttons + if (__builtin_expect(raw_len >= L->y_offset + 1, 1)) { + phys_buttons = raw[L->buttons_offset] & ((L->buttons_bits >= 8) ? 0xFF : ((1u << L->buttons_bits) - 1)); + phys_x = (int8_t)raw[L->x_offset]; + phys_y = (int8_t)raw[L->y_offset]; + if (L->has_wheel && L->wheel_offset < raw_len) phys_wheel = (int8_t)raw[L->wheel_offset]; + if (L->has_pan && L->pan_offset < raw_len) phys_pan = (int8_t)raw[L->pan_offset]; + goto accumulate; + } + } + + // --- Generic path: arbitrary bit-width and non-aligned fields --- + // Extract buttons (handle both 8-bit and 16-bit button fields) if (L->buttons_offset < raw_len) { if (L->buttons_bits > 8 && L->buttons_offset + 1 < raw_len) { @@ -1668,6 +1725,7 @@ static void __not_in_flash_func(forward_raw_mouse_report)(const uint8_t *raw, ui phys_pan = (int8_t)raw[L->pan_offset]; } +accumulate: // --- Accumulate into shared state (spinlock-protected inside each call) --- kmbox_update_physical_buttons(phys_buttons & 0x1F); @@ -1735,7 +1793,7 @@ void __not_in_flash_func(process_kbd_report)(const hid_keyboard_report_t *report } static uint32_t activity_counter = 0; - if (++activity_counter % KEYBOARD_ACTIVITY_THROTTLE == 0) + if ((++activity_counter & KEYBOARD_ACTIVITY_MASK) == 0) { neopixel_trigger_activity_flash_color(COLOR_KEYBOARD_ACTIVITY); } @@ -1755,7 +1813,7 @@ void __not_in_flash_func(process_mouse_report)(const hid_mouse_report_t *report) } static uint32_t activity_counter = 0; - if (++activity_counter % MOUSE_ACTIVITY_THROTTLE == 0) + if ((++activity_counter & MOUSE_ACTIVITY_MASK) == 0) { neopixel_trigger_activity_flash_color(0x000000FF); // Blue for mouse activity } @@ -1848,22 +1906,23 @@ void hid_device_task(void) // and sets was_active=true. We drain everything here. if (tud_hid_n_ready(mouse_device_instance)) { - bool has_kmbox = kmbox_has_pending_movement(); + // Cache humanization mode for this frame — avoid 3 function calls per frame + const humanization_mode_t frame_human_mode = smooth_get_humanization_mode(); + + // Atomic check-and-drain: single spinlock instead of separate + // kmbox_has_pending_movement() + kmbox_get_mouse_report_16() (was 2 spinlock roundtrips) + uint8_t buttons; + int16_t x, y; + int8_t wheel, pan; + bool has_kmbox = kmbox_try_drain_mouse_16(last_sent_buttons, + &buttons, &x, &y, &wheel, &pan); bool has_smooth = smooth_has_pending(); - // Cheaply read current button byte without draining accumulators - uint8_t current_buttons = kmbox_get_current_buttons(); - bool buttons_changed = (current_buttons != last_sent_buttons); + bool buttons_changed = (buttons != last_sent_buttons); bool has_pending = has_kmbox || has_smooth || buttons_changed; if (!has_pending) goto check_idle; - // Drain accumulators (spinlock-protected, safe from both cores) - uint8_t buttons; - int16_t x, y; - int8_t wheel, pan; - kmbox_get_mouse_report_16(&buttons, &x, &y, &wheel, &pan); - // Process smooth injection (int16_t for high-DPI support) int16_t smooth_x = 0, smooth_y = 0; if (has_smooth) { @@ -1888,7 +1947,7 @@ 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 && + if (frame_human_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 @@ -1946,7 +2005,7 @@ void hid_device_task(void) // 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) { + if (frame_human_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]. @@ -2006,7 +2065,7 @@ void hid_device_task(void) tud_hid_n_report(e->device_instance, e->report_id, e->data, e->len); } __dmb(); - vendor_fwd_queue.tail = (vendor_fwd_queue.tail + 1) % VENDOR_QUEUE_SIZE; + vendor_fwd_queue.tail = (vendor_fwd_queue.tail + 1) & VENDOR_QUEUE_MASK; } } @@ -2095,13 +2154,15 @@ void hid_host_task(void) { // Drain SET_REPORT passthrough queue (Core0 → Core1). // Forward vendor SET_REPORT requests from the downstream PC to the real mouse. - while (set_report_queue.tail != set_report_queue.head) { + // Process at most 1 per call — tuh_hid_set_report() may block on USB + // control transfer, and draining the full queue could stall Core1's PIO USB. + if (set_report_queue.tail != set_report_queue.head) { set_report_entry_t *e = &set_report_queue.entries[set_report_queue.tail]; tuh_hid_set_report(e->host_dev_addr, e->host_instance, e->report_id, e->report_type, (void*)e->data, e->len); __dmb(); - set_report_queue.tail = (set_report_queue.tail + 1) % VENDOR_QUEUE_SIZE; + set_report_queue.tail = (set_report_queue.tail + 1) & VENDOR_QUEUE_MASK; } } @@ -2284,6 +2345,9 @@ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, const uint8_t *desc_re host_mouse_layout.report_size = 8; } + // Classify layout for fast-path dispatch (after all fixups) + classify_mouse_layout(&host_mouse_layout); + // Extract mouse report ID if (host_mouse_layout.valid && host_mouse_layout.has_report_id) { host_mouse_has_report_id = true; @@ -2496,7 +2560,7 @@ static void __not_in_flash_func(queue_vendor_report)(uint8_t device_instance, ui } // Queue for Core0 to forward via interrupt IN endpoint - uint8_t next_head = (vendor_fwd_queue.head + 1) % VENDOR_QUEUE_SIZE; + uint8_t next_head = (vendor_fwd_queue.head + 1) & VENDOR_QUEUE_MASK; if (next_head == vendor_fwd_queue.tail) return; // Queue full, drop vendor_report_entry_t *e = &vendor_fwd_queue.entries[vendor_fwd_queue.head]; @@ -2674,7 +2738,7 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_ if (buffer != NULL && bufsize > 0 && instance < mirrored_itf_count && mirrored_itfs[instance].active) { mirrored_interface_t *mitf = &mirrored_itfs[instance]; - uint8_t next_head = (set_report_queue.head + 1) % VENDOR_QUEUE_SIZE; + uint8_t next_head = (set_report_queue.head + 1) & VENDOR_QUEUE_MASK; if (next_head != set_report_queue.tail) { set_report_entry_t *e = &set_report_queue.entries[set_report_queue.head]; e->host_dev_addr = mitf->host_dev_addr; From 248d79908cde0a6eb595b1f1537bd195d14ef562 Mon Sep 17 00:00:00 2001 From: Ramsey McGrath Date: Thu, 26 Feb 2026 02:25:57 -0500 Subject: [PATCH 6/6] code tidying --- CMakeLists.txt | 4 +- bridge/CMakeLists.txt | 4 + bridge/fast_commands.h | 102 ++--------- bridge/makcu_translator.c | 10 +- defines.h | 98 +---------- lib/fast-protocol/CMakeLists.txt | 3 + lib/fast-protocol/include/fast_protocol.h | 196 ++++++++++++++++++++++ lib/hid-defs/CMakeLists.txt | 2 + lib/hid-defs/include/hid_defs.h | 20 +++ lib/kmbox-commands/CMakeLists.txt | 1 + lib/kmbox-commands/kmbox_commands.h | 14 +- lib/wire-protocol/CMakeLists.txt | 1 + lib/wire-protocol/include/wire_protocol.h | 17 +- smooth_injection.h | 17 +- 14 files changed, 280 insertions(+), 209 deletions(-) create mode 100644 lib/fast-protocol/CMakeLists.txt create mode 100644 lib/fast-protocol/include/fast_protocol.h create mode 100644 lib/hid-defs/CMakeLists.txt create mode 100644 lib/hid-defs/include/hid_defs.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dd3dcbb..4c3de97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,8 @@ add_subdirectory(lib/Pico-PIO-USB) add_subdirectory(lib/kmbox-commands) # Shared libraries +add_subdirectory(lib/hid-defs) +add_subdirectory(lib/fast-protocol) add_subdirectory(lib/wire-protocol) add_subdirectory(lib/peri-clock) add_subdirectory(lib/dma-uart) @@ -161,7 +163,7 @@ target_link_libraries(PIOKMbox pico_pio_usb) target_link_libraries(PIOKMbox kmbox_commands) # Link shared libraries -target_link_libraries(PIOKMbox wire_protocol peri_clock dma_uart led_utils) +target_link_libraries(PIOKMbox hid_defs fast_protocol wire_protocol peri_clock dma_uart led_utils) # Add PIO USB HCD implementation directly target_sources(PIOKMbox PRIVATE diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index f956cbe..99c2f47 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -130,6 +130,8 @@ if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/lut.pio) endif() # Add shared libraries +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../lib/hid-defs hid-defs) +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../lib/fast-protocol fast-protocol) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../lib/kmbox-commands kmbox-commands) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../lib/wire-protocol wire-protocol) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../lib/peri-clock peri-clock) @@ -149,6 +151,8 @@ set(BRIDGE_LIBS hardware_uart hardware_adc kmbox_commands + hid_defs + fast_protocol wire_protocol dma_uart led_utils diff --git a/bridge/fast_commands.h b/bridge/fast_commands.h index fb9c309..7410e6e 100644 --- a/bridge/fast_commands.h +++ b/bridge/fast_commands.h @@ -1,101 +1,21 @@ /** - * Fast Binary Command Definitions for Bridge → KMBox UART + * Fast Binary Command Definitions for Bridge -> KMBox UART * - * All bridge translators (Ferrum, Makcu, tracker) should produce these - * 8-byte packets instead of text strings. The KMBox already has an - * optimized binary parser that handles them with zero string parsing. - * - * Key command IDs: - * 0x01 FAST_CMD_MOUSE_MOVE — direct accumulator (buttons + wheel + move) - * 0x02 FAST_CMD_MOUSE_CLICK — button click with repeat count - * 0x07 FAST_CMD_SMOOTH_MOVE — smooth injection queue (humanized) + * Thin wrapper — all definitions now live in the shared library + * lib/fast-protocol/include/fast_protocol.h. This file exists so + * existing bridge #include "fast_commands.h" directives continue to work. */ #ifndef BRIDGE_FAST_COMMANDS_H #define BRIDGE_FAST_COMMANDS_H -#include -#include - -// Command IDs (must match defines.h on KMBox side) -#define FAST_CMD_MOUSE_MOVE 0x01 -#define FAST_CMD_MOUSE_CLICK 0x02 -#define FAST_CMD_SMOOTH_MOVE 0x07 -#define FAST_CMD_SMOOTH_CONFIG 0x08 -#define FAST_CMD_SMOOTH_CLEAR 0x09 -#define FAST_CMD_CYCLE_HUMAN 0x0F -#define FAST_CMD_PING 0xFE - -// Smooth injection modes (must match inject_mode_t on KMBox side) -#define INJECT_MODE_IMMEDIATE 0 -#define INJECT_MODE_SMOOTH 1 -#define INJECT_MODE_VELOCITY_MATCHED 2 -#define INJECT_MODE_MICRO 3 - -#define FAST_CMD_PACKET_SIZE 8 - -// ============================================================================ -// Inline packet builders — produce 8-byte packets, return 8 always -// ============================================================================ - -/** - * Build FAST_CMD_SMOOTH_MOVE (0x07) packet. - * KMBox routes this through smooth_inject_movement() which applies - * humanization (easing, subdivision, tremor, overshoot) automatically. - * Use for: km.move(), tracker aim commands, makcu MOVE. - */ -static inline size_t fast_build_smooth_move(uint8_t *buf, int16_t x, int16_t y, uint8_t mode) { - buf[0] = FAST_CMD_SMOOTH_MOVE; - buf[1] = (uint8_t)(x & 0xFF); - buf[2] = (uint8_t)((x >> 8) & 0xFF); - buf[3] = (uint8_t)(y & 0xFF); - buf[4] = (uint8_t)((y >> 8) & 0xFF); - buf[5] = mode; - buf[6] = 0; - buf[7] = 0; - return FAST_CMD_PACKET_SIZE; -} - -/** - * Build FAST_CMD_MOUSE_MOVE (0x01) packet. - * KMBox routes this through kmbox_add_mouse_movement() (direct accumulator) - * plus optional buttons + wheel. No smooth queue / humanization subdivision. - * Use for: button state changes, wheel, raw passthrough moves. - */ -static inline size_t fast_build_mouse_move(uint8_t *buf, int16_t x, int16_t y, - uint8_t buttons, int8_t wheel) { - buf[0] = FAST_CMD_MOUSE_MOVE; - buf[1] = (uint8_t)(x & 0xFF); - buf[2] = (uint8_t)((x >> 8) & 0xFF); - buf[3] = (uint8_t)(y & 0xFF); - buf[4] = (uint8_t)((y >> 8) & 0xFF); - buf[5] = buttons; - buf[6] = (uint8_t)wheel; - buf[7] = 0; - return FAST_CMD_PACKET_SIZE; -} - -/** - * Build FAST_CMD_MOUSE_CLICK (0x02) packet. - * KMBox generates press + release pairs internally. - */ -static inline size_t fast_build_mouse_click(uint8_t *buf, uint8_t button, uint8_t count) { - buf[0] = FAST_CMD_MOUSE_CLICK; - buf[1] = button; - buf[2] = count; - buf[3] = 0; - buf[4] = 0; - buf[5] = 0; - buf[6] = 0; - buf[7] = 0; - return FAST_CMD_PACKET_SIZE; -} +#include "fast_protocol.h" -// Button masks (match HID standard) -#define FAST_BTN_LEFT 0x01 -#define FAST_BTN_RIGHT 0x02 -#define FAST_BTN_MIDDLE 0x04 -#define FAST_BTN_BACK 0x08 -#define FAST_BTN_FORWARD 0x10 +// Legacy button mask aliases — prefer HID_BTN_* from hid_defs.h in new code +#define FAST_BTN_LEFT HID_BTN_LEFT +#define FAST_BTN_RIGHT HID_BTN_RIGHT +#define FAST_BTN_MIDDLE HID_BTN_MIDDLE +#define FAST_BTN_BACK HID_BTN_BACK +#define FAST_BTN_FORWARD HID_BTN_FORWARD #endif // BRIDGE_FAST_COMMANDS_H diff --git a/bridge/makcu_translator.c b/bridge/makcu_translator.c index ead44dc..bc7643b 100644 --- a/bridge/makcu_translator.c +++ b/bridge/makcu_translator.c @@ -66,11 +66,11 @@ uint16_t makcu_build_response( // Helper: Convert Makcu button number to HID button mask static uint8_t makcu_button_to_mask(uint8_t button) { switch (button) { - case 1: return 0x01; // Left - case 2: return 0x02; // Right - case 3: return 0x04; // Middle - case 4: return 0x08; // Side1 - case 5: return 0x10; // Side2 + case 1: return HID_BTN_LEFT; + case 2: return HID_BTN_RIGHT; + case 3: return HID_BTN_MIDDLE; + case 4: return HID_BTN_BACK; + case 5: return HID_BTN_FORWARD; default: return 0x00; } } diff --git a/defines.h b/defines.h index 14f4f7f..9897679 100644 --- a/defines.h +++ b/defines.h @@ -103,102 +103,8 @@ #define BRIDGE_CMD_PING 0xFE // Keepalive #define BRIDGE_CMD_RESET 0xFF // Reset state -// Command IDs (must match bridge/fast_commands.h) -#define FAST_CMD_MOUSE_MOVE 0x01 // Direct accumulator (buttons + wheel + move) -#define FAST_CMD_MOUSE_CLICK 0x02 // Button click with repeat count -#define FAST_CMD_SMOOTH_MOVE 0x07 // Smooth injection queue (humanized) -#define FAST_CMD_SMOOTH_CONFIG 0x08 // Configure smooth injection -#define FAST_CMD_SMOOTH_CLEAR 0x09 // Clear smooth injection queue -#define FAST_CMD_TIMED_MOVE 0x0A // Movement with timestamp for sync -#define FAST_CMD_MULTI_MOVE 0x0B // Multiple movements in one packet -#define FAST_CMD_KEY_COMBO 0x0C // Keyboard key combination -#define FAST_CMD_KEY_PRESS 0x0C // Single key press (alias for KEY_COMBO) -#define FAST_CMD_INFO 0x0D // Request system info -#define FAST_CMD_INFO_EXT 0x0E // Request extended stats -#define FAST_CMD_CYCLE_HUMAN 0x0F // Cycle humanization mode -#define FAST_CMD_XBOX_INPUT 0x20 // Xbox gamepad: buttons + triggers -#define FAST_CMD_XBOX_STICK_L 0x22 // Xbox left stick X/Y -#define FAST_CMD_XBOX_STICK_R 0x23 // Xbox right stick X/Y -#define FAST_CMD_XBOX_RELEASE 0x27 // Xbox clear all injection overrides -#define FAST_CMD_XBOX_STATUS 0x28 // Xbox console mode status report -#define FAST_CMD_SYNC 0xFC // Clock synchronization -#define FAST_CMD_RESPONSE 0xFD // Generic response -#define FAST_CMD_PING 0xFE // Keepalive ping -#define FAST_CMD_PACKET_SIZE 8 // Fixed 8-byte packet size - -// Timed move command: 0x0A (for clock-synchronized injection) -typedef struct __attribute__((packed, aligned(4))) { - uint8_t cmd; // 0x0A - int16_t x; // X movement - int16_t y; // Y movement - uint16_t time_us; // Execution time offset (microseconds from sync) - uint8_t mode; // Injection mode -} fast_cmd_timed_t; - -_Static_assert(sizeof(fast_cmd_timed_t) == 8, "fast_cmd_timed_t must be 8 bytes"); - -// Mouse move command: 0x01 -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x01 - int16_t x, y; // Movement - uint8_t buttons; // Button state - int8_t wheel; // Wheel movement - uint8_t pad[2]; // Padding to 8 bytes -} fast_cmd_move_t; - -typedef fast_cmd_move_t fast_cmd_mouse_move_t; // Alias for compatibility - -// Multi-move command: 0x0B -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x0B - int8_t x1, y1; // First movement - int8_t x2, y2; // Second movement - int8_t x3, y3; // Third movement - uint8_t pad; // Padding to 8 bytes -} fast_cmd_multi_t; - -// Click command: 0x02 -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x02 - uint8_t button; // Button mask - uint8_t count; // Click count - uint8_t pad[5]; // Padding to 8 bytes -} fast_cmd_click_t; - -// Key press/combo command: 0x0C -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x0C - uint8_t modifiers; // Modifier keys - uint8_t keycode; // Primary keycode (for single key) - uint8_t keys[5]; // Additional keycodes (for combo) -} fast_cmd_key_t; - -typedef fast_cmd_key_t fast_cmd_combo_t; // Alias for compatibility - -// Smooth move command: 0x07 -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x07 - int16_t x; // X movement - int16_t y; // Y movement - uint8_t mode; // Injection mode - uint8_t pad[2]; // Padding to 8 bytes -} fast_cmd_smooth_t; - -// Config command: 0x08 -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0x08 - uint8_t max_per_frame; // Max pixels per frame - uint8_t vel_match; // Velocity matching enable - uint8_t pad[5]; // Padding to 8 bytes -} fast_cmd_config_t; - -// Sync command: 0xFC -typedef struct __attribute__((packed)) { - uint8_t cmd; // 0xFC - uint32_t timestamp; // PC timestamp - uint16_t seq_num; // Sequence number - uint8_t pad; // Padding to 8 bytes -} fast_cmd_sync_t; +// Fast command IDs, packed structs, and packet builders — shared with bridge +#include "fast_protocol.h" #define DEBUG_OUTPUT_USB_CDC 0 // Always disable debug output over USB CDC diff --git a/lib/fast-protocol/CMakeLists.txt b/lib/fast-protocol/CMakeLists.txt new file mode 100644 index 0000000..1e8b833 --- /dev/null +++ b/lib/fast-protocol/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(fast_protocol INTERFACE) +target_include_directories(fast_protocol INTERFACE include) +target_link_libraries(fast_protocol INTERFACE hid_defs) diff --git a/lib/fast-protocol/include/fast_protocol.h b/lib/fast-protocol/include/fast_protocol.h new file mode 100644 index 0000000..73c468f --- /dev/null +++ b/lib/fast-protocol/include/fast_protocol.h @@ -0,0 +1,196 @@ +/** + * Fast Binary Command Protocol + * + * Shared header for the 8-byte fixed-size binary command protocol used + * between Bridge and KMBox over UART. Included by both firmware targets. + * + * Contains: + * - Command IDs (FAST_CMD_*) + * - Injection mode constants (INJECT_MODE_*) + * - Packed struct typedefs for type-punned parsing + * - Inline packet builders + * - HID button masks (via hid_defs.h) + */ + +#ifndef FAST_PROTOCOL_H +#define FAST_PROTOCOL_H + +#include +#include +#include +#include "hid_defs.h" + +// ============================================================================ +// Command IDs (8-byte fixed-size packets, Bridge <-> KMBox UART) +// ============================================================================ + +#define FAST_CMD_MOUSE_MOVE 0x01 // Direct accumulator (buttons + wheel + move) +#define FAST_CMD_MOUSE_CLICK 0x02 // Button click with repeat count +#define FAST_CMD_SMOOTH_MOVE 0x07 // Smooth injection queue (humanized) +#define FAST_CMD_SMOOTH_CONFIG 0x08 // Configure smooth injection +#define FAST_CMD_SMOOTH_CLEAR 0x09 // Clear smooth injection queue +#define FAST_CMD_TIMED_MOVE 0x0A // Movement with timestamp for sync +#define FAST_CMD_MULTI_MOVE 0x0B // Multiple movements in one packet +#define FAST_CMD_KEY_COMBO 0x0C // Keyboard key combination +#define FAST_CMD_KEY_PRESS 0x0C // Single key press (alias for KEY_COMBO) +#define FAST_CMD_INFO 0x0D // Request system info +#define FAST_CMD_INFO_EXT 0x0E // Request extended stats +#define FAST_CMD_CYCLE_HUMAN 0x0F // Cycle humanization mode +#define FAST_CMD_XBOX_INPUT 0x20 // Xbox gamepad: buttons + triggers +#define FAST_CMD_XBOX_STICK_L 0x22 // Xbox left stick X/Y +#define FAST_CMD_XBOX_STICK_R 0x23 // Xbox right stick X/Y +#define FAST_CMD_XBOX_RELEASE 0x27 // Xbox clear all injection overrides +#define FAST_CMD_XBOX_STATUS 0x28 // Xbox console mode status report +#define FAST_CMD_SYNC 0xFC // Clock synchronization +#define FAST_CMD_RESPONSE 0xFD // Generic response +#define FAST_CMD_PING 0xFE // Keepalive ping +#define FAST_CMD_PACKET_SIZE 8 // Fixed 8-byte packet size + +// ============================================================================ +// Injection Modes +// ============================================================================ +// On the KMBox side, smooth_injection.h provides inject_mode_t as an enum +// with the same values. It sets _INJECT_MODES_DEFINED to suppress these +// macros so the enum and macros don't collide in the same translation unit. + +#ifndef _INJECT_MODES_DEFINED +#define INJECT_MODE_IMMEDIATE 0 +#define INJECT_MODE_SMOOTH 1 +#define INJECT_MODE_VELOCITY_MATCHED 2 +#define INJECT_MODE_MICRO 3 +#endif + +// ============================================================================ +// Packed Struct Typedefs (for type-punned parsing on KMBox side) +// ============================================================================ + +// Timed move command: 0x0A (for clock-synchronized injection) +typedef struct __attribute__((packed, aligned(4))) { + uint8_t cmd; // 0x0A + int16_t x; // X movement + int16_t y; // Y movement + uint16_t time_us; // Execution time offset (microseconds from sync) + uint8_t mode; // Injection mode +} fast_cmd_timed_t; + +_Static_assert(sizeof(fast_cmd_timed_t) == 8, "fast_cmd_timed_t must be 8 bytes"); + +// Mouse move command: 0x01 +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x01 + int16_t x, y; // Movement + uint8_t buttons; // Button state + int8_t wheel; // Wheel movement + uint8_t pad[2]; // Padding to 8 bytes +} fast_cmd_move_t; + +typedef fast_cmd_move_t fast_cmd_mouse_move_t; // Alias for compatibility + +// Multi-move command: 0x0B +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x0B + int8_t x1, y1; // First movement + int8_t x2, y2; // Second movement + int8_t x3, y3; // Third movement + uint8_t pad; // Padding to 8 bytes +} fast_cmd_multi_t; + +// Click command: 0x02 +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x02 + uint8_t button; // Button mask + uint8_t count; // Click count + uint8_t pad[5]; // Padding to 8 bytes +} fast_cmd_click_t; + +// Key press/combo command: 0x0C +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x0C + uint8_t modifiers; // Modifier keys + uint8_t keycode; // Primary keycode (for single key) + uint8_t keys[5]; // Additional keycodes (for combo) +} fast_cmd_key_t; + +typedef fast_cmd_key_t fast_cmd_combo_t; // Alias for compatibility + +// Smooth move command: 0x07 +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x07 + int16_t x; // X movement + int16_t y; // Y movement + uint8_t mode; // Injection mode + uint8_t pad[2]; // Padding to 8 bytes +} fast_cmd_smooth_t; + +// Config command: 0x08 +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0x08 + uint8_t max_per_frame; // Max pixels per frame + uint8_t vel_match; // Velocity matching enable + uint8_t pad[5]; // Padding to 8 bytes +} fast_cmd_config_t; + +// Sync command: 0xFC +typedef struct __attribute__((packed)) { + uint8_t cmd; // 0xFC + uint32_t timestamp; // PC timestamp + uint16_t seq_num; // Sequence number + uint8_t pad; // Padding to 8 bytes +} fast_cmd_sync_t; + +// ============================================================================ +// Inline Packet Builders +// ============================================================================ + +/** + * Build FAST_CMD_SMOOTH_MOVE (0x07) packet. + * KMBox routes this through smooth_inject_movement() which applies + * humanization (easing, subdivision, tremor, overshoot) automatically. + */ +static inline size_t fast_build_smooth_move(uint8_t *buf, int16_t x, int16_t y, uint8_t mode) { + buf[0] = FAST_CMD_SMOOTH_MOVE; + buf[1] = (uint8_t)(x & 0xFF); + buf[2] = (uint8_t)((x >> 8) & 0xFF); + buf[3] = (uint8_t)(y & 0xFF); + buf[4] = (uint8_t)((y >> 8) & 0xFF); + buf[5] = mode; + buf[6] = 0; + buf[7] = 0; + return FAST_CMD_PACKET_SIZE; +} + +/** + * Build FAST_CMD_MOUSE_MOVE (0x01) packet. + * KMBox routes this through kmbox_add_mouse_movement() (direct accumulator). + * No smooth queue / humanization subdivision. + */ +static inline size_t fast_build_mouse_move(uint8_t *buf, int16_t x, int16_t y, + uint8_t buttons, int8_t wheel) { + buf[0] = FAST_CMD_MOUSE_MOVE; + buf[1] = (uint8_t)(x & 0xFF); + buf[2] = (uint8_t)((x >> 8) & 0xFF); + buf[3] = (uint8_t)(y & 0xFF); + buf[4] = (uint8_t)((y >> 8) & 0xFF); + buf[5] = buttons; + buf[6] = (uint8_t)wheel; + buf[7] = 0; + return FAST_CMD_PACKET_SIZE; +} + +/** + * Build FAST_CMD_MOUSE_CLICK (0x02) packet. + * KMBox generates press + release pairs internally. + */ +static inline size_t fast_build_mouse_click(uint8_t *buf, uint8_t button, uint8_t count) { + buf[0] = FAST_CMD_MOUSE_CLICK; + buf[1] = button; + buf[2] = count; + buf[3] = 0; + buf[4] = 0; + buf[5] = 0; + buf[6] = 0; + buf[7] = 0; + return FAST_CMD_PACKET_SIZE; +} + +#endif // FAST_PROTOCOL_H diff --git a/lib/hid-defs/CMakeLists.txt b/lib/hid-defs/CMakeLists.txt new file mode 100644 index 0000000..a700156 --- /dev/null +++ b/lib/hid-defs/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(hid_defs INTERFACE) +target_include_directories(hid_defs INTERFACE include) diff --git a/lib/hid-defs/include/hid_defs.h b/lib/hid-defs/include/hid_defs.h new file mode 100644 index 0000000..8e4d810 --- /dev/null +++ b/lib/hid-defs/include/hid_defs.h @@ -0,0 +1,20 @@ +/** + * Shared HID Definitions + * + * Canonical HID mouse button bit masks used by both KMBox firmware, + * bridge firmware, wire protocol, and fast command protocol. + * + * Values follow the USB HID Usage Table, Button Page (0x09). + */ + +#ifndef HID_DEFS_H +#define HID_DEFS_H + +// HID mouse button bit masks +#define HID_BTN_LEFT 0x01 +#define HID_BTN_RIGHT 0x02 +#define HID_BTN_MIDDLE 0x04 +#define HID_BTN_BACK 0x08 +#define HID_BTN_FORWARD 0x10 + +#endif // HID_DEFS_H diff --git a/lib/kmbox-commands/CMakeLists.txt b/lib/kmbox-commands/CMakeLists.txt index 20e06e7..ede7109 100644 --- a/lib/kmbox-commands/CMakeLists.txt +++ b/lib/kmbox-commands/CMakeLists.txt @@ -11,4 +11,5 @@ target_include_directories(kmbox_commands PUBLIC # Link with pico_stdlib for time functions target_link_libraries(kmbox_commands pico_stdlib + hid_defs ) \ No newline at end of file diff --git a/lib/kmbox-commands/kmbox_commands.h b/lib/kmbox-commands/kmbox_commands.h index e7bf8d0..1a7cdf8 100644 --- a/lib/kmbox-commands/kmbox_commands.h +++ b/lib/kmbox-commands/kmbox_commands.h @@ -14,12 +14,14 @@ // Button Definitions //--------------------------------------------------------------------+ -// HID Button bit masks (compatible with FAST_BTN_* protocol) -#define KMBOX_HID_BTN_LEFT 0x01 -#define KMBOX_HID_BTN_RIGHT 0x02 -#define KMBOX_HID_BTN_MIDDLE 0x04 -#define KMBOX_HID_BTN_BACK 0x08 -#define KMBOX_HID_BTN_FORWARD 0x10 +#include "hid_defs.h" + +// Legacy aliases — prefer HID_BTN_* from hid_defs.h in new code +#define KMBOX_HID_BTN_LEFT HID_BTN_LEFT +#define KMBOX_HID_BTN_RIGHT HID_BTN_RIGHT +#define KMBOX_HID_BTN_MIDDLE HID_BTN_MIDDLE +#define KMBOX_HID_BTN_BACK HID_BTN_BACK +#define KMBOX_HID_BTN_FORWARD HID_BTN_FORWARD typedef enum { KMBOX_BUTTON_LEFT = 0, diff --git a/lib/wire-protocol/CMakeLists.txt b/lib/wire-protocol/CMakeLists.txt index 7254499..46095da 100644 --- a/lib/wire-protocol/CMakeLists.txt +++ b/lib/wire-protocol/CMakeLists.txt @@ -1,2 +1,3 @@ add_library(wire_protocol INTERFACE) target_include_directories(wire_protocol INTERFACE include) +target_link_libraries(wire_protocol INTERFACE hid_defs) diff --git a/lib/wire-protocol/include/wire_protocol.h b/lib/wire-protocol/include/wire_protocol.h index c5aa6df..2c03b0a 100644 --- a/lib/wire-protocol/include/wire_protocol.h +++ b/lib/wire-protocol/include/wire_protocol.h @@ -18,6 +18,7 @@ #include #include +#include "hid_defs.h" // ============================================================================ // Command Bytes @@ -52,14 +53,16 @@ #define WIRE_MAX_PACKET 8 // Maximum packet size in bytes -// Button masks (match HID standard) -#define WIRE_BTN_LEFT 0x01 -#define WIRE_BTN_RIGHT 0x02 -#define WIRE_BTN_MIDDLE 0x04 -#define WIRE_BTN_BACK 0x08 -#define WIRE_BTN_FORWARD 0x10 +// Button masks — canonical definitions in hid_defs.h (HID_BTN_*) +// Legacy aliases for existing wire-protocol consumers +#define WIRE_BTN_LEFT HID_BTN_LEFT +#define WIRE_BTN_RIGHT HID_BTN_RIGHT +#define WIRE_BTN_MIDDLE HID_BTN_MIDDLE +#define WIRE_BTN_BACK HID_BTN_BACK +#define WIRE_BTN_FORWARD HID_BTN_FORWARD -// Smooth injection modes +// Injection modes — canonical definitions in fast_protocol.h (INJECT_MODE_*) +// Legacy aliases for existing wire-protocol consumers #define WIRE_INJECT_IMMEDIATE 0 #define WIRE_INJECT_SMOOTH 1 #define WIRE_INJECT_VELOCITY_MATCHED 2 diff --git a/smooth_injection.h b/smooth_injection.h index 62fc29c..78e4f6b 100644 --- a/smooth_injection.h +++ b/smooth_injection.h @@ -42,16 +42,27 @@ // Injection Modes //--------------------------------------------------------------------+ +// This enum is the canonical typed definition on the KMBox side. +// fast_protocol.h also defines INJECT_MODE_* as macros for the bridge +// side (which doesn't include this header). We undef any existing macros +// (in case fast_protocol.h was included first via defines.h) and set +// _INJECT_MODES_DEFINED to prevent fast_protocol.h from (re)defining them. +#undef INJECT_MODE_IMMEDIATE +#undef INJECT_MODE_SMOOTH +#undef INJECT_MODE_VELOCITY_MATCHED +#undef INJECT_MODE_MICRO +#define _INJECT_MODES_DEFINED + typedef enum { // Immediate: Add directly to accumulator (legacy behavior) INJECT_MODE_IMMEDIATE = 0, - + // Smooth: Spread movement across frames to match max per-frame rate INJECT_MODE_SMOOTH, - + // Velocity-matched: Blend with current mouse velocity INJECT_MODE_VELOCITY_MATCHED, - + // Micro: For tiny sub-pixel adjustments (anti-recoil, aim correction) INJECT_MODE_MICRO, } inject_mode_t;