diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..b7f2e2f
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,18 @@
+BasedOnStyle: LLVM
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+ColumnLimit: 120
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+BreakBeforeBraces: Allman
+PointerAlignment: Left
+AccessModifierOffset: -4
+NamespaceIndentation: None
+FixNamespaceComments: true
+IncludeBlocks: Regroup
+SortIncludes: true
+SpaceAfterCStyleCast: true
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..f4471a9
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,36 @@
+Checks: >
+ -*,
+ bugprone-*,
+ -bugprone-easily-swappable-parameters,
+ modernize-*,
+ -modernize-use-trailing-return-type,
+ -modernize-use-ranges,
+ performance-*,
+ readability-*,
+ -readability-identifier-length,
+ -readability-magic-numbers,
+ -readability-function-cognitive-complexity,
+ readability-implicit-bool-conversion,
+ cppcoreguidelines-*,
+ -cppcoreguidelines-avoid-magic-numbers,
+ -cppcoreguidelines-pro-type-reinterpret-cast,
+ -cppcoreguidelines-pro-type-vararg,
+ -cppcoreguidelines-owning-memory,
+ -cppcoreguidelines-special-member-functions,
+ -cppcoreguidelines-pro-bounds-pointer-arithmetic,
+ -cppcoreguidelines-pro-bounds-constant-array-index,
+ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
+ -cppcoreguidelines-avoid-c-arrays,
+ -cppcoreguidelines-pro-type-const-cast,
+ -cppcoreguidelines-pro-type-member-init,
+ -modernize-avoid-c-arrays,
+ -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
+ misc-*,
+ -misc-non-private-member-variables-in-classes,
+ -misc-include-cleaner,
+ -misc-use-anonymous-namespace,
+ portability-*,
+ -portability-avoid-pragma-once
+
+WarningsAsErrors: ''
+HeaderFilterRegex: '.*'
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8fb0d34
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+* text=auto
+
+*.cpp text eol=lf
+*.hpp text eol=lf
+*.h text eol=lf
+*.c text eol=lf
+*.txt text eol=lf
+*.md text eol=lf
+*.sh text eol=lf
+*.yml text eol=lf
+*.yaml text eol=lf
+CMakeLists.txt text eol=lf
+*.cmake text eol=lf
+*.json text eol=lf
+module.prop text eol=lf
+
+*.png binary
+*.jpg binary
+*.zip binary
+*.tar.gz binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..0129b14
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,118 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ release:
+ types: [published]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install LLVM tools
+ uses: egor-tensin/setup-clang@v2
+ with:
+ version: 22
+
+ - name: Check formatting
+ run: clang-format-22 --dry-run --Werror $(find src tests -name '*.cpp' -o -name '*.hpp' -o -name '*.h')
+
+ - name: Generate compile commands for lint
+ run: cmake -S . -B build_lint -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
+
+ - name: Run clang-tidy
+ run: clang-tidy-22 -p build_lint $(find src tests -name '*.cpp')
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Configure
+ run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
+
+ - name: Build
+ run: cmake --build build --target procon2droid_tests
+
+ - name: Run tests
+ run: ctest --test-dir build --output-on-failure
+
+ build-android:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ abi: [arm64-v8a, armeabi-v7a]
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up Android NDK
+ uses: nttld/setup-ndk@v1
+ with:
+ ndk-version: r27c
+ link-to-sdk: true
+
+ - name: Configure ${{ matrix.abi }}
+ run: |
+ cmake -S . -B build_${{ matrix.abi }} \
+ -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
+ -DANDROID_ABI=${{ matrix.abi }} \
+ -DANDROID_PLATFORM=android-24 \
+ -DPROCON2DROID_BUILD_TESTS=OFF
+
+ - name: Build ${{ matrix.abi }}
+ run: cmake --build build_${{ matrix.abi }} --target enable rumble
+
+ - name: Upload binaries
+ uses: actions/upload-artifact@v7
+ with:
+ name: binaries-${{ matrix.abi }}
+ path: |
+ build_${{ matrix.abi }}/src/enable
+ build_${{ matrix.abi }}/src/rumble
+
+ package:
+ needs: build-android
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release'
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Download arm64 binaries
+ uses: actions/download-artifact@v8
+ with:
+ name: binaries-arm64-v8a
+ path: dist/arm64-v8a
+
+ - name: Download armv7 binaries
+ uses: actions/download-artifact@v8
+ with:
+ name: binaries-armeabi-v7a
+ path: dist/armeabi-v7a
+
+ - name: Regenerate module.prop
+ run: cmake -S . -B build_dummy
+
+ - name: Package Magisk module
+ run: |
+ for abi in arm64-v8a armeabi-v7a; do
+ mkdir -p "package_$abi/system/bin"
+ cp module.prop "package_$abi/"
+ cp service.sh "package_$abi/"
+ cp "dist/$abi/enable" "package_$abi/system/bin/"
+ cp "dist/$abi/rumble" "package_$abi/system/bin/"
+ chmod +x "package_$abi/system/bin/"*
+ zip -r "procon2droid-${GITHUB_REF_NAME}-$abi.zip" "package_$abi/"
+ done
+
+ - name: Upload release assets
+ uses: softprops/action-gh-release@v3
+ with:
+ files: |
+ procon2droid-*.zip
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1321669
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+build/
+build-debug/
+build-android-arm64/
+build-android-armv7/
+build_android/
+build_dummy/
+compile_commands.json
+*.o
+*.obj
+*.exe
+*.out
+*.app
+
+# Packaging outputs
+*.zip
+*.tar.gz
+
+# Generated files
+module.prop
+
+# IDE / editor
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+Thumbs.db
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..4015581
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.21)
+
+project(procon2droid
+ VERSION 1.1.0
+ DESCRIPTION "Magisk/KernelSU module for Nintendo Switch 2 Pro Controller"
+ LANGUAGES CXX
+)
+
+# Generate module.prop from template so version stays in sync with CMake
+math(EXPR MODULE_VERSION_CODE "${PROJECT_VERSION_MAJOR} * 10000 + ${PROJECT_VERSION_MINOR} * 100 + ${PROJECT_VERSION_PATCH}")
+configure_file(
+ "${CMAKE_SOURCE_DIR}/module.prop.in"
+ "${CMAKE_SOURCE_DIR}/module.prop"
+ @ONLY
+)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Android")
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+endif()
+
+include(FetchContent)
+
+FetchContent_Declare(
+ spdlog
+ GIT_REPOSITORY https://github.com/gabime/spdlog.git
+ GIT_TAG v1.17.0
+)
+set(SPDLOG_USE_STD_FORMAT ON CACHE BOOL "" FORCE)
+FetchContent_MakeAvailable(spdlog)
+
+add_subdirectory(src)
+
+option(PROCON2DROID_BUILD_TESTS "Build test suite" ON)
+
+if(PROCON2DROID_BUILD_TESTS AND NOT CMAKE_SYSTEM_NAME STREQUAL "Android")
+ enable_testing()
+ add_subdirectory(tests)
+endif()
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000..a129598
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,91 @@
+{
+ "version": 6,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 21,
+ "patch": 0
+ },
+ "configurePresets": [
+ {
+ "name": "native",
+ "hidden": false,
+ "description": "Native build with tests (Linux/macOS/Windows with VS+Clang)",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release",
+ "PROCON2DROID_BUILD_TESTS": "ON"
+ }
+ },
+ {
+ "name": "native-debug",
+ "hidden": false,
+ "description": "Native debug build with tests and debug info",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build-debug",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "PROCON2DROID_BUILD_TESTS": "ON"
+ }
+ },
+ {
+ "name": "android-arm64",
+ "hidden": false,
+ "description": "Android cross-compile for arm64-v8a. Requires ANDROID_NDK_HOME env var.",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build-android-arm64",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release",
+ "CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
+ "ANDROID_ABI": "arm64-v8a",
+ "ANDROID_PLATFORM": "android-24",
+ "PROCON2DROID_BUILD_TESTS": "OFF"
+ }
+ },
+ {
+ "name": "android-armv7",
+ "hidden": false,
+ "description": "Android cross-compile for armeabi-v7a. Requires ANDROID_NDK_HOME env var.",
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/build-android-armv7",
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release",
+ "CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
+ "ANDROID_ABI": "armeabi-v7a",
+ "ANDROID_PLATFORM": "android-24",
+ "PROCON2DROID_BUILD_TESTS": "OFF"
+ }
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "native",
+ "configurePreset": "native",
+ "targets": ["procon2droid_tests"]
+ },
+ {
+ "name": "native-all",
+ "configurePreset": "native",
+ "description": "Build everything (tests only on native)"
+ },
+ {
+ "name": "android-arm64",
+ "configurePreset": "android-arm64",
+ "targets": ["enable", "rumble"]
+ },
+ {
+ "name": "android-armv7",
+ "configurePreset": "android-armv7",
+ "targets": ["enable", "rumble"]
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "native",
+ "configurePreset": "native",
+ "output": {
+ "outputOnFailure": true
+ }
+ }
+ ]
+}
diff --git a/README.md b/README.md
index b5367f6..b6aae46 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,114 @@
# procon2droid
-This is a root module that adds support for the Nintendo Switch 2 Pro Controller to Android. It supports normal input as well as HD Rumble. This project is greatly inspired by https://handheldlegend.github.io/procon2tool/.
-Note that only USB (not Bluetooth) is supported.
+[](https://github.com/hapqe/procon2droid/actions/workflows/ci.yml)
+[](LICENSE)
+[](https://en.cppreference.com/w/cpp/20)
-For installation grab the [latest release](https://github.com/hapqe/procon2droid/releases/tag/v1.0).
+> Magisk/KernelSU module that adds **Nintendo Switch 2 Pro Controller** support to Android — with full **HD Rumble**.
+
+**USB only** (not Bluetooth).
+
+---
+
+## Features
+
+- **Full input forwarding** — All buttons, sticks, and sensors work through a virtual uinput device
+- **HD Rumble** — Translates Android `FF_RUMBLE` effects into Switch 2 Pro Controller haptic HID reports
+- **Zero UI** — Runs entirely headless via Magisk/KernelSU service
+- **Lightweight** — Two small statically-linked binaries
+
+---
+
+## Requirements
+
+- Rooted Android device with **Magisk** or **KernelSU**
+- Nintendo Switch 2 Pro Controller connected via **USB cable**
+
+---
+
+## Installation
+
+Grab the [latest release](https://github.com/hapqe/procon2droid/releases) and flash the zip in Magisk/KernelSU.
+
+```bash
+# Or via Magisk CLI
+magisk --install-module procon2droid-v1.1-arm64-v8a.zip
+```
+
+Then reboot. The controller will be automatically detected when plugged in.
+
+---
+
+## How it works
+
+```mermaid
+flowchart TB
+ A["Switch 2 Pro Controller"] -- USB --> B["Android Kernel
(HID driver)"]
+ B -- /dev/input/event* --> C["rumble daemon
(uinput proxy)"]
+ C -- /dev/hidraw* --> D["enable
(one-shot init)"]
+ C --> E["Virtual gamepad
with FF_RUMBLE"]
+ D --> A
+```
+
+1. **`service.sh`** polls `lsusb` for VID `057e` / PID `2069`
+2. On detect: runs **`enable`** → sends the 17-packet USB init sequence to wake the controller, then releases the interface back to the kernel
+3. Then launches **`rumble`** → creates a virtual uinput device, forwards all input events, and intercepts `EV_FF` rumble commands to translate them into HID haptic reports
+
+---
+
+## Building
+
+### Prerequisites
+
+| Target | Tools |
+|--------|-------|
+| Tests (native) | CMake, Ninja, Clang, Catch2 (fetched automatically) |
+| Android binaries | Android NDK r26+ |
+
+### Run tests
+
+```bash
+cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
+cmake --build build --target procon2droid_tests
+ctest --test-dir build --output-on-failure
+```
+
+### Cross-compile for Android
+
+Requires the [Android NDK](https://developer.android.com/ndk/downloads). Set the `ANDROID_NDK_HOME` environment variable to the NDK root, then use the preset:
+
+```bash
+# Windows (PowerShell)
+$env:ANDROID_NDK_HOME = "C:\path\to\android-ndk"
+cmake --preset=android-arm64
+cmake --build --preset=android-arm64
+
+# Linux/macOS
+export ANDROID_NDK_HOME=/path/to/android-ndk
+cmake --preset=android-arm64
+cmake --build --preset=android-arm64
+```
+
+For `armeabi-v7a`, use the `android-armv7` preset instead.
+
+Or manually:
+
+```bash
+cmake -S . -B build_android -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
+ -DANDROID_ABI=arm64-v8a \
+ -DANDROID_PLATFORM=android-24 \
+ -DPROCON2DROID_BUILD_TESTS=OFF
+
+cmake --build build_android --target enable rumble
+```
+
+## Credits
+
+- Init sequence derived from [HandHeldLegend's procon2tool](https://handheldlegend.github.io/procon2tool/)
+
+---
+
+## License
+
+MIT
diff --git a/module.prop b/module.prop.in
similarity index 69%
rename from module.prop
rename to module.prop.in
index 87161d6..e88015b 100644
--- a/module.prop
+++ b/module.prop.in
@@ -1,6 +1,6 @@
id=procon2droid
name=Switch 2 Pro Controller Support
-version=v1.0
-versionCode=1
+version=v@PROJECT_VERSION@
+versionCode=@MODULE_VERSION_CODE@
author=hapqe
description=Enables the Switch 2 Pro Controller and forwards rumble data
diff --git a/service.sh b/service.sh
index be62c42..5b1cd30 100755
--- a/service.sh
+++ b/service.sh
@@ -1,32 +1,80 @@
#!/system/bin/sh
-# wait for the boot
-while [ "$(getprop sys.boot_completed)" != "1" ]; do
- sleep 5
-done
+# Magisk late_start service: monitors for Switch 2 Pro Controller (057e:2069)
+# and runs enable (one-shot init) then rumble (persistent proxy).
-MODDIR="/data/adb/modules/procon2droid"
+MODDIR="${MODDIR:-/data/adb/modules/procon2droid}"
ENABLE_BIN="$MODDIR/system/bin/enable"
RUMBLE_BIN="$MODDIR/system/bin/rumble"
+PIDFILE="$MODDIR/rumble.pid"
+LOGFILE="$MODDIR/service.log"
+
+log() {
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOGFILE"
+}
+
+# Sanity checks
+if [ ! -x "$ENABLE_BIN" ]; then
+ log "ERROR: enable binary not found or not executable: $ENABLE_BIN"
+ exit 1
+fi
-# make binaries executable
+if [ ! -x "$RUMBLE_BIN" ]; then
+ log "ERROR: rumble binary not found or not executable: $RUMBLE_BIN"
+ exit 1
+fi
+
+# Ensure binaries are executable (in case module was pushed manually)
chmod +x "$ENABLE_BIN" "$RUMBLE_BIN"
+# Clean up rumble if this script is killed
+cleanup() {
+ if [ -f "$PIDFILE" ]; then
+ PID=$(cat "$PIDFILE")
+ if kill -0 "$PID" 2>/dev/null; then
+ log "Cleanup: stopping rumble ($PID)"
+ kill "$PID"
+ fi
+ rm -f "$PIDFILE"
+ fi
+}
+trap cleanup TERM INT EXIT
+
+log "Service started. Monitoring for 057e:2069..."
+
while true; do
- if lsusb | grep -q "057e:2069"; then
-
- # no rumble, controller was plugged in
- if ! pgrep -f "$RUMBLE_BIN" > /dev/null; then
- echo "Pro Controller Detected: Enabling features..."
- "$ENABLE_BIN"
- "$RUMBLE_BIN" &
- echo -1000 > /proc/$!/oom_score_adj
+ CONTROLLER_PRESENT=false
+ if command -v lsusb >/dev/null 2>&1; then
+ if lsusb | grep -q "057e:2069"; then
+ CONTROLLER_PRESENT=true
+ fi
+ fi
+
+ if [ "$CONTROLLER_PRESENT" = true ]; then
+ if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
+ log "Controller detected. Running enable + rumble..."
+
+ "$ENABLE_BIN" >> "$LOGFILE" 2>&1
+ ENABLE_STATUS=$?
+
+ if [ $ENABLE_STATUS -ne 0 ]; then
+ log "WARNING: enable exited with status $ENABLE_STATUS"
+ fi
+
+ "$RUMBLE_BIN" >> "$LOGFILE" 2>&1 &
+ RUMBLE_PID=$!
+ echo "$RUMBLE_PID" > "$PIDFILE"
+ echo -1000 > /proc/$RUMBLE_PID/oom_score_adj
+ log "Rumble started (pid=$RUMBLE_PID)"
fi
else
- # cleanup
- if pgrep -f "$RUMBLE_BIN" > /dev/null; then
- echo "Pro Controller Removed: Cleaning up..."
- pkill -f "$RUMBLE_BIN"
+ if [ -f "$PIDFILE" ]; then
+ PID=$(cat "$PIDFILE")
+ if kill -0 "$PID" 2>/dev/null; then
+ log "Controller removed. Stopping rumble (pid=$PID)..."
+ kill "$PID"
+ fi
+ rm -f "$PIDFILE"
fi
fi
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..8052055
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,64 @@
+add_library(procon2droid_common STATIC
+ common/device_finder.cpp
+ common/haptic_report.cpp
+)
+
+target_include_directories(procon2droid_common PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+target_link_libraries(procon2droid_common PUBLIC
+ spdlog::spdlog
+)
+
+if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
+ target_compile_options(procon2droid_common PRIVATE /W4 /WX)
+else()
+ target_compile_options(procon2droid_common PRIVATE -Wall -Wextra -Wpedantic -Werror)
+endif()
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Android")
+ target_link_options(procon2droid_common PUBLIC -static-libstdc++)
+endif()
+
+# Main binaries are Linux/Android only (require kernel headers)
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Android")
+ add_executable(enable
+ enable/main.cpp
+ )
+
+ target_link_libraries(enable PRIVATE procon2droid_common)
+
+ target_compile_options(enable PRIVATE
+ -Wall -Wextra -Wpedantic -Werror
+ )
+
+ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
+ set_target_properties(enable PROPERTIES
+ LINK_FLAGS "-static-libstdc++"
+ )
+ add_custom_command(TARGET enable POST_BUILD
+ COMMAND ${CMAKE_STRIP} --strip-all $
+ )
+ endif()
+
+ add_executable(rumble
+ rumble/main.cpp
+ rumble/uinput_proxy.cpp
+ )
+
+ target_link_libraries(rumble PRIVATE procon2droid_common)
+
+ target_compile_options(rumble PRIVATE
+ -Wall -Wextra -Wpedantic -Werror
+ )
+
+ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
+ set_target_properties(rumble PROPERTIES
+ LINK_FLAGS "-static-libstdc++"
+ )
+ add_custom_command(TARGET rumble POST_BUILD
+ COMMAND ${CMAKE_STRIP} --strip-all $
+ )
+ endif()
+endif()
diff --git a/src/common/device_finder.cpp b/src/common/device_finder.cpp
new file mode 100644
index 0000000..6a81a4a
--- /dev/null
+++ b/src/common/device_finder.cpp
@@ -0,0 +1,179 @@
+#include "device_finder.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace procon2droid
+{
+
+namespace
+{
+
+bool matches_target_vidpid(std::string_view vid, std::string_view pid)
+{
+ return vid == kTargetVid && pid == kTargetPid;
+}
+
+using DirPtr = std::unique_ptr;
+
+inline constexpr std::string_view kLogTag = "device_finder";
+inline constexpr std::string_view kEventPrefix = "event";
+inline constexpr std::string_view kHidrawPrefix = "hidraw";
+inline constexpr std::string_view kHidIdKey = "HID_ID=";
+
+} // namespace
+
+std::optional find_usb_device()
+{
+ DIR* dp = opendir("/sys/bus/usb/devices");
+ if (!dp)
+ {
+ spdlog::error("{}: cannot open /sys/bus/usb/devices", kLogTag);
+ return std::nullopt;
+ }
+
+ DirPtr dp_guard{dp, closedir};
+ const struct dirent* entry = nullptr;
+
+ while ((entry = readdir(dp_guard.get())))
+ {
+ if (entry->d_name[0] == '.')
+ {
+ continue;
+ }
+
+ const std::string dir = std::string("/sys/bus/usb/devices/") + entry->d_name;
+ std::ifstream vid_file(dir + "/idVendor");
+ std::ifstream pid_file(dir + "/idProduct");
+ std::string vid;
+ std::string pid;
+
+ if (!(vid_file >> vid && pid_file >> pid))
+ {
+ continue;
+ }
+ if (!matches_target_vidpid(vid, pid))
+ {
+ continue;
+ }
+
+ std::ifstream bus_file(dir + "/busnum");
+ std::ifstream dev_file(dir + "/devnum");
+ int busnum = 0;
+ int devnum = 0;
+
+ if (bus_file >> busnum && dev_file >> devnum)
+ {
+ char path[64];
+ snprintf(path, sizeof(path), "/dev/bus/usb/%03d/%03d", busnum, devnum);
+ spdlog::info("{}: found USB device at {}", kLogTag, path);
+ return std::string(path);
+ }
+ }
+
+ return std::nullopt;
+}
+
+std::optional find_input_event()
+{
+ DIR* dir = opendir("/sys/class/input/");
+ if (!dir)
+ {
+ spdlog::error("{}: cannot open /sys/class/input/", kLogTag);
+ return std::nullopt;
+ }
+
+ DirPtr dir_guard{dir, closedir};
+ const struct dirent* entry = nullptr;
+
+ while ((entry = readdir(dir_guard.get())) != nullptr)
+ {
+ const std::string name = entry->d_name;
+ if (!name.starts_with(kEventPrefix))
+ {
+ continue;
+ }
+
+ const std::string base = "/sys/class/input/" + name + "/device/id/";
+ std::ifstream vid_file(base + "vendor");
+ std::ifstream pid_file(base + "product");
+ std::string vid_str;
+ std::string pid_str;
+
+ if (!(vid_file >> vid_str && pid_file >> pid_str))
+ {
+ continue;
+ }
+
+ try
+ {
+ const uint32_t vid = std::stoul(vid_str, nullptr, 16);
+ const uint32_t pid = std::stoul(pid_str, nullptr, 16);
+
+ if (vid == kTargetVidHex && pid == kTargetPidHex)
+ {
+ const std::string path = "/dev/input/" + name;
+ spdlog::info("{}: found input at {}", kLogTag, path);
+ return path;
+ }
+ }
+ catch (...)
+ {
+ continue;
+ }
+ }
+
+ return std::nullopt;
+}
+
+std::optional find_hidraw_device()
+{
+ DIR* dir = opendir("/sys/class/hidraw/");
+ if (!dir)
+ {
+ spdlog::error("{}: cannot open /sys/class/hidraw/", kLogTag);
+ return std::nullopt;
+ }
+
+ DirPtr dir_guard{dir, closedir};
+ const struct dirent* entry = nullptr;
+
+ while ((entry = readdir(dir_guard.get())) != nullptr)
+ {
+ const std::string name = entry->d_name;
+ if (!name.starts_with(kHidrawPrefix))
+ {
+ continue;
+ }
+
+ std::ifstream file("/sys/class/hidraw/" + name + "/device/uevent");
+ std::string line;
+ while (std::getline(file, line))
+ {
+ if (line.find(kHidIdKey) == std::string::npos)
+ {
+ continue;
+ }
+
+ // HID_ID uevent lines may use uppercase hex; do case-insensitive check.
+ std::string upper_line = line;
+ std::transform(upper_line.begin(), upper_line.end(), upper_line.begin(), ::toupper);
+ const bool vid_match = upper_line.find(kTargetVid) != std::string::npos;
+ const bool pid_match = upper_line.find(kTargetPid) != std::string::npos;
+
+ if (vid_match && pid_match)
+ {
+ const std::string path = "/dev/" + name;
+ spdlog::info("device_finder: found hidraw at {}", path);
+ return path;
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
+} // namespace procon2droid
diff --git a/src/common/device_finder.hpp b/src/common/device_finder.hpp
new file mode 100644
index 0000000..e71225e
--- /dev/null
+++ b/src/common/device_finder.hpp
@@ -0,0 +1,24 @@
+// Scans sysfs for the Nintendo Switch 2 Pro Controller (VID 057e / PID 2069)
+// and returns device paths for USB, input event, and hidraw nodes.
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace procon2droid
+{
+
+inline constexpr std::string_view kTargetVid = "057e";
+inline constexpr std::string_view kTargetPid = "2069";
+inline constexpr uint32_t kTargetVidHex = 0x057E;
+inline constexpr uint32_t kTargetPidHex = 0x2069;
+
+std::optional find_usb_device();
+
+std::optional find_input_event();
+
+std::optional find_hidraw_device();
+
+} // namespace procon2droid
diff --git a/src/common/haptic_report.cpp b/src/common/haptic_report.cpp
new file mode 100644
index 0000000..b2b1d79
--- /dev/null
+++ b/src/common/haptic_report.cpp
@@ -0,0 +1,48 @@
+#include "haptic_report.hpp"
+
+#include
+
+namespace procon2droid
+{
+
+[[nodiscard]] std::array
+build_haptic_report(uint16_t strong_magnitude, uint16_t weak_magnitude, uint8_t counter, bool active)
+{
+ std::array report{};
+ report[0] = kHapticCommandId;
+
+ const uint8_t seq_byte = 0x50 | (counter & 0x0F);
+ report[1] = seq_byte;
+ report[17] = seq_byte;
+
+ auto l_pattern = kHapticPatternOff;
+ auto r_pattern = kHapticPatternOff;
+
+ if (active)
+ {
+ if (strong_magnitude > 32768)
+ {
+ l_pattern = kHapticPatternStrong;
+ }
+ else if (strong_magnitude > 0)
+ {
+ l_pattern = kHapticPatternWeak;
+ }
+
+ if (weak_magnitude > 32768)
+ {
+ r_pattern = kHapticPatternStrong;
+ }
+ else if (weak_magnitude > 0)
+ {
+ r_pattern = kHapticPatternWeak;
+ }
+ }
+
+ std::copy(l_pattern.begin(), l_pattern.end(), report.begin() + 2);
+ std::copy(r_pattern.begin(), r_pattern.end(), report.begin() + 18);
+
+ return report;
+}
+
+} // namespace procon2droid
diff --git a/src/common/haptic_report.hpp b/src/common/haptic_report.hpp
new file mode 100644
index 0000000..cbbb7a1
--- /dev/null
+++ b/src/common/haptic_report.hpp
@@ -0,0 +1,21 @@
+// Builds the 64-byte HID haptic report from FF_RUMBLE magnitudes.
+#pragma once
+
+#include
+#include
+#include
+
+namespace procon2droid
+{
+
+inline constexpr size_t kHapticReportSize = 64;
+inline constexpr uint8_t kHapticCommandId = 0x02;
+
+inline constexpr std::array kHapticPatternStrong{0x93, 0x35, 0x36, 0x1c, 0x0d};
+inline constexpr std::array kHapticPatternWeak{0x4b, 0x7d, 0x80, 0x5a, 0x02};
+inline constexpr std::array kHapticPatternOff{0x00, 0x00, 0x00, 0x00, 0x00};
+
+[[nodiscard]] std::array
+build_haptic_report(uint16_t strong_magnitude, uint16_t weak_magnitude, uint8_t counter, bool active);
+
+} // namespace procon2droid
diff --git a/src/common/init_sequence.hpp b/src/common/init_sequence.hpp
new file mode 100644
index 0000000..3fb78c1
--- /dev/null
+++ b/src/common/init_sequence.hpp
@@ -0,0 +1,81 @@
+// constexpr init packet data for waking the Switch 2 Pro Controller over USB.
+// Derived from HandHeldLegend's procon2tool.
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace procon2droid
+{
+
+struct InitPacket
+{
+ std::span data;
+};
+
+constexpr std::array kCmd03{0x03, 0x91, 0x00, 0x0d, 0x00, 0x08, 0x00, 0x00,
+ 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+constexpr std::array kCmd07{0x07, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd16{0x16, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmdMac{0x15, 0x91, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x02, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+constexpr std::array kCmdLtk{0x15, 0x91, 0x00, 0x02, 0x00, 0x11, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+constexpr std::array kCmd15_03{0x15, 0x91, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd09{0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmdImu02{0x0c, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd11{0x11, 0x91, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd0a{0x0a, 0x91, 0x00, 0x08, 0x00, 0x14, 0x00, 0x00, 0x01, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x35, 0x00, 0x46,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmdImu04{0x0c, 0x91, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmdHaptics{0x03, 0x91, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd10{0x10, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd01{0x01, 0x91, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd03Alt{0x03, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00};
+
+constexpr std::array kCmd0aAlt{0x0a, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00};
+
+constexpr std::array kCmdLed{0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+inline constexpr std::array kInitSequence{{
+ {kCmd03},
+ {kCmd07},
+ {kCmd16},
+ {kCmdMac},
+ {kCmdLtk},
+ {kCmd15_03},
+ {kCmd09},
+ {kCmdImu02},
+ {kCmd11},
+ {kCmd0a},
+ {kCmdImu04},
+ {kCmdHaptics},
+ {kCmd10},
+ {kCmd01},
+ {kCmd03Alt},
+ {kCmd0aAlt},
+ {kCmdLed},
+}};
+
+inline constexpr size_t kInitSequenceLength = kInitSequence.size();
+
+} // namespace procon2droid
diff --git a/src/enable.cpp b/src/enable.cpp
deleted file mode 100644
index ec54885..0000000
--- a/src/enable.cpp
+++ /dev/null
@@ -1,125 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-// --- initialization commands as found on https://github.com/HandHeldLegend/handheldlegend.github.io/blob/master/procon2tool/index.html ---
-const uint8_t CMD_03[] = {0x03, 0x91, 0x00, 0x0d, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
-const uint8_t CMD_07[] = {0x07, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_16[] = {0x16, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_MAC[] = {0x15, 0x91, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
-const uint8_t CMD_LTK[] = {0x15, 0x91, 0x00, 0x02, 0x00, 0x11, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
-const uint8_t CMD_15_03[] = {0x15, 0x91, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00};
-const uint8_t CMD_09[] = {0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_IMU_02[] = {0x0c, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00};
-const uint8_t CMD_11[] = {0x11, 0x91, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_0A[] = {0x0a, 0x91, 0x00, 0x08, 0x00, 0x14, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x35, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_IMU_04[] = {0x0c, 0x91, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00};
-const uint8_t CMD_HAPTICS[] = {0x03, 0x91, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00};
-const uint8_t CMD_10[] = {0x10, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_01[] = {0x01, 0x91, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00};
-const uint8_t CMD_03_ALT[] = {0x03, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00};
-const uint8_t CMD_0A_ALT[] = {0x0a, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00};
-const uint8_t CMD_LED[] = {0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
-struct InitPacket { const uint8_t* data; size_t len; };
-InitPacket init_sequence[] = {
- {CMD_03, sizeof(CMD_03)}, {CMD_07, sizeof(CMD_07)}, {CMD_16, sizeof(CMD_16)},
- {CMD_MAC, sizeof(CMD_MAC)}, {CMD_LTK, sizeof(CMD_LTK)}, {CMD_15_03, sizeof(CMD_15_03)},
- {CMD_09, sizeof(CMD_09)}, {CMD_IMU_02, sizeof(CMD_IMU_02)}, {CMD_11, sizeof(CMD_11)},
- {CMD_0A, sizeof(CMD_0A)}, {CMD_IMU_04, sizeof(CMD_IMU_04)}, {CMD_HAPTICS, sizeof(CMD_HAPTICS)},
- {CMD_10, sizeof(CMD_10)}, {CMD_01, sizeof(CMD_01)}, {CMD_03_ALT, sizeof(CMD_03_ALT)},
- {CMD_0A_ALT, sizeof(CMD_0A_ALT)}, {CMD_LED, sizeof(CMD_LED)}
-};
-
-std::string find_raw_usb_device() {
- DIR *dp = opendir("/sys/bus/usb/devices");
- if (!dp) return "";
- struct dirent *entry;
- while ((entry = readdir(dp))) {
- if (entry->d_name[0] == '.') continue;
- std::string dir = std::string("/sys/bus/usb/devices/") + entry->d_name;
- std::ifstream vid_file(dir + "/idVendor"), pid_file(dir + "/idProduct");
- std::string vid, pid;
- if (vid_file >> vid && pid_file >> pid && vid == "057e" && pid == "2069") { // Switch 2 Pro Controller
- std::ifstream bus_file(dir + "/busnum"), dev_file(dir + "/devnum");
- int busnum, devnum;
- if (bus_file >> busnum && dev_file >> devnum) {
- char path[64];
- snprintf(path, sizeof(path), "/dev/bus/usb/%03d/%03d", busnum, devnum);
- closedir(dp);
- return std::string(path);
- }
- }
- }
- closedir(dp);
- return "";
-}
-
-int main() {
- std::cout << "Locating the Controller" << std::endl;
-
- std::string usbPath = find_raw_usb_device();
- if (usbPath.empty()) {
- std::cerr << "Error: Controller not found. Is it plugged in?" << std::endl;
- return 1;
- }
-
- int fd = open(usbPath.c_str(), O_RDWR);
- if (fd < 0) {
- std::cerr << "Error: Could not open USB device. Are you running as root/su?" << std::endl;
- return 1;
- }
-
- // Detach kernel driver momentarily from Interface 1
- struct usbdevfs_ioctl disconnect_cmd = {1, USBDEVFS_DISCONNECT, NULL};
- ioctl(fd, USBDEVFS_IOCTL, &disconnect_cmd);
-
- int iface = 1;
- if (ioctl(fd, USBDEVFS_CLAIMINTERFACE, &iface) < 0) {
- std::cerr << "Error: Failed to claim USB Interface 1." << std::endl;
- close(fd);
- return 1;
- }
-
- std::cout << "Waking up controller..." << std::endl;
-
- // Discover correct Bulk OUT endpoint silently
- int target_ep = -1;
- for (int test_ep = 1; test_ep <= 5; test_ep++) {
- struct usbdevfs_bulktransfer bulk_test = {(unsigned int)test_ep, sizeof(CMD_03), 1000, (void*)CMD_03};
- if (ioctl(fd, USBDEVFS_BULK, &bulk_test) >= 0) {
- target_ep = test_ep;
- break;
- }
- }
-
- if (target_ep == -1) {
- std::cerr << "Error: Could not find valid bulk endpoint." << std::endl;
- close(fd);
- return 1;
- }
-
- // Send the rest of the command sequence
- for (size_t i = 1; i < sizeof(init_sequence) / sizeof(init_sequence[0]); i++) {
- struct usbdevfs_bulktransfer bulk = {(unsigned int)target_ep, (unsigned int)init_sequence[i].len, 1000, (void*)init_sequence[i].data};
- ioctl(fd, USBDEVFS_BULK, &bulk);
- usleep(10000); // 10ms wait
-
- // Dummy read to clear potential ACKs
- unsigned char dummy[32];
- struct usbdevfs_bulktransfer bulk_in = {(unsigned int)(target_ep | 0x80), 32, 20, dummy};
- ioctl(fd, USBDEVFS_BULK, &bulk_in);
- }
-
- // Clean up and release control back to Android OS
- ioctl(fd, USBDEVFS_RELEASEINTERFACE, &iface);
- close(fd);
-
- std::cout << "Success! Controller is awake. Handing control back to Android." << std::endl;
- return 0;
-}
diff --git a/src/enable/main.cpp b/src/enable/main.cpp
new file mode 100644
index 0000000..d5cc065
--- /dev/null
+++ b/src/enable/main.cpp
@@ -0,0 +1,115 @@
+// One-shot USB init binary: claims interface 1, sends the 17-packet init
+// sequence, then releases the interface so Android HID can take over.
+#include "../common/device_finder.hpp"
+#include "../common/init_sequence.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+
+int find_bulk_out_endpoint(int fd, const uint8_t* probe_data, size_t probe_len)
+{
+ for (int ep = 1; ep <= 5; ++ep)
+ {
+ struct usbdevfs_bulktransfer bt{};
+ bt.ep = static_cast(ep);
+ bt.len = static_cast(probe_len);
+ bt.timeout = 1000;
+ bt.data = const_cast(probe_data); // NOLINT(cppcoreguidelines-pro-type-const-cast)
+
+ if (ioctl(fd, USBDEVFS_BULK, &bt) >= 0)
+ {
+ return ep;
+ }
+ }
+ return -1;
+}
+
+void send_init_sequence(int fd, int bulk_out_ep)
+{
+ const unsigned int in_ep = static_cast(bulk_out_ep) | 0x80;
+
+ for (size_t i = 1; i < procon2droid::kInitSequenceLength; ++i)
+ {
+ const auto& pkt = procon2droid::kInitSequence[i];
+
+ struct usbdevfs_bulktransfer bulk_out{};
+ bulk_out.ep = static_cast(bulk_out_ep);
+ bulk_out.len = static_cast(pkt.data.size());
+ bulk_out.timeout = 1000;
+ bulk_out.data = const_cast(pkt.data.data()); // NOLINT(cppcoreguidelines-pro-type-const-cast)
+
+ ioctl(fd, USBDEVFS_BULK, &bulk_out);
+ usleep(10000);
+
+ unsigned char dummy[32]{};
+ struct usbdevfs_bulktransfer bulk_in{};
+ bulk_in.ep = in_ep;
+ bulk_in.len = sizeof(dummy);
+ bulk_in.timeout = 20;
+ bulk_in.data = dummy;
+
+ ioctl(fd, USBDEVFS_BULK, &bulk_in);
+ }
+}
+
+} // namespace
+
+int main()
+{
+ using namespace procon2droid;
+
+ spdlog::info("Locating the Controller");
+
+ auto usb_path = find_usb_device();
+ if (!usb_path)
+ {
+ spdlog::error("Controller not found. Is it plugged in?");
+ return EXIT_FAILURE;
+ }
+
+ const int fd = open(usb_path->c_str(), O_RDWR);
+ if (fd < 0)
+ {
+ spdlog::error("Could not open USB device. Are you running as root/su?");
+ return EXIT_FAILURE;
+ }
+
+ struct usbdevfs_ioctl disconnect_cmd{};
+ disconnect_cmd.ifno = 1;
+ disconnect_cmd.ioctl_code = USBDEVFS_DISCONNECT;
+ ioctl(fd, USBDEVFS_IOCTL, &disconnect_cmd);
+
+ int iface = 1;
+ if (ioctl(fd, USBDEVFS_CLAIMINTERFACE, &iface) < 0)
+ {
+ spdlog::error("Failed to claim USB Interface 1");
+ close(fd);
+ return EXIT_FAILURE;
+ }
+
+ spdlog::info("Waking up controller...");
+
+ const int bulk_ep = find_bulk_out_endpoint(fd, kInitSequence[0].data.data(), kInitSequence[0].data.size());
+ if (bulk_ep < 0)
+ {
+ spdlog::error("Could not find valid bulk endpoint");
+ ioctl(fd, USBDEVFS_RELEASEINTERFACE, &iface);
+ close(fd);
+ return EXIT_FAILURE;
+ }
+
+ send_init_sequence(fd, bulk_ep);
+
+ ioctl(fd, USBDEVFS_RELEASEINTERFACE, &iface);
+ close(fd);
+
+ spdlog::info("Success! Controller is awake. Handing control back to Android.");
+ return EXIT_SUCCESS;
+}
diff --git a/src/rumble.cpp b/src/rumble.cpp
deleted file mode 100644
index db15cee..0000000
--- a/src/rumble.cpp
+++ /dev/null
@@ -1,238 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include