Skip to content

About

Firmware for an nRF52840 puck: a Steam Controller over Valve's own RF protocol, presented to a Nintendo Switch 2 as a Pro Controller over USB, with a BLE wake for a sleeping console.

Topics

Resources

Stars

24 stars

Watchers

0 watching

Forks

Latest commit

 

History

39 Commits

Folders and files

Repository files navigation

Pair-Raught

Pair-Raught

Firmware for an nRF52840 that takes input from a Valve Steam Controller over Valve's own RF protocol and presents it to a Nintendo Switch 2 as a Pro Controller, a Switch 2 Pro Controller or a pair of Joy-Con. It also wakes the console from sleep over BLE.

Raught is the past tense of reach. It reaches a console that is asleep, and it reaches across two systems that were never meant to touch.

Pair-Raught is an independent project. It is not affiliated with, endorsed by or sponsored by Nintendo or Valve. Nintendo Switch, Joy-Con, Valve, Steam and Steam Controller are trademarks of their respective owners.

What it does

Steam Controller in, Nintendo controller out. The controller pairs to the puck the way it pairs to Valve's own dongle, over RF. The console sees the controller type you picked, with buttons, both sticks, motion, and rumble.

Wakes a sleeping console. Hold the QAM button (Quick Access Menu), the one with three dots between the trackpads, for half a second. The puck hands its radio to BLE, wakes the console, presses B to clear the lock screen, and hands the radio straight back to RF. It wakes from every controller type: any type but the Switch 2 Pro Controller restarts once as one to do it, then comes back as itself. Waking is the point of the BLE support; it is not a general BLE controller mode.

Chimes on the controller. A short melody plays before a wake, and another before the puck restarts as a different controller type, so a button held for seconds is not answered with silence. They play through the controller's own haptics. Each type has its own chime, edited on the config page on a pitch-by-time graph: notes can overlap across haptics, slide, and add a buzz on the rumble motors, and a MIDI file can be imported as a starting point. Each chime can be turned off.

Controller types. The puck is one of these to a console, kept across power-ups until another is picked, on the config page or by holding all four back paddles plus a face button for two seconds (the hold time is a setting):

Hold back 4 + Type
B Pro Controller
B + R1 Pro Controller, with the right trackpad as a USB mouse
A Switch 2 Pro Controller
X Joy-Con pair
Y the Steam dongle, for one power-up

The Switch 2 Pro Controller is copied from a real one: the console draws it as one, and the upper back paddles (L4 and R4) become its GL and GR buttons. Sticks, motion with calibration, and rumble all work, and plugging it in registers the puck for waking. The gyro's strength is set per axis on the config page, and the trackpads can stand in for the sticks. It has no serial console and no mouse, since the real one has neither.

The Joy-Con pair is two controllers from one puck: a right Joy-Con over USB and a left one over BLE, sharing the radio with the Steam Controller. The console treats them as one pair. While the right trackpad is touched, the right Joy-Con becomes a mouse the way a real one does lying on a table, which games such as Star Fox use. Both halves carry motion. The left Joy-Con registers with the console once, the first time, from Change Grip/Order. Switching to or from the pair takes the puck off USB for a few seconds, as an unplug does, so a game in progress asks for the new controller by itself.

The Steam dongle identity is how a controller gets paired to the puck through Steam in the first place. Without a controller to hold the chord, such as one paired away to another puck, replug the puck three times in a row, pulling it within a second or two the first two times. The third power-up is the Steam dongle.

Off is off. In every controller type, with the Steam Controller turned off for a couple of seconds, the puck leaves USB, so the console frees its player slot, and comes back when the controller does. Steam + Y, held a quarter of a second, turns the controller off.

Configures itself. In dongle mode the puck appears as a small read-only USB drive holding a config page. Open it in Chrome or Edge and it talks back to the puck over WebSerial. Nothing to install, and no copy of the page that can disagree with the firmware behind it.

Motion, rumble, and a trackpad mouse. The IMU reaches the console and its calibration survives a power cycle. Rumble arrives from the console and is relayed to the controller over RF. In the "Pro Controller, with mouse" type the right trackpad drives a USB mouse, which works in games that take one, such as Factorio. The console's own menus and Nintendo's games do not use a USB mouse; games built for the Joy-Con mouse, such as Star Fox, need the Joy-Con pair.

What you need

An nRF52840 Pro Micro board. This is the one used to build and test everything here: nRF52840 Pro Micro. Anything that presents a nice!nano compatible UF2 bootloader should work, but nothing else has been tried. Check the SoftDevice before you flash, see the next section.

A USB cable that carries data. Plenty of cables sold with small electronics are charge only. If the board never appears as a drive or a serial port on any port you try, suspect the cable before the board.

A Steam Controller (2026), and Steam on a PC to pair it to the puck. Pairing happens over USB with both plugged into the same machine; there is no over-the-air pairing.

Chrome or Edge for the config page. It uses WebSerial, which Firefox and Safari lack. Firefox can use it through an add-on.

A Switch 2, for the half of this that talks to a console.

Before you flash: check the SoftDevice

Do this first. It is the difference between a board that works and one that looks dead, and there is no error message anywhere to tell you which you have.

Plug the board in. A new one has no firmware, so it comes up in its bootloader and appears as a drive called NICENANO. Open INFO_UF2.TXT on it and read the SoftDevice line:

SoftDevice: S140 version 6.1.1      good, carry on
SoftDevice: not found               stop, see "Restoring the SoftDevice" below

The firmware does not use the SoftDevice. It runs Zephyr's own link layer. What matters is that the bootloader decides where the application starts based on whether a SoftDevice is sitting at 0x1000. With one present, applications start at 0x26000, where this firmware is built to run. With none, they start at 0x1000, and an image built for 0x26000 lands a quarter of a megabyte away from where it expects to be, faults on its first call, and reboots forever.

Flashing

  1. Plug the board in. A board with nothing installed comes up in its bootloader and appears as a drive called NICENANO. If no drive appears, see "Getting into the bootloader" below.
  2. Copy switch2_ll.uf2 onto that drive.
  3. The drive disappears on its own within a couple of seconds and the board reboots as the puck. A drive called PAIR-RAUGHT takes its place.

That is the whole thing. No tools to install.

What Windows says about the copy

Go by the drive, not the dialog.

This error means it worked:

Error 0x800701B1: A device which does not exist was specified.

The board takes the last block, flashes it, and reboots immediately, so the drive is gone before Windows can finish the copy it thinks is still running. Nothing is wrong. Click Cancel.

This error means you copied onto the wrong drive:

There is not enough space on the disk.

A puck already running this firmware presents its own drive, PAIR-RAUGHT, which holds the config page and is far too small for a firmware image. It can take the same drive letter the bootloader had, so check the volume name before copying: NICENANO is the bootloader and takes firmware, PAIR-RAUGHT is the running puck and does not.

No error, and the drive still sitting there after a few seconds, means it did not work. Windows can report a copy to a removable drive as finished while the tail of the file is still in its write cache, and the bootloader waits for every block before it does anything. Eject the drive to force the cache out, or copy the file again.

Either way, do not unplug the board to find out. A partly written application runs and faults, which looks exactly like a dead board.

Getting into the bootloader

A board only lands in the bootloader by itself when it has no application to run. If yours arrived with firmware on it, or you have flashed it before, it boots that instead and no drive appears.

Reset it twice in quick succession, roughly as fast as a mouse double-click. The drive appears and stays put, waiting.

Most of these boards have no reset button. They have two exposed contacts marked RST and GND, and you reset the board by briefly bridging them, with tweezers, a paperclip, or a scrap of wire. So the sequence is: touch the two contacts, break contact, touch them again, all inside about half a second.

Too slow and the board just restarts into whatever it was running, so try again. Nothing is harmed by getting it wrong.

If your board does have a button, press it twice the same way.

Check the LED to see which state you are in. A slow pulse means the bootloader is waiting for a file. A fast pulse means an application is running and faulting, which is covered further down.

Reflashing

With the puck in dongle mode, open the config page and press Update firmware. The puck restarts into its bootloader, the NICENANO drive appears, and you copy the new switch2_ll.uf2 onto it. No reset contacts needed.

As a Switch 2 Pro Controller the puck has no drive and no serial port, so from a Windows PC run tools/bootsel.ps1, which asks it for the bootloader over USB. With -Pro1 it restarts once as the Switch 1 Pro Controller instead, which has a serial port. With -Dongle it restarts once in Steam dongle mode, the way back for a puck whose controller was paired away and can no longer send the chord.

powershell -ExecutionPolicy Bypass -File tools/bootsel.ps1

Otherwise, the same three steps as flashing. Reset the board twice as above, the bootloader drive comes back, and you copy the new switch2_ll.uf2 onto it.

Reading the LED
slow pulse    bootloader, waiting for a UF2
fast pulse    the application is faulting and resetting

A board that flashes successfully and then fast-pulses forever is almost always the SoftDevice problem above. Enough rapid resets will trip the bootloader's double-reset detection and drop it into the bootloader on its own, which makes it look like it went there deliberately.

Setting up

Do these in order. The puck arrives from a fresh flash in Steam dongle mode, which is where pairing a controller has to happen.

1. Pair the Steam Controller

Pairing is done by Steam over USB. There is no over-the-air pairing, so both the puck and the controller have to be plugged into the same PC.

If the controller is already paired to an official Valve puck, use its second slot. The Steam Controller holds two RF pairings. A controller that has been paired to a Valve puck is using the first one, and pairing again without moving to the second will take that pairing away.

Slots are chosen at power-on, not with the controller running:

  1. Turn the controller off. Steam + Y does it, held for a couple of seconds if Steam is not running. Holding the Steam button on its own also gets there eventually.
  2. Hold L1 + A + Steam to turn it back on. That selects the second slot. R1 + A + Steam selects the first.
  3. The start-up chime is different, which is how you know it took.

Then pair:

  1. Plug the puck into the PC. On a fresh flash it is already in dongle mode.
  2. Plug the Steam Controller into the same PC.
  3. In Steam, open the controller settings and start the pairing flow for a new controller.
  4. Follow it through. Steam writes the bond to the puck and matches the controller to it by serial number.

A "Pairing Failed (16)" message does not mean pairing failed. Steam writes the bond first and then waits for the controller to answer over the radio. The error is that wait timing out, and the bond is already on the puck by then. Unplug the controller, power it on away from the PC, and see whether the puck picks it up before pairing again.

2. Pick a controller type

Dongle mode lasts one power-up. Once a controller is paired, the puck comes up as a Switch 2 Pro Controller on every power-up, the default type. To get back to dongle mode later, hold the four back paddles plus Y, or replug the puck three times quickly and leave the third one in.

For another type, hold the four back paddles plus that type's button for two seconds, from the table under "Controller types" above. The puck plays that type's chime, restarts as it, and stays one from then on. The config page can pick the type too.

3. Connect to the Switch 2

Plug the puck into a USB port on the console that supplies power. The dock's ports do. The port on the console itself may not, depending on how it is being used.

The console shows the type you picked. Buttons, both sticks, motion and rumble all work from here.

If the console offers a controller update, it has newer controller firmware than the version the puck reports. Accept it. The puck discards the update, raises its reported version to the next major version and restarts, the way a real controller does after an update, so the offer does not come back. The version can also be set by hand under Reported firmware on the config page.

For the Joy-Con pair, the left Joy-Con has to be registered once: open Change Grip/Order on the console and wait for the pair to appear. After that it reconnects on its own. If the left Joy-Con never shows up there, hold the four back paddles + L3 + R3 with Change Grip/Order open, which offers it for registration again.

4. Waking the console

Plugged in as a Switch 2 Pro Controller, the puck registers with the console for waking, the way a real one does. Nothing more is needed: holding QAM wakes the console from sleep, from any controller type. If the console is told to forget its controllers, plug the puck in as a Pro Controller 2 again. The config page shows Console: Registered once a console is stored; until then QAM does nothing.

If the puck has never been plugged in as a Pro Controller 2, it can register over Bluetooth instead. The puck has one radio, so it borrows it from RF for as long as this takes and hands it back afterwards. The controller goes quiet meanwhile.

  1. On the console, open Change Grip/Order.
  2. Hold the four back paddles + L3 + R3 (both stick clicks) on the Steam Controller.
  3. Wait for the puck to appear and be registered. It returns to RF on its own, and gives up after two minutes if no console registers it.

From a PC instead: press Pair with a console on the config page, which reboots the puck into pairing mode, then open Change Grip/Order. The console has to be in Bluetooth range of the PC.

Configuring

Open RAUGHT.HTM from the puck's own drive in Chrome or Edge. It talks back over WebSerial, so nothing needs installing, and the page can never be out of step with the firmware serving it.

The drive is only present in dongle mode. In Pro Controller mode the puck does not offer one, because a console has no use for a drive and adding interfaces to a working Pro Controller is a risk with nothing to gain.

Starting over

The config page has a Factory reset button. It erases everything the puck remembers: the paired controller, the registered console, the settings and the motion calibration. Use it for a bad pairing, or before passing the puck to someone else.

Reflashing does not do this. The application and the stored data live in different parts of flash, and the bootloader will not write to the part holding the data, so a puck reflashed a dozen times still knows everything it ever knew.

Restoring the SoftDevice

Needed only if INFO_UF2.TXT says SoftDevice: not found. No debug probe required.

Serial DFU cannot do this. adafruit-nrfutil checks bootloader versions, and its SoftDevice packages target Adafruit's 0.9.1 while a nice!nano runs 0.6.0, so it refuses. UF2 can, because with no SoftDevice installed the bootloader's writable window starts at 0x1000, which is exactly where the SoftDevice goes.

You need an S140 6.1.1 image as a UF2 targeting 0x1000. If you have the Adafruit nRF52 Arduino core installed, one can be carved out of any of its combined bootloader hexes: the SoftDevice occupies 0x1000 up to about 0x25E00, with the MBR below it and the bootloader far above at 0xF4000.

Copy that UF2 onto the bootloader drive, then check INFO_UF2.TXT again. It should now report the SoftDevice, and flashing works normally from there.

Only 0x1000..0x26000 is written. The bootloader and the MBR are untouched, so a failed attempt leaves the board no worse off than it already is.

Building
./build.sh

Needs:

  • the nRF Connect SDK v3.4.0, and nrfutil with its toolchain-manager command, or, without nrfutil, an SDK fetched with west and the Zephyr SDK toolchain (see the top of build.sh)
  • bash, which on Windows means Git Bash
  • xxd, which ships with Git for Windows and with vim, and on some Linux systems is a separate package
  • optionally adafruit-nrfutil, which lets the build also produce a serial-DFU .zip

build.sh looks for the SDK at C:/ncs/v3.4.0 on Windows and ~/ncs/v3.4.0 on Linux and macOS. If yours is somewhere else, point NCS_WORKSPACE at it. The build works in a pair-raught folder next to the SDK, which it clears on every run.

NCS_WORKSPACE=/path/to/ncs/v3.4.0 ./build.sh

The output is switch2_ll/switch2_ll.uf2, which is the file you copy onto the board.

Before the first build, apply both patches to Zephyr's software link layer in the SDK. The build does not do this for you, and it only needs doing once per SDK install. Without the first the console never completes a connection ("The BLE protocol" below explains why). Without the second the Joy-Con pair builds but loses the Steam Controller while its left Joy-Con is connected, because RF cannot poll between BLE events.

cd <nRF Connect SDK>/zephyr
git apply <path to this repo>/patches/ull_peripheral-accept-5ms.patch
git apply <path to this repo>/patches/lll-radio-idle-hook.patch

The first patch now also covers connection updates, which keep the Joy-Con pair's left Joy-Con connected at the HOME menu. An SDK patched with the copy from the first release needs both applied again:

cd <nRF Connect SDK>/zephyr
git checkout subsys/bluetooth/controller/ll_sw
git apply <path to this repo>/patches/ull_peripheral-accept-5ms.patch
git apply <path to this repo>/patches/lll-radio-idle-hook.patch
The BLE protocol

The console opens controller links at a 5 ms interval, below what the Bluetooth spec allows, so a closed-source link layer refuses silently and reports nothing. That is why this is built on Zephyr's software controller rather than a vendor stack, and it is most of the reason the work took as long as it did.

Research material

Comments in the firmware cite files under reference/ and docs/captures/: the Arduino proof of concept the BLE work started from, captures of real controllers, and the scripts that turned them into the ns2_*.h headers. Those were working material and are not part of this repository.

Comments also cite "the research notes", the command list in ndeadly/switch2_controller_research, commands.md.

Relationship to OpenPuck

This is not a fork of OpenPuck, and it is not a drop-in replacement for it. It is separate firmware, written for Zephyr, and it would not exist without that project.

OpenPuck is where the Steam Controller side of this comes from. Its Arduino firmware worked out Valve's RF protocol, the dongle's HID interfaces, the haptics framing and the Pro Controller mapping, and parts of this firmware were ported from it rather than worked out again. Files say what they were ported from where it matters, for example rf_radio.h, rf_link.h, puck_input.h and puck_hid.c.

What is new here is the Switch 2 side: the BLE work, the wake, the config surface, and the move to a link layer whose source can be patched.

If you want a Steam Controller dongle, use OpenPuck. This is for driving a Switch 2 with one.

The chimes, and SteamHapticsSinger

The controller plays tones because of SteamHapticsSinger, which plays MIDI files on a Steam Controller's haptics and supports the 2026 controller through a puck. Reading it is where the command came from: output report 0x83, carrying a haptic, a gain and a frequency in hertz. Without it the search would have ended at Steam's vibration test, which only picks a preset.

No code was taken from it. It is BSD-3-clause; the credit is owed rather than required.

How this was written

Claude, Anthropic's model, assisted with this project, including this firmware.

Every behavior claimed here was tested on real hardware. The puck was flashed and run against a real Switch 2 and a real Steam Controller, and the protocol findings came from radio captures of both. Where something is believed but not demonstrated, it is written down as unverified rather than claimed.

License

AGPL-3.0-only. See LICENSE.

Parts of this firmware were ported from OpenPuck, which is AGPL-3.0, so this carries the same license. That means you are free to use, change and redistribute it, and that anything you distribute built on it has to be open under the same terms.

About

Firmware for an nRF52840 puck: a Steam Controller over Valve's own RF protocol, presented to a Nintendo Switch 2 as a Pro Controller over USB, with a BLE wake for a sleeping console.

Topics

Resources

Stars

24 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages