mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-23 07:02:16 -05:00
The receiver stopped working after a controller firmware update. Original diagnosis and fix by @danbosscher; extended here for Bluetooth, Steam handoff, and several defects found during QA on the hardware. The puck publishes a controller interface per slot plus a management endpoint, and on current firmware an empty slot rejects the lizard-mode command outright. - Enumerate only the controller collection (FF00:0001), excluding the puck's management endpoint at FF00:0002 - Wait for a live 0x42/0x45 state report before sending feature commands, and skip slots that never produce one - Prefer an exclusive claim but accept a shared handle when Windows keeps a compatible write handle open — only while Steam is absent, since a handle held with Steam running is probably Steam's, and driving the controller alongside it is worse than refusing. Refusing is what escalates to the device cycle that takes the handle back. - Add the Nereid receiver PID (0x1305) Defects found by QA and fixed here: - The battery payload is [chargeState, percent] on every transport, not just Bluetooth. A 92% battery was reported, and fed to the DS4 battery level, as 1%. - Game mode could not tell an idle receiver from a contested one, so a controller that was merely switched off triggered three device cycles, restarting the receiver's devnodes for nothing. - A receiver raises no device event when a slot becomes occupied, so waking a controller went unnoticed for as long as it was left. Poll for it instead. - Overlapping cycles undid successful claims: a cycle is asynchronous, and the arrivals it generated re-entered the acquire path and fired another on top of it, ripping the device back out a second after game mode came up. Cycles are now spaced. - Failure was announced while the last cycle was still landing — the alert fired one second before success. The verdict now waits out a grace period. Verified on the puck, Bluetooth, and wired: handoff to Steam on game launch, reclaim afterwards, and no duplicate input. Reclaiming takes around nine seconds on a four-slot receiver, dominated by SetupDiCallClassInstaller tearing down each interface in turn. Three attempts to shorten it (batching the cycle, a shorter settle, cycling only the contested interface) all regressed the reclaim and were reverted; it needs to be worked out against hardware rather than reasoned about. Co-Authored-By: Dan Bosscher <danbosscher@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59 lines
2.5 KiB
C++
59 lines
2.5 KiB
C++
#pragma once
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
class HidDevice {
|
|
public:
|
|
// Returns device paths for all HID interfaces matching vid/pid/usagePage/usage.
|
|
// Pass usagePage=0 to return all matching interfaces; usage=0 matches any usage.
|
|
static std::vector<std::wstring> Enumerate(uint16_t vid, uint16_t pid,
|
|
uint16_t usagePage = 0,
|
|
uint16_t usage = 0);
|
|
|
|
HidDevice() = default;
|
|
~HidDevice() { Close(); }
|
|
HidDevice(const HidDevice&) = delete;
|
|
HidDevice& operator=(const HidDevice&) = delete;
|
|
HidDevice(HidDevice&& o) noexcept;
|
|
HidDevice& operator=(HidDevice&& o) noexcept;
|
|
|
|
// Open with shared read/write access for idle tracking.
|
|
// Returns false if device is not found.
|
|
bool Open(const std::wstring& path);
|
|
// Close and reopen with a different share mode (e.g. to claim or release
|
|
// exclusive write access). Caller must ensure the read thread is stopped first.
|
|
bool Reopen(DWORD shareMode);
|
|
void Close();
|
|
bool IsOpen() const { return m_handle != INVALID_HANDLE_VALUE; }
|
|
|
|
// Send a HID output report (interrupt OUT / SET_REPORT Output type).
|
|
// data[0] must be the report ID. Padded to OutputReportByteLength automatically.
|
|
bool SendOutputReport(const uint8_t* data, size_t size);
|
|
|
|
// Write a HID output report through the interrupt output endpoint via WriteFile.
|
|
// data[0] must be the report ID. Padded to OutputReportByteLength automatically.
|
|
bool WriteOutputReport(const uint8_t* data, size_t size);
|
|
|
|
// Send a HID feature report (SET_REPORT Feature type via EP0 control pipe).
|
|
// data[0] must be the feature report ID. Padded to FeatureReportByteLength automatically.
|
|
// This is the command channel the original Steam Controller used for all firmware commands.
|
|
bool SendFeatureReport(const uint8_t* data, size_t size);
|
|
|
|
ULONG OutputReportByteLength() const { return m_outputReportLen; }
|
|
ULONG FeatureReportByteLength() const { return m_featureReportLen; }
|
|
|
|
// Read the next HID input report. buffer[0] will be the report ID on return.
|
|
// Returns bytes read, or 0 on timeout/error.
|
|
size_t ReadInputReport(uint8_t* buffer, size_t size, uint32_t timeoutMs = 1000);
|
|
|
|
private:
|
|
HANDLE m_handle = INVALID_HANDLE_VALUE;
|
|
HANDLE m_event = INVALID_HANDLE_VALUE;
|
|
ULONG m_outputReportLen = 64;
|
|
ULONG m_featureReportLen = 64;
|
|
std::wstring m_path;
|
|
};
|