Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dea7078
Modernize for current Linux/Python: prefer btmgmt over hciconfig/hcitool
May 1, 2026
d1a226d
Fix Python 3.12 breakage in protocol writer and disconnect path
May 1, 2026
8bab5f9
Fix MyBoundedSemaphore on Python 3.10+: stop using self._loop
May 1, 2026
4b8f3cd
Suppress CancelledError noise from writer task on clean shutdown
May 1, 2026
8d6481c
Don't log clean shutdown as ERROR
May 1, 2026
6b55fa8
Add list_paired_switches.sh to inspect bond history
May 1, 2026
a7b64db
Quiet routine 'Code is running X s too slow' warnings
May 1, 2026
f4f7ca0
Fix '-r auto' picker when multiple paired Switches exist
May 1, 2026
6194a3e
Show last-bond timestamp and an abort option in '-r auto' picker
May 1, 2026
d7ed246
Switch CLI to prompt_toolkit: live prompt, tab completion, history
May 1, 2026
eb3676a
Fix mash/test_buttons stop-on-enter under prompt_toolkit
May 1, 2026
63e26a3
Default -r to 'auto'; fall back to initial pairing if no Switch paired
May 1, 2026
aa479dc
Default controller arg to PRO_CONTROLLER
May 1, 2026
4a413d4
Use argparse choices/default instead of hardcoded controller help
May 1, 2026
c5374cb
Sort paired-Switch picker by last-bond mtime (most recent first)
May 1, 2026
7065631
Add 'pair new' option to the -r auto picker
May 1, 2026
025dbfe
Stop prompting to unpair existing Switches before initial pairing
May 1, 2026
c7e02fe
Add 'unpair' option to the -r auto menu and loop until a final choice
May 1, 2026
1ecdda3
Update README install + bluetooth service setup for current behavior
May 1, 2026
41c7c48
Note CodeReady Builder repo for bluez-libs-devel on RHEL-likes
May 1, 2026
4efd91c
Drop aioconsole dependency
May 1, 2026
bc28eae
Fix install path: drop dbus-python from install_requires, use --syste…
May 1, 2026
70674cf
README: rewrite Installation and Bluetooth setup as numbered steps
May 1, 2026
4847727
README: use .venv/bin/python and require cd into project dir for CLI
May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 285 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,78 +13,316 @@ Emulation of JOYCON_R, JOYCON_L and PRO_CONTROLLER. Able to send:
- nfc for amiibo read & owner registration

## Installation
- Install dependencies
Raspbian:

Tested on Python 3.9+ and BlueZ 5.55+ (verified on Raspbian and Oracle
Linux 10.1 with Python 3.12 / BlueZ 5.83). The legacy `hciconfig` /
`hcitool` tools are deprecated on modern distributions; this project
prefers `btmgmt` (the modern bluez management tool) and falls back to
the legacy tools only when present.

These steps assume a fresh setup. Run them in order. Everything below
is reversible — nothing is installed system-wide except a few distro
packages and a systemd drop-in (covered in *Bluetooth service setup*
below).

### Step 1 — Get the source

```bash
git clone https://github.com/Poohl/joycontrol.git
cd joycontrol
```

(Substitute your own fork URL if you're using one.)

### Step 2 — Install the system packages

joycontrol talks to BlueZ over D-Bus and reads HID devices via HIDAPI,
so a few distro packages have to be present. This step is the only one
that needs to touch system state outside the project directory.

**Debian / Ubuntu / Raspberry Pi OS (Raspbian):**

```bash
sudo apt update
sudo apt install python3-venv python3-dbus libhidapi-hidraw0 libbluetooth-dev bluez bluez-tools
```

**Fedora / RHEL / Oracle Linux 10:**

`bluez-libs-devel` lives in the **CodeReady Builder** repo, which is
disabled by default — enable it once with the line that matches your
distro:

```bash
# Oracle Linux 10
sudo dnf config-manager --enable ol10_codeready_builder

# AlmaLinux / Rocky Linux 10
sudo dnf config-manager --set-enabled crb

# Red Hat Enterprise Linux 10 (with an active subscription)
sudo subscription-manager repos --enable codeready-builder-for-rhel-10-x86_64-rpms
```

Then install the packages:

```bash
sudo dnf install python3 python3-dbus hidapi bluez bluez-libs-devel
```

> **Why no `bluez-tools`?** On Debian-family distros, `btmgmt` ships in
> a separate `bluez-tools` package; on RHEL-family distros it lives
> *inside* the main `bluez` package. Either way, you end up with
> `btmgmt` available — that's what joycontrol uses.

### Step 3 — Create a Python virtualenv

A virtualenv keeps the project's Python dependencies isolated from
the rest of your system so they don't conflict with anything else.

From the cloned project directory, run:

```bash
python3 -m venv --system-site-packages .venv
```

This creates a `.venv/` folder inside the project. The
`--system-site-packages` flag is **important**: it lets the venv see
the distro-installed `python3-dbus` from step 2.

> **Why `--system-site-packages`?** `python3-dbus` is a C extension
> that links against your system's D-Bus libraries. Building it from
> source via `pip` requires a C toolchain plus dbus/glib headers, and
> usually fails. Having the venv inherit the distro package
> sidesteps that whole problem.

### Step 4 — Install joycontrol

This installs the joycontrol package and its remaining Python
dependencies (`hid`, `crc8`, `prompt-toolkit`) *into* the venv:

```bash
sudo .venv/bin/pip install .
```

`sudo` is needed here only because the next step (running joycontrol)
must be root to access raw Bluetooth sockets, and the venv files
should be readable by root.

### Step 5 — Verify the install

```bash
sudo .venv/bin/python -c "import dbus, hid, crc8, prompt_toolkit"
```

Should print nothing and exit cleanly. If you get
`ModuleNotFoundError: No module named 'dbus'`, you forgot
`--system-site-packages` in step 3 — delete `.venv/` and redo step 3.

---

After these five steps the Python side is done. You **also** need to
adjust BlueZ so the Switch will accept connections — that's the next
section. Without that, the script will start but the Switch will
reject the controller during pairing.

## Bluetooth service setup

The Switch is picky about what it connects to. If the host advertises
extra Bluetooth profiles (audio remote, SIM access, regular HID
input), the Switch sees too many service records and **refuses to
pair**. We tell BlueZ to drop those plugins so the controller is the
only thing the Switch sees.

The cleanest way is a **systemd drop-in override** — a small
configuration file that adjusts the existing `bluetooth.service`
without touching anything BlueZ ships, so it survives package updates.

### Step 1 — Find your bluetoothd binary path

```bash
sudo apt install python3-dbus libhidapi-hidraw0 libbluetooth-dev bluez
systemctl cat bluetooth.service | grep -m1 ExecStart=/usr
```
Python: (a setup.py is present but not yet up to date)
Note that pip here _has_ to be run as root, as otherwise the packages are not available to the root user.

You'll see one of these two paths in the output:

| Distro family | `bluetoothd` path |
|------------------------------|-----------------------------------------|
| Fedora / RHEL / Oracle Linux | `/usr/libexec/bluetooth/bluetoothd` |
| Debian / Ubuntu / Raspbian | `/usr/lib/bluetooth/bluetoothd` |

Note which one you have — you'll plug it into the next step.

### Step 2 — Write the override

Replace `/usr/libexec/bluetooth/bluetoothd` below with the path you
found in step 1 if yours differs:

```bash
sudo mkdir -p /etc/systemd/system/bluetooth.service.d
sudo tee /etc/systemd/system/bluetooth.service.d/override.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/libexec/bluetooth/bluetoothd -C -P sap,input,avrcp
EOF
```

The blank `ExecStart=` line is important — it tells systemd to discard
BlueZ's default command before applying ours.

### Step 3 — Reload and restart bluetoothd

```bash
sudo systemctl daemon-reload
sudo systemctl restart bluetooth.service
```

### Step 4 — Verify

```bash
sudo pip3 install aioconsole hid crc8
```
If you are unsure if the packages are properly installed, try running `sudo python3` and import each using `import package_name`.

- setup bluetooth
- [I shouldn't have to say this, but] make sure you have a working Bluetooth adapter\
If you are running inside a VM, the PC might but not the VM. Check for a controller using `bluetoothctl show` or `bluetoothctl list`. Also a good indicator it the actual os reporting to not have bluetooth anymore.
- disable SDP [only necessary when pairing]\
change the `ExecStart` parameter in `/lib/systemd/system/bluetooth.service` to `ExecStart=/usr/lib/bluetooth/bluetoothd -C -P sap,input,avrcp`.\
This is to remove the additional reported features as the switch only looks for a controller.\
This also breaks all other Bluetooth gadgets, as this also disabled the needed drivers.
- disable input plugin [experimental alternative to above when not pairing]\
When not pairing, you can get away with only disabling the `input` plugin, only breaking bluetooth-input devices on your PC. Do so by changing `ExecStart` to `ExecStart=/usr/lib/bluetooth/bluetoothd -C -P input` instead.
- Restart bluetooth-deamon to apply the changes:
```bash
sudo systemctl daemon-reload
sudo systemctl restart bluetooth.service
```
- see [Issue #4](https://github.com/Poohl/joycontrol/issues/4) if despite that the switch doesn't connect or disconnects randomly.
ps -ef | grep bluetoothd | grep -v grep
```

You should see the daemon running with the `-C -P sap,input,avrcp`
flags appended, e.g.:

```
root 7274 ... /usr/libexec/bluetooth/bluetoothd -C -P sap,input,avrcp
```

Confirm there's a working adapter too (especially if you're inside a
VM — the host might have Bluetooth, but the VM might not):

```bash
bluetoothctl show
```

You want a `Controller` line with a real BD address (e.g. `5C:F3:70:…`).
If you don't, joycontrol won't be able to do anything until you fix
the adapter situation — typically by passing through a USB Bluetooth
dongle or running on bare metal.

### What this breaks (host-wide)

The override disables three BlueZ plugins for *everything* on this
machine, not just joycontrol. After applying it:

- `input` — Bluetooth keyboards / mice / game controllers won't work.
- `sap` — SIM Access Profile (used to share a phone's SIM with a
car kit) won't work; almost certainly nobody cares.
- `avrcp` — media remote control over Bluetooth (play/pause from BT
headphones, etc.) won't work.

For *reconnecting* to a Switch you've already paired with, you can
sometimes get away with disabling only `input`. But **initial pairing
needs all three** disabled or the Switch refuses the connection. See
[Issue #4](https://github.com/Poohl/joycontrol/issues/4) for the
underlying details.

To revert: delete `/etc/systemd/system/bluetooth.service.d/override.conf`
and restart bluetooth.service.

## Command line interface example
There is a simple CLI (`sudo python3 run_controller_cli.py`) provided with this app. Startup-options are:

A simple CLI lives in `run_controller_cli.py`. All commands below
assume you're in the project directory and have set up the venv as
described in *Installation*:

```bash
cd /path/to/joycontrol # the directory you cloned into
```

Bare invocation:

```bash
sudo .venv/bin/python run_controller_cli.py
```

…defaults to emulating a Pro Controller and reconnecting to your most
recently paired Switch (or falling through to initial pairing if none).
Use `.venv/bin/python` (not `python3`) so the venv's prompt-toolkit /
hid / crc8 deps are picked up.

Startup options:

```
usage: run_controller_cli.py [-h] [-l LOG] [-d DEVICE_ID]
[--spi_flash SPI_FLASH] [-r RECONNECT_BT_ADDR]
[--nfc NFC]
controller
[{JOYCON_L,JOYCON_R,PRO_CONTROLLER}]

positional arguments:
controller JOYCON_R, JOYCON_L or PRO_CONTROLLER
{JOYCON_L,JOYCON_R,PRO_CONTROLLER}
controller type to emulate (default: PRO_CONTROLLER)

optional arguments:
options:
-h, --help show this help message and exit
-l LOG, --log LOG BT-communication logfile output
-d DEVICE_ID, --device_id DEVICE_ID
not fully working yet, the BT-adapter to use
--spi_flash SPI_FLASH
controller SPI-memory dump to use
-r RECONNECT_BT_ADDR, --reconnect_bt_addr RECONNECT_BT_ADDR
The Switch console Bluetooth address (or "auto" for
automatic detection), for reconnecting as an already
paired controller.
--nfc NFC amiibo dump placed on the controller. Equivalent to
the nfc command.
Switch BD address, "auto" (the default) for picker,
or "" / "none" to force initial pairing
--nfc NFC amiibo dump placed on the controller (same as the
in-prompt `nfc` command)
```

### Pairing / reconnecting

When at least one Switch is already paired, the script presents an
interactive picker:

```
found the following paired switches, please choose one:
1: /org/bluez/hci0/dev_AA_AA_AA_AA_AA_AA (last bond: 2026-05-01 16:36:48)
2: /org/bluez/hci0/dev_BB_BB_BB_BB_BB_BB (last bond: 2026-05-01 14:31:04)
n: pair a new Switch
u: unpair a Switch
0: abort
number 1 - 2, n to pair new, u to unpair, 0 to abort [1]:
```

To use the script:
- start it (this is a minimal example)
| Input | Result |
|----------------|----------------------------------------------------------------------|
| Enter | reconnect to the most-recently bonded Switch (option 1) |
| `1`–`N` | reconnect to that entry |
| `n` / `new` | initial-pairing flow — open *Change Grip/Order* on the Switch |
| `u` / `unpair` | pick a Switch to forget (with `y/N` confirm), menu reflows |
| `0` / `q` | exit cleanly |

To bypass the picker entirely:
- `-r 04:03:D6:8F:08:B5` — reconnect to that specific BD address
- `-r ""` or `-r none` — force initial pairing even if other Switches are paired

If no Switch is paired yet, the picker is skipped and the script goes
straight to the initial-pairing flow — open *Change Grip/Order* on the
Switch.

### Inspecting paired Switches

From the project directory, `scripts/list_paired_switches.sh` lists
paired devices for the default adapter, sorted by last-bond timestamp:

```bash
sudo python3 run_controller_cli.py PRO_CONTROLLER
sudo ./scripts/list_paired_switches.sh
# 04:03:D6:8F:08:B5 Nintendo Switch 2026-05-01 16:36:48
```
- The cli does sanity checks on startup, you might get promps telling you they failed. Check the command-line options and your setup in this case. (Note: not the logging messages). You can however still try to proceed, sometimes it works despite the warnings.

- Afterwards a PRO_CONTROLLER instance waiting for the Switch to connect is created.

- If you didn't pass the `-r` option, Open the "Change Grip/Order" menu of the Switch and wait for it to pair.
### Inside the prompt

- If you already connected the emulated controller once, you can use the reconnect option of the script (`-r <Switch Bluetooth Mac address>`). Don't open the "Change Grip/Order" menu in this case, just make sure the switch is turned on. You can find out a paired mac address using the `bluetoothctl paired-devices` system command or pass `-r auto` as address for automatic detection.
Once connected, a `cmd >>` prompt opens with:

- After connecting, a command line interface is opened.
Note: Press \<enter> if you don't see a prompt.
- **Tab** — completes commands and button names.
- **↑ / ↓** — recalls previous commands (persisted at
`~/.local/state/joycontrol/cli_history`, overridable via
`$JOYCONTROL_STATE_DIR` or `$XDG_STATE_HOME`).
- Logs render *above* the prompt without disturbing your input.
- **Ctrl-D** / **Ctrl-C** / `exit` exit cleanly.

Call "help" to see a list of available commands.
Type `help` for the full command list (button names, `stick`, `mash`,
`hold`/`release`, `nfc`, `pause`/`unpause`, etc.).

## API

Expand Down Expand Up @@ -112,9 +350,9 @@ await controller_state.send()
```

## Issues
- Some bluetooth adapters seem to cause disconnects for reasons unknown, try to use an usb adapter or a raspi instead.
- Incompatibility with Bluetooth "input" plugin requires it to be disabled (along with the others), see [Issue #8](https://github.com/mart1nro/joycontrol/issues/8)
- The reconnect doesn't ever connect, `bluetoothctl` shows the connection constantly turning on and off. This means the switch tries initial pairing, you have to unpair the switch and try without the `-r` option again.
- Some Bluetooth adapters cause disconnects for reasons unknowntry a USB adapter or a Raspberry Pi instead.
- Incompatibility with Bluetooth "input" plugin (and `sap` / `avrcp` for initial pairing) requires them to be disabled — see the *Bluetooth service setup* section above and [Issue #8](https://github.com/mart1nro/joycontrol/issues/8).
- Reconnect spins (`bluetoothctl` shows the connection bouncing on/off) usually means the Switch lost its bond key but the host still has one. Use the `u` / unpair option in the picker to forget the host's bond, then pick `n` to pair fresh.
- ...

## Thanks
Expand Down
Loading