# Griffin PowerMate — Windows 11 Controller A modern, fully‑customizable Windows 11 application for the **Griffin PowerMate** USB knob (the original 2001 multifunction controller, discontinued in 2018). Turn, press, double/triple‑press, long‑press, and press‑while‑turning — every interaction is mappable, per application, through a clean GUI. The blue LED is fully driven by the app (solid, software pulse, flash, or volume‑tracking with fade). This project is a from‑scratch Python reimplementation inspired by Nathan Sweet's original Java utility ([EsotericSoftware/powermate](https://github.com/EsotericSoftware/powermate)), rebuilt with native Windows audio, a full gesture engine, per‑app profiles, a CustomTkinter UI, and a one‑file `.exe` build. It was reverse‑engineered and verified against real PowerMate hardware on Windows 11. --- ## Table of Contents 1. [Features](#features) 2. [Hardware & Protocol](#hardware--protocol) 3. [Quick Start](#quick-start) 4. [USB Driver Setup (Zadig)](#usb-driver-setup-zadig) 5. [Using the App](#using-the-app) 6. [Triggers](#triggers) 7. [Actions](#actions) 8. [LED Modes](#led-modes) 9. [Per‑App Profiles](#per-app-profiles) 10. [Configuration File](#configuration-file) 11. [Timing & Sensitivity](#timing--sensitivity) 12. [Building the EXE](#building-the-exe) 13. [Architecture](#architecture) 14. [Reverse‑Engineering Notes](#reverse-engineering-notes-windows--winusb) 15. [Security Considerations](#security-considerations) 16. [Testing](#testing) 17. [Troubleshooting](#troubleshooting) 18. [Project Layout](#project-layout) 19. [Changelog](#changelog) 20. [Credits & License](#credits--license) --- ## Features - **Every knob interaction is mappable** — rotate left/right, single/double/triple press, long press, and press‑while‑rotating (both directions). - **Per‑application profiles** — different bindings depending on the focused app, with inheritance from a `default` profile. - **Rich action set** — system volume, mute, **microphone mute**, media transport (play/pause, next, previous), scroll, arbitrary **keyboard shortcuts**, **3‑step keyboard macros**, **launch** apps/files/URLs, and **run** shell commands. - **Knob activity / debug panel** — a live, timestamped log of incoming knob events and the action each one triggers. - **Translucent on‑screen display (OSD)** — a two‑line overlay in the bottom‑right corner (over the taskbar) that shows the gesture and its bound action on every turn/press. Fully styleable (opacity, background color, font family/color/size 8–24pt, duration). - **Start with Windows** — one‑click toggle to launch automatically at login. - **Full LED control** — `off`, `solid`, software `pulse`, `flash`, and `volume` (brightness follows the system volume, then fades). - **Modern GUI** (CustomTkinter) — profile sidebar with a running‑app picker, a trigger→action grid with a "record shortcut" capture field, an LED panel with a live test button, and a timing panel. - **System tray** — runs quietly in the background; pause/resume; settings; quit. - **Live config reload** — edit `config.json` by hand and changes apply within ~1s. - **Single‑file executable** — `PowerMate.exe` bundles Python, all dependencies, and the native `libusb` DLL. No Python install required on the target machine. --- ## Hardware & Protocol | Property | Value | |---|---| | USB Vendor ID | `0x077D` | | USB Product ID | `0x0410` | | Interface | 0 (HID class, but driven via libusb/WinUSB) | | Input endpoint | `0x81` — 6‑byte interrupt reports | | Output endpoint | `0x02` — 1‑byte interrupt (LED brightness) | **Input report (endpoint `0x81`, 6 bytes):** | Byte | Meaning | |---|---| | `0` | Button state — bit 0 set = pressed | | `1` | Relative rotation delta, **signed** (`+` clockwise, `−` counter‑clockwise) | | `2–5` | Status/reserved (unused by this app) | **LED brightness (endpoint `0x02`):** a single interrupt‑OUT byte, `0`–`255`. This is the method this app uses on Windows — see [Reverse‑Engineering Notes](#reverse-engineering-notes-windows--winusb) for why the vendor *control* transfer is deliberately avoided there. When polled slowly during a fast spin, the device coalesces rotation into a single larger delta (e.g. `+15`), so the gesture engine accumulates deltas rather than assuming one detent per packet. --- ## Quick Start ### Option A — Run the prebuilt EXE (end users) 1. Install the **WinUSB** driver on the PowerMate with Zadig — see [USB Driver Setup](#usb-driver-setup-zadig). *(One time.)* 2. Download / copy **`PowerMate.exe`** and double‑click it. 3. A tray icon appears. Turn the knob — your system volume changes. Right‑click the tray icon → **Settings** to customize. ### Option B — Run from source (developers) ```powershell # 1. Python 3.11+ (64-bit). 3.14 is tested. python --version # 2. Install dependencies pip install -r requirements.txt # 3. Install the WinUSB driver via Zadig (see below) # 4. Run python main.py ``` --- ## USB Driver Setup (Zadig) By default Windows binds the PowerMate to the generic **HID** driver, which does **not** allow the low‑level USB access this app needs. You must rebind it to **WinUSB** once. 1. Download **Zadig**: (portable, no install). 2. Plug in the PowerMate. 3. In Zadig: **Options → List All Devices**. 4. In the dropdown, select **Griffin PowerMate** (it shows `077D 0410`). 5. Choose **WinUSB** as the replacement driver (the box to the right of the green arrow). 6. Click **Replace Driver** and wait. A completion dialog — or a harmless error — is normal. > **Reversible:** This dedicates the knob to this app (it stops acting as a generic HID > device, so the old Griffin software won't see it). To undo: Device Manager → the > PowerMate → **Driver → Roll Back Driver** (or uninstall the device and replug). > **Why not libusb‑win32?** This app uses the modern **libusb‑1.0** backend, which works > best with the **WinUSB** driver. `libusb-win32` (libusb0) may also work but WinUSB is > recommended. --- ## Using the App The app starts minimized to the **system tray**. Right‑click the tray icon for: - **Settings** — open the configuration window. - **Pause / Resume** — temporarily stop/restart responding to the knob. - **Exit** — quit. ### Window controls (bottom bar) - **Save** / **Reload** — persist or re‑read `config.json`. - **Minimize to Tray** — hide the window but keep the app running in the background. - **Quit** — fully exit the application. - **Start with Windows** — checkbox that registers/unregisters the app in the per‑user startup (HKCU `…\Run`) so it launches automatically at login. - Clicking the window's **X** also minimizes to the tray (it does **not** quit). ### Knob Activity panel The panel above the bottom bar shows a live, timestamped log of every event received from the knob — the trigger, the focused app, and the action taken (e.g. `12:01:03 rotate_right +2 [chrome.exe] -> scroll_down`). Use it to confirm that gestures like **triple‑press** and **long‑press** are firing and to debug bindings. **Clear** empties the log. ### On‑screen display (OSD) A small translucent overlay appears in the **bottom‑right corner, over the taskbar**, each time you use the knob. It shows two lines: - **Line 1** — the gesture: *Single Press, Double Press, Triple Press, Long Press, Knob Left, Knob Right, Press + Knob Left/Right*. - **Line 2** — the action bound to that gesture in the **active** profile (e.g. *Volume Up*, *Play / Pause*, or the key combo for a `key`/`macro`). Unbound gestures show `—`. It is always‑on‑top, never steals keyboard focus, stays out of Alt‑Tab and the taskbar, and auto‑hides ~1.2s after the last event with a brief fade (the timer resets while you keep turning). It works even when the main window is minimized to the tray. Configure it on the **OSD tab**: enable/disable, opacity, duration, background color (hex or color picker), font family (any installed font), font color, and font size (8–24pt). **Preview** flashes a sample using the current settings. Settings persist in the `osd` block of `config.json` and apply live. ### The Settings window - **Profiles (left sidebar)** — `default` plus one entry per app. Click to select. - **+ Add Profile** opens a picker listing your **currently running apps**; click one to create a profile for it (e.g. `chrome.exe`). - **- Delete Profile** removes the selected profile (`default` cannot be deleted). - **Bindings tab** — a row for each of the 8 triggers. Pick an **action** and fill in its parameters. The `key` action has a **Record** button: click it, press your shortcut, and it captures the combo (e.g. `ctrl+shift+n`). - **LED tab** — choose the mode, brightness, pulse speed/waveform, flash timing, and volume‑fade timing. **Test LED** applies the settings to the device live. - **Timing tab** — multi‑click window, long‑press threshold, rotation sensitivity. - **OSD tab** — enable the on‑screen display and style it (opacity, duration, background color, font family/color/size 8–24pt); **Preview** flashes a sample. - **Save** writes everything to `config.json`; **Reload** re‑reads it. --- ## Triggers | Trigger | How to perform it | |---|---| | `rotate_right` | Turn clockwise | | `rotate_left` | Turn counter‑clockwise | | `press` | One click (fires after the multi‑click window elapses) | | `double_press` | Two quick clicks | | `triple_press` | Three quick clicks | | `long_press` | Hold the button past the long‑press threshold (default 500 ms) | | `press_rotate_right` | Hold the button **and** turn clockwise | | `press_rotate_left` | Hold the button **and** turn counter‑clockwise | A single press intentionally waits for the multi‑click window (default 250 ms) so it can be distinguished from a double/triple press. Press‑while‑rotating suppresses the click so it doesn't also register as a press. --- ## Actions | Action | Parameters | Description | |---|---|---| | `volume_up` | `amount` (0.0–1.0) | Raise system volume | | `volume_down` | `amount` (0.0–1.0) | Lower system volume | | `mute` | — | Toggle system mute | | `mic_mute_toggle` | — | Toggle microphone mute | | `media_play_pause` | — | Media play/pause key | | `media_next` | — | Next track | | `media_prev` | — | Previous track | | `scroll_up` | `amount` (lines) | Scroll up | | `scroll_down` | `amount` (lines) | Scroll down | | `key` | `keys` (e.g. `"ctrl+z"`) | Send a keyboard shortcut | | `macro` | `sequence` (list, up to 3) | Send several key combos in order, e.g. `["ctrl+c","alt+tab","ctrl+v"]` | | `launch` | `path` | Open a file, app, or URL | | `run` | `command` | Run a shell command | | `none` | — | Do nothing (also used to inherit from `default`) | Volume and mic actions use **pycaw** (native Windows Core Audio) — no helper executables. Keyboard and media actions use the **keyboard** library; shortcut syntax is `modifier+modifier+key`, e.g. `alt+left`, `ctrl+shift+escape`, `f5`. --- ## LED Modes | Mode | Behavior | |---|---| | `off` | LED off | | `solid` | Constant brightness (set by `brightness`, 0.0–1.0) | | `pulse` | Smooth software sine pulse; `pulse_speed` (0–510) sets the period | | `flash` | On/off at `flash_on_ms` / `flash_off_ms` | | `volume` | Brightness tracks the system volume, then fades after `fade_delay`/`fade_speed` | > On Windows the LED is driven entirely via interrupt‑OUT writes, so **pulse and flash are > implemented in software** (the device's hardware‑pulse command uses a control transfer > that is incompatible with simultaneous reads on WinUSB — see notes below). --- ## Per‑App Profiles Each profile maps triggers to actions. The **`default`** profile applies whenever the focused app has no profile of its own. App profiles **inherit** from `default`: any trigger you don't explicitly set falls back to the default binding. The active app is detected from the foreground window's process name (e.g. `chrome.exe`, `code.exe`, `vlc.exe`) via the Win32 API. **Example:** make the knob scroll in Chrome but keep controlling volume everywhere else — add a `chrome.exe` profile that overrides only `rotate_left`/`rotate_right`. A ready-made **`spotify.exe`** profile ships in the defaults: it maps single-press to play/pause (`space`), double/triple-press to next/previous track (`ctrl+right` / `ctrl+left`), and inherits system-volume control on rotation. Because it sends Spotify's own in-app shortcuts while Spotify is focused, play/pause toggles reliably (the global media key can otherwise be routed to a different app by Windows). --- ## Configuration File Settings live in **`config.json`** next to the app (created on first run from defaults; a template is provided as `config.example.json`). It is human‑editable and **hot‑reloaded** within ~1 second of saving. ```json { "timing": { "multi_click_ms": 250, "long_press_ms": 500, "rotate_sensitivity": 1.0, "rotate_acceleration": 0.0 }, "led": { "mode": "volume", "brightness": 1.0, "pulse_speed": 255, "pulse_table": 0, "flash_on_ms": 200, "flash_off_ms": 800, "fade_speed": 1.0, "fade_delay": 1.0 }, "start_with_windows": false, "profiles": { "default": { "rotate_right": { "action": "volume_up", "amount": 0.04 }, "rotate_left": { "action": "volume_down", "amount": 0.04 }, "press": { "action": "mute" }, "double_press": { "action": "media_play_pause" }, "triple_press": { "action": "media_next" }, "long_press": { "action": "mic_mute_toggle" }, "press_rotate_right": { "action": "scroll_down", "amount": 3 }, "press_rotate_left": { "action": "scroll_up", "amount": 3 } }, "chrome.exe": { "rotate_right": { "action": "scroll_down", "amount": 3 }, "rotate_left": { "action": "scroll_up", "amount": 3 }, "long_press": { "action": "key", "keys": "f5" } } } } ``` --- ## Timing & Sensitivity | Setting | Default | Meaning | |---|---|---| | `multi_click_ms` | 250 | Window to detect double/triple clicks; a single press fires after this | | `long_press_ms` | 500 | Hold time required to trigger `long_press` | | `rotate_sensitivity` | 1.0 | Multiplier applied to each rotation delta before accumulation | | `rotate_acceleration` | 0.0 | Reserved for future acceleration curves | Lower `multi_click_ms` for snappier single presses (at the cost of harder double/triple detection); raise `rotate_sensitivity` to make a small turn do more. --- ## Building the EXE Requires `pyinstaller` (`pip install pyinstaller`) and the `libusb` package (in `requirements.txt`, which bundles the native DLL). ```powershell pip install -r requirements.txt pyinstaller pyinstaller --noconfirm --clean build.spec # Output: dist\PowerMate.exe (one file, ~25 MB) ``` `build.spec` automatically: - locates and bundles `libusb-1.0.dll` from the `libusb` package, - collects CustomTkinter's theme/asset data files, - bundles `assets/icon.ico`, - adds the required hidden imports (`comtypes`, `pycaw`, `pystray._win32`, pywin32 modules). At runtime the app finds the bundled DLL via `sys._MEIPASS`, so the EXE is fully self‑contained — only the **WinUSB driver** (Zadig) is needed on the target machine. --- ## Architecture ``` main.py App orchestration: worker thread, LED thread, GUI, tray, config wiring device.py USB layer: libusb backend discovery, find/claim, interrupt read, LED write gesture.py State machine: raw button/rotation -> high-level triggers (click timing) led.py LED controller thread: solid / software pulse / flash / volume-fade audio.py Native volume + mic control (pycaw), thread-safe across COM apartments actions.py Action executors (volume, media, scroll, keys, launch, run) app_monitor.py Foreground process detection for per-app profiles config.py Load/save/validate config.json with live file-watch reload tray.py System tray icon + menu (pystray) ui/ CustomTkinter GUI (app_window, profile_view, action_editor, led_panel, timing_panel) ``` **Threading model** - A **worker thread** owns the USB device: it reconnects on unplug, reads endpoint `0x81`, feeds packets to the gesture engine, resolves the active‑app profile, and dispatches actions. - A **LED thread** continuously applies the chosen LED mode. - The **GUI** runs on the main thread; status updates from other threads are marshalled with `Tk.after()`. - The **tray** runs its own thread. - **COM** is initialized per‑thread (audio is called from both the worker and LED threads), using thread‑local endpoint interfaces. --- ## Reverse‑Engineering Notes (Windows / WinUSB) These findings, discovered while validating against real hardware, are baked into the app: 1. **The device must be on the WinUSB driver.** With the default HID driver, libusb can enumerate the device and even fake `set_configuration`/`claim_interface`, but every real transfer fails with *"Operation not supported on this platform."* Zadig → WinUSB fixes it. 2. **Do not call `set_configuration()` on Windows.** WinUSB has already configured the device; calling it again invalidates the endpoint pipe handles, after which interrupt reads fail with *"Entity not found."* The app claims interface 0 and reads directly. 3. **Set the LED via interrupt‑OUT (endpoint `0x02`), not a vendor control transfer.** A vendor *control* transfer for brightness **permanently breaks the interrupt‑IN read pipe** on WinUSB ("Entity not found" on all subsequent reads) until the device is power‑cycled. The single‑byte interrupt‑OUT write — the method used by the original hardware — sets brightness reliably and does **not** disturb reads. Consequently the device's *hardware* pulse modes (also control transfers) are disabled on Windows, and pulse/flash are done in software. 4. **Rotation batches under slow polling.** A fast spin polled slowly arrives as one large signed delta, so the gesture engine accumulates deltas instead of counting packets. 5. **pyusb needs a native backend.** pyusb is bindings only; the `libusb` pip package supplies `libusb-1.0.dll`, which `device.py` locates automatically (bundle dir, then the installed package). --- ## Security Considerations A high-level assessment of the threat model: - **No remote attack surface.** The app opens no network sockets, servers, or ports. It talks only to a local USB device and the local Windows audio/input APIs. - **Runs with normal user privileges.** No elevation is required or requested. The *Start with Windows* entry is written to the per-user `HKCU` Run key (not system-wide `HKLM`), and its command is built from the executable/script path — not from user input, so it isn't injectable. - **Code-execution actions are user-defined and local.** The `run` (shell command), `launch` (open file/app/URL), `key`, and `macro` actions execute what *you* put in your own `config.json`. They run with your privileges — only bind commands you trust. The threat model is "the user configures their own knob"; there is no path for a remote party to inject actions. - `run` uses the shell (`shell=True`) so that shell features work; treat its value like a line you'd type into a terminal yourself. - **Input is bounded and parsed safely.** USB reports are fixed-size 6-byte reads; the config is parsed with `json.load` (no `eval`/`pickle`), and a malformed config falls back to defaults rather than crashing. - **Keyboard output only.** The app uses the `keyboard` library to *send* keystrokes for bindings; it does not install a global key logger/hook. No high-severity issues were identified. If you distribute the EXE, dependencies are sourced from PyPI and pinned in `requirements.txt`; rebuild from source if you require a verified supply chain. ## Testing Three test suites run **without** hardware: ```powershell python _test_gesture.py # gesture state machine: all 8 triggers + regressions python _test_features.py # macro action, startup module, debug panel, triple/long bindings python _smoketest_gui.py # builds the GUI, exercises profiles/panels/save, tears down ``` The gesture tests cover single/double/triple/long press, both rotations, press‑while‑rotate, and regressions for "no spurious press after press+rotate" and "no long‑press while rotating." The GUI smoke test verifies the window builds, the default profile auto‑loads all 8 trigger editors, bindings round‑trip through save, the LED/Timing tabs populate, and profile inheritance works. Hardware was validated end‑to‑end: rotation → volume, press → mute, button detection, and LED control all confirmed on a real PowerMate over WinUSB. --- ## Troubleshooting **"PowerMate not found" (red status / tray)** - Confirm the **WinUSB** driver is installed via Zadig (this is the most common cause). - Replug the device; check the status bar in **Settings**. - Inspect `powermate.log` next to the app. **Reads stop / LED works but knob does nothing** - Almost always a stale driver state — unplug and replug to power‑cycle the device. - Ensure no other program is holding the device (only one process can claim WinUSB). **LED doesn't light** - Make sure the LED mode isn't `off` and (for `solid`) brightness isn't 0. - Switch to `flash` briefly to confirm the device responds. **Keyboard shortcuts don't fire** - The target app must have focus. Some apps capture keys differently. - Verify the `keys` syntax (e.g. `ctrl+z`, `alt+tab`, `f5`). **Config changes don't apply** - The watcher reloads within ~1s; if your editor doesn't update the file's mtime, use **Reload** in Settings or restart. **Run from source for verbose logs** ```powershell python main.py debug ``` --- ## Project Layout ``` powermate-app/ ├── main.py # entry point / orchestration ├── device.py # USB communication + libusb backend discovery ├── gesture.py # gesture state machine ├── led.py # LED controller thread ├── audio.py # volume / mic (pycaw, COM-thread-safe) ├── actions.py # action executors ├── app_monitor.py # active-app detection ├── config.py # config load/save/watch ├── startup.py # start-with-Windows (HKCU Run key) ├── tray.py # system tray ├── ui/ │ ├── app_window.py # main settings window │ ├── profile_view.py # trigger→action grid │ ├── action_editor.py # action picker + key-capture │ ├── led_panel.py # LED settings + live test │ └── timing_panel.py # gesture timing ├── assets/icon.ico ├── config.example.json # template (your config.json is git-ignored) ├── requirements.txt ├── build.spec # PyInstaller one-file build ├── install.bat # convenience installer (deps + driver guidance) ├── _test_gesture.py # gesture unit tests ├── _test_features.py # macro / startup / debug-panel tests ├── _smoketest_gui.py # GUI smoke test ├── README.md └── QUICKSTART.md ``` --- ## Changelog See [`CHANGELOG.md`](CHANGELOG.md) for the full version history. Highlights: - **1.1.1** — security/memory hardening (config deepcopy, reload-spam fix, debug-panel idle optimization) and a documented vulnerability assessment. - **1.1.0** — audio control fix (pycaw API change), 3-step macros, knob-activity debug panel, Start-with-Windows toggle, tray/quit controls, and a `spotify.exe` profile. - **1.0.0** — initial release. ## Credits & License - Original Java PowerMate utility: **Nathan Sweet / Esoteric Software** — (New BSD License). - USB protocol reference: the Linux kernel **`drivers/input/misc/powermate.c`** driver. - Built on [pyusb](https://github.com/pyusb/pyusb), [libusb](https://libusb.info/), [pycaw](https://github.com/AndreMiras/pycaw), [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter), [pystray](https://github.com/moses-palmer/pystray), and [pywin32](https://github.com/mhammond/pywin32). Released under the **New BSD (3‑Clause) License**, consistent with the original project. See [`LICENSE`](LICENSE).