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/.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 0000000..ed1fd88 Binary files /dev/null and b/tools/kmbox_relay differ diff --git a/tools/kmbox_relay.c b/tools/kmbox_relay.c new file mode 100644 index 0000000..137b2ad --- /dev/null +++ b/tools/kmbox_relay.c @@ -0,0 +1,1329 @@ +/* + * 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) + * -w, --web PORT Enable web dashboard on PORT (default: 8080) + * -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; + + /* 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; + +/* ========================================================================== */ +/* 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++; + 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) { + 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); +} + +/* ========================================================================== */ +/* 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 */ +/* ========================================================================== */ + +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); + 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) { + fd_set rfds; + struct timeval tv; + int maxfd = 0; + + FD_ZERO(&rfds); + +#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; + 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 + + /* 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 (%.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, + 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..."); +} + +/* ========================================================================== */ +/* 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" + " -w, --web PORT Enable web dashboard (default port: 8080)\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->http_port = 0; /* 0 = disabled */ + 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 (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'; + } 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; + r->http = INVALID_SOCK; + r->http_client = 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); + + /* 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); + + r->running = true; + run_relay(r); + + /* Cleanup */ + if (r->http != INVALID_SOCK) sock_close(r->http); + 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) {