Update Xbox controller firmware on Linux and macOS

xboxctl inspects and updates the firmware of Xbox (GIP) controllers from Linux and macOS, without the Xbox Accessories app. It speaks the same firmware update protocol (“UFU”, carried in GIP command 0x0c) as the Windows updater and downloads firmware from the same Microsoft feed.

Contents

Supported controllers

Controllers must be connected with a USB cable; updates over the Xbox Wireless Adapter or Bluetooth are not supported.

Controller USB ID Codename Tested
Xbox Series X|S Controller 045e:0b12 MERL ✅
Xbox One S Controller 045e:02ea CRET ✅
Xbox Elite Controller Series 2 045e:0b00 DELP ❌
Xbox Adaptive Controller 045e:0b0a ZEPH ❌
Xbox Adaptive Joystick 045e:0b1a, 045e:0b1c DURA ❌
Xbox One Elite Controller 045e:02e3 TROY ❌
Xbox One Controller (2015) 045e:02dd LANG ❌
Xbox One Controller (original) 045e:02d1, 045e:02d2 ARGO, ARGD ❌

The original Xbox One controller uses a legacy update protocol that xboxctl does not support.

Untested controllers may well work: xboxctl info and xboxctl update --latest --dry-run are read-only and show whether a controller speaks the update protocol and what an update would change. If you try one, please open an issue with the result.

Microsoft’s feed also lists models that xboxctl has never seen (codenames NORL, ORRE, PARK, NICO); their firmware layouts suggest accessories other than controllers. Feed entries with a -cc- suffix (variants of 0b17 and 0b32) cannot be selected yet, and the SCUF controller (2e95:0504) gets MERL firmware, which update refuses because the USB IDs differ.

Installation

On Linux (x86_64, aarch64) and Apple Silicon Macs:

curl -fsSL https://raw.githubusercontent.com/bb441db/xboxctl/main/install.sh | sh

Or with wget:

wget -qO- https://raw.githubusercontent.com/bb441db/xboxctl/main/install.sh | sh

The script downloads the binary for your system from the latest release, checks its SHA-256 and installs it to ~/.local/bin (Linux) or /usr/local/bin (macOS). Set XBOXCTL_INSTALL_DIR to install elsewhere, or XBOXCTL_VERSION to a release tag to install that release. The binaries have no runtime dependencies; you can also download them from the releases page directly.

On Linux, install the udev rule below so that xboxctl runs without root. On macOS, run it with sudo; see Permissions.

Using a Steam Deck or another SteamOS device? Follow the step by step guide for SteamOS.

Usage

xboxctl list                       # connected controllers (read-only)
xboxctl info                       # identity and per-component firmware versions (read-only)
xboxctl download                   # current firmware for the connected model
xboxctl download --list            # every model in Microsoft's feed
xboxctl update --latest --dry-run  # compare the controller with the current firmware
xboxctl update --latest            # install the current firmware
xboxctl update <package.bin>       # install an rdhm package file
xboxctl rumble                     # run each rumble motor briefly (--strength 1-100)
xboxctl diag                       # controller info, then a live view with stick drift check
xboxctl calibrate                  # guided thumbstick calibration (--stick left|right|both)
xboxctl calibrate --dry-run        # show the stored calibration, change nothing
xboxctl calibrate --trigger both   # guided trigger calibration (--trigger left|right|both)

With several controllers connected, choose one with --device BUS:ADDRESS from xboxctl list.

update first shows what would change and stops if the controller is already up to date. During an update the controller restarts several times (it re-enumerates on USB) and xboxctl reconnects to it. Keep it connected until the update finishes.

Downloads are checked against the SHA-256 in their file name and cached in the user cache directory (~/.cache/xboxctl/firmware on Linux, ~/Library/Caches/xboxctl/firmware on macOS). Under sudo the files still go to, and belong to, the user who ran it.

Testing rumble

xboxctl rumble runs the handle motors and then the trigger motors for a second each, by talking to the controller directly over USB. It changes nothing on the controller. Use it to tell a controller fault from a driver problem: if the motors move here but not in a game or browser, the controller is fine. Before the test it sends a few zero-strength packets so the controller accepts it on repeated runs; see the rumble notes.

Wired rumble does not work through macOS itself for Xbox One, Series X|S and Elite Series 2 controllers (checked on macOS 15.7 and reported on 27.0.1), so this is the way to check the hardware there. See the notes on the macOS driver.

Diagnosing a controller

xboxctl diag shows what the controller reports about itself (identity and firmware versions) and then opens a live view of the buttons, both sticks and both triggers. It changes nothing on the controller.

  A LB                      L  +12%   -3%   R   +0%   +0%   LT   0%  RT 100%

The controller only reports changes, so press any button first: that report also tells where the sticks rest. Press every button and push the sticks and triggers all the way to see if something does not respond. If you leave a stick alone for a few seconds and it rests 6% to 30% of its travel away from the centre, the view warns about possible stick drift; xboxctl calibrate may fix that. A stick that stays further out makes the view ask you to let go of it: a stick that does not come back is drifting badly. A few percent off-centre (3% to 6%) is only noted in the summary. Hold the Xbox button for two seconds to finish, or use --duration SECONDS. The summary at the end lists the buttons you did not press, the resting state from the first report, how far each stick and trigger reached after that (“not moved” if you left it alone) and whether either stick drifted.

Calibrating the sticks

xboxctl calibrate walks you through recalibrating the thumbsticks, like the Xbox Accessories app does. For each stick it asks you to turn it clockwise 6 times and then counter-clockwise 6 times at the outer edge, with a counter that follows your progress, and it finishes with a check: push the stick up, right, down and left and let go, and xboxctl confirms that it reached the edge and came back to the centre. Without --stick it does the left stick and then the right one.

The controller learns the stick’s range itself and stores it, so the result stays after you unplug the controller. It also erases a stick’s stored calibration the moment the calibration starts: finish all the steps, and if you stop halfway run the command again (the controller falls back to its default calibration meanwhile). xboxctl calibrate --dry-run only shows the stored range of each stick.

Checked on an Xbox Series X|S controller (firmware 5.23). Firmware that does not support calibration does not answer (a Series X|S on 5.13.3146.0 did not) or refuses (an Xbox One S does, also on its latest firmware, 5.23, which has no calibration code), and xboxctl says so; xboxctl update --latest brings the controller up to date. A firmware update leaves the stored calibration alone: it was unchanged after going from 5.23 down to 5.13.3146.0 and back.

Calibrating the triggers

xboxctl calibrate --trigger left|right|both is the same for the triggers: it asks for 12 full presses (what the Xbox Accessories app asks for), follows them, and then checks that the trigger reads zero at rest and reaches the end of its range when fully pressed. Checked on an Xbox Series X|S controller (firmware 5.23): the controller learns the range itself and stores it, and the result stays after a power cycle, as for the sticks. xboxctl calibrate --trigger both --dry-run only reads, and prints the stored range of each trigger with the raw replies.

Permissions

Linux

xboxctl detaches xpad from the controller while it runs and needs read/write access to the controller’s USB device node. Install this udev rule once to run it without root. It gives the logged-in user access to GIP controllers (USB class ff/47/d0):

echo 'SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{bDeviceClass}=="ff", ATTR{bDeviceSubClass}=="47", ATTR{bDeviceProtocol}=="d0", TAG+="uaccess"' \
  | sudo tee /etc/udev/rules.d/70-xboxctl.rules
sudo udevadm control --reload && sudo udevadm trigger

macOS

A system driver (XboxUSBDevice) holds the controller exclusively, and there is no udev equivalent for taking it over. Commands that talk to the controller (info, update, rumble, …) must run as root:

sudo xboxctl info
sudo xboxctl update --latest

list and download do not need root.

Building from source

Build with Rust (the toolchain is pinned in rust-toolchain.toml) and a C compiler; libusb is compiled in.

git clone https://github.com/bb441db/xboxctl
cd xboxctl
cargo build --release      # target/release/xboxctl

License

0BSD.