Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ More complete examples live in `examples/`, including the streaming-host-oriente
- [Platform support](docs/platform-support.md): device availability, platform limits, and Linux permissions.
- [Windows driver package](docs/windows-driver.md): installation, licensing, diagnostics, and contributor builds.
- [macOS gamepad setup](docs/macos-gamepad.md): installation, licensing, and signed builds.
- [ESP32-S3 USB bridge](docs/esp32-bridge.md): backend selection, slot limits, and pairing.
- [Streaming-host integration](docs/streaming-host-integration.md): integration pattern.
- [Development](docs/development.md): build, test, and documentation commands.

Expand Down
1 change: 1 addition & 0 deletions dockle.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ inputs = [
"docs/end-user-gamepad-guide.md",
"docs/usage.md",
"docs/platform-support.md",
"docs/esp32-bridge.md",
"docs/windows-driver.md",
"docs/macos-gamepad.md",
"docs/streaming-host-integration.md",
Expand Down
94 changes: 94 additions & 0 deletions docs/esp32-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ESP32-S3 USB bridge

The `esp32_bridge` backend creates devices on an ESP32-S3 connected to the host
over USB. The board enumerates as a composite USB device: a CDC-ACM serial port
that the backend talks to, plus up to two device slots. The operating system
binds its inbox drivers to the slots (`xusb22` for XInput, `hidclass` or the
kernel HID drivers for HID).

Firmware, a browser flasher, and supported boards are maintained in
[esp32s3-hid-bridge](https://github.com/cheetahdotcat/esp32s3-hid-bridge). The
wire protocol is defined in `src/platform/esp32/esp32_bridge_protocol.hpp`; the
firmware keeps a C copy of it.

## Selecting the backend

Set `RuntimeOptions::backend` to `BackendKind::esp32_bridge`, or let
`BackendKind::platform_default` select the bridge:

| Condition (checked in order) | Backend used |
|----------------------------------------------------|-------------------|
| `esp32_bridge.autodetect` is `false` | platform |
| `LVH_BACKEND=platform` | platform |
| `LVH_BACKEND=esp32_bridge` | bridge |
| `LVH_ESP32_PORT` or `esp32_bridge.port` is set | bridge, that port |
| A port with USB serial number `LVHB-*` is attached | bridge |
| Otherwise | platform |

Detection runs once, when the runtime is created. Applications that let users
choose the bridge explicitly should set `esp32_bridge.autodetect = false`.
`LVH_ESP32_PORT` also overrides the port for `BackendKind::esp32_bridge`. When
`platform_default` selected the bridge and no bridge answers, gamepads fall
back to the platform backend.
`lvh::find_esp32_bridge_ports()` lists attached bridges without opening them.
Port discovery is available on Linux and Windows; other platforms need an
explicit port.

## Slots and profiles

The ESP32-S3 USB controller limits the bridge to two slots next to the serial
port. Keyboard and mouse share one slot; each gamepad takes one.

| Slot A | Slot B |
|----------------|-------------------------|
| keyboard+mouse | none or one gamepad |
| Xbox | PlayStation or Switch |
| PlayStation | same PlayStation family |

| Requested profile | Slot type | USB identity |
|----------------------------|---------------------------------------------------|------------------|
| Xbox 360, One, Series | XInput (`XUSB10` compatible ID) | bridge VID/PID |
| Generic | XInput | bridge VID/PID |
| DualSense | HID with cached feature reports | `054C:0CE6` |
| DualShock 4 | HID with cached feature reports | `054C:09CC` |
| Switch Pro | HID; subcommand replies generated by the backend | `057E:2009` |
| Keyboard, mouse | HID keyboard, relative and absolute pointer | bridge VID/PID |

Every slot change re-enumerates the whole bridge, so other devices disconnect
for about one second. `create_gamepad()` returns after the bridge is back.
Keyboard and mouse reports are queued in order and never replayed after a
reconnect; the latest gamepad report is.

Text that is not US-ASCII, and keyboard or mouse requests when both slots are
taken, go to the platform backend. Touchscreen, trackpad, and pen tablet
devices are not available.

## Pairing

The bridge accepts devices and input only from paired hosts, unless pairing
was turned off on the bridge.

- When a host connects without a stored pairing, the backend asks the bridge to
pair and logs a warning. The user confirms by pressing BOOT on the bridge.
Devices created before that succeed; they appear once pairing completes.
- The bridge returns a secret, which the backend stores per bridge in
`esp32_bridge.credential_dir` (default `%ProgramData%\libvirtualhid\esp32-bridge`
on Windows, `$XDG_CONFIG_HOME/libvirtualhid/esp32-bridge` elsewhere). Only the
file owner, and on Windows SYSTEM and Administrators, can read it.
- Each connection authenticates with HMAC-SHA256 over a fresh challenge.
- `esp32_bridge.client_name` labels the pairing on the bridge.
- `lvh::esp32_bridge_status()` reports whether the bridge is connected or waits
for the BOOT button.

Pairing keeps other non-administrator programs from sending input through the
bridge. It does not protect against software running as administrator.

## Troubleshooting

| Problem | Fix |
|-------------------------------------------|-------------------------------------------------------------------------|
| `no ESP32-S3 bridge answered` | Check the cable and that the board runs the bridge firmware. |
| `bridge firmware speaks a different ...` | Flash firmware that matches this libvirtualhid version. |
| `bridge is not paired with this host` | Press BOOT on the bridge while its LED blinks quickly. |
| Devices disappear for a second | Expected when a device is added or removed. |
| A second gamepad fails to appear | Both slots are in use, or the combination is not in the table above. |
1 change: 1 addition & 0 deletions docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and effective profile capabilities before enabling optional features.
| Linux | Generic and Xbox 360 through `uinput`; PlayStation, Switch Pro, Xbox One and Series through `uhid` | Keyboard, mouse, touchscreen, trackpad, pen tablet through `uinput`; XTest keyboard and mouse fallback | Writable device nodes and required kernel modules |
| FreeBSD | Built-in profiles through `uinput` | Keyboard, mouse, touchscreen, trackpad, pen tablet through `uinput`; XTest fallback | Writable `uinput` device node |
| macOS | Built-in profiles through the virtual HID broker | CoreGraphics keyboard and mouse | Signed broker, Apple entitlement, system permission, and machine license |
| ESP32-S3 | Xbox, PlayStation, Switch Pro on the board; two slots in total | Keyboard and mouse on the board | Bridge firmware; see [ESP32-S3 USB bridge](esp32-bridge.md) |

## Windows

Expand Down
15 changes: 13 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@ function(libvirtualhid_add_library target_name)
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/core/profiles.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/core/report.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/core/runtime.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/core/types.cpp")
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/core/types.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/bridge_client.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/credential_store.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/esp32_backend.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/hmac_sha256.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/keyboard_mouse_reports.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/serial_port.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/esp32/xinput_reports.cpp"
"${PROJECT_SOURCE_DIR}/third-party/lizardbyte-common/src/common/env.cpp")
target_include_directories(${target_name}
PRIVATE
"${PROJECT_SOURCE_DIR}/third-party/lizardbyte-common/src/include")

if(LIBVIRTUALHID_USES_THREADS)
find_package(Threads REQUIRED)
Expand Down Expand Up @@ -76,7 +87,6 @@ function(libvirtualhid_add_library target_name)
elseif(WIN32)
target_sources(${target_name}
PRIVATE
"${PROJECT_SOURCE_DIR}/third-party/lizardbyte-common/src/common/env.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/windows/windows_backend.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/windows/windows_broker_client.cpp"
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platform/windows/windows_license.cpp")
Expand All @@ -92,6 +102,7 @@ function(libvirtualhid_add_library target_name)
target_link_libraries(${target_name}
PRIVATE
advapi32
cfgmgr32
setupapi)
elseif(APPLE)
target_sources(${target_name}
Expand Down
10 changes: 8 additions & 2 deletions src/core/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

// local includes
#include "core/backend.hpp"
#include "platform/esp32/esp32_backend.hpp"

namespace lvh::detail {
namespace {
Expand Down Expand Up @@ -187,10 +188,15 @@ namespace lvh::detail {

} // namespace

std::unique_ptr<Backend> create_backend(BackendKind kind) {
if (kind == BackendKind::fake) {
std::unique_ptr<Backend> create_backend(const RuntimeOptions &options) {
if (options.backend == BackendKind::fake) {
return std::make_unique<FakeBackend>();
}
// An attached ESP32-S3 bridge can replace the platform backend, so existing
// consumers use it without code changes.
if (auto bridge = esp32::create_backend_if_selected(options); bridge != nullptr) {
return bridge;
}

return create_platform_backend();
}
Expand Down
6 changes: 3 additions & 3 deletions src/core/backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -558,12 +558,12 @@ namespace lvh::detail {
};

/**
* @brief Create a backend for the requested backend kind.
* @brief Create the backend requested by runtime options.
*
* @param kind Requested backend kind.
* @param options Runtime options naming the backend kind and its settings.
* @return Backend implementation.
*/
std::unique_ptr<Backend> create_backend(BackendKind kind);
std::unique_ptr<Backend> create_backend(const RuntimeOptions &options);

/**
* @brief Create the platform default backend for the current operating system.
Expand Down
2 changes: 1 addition & 1 deletion src/core/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ namespace lvh::detail {
explicit RuntimeState(RuntimeOptions runtime_options):
options {std::move(runtime_options)},
logger {std::make_shared<Logger>(options.log_callback)},
backend {create_backend(options.backend)},
backend {create_backend(options)},
caps {backend->capabilities()} {
logger->emit(LogLevel::info, "initialized " + caps.backend_name + " backend");
}
Expand Down
46 changes: 46 additions & 0 deletions src/include/libvirtualhid/esp32_bridge.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @file src/include/libvirtualhid/esp32_bridge.hpp
* @brief ESP32-S3 USB bridge discovery helpers.
*/
#pragma once

// standard includes
#include <string>
#include <vector>

namespace lvh {

/**
* @brief List serial ports of attached ESP32-S3 USB bridges.
*
* Bridges are recognized by their `LVHB-` USB serial number. Discovery is
* supported on Linux and Windows; other platforms return an empty list.
*
* @return Port paths such as `COM5` or `/dev/ttyACM0`.
*/
std::vector<std::string> find_esp32_bridge_ports();

/**
* @brief State of the ESP32-S3 bridge used by this process.
*/
struct Esp32BridgeStatus {
bool active = false; ///< A runtime in this process uses an ESP32-S3 bridge.
bool connected = false; ///< The bridge answered and accepts input from this host.
bool awaiting_pairing = false; ///< The bridge waits for its BOOT button to pair this host.
bool pairing_required = false; ///< The bridge requires pairing (reported by the last handshake).
std::string port; ///< Open serial port, empty while disconnected.
std::string device_id; ///< Bridge device id (upper-case hex MAC), empty before the first handshake.
std::string firmware_version; ///< Firmware version from the last handshake.
};

/**
* @brief Get the state of the bridge used by the most recently started bridge runtime.
*
* Does not open any port. Returns `active == false` when no runtime in this
* process uses the bridge.
*
* @return Bridge state.
*/
Esp32BridgeStatus esp32_bridge_status();

} // namespace lvh
1 change: 1 addition & 0 deletions src/include/libvirtualhid/libvirtualhid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

// local includes
#include <libvirtualhid/esp32_bridge.hpp>
#include <libvirtualhid/gamepad_adapter.hpp>
#include <libvirtualhid/license.hpp>
#include <libvirtualhid/profiles.hpp>
Expand Down
22 changes: 21 additions & 1 deletion src/include/libvirtualhid/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ namespace lvh {
*/
enum class BackendKind {
fake, ///< In-memory backend for tests and API validation.
platform_default, ///< Native backend for the current platform.
platform_default, ///< Native backend for the current platform, or an attached ESP32-S3 bridge (see Esp32BridgeOptions::autodetect).
esp32_bridge, ///< Devices enumerate as USB devices of an attached ESP32-S3 bridge.
};

/**
Expand All @@ -125,6 +126,20 @@ namespace lvh {
*/
using LogCallback = std::function<void(LogLevel level, const std::string &message)>;

/**
* @brief Settings of the ESP32-S3 USB bridge backend.
*
* BackendKind::platform_default switches to the bridge when #autodetect is
* set and `LVH_BACKEND=esp32_bridge`, `LVH_ESP32_PORT`, or #port is set, or a
* bridge is attached. `LVH_BACKEND=platform` keeps the platform backend.
*/
struct Esp32BridgeOptions {
std::string port; ///< Serial port such as `COM5` or `/dev/ttyACM0`; empty auto-detects by the `LVHB-` USB serial number (Linux and Windows).
bool autodetect = true; ///< Let BackendKind::platform_default switch to the bridge; set `false` when users choose the bridge explicitly.
std::string credential_dir; ///< Directory of pairing secrets; empty uses `%ProgramData%/libvirtualhid/esp32-bridge` or `$XDG_CONFIG_HOME/libvirtualhid/esp32-bridge`.
std::string client_name = "libvirtualhid"; ///< Application name of this host's pairing; the host name is appended.
};

/**
* @brief Runtime creation options.
*/
Expand All @@ -138,6 +153,11 @@ namespace lvh {
* @brief Optional callback for diagnostic messages.
*/
LogCallback log_callback;

/**
* @brief Settings of the ESP32-S3 USB bridge backend.
*/
Esp32BridgeOptions esp32_bridge;
};

/**
Expand Down
Loading