Add SayoDevice support, 15 key pads and theme styles
Second protocol family (8089:000b) behind a new backend seam, 15 key and 3 knob pads, Knobs North plus the style and colour grid, Windows import fix, window resize fix.
This commit is contained in:
422
README.md
422
README.md
@@ -15,7 +15,7 @@
|
||||
<img alt="Wayland and X11" src="https://img.shields.io/badge/Wayland%20%2B%20X11-yes-202326?style=flat-square">
|
||||
<img alt="Python 3.9 or newer" src="https://img.shields.io/badge/python-3.9%2B-202326?style=flat-square&logo=python&logoColor=white">
|
||||
<img alt="Qt 6" src="https://img.shields.io/badge/Qt-6-202326?style=flat-square&logo=qt&logoColor=white">
|
||||
<img alt="Tested on USB ID 1189:8840" src="https://img.shields.io/badge/tested%20on-1189%3A8840-eb8a2f?style=flat-square">
|
||||
<img alt="Tested on USB IDs 1189:8840 and 8089:000b" src="https://img.shields.io/badge/tested%20on-1189%3A8840%20%7C%208089%3A000b-eb8a2f?style=flat-square">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -124,7 +124,7 @@ Both write straight to the pad's own memory, so you can even use both. Pick whic
|
||||
| | MacroPad | [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) |
|
||||
|---|---|---|
|
||||
| How you use it | Click a drawing of your pad | Write a YAML file, run a command |
|
||||
| Pads | `1189:8840` (12 keys, 2 knobs) | `1189:8840`, `8842`, `8890`, several layouts |
|
||||
| Pads | `1189:8840` (12 or 15 keys, 2 or 3 knobs) and `8089:000b` (SayoDevice, 8 keys) | `1189:8840`, `8842`, `8890`, several layouts |
|
||||
| Systems | Linux | Linux, macOS, Windows |
|
||||
| Shortcuts and media keys | Yes | Yes |
|
||||
| Key sequences with delays | Not yet | Yes |
|
||||
@@ -142,128 +142,94 @@ Other people have built GUIs for these pads too, mostly for the smaller 3 and 6
|
||||
|
||||
## 🚀 Install
|
||||
|
||||
One command does everything: the app, the Qt libraries if you need them, the menu entry, and the USB permission rule. It asks before each step that needs your password, and it's safe to run again to update.
|
||||
|
||||
If you're comfortable in a terminal, [skip to the command](#step-2-copy-this-line). The steps below spell it out for anyone newer to Linux, since these pads are a lot of people's first reason to open one.
|
||||
|
||||
### Step 1: Open a terminal
|
||||
|
||||
A terminal is a window where you type commands instead of clicking. Nothing below can damage your system.
|
||||
|
||||
- **KDE Plasma** (CachyOS, Bazzite, Kubuntu, Fedora KDE, KDE neon): press the **Meta key** (the one with the Windows logo), type **`Konsole`**, press **Enter**.
|
||||
- **GNOME** (Ubuntu, Fedora, Pop!_OS): press the **Meta key**, type **`Terminal`**, press **Enter**.
|
||||
- **Almost anywhere**: try pressing **`Ctrl` + `Alt` + `T`** together.
|
||||
|
||||
### Step 2: Copy this line
|
||||
|
||||
Click the copy button on the right side of this box (or select the text and press `Ctrl+C`):
|
||||
One command does everything: the app, the Qt libraries if you need them, the
|
||||
menu entry, and the USB permission rule. It asks before each step that needs
|
||||
your password, and it's safe to run again to update.
|
||||
|
||||
```
|
||||
curl -fsSL https://raw.githubusercontent.com/Probler-Yt/MacroPad/main/install.sh | bash
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>What does that line actually do? (optional reading)</summary>
|
||||
Then **unplug the pad and plug it back in** so Linux applies the permission,
|
||||
open your app menu, type **MacroPad** and click it.
|
||||
|
||||
In plain words: "download the MacroPad installer from GitHub (`curl`), and run it (`| bash`)". The installer is a normal text file, and you can [read it here](install.sh) before running it if you like. Being suspicious of random commands from the internet is a healthy instinct, and I respect it.
|
||||
A **green dot** and **Pad ready** at the bottom left means it's working.
|
||||
Anything else, and the panel on the right explains it, or see
|
||||
[When something goes wrong](#-when-something-goes-wrong).
|
||||
|
||||
</details>
|
||||
|
||||
### Step 3: Paste it and press Enter
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Pasting into a terminal is **`Ctrl` + `Shift` + `V`**, not `Ctrl+V`. Right click, then Paste, also works.
|
||||
|
||||
### Step 4: Answer the questions
|
||||
|
||||
The installer says what it's doing as it goes, and asks before anything that needs your password. Pressing Enter accepts the default.
|
||||
|
||||
It needs the password for two things: installing Qt on systems that don't have it, and writing one permission file so the app can talk to your pad. Everything else stays in your home folder.
|
||||
|
||||
> [!NOTE]
|
||||
> Nothing appears on screen while you type a password in a terminal. No dots, no stars. That's deliberate, not a frozen prompt.
|
||||
|
||||
First run downloads around 100 MB on some systems. You'll see **All done.** when it's finished.
|
||||
|
||||
### Step 5: Replug the pad
|
||||
|
||||
Unplug it and plug it back in, so Linux applies the new permission.
|
||||
|
||||
### Step 6: Open MacroPad
|
||||
|
||||
Open your app menu, type **MacroPad**, and click it.
|
||||
The app reads your pad when it opens, so the drawing fills in with whatever is
|
||||
already on it. Keys still showing as hatched are ones it couldn't read; press
|
||||
**Read pad** to ask again.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/first-launch.png" width="820" alt="MacroPad on its first launch: every key is striped because the app hasn't written anything yet">
|
||||
</p>
|
||||
|
||||
A **green dot** and **Pad ready** at the bottom left means it's working. Anything else, and the panel on the right explains it, or see [When something goes wrong](#-when-something-goes-wrong).
|
||||
<details>
|
||||
<summary>New to the terminal? The longer version</summary>
|
||||
|
||||
The app reads your pad when it opens, so the drawing should fill in with whatever is already on it. Keys still showing as hatched are ones it couldn't read; press **Read pad** to ask again.
|
||||
A terminal is a window where you type commands instead of clicking. To open
|
||||
one: on KDE Plasma press the Meta key, type `Konsole`, press Enter. On GNOME,
|
||||
Meta key, type `Terminal`, Enter. Almost anywhere, `Ctrl` + `Alt` + `T`.
|
||||
|
||||
> [!TIP]
|
||||
> To keep it handy: right click **MacroPad** in your app menu and choose **Pin to Task Manager** (Plasma) or **Pin to Dash** (GNOME).
|
||||
Paste with **`Ctrl` + `Shift` + `V`**, not `Ctrl+V`. Right click then Paste
|
||||
also works.
|
||||
|
||||
That command downloads the installer from GitHub (`curl`) and runs it
|
||||
(`| bash`). It's a normal text file and you can [read it first](install.sh).
|
||||
Being suspicious of commands off the internet is a healthy instinct.
|
||||
|
||||
The installer needs your password for two things: installing Qt on systems
|
||||
that don't have it, and writing one permission file so the app can reach your
|
||||
pad. Everything else stays in your home folder. Nothing appears on screen
|
||||
while you type a password in a terminal, no dots or stars, which is deliberate
|
||||
rather than a frozen prompt. First run downloads around 100 MB on some
|
||||
systems, and you'll see **All done.** at the end.
|
||||
|
||||
To keep it handy, right click **MacroPad** in your app menu and choose **Pin
|
||||
to Task Manager** (Plasma) or **Pin to Dash** (GNOME).
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Your first binding
|
||||
|
||||
### 1. Click a key on the drawing
|
||||
**Click a key on the drawing.** It gets an orange outline and the panel on the
|
||||
right shows what it currently does.
|
||||
|
||||
It gets an orange outline, and the panel on the right shows what it currently does.
|
||||
**Say what it should do.** Four ways, whichever suits:
|
||||
|
||||
### 2. Type what you want it to do
|
||||
- **Shortcut**, and type the combo with `+` between keys: `ctrl+z`,
|
||||
`ctrl+shift+t`, `meta+e`, `f5`. Underneath, it confirms what the key will
|
||||
send.
|
||||
- **Record**, then press the combo on your keyboard and it fills the box in
|
||||
for you. It records the physical key, so the pad sends that same key
|
||||
whatever your layout. Your desktop grabs some combos (most `Meta` ones)
|
||||
before any app sees them, so type those instead.
|
||||
- **Media key**, and pick from the list. Volume and brightness on the knobs is
|
||||
a classic.
|
||||
- **Nothing**, to clear the key.
|
||||
|
||||
Click **Shortcut**, then type the combo into the box using `+` between keys: `ctrl+z`, `ctrl+shift+t`, `meta+e`, `f5`. Underneath, it confirms what the key will send. The key on the drawing gets a strip of orange tape to show it's changed but not sent yet.
|
||||
Either way the key gets a strip of orange tape, meaning changed but not sent.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-type.png" width="820" alt="Typing ctrl+z for key 2. The app says Sends Ctrl+Z and the key has orange tape on it">
|
||||
</p>
|
||||
|
||||
### Or press Record and just do it
|
||||
|
||||
Click **Record**, then press the actual combo on your keyboard. The app catches it and fills in the box for you. It records the physical key you pressed, so the pad presses that same key whatever your keyboard layout (UK, German, French, anything).
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-record.png" width="820" alt="Record mode: the box glows orange and says press the combo now">
|
||||
</p>
|
||||
|
||||
> [!NOTE]
|
||||
> Your desktop grabs some combos (like most `Meta` shortcuts) before any app can see them. If Record doesn't catch one, just type it instead.
|
||||
|
||||
### Or pick a media key
|
||||
|
||||
Click **Media key** and pick from the list. Volume and brightness on the knobs is a classic.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-media.png" width="820" alt="Choosing Volume down for dial 1 turn left from the list of media keys">
|
||||
</p>
|
||||
|
||||
### Or make it do nothing
|
||||
|
||||
To clear a binding, click **Nothing** and write it. Keys set this way show **Nothing** in grey.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-nothing.png" width="820" alt="Key 5 set to Nothing, waiting to be written, with key 1 already showing Nothing in grey">
|
||||
</p>
|
||||
|
||||
### 3. Write it to the pad
|
||||
|
||||
Click **Write to pad**. Changed a few things? The orange **Write N changes** button at the bottom right sends them all at once.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-pending.png" width="820" alt="Three keys with orange tape waiting to be written, and a Write 3 changes button">
|
||||
</p>
|
||||
|
||||
The bottom bar tells you exactly what happened. Green means it worked.
|
||||
**Write it.** Click **Write to pad**, or if you've changed several, the orange
|
||||
**Write N changes** button at the bottom right sends them all at once. The
|
||||
bottom bar says exactly what happened, and green means it worked.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/step-written.png" width="820" alt="After writing: the keys are solid white and the bottom bar says Wrote 3 changes to the pad">
|
||||
</p>
|
||||
|
||||
### 4. Press the key
|
||||
|
||||
**Your bindings live on the pad itself**, so they keep working after a reboot, on another computer, even on Windows. Nothing stays running in the background, and you can close the app. (The one exception is [actions](#-make-a-key-open-a-web-page-an-app-type-text-or-run-a-command), which are opt in.)
|
||||
**Press the key.** Your bindings live on the pad itself, so they keep working
|
||||
after a reboot, on another computer, even on Windows. Nothing runs in the
|
||||
background and you can close the app. The one exception is
|
||||
[actions](#-make-a-key-open-a-web-page-an-app-type-text-or-run-a-command),
|
||||
which are opt in.
|
||||
|
||||
---
|
||||
|
||||
@@ -363,7 +329,7 @@ Some people stand the pad up with the knobs on top. Some lay it flat with the kn
|
||||
|
||||
## 📥 Already set it up with the vendor app?
|
||||
|
||||
If you configured your pad with `MINI_KEYBOARD.exe` and recorded it with the included capture tool ([how](#capture-what-the-vendor-software-sends)), click **Import capture...** and choose the file. The app reads what was saved and fills in the drawing, so those keys stop showing as unknown.
|
||||
If you configured your pad with `MINI_KEYBOARD.exe` and recorded it with the included capture tool ([how](docs/protocol.md)), click **Import capture...** and choose the file. The app reads what was saved and fills in the drawing, so those keys stop showing as unknown.
|
||||
|
||||
Only do this if nothing has changed on the pad since that capture.
|
||||
|
||||
@@ -410,7 +376,7 @@ The app is quite clever here. It checks the three usual reasons this happens and
|
||||
|
||||
- **No rule yet.** The permission file doesn't exist.
|
||||
- **Rule not loaded.** It exists, but Linux hasn't noticed. Replug the pad.
|
||||
- **Rule named wrong.** If you made one yourself called something like `99-macropad.rules`, it's read *too late* to work. It needs a number below 73. The app gives you the exact rename command. ([Why?](#the-udev-rule-that-cost-me-an-hour))
|
||||
- **Rule named wrong.** If you made one yourself called something like `99-macropad.rules`, it's read *too late* to work. It needs a number below 73. The app gives you the exact rename command. ([Why?](docs/protocol.md))
|
||||
|
||||
### "Heads up: keyd is remapping this pad"
|
||||
|
||||
@@ -509,7 +475,9 @@ A few ideas for KDE Plasma, where these are the default shortcuts:
|
||||
|
||||
## 🧬 Other pads in this family
|
||||
|
||||
This app is **confirmed on `1189:8840`**, the 12 key, 2 knob pad. It's the only one it knows so far.
|
||||
This app is **confirmed on `1189:8840`**, in the 12 key, 2 knob and 15 key, 3 knob shapes, and on **`8089:000b`**, a SayoDevice 8 key pad that speaks an entirely different protocol.
|
||||
|
||||
Those two are separate backends. A pad family is one file in `macropad_gui/backends/` implementing six methods, and everything above that line works in terms of keys and knobs rather than bytes, so adding a third family does not touch the app.
|
||||
|
||||
> [!TIP]
|
||||
> **Have a different pad right now?** [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) supports `1189:8840`, `1189:8842` and `1189:8890` in several layouts (3, 6, 9, 12 and 15 keys) from the command line, today.
|
||||
@@ -520,57 +488,17 @@ Siblings come in other sizes, and several share vendor ID `1189`. **They don't a
|
||||
<img src="docs/images/unsupported.png" width="820" alt="The app explaining that it found a 1189:8890 pad that it doesn't support yet">
|
||||
</p>
|
||||
|
||||
### Start here: ask the pad
|
||||
|
||||
Install MacroPad, plug your pad in, and run:
|
||||
**If you have one of those pads**, MacroPad can ask it what it is without
|
||||
writing anything to it:
|
||||
|
||||
```
|
||||
macropad detect
|
||||
```
|
||||
|
||||
On most pads in this family that's the whole job. It asks the pad how many keys and knobs it has, reads back what's currently bound, and prints a report. Paste that into [an issue](https://github.com/Probler-Yt/MacroPad/issues) and your pad gets added to the known list.
|
||||
|
||||
The app offers the same thing: when it sees a pad it doesn't recognise, the panel has an **Ask the pad what it is** button.
|
||||
|
||||
### How it decides
|
||||
|
||||
Detection is read only, and the pad has to pass two tests before the app will write anything to it:
|
||||
|
||||
1. It answers the `0xFB` query with a sensible key and knob count.
|
||||
2. It then reports a full layer, describing exactly the control slots that count implies.
|
||||
|
||||
A pad from a different family fails one of those, and the app leaves it alone rather than guessing.
|
||||
|
||||
The pad reports how many keys and knobs it has, but not how they're arranged, so the first guess at rows and columns may be wrong. Fix it with [Edit layout](#-if-the-drawing-doesnt-match-your-pad) and the drawing will match your hardware.
|
||||
|
||||
### If it doesn't answer
|
||||
|
||||
Some pads in this family speak an older protocol and won't answer. Working those out needs a capture of the vendor software, the same process used for this one. You'll need `MINI_KEYBOARD.exe` and Wine.
|
||||
|
||||
#### Capture what the vendor software sends
|
||||
|
||||
1. **See what your pad is:**
|
||||
```
|
||||
python3 ~/.local/share/macropad/app/macropad-probe.py
|
||||
```
|
||||
This only reads. It lists your pad's ID and its USB interfaces.
|
||||
|
||||
2. **Start the capture** (it watches USB traffic; it never sends anything):
|
||||
```
|
||||
cd ~/.local/share/macropad/app
|
||||
sudo python3 macropad-capture.py
|
||||
```
|
||||
|
||||
3. **In a second terminal**, open the vendor app with Wine:
|
||||
```
|
||||
wine MINI_KEYBOARD.exe
|
||||
```
|
||||
|
||||
4. In the vendor app, set **key 1 to the letter `a`** and click its save/apply button. Then set **each knob's turn left, press and turn right** to different letters, and save again.
|
||||
|
||||
5. Go back to the first terminal and press **`Ctrl+C`**. You now have a file called `macropad-capture.txt`.
|
||||
|
||||
6. **[Open an issue](https://github.com/Probler-Yt/MacroPad/issues)** with that file, your `macropad-probe.py` output, and a photo of your pad. That's genuinely all it takes to get your pad supported.
|
||||
Paste what that prints into an issue and your pad can be added. The full
|
||||
process, including what the app checks before it will write to an unknown
|
||||
pad and what to do when one doesn't answer, is in
|
||||
**[docs/adding-a-pad.md](docs/adding-a-pad.md)**.
|
||||
|
||||
---
|
||||
|
||||
@@ -586,211 +514,13 @@ That said, the app is built on Qt, which runs on Windows and macOS too. The only
|
||||
|
||||
## 🤓 For the nerds
|
||||
|
||||
<details>
|
||||
<summary><b>How this pad was reverse engineered</b></summary>
|
||||
Both wire protocols, byte by byte, plus the project layout and how the
|
||||
backends are split, live in **[docs/protocol.md](docs/protocol.md)**.
|
||||
|
||||
1. **Probe.** `macropad-probe.py` walks `/sys/class/hidraw` and decodes each HID report descriptor. The pad shows up as two interfaces: a normal keyboard (`Generic Desktop`) and a vendor defined one (usage page `0xff00`) with a 64 byte input and output report, ID 3. That's the config channel.
|
||||
2. **Guess.** The upstream project documents two protocols, "Legacy" and "Extended". Both were tried. The pad accepted the reports and then quietly ignored them. Educated guessing had run out.
|
||||
3. **Watch.** The breakthrough was noticing `MINI_KEYBOARD.exe` runs under Wine. Linux's `usbmon` can record every USB transfer, so `macropad-capture.py` watched the real software write a single key. Twenty minutes of watching beat hours of guessing.
|
||||
4. **Repeat.** Separate captures of the knobs and of media keys filled in the rest. Every packet from every capture is now replayed by the test suite.
|
||||
|
||||
Lesson learned the hard way: [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) had already worked out most of this, and its source is the best reference for these pads. Search before you sniff.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>The wire format</b></summary>
|
||||
|
||||
Every binding is two 65 byte writes to `/dev/hidrawN`: a config report, then a commit.
|
||||
|
||||
```
|
||||
03 fd 01 01 01 00 00 00 00 00 01 00 05 00 ... key 1 -> 'b'
|
||||
│ │ │ │ │ └──┬┘ └───┬──┘ │ │ └── keycode (HID usage 0x05 = b)
|
||||
│ │ │ │ │ │ │ │ └───── modifier bitmask
|
||||
│ │ │ │ │ │ │ └──────── count
|
||||
│ │ │ │ │ │ └───────────── unused
|
||||
│ │ │ │ │ └──────────────────── delay? (unverified, see below)
|
||||
│ │ │ │ └────────────────────────── type: 1 keys, 2 media, 3 mouse, 8 LED
|
||||
│ │ │ └───────────────────────────── layer, counted from 1
|
||||
│ │ └──────────────────────────────── action: which control
|
||||
│ └─────────────────────────────────── magic
|
||||
└────────────────────────────────────── report ID
|
||||
|
||||
03 fd fe ff 00 00 ... commit
|
||||
```
|
||||
|
||||
**Action bytes on 1189:8840**
|
||||
|
||||
| Control | Bytes |
|
||||
|---|---|
|
||||
| Keys 1 to 12 | `0x01` to `0x0c` (the firmware reserves up to `0x0f`) |
|
||||
| Knob 1: turn left, press, turn right | `0x10`, `0x11`, `0x12` |
|
||||
| Knob 2: turn left, press, turn right | `0x13`, `0x14`, `0x15` |
|
||||
|
||||
**Media keys** pack differently: count is always `2`, and the 16 bit consumer usage follows it directly, little endian.
|
||||
|
||||
```
|
||||
03 fd 08 01 02 00 00 00 00 00 02 92 01 00 ... key 8 -> Calculator (usage 0x0192)
|
||||
```
|
||||
|
||||
**Modifier bitmask** (standard HID): `0x01` Ctrl, `0x02` Shift, `0x04` Alt, `0x08` Meta, and `0x10` to `0x80` for the right hand versions.
|
||||
|
||||
The vendor app sometimes sends several configs and one commit at the end. Both styles work. This app commits after every binding, so it always knows exactly which ones landed.
|
||||
|
||||
**Delays** are the open question. The byte field above comes from rOzzy1987's format and hasn't been seen on this pad. ch57x-keyboard-tool sends a delay as a separate report of type `5` instead, which is probably right. That's why sequences are switched off in the app until a capture settles it.
|
||||
|
||||
</details>
|
||||
|
||||
<a name="what-we-found"></a>
|
||||
<details>
|
||||
<summary><b>Three sources, compared</b></summary>
|
||||
|
||||
What each source sends to a `1189:8840`. The pad accepts the ch57x-keyboard-tool style and the vendor style, so it's evidently tolerant. Nothing here is a new discovery about the hardware; the last column is simply what the vendor's own software does, captured independently.
|
||||
|
||||
| | rOzzy1987 "Extended" | [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) | Vendor app (captured) |
|
||||
|---|---|---|---|
|
||||
| Byte 1 of a config report | `0xFE` | `0xFE` | **`0xFD`** |
|
||||
| Layers counted from | `0` | `1` | `1` |
|
||||
| First knob action | `13` | `0x10` | `0x10` |
|
||||
| Media: count, usage offset | from payload, 12 | `0`, 11 | **`2`**, 11 |
|
||||
| Delay | bytes 5 and 6 | separate report, type `5` | not captured yet |
|
||||
| Finishing a write | flash command | `aa aa`, `fd fe ff`, `aa aa` | `fd fe ff` |
|
||||
|
||||
rOzzy1987's format is for different product IDs, which is why porting it to this pad failed until the capture.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Reading the pad, and how we nearly missed it</b></summary>
|
||||
|
||||
The project ran for weeks on the assumption that these pads were write only. Two things caused that:
|
||||
|
||||
- Nothing documented a read. Neither rOzzy1987's project nor ch57x-keyboard-tool implements one.
|
||||
- The capture tool only recorded **outgoing** transfers. So even though the pad had been answering all along, nothing was ever written down.
|
||||
|
||||
What settled it was noticing that the vendor software has a "view settings" button, and that it showed bindings written by MacroPad. It couldn't have known those from a file, so it was asking the pad. Capturing both directions gave the answer.
|
||||
|
||||
Two query commands, both answered on the interrupt IN endpoint of the same report id:
|
||||
|
||||
```
|
||||
03 fb ... what are you?
|
||||
03 fb 0c 02 reply: 12 keys, 2 knobs
|
||||
|
||||
03 fa 0f 03 01 02 ... send me layer 1
|
||||
(15 key slots, 3 layers, layer 1, 2 knobs)
|
||||
03 fa 02 01 01 00 ... 01 05 17 key 2 is ctrl+alt+t
|
||||
03 fa 10 01 02 00 ... 01 ea 00 dial 1 left is volume down
|
||||
... one report per control, then silence
|
||||
```
|
||||
|
||||
Replies use the same layout as a config write, with `0xFA` in place of the `0xFD` magic. Media replies carry a count of 1 where writes send 2; both are accepted.
|
||||
|
||||
Three things fell out of it:
|
||||
|
||||
- **The pad reports its own layout**, so key and knob counts don't have to be guessed from the USB ID.
|
||||
- **There are three layers**, and the firmware keeps 15 key slots and 3 knobs whatever the hardware has. Ours answers for 6 controls that don't physically exist.
|
||||
- **The dial mapping is confirmed from the other direction.** Reading back what we had written returns exactly the mapping we assumed, which is stronger evidence than a write that appeared to work.
|
||||
|
||||
The vendor's request has trailing bytes that change between runs and look like uninitialised memory from the Windows program. Zeros work fine.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>usbmon only shows you 32 bytes</b></summary>
|
||||
|
||||
The kernel's `usbmon` text interface caps captured data at 32 bytes per transfer. Every line in a capture is exactly 32 bytes, even though the reports are 65. For single keys and media that's plenty. For long key sequences, anything past the 10th key lands in bytes nobody has seen yet. If you're capturing long macros, use Wireshark or the binary `/dev/usbmonN` interface instead.
|
||||
|
||||
</details>
|
||||
|
||||
<a name="the-udev-rule-that-cost-me-an-hour"></a>
|
||||
<details>
|
||||
<summary><b>The udev rule that cost me an hour</b></summary>
|
||||
|
||||
The standard way to give desktop users access to a device is `TAG+="uaccess"` in a udev rule. Everyone names these `99-something.rules`. **That doesn't work.**
|
||||
|
||||
udev reads every rules file in one lexical order by filename. The tag is only turned into an actual permission by systemd's `73-seat-late.rules`, which contains:
|
||||
|
||||
```
|
||||
TAG=="uaccess", ENV{MAJOR}!="", RUN{builtin}+="uaccess"
|
||||
```
|
||||
|
||||
A rule in `99-macropad.rules` adds the tag *after* that line has already run. The device is tagged, and nothing ever acts on it. Same for a file with no number at all, since letters sort after digits. The rule has to sort before `73-`, so MacroPad uses `60-macropad.rules`.
|
||||
|
||||
The app's permission check reads `udevadm info` to see whether the device got the tag, finds any rule file mentioning the pad, and compares its name against whichever file on *your* system applies the permission.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Where the app keeps its notes</b></summary>
|
||||
|
||||
`~/.config/macropad/state-1189-8840.json` holds what the app last read from or wrote to the pad. Since the pad can be read, this is a cache rather than the source of truth. Every entry is exactly a command the CLI understands:
|
||||
|
||||
```json
|
||||
{
|
||||
"device": "1189:8840",
|
||||
"format": 1,
|
||||
"layers": {
|
||||
"1": {
|
||||
"dial1-left": { "binding": { "media": "volumedown" }, "written": "2026-09-11T18:02:11" },
|
||||
"key4": { "binding": { "keys": "super+ctrl+left" }, "written": "2026-09-11T18:02:11" },
|
||||
"key7": { "reason": "write interrupted after 1 of 2 reports", "unknown": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It's written atomically (write, then rename), so a crash can never leave half a file.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Command line</b></summary>
|
||||
|
||||
No window needed. `macropad.py` is plain Python with nothing to install:
|
||||
|
||||
```
|
||||
cd ~/.local/share/macropad/app
|
||||
python3 macropad.py controls # every name you can use
|
||||
python3 macropad.py set key1 ctrl+c
|
||||
python3 macropad.py set dial1-right volumeup
|
||||
python3 macropad.py set key3 "ctrl+shift+n" --dry-run # show the bytes, send nothing
|
||||
python3 macropad.py set action:0x10 f5 # raw action byte, for other pads
|
||||
macropad doctor # the app's permission check, in text
|
||||
```
|
||||
|
||||
The CLI doesn't update the app's notes. Things set this way show as whatever the app last knew.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Tests</b></summary>
|
||||
|
||||
```
|
||||
python3 -m unittest -v
|
||||
```
|
||||
|
||||
The suite replays every packet from every real capture in `tests/captures/`, decodes it, re-encodes it, and checks the bytes match exactly. It also covers the permission diagnosis, half-finished writes, and builds the whole window offscreen. Screenshots in this README are generated by `docs/make_screenshots.py` from the real app.
|
||||
|
||||
</details>
|
||||
|
||||
### Project layout
|
||||
|
||||
```
|
||||
macropad.py the protocol and the command line tool (no dependencies)
|
||||
macropad_gui/
|
||||
core.py bindings, the app's notes, the permission check, writing
|
||||
padview.py the drawing of the pad
|
||||
themes.py palettes and drawing styles
|
||||
fonts/ the Sketch theme's handwriting font, with its licence
|
||||
app.py the window
|
||||
keymap.py physical key to HID name, for Record
|
||||
macropad-probe.py read only: what is this device?
|
||||
macropad-capture.py watch what the vendor software sends (usbmon)
|
||||
macropad-handshake.py which report IDs does a pad accept?
|
||||
install.sh, uninstall.sh
|
||||
packaging/ launcher icon and the udev rule
|
||||
tests/ real captures and the tests that replay them
|
||||
```
|
||||
The short version: `1189:8840` was reverse engineered from USB captures of
|
||||
the vendor's software, and `8089:000b` from WebHID captures of the vendor's
|
||||
web configurator. Every packet in both is replayed by the test suite, so
|
||||
nothing in there is a guess.
|
||||
|
||||
---
|
||||
|
||||
@@ -798,6 +528,8 @@ tests/ real captures and the tests that replay them
|
||||
|
||||
- [x] Keys, shortcuts and media keys on `1189:8840`
|
||||
- [x] Both knobs, all three actions each
|
||||
- [x] 15 key, 3 knob pads, knobs on either side
|
||||
- [x] A second protocol: SayoDevice `8089:000b`, decoded from scratch
|
||||
- [x] Upright and flat views
|
||||
- [x] Reading the pad, so the app shows what's really on it
|
||||
- [x] One line installer for any distro
|
||||
|
||||
Reference in New Issue
Block a user