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
35 changes: 34 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
@endcode</td>
</tr>
<tr>
<td rowspan="4">Choices</td>
<td rowspan="5">Choices</td>
<td>all</td>
<td>Windows only: prefer Virtual HID Broker when it is available and licensed, with ViGEmBus as a limited fallback.</td>
</tr>
Expand All @@ -364,12 +364,45 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
<td>vigembus</td>
<td>Windows only: use ViGEmBus for gamepads and hide Virtual HID Broker status and licensing details.</td>
</tr>
<tr>
<td>esp32_bridge</td>
<td>Windows only: publish gamepads, keyboard, and mouse through an ESP32-S3 USB bridge plugged into this PC.
The bridge enumerates as USB devices, and Virtual HID Broker notices are not shown. It holds two devices: keyboard and mouse share one, which leaves
one gamepad, and only one Xbox controller fits. Sunshine selects this automatically on first start when
a bridge is attached and no backend has been chosen. The bridge only accepts input from paired hosts:
the first time Sunshine connects, press the BOOT button on the bridge to pair it. The pairing secret is
stored in `%ProgramData%\libvirtualhid\esp32-bridge`.</td>
</tr>
<tr>
<td>none</td>
<td>Disable virtual gamepads on Windows and macOS without changing keyboard or mouse input. Suppress broker and driver choice notices.</td>
</tr>
</table>

### esp32_bridge_port

<table>
<tr>
<td>Description</td>
<td colspan="2">
Serial port of the ESP32-S3 USB bridge when `gamepad_driver` is `esp32_bridge`. Leave empty to find the
bridge automatically by its `LVHB-` USB serial number. Plug the bridge in before starting Sunshine.
</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}
empty (auto-detect)
@endcode</td>
</tr>
<tr>
<td>Example</td>
<td colspan="2">@code{}
esp32_bridge_port = COM5
@endcode</td>
</tr>
</table>

### gamepad

<table>
Expand Down
12 changes: 12 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ namespace config {
true, // client gamepads with motion events use PlayStation-style emulation
true, // client gamepads with touchpads use PlayStation-style emulation
true, // virtualhid_randomize_mac
{}, // esp32_bridge_port: auto-detect

true, // keyboard enabled
false, // key_rightalt_to_key_win
Expand Down Expand Up @@ -1091,6 +1092,15 @@ namespace config {
return true;
}

bool select_esp32_bridge_if_detected(const bool bridge_detected) {
if (!bridge_detected || !input.gamepad_driver.empty() || !persist_config_option_if_missing("gamepad_driver", GAMEPAD_DRIVER_ESP32_BRIDGE)) {
return false;
}

input.gamepad_driver = GAMEPAD_DRIVER_ESP32_BRIDGE;
return true;
}

/**
* @brief Consume a string setting from the parsed configuration map.
*
Expand Down Expand Up @@ -1862,6 +1872,7 @@ namespace config {
GAMEPAD_DRIVER_ALL,
GAMEPAD_DRIVER_VIRTUALHID,
GAMEPAD_DRIVER_VIGEMBUS,
GAMEPAD_DRIVER_ESP32_BRIDGE,
GAMEPAD_DRIVER_NONE,
});
string_restricted_f(vars, "gamepad"s, input.gamepad, get_supported_gamepad_options());
Expand All @@ -1875,6 +1886,7 @@ namespace config {
bool_f(vars, "motion_as_ds4", input.motion_as_ds4);
bool_f(vars, "touchpad_as_ds4", input.touchpad_as_ds4);
bool_f(vars, "virtualhid_randomize_mac", input.virtualhid_randomize_mac);
string_f(vars, "esp32_bridge_port", input.esp32_bridge_port);

bool_f(vars, "mouse", input.mouse);
bool_f(vars, "keyboard", input.keyboard);
Expand Down
10 changes: 10 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ namespace config {
inline constexpr std::string_view GAMEPAD_DRIVER_ALL = "all"; ///< Allow every available Windows virtual gamepad driver.
inline constexpr std::string_view GAMEPAD_DRIVER_VIRTUALHID = "virtualhid"; ///< Use Virtual HID Broker for gamepads on Windows or macOS.
inline constexpr std::string_view GAMEPAD_DRIVER_VIGEMBUS = "vigembus"; ///< Allow only ViGEmBus for Windows gamepads.
inline constexpr std::string_view GAMEPAD_DRIVER_ESP32_BRIDGE = "esp32_bridge"; ///< Publish gamepads, keyboard, and mouse through an ESP32-S3 USB bridge.
inline constexpr std::string_view GAMEPAD_DRIVER_NONE = "none"; ///< Disable virtual gamepads on Windows and macOS.

// track modified config options
Expand Down Expand Up @@ -308,6 +309,7 @@ namespace config {
bool motion_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with motion controls.
bool touchpad_as_ds4; ///< Prefer PlayStation-style emulation for client gamepads with touchpad input.
bool virtualhid_randomize_mac; ///< Randomize the libvirtualhid virtual controller MAC address.
std::string esp32_bridge_port; ///< ESP32-S3 bridge serial port such as `COM5`; empty auto-detects.

bool keyboard; ///< Enable keyboard input from clients.
bool key_rightalt_to_key_win; ///< Map the client Right Alt key to the Windows key.
Expand Down Expand Up @@ -458,4 +460,12 @@ namespace config {
* @return True when the `all` preference was persisted and applied; otherwise, false.
*/
bool select_all_gamepad_drivers_if_licensed(bool virtualhid_licensed);

/**
* @brief Select the ESP32-S3 USB bridge when one is attached and the user has not made a choice.
*
* @param bridge_detected Whether an ESP32-S3 bridge is attached.
* @return True when the `esp32_bridge` preference was persisted and applied; otherwise, false.
*/
bool select_esp32_bridge_if_detected(bool bridge_detected);
} // namespace config
67 changes: 58 additions & 9 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "platform/windows/misc.h"
#include "platform/windows/utf_utils.h"

#include <libvirtualhid/esp32_bridge.hpp>
#include <Windows.h>
#elif defined(__APPLE__)
#include "platform/macos/misc.h"
Expand Down Expand Up @@ -2107,16 +2108,22 @@ namespace confighttp {
std::string backend_name;
std::string runtime_error_message;

try {
const auto runtime = platf::virtualhid::create_runtime();
if (runtime) {
const auto &capabilities = runtime->capabilities();
backend_name = capabilities.backend_name;
requires_installed_driver = capabilities.requires_installed_driver;
output_tree = build_driver_status(driver_detected || capabilities.supports_gamepad, version_str, minimum_version);
if (platf::virtualhid::esp32_bridge_selected()) {
// The input context owns the bridge port; report it without opening a second runtime.
backend_name = "esp32_bridge";
requires_installed_driver = false;
} else {
try {
const auto runtime = platf::virtualhid::create_runtime();
if (runtime) {
const auto &capabilities = runtime->capabilities();
backend_name = capabilities.backend_name;
requires_installed_driver = capabilities.requires_installed_driver;
output_tree = build_driver_status(driver_detected || capabilities.supports_gamepad, version_str, minimum_version);
}
} catch (const std::bad_alloc &exception) {
runtime_error_message = exception.what();
}
} catch (const std::bad_alloc &exception) {
runtime_error_message = exception.what();
}

output_tree["backend_name"] = backend_name;
Expand Down Expand Up @@ -2183,6 +2190,47 @@ namespace confighttp {
send_response(response, output_tree);
}

/**
* @brief Get the connection and pairing state of the ESP32-S3 USB bridge.
*
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/esp32-bridge/status|:| GET|:| null}
*/
void getEsp32BridgeStatus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}

print_req(request);

nlohmann::json output_tree;
output_tree["selected"] = config::input.gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE;
#ifdef _WIN32
const auto status = lvh::esp32_bridge_status();
output_tree["active"] = status.active;
output_tree["connected"] = status.connected;
output_tree["awaiting_pairing"] = status.awaiting_pairing;
output_tree["pairing_required"] = status.pairing_required;
output_tree["port"] = status.port;
output_tree["device_id"] = status.device_id;
output_tree["firmware_version"] = status.firmware_version;
// Discovery reads USB serial numbers only; it never opens a port.
output_tree["detected"] = !lvh::find_esp32_bridge_ports().empty();
#else
output_tree["active"] = false;
output_tree["connected"] = false;
output_tree["awaiting_pairing"] = false;
output_tree["pairing_required"] = false;
output_tree["port"] = "";
output_tree["device_id"] = "";
output_tree["firmware_version"] = "";
output_tree["detected"] = false;
#endif
send_response(response, output_tree);
}

/**
* @brief Get the current libvirtualhid machine license status.
*
Expand Down Expand Up @@ -2511,6 +2559,7 @@ namespace confighttp {
server.resource["^/api/virtual-input/license$"]["GET"] = getVirtualInputLicense;
server.resource["^/api/virtual-input/license$"]["POST"] = updateVirtualInputLicense;
server.resource["^/api/virtual-input/status$"]["GET"] = getVirtualInputStatus;
server.resource["^/api/esp32-bridge/status$"]["GET"] = getEsp32BridgeStatus;

// static/dynamic resources
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
Expand Down
8 changes: 8 additions & 0 deletions src/confighttp.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ namespace confighttp {

void getVirtualInputStatus(const resp_https_t &response, const req_https_t &request);

/**
* @brief Get the connection and pairing state of the ESP32-S3 USB bridge.
*
* @param response The HTTP response object.
* @param request The HTTP request object.
*/
void getEsp32BridgeStatus(const resp_https_t &response, const req_https_t &request);

/**
* @brief Return the host's permission statuses to the Web UI.
*
Expand Down
3 changes: 3 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
// lib includes
#include <rs.h>
#ifdef _WIN32
#include <libvirtualhid/esp32_bridge.hpp>
#include <libvirtualhid/license.hpp>
#endif

Expand Down Expand Up @@ -285,6 +286,8 @@ int main(int argc, char *argv[]) {
}

#ifdef _WIN32
// Check for an attached bridge before applying the default selection.
config::select_esp32_bridge_if_detected(!lvh::find_esp32_bridge_ports().empty());
config::select_all_gamepad_drivers_if_licensed(lvh::get_license_status().license.licensed());
#endif

Expand Down
7 changes: 7 additions & 0 deletions src/platform/linux/input/virtualhid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ using namespace std::literals;
namespace platf {
platform_caps::caps_t get_capabilities() {
platform_caps::caps_t caps = 0;
// The bridge port is owned by the input context and has no pen or touchscreen slot.
if (virtualhid::esp32_bridge_selected()) {
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
return caps;
}
const auto runtime = virtualhid::create_runtime();
if (!runtime) {
return caps;
Expand Down
7 changes: 7 additions & 0 deletions src/platform/macos/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ namespace platf {

platform_caps::caps_t get_capabilities() {
platform_caps::caps_t caps = 0;
// The bridge port is owned by the input context and has no pen or touchscreen slot.
if (virtualhid::esp32_bridge_selected()) {
if (virtualhid::configured_gamepad_supports_controller_extensions()) {
caps |= platform_caps::controller_touch;
}
return caps;
}
const auto runtime = virtualhid::create_runtime();
if (!runtime) {
return caps;
Expand Down
16 changes: 14 additions & 2 deletions src/platform/virtualhid_input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ namespace platf::virtualhid {
} // namespace

input_context_t::input_context_t():
input_context_t {lvh::BackendKind::platform_default} {}
input_context_t {configured_backend()} {}

input_context_t::input_context_t(lvh::BackendKind backend):
runtime {create_runtime(backend)} {
Expand Down Expand Up @@ -606,10 +606,22 @@ namespace platf::virtualhid {
}
}

bool esp32_bridge_selected() {
return config::input.gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE;
}

lvh::BackendKind configured_backend() {
return esp32_bridge_selected() ? lvh::BackendKind::esp32_bridge : lvh::BackendKind::platform_default;
}

std::unique_ptr<lvh::Runtime> create_runtime(lvh::BackendKind backend) {
lvh::RuntimeOptions options;
options.backend = backend;
options.log_callback = log_virtualhid_message;
options.esp32_bridge.port = config::input.esp32_bridge_port;
options.esp32_bridge.client_name = "Sunshine";
// The bridge is an explicit gamepad_driver choice; never switch to it behind another choice.
options.esp32_bridge.autodetect = false;
return lvh::Runtime::create(options);
}

Expand Down Expand Up @@ -677,7 +689,7 @@ namespace platf::virtualhid {
const bool virtualhid_selected,
const std::string_view gamepad_driver
) {
if (gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID || gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
if (gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID || gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE || gamepad_driver == config::GAMEPAD_DRIVER_NONE) {
return false;
}
if (gamepad_driver == config::GAMEPAD_DRIVER_VIGEMBUS || !virtualhid_selected) {
Expand Down
17 changes: 17 additions & 0 deletions src/platform/virtualhid_input.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ namespace platf::virtualhid {
*/
client_context_t &get_client_context(client_input_t *input);

/**
* @brief Check whether `gamepad_driver = esp32_bridge` is selected.
*
* The bridge serial port can only be opened once, so in this mode the shared
* input context owns the only runtime and capability probes must not create another.
*
* @return True when the ESP32-S3 bridge carries all virtual input.
*/
bool esp32_bridge_selected();

/**
* @brief Return the backend for the configured gamepad driver.
*
* @return `esp32_bridge` when the bridge is selected, otherwise `platform_default`.
*/
lvh::BackendKind configured_backend();

/**
* @brief Create a platform-default libvirtualhid runtime.
*
Expand Down
13 changes: 8 additions & 5 deletions src/platform/windows/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,9 @@ namespace platf {
*/
bool should_allocate_vigembus_gamepad(const bool virtualhid_selected) {
if (!virtualhid::should_try_vigembus_fallback(config::input.gamepad, virtualhid_selected, config::input.gamepad_driver)) {
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID && !virtualhid_selected) {
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE && !virtualhid_selected) {
BOOST_LOG(warning) << "ESP32-S3 USB bridge is selected but is not connected or has no free slot; ViGEmBus fallback is disabled by configuration"sv;
} else if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID && !virtualhid_selected) {
BOOST_LOG(warning) << "Virtual HID Driver is selected but is unavailable or unlicensed; ViGEmBus fallback is disabled by configuration"sv;
} else {
BOOST_LOG(warning) << "libvirtualhid could not create the requested gamepad profile, and ViGEm fallback cannot emulate "sv << config::input.gamepad;
Expand Down Expand Up @@ -1300,11 +1302,11 @@ namespace platf {
}

if (!raw->virtualhid.runtime) {
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID) {
if (config::input.gamepad_driver == config::GAMEPAD_DRIVER_VIRTUALHID || config::input.gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE) {
gps = virtualhid::static_supported_gamepads();
for (auto &gamepad : gps) {
gamepad.is_enabled = false;
gamepad.reason_disabled = "gamepads.virtualhid-not-available";
gamepad.reason_disabled = config::input.gamepad_driver == config::GAMEPAD_DRIVER_ESP32_BRIDGE ? "gamepads.esp32-bridge-not-available" : "gamepads.virtualhid-not-available";
}
} else {
gps = vigembus_supported_gamepads(raw->vigem != nullptr);
Expand All @@ -1314,7 +1316,7 @@ namespace platf {

const auto &capabilities = raw->virtualhid.runtime->capabilities();
const auto licensed = !capabilities.requires_installed_driver || lvh::get_license_status().license.licensed();
if (const auto use_virtualhid = virtualhid::should_use_gamepad_runtime(capabilities, config::input.gamepad_driver, licensed); !use_virtualhid && config::input.gamepad_driver != config::GAMEPAD_DRIVER_VIRTUALHID) {
if (const auto use_virtualhid = virtualhid::should_use_gamepad_runtime(capabilities, config::input.gamepad_driver, licensed); !use_virtualhid && config::input.gamepad_driver != config::GAMEPAD_DRIVER_VIRTUALHID && config::input.gamepad_driver != config::GAMEPAD_DRIVER_ESP32_BRIDGE) {
gps = vigembus_supported_gamepads(raw->vigem != nullptr);
return gps;
}
Expand All @@ -1334,7 +1336,8 @@ namespace platf {
caps |= platform_caps::controller_touch;
}

if (config::input.native_pen_touch) {
// The bridge has no pen or touchscreen slot; clients fall back to mouse input.
if (config::input.native_pen_touch && !virtualhid::esp32_bridge_selected()) {
const auto runtime = virtualhid::create_runtime();
if (runtime) {
const auto &capabilities = runtime->capabilities();
Expand Down
Loading