mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-23 07:02:16 -05:00
Add Bluetooth LE support for the Steam Controller
The controller now works wired, through the dongle, or paired over Bluetooth, with the full feature set: game mode, rumble, trackpad haptics, battery, PlayStation-mode gyro, and taking the controller back from a running Steam. Windows exposes the entire vendor protocol over HID-over-GATT under PID 0x1303 — feature reports, output reports, and the 0x45 state report (54 bytes, ~157 Hz) all work through the existing HidDevice stack, so support is a thin layer rather than a new transport backend. - Add SC2026_BT_PID to enumeration and a per-slot Transport tag, detected from the interface path (HOGP GUID / bthenum markers win over PID tokens) and logged on CONNECT/DISCONNECT - Swap the battery report 0x43 payload bytes on Bluetooth slots — the firmware sends [chargeState, percent], reversed from USB/dongle - Bluetooth-specific disconnect alert copy, and a notification when acquisition gives up rather than a silently dead pad Device cycling had to be reworked for reclaiming the controller from Steam. DIF_PROPERTYCHANGE reports success while leaving the device and Steam's handle untouched; CM_Query_And_Remove_SubTree is vetoed by open handles; pnputil /restart-device works once, then leaves the devnode reboot-pending and refuses thereafter. Disable+enable works repeatedly. Two traps found against live hardware. SetupDiCallClassInstaller returned ERROR_INVALID_DATA for a disable Windows had genuinely performed — the app logged the device removal a tenth of a second before the call reported failure — so return codes cannot gate the re-enable, or the controller is stranded disabled and needs Device Manager to recover. CM_Get_DevNode_Status is now the source of truth: whether the node went down decides if a real cycle happened, the re-enable always runs and retries until the node confirms it is enabled, and only a stranded device counts as failure. Some devices also reject a global scope change, so DICS_FLAG_CONFIGSPECIFIC is tried as well. SteamProbe becomes a transport discovery tool: detailed enumeration with transport detection, interface selection, non-fatal lizard-off with a raw-dump fallback, an output-report test, report-rate measurement, and --enum / --imu modes. The windowless cycle helper gains its own cycle.log, since it cannot share the tray app's events.log (held open _SH_DENYWR) and was previously impossible to diagnose. Verified on hardware: game mode, rumble, haptics, battery, IMU streaming, clean removal on power-off/sleep/range loss with automatic reconnect, yielding to Steam on game launch, and reclaiming afterwards (~1.2s). Also confirmed the firmware enforces one host transport at a time, so no cross-transport deduplication is needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c3f4aacf0
commit
50a8ae246d
9 files changed
+483
-98
No files matched your search
@@ -11,6 +11,7 @@
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slot
|
||||
@@ -18,6 +19,7 @@
|
||||
|
||||
struct ControllerManager::Slot {
|
||||
std::wstring path;
|
||||
SteamController::Transport transport = SteamController::Transport::Unknown;
|
||||
std::unique_ptr<SteamController> sc;
|
||||
std::unique_ptr<VirtualController> vc;
|
||||
TrackpadMouse trackpad;
|
||||
@@ -295,17 +297,25 @@ void ControllerManager::SyncDevices() {
|
||||
bool alive = std::any_of(livePaths.begin(), livePaths.end(),
|
||||
[&](const auto& p) { return p == (*it)->path; });
|
||||
if (!alive) {
|
||||
EventLog::Write("DISCONNECT: OS removed device (gameMode=%d, lastBattery=%d%%) %ls",
|
||||
EventLog::Write("DISCONNECT: OS removed device (transport=%s, gameMode=%d, lastBattery=%d%%) %ls",
|
||||
SteamController::TransportName((*it)->transport),
|
||||
(*it)->gameModeActive ? 1 : 0,
|
||||
(*it)->lastBatteryPercent, (*it)->path.c_str());
|
||||
// Alert only when the controller was in active use; deliberate
|
||||
// unplugs while idle (and our own device cycles, which release
|
||||
// slots before cycling) stay quiet.
|
||||
if ((*it)->gameModeActive && m_alertFn)
|
||||
m_alertFn(L"Controller disconnected",
|
||||
L"The USB device was removed. Check the cable or dongle — "
|
||||
L"if this happens randomly, Windows USB power saving may be "
|
||||
L"suspending the port.");
|
||||
if ((*it)->gameModeActive && m_alertFn) {
|
||||
if ((*it)->transport == SteamController::Transport::Bluetooth)
|
||||
m_alertFn(L"Controller disconnected",
|
||||
L"The Bluetooth connection dropped — the controller "
|
||||
L"powered off, went to sleep, or is out of range. "
|
||||
L"Press the Steam button to reconnect.");
|
||||
else
|
||||
m_alertFn(L"Controller disconnected",
|
||||
L"The USB device was removed. Check the cable or dongle — "
|
||||
L"if this happens randomly, Windows USB power saving may be "
|
||||
L"suspending the port.");
|
||||
}
|
||||
DisableGameModeSlot(**it);
|
||||
it = m_slots.erase(it);
|
||||
} else {
|
||||
@@ -325,10 +335,14 @@ void ControllerManager::SyncDevices() {
|
||||
|
||||
void ControllerManager::OpenSlot(const std::wstring& path) {
|
||||
auto slot = std::make_unique<Slot>();
|
||||
slot->path = path;
|
||||
slot->sc = std::make_unique<SteamController>();
|
||||
slot->path = path;
|
||||
slot->transport = SteamController::TransportFromPath(path);
|
||||
slot->sc = std::make_unique<SteamController>();
|
||||
if (!slot->sc->Open(path)) return;
|
||||
|
||||
EventLog::Write("CONNECT: transport=%s %ls",
|
||||
SteamController::TransportName(slot->transport), path.c_str());
|
||||
|
||||
m_slots.push_back(std::move(slot));
|
||||
|
||||
if (IsGameModeActive()) {
|
||||
@@ -479,11 +493,19 @@ void ControllerManager::ReadLoop(Slot* slot) {
|
||||
// Battery status — update DS4 battery level; no further processing needed.
|
||||
if (buf[0] == SteamController::REPORT_BATTERY_STATUS) {
|
||||
if (n >= 3) {
|
||||
// USB/dongle payload order is [percent, chargeState]; over
|
||||
// Bluetooth the firmware sends the same two bytes swapped —
|
||||
// observed BT reports held a constant 1 (CHARGE_STATE_DISCHARGING)
|
||||
// in buf[1] while buf[2] tracked the real charge level.
|
||||
uint8_t percent = buf[1];
|
||||
uint8_t chargeState = buf[2];
|
||||
if (slot->transport == SteamController::Transport::Bluetooth)
|
||||
std::swap(percent, chargeState);
|
||||
if (slot->vc)
|
||||
slot->vc->SetBatteryState(buf[1], buf[2]);
|
||||
if (slot->lastBatteryPercent != static_cast<int>(buf[1])) {
|
||||
EventLog::Write("BATTERY: %u%% (chargeState=%u)", buf[1], buf[2]);
|
||||
slot->lastBatteryPercent = static_cast<int>(buf[1]);
|
||||
slot->vc->SetBatteryState(percent, chargeState);
|
||||
if (slot->lastBatteryPercent != static_cast<int>(percent)) {
|
||||
EventLog::Write("BATTERY: %u%% (chargeState=%u)", percent, chargeState);
|
||||
slot->lastBatteryPercent = static_cast<int>(percent);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
||||
Reference in new issue
Block a user