Switch Pro Controller (NS1)
The switch_pro component provides espp::SwitchPro, a Nintendo Switch Pro
controller (NS1) USB emulation protocol engine: it implements the Switch Pro
controller’s USB HID handshake and input-report protocol so an ESP32-S3 (or other
native-USB ESP) can present itself to a Nintendo Switch as a Pro Controller.
It is transport-light — the class owns the controller state and the
request/response state machine but performs no USB I/O. Drive it from a USB HID
interface (the espp usb_device HID function is the intended pairing): feed
host OUTPUT reports (the handshake) to on_hid_report() and send the replies
(and the periodic standard input report) back as HID INPUT reports. The HID report
descriptor and report packing come from the hid-rp component.
Warning
Emulation only. A real Switch only binds a device advertising Nintendo’s
Pro Controller USB VID/PID (0x057E / 0x2009) and identity strings
(exposed as espp::SwitchPro constants). Use these to emulate / test against
a Switch you own; do not ship a product impersonating Nintendo hardware.
API Reference
Header File
Classes
-
class SwitchPro : public espp::BaseComponent
Nintendo Switch Pro controller (NS1) USB emulation protocol engine.
This class implements the Nintendo Switch Pro controller’s USB HID handshake and input-report protocol so an ESP device can present itself to a Nintendo Switch (NS1) as a Pro Controller. It is **transport-light**: it owns the controller state + the request/response state machine but performs no USB I/O itself. Drive it from a USB HID interface (e.g. the espp `usb_device` component’s HID function): feed host OUTPUT reports to `on_hid_report()` and send the returned reply (and periodic `get_input_report()` reports) back as HID INPUT reports. See the example.
The HID report descriptor, input-report packing, and the SPI-ROM calibration/config blobs come from the espp `hid-rp` component (`switch_pro_descriptor()`, `SwitchProGamepadInputReport`).
Switch Pro example
extern "C" void app_main(void) { espp::Logger logger({.tag = "SwitchProExample", .level = espp::Logger::Verbosity::INFO}); logger.info("Starting Nintendo Switch Pro (NS1) USB controller example"); // The protocol engine: owns the controller state + the USB HID handshake. espp::SwitchPro controller({.log_level = espp::Logger::Verbosity::WARN}); // Outgoing HID reports are produced from two contexts (the TinyUSB task, when a // host OUTPUT report arrives, and our periodic sender). Funnel them through one // queue drained by a single sender task so we never call write_hid_report() from // the TinyUSB task (where spinning for the endpoint to drain would deadlock). struct OutReport { uint8_t id; std::vector<uint8_t> data; }; std::deque<OutReport> tx_queue; std::mutex tx_mutex; std::condition_variable tx_cv; auto enqueue = [&](espp::SwitchPro::ReportData rd) { { std::lock_guard<std::mutex> lock(tx_mutex); // cap the queue so a misbehaving host cannot grow it without bound if (tx_queue.size() < 16) tx_queue.push_back({rd.first, std::move(rd.second)}); } tx_cv.notify_one(); }; // Configure the USB device as a single HID interface advertising the Switch Pro // report descriptor, with an interrupt-OUT endpoint so we receive the host's // OUTPUT reports (the handshake). VID/PID/strings are Nintendo's Pro Controller // identifiers -- required for a real Switch to bind it; EMULATION / testing only. espp::UsbDevice::Config usb_cfg; usb_cfg.vid = espp::SwitchPro::vid; usb_cfg.pid = espp::SwitchPro::pid; usb_cfg.manufacturer = espp::SwitchPro::manufacturer_name; usb_cfg.product = espp::SwitchPro::product_name; usb_cfg.log_level = espp::Logger::Verbosity::WARN; espp::UsbDevice::HidFunction hid; hid.interface_name = "Switch Pro Controller"; hid.report_descriptor = controller.get_report_descriptor(); hid.has_out_endpoint = true; // receive host OUTPUT reports (the handshake) hid.poll_interval_ms = 8; // the real Pro Controller polls at 8 ms (full-speed) hid.on_receive = [&](std::span<const uint8_t> data) { // TinyUSB task context: compute the reply and queue it (don't send here). if (data.empty()) return; if (auto reply = controller.on_hid_report(data[0], data.data(), data.size())) enqueue(std::move(*reply)); }; usb_cfg.hid = hid; espp::UsbDevice usb(usb_cfg); // On mount, kick off the handshake: the controller proactively sends its // device-init (0x81) report. usb.set_mount_callback([&]() { if (auto init = controller.on_attach()) enqueue(std::move(*init)); }); std::error_code ec; if (!usb.initialize(ec)) { logger.error("Failed to initialize USB device: {}", ec.message()); return; } logger.info("USB HID Switch Pro controller ready; connect it to a Nintendo Switch."); std::atomic<bool> running{true}; // Sender task: drain queued replies first (retrying, since this is our own task // and may block), otherwise stream the standard input report once the host has // enabled reports. std::thread sender([&]() { while (running.load()) { OutReport rep; bool have = false; { std::unique_lock<std::mutex> lock(tx_mutex); tx_cv.wait_for(lock, 15ms, [&]() { return !tx_queue.empty() || !running.load(); }); if (!tx_queue.empty()) { rep = std::move(tx_queue.front()); tx_queue.pop_front(); have = true; } } if (have) { std::error_code send_ec; for (int i = 0; i < 20 && !usb.write_hid_report(rep.id, rep.data, send_ec); ++i) std::this_thread::sleep_for(1ms); } else if (controller.is_ready()) { auto std_report = controller.get_input_report(); if (!std_report.empty()) { std::error_code send_ec; usb.write_hid_report(controller.input_report_id(), std_report, send_ec); // best-effort } } } }); // Demo: once ready, cycle A / B / X / Y (500 ms each) and sweep the left stick, // so a connected Switch shows live input. using InputReport = espp::SwitchPro::InputReport; int step = 0; while (true) { if (controller.is_ready()) { const int which = step % 4; const float angle = (step % 20) / 20.0f * 2.0f * 3.14159265f; controller.update_input_report([&](InputReport &r) { r.reset(); r.set_button_a(which == 0); r.set_button_b(which == 1); r.set_button_x(which == 2); r.set_button_y(which == 3); r.set_left_joystick(0.5f * std::cos(angle), 0.5f * std::sin(angle)); }); ++step; } std::this_thread::sleep_for(250ms); } running.store(false); tx_cv.notify_all(); sender.join(); }
Note
**Emulation only.** A real Switch only binds a device advertising Nintendo’s Pro Controller USB VID/PID (0x057E / 0x2009) and identity strings (exposed here as constants). Use these to emulate/test against a Switch you own; do not ship a product impersonating Nintendo hardware.
Public Types
-
using InputReport = espp::SwitchProGamepadInputReport<>
The hid-rp standard input report type (report id 0x30).
-
using ReportData = std::pair<uint8_t, std::vector<uint8_t>>
A HID report to send to the host: {report id, report bytes}.
Public Functions
-
inline std::vector<uint8_t> get_report_descriptor() const
The HID report descriptor bytes (the full Switch Pro descriptor).
- Returns:
A copy of the report descriptor bytes.
-
inline uint8_t input_report_id() const
The report id of the standard input report (0x30).
-
inline bool is_ready() const
Whether the handshake has progressed far enough that the host has enabled input reports (i.e. it is meaningful to stream them).
-
std::optional<ReportData> on_attach()
Kick off the initialization sequence.
Call once when the USB device is attached/mounted. Returns the initial device-info report the controller sends unprompted to start the handshake (report id 0x81), or std::nullopt if none.
-
std::optional<ReportData> on_hid_report(uint8_t report_id, const uint8_t *data, size_t len)
Handle a host OUTPUT report (host -> device) and produce the reply.
Feed every OUTPUT report the host sends (report ids 0x80 init, 0x01 output / subcommand, 0x10 rumble) here; byte 0 of
datamust be the report id. Returns the INPUT report to send back (report id + bytes), or std::nullopt if no reply is warranted.- Parameters:
report_id – The HID report id (ignored; the type is read from data[0]).
data – The received report bytes (data[0] is the report id/type).
len – Number of bytes at
data.
-
std::vector<uint8_t> get_input_report() const
The current standard input report bytes (report id 0x30 payload).
- Returns:
The report bytes, or an empty vector if not ready yet.
-
void update_input_report(const std::function<void(InputReport&)> &fn)
Thread-safely mutate the input report (buttons / joysticks / dpad).
The callback receives the underlying `hid-rp` input report; use its setters (e.g. `set_button_a()`, `set_left_joystick()`, `set_dpad()`). Housekeeping fields (USB-powered, battery, connection info) are (re)applied afterward so a caller-issued `reset()` does not clear them.
- Parameters:
fn – Callback that mutates the input report.
-
void set_battery_level(uint8_t level)
Set the reported battery level (0-100).
-
void set_trigger_elapsed_times(const std::array<uint16_t, 7> ×_10ms)
Set the trigger-buttons-elapsed times (subcommand 0x04 reply), in units of 10 ms, order L,R,ZL,ZR,SL,SR,HOME. Optional; defaults to 0.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Attributes
-
static constexpr uint16_t vid = 0x057E
Nintendo USB VID.
-
static constexpr uint16_t pid = 0x2009
Switch Pro Controller USB PID.
-
static constexpr uint16_t bcd_device = 0x0200
bcdDevice.
-
static constexpr uint16_t bcd_usb = 0x0200
bcdUSB (USB 2.0).
-
static constexpr const char *manufacturer_name = "Nintendo Co., Ltd."
iManufacturer.
-
static constexpr const char *product_name = "Pro Controller"
iProduct.
-
using InputReport = espp::SwitchProGamepadInputReport<>