├── .github └── workflows │ ├── ci.yml │ └── python-ci.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs ├── python ├── README.md ├── pyproject.toml ├── qmk_hid │ ├── __init__.py │ ├── __pycache__ │ │ └── uf2conv.cpython-311.pyc │ ├── firmware_update.py │ ├── gui.py │ ├── protocol.py │ └── uf2conv.py └── requirements.txt ├── res └── logo_cropped_transparent_keyboard_48x48.ico ├── rust-toolchain.toml ├── screenshots └── qmk_gui_screenshot.png └── src ├── factory.rs ├── lib.rs ├── main.rs ├── raw_hid.rs └── via.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | on: 3 | push: 4 | 5 | jobs: 6 | # Enable later 7 | #freebsd-cross-build: 8 | # name: Cross-Build for FreeBSD 9 | # runs-on: 'ubuntu-24.04' 10 | # steps: 11 | # - uses: actions/checkout@v4 12 | 13 | # - name: Setup Rust toolchain 14 | # run: rustup show 15 | 16 | # - name: Install cross compilation tool 17 | # run: cargo install cross 18 | 19 | # - name: Build FreeBSD tool 20 | # run: cross build --target=x86_64-unknown-freebsd 21 | 22 | # - name: Upload FreeBSD App 23 | # uses: actions/upload-artifact@v4 24 | # with: 25 | # name: qmk_hid_freebsd 26 | # path: target/x86_64-unknown-freebsd/debug/qmk_hid 27 | 28 | build: 29 | name: Build Linux 30 | runs-on: ubuntu-24.04 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Install dependencies 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y libudev-dev 38 | 39 | - name: Setup Rust toolchain 40 | run: rustup show 41 | 42 | - name: Build Linux tool 43 | run: | 44 | cargo build 45 | cargo build --release 46 | 47 | - name: Check if Linux tool can start 48 | run: cargo run -- --help 49 | 50 | - name: Upload Linux Debug tool 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: qmk_hid_debug_linux 54 | path: target/debug/qmk_hid 55 | 56 | - name: Upload Linux Relase tool 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: qmk_hid_linux 60 | path: target/release/qmk_hid 61 | 62 | build-windows: 63 | name: Build Windows 64 | runs-on: windows-2022 65 | steps: 66 | - uses: actions/checkout@v4 67 | 68 | - name: Setup Rust toolchain 69 | run: rustup show 70 | 71 | - name: Build Windows tool 72 | run: | 73 | cargo build 74 | cargo build --release 75 | 76 | - name: Check if Windows tool can start 77 | run: cargo run -- --help 78 | 79 | - name: Upload Windows Debug App 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: qmk_hid_debug_windows 83 | path: target/debug/qmk_hid.exe 84 | 85 | - name: Upload Windows Release App 86 | uses: actions/upload-artifact@v4 87 | with: 88 | name: qmk_hid_windows 89 | path: target/release/qmk_hid.exe 90 | 91 | lints: 92 | name: Lints 93 | runs-on: ubuntu-24.04 94 | steps: 95 | - uses: actions/checkout@v4 96 | 97 | - name: Install dependencies 98 | run: | 99 | sudo apt-get update 100 | sudo apt-get install -y libudev-dev 101 | 102 | - name: Setup Rust toolchain 103 | run: rustup show 104 | 105 | - name: Run cargo fmt 106 | run: cargo fmt --all -- --check 107 | 108 | - name: Run cargo clippy 109 | run: cargo clippy -- -D warnings 110 | -------------------------------------------------------------------------------- /.github/workflows/python-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | on: 3 | push: 4 | 5 | jobs: 6 | build-gui: 7 | name: Build GUI 8 | runs-on: windows-2022 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Download releases to bundle 13 | run: | 14 | mkdir releases 15 | mkdir releases\0.2.9 16 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.9/framework_ansi_default_v0.2.9.uf2 -OutFile releases\0.2.9\framework_ansi_default.uf2 17 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.9/framework_iso_default_v0.2.9.uf2 -OutFile releases\0.2.9\framework_iso_default.uf2 18 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.9/framework_jis_default_v0.2.9.uf2 -OutFile releases\0.2.9\framework_jis_default.uf2 19 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.9/framework_numpad_default_v0.2.9.uf2 -OutFile releases\0.2.9\framework_numpad_default.uf2 20 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.9/framework_macropad_default_v0.2.9.uf2 -OutFile releases\0.2.9\framework_macropad_default.uf2 21 | mkdir releases\0.2.8 22 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.8/framework_ansi_default_v0.2.8.uf2 -OutFile releases\0.2.8\framework_ansi_default.uf2 23 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.8/framework_iso_default_v0.2.8.uf2 -OutFile releases\0.2.8\framework_iso_default.uf2 24 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.8/framework_jis_default_v0.2.8.uf2 -OutFile releases\0.2.8\framework_jis_default.uf2 25 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.8/framework_numpad_default_v0.2.8.uf2 -OutFile releases\0.2.8\framework_numpad_default.uf2 26 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.8/framework_macropad_default_v0.2.8.uf2 -OutFile releases\0.2.8\framework_macropad_default.uf2 27 | mkdir releases\0.2.7 28 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.7/framework_ansi_default_v0.2.7.uf2 -OutFile releases\0.2.7\framework_ansi_default.uf2 29 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.7/framework_iso_default_v0.2.7.uf2 -OutFile releases\0.2.7\framework_iso_default.uf2 30 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.7/framework_jis_default_v0.2.7.uf2 -OutFile releases\0.2.7\framework_jis_default.uf2 31 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.7/framework_numpad_default_v0.2.7.uf2 -OutFile releases\0.2.7\framework_numpad_default.uf2 32 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.7/framework_macropad_default_v0.2.7.uf2 -OutFile releases\0.2.7\framework_macropad_default.uf2 33 | mkdir releases\0.2.6 34 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.6/framework_ansi_default_v0.2.6.uf2 -OutFile releases\0.2.6\framework_ansi_default.uf2 35 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.6/framework_iso_default_v0.2.6.uf2 -OutFile releases\0.2.6\framework_iso_default.uf2 36 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.6/framework_jis_default_v0.2.6.uf2 -OutFile releases\0.2.6\framework_jis_default.uf2 37 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.6/framework_numpad_default_v0.2.6.uf2 -OutFile releases\0.2.6\framework_numpad_default.uf2 38 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.6/framework_macropad_default_v0.2.6.uf2 -OutFile releases\0.2.6\framework_macropad_default.uf2 39 | mkdir releases\0.2.5 40 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.5/framework_ansi_default_v0.2.5.uf2 -OutFile releases\0.2.5\framework_ansi_default.uf2 41 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.5/framework_iso_default_v0.2.5.uf2 -OutFile releases\0.2.5\framework_iso_default.uf2 42 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.5/framework_jis_default_v0.2.5.uf2 -OutFile releases\0.2.5\framework_jis_default.uf2 43 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.5/framework_numpad_default_v0.2.5.uf2 -OutFile releases\0.2.5\framework_numpad_default.uf2 44 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.5/framework_macropad_default_v0.2.5.uf2 -OutFile releases\0.2.5\framework_macropad_default.uf2 45 | mkdir releases\0.2.4 46 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.4/framework_ansi_default_v0.2.4.uf2 -OutFile releases\0.2.4\framework_ansi_default.uf2 47 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.4/framework_iso_default_v0.2.4.uf2 -OutFile releases\0.2.4\framework_iso_default.uf2 48 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.4/framework_jis_default_v0.2.4.uf2 -OutFile releases\0.2.4\framework_jis_default.uf2 49 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.4/framework_numpad_default_v0.2.4.uf2 -OutFile releases\0.2.4\framework_numpad_default.uf2 50 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.4/framework_macropad_default_v0.2.4.uf2 -OutFile releases\0.2.4\framework_macropad_default.uf2 51 | mkdir releases\0.2.3 52 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.3/framework_ansi_default_v0.2.3.uf2 -OutFile releases\0.2.3\framework_ansi_default.uf2 53 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.3/framework_iso_default_v0.2.3.uf2 -OutFile releases\0.2.3\framework_iso_default.uf2 54 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.3/framework_jis_default_v0.2.3.uf2 -OutFile releases\0.2.3\framework_jis_default.uf2 55 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.3/framework_numpad_default_v0.2.3.uf2 -OutFile releases\0.2.3\framework_numpad_default.uf2 56 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.3/framework_macropad_default_v0.2.3.uf2 -OutFile releases\0.2.3\framework_macropad_default.uf2 57 | mkdir releases\0.2.2 58 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.2/framework_ansi_default_v0.2.2.uf2 -OutFile releases\0.2.2\framework_ansi_default.uf2 59 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.2/framework_iso_default_v0.2.2.uf2 -OutFile releases\0.2.2\framework_iso_default.uf2 60 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.2/framework_jis_default_v0.2.2.uf2 -OutFile releases\0.2.2\framework_jis_default.uf2 61 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.2/framework_numpad_default_v0.2.2.uf2 -OutFile releases\0.2.2\framework_numpad_default.uf2 62 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.2/framework_macropad_default_v0.2.2.uf2 -OutFile releases\0.2.2\framework_macropad_default.uf2 63 | mkdir releases\0.2.1 64 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.1/framework_ansi_default_v0.2.1.uf2 -OutFile releases\0.2.1\framework_ansi_default.uf2 65 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.1/framework_iso_default_v0.2.1.uf2 -OutFile releases\0.2.1\framework_iso_default.uf2 66 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.1/framework_jis_default_v0.2.1.uf2 -OutFile releases\0.2.1\framework_jis_default.uf2 67 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.1/framework_numpad_default_v0.2.1.uf2 -OutFile releases\0.2.1\framework_numpad_default.uf2 68 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.1/framework_macropad_default_v0.2.1.uf2 -OutFile releases\0.2.1\framework_macropad_default.uf2 69 | mkdir releases\0.2.0 70 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.0/framework_ansi_default_v0.2.0.uf2 -OutFile releases\0.2.0\framework_ansi_default.uf2 71 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.0/framework_iso_default_v0.2.0.uf2 -OutFile releases\0.2.0\framework_iso_default.uf2 72 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.0/framework_jis_default_v0.2.0.uf2 -OutFile releases\0.2.0\framework_jis_default.uf2 73 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.0/framework_numpad_default_v0.2.0.uf2 -OutFile releases\0.2.0\framework_numpad_default.uf2 74 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.2.0/framework_macropad_default_v0.2.0.uf2 -OutFile releases\0.2.0\framework_macropad_default.uf2 75 | mkdir releases\0.1.9 76 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_ansi_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_ansi_default.uf2 77 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_iso_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_iso_default.uf2 78 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_jis_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_jis_default.uf2 79 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_numpad_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_numpad_default.uf2 80 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_macropad_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_macropad_default.uf2 81 | mkdir releases\0.1.8 82 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_ansi_default.uf2 -OutFile releases\0.1.8\framework_ansi_default.uf2 83 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_iso_default.uf2 -OutFile releases\0.1.8\framework_iso_default.uf2 84 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_jis_default.uf2 -OutFile releases\0.1.8\framework_jis_default.uf2 85 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_numpad_default.uf2 -OutFile releases\0.1.8\framework_numpad_default.uf2 86 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_gridpad_default.uf2 -OutFile releases\0.1.8\framework_gridpad_default.uf2 87 | mkdir releases\0.1.7 88 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_ansi_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_ansi_default.uf2 89 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_iso_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_iso_default.uf2 90 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_jis_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_jis_default.uf2 91 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_numpad_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_numpad_default.uf2 92 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_gridpad_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_gridpad_default.uf2 93 | mkdir releases\0.1.6 94 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_ansi_default.uf2 -OutFile releases\0.1.6\framework_ansi_default.uf2 95 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_iso_default.uf2 -OutFile releases\0.1.6\framework_iso_default.uf2 96 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_jis_default.uf2 -OutFile releases\0.1.6\framework_jis_default.uf2 97 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_numpad_default.uf2 -OutFile releases\0.1.6\framework_numpad_default.uf2 98 | Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_gridpad_default.uf2 -OutFile releases\0.1.6\framework_gridpad_default.uf2 99 | 100 | # To run locally, need to make sure to include the pywin32 DLL 101 | # pyinstaller --onefile, --name "python/qmk_gui/gui.py", --windowed, --add-data "releases;releases" --path C:\users\skype\appdata\local\packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\localcache\local-packages\Python312\site-packages\pywin32_system32 --icon=res\logo_cropped_transparent_keyboard_48x48.ico --add-data 'res;res' -p python/qmk_hid python/qmk_hid/gui.py 102 | - name: Create Executable 103 | uses: JohnAZoidberg/pyinstaller-action@dont-clean 104 | with: 105 | python_ver: '3.12' 106 | spec: python/qmk_hid/gui.py 107 | requirements: 'python/requirements.txt' 108 | upload_exe_with_name: 'qmk_gui.exe' 109 | options: --onefile, --name "qmk_gui", --windowed, --add-data "releases;releases" --icon=res/logo_cropped_transparent_keyboard_48x48.ico --add-data 'res;res' -p python/qmk_hid 110 | 111 | package-python: 112 | name: Package Python 113 | runs-on: ubuntu-24.04 114 | steps: 115 | - uses: actions/checkout@v4 116 | 117 | - name: Build 118 | run: | 119 | cd python 120 | python3 -m venv venv 121 | source venv/bin/activate 122 | python3 -m pip install --upgrade build 123 | python3 -m pip install --upgrade hatch 124 | python3 -m pip install --upgrade twine 125 | python3 -m build 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | releases/ 3 | 4 | # Python 5 | __pycache__ 6 | venv/ 7 | 8 | # pyinstaller 9 | qmk_gui*.spec 10 | build/ 11 | dist/ 12 | 13 | # Hatch 14 | _version.py 15 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.15" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.8" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 40 | dependencies = [ 41 | "windows-sys 0.52.0", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys 0.52.0", 52 | ] 53 | 54 | [[package]] 55 | name = "cc" 56 | version = "1.1.10" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" 59 | 60 | [[package]] 61 | name = "cfg-if" 62 | version = "1.0.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 65 | 66 | [[package]] 67 | name = "clap" 68 | version = "4.5.15" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" 71 | dependencies = [ 72 | "clap_builder", 73 | "clap_derive", 74 | ] 75 | 76 | [[package]] 77 | name = "clap_builder" 78 | version = "4.5.15" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" 81 | dependencies = [ 82 | "anstream", 83 | "anstyle", 84 | "clap_lex", 85 | "strsim", 86 | ] 87 | 88 | [[package]] 89 | name = "clap_derive" 90 | version = "4.5.13" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 93 | dependencies = [ 94 | "heck", 95 | "proc-macro2", 96 | "quote", 97 | "syn", 98 | ] 99 | 100 | [[package]] 101 | name = "clap_lex" 102 | version = "0.7.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 105 | 106 | [[package]] 107 | name = "colorchoice" 108 | version = "1.0.2" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 111 | 112 | [[package]] 113 | name = "heck" 114 | version = "0.5.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 117 | 118 | [[package]] 119 | name = "hidapi" 120 | version = "2.6.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "9e58251020fe88fe0dae5ebcc1be92b4995214af84725b375d08354d0311c23c" 123 | dependencies = [ 124 | "cc", 125 | "cfg-if", 126 | "libc", 127 | "pkg-config", 128 | "windows-sys 0.48.0", 129 | ] 130 | 131 | [[package]] 132 | name = "is_terminal_polyfill" 133 | version = "1.70.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 136 | 137 | [[package]] 138 | name = "libc" 139 | version = "0.2.155" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 142 | 143 | [[package]] 144 | name = "pkg-config" 145 | version = "0.3.30" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 148 | 149 | [[package]] 150 | name = "proc-macro2" 151 | version = "1.0.86" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 154 | dependencies = [ 155 | "unicode-ident", 156 | ] 157 | 158 | [[package]] 159 | name = "qmk_hid" 160 | version = "0.1.12" 161 | dependencies = [ 162 | "clap", 163 | "hidapi", 164 | "static_vcruntime", 165 | ] 166 | 167 | [[package]] 168 | name = "quote" 169 | version = "1.0.36" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 172 | dependencies = [ 173 | "proc-macro2", 174 | ] 175 | 176 | [[package]] 177 | name = "static_vcruntime" 178 | version = "2.0.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b" 181 | 182 | [[package]] 183 | name = "strsim" 184 | version = "0.11.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 187 | 188 | [[package]] 189 | name = "syn" 190 | version = "2.0.74" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "unicode-ident", 197 | ] 198 | 199 | [[package]] 200 | name = "unicode-ident" 201 | version = "1.0.12" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 204 | 205 | [[package]] 206 | name = "utf8parse" 207 | version = "0.2.2" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 210 | 211 | [[package]] 212 | name = "windows-sys" 213 | version = "0.48.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 216 | dependencies = [ 217 | "windows-targets 0.48.5", 218 | ] 219 | 220 | [[package]] 221 | name = "windows-sys" 222 | version = "0.52.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 225 | dependencies = [ 226 | "windows-targets 0.52.6", 227 | ] 228 | 229 | [[package]] 230 | name = "windows-targets" 231 | version = "0.48.5" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 234 | dependencies = [ 235 | "windows_aarch64_gnullvm 0.48.5", 236 | "windows_aarch64_msvc 0.48.5", 237 | "windows_i686_gnu 0.48.5", 238 | "windows_i686_msvc 0.48.5", 239 | "windows_x86_64_gnu 0.48.5", 240 | "windows_x86_64_gnullvm 0.48.5", 241 | "windows_x86_64_msvc 0.48.5", 242 | ] 243 | 244 | [[package]] 245 | name = "windows-targets" 246 | version = "0.52.6" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 249 | dependencies = [ 250 | "windows_aarch64_gnullvm 0.52.6", 251 | "windows_aarch64_msvc 0.52.6", 252 | "windows_i686_gnu 0.52.6", 253 | "windows_i686_gnullvm", 254 | "windows_i686_msvc 0.52.6", 255 | "windows_x86_64_gnu 0.52.6", 256 | "windows_x86_64_gnullvm 0.52.6", 257 | "windows_x86_64_msvc 0.52.6", 258 | ] 259 | 260 | [[package]] 261 | name = "windows_aarch64_gnullvm" 262 | version = "0.48.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 265 | 266 | [[package]] 267 | name = "windows_aarch64_gnullvm" 268 | version = "0.52.6" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 271 | 272 | [[package]] 273 | name = "windows_aarch64_msvc" 274 | version = "0.48.5" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 277 | 278 | [[package]] 279 | name = "windows_aarch64_msvc" 280 | version = "0.52.6" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 283 | 284 | [[package]] 285 | name = "windows_i686_gnu" 286 | version = "0.48.5" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 289 | 290 | [[package]] 291 | name = "windows_i686_gnu" 292 | version = "0.52.6" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 295 | 296 | [[package]] 297 | name = "windows_i686_gnullvm" 298 | version = "0.52.6" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 301 | 302 | [[package]] 303 | name = "windows_i686_msvc" 304 | version = "0.48.5" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 307 | 308 | [[package]] 309 | name = "windows_i686_msvc" 310 | version = "0.52.6" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 313 | 314 | [[package]] 315 | name = "windows_x86_64_gnu" 316 | version = "0.48.5" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 319 | 320 | [[package]] 321 | name = "windows_x86_64_gnu" 322 | version = "0.52.6" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 325 | 326 | [[package]] 327 | name = "windows_x86_64_gnullvm" 328 | version = "0.48.5" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 331 | 332 | [[package]] 333 | name = "windows_x86_64_gnullvm" 334 | version = "0.52.6" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 337 | 338 | [[package]] 339 | name = "windows_x86_64_msvc" 340 | version = "0.48.5" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 343 | 344 | [[package]] 345 | name = "windows_x86_64_msvc" 346 | version = "0.52.6" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 349 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qmk_hid" 3 | version = "0.1.12" 4 | edition = "2021" 5 | license = "BSD-3-Clause" 6 | description = "Commandline tool to interact with QMK devices via their raw HID interface" 7 | authors = ["Daniel Schaefer "] 8 | keywords = ["hid", "qmk", "keyboard"] 9 | repository = "https://github.com/FrameworkComputer/qmk_hid" 10 | 11 | [dependencies] 12 | clap = { version = "4.5", features = ["derive"] } 13 | # hidapi 2.6.2 needs nightly 14 | # See: https://github.com/ruabmbua/hidapi-rs/pull/158 15 | hidapi = { version = "=2.6.1" } 16 | 17 | [build-dependencies] 18 | static_vcruntime = "2.0" 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Framework Computer Inc 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QMK HID 2 | 3 | Commandline tool to interact with QMK devices via their raw HID interface. 4 | 5 | Currently focusing on the VIA API. 6 | It will soon be superceded by QMK XAP, but that isn't ready yet. 7 | 8 | Tested to work on Windows and Linux, without any drivers or admin privileges. 9 | 10 | ### GUI 11 | 12 | There is also an easy to use GUI tool that does not require commandline interaction. 13 | 14 | See [GUI README](python/README.md) 15 | 16 | ![](screenshots/qmk_gui_screenshot.png) 17 | 18 | ### Supported devices 19 | 20 | The tool is generic and works for any device using [QMK Firmware](https://qmk.fm/). 21 | 22 | But it was built for, and some functionality only works with, 23 | [Framework 16 keyboard modules](https://frame.work/tw/en/products/laptop16-diy-amd-7040?tab=modules). 24 | This includes all keyboard variants, the numpad and macropad. 25 | 26 | ## Running 27 | 28 | Download the latest binary from the [releases page](https://github.com/FrameworkComputer/qmk_hid/releases). 29 | 30 | The examples call the binary with the name `qmk_hid`, as used on Linux. 31 | If you're on Windows, use `qmk_hid.exe`, and when building from source, 32 | use `cargo run --`. 33 | 34 | ###### Show the help 35 | 36 | ```sh 37 | > qmk_hid 38 | RAW HID and VIA commandline for QMK devices 39 | 40 | Usage: qmk_hid [OPTIONS] [COMMAND] 41 | 42 | Commands: 43 | via Via 44 | qmk QMK 45 | help Print this message or the help of the given subcommand(s) 46 | 47 | Options: 48 | -l, --list List connected HID devices 49 | -v, --verbose Verbose outputs to the console 50 | --vid VID (Vendor ID) in hex digits 51 | --pid PID (Product ID) in hex digits 52 | -h, --help Print help information 53 | -V, --version Print version information 54 | 55 | > qmk_hid via 56 | Via 57 | 58 | Usage: qmk_hid via [OPTIONS] 59 | 60 | Options: 61 | --info 62 | Get VIA protocol and config information (most likely NOT what you're looking for) 63 | --device-indication 64 | Flash device indication (backlight) 3x 65 | --rgb-brightness [] 66 | Set RGB brightness percentage or get, if no value provided 67 | --rgb-effect [] 68 | Set RGB effect or get, if no value provided 69 | --rgb-effect-speed [] 70 | Set RGB effect speed or get, if no value provided (0-255) 71 | --rgb-hue [] 72 | Set RGB hue or get, if no value provided. (0-255) 73 | --rgb-color 74 | Set RGB color [possible values: red, yellow, green, cyan, blue, purple, white] 75 | --rgb-saturation [] 76 | Set RGB saturation or get, if no value provided. (0-255) 77 | --backlight [] 78 | Set backlight brightness percentage or get, if no value provided 79 | --backlight-breathing [] 80 | Set backlight breathing or get, if no value provided [possible values: true, false] 81 | --save 82 | Save RGB/backlight value, otherwise it won't persist through keyboard reboot. Can be used by itself or together with other argument 83 | --eeprom-reset 84 | Reset the EEPROM contents (Not supported by all firmware) 85 | --bootloader 86 | Jump to the bootloader (Not supported by all firmware) 87 | -h, --help 88 | Print help information 89 | 90 | > qmk_hid qmk 91 | QMK 92 | 93 | Usage: qmk_hid qmk [OPTIONS] 94 | 95 | Options: 96 | -c, --console Listen to the console. Better to use `qmk console` (https://github.com/qmk/qmk_cli) 97 | -h, --help Print help information 98 | ``` 99 | 100 | ###### List available devices 101 | ```sh 102 | > qmk_hid -l 103 | 32ac:0014 104 | Manufacturer: "Framework Computer Inc" 105 | Product: "Framework 16 Numpad" 106 | FW Version: 0.1.3 107 | Serial No: "FRALDLENA100000000" 108 | ``` 109 | 110 | ###### Control that device 111 | 112 | ```sh 113 | # If there is only one device, no filter needed 114 | > qmk_hid via --backlight 115 | Brightness: 0% 116 | 117 | # If there are multiple devices, need to filter by either VID, PID or both 118 | > qmk_hid via --backlight 119 | More than 1 device found. Select a specific device with --vid and --pid 120 | > qmk_hid --vid 3434 via --backlight 121 | Brightness: 0% 122 | 123 | # Get current RGB brightness 124 | > qmk_hid via --rgb-brightness 50 125 | Brightness: 50% 126 | 127 | # Set new RGB brightness 128 | > qmk_hid via --rgb-brightness 100 129 | Brightness: 100% 130 | ``` 131 | 132 | **NOTE:** By default the settings are not saved. To make them persistent add 133 | the `--save` argument. Or run `qmk_hid via --save` by itself. Examples: 134 | 135 | ``` 136 | # Save directly 137 | > qmk_hid via --rgb-brightness 100 --save 138 | 139 | # Make a couple changes and save everything 140 | > qmk_hid via --rgb-effect 1 141 | > qmk_hid via --rgb-color red 142 | > qmk_hid via --rgb-brightness 100 143 | > qmk_hid via --save 144 | ``` 145 | 146 | ###### Jumping to the bootloader, to reflash. 147 | 148 | Note: This will only work when the QMK firmware has this command enabled. This 149 | is not the default upstream behavior. 150 | 151 | ```sh 152 | > qmk_hid via --bootloader 153 | ``` 154 | 155 | ###### Reset EEPROM contents / Clear VIA config 156 | 157 | VIA stores its config in EEPROM (sometimes emulated in flash). 158 | When using a different keyboard with the same controller board you'll want to 159 | clear it, otherwise the previously stored VIA config overrides the hardcoded 160 | one. 161 | 162 | This becomes obvious when trying to change the hardcoded keymap but the 163 | behavior does not change. 164 | 165 | The command only does something when the firmware has `VIA_EEPROM_ALLOW_RESET` defined. 166 | 167 | ```sh 168 | > qmk_hid via --eeprom-config 169 | ``` 170 | 171 | ###### Testing the RGB LEDs 172 | 173 | ```sh 174 | # Use "device indication" to flash backlight 3 times 175 | qmk_hid via --device-indication 176 | 177 | # Turn RGB off 178 | qmk_hid via --rgb-effect 0 179 | 180 | # Turn all LEDs on 181 | qmk_hid via --rgb-effect 1 182 | 183 | # Change color 184 | qmk_hid via --rgb-color red 185 | qmk_hid via --rgb-color yellow 186 | qmk_hid via --rgb-color green 187 | qmk_hid via --rgb-color cyan 188 | qmk_hid via --rgb-color blue 189 | qmk_hid via --rgb-color purple 190 | qmk_hid via --rgb-color white 191 | 192 | # Enable a mode that reacts to keypresses 193 | # Note that the effect numbers can be different per keyboard 194 | # On Framework 16 we currently enable all, then 38 is `SOLID_REACTIVE_MULTICROSS` 195 | qmk_hid via --rgb-effect 38 196 | ``` 197 | 198 | ## Building from source 199 | 200 | Pre-requisites: 201 | 202 | - [Rust](https://rustup.rs/) 203 | - libudev (`libudev-dev` on Ubuntu, `systemd-devel` on Fedora) 204 | 205 | ```sh 206 | # Directly run 207 | cargo run 208 | 209 | # Build and run executable (on Linux) 210 | cargo build 211 | ./target/debug/qmk_hid 212 | ``` 213 | 214 | ## Running on Linux 215 | 216 | To avoid needing root privileges to access the keyboard please follow the 217 | official [QMK guide](https://docs.qmk.fm/#/faq_build?id=linux-udev-rules) for 218 | how to install udev rules. After doing that, you'll be able to interact with 219 | the keyboards as a regular user. 220 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | static_vcruntime::metabuild(); 3 | } 4 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Python 2 | 3 | ## Installing 4 | 5 | Pre-requisites: Python with pip 6 | 7 | ```sh 8 | python3 -m pip install qmk_hid 9 | ``` 10 | 11 | ## GUI 12 | 13 | On Linux install Python requirements via `python3 -m pip install -r requirements.txt` and run `python3 qmk_hid/gui.py`. 14 | On Windows download the `qmk_gui.exe` and run it. 15 | 16 | ## Developing 17 | 18 | One time setup 19 | 20 | ``` 21 | # Install dependencies on Ubuntu 22 | sudo apt install python3 python3-tk python3-devel libhidapi-dev 23 | # Install dependencies on Fedora 24 | sudo dnf install python3 python3-tkinter hidapi-devel 25 | # Create local venv and enter it 26 | python3 -m venv venv 27 | source venv/bin/activate 28 | # Install package into local env 29 | python3 -m pip install -e . 30 | ``` 31 | 32 | Developing 33 | 34 | ``` 35 | # In every new shell, source the virtual environment 36 | source venv/bin/activate 37 | # Launch GUI or commandline 38 | qmk_gui 39 | 40 | # Launch Python REPL and import the library 41 | # As example, launch the GUI 42 | > python3 43 | >>> from qmk_hid import gui 44 | >>> gui.main() 45 | ``` 46 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs", "hatch-requirements-txt"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "qmk_hid" 7 | # TODO: Dynamic version from git (requires tags) 8 | #dynamic = ["version"] 9 | dynamic = ["dependencies"] 10 | version = "0.1.8" 11 | description = 'A GUI tool to control QMK keyboard, specifically of the Framework Laptop 16' 12 | # TODO: Custom README for python project 13 | readme = "README.md" 14 | requires-python = ">=3.7" 15 | license = { text = "MIT" } 16 | keywords = [ 17 | "hatch", 18 | ] 19 | authors = [ 20 | { name = "Daniel Schaefer", email = "dhs@frame.work" }, 21 | ] 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "License :: OSI Approved :: MIT License", 25 | "Programming Language :: Python :: 3", 26 | ] 27 | 28 | [project.urls] 29 | Issues = "https://github.com/FrameworkComputer/qmk_hid/issues" 30 | Source = "https://github.com/FrameworkComputer/qmk_hid" 31 | 32 | # TODO: There's no CLI yet 33 | # [project.scripts] 34 | # qmk_hid = "qmk_hid.gui:main_cli" 35 | 36 | [project.gui-scripts] 37 | qmk_gui = "qmk_hid.gui:main" 38 | 39 | #[tool.hatch.version] 40 | #source = "vcs" 41 | # 42 | #[tool.hatch.build.hooks.vcs] 43 | #version-file = "qmk_hid/_version.py" 44 | 45 | [tool.hatch.metadata.hooks.requirements_txt] 46 | files = ["requirements.txt"] 47 | 48 | [tool.hatch.build.targets.sdist] 49 | exclude = [ 50 | "/.github", 51 | ] 52 | 53 | # TODO: Maybe typing with mypy 54 | # [tool.hatch.build.targets.wheel.hooks.mypyc] 55 | # enable-by-default = false 56 | # dependencies = ["hatch-mypyc>=0.14.1"] 57 | # require-runtime-dependencies = true 58 | # mypy-args = [ 59 | # "--no-warn-unused-ignores", 60 | # ] 61 | # 62 | # [tool.mypy] 63 | # disallow_untyped_defs = false 64 | # follow_imports = "normal" 65 | # ignore_missing_imports = true 66 | # pretty = true 67 | # show_column_numbers = true 68 | # warn_no_return = false 69 | # warn_unused_ignores = true 70 | -------------------------------------------------------------------------------- /python/qmk_hid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrameworkComputer/qmk_hid/5988b109b9e2dfd2f0d50ef365f11bb71d9d0c76/python/qmk_hid/__init__.py -------------------------------------------------------------------------------- /python/qmk_hid/__pycache__/uf2conv.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrameworkComputer/qmk_hid/5988b109b9e2dfd2f0d50ef365f11bb71d9d0c76/python/qmk_hid/__pycache__/uf2conv.cpython-311.pyc -------------------------------------------------------------------------------- /python/qmk_hid/firmware_update.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from qmk_hid.protocol import bootloader_jump 5 | from qmk_hid import uf2conv 6 | 7 | def dev_to_str(dev): 8 | return dev['path'] 9 | 10 | def flash_firmware(dev, fw_path): 11 | print(f"Flashing {fw_path} onto {dev_to_str(dev)}") 12 | 13 | # First jump to bootloader 14 | drives = uf2conv.list_drives() 15 | if not drives: 16 | print("Jump to bootloader") 17 | bootloader_jump(dev) 18 | 19 | timeout = 10 # 5s 20 | while not drives: 21 | if timeout == 0: 22 | print("Failed to find device in bootloader") 23 | # TODO: Handle return value 24 | return False 25 | # Wait for it to appear 26 | time.sleep(0.5) 27 | timeout -= 1 28 | drives = uf2conv.get_drives() 29 | 30 | 31 | if len(drives) == 0: 32 | print("No drive to deploy.") 33 | return False 34 | 35 | # Firmware is pretty small, can just fit it all into memory 36 | with open(fw_path, 'rb') as f: 37 | fw_buf = f.read() 38 | 39 | for d in drives: 40 | print("Flashing {} ({})".format(d, uf2conv.board_id(d))) 41 | uf2conv.write_file(d + "/NEW.UF2", fw_buf) 42 | 43 | print("Flashing finished") 44 | 45 | 46 | # Example return value 47 | # { 48 | # '0.1.7': { 49 | # 'ansi': 'framework_ansi_default_v0.1.7.uf2', 50 | # 'gridpad': 'framework_gridpad_default_v0.1.7.uf2' 51 | # }, 52 | # '0.1.8': { 53 | # 'ansi': 'framework_ansi_default.uf2', 54 | # 'gridpad': 'framework_gridpad_default.uf2', 55 | # } 56 | # } 57 | def find_releases(res_path, filename_format): 58 | from os import listdir 59 | from os.path import isfile, join 60 | import re 61 | 62 | releases = {} 63 | try: 64 | versions = listdir(os.path.join(res_path, "releases")) 65 | except FileNotFoundError: 66 | return releases 67 | 68 | for version in versions: 69 | path = join(res_path, "releases", version) 70 | releases[version] = {} 71 | for filename in listdir(path): 72 | if not isfile(join(path, filename)): 73 | continue 74 | type_search = re.search(filename_format, filename) 75 | if not type_search: 76 | print(f"Filename '{filename}' not matching patten!") 77 | sys.exit(1) 78 | continue 79 | fw_type = type_search.group(1) 80 | releases[version][fw_type] = os.path.join(res_path, "releases", version, filename) 81 | return releases 82 | -------------------------------------------------------------------------------- /python/qmk_hid/gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import subprocess 5 | import time 6 | 7 | import tkinter as tk 8 | from tkinter import ttk, messagebox 9 | 10 | if os.name == 'nt': 11 | from win32api import GetKeyState, keybd_event 12 | from win32con import VK_NUMLOCK, VK_CAPITAL 13 | import winreg 14 | 15 | import webbrowser 16 | 17 | from qmk_hid.protocol import * 18 | from qmk_hid import firmware_update 19 | 20 | # TODO: 21 | # - Get current values 22 | # - Set sliders to current values 23 | 24 | PROGRAM_VERSION = "0.2.0" 25 | 26 | DEBUG_PRINT = False 27 | 28 | def debug_print(*args): 29 | if DEBUG_PRINT: 30 | print(args) 31 | 32 | def format_fw_ver(fw_ver): 33 | fw_ver_major = (fw_ver & 0xFF00) >> 8 34 | fw_ver_minor = (fw_ver & 0x00F0) >> 4 35 | fw_ver_patch = (fw_ver & 0x000F) 36 | return f"{fw_ver_major}.{fw_ver_minor}.{fw_ver_patch}" 37 | 38 | 39 | def get_numlock_state(): 40 | if os.name == 'nt': 41 | return GetKeyState(VK_NUMLOCK) 42 | else: 43 | try: 44 | # TODO: This doesn't work on wayland 45 | # In GNOME we can do gsettings set org.gnome.settings-daemon.peripherals.keyboard numlock-state on 46 | output = subprocess.run(['numlockx', 'status'], stdout=subprocess.PIPE).stdout 47 | if b'on' in output: 48 | return True 49 | elif b'off' in output: 50 | return False 51 | except FileNotFoundError: 52 | # Ignore tool not found, just return None 53 | pass 54 | 55 | def main(): 56 | devices = find_devs(show=False, verbose=False) 57 | # print("Found {} devices".format(len(devices))) 58 | 59 | root = tk.Tk() 60 | root.title("QMK Keyboard Control") 61 | ico = "logo_cropped_transparent_keyboard_48x48.ico" 62 | res_path = resource_path() 63 | if os.name == 'nt': 64 | root.iconbitmap(f"{res_path}/res/{ico}") 65 | 66 | tabControl = ttk.Notebook(root) 67 | tab1 = ttk.Frame(tabControl) 68 | tab_fw_update = ttk.Frame(tabControl) 69 | tab2 = ttk.Frame(tabControl) 70 | tabControl.add(tab1, text="Home") 71 | tabControl.add(tab_fw_update, text="Firmware Update") 72 | tabControl.add(tab2, text="Advanced") 73 | tabControl.pack(expand=1, fill="both") 74 | 75 | # Device Checkboxes 76 | detected_devices_frame = ttk.LabelFrame(tab1, text="Detected Devices", style="TLabelframe") 77 | detected_devices_frame.pack(fill="x", padx=10, pady=5) 78 | 79 | global device_checkboxes 80 | device_checkboxes = {} 81 | for dev in devices: 82 | device_info = "{}\nSerial No: {}\nFW Version: {}\n".format( 83 | dev['product_string'], 84 | dev['serial_number'], 85 | format_fw_ver(dev['release_number']) 86 | ) 87 | checkbox_var = tk.BooleanVar(value=True) 88 | checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton") 89 | checkbox.pack(anchor="w") 90 | device_checkboxes[dev['path']] = (checkbox_var, checkbox) 91 | 92 | # Online Info 93 | info_frame = ttk.LabelFrame(tab1, text="Online Info", style="TLabelframe") 94 | info_frame.pack(fill="x", padx=10, pady=5) 95 | infos = { 96 | "VIA Web Interface": "https://keyboard.frame.work", 97 | "Firmware Releases": "https://github.com/FrameworkComputer/qmk_firmware/releases", 98 | "Tool Releases": "https://github.com/FrameworkComputer/qmk_hid/releases", 99 | "Keyboard Hotkeys": "https://knowledgebase.frame.work/hotkeys-on-the-framework-laptop-16-keyboard-rkYIwFQPp", 100 | "Macropad Layout": "https://knowledgebase.frame.work/default-keymap-for-the-rgb-macropad-rkBIgqmva", 101 | "Numpad Layout": "https://knowledgebase.frame.work/default-keymap-for-the-numpad-rJZv44owa", 102 | } 103 | for (i, (text, url)) in enumerate(infos.items()): 104 | # Organize in columns of three 105 | row = int(i / 3) 106 | column = i % 3 107 | btn = ttk.Button(info_frame, text=text, command=lambda url=url: open_browser_func(url), style="TButton") 108 | btn.grid(row=row, column=column) 109 | 110 | # Device Control Buttons 111 | device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") 112 | device_control_frame.pack(fill="x", padx=10, pady=5) 113 | control_buttons = { 114 | "Bootloader": "bootloader", 115 | "Save Changes": "save_changes", 116 | } 117 | for text, action in control_buttons.items(): 118 | ttk.Button(device_control_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) 119 | 120 | # Brightness Slider 121 | brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") 122 | brightness_frame.pack(fill="x", padx=10, pady=5) 123 | global brightness_scale 124 | brightness_scale = tk.Scale(brightness_frame, from_=0, to=255, orient='horizontal', command=lambda value: perform_action(devices, 'brightness', value=int(value))) 125 | brightness_scale.set(120) # Default value 126 | brightness_scale.pack(fill="x", padx=5, pady=5) 127 | 128 | # RGB color 129 | rgb_color_buttons = { 130 | "Red": "red", 131 | "Green": "green", 132 | "Blue": "blue", 133 | "White": "white", 134 | "Off": "off", 135 | } 136 | btn_frame = ttk.Frame(brightness_frame) 137 | btn_frame.pack(side=tk.TOP) 138 | for text, action in rgb_color_buttons.items(): 139 | btn = ttk.Button(btn_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton") 140 | btn.pack(side="left", padx=5, pady=5) 141 | 142 | # RGB Effect Combo Box 143 | rgb_effect_label = tk.Label(brightness_frame, text="RGB Effect") 144 | rgb_effect_label.pack(side=tk.LEFT, padx=5, pady=5) 145 | rgb_effect_combo = ttk.Combobox(brightness_frame, values=RGB_EFFECTS, style="TCombobox", state="readonly") 146 | rgb_effect_combo.pack(side=tk.LEFT, padx=5, pady=5) 147 | rgb_effect_combo.bind("<>", lambda event: perform_action(devices, 'rgb_effect', value=RGB_EFFECTS.index(rgb_effect_combo.get()))) 148 | 149 | # White backlight keyboard 150 | rgb_effect_label = tk.Label(brightness_frame, text="White Effect") 151 | rgb_effect_label.pack(side=tk.LEFT, padx=5, pady=5) 152 | ttk.Button(brightness_frame, text="Breathing", command=lambda a=action: perform_action(devices, "breathing_on"), style="TButton").pack(side="left", padx=5, pady=5) 153 | ttk.Button(brightness_frame, text="None", command=lambda a=action: perform_action(devices, "breathing_off"), style="TButton").pack(side="left", padx=5, pady=5) 154 | 155 | # Tab 2 156 | # Advanced Device Control Buttons 157 | eeprom_frame = ttk.LabelFrame(tab2, text="EEPROM", style="TLabelframe") 158 | eeprom_frame.pack(fill="x", padx=5, pady=5) 159 | tk.Label(eeprom_frame, text="Clear user configured settings").pack(side="top", padx=5, pady=5) 160 | ttk.Button(eeprom_frame, text="Reset EEPROM", command=lambda: perform_action(devices, 'reset_eeprom'), style="TButton").pack(side="left", padx=5, pady=5) 161 | 162 | bios_mode_frame = ttk.LabelFrame(tab2, text="BIOS Mode", style="TLabelframe") 163 | bios_mode_frame.pack(fill="x", padx=5, pady=5) 164 | tk.Label(bios_mode_frame, text="Disable function buttons, force F1-12").pack(side="top", padx=5, pady=5) 165 | ttk.Button(bios_mode_frame, text="Enable", command=lambda: perform_action(devices, 'bios_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) 166 | ttk.Button(bios_mode_frame, text="Disable", command=lambda: perform_action(devices, 'bios_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) 167 | 168 | factory_mode_frame = ttk.LabelFrame(tab2, text="Factory Mode", style="TLabelframe") 169 | factory_mode_frame.pack(fill="x", padx=5, pady=5) 170 | tk.Label(factory_mode_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) 171 | ttk.Button(factory_mode_frame, text="Enable", command=lambda: perform_action(devices, 'factory_mode', value=True), style="TButton").pack(side="left", padx=5, pady=5) 172 | ttk.Button(factory_mode_frame, text="Disable", command=lambda: perform_action(devices, 'factory_mode', value=False), style="TButton").pack(side="left", padx=5, pady=5) 173 | 174 | # Unreliable on Linux 175 | # Different versions of numlockx behave differently 176 | # Xorg vs Wayland is different 177 | if os.name == 'nt': 178 | numlock_frame = ttk.LabelFrame(tab2, text="OS Numlock Setting", style="TLabelframe") 179 | numlock_frame.pack(fill="x", padx=5, pady=5) 180 | numlock_state_var = tk.StringVar() 181 | numlock_state_var.set("State: Unknown") 182 | numlock_state_label = tk.Label(numlock_frame, textvariable=numlock_state_var).pack(side="top", padx=5, pady=5) 183 | refresh_btn = ttk.Button(numlock_frame, text="Refresh", command=lambda: update_numlock_state(numlock_state_var), style="TButton", state=tk.DISABLED) 184 | refresh_btn.pack(side="left", padx=5, pady=5) 185 | toggle_btn = ttk.Button(numlock_frame, text="Emulate numlock button press", command=lambda: toggle_numlock(), style="TButton", state=tk.DISABLED) 186 | toggle_btn.pack(side="left", padx=5, pady=5) 187 | 188 | update_numlock_state(numlock_state_var, refresh_btn, toggle_btn) 189 | 190 | # TODO: Maybe hide behind secret shortcut 191 | if os.name == 'nt': 192 | registry_frame = ttk.LabelFrame(tab2, text="Windows Registry Tweaks", style="TLabelframe") 193 | registry_frame.pack(fill="x", padx=5, pady=5) 194 | tk.Label(registry_frame, text="Disabled. Only for very advanced debugging").pack(side="top", padx=5, pady=5) 195 | ttk.Button(registry_frame, text="Enable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, True), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) 196 | toggle_btn = ttk.Button(registry_frame, text="Disable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, False), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) 197 | 198 | # Only in the pyinstaller bundle are the FW update binaries included 199 | releases = firmware_update.find_releases(resource_path(), r'framework_(.*)_default.*\.uf2') 200 | if not releases: 201 | tk.Label(tab_fw_update, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5) 202 | else: 203 | versions = sorted(list(releases.keys()), reverse=True) 204 | 205 | flash_btn = None 206 | fw_type_combo = None 207 | 208 | fw_update_frame = ttk.LabelFrame(tab_fw_update, text="Update Firmware", style="TLabelframe") 209 | fw_update_frame.pack(fill="x", padx=5, pady=5) 210 | #tk.Label(fw_update_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) 211 | fw_ver_combo = ttk.Combobox(fw_update_frame, values=versions, style="TCombobox", state="readonly") 212 | fw_ver_combo.pack(side=tk.LEFT, padx=5, pady=5) 213 | fw_ver_combo.current(0) 214 | fw_ver_combo.bind("<>", lambda event: select_fw_version(fw_ver_combo.get(), fw_type_combo, releases)) 215 | fw_type_combo = ttk.Combobox(fw_update_frame, values=list(releases[versions[0]]), style="TCombobox", state="readonly") 216 | fw_type_combo.pack(side=tk.LEFT, padx=5, pady=5) 217 | fw_type_combo.bind("<>", lambda event: select_fw_type(fw_type_combo.get(), flash_btn)) 218 | flash_btn = ttk.Button(fw_update_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), fw_type_combo.get()), state=tk.DISABLED, style="TButton") 219 | flash_btn.pack(side="left", padx=5, pady=5) 220 | 221 | program_ver_label = tk.Label(tab1, text=f"Program Version: {PROGRAM_VERSION}") 222 | program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) 223 | 224 | root.mainloop() 225 | 226 | def update_numlock_state(state_var, refresh_btn=None, toggle_btn=None): 227 | numlock_on = get_numlock_state() 228 | if numlock_on is None and os != 'nt': 229 | state_var.set("Unknown, please install the 'numlockx' command") 230 | else: 231 | if refresh_btn: 232 | refresh_btn.config(state=tk.NORMAL) 233 | if toggle_btn: 234 | toggle_btn.config(state=tk.NORMAL) 235 | state_var.set("On (Numbers)" if numlock_on else "Off (Arrows)") 236 | 237 | 238 | def toggle_numlock(): 239 | if os.name == 'nt': 240 | keybd_event(VK_NUMLOCK, 0x3A, 0x1, 0) 241 | keybd_event(VK_NUMLOCK, 0x3A, 0x3, 0) 242 | else: 243 | out = subprocess.check_output(['numlockx', 'toggle']) 244 | 245 | 246 | def is_pyinstaller(): 247 | return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') 248 | 249 | 250 | def resource_path(): 251 | """ Get absolute path to resource, works for dev and for PyInstaller""" 252 | try: 253 | # PyInstaller creates a temp folder and stores path in _MEIPASS 254 | base_path = sys._MEIPASS 255 | except Exception: 256 | base_path = os.path.abspath(".") 257 | 258 | return base_path 259 | 260 | # TODO: Possibly use this 261 | def backlight_watcher(window, devs): 262 | prev_brightness = {} 263 | while True: 264 | for dev in devs: 265 | brightness = get_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS) 266 | rgb_brightness = get_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS) 267 | 268 | br_changed = False 269 | rgb_br_changed = False 270 | if dev['path'] in prev_brightness: 271 | if brightness != prev_brightness[dev['path']]['brightness']: 272 | debug_print("White Brightness Changed") 273 | br_changed = True 274 | if rgb_brightness != prev_brightness[dev['path']]['rgb_brightness']: 275 | debug_print("RGB Brightness Changed") 276 | rgb_br_changed = True 277 | prev_brightness[dev['path']] = { 278 | 'brightness': brightness, 279 | 'rgb_brightness': rgb_brightness, 280 | } 281 | 282 | if br_changed or rgb_br_changed: 283 | # Update other keyboards 284 | new_brightness = brightness if br_changed else rgb_brightness 285 | debug_print("Updating based on {}".format(dev['product_string'])) 286 | debug_print("Update other keyboards to: {:02.2f}% ({})".format(new_brightness * 100 / 255, new_brightness)) 287 | for other_dev in devs: 288 | debug_print("Updating {}".format(other_dev['product_string'])) 289 | if dev['path'] != other_dev['path']: 290 | set_brightness(other_dev, new_brightness) 291 | set_rgb_brightness(other_dev, new_brightness) 292 | #time.sleep(1) 293 | # Avoid it triggering an update in the other direction 294 | prev_brightness[other_dev['path']] = { 295 | 'brightness': get_backlight(other_dev, BACKLIGHT_VALUE_BRIGHTNESS), 296 | 'rgb_brightness': get_rgb_u8(other_dev, RGB_MATRIX_VALUE_BRIGHTNESS), 297 | } 298 | debug_print() 299 | # Avoid high CPU usage 300 | time.sleep(1) 301 | 302 | 303 | def restart_hint(): 304 | parent = tk.Tk() 305 | parent.title("Restart Application") 306 | message = tk.Message(parent, text="After updating a device,\n restart the application to reload the connections.", width=800) 307 | message.pack(padx=20, pady=20) 308 | parent.mainloop() 309 | 310 | def info_popup(msg): 311 | parent = tk.Tk() 312 | parent.title("Info") 313 | message = tk.Message(parent, text=msg, width=800) 314 | message.pack(padx=20, pady=20) 315 | parent.mainloop() 316 | 317 | 318 | def replug_hint(): 319 | parent = tk.Tk() 320 | parent.title("Replug Keyboard") 321 | message = tk.Message(parent, text="After changing selective suspend setting, make sure to unplug and re-plug the device to apply the settings.", width=800) 322 | message.pack(padx=20, pady=20) 323 | parent.mainloop() 324 | 325 | 326 | 327 | def selective_suspend_wrapper(dev, enable): 328 | if enable: 329 | selective_suspend_registry(dev['product_id'], False, set=True) 330 | replug_hint() 331 | else: 332 | selective_suspend_registry(dev['product_id'], False, set=False) 333 | replug_hint() 334 | 335 | 336 | def selective_suspend_registry(pid, verbose, set=None): 337 | # The set of keys we care about (under HKEY_LOCAL_MACHINE) are 338 | # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013\Device Parameters\SelectiveSuspendEnabled 339 | # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013&MI_00\Device Parameters\SelectiveSuspendEnabled 340 | # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013&MI_01\Device Parameters\SelectiveSuspendEnabled 341 | # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013&MI_02\Device Parameters\SelectiveSuspendEnabled 342 | # SYSTEM\CurrentControlSet\Enum\USB\VID_32AC&PID_0013&MI_03\Device Parameters\SelectiveSuspendEnabled 343 | # Where 0013 is the USB PID 344 | # 345 | # Additionally 346 | # SYSTEM\CurrentControlSet\Control\usbflags\32AC00130026\osvc 347 | # Where 32AC is the VID, 0013 is the PID, 0026 is the bcdDevice (version) 348 | long_pid = "{:0>4X}".format(pid) 349 | aReg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 350 | 351 | if set is not None: 352 | if set: 353 | print("Setting SelectiveSuspendEnabled to ENABLE") 354 | else: 355 | print("Setting SelectiveSuspendEnabled to DISABLE") 356 | 357 | for mi in ['', '&MI_00', '&MI_01', '&MI_02', '&MI_03']: 358 | dev = f'VID_32AC&PID_{long_pid}' 359 | print(dev) 360 | parent_name = r'SYSTEM\CurrentControlSet\Enum\USB\\' + dev + mi 361 | try: 362 | aKey = winreg.OpenKey(aReg, parent_name) 363 | except EnvironmentError as e: 364 | raise e 365 | #continue 366 | numSubkeys, numValues, lastModified = winreg.QueryInfoKey(aKey) 367 | if verbose: 368 | print(dev)#, numSubkeys, numValues, lastModified) 369 | for i in range(numSubkeys): 370 | try: 371 | aValue_name = winreg.EnumKey(aKey, i) 372 | if verbose: 373 | print(f' {aValue_name}') 374 | aKey = winreg.OpenKey(aKey, aValue_name) 375 | 376 | with winreg.OpenKey(aKey, 'Device Parameters', access=winreg.KEY_WRITE) as oKey: 377 | if set is not None: 378 | if set: 379 | #winreg.SetValueEx(oKey, 'SelectiveSuspendEnabled', 0, winreg.REG_BINARY, b'\x01') 380 | winreg.SetValueEx(oKey, 'SelectiveSuspendEnabled', 0, winreg.REG_DWORD, 1) 381 | else: 382 | #winreg.SetValueEx(oKey, 'SelectiveSuspendEnabled', 0, winreg.REG_BINARY, b'\x00') 383 | winreg.SetValueEx(oKey, 'SelectiveSuspendEnabled', 0, winreg.REG_DWORD, 0) 384 | 385 | with winreg.OpenKey(aKey, 'Device Parameters', access=winreg.KEY_READ+winreg.KEY_WRITE) as oKey: 386 | (sValue, keyType) = winreg.QueryValueEx(oKey, "SelectiveSuspendEnabled") 387 | if verbose: 388 | if keyType == winreg.REG_DWORD: 389 | print(f' {sValue} (DWORD)') 390 | elif keyType == winreg.REG_BINARY: 391 | print(f' {sValue} (BINARY)') 392 | elif keyType == winreg.REG_NONE: 393 | print(f' {sValue} (NONE)') 394 | else: 395 | print(f' {sValue} (Type: f{keyType})') 396 | except EnvironmentError as e: 397 | raise e 398 | 399 | def disable_devices(devices): 400 | # Disable checkbox of selected devices 401 | for dev in devices: 402 | for path, (checkbox_var, checkbox) in device_checkboxes.items(): 403 | if path == dev['path']: 404 | checkbox_var.set(False) 405 | checkbox.config(state=tk.DISABLED) 406 | 407 | def perform_action(devices, action, value=None): 408 | if action == "bootloader": 409 | disable_devices(devices) 410 | restart_hint() 411 | 412 | if action == "off": 413 | brightness_scale.set(0) 414 | 415 | action_map = { 416 | "bootloader": lambda dev: bootloader_jump(dev), 417 | "save_changes": save, 418 | "eeprom_reset": eeprom_reset, 419 | "bios_mode": lambda dev: bios_mode(dev, value), 420 | "factory_mode": lambda dev: factory_mode(dev, value), 421 | "red": lambda dev: set_rgb_color(dev, RED_HUE, 255), 422 | "green": lambda dev: set_rgb_color(dev, GREEN_HUE, 255), 423 | "blue": lambda dev: set_rgb_color(dev, BLUE_HUE, 255), 424 | "white": lambda dev: set_rgb_color(dev, None, 0), 425 | "off": lambda dev: set_rgb_brightness(dev, 0), 426 | "breathing_on": lambda dev: set_white_effect(dev, True), 427 | "breathing_off": lambda dev: set_white_effect(dev, False), 428 | "brightness": lambda dev: set_white_rgb_brightness(dev, value), 429 | "rgb_effect": lambda dev: set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, value), 430 | } 431 | selected_devices = get_selected_devices(devices) 432 | for dev in selected_devices: 433 | if action in action_map: 434 | action_map[action](dev) 435 | 436 | def get_selected_devices(devices): 437 | return [dev for dev in devices if dev['path'] in device_checkboxes and device_checkboxes[dev['path']][0].get()] 438 | 439 | def set_pattern(devices, pattern_name): 440 | selected_devices = get_selected_devices(devices) 441 | for dev in selected_devices: 442 | pattern(dev, pattern_name) 443 | 444 | def select_fw_version(ver, fw_type_combo, releases): 445 | # After selecting a version, we can list the types of firmware available for this version 446 | types = list(releases[ver]) 447 | fw_type_combo.config(values=types) 448 | fw_type_combo.current(0) 449 | 450 | def select_fw_type(_fw_type, flash_btn): 451 | # Once the user has selected a type, the exact firmware file is known and can be flashed 452 | flash_btn.config(state=tk.NORMAL) 453 | 454 | def tk_flash_firmware(devices, releases, version, fw_type): 455 | selected_devices = get_selected_devices(devices) 456 | if len(selected_devices) != 1: 457 | info_popup('To flash select exactly 1 device.') 458 | return 459 | dev = selected_devices[0] 460 | firmware_update.flash_firmware(dev, releases[version][fw_type]) 461 | # Disable device that we just flashed 462 | disable_devices(devices) 463 | restart_hint() 464 | 465 | if __name__ == "__main__": 466 | main() 467 | -------------------------------------------------------------------------------- /python/qmk_hid/protocol.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import hid 5 | 6 | FWK_VID = 0x32AC 7 | 8 | QMK_INTERFACE = 0x01 9 | RAW_HID_BUFFER_SIZE = 32 10 | 11 | RAW_USAGE_PAGE = 0xFF60 12 | CONSOLE_USAGE_PAGE = 0xFF31 13 | # Generic Desktop 14 | G_DESK_USAGE_PAGE = 0x01 15 | CONSUMER_USAGE_PAGE = 0x0C 16 | 17 | GET_PROTOCOL_VERSION = 0x01 # always 0x01 18 | GET_KEYBOARD_VALUE = 0x02 19 | SET_KEYBOARD_VALUE = 0x03 20 | # DynamicKeymapGetKeycode = 0x04 21 | # DynamicKeymapSetKeycode = 0x05 22 | # DynamicKeymapReset = 0x06 23 | CUSTOM_SET_VALUE = 0x07 24 | CUSTOM_GET_VALUE = 0x08 25 | CUSTOM_SAVE = 0x09 26 | EEPROM_RESET = 0x0A 27 | BOOTLOADER_JUMP = 0x0B 28 | 29 | CHANNEL_CUSTOM = 0 30 | CHANNEL_BACKLIGHT = 1 31 | CHANNEL_RGB_LIGHT = 2 32 | CHANNEL_RGB_MATRIX = 3 33 | CHANNEL_AUDIO = 4 34 | 35 | BACKLIGHT_VALUE_BRIGHTNESS = 1 36 | BACKLIGHT_VALUE_EFFECT = 2 37 | 38 | RGB_MATRIX_VALUE_BRIGHTNESS = 1 39 | RGB_MATRIX_VALUE_EFFECT = 2 40 | RGB_MATRIX_VALUE_EFFECT_SPEED = 3 41 | RGB_MATRIX_VALUE_COLOR = 4 42 | 43 | RED_HUE = 0 44 | YELLOW_HUE = 43 45 | GREEN_HUE = 85 46 | CYAN_HUE = 125 47 | BLUE_HUE = 170 48 | PURPLE_HUE = 213 49 | 50 | RGB_EFFECTS = [ 51 | "Off", 52 | "SOLID_COLOR", 53 | "ALPHAS_MODS", 54 | "GRADIENT_UP_DOWN", 55 | "GRADIENT_LEFT_RIGHT", 56 | "BREATHING", 57 | "BAND_SAT", 58 | "BAND_VAL", 59 | "BAND_PINWHEEL_SAT", 60 | "BAND_PINWHEEL_VAL", 61 | "BAND_SPIRAL_SAT", 62 | "BAND_SPIRAL_VAL", 63 | "CYCLE_ALL", 64 | "CYCLE_LEFT_RIGHT", 65 | "CYCLE_UP_DOWN", 66 | "CYCLE_OUT_IN", 67 | "CYCLE_OUT_IN_DUAL", 68 | "RAINBOW_MOVING_CHEVRON", 69 | "CYCLE_PINWHEEL", 70 | "CYCLE_SPIRAL", 71 | "DUAL_BEACON", 72 | "RAINBOW_BEACON", 73 | "RAINBOW_PINWHEELS", 74 | "RAINDROPS", 75 | "JELLYBEAN_RAINDROPS", 76 | "HUE_BREATHING", 77 | "HUE_PENDULUM", 78 | "HUE_WAVE", 79 | "PIXEL_FRACTAL", 80 | "PIXEL_FLOW", 81 | "PIXEL_RAIN", 82 | "TYPING_HEATMAP", 83 | "DIGITAL_RAIN", 84 | "SOLID_REACTIVE_SIMPLE", 85 | "SOLID_REACTIVE", 86 | "SOLID_REACTIVE_WIDE", 87 | "SOLID_REACTIVE_MULTIWIDE", 88 | "SOLID_REACTIVE_CROSS", 89 | "SOLID_REACTIVE_MULTICROSS", 90 | "SOLID_REACTIVE_NEXUS", 91 | "SOLID_REACTIVE_MULTINEXUS", 92 | "SPLASH", 93 | "MULTISPLASH", 94 | "SOLID_SPLASH", 95 | "SOLID_MULTISPLASH", 96 | ] 97 | 98 | def find_devs(show, verbose): 99 | if verbose: 100 | show = True 101 | 102 | devices = [] 103 | for device_dict in hid.enumerate(): 104 | vid = device_dict["vendor_id"] 105 | pid = device_dict["product_id"] 106 | product = device_dict["product_string"] 107 | manufacturer = device_dict["manufacturer_string"] 108 | sn = device_dict['serial_number'] 109 | interface = device_dict['interface_number'] 110 | path = device_dict['path'] 111 | 112 | if vid != FWK_VID: 113 | if verbose: 114 | print("Vendor ID not matching") 115 | continue 116 | 117 | if interface != QMK_INTERFACE: 118 | if verbose: 119 | print("Interface not matching") 120 | continue 121 | # For some reason on Linux it'll always show usage_page==0 122 | if os.name == 'nt' and device_dict['usage_page'] not in [RAW_USAGE_PAGE, CONSOLE_USAGE_PAGE]: 123 | if verbose: 124 | print("Usage Page not matching") 125 | continue 126 | # Lots of false positives, so at least skip Framework false positives 127 | if vid == FWK_VID and pid not in [0x12, 0x13, 0x14, 0x18, 0x19]: 128 | if verbose: 129 | print("False positive, device is not allowed") 130 | continue 131 | 132 | fw_ver = device_dict["release_number"] 133 | 134 | if (os.name == 'nt' and device_dict['usage_page'] == RAW_USAGE_PAGE) or verbose: 135 | if show: 136 | print(f"Manufacturer: {manufacturer}") 137 | print(f"Product: {product}") 138 | print("FW Version: {}".format(format_fw_ver(fw_ver))) 139 | print(f"Serial No: {sn}") 140 | 141 | if verbose: 142 | print(f"Path: {path}") 143 | print(f"VID/PID: {vid:02X}:{pid:02X}") 144 | print(f"Interface: {interface}") 145 | # TODO: print Usage Page 146 | print("") 147 | 148 | devices.append(device_dict) 149 | 150 | return devices 151 | 152 | 153 | def send_message(dev, message_id, msg, out_len): 154 | data = [0xFE] * RAW_HID_BUFFER_SIZE 155 | data[0] = 0x00 # NULL report ID 156 | data[1] = message_id 157 | 158 | if msg: 159 | if len(msg) > RAW_HID_BUFFER_SIZE-2: 160 | print("Message too big. BUG. Please report") 161 | sys.exit(1) 162 | for i, x in enumerate(msg): 163 | data[2+i] = x 164 | 165 | try: 166 | # TODO: Do this somewhere outside 167 | h = hid.device() 168 | h.open_path(dev['path']) 169 | #h.set_nonblocking(0) 170 | h.write(data) 171 | 172 | if out_len == 0: 173 | return None 174 | 175 | out_data = h.read(out_len+3) 176 | return out_data 177 | except (IOError, OSError) as ex: 178 | disable_devices([dev]) 179 | debug_print("Error ({}): ".format(dev['path']), ex) 180 | 181 | def set_keyboard_value(dev, value, number): 182 | msg = [value, number] 183 | send_message(dev, SET_KEYBOARD_VALUE, msg, 0) 184 | 185 | def set_rgb_u8(dev, value, value_data): 186 | msg = [CHANNEL_RGB_MATRIX, value, value_data] 187 | send_message(dev, CUSTOM_SET_VALUE, msg, 0) 188 | 189 | # Returns brightness level: x/255 190 | def get_rgb_u8(dev, value): 191 | msg = [CHANNEL_RGB_MATRIX, value] 192 | output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) 193 | if output[0] == 255: # Not RGB 194 | return None 195 | return output[3] 196 | 197 | # Returns (hue, saturation) 198 | def get_rgb_color(dev): 199 | msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR] 200 | output = send_message(dev, CUSTOM_GET_VALUE, msg, 2) 201 | return (output[3], output[4]) 202 | 203 | # Returns brightness level: x/255 204 | def get_backlight(dev, value): 205 | msg = [CHANNEL_BACKLIGHT, value] 206 | output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) 207 | return output[3] 208 | 209 | def set_backlight(dev, value, value_data): 210 | msg = [CHANNEL_BACKLIGHT, value, value_data] 211 | send_message(dev, CUSTOM_SET_VALUE, msg, 0) 212 | 213 | def save(dev): 214 | save_rgb(dev) 215 | save_backlight(dev) 216 | 217 | def save_rgb(dev): 218 | msg = [CHANNEL_RGB_MATRIX] 219 | send_message(dev, CUSTOM_SAVE, msg, 0) 220 | 221 | def save_backlight(dev): 222 | msg = [CHANNEL_BACKLIGHT] 223 | send_message(dev, CUSTOM_SAVE, msg, 0) 224 | 225 | def eeprom_reset(dev): 226 | send_message(dev, EEPROM_RESET, None, 0) 227 | 228 | 229 | def bootloader_jump(dev): 230 | send_message(dev, BOOTLOADER_JUMP, None, 0) 231 | 232 | 233 | def bios_mode(dev, enable): 234 | param = 0x01 if enable else 0x00 235 | send_message(dev, BOOTLOADER_JUMP, [0x05, param], 0) 236 | 237 | 238 | def factory_mode(dev, enable): 239 | param = 0x01 if enable else 0x00 240 | send_message(dev, BOOTLOADER_JUMP, [0x06, param], 0) 241 | 242 | 243 | def set_rgb_brightness(dev, brightness): 244 | set_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS, brightness) 245 | 246 | 247 | def set_brightness(dev, brightness): 248 | set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) 249 | 250 | def set_white_effect(dev, breathing_on): 251 | set_backlight(dev, BACKLIGHT_VALUE_EFFECT, breathing_on) 252 | 253 | # Set both 254 | def set_white_rgb_brightness(dev, brightness): 255 | set_brightness(dev, brightness) 256 | set_rgb_brightness(dev, brightness) 257 | 258 | 259 | def set_rgb_color(dev, hue, saturation): 260 | (cur_hue, cur_sat) = get_rgb_color(dev) 261 | if hue is None: 262 | hue = cur_hue 263 | msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR, hue, saturation] 264 | send_message(dev, CUSTOM_SET_VALUE, msg, 0) 265 | 266 | -------------------------------------------------------------------------------- /python/qmk_hid/uf2conv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-License-Identifier: MIT 4 | # Copyright (c) Microsoft Corporation and others 5 | # Taken from: https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py 6 | # And modified, some changes already upstreamed 7 | 8 | # yapf: disable 9 | import sys 10 | import struct 11 | import subprocess 12 | import re 13 | import os 14 | import os.path 15 | import argparse 16 | import json 17 | 18 | # Don't even need -b. hex has this embedded 19 | # > ./util/uf2conv.py .build/framework_ansi_default.hex -o ansi.uf2 -b 0x10000000 -f rp2040 --convert --blocks-reserved 1 20 | # Converted to 222 blocks 21 | # Converted to uf2, output size: 113664, start address: 0x10000000 22 | # Wrote 113664 bytes to ansi.uf2 23 | # # 113664 / 512 = 222 24 | # 25 | # > ./util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert --blocks-offset 222 26 | # Converted to 1 blocks 27 | # Converted to uf2, output size: 512, start address: 0x100ff000 28 | # Wrote 512 bytes to serial.uf2 29 | 30 | 31 | 32 | UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" 33 | UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected 34 | UF2_MAGIC_END = 0x0AB16F30 # Ditto 35 | 36 | INFO_FILE = "/INFO_UF2.TXT" 37 | 38 | appstartaddr = 0x2000 39 | familyid = 0x0 40 | 41 | 42 | def is_uf2(buf): 43 | w = struct.unpack(" 476: 77 | assert False, "Invalid UF2 data size at " + ptr 78 | newaddr = hd[3] 79 | if (hd[2] & 0x2000) and (currfamilyid == None): 80 | currfamilyid = hd[7] 81 | if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): 82 | currfamilyid = hd[7] 83 | curraddr = newaddr 84 | if familyid == 0x0 or familyid == hd[7]: 85 | appstartaddr = newaddr 86 | print(f" flags: 0x{hd[2]:02x}") 87 | print(f" addr: 0x{hd[3]:02x}") 88 | print(f" len: {hd[4]}") 89 | print(f" block no: {hd[5]}") 90 | print(f" blocks: {hd[6]}") 91 | print(f" size/famid: {hd[7]}") 92 | print() 93 | padding = newaddr - curraddr 94 | if padding < 0: 95 | assert False, "Block out of order at " + ptr 96 | if padding > 10*1024*1024: 97 | assert False, "More than 10M of padding needed at " + ptr 98 | if padding % 4 != 0: 99 | assert False, "Non-word padding size at " + ptr 100 | while padding > 0: 101 | padding -= 4 102 | outp.append(b"\x00\x00\x00\x00") 103 | if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): 104 | outp.append(block[32 : 32 + datalen]) 105 | curraddr = newaddr + datalen 106 | if hd[2] & 0x2000: 107 | if hd[7] in families_found.keys(): 108 | if families_found[hd[7]] > newaddr: 109 | families_found[hd[7]] = newaddr 110 | else: 111 | families_found[hd[7]] = newaddr 112 | if prev_flag == None: 113 | prev_flag = hd[2] 114 | if prev_flag != hd[2]: 115 | all_flags_same = False 116 | if blockno == (numblocks - 1): 117 | print("--- UF2 File Header Info ---") 118 | families = load_families() 119 | for family_hex in families_found.keys(): 120 | family_short_name = "" 121 | for name, value in families.items(): 122 | if value == family_hex: 123 | family_short_name = name 124 | print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex)) 125 | print("Target Address is 0x{:08x}".format(families_found[family_hex])) 126 | if all_flags_same: 127 | print("All block flag values consistent, 0x{:04x}".format(hd[2])) 128 | else: 129 | print("Flags were not all the same") 130 | print("----------------------------") 131 | if len(families_found) > 1 and familyid == 0x0: 132 | outp = [] 133 | appstartaddr = 0x0 134 | return b"".join(outp) 135 | 136 | def convert_to_carray(file_content): 137 | outp = "const unsigned long bindata_len = %d;\n" % len(file_content) 138 | outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" 139 | for i in range(len(file_content)): 140 | if i % 16 == 0: 141 | outp += "\n" 142 | outp += "0x%02x, " % file_content[i] 143 | outp += "\n};\n" 144 | return bytes(outp, "utf-8") 145 | 146 | def convert_to_uf2(file_content, blocks_reserved=0, blocks_offset=0): 147 | global familyid 148 | datapadding = b"" 149 | while len(datapadding) < 512 - 256 - 32 - 4: 150 | datapadding += b"\x00\x00\x00\x00" 151 | numblocks = (len(file_content) + 255) // 256 152 | outp = [] 153 | for blockno in range(numblocks): 154 | ptr = 256 * blockno 155 | chunk = file_content[ptr:ptr + 256] 156 | flags = 0x0 157 | if familyid: 158 | flags |= 0x2000 159 | hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": 243 | drives.append(words[0]) 244 | else: 245 | rootpath = "/media" 246 | if sys.platform == "darwin": 247 | rootpath = "/Volumes" 248 | elif sys.platform == "linux": 249 | tmp = rootpath + "/" + os.environ["USER"] 250 | if os.path.isdir(tmp): 251 | rootpath = tmp 252 | tmp = "/run" + rootpath + "/" + os.environ["USER"] 253 | if os.path.isdir(tmp): 254 | rootpath = tmp 255 | for d in os.listdir(rootpath): 256 | drives.append(os.path.join(rootpath, d)) 257 | 258 | 259 | def has_info(d): 260 | try: 261 | return os.path.isfile(d + INFO_FILE) 262 | except: 263 | return False 264 | 265 | return list(filter(has_info, drives)) 266 | 267 | 268 | def board_id(path): 269 | with open(path + INFO_FILE, mode='r') as file: 270 | file_content = file.read() 271 | return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) 272 | 273 | 274 | def list_drives(): 275 | for d in get_drives(): 276 | print(d, board_id(d)) 277 | 278 | 279 | def write_file(name, buf): 280 | with open(name, "wb") as f: 281 | f.write(buf) 282 | print("Wrote %d bytes to %s" % (len(buf), name)) 283 | 284 | 285 | def load_families(): 286 | # The expectation is that the `uf2families.json` file is in the same 287 | # directory as this script. Make a path that works using `__file__` 288 | # which contains the full path to this script. 289 | filename = "uf2families.json" 290 | pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) 291 | with open(pathname) as f: 292 | raw_families = json.load(f) 293 | 294 | families = {} 295 | for family in raw_families: 296 | families[family["short_name"]] = int(family["id"], 0) 297 | 298 | return families 299 | 300 | 301 | def main(): 302 | global appstartaddr, familyid 303 | def error(msg): 304 | print(msg, file=sys.stderr) 305 | sys.exit(1) 306 | parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') 307 | parser.add_argument('input', metavar='INPUT', type=str, nargs='?', 308 | help='input file (HEX, BIN or UF2)') 309 | parser.add_argument('-b' , '--base', dest='base', type=str, 310 | default="0x2000", 311 | help='set base address of application for BIN format (default: 0x2000)') 312 | parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, 313 | help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') 314 | parser.add_argument('-d' , '--device', dest="device_path", 315 | help='select a device path to flash') 316 | parser.add_argument('-l' , '--list', action='store_true', 317 | help='list connected devices') 318 | parser.add_argument('-c' , '--convert', action='store_true', 319 | help='do not flash, just convert') 320 | parser.add_argument('-D' , '--deploy', action='store_true', 321 | help='just flash, do not convert') 322 | parser.add_argument('-f' , '--family', dest='family', type=str, 323 | default="0x0", 324 | help='specify familyID - number or name (default: 0x0)') 325 | parser.add_argument('--blocks-offset', dest='blocks_offset', type=str, 326 | default="0x0", 327 | help='TODO') 328 | parser.add_argument('--blocks-reserved', dest='blocks_reserved', type=str, 329 | default="0x0", 330 | help='TODO') 331 | parser.add_argument('-C' , '--carray', action='store_true', 332 | help='convert binary file to a C array, not UF2') 333 | parser.add_argument('-i', '--info', action='store_true', 334 | help='display header information from UF2, do not convert') 335 | args = parser.parse_args() 336 | appstartaddr = int(args.base, 0) 337 | blocks_offset = int(args.blocks_offset, 0) 338 | blocks_reserved = int(args.blocks_reserved, 0) 339 | 340 | families = load_families() 341 | 342 | if args.family.upper() in families: 343 | familyid = families[args.family.upper()] 344 | else: 345 | try: 346 | familyid = int(args.family, 0) 347 | except ValueError: 348 | error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) 349 | 350 | if args.list: 351 | list_drives() 352 | else: 353 | if not args.input: 354 | error("Need input file") 355 | with open(args.input, mode='rb') as f: 356 | inpbuf = f.read() 357 | from_uf2 = is_uf2(inpbuf) 358 | ext = "uf2" 359 | if args.deploy: 360 | outbuf = inpbuf 361 | elif from_uf2 and not args.info: 362 | outbuf = convert_from_uf2(inpbuf) 363 | ext = "bin" 364 | elif from_uf2 and args.info: 365 | outbuf = "" 366 | convert_from_uf2(inpbuf) 367 | 368 | elif is_hex(inpbuf): 369 | outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), blocks_reserved, blocks_offset) 370 | elif args.carray: 371 | outbuf = convert_to_carray(inpbuf) 372 | ext = "h" 373 | else: 374 | outbuf = convert_to_uf2(inpbuf, blocks_reserved, blocks_offset) 375 | if not args.deploy and not args.info: 376 | print("Converted to %s, output size: %d, start address: 0x%x" % 377 | (ext, len(outbuf), appstartaddr)) 378 | if args.convert or ext != "uf2": 379 | drives = [] 380 | if args.output == None: 381 | args.output = "flash." + ext 382 | else: 383 | drives = get_drives() 384 | 385 | if args.output: 386 | write_file(args.output, outbuf) 387 | else: 388 | if len(drives) == 0: 389 | error("No drive to deploy.") 390 | if outbuf: 391 | for d in drives: 392 | print("Flashing %s (%s)" % (d, board_id(d))) 393 | write_file(d + "/NEW.UF2", outbuf) 394 | 395 | 396 | if __name__ == "__main__": 397 | main() 398 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | hidapi==0.14.0.post2 2 | PySimpleGUI-4-foss==4.60.4.1 3 | pywin32; os_name == 'nt' 4 | -------------------------------------------------------------------------------- /res/logo_cropped_transparent_keyboard_48x48.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrameworkComputer/qmk_hid/5988b109b9e2dfd2f0d50ef365f11bb71d9d0c76/res/logo_cropped_transparent_keyboard_48x48.ico -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "1.75.0" 4 | components = ["rust-src", "clippy", "rustfmt"] 5 | targets = ["x86_64-unknown-uefi"] 6 | -------------------------------------------------------------------------------- /screenshots/qmk_gui_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrameworkComputer/qmk_hid/5988b109b9e2dfd2f0d50ef365f11bb71d9d0c76/screenshots/qmk_gui_screenshot.png -------------------------------------------------------------------------------- /src/factory.rs: -------------------------------------------------------------------------------- 1 | use hidapi::HidDevice; 2 | 3 | use crate::raw_hid::send_message; 4 | use crate::via::ViaCommandId; 5 | use crate::QmkError; 6 | 7 | /// Send a factory command, currently only supported in Framework 16 keyboards 8 | pub fn send_factory_command(dev: &HidDevice, command: u8, value: u8) -> Result<(), QmkError> { 9 | let msg = vec![command, value]; 10 | let _ = send_message(dev, ViaCommandId::BootloaderJump as u8, Some(&msg), 0)?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod factory; 2 | pub mod raw_hid; 3 | pub mod via; 4 | 5 | extern crate hidapi; 6 | 7 | use std::fmt; 8 | 9 | use crate::raw_hid::*; 10 | use crate::via::*; 11 | 12 | use hidapi::{DeviceInfo, HidApi, HidDevice, HidError}; 13 | 14 | #[derive(Debug)] 15 | pub struct QmkError; 16 | 17 | impl fmt::Display for QmkError { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | write!(f, "QMK Error") 20 | } 21 | } 22 | 23 | impl std::error::Error for QmkError {} 24 | 25 | #[derive(Clone, Copy, Debug, PartialEq, clap::ValueEnum)] 26 | pub enum Color { 27 | /// 0° and 255° => 0 28 | Red, 29 | /// Yellow (120°) => 43 30 | Yellow, 31 | /// Green (120°) => 85 32 | Green, 33 | /// Cyan (180°) => 125 34 | Cyan, 35 | /// Blue (240°) => 170 36 | Blue, 37 | /// Purple (300°) => 213 38 | Purple, 39 | /// Saturation 0 40 | White, 41 | } 42 | 43 | const QMK_INTERFACE: i32 = 0x01; 44 | 45 | pub struct Found { 46 | pub raw_usages: Vec, 47 | pub console_usages: Vec, 48 | } 49 | 50 | /// Format BCD version 51 | /// 52 | /// # Examples 53 | /// 54 | /// ``` 55 | /// let ver = format_bcd(0x0213); 56 | /// assert_eq!(ver, "2.1.3"); 57 | /// ``` 58 | fn format_bcd(bcd: u16) -> String { 59 | let bytes = bcd.to_be_bytes(); 60 | let major = bytes[0]; 61 | let minor = (bytes[1] & 0xF0) >> 4; 62 | let patch = bytes[1] & 0x0F; 63 | format!("{major}.{minor}.{patch}") 64 | } 65 | 66 | const NOT_SET: &str = "NOT SET"; 67 | 68 | pub fn find_devices( 69 | api: &HidApi, 70 | list: bool, 71 | verbose: bool, 72 | args_vid: Option<&str>, 73 | args_pid: Option<&str>, 74 | ) -> Found { 75 | let mut found: Found = Found { 76 | raw_usages: vec![], 77 | console_usages: vec![], 78 | }; 79 | for dev_info in api.device_list() { 80 | let vid = dev_info.vendor_id(); 81 | let pid = dev_info.product_id(); 82 | let interface = dev_info.interface_number(); 83 | 84 | // Print device information 85 | let usage_page = dev_info.usage_page(); 86 | if ![RAW_USAGE_PAGE, CONSOLE_USAGE_PAGE].contains(&usage_page) { 87 | continue; 88 | } 89 | 90 | if (list && usage_page == RAW_USAGE_PAGE) || verbose { 91 | println!("{vid:04x}:{pid:04x}"); 92 | let fw_ver = dev_info.release_number(); 93 | println!( 94 | " Manufacturer: {:?}", 95 | dev_info.manufacturer_string().unwrap_or(NOT_SET) 96 | ); 97 | println!( 98 | " Product: {:?}", 99 | dev_info.product_string().unwrap_or(NOT_SET) 100 | ); 101 | println!(" FW Version: {}", format_bcd(fw_ver)); 102 | println!( 103 | " Serial No: {:?}", 104 | dev_info.serial_number().unwrap_or(NOT_SET) 105 | ); 106 | 107 | if verbose { 108 | println!(" VID/PID: {vid:04x}:{pid:04x}"); 109 | println!(" Interface: {}", dev_info.interface_number()); 110 | println!(" Path: {:?}", dev_info.path()); 111 | print!(" Usage Page: 0x{:04X}", dev_info.usage_page()); 112 | match usage_page { 113 | RAW_USAGE_PAGE => println!(" (RAW_USAGE_PAGE)"), 114 | CONSOLE_USAGE_PAGE => println!(" (CONSOLE_USAGE_PAGE)"), 115 | G_DESK_USAGE_PAGE => println!(" (Generic Desktop Usage Page)"), 116 | CONSUMER_USAGE_PAGE => println!(" (CONSUMER_USAGE_PAGE)"), 117 | _ => println!(), 118 | } 119 | } 120 | } 121 | 122 | // TODO: Use clap-num for this 123 | let args_vid = args_vid 124 | .as_ref() 125 | .map(|s| u16::from_str_radix(s, 16).unwrap()); 126 | let args_pid = args_pid 127 | .as_ref() 128 | .map(|s| u16::from_str_radix(s, 16).unwrap()); 129 | 130 | // If filtering for specific VID or PID, skip all that don't match 131 | if let Some(args_vid) = args_vid { 132 | if vid != args_vid { 133 | continue; 134 | } 135 | } 136 | if let Some(args_pid) = args_pid { 137 | if pid != args_pid { 138 | continue; 139 | } 140 | } 141 | 142 | match usage_page { 143 | RAW_USAGE_PAGE => { 144 | if interface != QMK_INTERFACE { 145 | println!( 146 | "Something is wrong with {vid}:{pid}. The interface isn't {QMK_INTERFACE}" 147 | ); 148 | } else { 149 | found.raw_usages.push(dev_info.clone()); 150 | } 151 | } 152 | CONSOLE_USAGE_PAGE => { 153 | found.console_usages.push(dev_info.clone()); 154 | } 155 | _ => {} 156 | } 157 | } 158 | 159 | found 160 | } 161 | 162 | pub fn new_hidapi() -> Result { 163 | HidApi::new() 164 | } 165 | 166 | pub fn save(save: bool, dev: &HidDevice) { 167 | if !save { 168 | return; 169 | } 170 | 171 | save_rgb(dev).unwrap(); 172 | save_backlight(dev).unwrap(); 173 | } 174 | 175 | pub fn color_as_hue(color: Color) -> u8 { 176 | match color { 177 | Color::Red => 0, 178 | Color::Yellow => 43, 179 | Color::Green => 85, 180 | Color::Cyan => 125, 181 | Color::Blue => 170, 182 | Color::Purple => 213, 183 | Color::White => 0, // Doesn't matter, only hue needs to be 0 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use clap::{Parser, Subcommand}; 4 | 5 | extern crate hidapi; 6 | 7 | use hidapi::{DeviceInfo, HidApi}; 8 | 9 | use qmk_hid::factory::*; 10 | use qmk_hid::raw_hid::*; 11 | use qmk_hid::via::*; 12 | use qmk_hid::*; 13 | 14 | #[derive(Subcommand, Debug)] 15 | enum Commands { 16 | Factory(FactorySubcommand), 17 | Via(ViaSubcommand), 18 | Qmk(QmkSubcommand), 19 | } 20 | 21 | /// Factory 22 | #[derive(Parser, Debug)] 23 | #[command(arg_required_else_help = true)] 24 | struct FactorySubcommand { 25 | /// Light up single LED 26 | #[arg(long)] 27 | led: Option, 28 | 29 | #[arg(short)] 30 | serialnum: bool, 31 | 32 | #[arg(long)] 33 | bios_mode: Option, 34 | 35 | #[arg(long)] 36 | factory_mode: Option, 37 | } 38 | 39 | /// QMK 40 | #[derive(Parser, Debug)] 41 | #[command(arg_required_else_help = true)] 42 | struct QmkSubcommand { 43 | /// Listen to the console. Better to use `qmk console` (https://github.com/qmk/qmk_cli) 44 | #[arg(short, long)] 45 | console: bool, 46 | } 47 | 48 | /// Via 49 | #[derive(Parser, Debug)] 50 | #[command(arg_required_else_help = true)] 51 | struct ViaSubcommand { 52 | /// Get VIA protocol and config information (most likely NOT what you're looking for) 53 | #[arg(long)] 54 | info: bool, 55 | 56 | /// Flash device indication (backlight) 3x 57 | #[arg(long)] 58 | device_indication: bool, 59 | 60 | /// Set RGB brightness percentage or get, if no value provided 61 | #[arg(long)] 62 | rgb_brightness: Option>, 63 | 64 | /// Set RGB effect or get, if no value provided 65 | #[arg(long)] 66 | rgb_effect: Option>, 67 | 68 | /// Set RGB effect speed or get, if no value provided (0-255) 69 | #[arg(long)] 70 | rgb_effect_speed: Option>, 71 | 72 | /// Set RGB hue or get, if no value provided. (0-255) 73 | #[arg(long)] 74 | rgb_hue: Option>, 75 | 76 | /// Set RGB color 77 | #[arg(long)] 78 | #[clap(value_enum)] 79 | rgb_color: Option, 80 | 81 | /// Set RGB saturation or get, if no value provided. (0-255) 82 | #[arg(long)] 83 | rgb_saturation: Option>, 84 | 85 | /// Set backlight brightness percentage or get, if no value provided 86 | #[arg(long)] 87 | backlight: Option>, 88 | 89 | /// Set backlight breathing or get, if no value provided 90 | #[arg(long)] 91 | backlight_breathing: Option>, 92 | 93 | /// Save RGB/backlight value, otherwise it won't persist through keyboard reboot. Can be used 94 | /// by itself or together with other argument. 95 | #[arg(long)] 96 | save: bool, 97 | 98 | // TODO: 99 | // - RGB light 100 | // - LED matrix 101 | // - audio 102 | /// Reset the EEPROM contents (Not supported by all firmware) 103 | #[arg(long)] 104 | eeprom_reset: bool, 105 | 106 | /// Jump to the bootloader (Not supported by all firmware) 107 | #[arg(long)] 108 | bootloader: bool, 109 | } 110 | 111 | /// RAW HID and VIA commandline for QMK devices 112 | #[derive(Parser, Debug)] 113 | #[command(version, arg_required_else_help = true)] 114 | struct ClapCli { 115 | #[command(subcommand)] 116 | command: Option, 117 | 118 | /// List connected HID devices 119 | #[arg(short, long)] 120 | list: bool, 121 | 122 | /// Verbose outputs to the console 123 | #[arg(short, long)] 124 | verbose: bool, 125 | 126 | /// VID (Vendor ID) in hex digits 127 | #[arg(long)] 128 | vid: Option, 129 | 130 | /// PID (Product ID) in hex digits 131 | #[arg(long)] 132 | pid: Option, 133 | } 134 | 135 | fn main() { 136 | let args: Vec = std::env::args().collect(); 137 | let args = ClapCli::parse_from(args); 138 | 139 | match HidApi::new() { 140 | Ok(api) => { 141 | let found = find_devices( 142 | &api, 143 | args.list, 144 | args.verbose, 145 | args.vid.as_deref(), 146 | args.pid.as_deref(), 147 | ); 148 | 149 | let dev_infos = match args.command { 150 | Some(Commands::Qmk(_)) => found.console_usages, 151 | Some(_) => found.raw_usages, 152 | None => return, 153 | }; 154 | 155 | if dev_infos.is_empty() { 156 | println!("No device found"); 157 | } else if dev_infos.len() == 1 { 158 | use_device(&args, &api, dev_infos.first().unwrap()); 159 | } else { 160 | println!("More than 1 device found. Select a specific device with --vid and --pid"); 161 | } 162 | } 163 | Err(e) => { 164 | eprintln!("Error: {e}"); 165 | } 166 | }; 167 | } 168 | 169 | fn use_device(args: &ClapCli, api: &HidApi, dev_info: &DeviceInfo) { 170 | let vid = dev_info.vendor_id(); 171 | let pid = dev_info.product_id(); 172 | let interface = dev_info.interface_number(); 173 | 174 | if args.verbose { 175 | println!("Connecting to {vid:04X}:{pid:04X} Interface: {interface}"); 176 | } 177 | 178 | let device = dev_info.open_device(api).unwrap(); 179 | 180 | match &args.command { 181 | Some(Commands::Factory(args)) => { 182 | if args.serialnum { 183 | send_factory_command(&device, 0x04, 0).unwrap(); 184 | } 185 | if let Some(bios_mode) = args.bios_mode { 186 | send_factory_command(&device, 0x05, bios_mode as u8).unwrap(); 187 | } 188 | if let Some(factory_mode) = args.factory_mode { 189 | send_factory_command(&device, 0x06, factory_mode as u8).unwrap(); 190 | } 191 | if let Some(led) = args.led { 192 | println!("Lighting up LED: {led}"); 193 | send_factory_command(&device, 0x02, led).unwrap(); 194 | } 195 | } 196 | Some(Commands::Qmk(args)) => { 197 | if args.console { 198 | qmk_console(&device); 199 | } 200 | } 201 | Some(Commands::Via(args)) => { 202 | if args.eeprom_reset { 203 | eeprom_reset(&device).unwrap(); 204 | } else if args.bootloader { 205 | bootloader_jump(&device).unwrap(); 206 | } else if args.info { 207 | let prot_ver = get_protocol_ver(&device).unwrap(); 208 | let uptime = get_keyboard_value(&device, ViaKeyboardValueId::Uptime).unwrap(); 209 | let layout_opts = 210 | get_keyboard_value(&device, ViaKeyboardValueId::LayoutOptions).unwrap(); 211 | let matrix_state = 212 | get_keyboard_value(&device, ViaKeyboardValueId::SwitchMatrixState).unwrap(); 213 | let fw_ver = 214 | get_keyboard_value(&device, ViaKeyboardValueId::FirmwareVersion).unwrap(); 215 | 216 | println!("Uptime: {:?}s", uptime / 1000); 217 | println!("VIA Protocol Version: 0x{prot_ver:04X}"); 218 | println!("Layout Options: 0x{layout_opts:08X}"); 219 | println!("Switch Matrix State: 0x{matrix_state:08X}"); // TODO: Decode 220 | println!("VIA FWVER: 0x{fw_ver:08X}"); 221 | } else if args.device_indication { 222 | // Works with RGB and single zone backlight keyboards 223 | // Device indication doesn't work well with all effects 224 | // So it's best to save the currently configured one, switch to solid color and later back. 225 | let cur_effect = get_rgb_u8(&device, ViaRgbMatrixValue::Effect as u8).unwrap(); 226 | // Solid effect is always 1 227 | set_rgb_u8(&device, ViaRgbMatrixValue::Effect as u8, 1).unwrap(); 228 | 229 | // QMK recommends to repeat this 6 times, every 200ms 230 | for _ in 0..6 { 231 | set_keyboard_value(&device, ViaKeyboardValueId::DeviceIndication, 0).unwrap(); 232 | thread::sleep(Duration::from_millis(200)); 233 | } 234 | 235 | // Restore effect 236 | set_rgb_u8(&device, ViaRgbMatrixValue::Effect as u8, cur_effect).unwrap(); 237 | } else if let Some(arg_brightness) = args.rgb_brightness { 238 | if let Some(percentage) = arg_brightness { 239 | let brightness = (255.0 * percentage as f32) / 100.0; 240 | set_rgb_u8( 241 | &device, 242 | ViaRgbMatrixValue::Brightness as u8, 243 | brightness.round() as u8, 244 | ) 245 | .unwrap(); 246 | } 247 | let brightness = get_rgb_u8(&device, ViaRgbMatrixValue::Brightness as u8).unwrap(); 248 | let percentage = (100.0 * brightness as f32) / 255.0; 249 | println!("Brightness: {}%", percentage.round()); 250 | save(args.save, &device); 251 | } else if let Some(arg_effect) = args.rgb_effect { 252 | if let Some(effect) = arg_effect { 253 | set_rgb_u8(&device, ViaRgbMatrixValue::Effect as u8, effect).unwrap(); 254 | } 255 | let effect = get_rgb_u8(&device, ViaRgbMatrixValue::Effect as u8).unwrap(); 256 | println!("Effect: {effect}"); 257 | save(args.save, &device); 258 | } else if let Some(arg_speed) = args.rgb_effect_speed { 259 | if let Some(speed) = arg_speed { 260 | set_rgb_u8(&device, ViaRgbMatrixValue::EffectSpeed as u8, speed).unwrap(); 261 | } 262 | let speed = get_rgb_u8(&device, ViaRgbMatrixValue::EffectSpeed as u8).unwrap(); 263 | println!("Effect Speed: {speed}"); 264 | save(args.save, &device); 265 | } else if let Some(arg_saturation) = &args.rgb_saturation { 266 | if let Some(saturation) = arg_saturation { 267 | set_rgb_color(&device, None, Some(*saturation)).unwrap(); 268 | } 269 | let (hue, saturation) = get_rgb_color(&device).unwrap(); 270 | println!("Color Hue: {hue}"); 271 | println!("Color Saturation: {saturation}"); 272 | save(args.save, &device); 273 | } else if let Some(arg_hue) = &args.rgb_hue { 274 | if let Some(hue) = arg_hue { 275 | set_rgb_color(&device, Some(*hue), None).unwrap(); 276 | } 277 | let (hue, saturation) = get_rgb_color(&device).unwrap(); 278 | println!("Color Hue: {hue}"); 279 | println!("Color Saturation: {saturation}"); 280 | save(args.save, &device); 281 | } else if let Some(color) = &args.rgb_color { 282 | if let Color::White = color { 283 | set_rgb_color(&device, None, Some(0)).unwrap(); 284 | } else { 285 | set_rgb_color(&device, Some(color_as_hue(*color)), Some(255)).unwrap(); 286 | } 287 | let (hue, saturation) = get_rgb_color(&device).unwrap(); 288 | println!("Color Hue: {hue}"); 289 | println!("Color Saturation: {saturation}"); 290 | save(args.save, &device); 291 | } else if let Some(arg_backlight) = args.backlight { 292 | if let Some(percentage) = arg_backlight { 293 | let brightness = (255.0 * percentage as f32) / 100.0; 294 | set_backlight( 295 | &device, 296 | ViaBacklightValue::Brightness as u8, 297 | brightness.round() as u8, 298 | ) 299 | .unwrap(); 300 | } 301 | let brightness = 302 | get_backlight(&device, ViaBacklightValue::Brightness as u8).unwrap(); 303 | let percentage = (100.0 * brightness as f32) / 255.0; 304 | println!("Brightness: {}%", percentage.round()); 305 | save(args.save, &device); 306 | } else if let Some(arg_breathing) = args.backlight_breathing { 307 | if let Some(breathing) = arg_breathing { 308 | set_backlight(&device, ViaBacklightValue::Effect as u8, breathing as u8) 309 | .unwrap(); 310 | } 311 | let breathing = get_backlight(&device, ViaBacklightValue::Effect as u8).unwrap(); 312 | println!("Breathing: : {:?}", breathing == 1); 313 | save(args.save, &device); 314 | } else if args.save { 315 | save(args.save, &device); 316 | } else { 317 | println!("No command specified."); 318 | } 319 | } 320 | _ => {} 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/raw_hid.rs: -------------------------------------------------------------------------------- 1 | //! Interact with the raw HID interface of QMK firmware 2 | 3 | use hidapi::HidDevice; 4 | 5 | use crate::QmkError; 6 | 7 | // ChibiOS won't respond if we send 32. 8 | // Currently I hardcoded it to ignore. But in upstream QMK/ChibiOS we have to send 33 bytes. 9 | // See raw_hid_send in tmk_core/protocol/chibios/usb_main.c 10 | pub const RAW_HID_BUFFER_SIZE: usize = 32; 11 | 12 | pub const RAW_USAGE_PAGE: u16 = 0xFF60; 13 | pub const CONSOLE_USAGE_PAGE: u16 = 0xFF31; 14 | /// Generic Desktop 15 | pub const G_DESK_USAGE_PAGE: u16 = 0x01; 16 | pub const CONSUMER_USAGE_PAGE: u16 = 0x0C; 17 | 18 | pub fn send_message( 19 | dev: &HidDevice, 20 | message_id: u8, 21 | msg: Option<&[u8]>, 22 | out_len: usize, 23 | ) -> Result, QmkError> { 24 | // TODO: Why fill the rest with 0xFE? hidapitester uses 0x00 25 | let mut data = vec![0xFE; RAW_HID_BUFFER_SIZE]; 26 | data[0] = 0x00; // NULL report ID 27 | data[1] = message_id; 28 | 29 | if let Some(msg) = msg { 30 | assert!(msg.len() <= RAW_HID_BUFFER_SIZE); 31 | let data_msg = &mut data[2..msg.len() + 2]; 32 | data_msg.copy_from_slice(msg); 33 | } 34 | 35 | //println!("Writing data: {:?}", data); 36 | let res = dev.write(&data); 37 | match res { 38 | Ok(_size) => { 39 | //println!("Written: {}", size); 40 | } 41 | Err(err) => { 42 | println!("Write err: {err:?}"); 43 | return Err(QmkError); 44 | } 45 | }; 46 | 47 | // No response expected 48 | if out_len == 0 { 49 | return Ok(vec![]); 50 | } 51 | 52 | dev.set_blocking_mode(true).unwrap(); 53 | 54 | let mut buf: Vec = vec![0xFE; RAW_HID_BUFFER_SIZE]; 55 | let res = dev.read(buf.as_mut_slice()); 56 | match res { 57 | Ok(_size) => { 58 | //println!("Read: {}", size); 59 | //println!("out_len: {}", out_len); 60 | //println!("buf: {:?}", buf); 61 | Ok(buf[1..out_len + 1].to_vec()) 62 | } 63 | Err(err) => { 64 | println!("Read err: {err:?}"); 65 | Err(QmkError) 66 | } 67 | } 68 | } 69 | 70 | // TODO: I actually don't think this is QMK specific 71 | // Same protocol as https://www.pjrc.com/teensy/hid_listen.html 72 | pub fn qmk_console(dev: &HidDevice) { 73 | loop { 74 | let mut buf: Vec = vec![0xFE; RAW_HID_BUFFER_SIZE]; 75 | let res = dev.read(buf.as_mut_slice()); 76 | match res { 77 | Ok(_size) => { 78 | let string = String::from_utf8_lossy(&buf); 79 | print!("{string}"); 80 | } 81 | Err(err) => { 82 | println!("Read err: {err:?}"); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/via.rs: -------------------------------------------------------------------------------- 1 | //! Implementing the VIA protocol supported by QMK keyboard firmware 2 | 3 | use hidapi::HidDevice; 4 | 5 | use crate::{raw_hid::*, QmkError}; 6 | 7 | #[repr(u8)] 8 | pub enum ViaCommandId { 9 | GetProtocolVersion = 0x01, // always 0x01 10 | GetKeyboardValue = 0x02, 11 | SetKeyboardValue = 0x03, 12 | //DynamicKeymapGetKeycode = 0x04, 13 | //DynamicKeymapSetKeycode = 0x05, 14 | //DynamicKeymapReset = 0x06, 15 | CustomSetValue = 0x07, 16 | CustomGetValue = 0x08, 17 | CustomSave = 0x09, 18 | EepromReset = 0x0A, 19 | BootloaderJump = 0x0B, 20 | //DynamicKeymapMacroGetCount = 0x0C, 21 | //DynamicKeymapMacroGetBufferSize = 0x0D, 22 | //DynamicKeymapMacroGetBuffer = 0x0E, 23 | //DynamicKeymapMacroSetBuffer = 0x0F, 24 | //DynamicKeymapMacroReset = 0x10, 25 | //DynamicKeymapGetLayerCount = 0x11, 26 | //DynamicKeymapGetBuffer = 0x12, 27 | //DynamicKeymapSetBuffer = 0x13, 28 | //DynamicKeymapGetEncoder = 0x14, 29 | //DynamicKeymapSetEncoder = 0x15, 30 | //Unhandled = 0xFF, 31 | } 32 | 33 | pub enum ViaKeyboardValueId { 34 | Uptime = 0x01, 35 | LayoutOptions = 0x02, 36 | SwitchMatrixState = 0x03, 37 | FirmwareVersion = 0x04, 38 | DeviceIndication = 0x05, 39 | } 40 | 41 | pub enum ViaChannelId { 42 | //CustomChannel = 0, 43 | BacklightChannel = 1, 44 | //RgblightChannel = 2, 45 | RgbMatrixChannel = 3, 46 | //AudioChannel = 4, 47 | } 48 | 49 | pub enum ViaBacklightValue { 50 | Brightness = 1, 51 | Effect = 2, 52 | } 53 | 54 | //enum ViaRgbLightValue { 55 | // Brightness = 1, 56 | // Effect = 2, 57 | // EffectSpeed = 3, 58 | // Color = 4, 59 | //} 60 | 61 | pub enum ViaRgbMatrixValue { 62 | Brightness = 1, 63 | Effect = 2, 64 | EffectSpeed = 3, 65 | Color = 4, 66 | } 67 | 68 | /// Get the VIA protocol version. Latest one is 0x000B 69 | pub fn get_protocol_ver(dev: &HidDevice) -> Result { 70 | let output = send_message(dev, ViaCommandId::GetProtocolVersion as u8, None, 2)?; 71 | debug_assert_eq!(output.len(), 2); 72 | Ok(u16::from_be_bytes(output.try_into().unwrap())) 73 | } 74 | 75 | pub fn get_keyboard_value(dev: &HidDevice, value: ViaKeyboardValueId) -> Result { 76 | // Must skip the first byte from the output, as we're sending a message of 1 and it's preserved in the output 77 | let msg = vec![value as u8]; 78 | let output = send_message(dev, ViaCommandId::GetKeyboardValue as u8, Some(&msg), 5)?; 79 | assert_eq!(output.len(), 5); 80 | Ok(u32::from_be_bytes(output[1..5].try_into().unwrap())) 81 | } 82 | 83 | pub fn set_keyboard_value( 84 | dev: &HidDevice, 85 | value: ViaKeyboardValueId, 86 | number: u32, 87 | ) -> Result<(), QmkError> { 88 | assert!(number < (1 << 8)); // TODO: Support u32 89 | let msg = vec![value as u8, number as u8]; 90 | send_message(dev, ViaCommandId::SetKeyboardValue as u8, Some(&msg), 0)?; 91 | Ok(()) 92 | } 93 | 94 | pub fn get_rgb_u8(dev: &HidDevice, value: u8) -> Result { 95 | let msg = vec![ViaChannelId::RgbMatrixChannel as u8, value]; 96 | let output = send_message(dev, ViaCommandId::CustomGetValue as u8, Some(&msg), 3)?; 97 | //println!("Current value: {:?}", output); 98 | Ok(output[2]) 99 | } 100 | 101 | pub fn set_rgb_u8(dev: &HidDevice, value: u8, value_data: u8) -> Result<(), QmkError> { 102 | // data = [ command_id, channel_id, value_id, value_data ] 103 | let msg = vec![ViaChannelId::RgbMatrixChannel as u8, value, value_data]; 104 | send_message(dev, ViaCommandId::CustomSetValue as u8, Some(&msg), 0)?; 105 | Ok(()) 106 | } 107 | 108 | pub fn save_rgb(dev: &HidDevice) -> Result<(), QmkError> { 109 | // data = [ command_id, channel_id, value_id, value_data ] 110 | let msg = vec![ViaChannelId::RgbMatrixChannel as u8]; 111 | send_message(dev, ViaCommandId::CustomSave as u8, Some(&msg), 0)?; 112 | Ok(()) 113 | } 114 | 115 | pub fn set_rgb_color( 116 | dev: &HidDevice, 117 | hue: Option, 118 | saturation: Option, 119 | ) -> Result<(), QmkError> { 120 | let (cur_hue, cur_saturation) = get_rgb_color(dev).unwrap(); 121 | 122 | let hue = hue.unwrap_or(cur_hue); 123 | let saturation = saturation.unwrap_or(cur_saturation); 124 | 125 | let msg = vec![ 126 | ViaChannelId::RgbMatrixChannel as u8, 127 | ViaRgbMatrixValue::Color as u8, 128 | hue, 129 | saturation, 130 | ]; 131 | send_message(dev, ViaCommandId::CustomSetValue as u8, Some(&msg), 0)?; 132 | 133 | Ok(()) 134 | } 135 | 136 | pub fn get_rgb_color(dev: &HidDevice) -> Result<(u8, u8), QmkError> { 137 | let msg = vec![ 138 | ViaChannelId::RgbMatrixChannel as u8, 139 | ViaRgbMatrixValue::Color as u8, 140 | ]; 141 | let output = send_message(dev, ViaCommandId::CustomGetValue as u8, Some(&msg), 4)?; 142 | Ok((output[2], output[3])) 143 | } 144 | 145 | pub fn get_backlight(dev: &HidDevice, value: u8) -> Result { 146 | let msg = vec![ViaChannelId::BacklightChannel as u8, value]; 147 | let output = send_message(dev, ViaCommandId::CustomGetValue as u8, Some(&msg), 3)?; 148 | Ok(output[2]) 149 | } 150 | 151 | pub fn set_backlight(dev: &HidDevice, value: u8, value_data: u8) -> Result<(), QmkError> { 152 | let msg = vec![ViaChannelId::BacklightChannel as u8, value, value_data]; 153 | send_message(dev, ViaCommandId::CustomSetValue as u8, Some(&msg), 0)?; 154 | Ok(()) 155 | } 156 | 157 | pub fn save_backlight(dev: &HidDevice) -> Result<(), QmkError> { 158 | // data = [ command_id, channel_id, value_id, value_data ] 159 | let msg = vec![ViaChannelId::BacklightChannel as u8]; 160 | send_message(dev, ViaCommandId::CustomSave as u8, Some(&msg), 0)?; 161 | Ok(()) 162 | } 163 | 164 | pub fn eeprom_reset(dev: &HidDevice) -> Result<(), QmkError> { 165 | let output = send_message(dev, ViaCommandId::EepromReset as u8, None, 0)?; 166 | debug_assert_eq!(output.len(), 0); 167 | Ok(()) 168 | } 169 | 170 | pub fn bootloader_jump(dev: &HidDevice) -> Result<(), QmkError> { 171 | let output = send_message(dev, ViaCommandId::BootloaderJump as u8, None, 0)?; 172 | debug_assert_eq!(output.len(), 0); 173 | Ok(()) 174 | } 175 | --------------------------------------------------------------------------------