| 1 | <div align="center"> |
| 2 | <img src="docs/logo.svg" width="320" alt="budctl"> |
| 3 | <p>Control your wireless earbuds from Linux and Android, without the vendor app.</p> |
| 4 | </div> |
| 5 | |
| 6 | Noise control, EQ, game mode, touch controls, battery and more. There is a command-line tool, |
| 7 | `budctl`, a desktop app, `budctl-gui`, and an [Android app](#android-app), all built on the same |
| 8 | library. |
| 9 | |
| 10 | - No telemetry and no account. budctl talks to your buds over Bluetooth, through BlueZ on Linux. |
| 11 | The Android app does not even have the internet permission. |
| 12 | - The settings live on the buds, so a change made on one device shows on the others. |
| 13 | |
| 14 | **Supported buds:** realme Buds T310. More are added as the author gets them. |
| 15 | |
| 16 | <p align="center"> |
| 17 | <img src="docs/screenshots/overview.png" width="560" alt="budctl-gui's Overview page: battery of each bud, noise control, and switches such as game mode and 3D sound"> |
| 18 | </p> |
| 19 | |
| 20 | ## Install |
| 21 | |
| 22 | You need Linux with BlueZ, and for the desktop app a Wayland desktop. Release builds are for |
| 23 | x86_64 and aarch64 (glibc 2.35 or newer). |
| 24 | |
| 25 | ```sh |
| 26 | curl -fsSL https://github.com/krolyxon/budctl/releases/latest/download/install.sh | sh |
| 27 | ``` |
| 28 | |
| 29 | This puts `budctl` and `budctl-gui` in `~/.local/bin`, and a launcher entry and its icon in |
| 30 | `~/.local/share`. The download is checked against its SHA-256 checksum. Options: |
| 31 | |
| 32 | ```sh |
| 33 | sh install.sh --version 0.1.1 # a given release instead of the latest |
| 34 | sh install.sh --prefix /usr/local # elsewhere (uses sudo when it must) |
| 35 | sh install.sh --uninstall # remove them again; your config and log stay |
| 36 | ``` |
| 37 | |
| 38 | To build from source instead (Rust 1.88 or newer), run `./install.sh --source` in a checkout. |
| 39 | |
| 40 | On Arch, `contrib/arch/PKGBUILD` packages the release builds: run `makepkg -si` in that directory. |
| 41 | |
| 42 | Pair the buds and connect them in your Bluetooth settings first. |
| 43 | |
| 44 | ## Command line |
| 45 | |
| 46 | ```sh |
| 47 | budctl list # paired buds, and whether budctl knows them |
| 48 | budctl status # battery, noise control, EQ, switches |
| 49 | budctl anc off|on|transparency # noise control |
| 50 | budctl anc light|medium|deep # noise cancelling, at this strength |
| 51 | budctl eq # the EQ presets, and the one in use |
| 52 | budctl eq deep-bass # select a preset |
| 53 | budctl eq custom-4 50=+3 16000=-2 # change bands of a custom preset (dB) |
| 54 | budctl eq new Gym # create a custom preset: flat, and put to use |
| 55 | budctl eq rename custom-4 "Gym mix" # rename a custom preset |
| 56 | budctl eq delete custom-4 # delete a custom preset |
| 57 | budctl game on|off # game mode (low latency) |
| 58 | budctl set bass-boost|spatial|wind|dual on|off |
| 59 | budctl dual # the phones and computers dual connection keeps |
| 60 | budctl bass low 2 # bass boost, band by band (low, medium, high): -5 to +5 |
| 61 | budctl touch # what each tap and hold does |
| 62 | budctl touch left triple-tap previous # give a gesture an action (--dry-run first, if you like) |
| 63 | budctl touch cycle off anc # the modes a hold steps through |
| 64 | budctl find start --yes # ring the buds (loud: take them out of your ears) |
| 65 | budctl find stop |
| 66 | ``` |
| 67 | |
| 68 | - `--json` prints machine-readable output for scripts and status bars (`"schema": 1`). |
| 69 | - `--debug` hex-dumps every frame sent and received, on stderr. |
| 70 | - `--device <MAC>` picks the buds when more than one supported pair is connected. Without it, |
| 71 | `"device"` in the config file (below) is used, if set. |
| 72 | |
| 73 | Exit codes: |
| 74 | |
| 75 | | Code | Meaning | |
| 76 | |---|---| |
| 77 | | 0 | success | |
| 78 | | 1 | usage error | |
| 79 | | 2 | buds not found or not connected | |
| 80 | | 3 | control channel busy | |
| 81 | | 4 | the buds rejected the command or did not answer | |
| 82 | |
| 83 | ## Desktop app |
| 84 | |
| 85 | `budctl-gui` has five pages: |
| 86 | |
| 87 | - **Overview:** battery, noise control and its strength, the switches, the bass level. |
| 88 | - **Equalizer:** the presets; create, rename and delete your own, and change their bands. |
| 89 | - **Gestures:** what each tap and hold does on each bud, and what a hold steps through. |
| 90 | - **Earbud settings:** dual connection and the devices it keeps, find my earbuds, firmware, audio codec. |
| 91 | - **App settings:** where the colors come from, the keys, the version. |
| 92 | |
| 93 | <table> |
| 94 | <tr> |
| 95 | <td valign="top"><img src="docs/screenshots/equalizer.png" alt="Equalizer: the built-in presets, a custom preset with its pencil and bin, and its six bands open below"></td> |
| 96 | <td valign="top"><img src="docs/screenshots/gestures.png" alt="Gestures: double tap, triple tap and hold for each bud, and the modes a hold steps through"></td> |
| 97 | </tr> |
| 98 | <tr> |
| 99 | <td valign="top"><img src="docs/screenshots/earbuds.png" alt="Earbud settings: the dual connection switch, the devices the buds keep, find my earbuds, firmware and codec"></td> |
| 100 | <td valign="top"><img src="docs/screenshots/keys.png" alt="The keys overlay, shown with ?"></td> |
| 101 | </tr> |
| 102 | </table> |
| 103 | |
| 104 | The screenshots show `--demo`'s made-up buds. |
| 105 | |
| 106 | The app connects when it needs to, and frees the buds' control channel 10 s after your last |
| 107 | change, so other apps can use it. A change shows "Saving…" until the buds confirm it. |
| 108 | |
| 109 | The mouse works everywhere. Vim-like keys work on top of it (press `?` in the window): |
| 110 | |
| 111 | | Keys | What they do | |
| 112 | |---|---| |
| 113 | | `j` `k`, arrows, Tab | move the cursor | |
| 114 | | `gg` `G` | first / last item | |
| 115 | | `h` `l` | previous / next choice, switch off / on; `l` opens a row's page | |
| 116 | | Enter, Space | toggle, step through the choices, or open | |
| 117 | | `J` `K`, Ctrl+Tab, `gt` `gT` | next / previous page | |
| 118 | | `1` … `5` | go to a page | |
| 119 | | `r` | read the buds again | |
| 120 | | Esc | hide the cursor | |
| 121 | | `q` | quit | |
| 122 | |
| 123 | The cursor shows only after you press a key, and the first key only shows it. |
| 124 | |
| 125 | **Look:** Nothing OS shapes and type in Material You colors: dot-matrix page names and battery |
| 126 | figures, LED rings and dotted sliders, pill switches and tracks. The fonts (Doto, Space Grotesk, |
| 127 | Space Mono) are built into the app. |
| 128 | |
| 129 | **Colors:** a built-in palette (near-black, white "on" states, red for live things such as the open |
| 130 | page's dot and "Saving…"), or a [matugen](https://github.com/InioX/matugen) palette: `--theme |
| 131 | <file>`, else `~/.config/budctl/theme.json`. The file is JSON with Material 3 roles, `on_x` spelled |
| 132 | `xFg`, all optional: `primary`, `primaryFg`, `primaryContainer`, `primaryContainerFg`, `tertiary`, |
| 133 | `tertiaryFg`, `surfaceLowest`, `surfaceLow`, `surfaceHigh`, `surfaceHighest`, `surfaceFg`, |
| 134 | `surfaceVariantFg`, `outline`, `outlineVariant`, `err`, `errFg` (`bg` and `fg` still work for |
| 135 | `surfaceLowest` and `surfaceFg`; other keys are ignored). A matugen template only needs lines like |
| 136 | `"primary": "{{colors.primary.default.hex}}"`. The file is read again whenever the window gets |
| 137 | focus, so a new wallpaper's colors show up there too. When the file is missing, the app writes the |
| 138 | built-in colors there: a starting point for a template. |
| 139 | |
| 140 | **Log:** each launch writes errors and session events (connected, each change and its answer, |
| 141 | closed) to `~/.local/state/budctl/gui.log`, replacing the last launch's. |
| 142 | |
| 143 | Other options: `--demo` shows made-up buds without Bluetooth; `--debug` and `--device` work as in |
| 144 | the command-line tool. |
| 145 | |
| 146 | ## Android app |
| 147 | |
| 148 | `android/` holds an Android app (Android 12 or newer) with the desktop app's pages and look, on a |
| 149 | phone: the same Rust library runs inside it, and only the Bluetooth link and the screen are Kotlin. |
| 150 | Its colors come from your wallpaper (Material You), or from the desktop app's near-black palette. |
| 151 | |
| 152 | <p align="center"> |
| 153 | <img src="docs/screenshots/android-overview.png" width="240" alt="The Android app's Overview: the battery rings, noise control and its strength, and the switches"> |
| 154 | <img src="docs/screenshots/android-equalizer.png" width="240" alt="The Android app's Equalizer: the presets, and a custom preset's six bands open below"> |
| 155 | <img src="docs/screenshots/android-gestures.png" width="240" alt="The Android app's Gestures: what each tap and hold does on each bud, and what a hold steps through"> |
| 156 | </p> |
| 157 | |
| 158 | The app talks to the buds only while it is on screen, and frees their control channel 10 s after |
| 159 | your last change or when you leave it, so your computer can use it. It has no internet permission, |
| 160 | and its build fails if anything adds one. A switch on its App page turns on the debug log, which you |
| 161 | can copy from there. |
| 162 | |
| 163 | To install it, download `budctl.apk` from the |
| 164 | [latest release](https://github.com/krolyxon/budctl/releases/latest) on your phone and open it |
| 165 | (Android asks you to allow installs from your browser or file manager). New versions come out as |
| 166 | new releases: an app such as Obtainium can watch them for you, since budctl itself never checks. |
| 167 | The APK is signed with budctl's own key. Its certificate's SHA-256 fingerprint is |
| 168 | `45:6F:9B:93:AA:40:1D:57:AA:6C:75:0D:FF:13:FA:82:5A:7B:28:B6:3D:D0:40:99:80:6F:A9:7C:14:75:23:12`. |
| 169 | |
| 170 | To build it yourself instead and put it on your phone (with USB debugging on), you need Android |
| 171 | Studio or the Android SDK with its NDK, the Rust targets |
| 172 | (`rustup target add aarch64-linux-android x86_64-linux-android`) and `cargo install cargo-ndk`: |
| 173 | |
| 174 | ```sh |
| 175 | cd android |
| 176 | ./gradlew installDebug |
| 177 | ``` |
| 178 | |
| 179 | A build of your own is signed with your machine's debug key, so Android will not install a release |
| 180 | over it, or the other way round: uninstall one before installing the other. |
| 181 | |
| 182 | How it is put together: [`docs/android.md`](docs/android.md). The screenshots show made-up buds |
| 183 | (`adb shell am start -n io.github.krolyxon.budctl/.MainActivity --ez demo true`). |
| 184 | |
| 185 | ## Config |
| 186 | |
| 187 | `~/.config/budctl/config.json` is created with the defaults the first time it is needed. Every key |
| 188 | is optional; a file with a mistake is reported and the defaults are used. |
| 189 | |
| 190 | ```json |
| 191 | { |
| 192 | "device": null, |
| 193 | "gui": { |
| 194 | "idle_seconds": 10, |
| 195 | "start_page": "overview", |
| 196 | "motion": true |
| 197 | } |
| 198 | } |
| 199 | ``` |
| 200 | |
| 201 | - `device`: the buds to use when `--device` is not given, as `"AA:BB:CC:DD:EE:FF"`. |
| 202 | - `gui.idle_seconds`: how long the app keeps the control channel open after the last change |
| 203 | (1–600). |
| 204 | - `gui.start_page`: `overview`, `equalizer`, `gestures`, `earbuds` or `settings`. |
| 205 | - `gui.motion`: `false` turns the app's animations off; every change then shows at once. |
| 206 | |
| 207 | ## How it works |
| 208 | |
| 209 | budctl speaks the buds' own control protocol over an RFCOMM socket. What is known about it, and |
| 210 | how sure, is written down in [`docs/protocol/`](docs/protocol/): the |
| 211 | [transport](docs/protocol/transport.md) and the [realme Buds T310](docs/protocol/realme_t310.md). |
| 212 | |
| 213 | ## License |
| 214 | |
| 215 | GPL-3.0-or-later. See [LICENSE](LICENSE). |
| 216 | |
| 217 | The desktop and Android apps carry the Doto, Space Grotesk and Space Mono fonts, under the SIL |
| 218 | Open Font License 1.1 ([`crates/budctl-gui/fonts/OFL.txt`](crates/budctl-gui/fonts/OFL.txt)). The |
| 219 | Doto in them is a static instance (rounded dots, black weight, Latin only), renamed "Doto Rounded" |
| 220 | as the license asks of modified fonts. |