Skip to content

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

Dual Joy‑Con to USB HID Gamepad (ESP32‑S3)

⚠️ Important, read before you buy anything: the Joy-Cons used and tested in this project are third-party replicas bought on AliExpress, not genuine Nintendo hardware. This has not been tested with genuine Nintendo Joy-Cons, I don't own any. See Compatibility below.

I wired a pair of Joy‑Con-style controllers directly into an ESP32‑S3 and turned them into a normal wired USB gamepad: no Bluetooth, no Switch, no dongle. The board enumerates as a standard HID controller, so it works anywhere a regular gamepad would (PC, emulators, whatever).

Phone with both Joy-Cons attached to the grip

Video: https://www.youtube.com/watch?v=64u_5oImT9Y PCB / schematic: https://oshwlab.com/scavenrage/gamepad_tablet_pro

The wired protocol the Joy-Con speaks over its rail contacts is documented. dekuNukem's Nintendo_Switch_Reverse_Engineering repo (https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering) captured the connector pinout and the exact handshake byte sequence; switchbrew.org's Joy-Con wiki page (https://switchbrew.org/wiki/Joy-Con) formalizes the full packet structure under the name "Nwcp", with named commands and report layouts. The handshake and status-poll commands in this firmware are taken directly from those sources (see Credits below). What this project adds is a full working implementation: ESP32‑S3 firmware that runs the handshake/poll/reconnect state machine end to end and bridges it to a standard USB HID gamepad, plus notes on how third-party replicas behave differently from the documented protocol.

Compatibility, read this first

The Joy-Cons I used and tested this with are third-party replicas bought on AliExpress, not genuine Nintendo parts. I don't own genuine Joy-Cons, so this firmware has never been tested against real Nintendo hardware. I can't say whether it works with them as-is.

Across different replica models, behavior isn't consistent either: with some of the cheaper clones I tried, the wired handshake in this firmware only succeeds if the Joy-Con has already been Bluetooth-paired at least once (e.g. to a Switch or a phone) before you plug it in wired. I don't know why. The documented protocol doesn't require this on genuine hardware, so it looks like a quirk of specific clone firmware, possibly related to the separate "Pairing" device command that this firmware never sends. If your Joy-Con doesn't respond to the handshake, try pairing it over Bluetooth first, then disconnect and plug it in wired.

Bottom line: your mileage may vary depending on which clone/batch you get, and this is all at your own risk.

Why

Pairing two Joy-Cons over Bluetooth to a phone/tablet doesn't really work well: they show up as two separate Bluetooth devices instead of one controller, and very few games actually support that configuration. Going wired sidesteps that entirely: it's just one HID gamepad, recognized everywhere.

There's also a practical reason: wired, the Joy-Cons are powered/charged straight from the phone, so I don't need to charge them separately. That matters when traveling, since with a USB-Y cable I can charge the phone and the Joy-Cons at the same time off a single cable.

The problem

Joy-Cons don't expose their pads over a friendly interface: the rail contacts talk a proprietary 1 Mbps UART protocol with an inverted TX line, and they need a specific handshake sequence before they start streaming input. The protocol itself is documented (see Credits), but there wasn't an existing firmware that implements the whole thing end to end on a microcontroller, with proper reconnect handling and a clean bridge to USB HID. That's what this project builds.

How it works

Each Joy-Con gets its own UART on the ESP32‑S3 (left on UART1: RX 8 / TX 9 / reset-pin 12, right on UART2: RX 1 / TX 2 / reset-pin 4), running at 1 Mbps with the TX line inverted, matching the documented connector pinout.

On boot, each Joy-Con goes through its own state machine:

  1. Detect: send the start sequence + an 0xA5 handshake packet, retry until the Joy-Con answers.
  2. Handshake: send an Inquiry command (0x01, returns the Joy-Con's MAC), then three configuration commands documented as DisconnectHid (0x11), CreateHidConnection (0x10) and SetHidInterval (0x12, sets the 15 ms polling interval), waiting for an ACK after each. If any of them times out, it drops back to Detect and tries again.
  3. Poll: send a status request every 15 ms and read back input reports (buttons + 12-bit stick data) from a queue filled by a dedicated FreeRTOS task per Joy-Con, one pinned to each core.
  4. If no valid frame comes in for 5 seconds, it's treated as disconnected and the state machine resets. This is what lets you unplug/replug a Joy-Con without rebooting the board.

The stick values (0–4095) are linearly rescaled to the signed 8-bit range HID expects (−127…127), with the Y axis inverted to match HID convention and the output clamped at the edges. It's a straight rescale, not a filtered/smoothed signal: good enough in practice, but worth knowing if you're chasing perfect linearity.

Buttons are remapped from the Joy-Con's own bit layout into an Xbox-style HID button order, since that's what most hosts expect. This part is original mapping work, not something pulled from a spec: figuring out which output bit each button should land on and building the remap layer.

There's a WS2812 LED on GPIO 21 for status: dim red while idle, yellow during handshake, blue/green if only one Joy-Con is connected, purple when both are up and polling, solid red on a communication error.

Hardware

Board: ESP32‑S3 Zero (Waveshare). Schematic and PCB files are in hardware/, already updated with the corrected, elongated PCB dimensions.

Bare boards: the elongated main PCB and the two small side boards

Note: in the build photos, the main PCB was lengthened by hand with soldered wire splices (visible on the flex cables in the photo below). That was the quick fix while I was still testing. The EasyEDA project in hardware/ is already updated with the corrected, elongated dimensions, so anyone building it fresh gets the fixed version, no splicing needed. The board is also easy to adapt if you're using a different grip/stand than mine, the mounting dimensions aren't fixed to one specific holder.

ESP32-S3 board mounted inside the grip, with the spliced flex cables visible

Mechanical build: the phone/tablet holder is a cheap generic Joy-Con grip/stand bought on Amazon, not a custom part. For the electrical connection to the Joy-Cons, I desoldered the connectors from a Joy-Con charging grip and reused them, soldering each one onto a small custom PCB. These two small PCBs slot into the sides of the holder (where the Joy-Cons attach) and carry the signals from the rail contacts to the ESP32‑S3. The two documented control pins on the rail connector, JDET and flow control, are wired to fixed levels (GND and 3.3V respectively) rather than driven by GPIOs, matching what the documented protocol expects from a permanently "connected" host.

Close-up of one side board with the reused Joy-Con rail connector

Status

It works reliably as a daily-use wired controller, but this is still a hobby project, not a polished product. If you build one, expect to do your own debugging on your specific Joy-Con hardware revision.

Possible future work

Ideas for later, based on parts of the documented protocol this firmware doesn't currently use:

  • Bluetooth support, so the ESP32‑S3 itself shows up as a single unified controller, with no USB cable at all, instead of just bridging to wired HID.
  • Driving the Joy-Con's own player-indicator LEDs (a documented HID command, SetIndicatorLed), in addition to the WS2812 on the board.
  • Switching to the faster 3.125 Mbps UART baud rate after handshake (a documented, optional command), instead of staying at 1 Mbps for the whole session.
  • Reading motion data (accelerometer/gyroscope), which the Joy-Con can report via a different input report type that this firmware never requests.

None of these are started yet.

Credits

License

CERN Open Hardware Licence Version 2 - Permissive (CERN-OHL-P-2.0)

About

This project transforms a pair of Switch Joy‑Con controllers into a fully‑featured USB HID gamepad using an ESP32‑S3 Waveshare Zero board. The goal was to create a single, wired, latency‑free controller that behaves like a professional‑grade gamepad and is compatible with systems expecting an Xbox‑style HID layout.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages