|
|
@@ -13,8 +13,8 @@
|
|
|
uint8_t g_fwdNewOnly = 1;
|
|
|
SteamPuckController g_steamPuck;
|
|
|
|
|
|
-// ---- cloned puck HID report descriptor: mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte FEATURE
|
|
|
-// command reports on report id 1/2. Each of the 4 interfaces uses this. ----
|
|
|
+// Cloned puck HID report descriptor: mouse(0x40)+keyboard(0x41)+vendor(FF00) with the 63-byte FEATURE
|
|
|
+// command reports on report id 1/2. Each of the 4 interfaces uses this.
|
|
|
static const uint8_t PUCK_HID_DESC[] = {
|
|
|
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x40, 0x09, 0x01, 0xA1, 0x00,
|
|
|
0x05, 0x09, 0x19, 0x01, 0x29, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
|
|
|
@@ -97,11 +97,10 @@ static Adafruit_USBD_HID hid[NSLOT];
|
|
|
|
|
|
// millis of last Steam OUTPUT/settings write; 0 at boot => lizard until Steam appears
|
|
|
static unsigned long g_steamAliveMs = 0;
|
|
|
-// Fall back to lizard this long after Steam's ~3s settings heartbeat stops. Keep >2x the cadence (7s): the
|
|
|
-// haptic relay is gated by !lizardActive(), so a shorter window lets a single jittered/delayed heartbeat flip
|
|
|
-// lizard mid-session while Steam is still running -> a haptic that arrives in that window gets gated out
|
|
|
-// (dropped/missing haptic). 7s tolerates one missed beat. (This matches main; do not shorten without a lizard
|
|
|
-// trigger that doesn't depend on the heartbeat timeout.)
|
|
|
+// Fall back to lizard this long after Steam's ~3s settings heartbeat stops. Keep >2x the cadence: the haptic
|
|
|
+// relay is gated by !lizardActive(), so a shorter window lets a jittered/delayed heartbeat flip lizard
|
|
|
+// mid-session while Steam is still running -> a haptic arriving in that window gets gated out (dropped). 7s
|
|
|
+// tolerates one missed beat. Do not shorten without a lizard trigger independent of the heartbeat timeout.
|
|
|
#define LIZARD_WD_MS 7000u
|
|
|
static bool g_autoLizard =
|
|
|
true; // master switch; false => Steam mode always forwards 0x45
|
|
|
@@ -123,8 +122,7 @@ static inline void hostStampAlive()
|
|
|
}
|
|
|
// Right after the host resumes from suspend, MUTE input forwarding briefly. Otherwise the controller's input
|
|
|
// in that instant (a trackpad click/trigger, or residual button state from the wake gesture) gets forwarded
|
|
|
-// as a real click/keypress into the just-woken desktop -- which was activating the highlighted Start tile
|
|
|
-// (Edge) and closing the Start menu on every wake. Set when task() sees the suspended->active transition.
|
|
|
+// as a real click/keypress into the just-woken desktop. Set when task() sees the suspended->active transition.
|
|
|
static unsigned long g_resumeMs = 0;
|
|
|
#define POST_RESUME_MUTE_MS 1500u
|
|
|
|
|
|
@@ -133,45 +131,41 @@ static unsigned long g_resumeMs = 0;
|
|
|
static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
uint8_t const *b, uint16_t n)
|
|
|
{
|
|
|
- // Steam OUTPUT reports 0x80-0x89. The haptic/actuator reports (0x80-0x86)
|
|
|
- // are relayed to the controller, and ONLY when they arrive on the CONNECTED slot's interface. We have one
|
|
|
- // controller but expose 4 puck slots, and a report aimed at a DIFFERENT slot made the controller buzz at
|
|
|
- // random -> that is what the slot gate below fixes. (This used to also clamp to 0x82-ONLY, which silently
|
|
|
- // dropped the ping / grip / test haptics: those ride other report IDs such as 0x85/0x86, so they never
|
|
|
- // reached the controller.) The 63-byte settings/config reports 0x87/0x88/0x89 are NOT haptics and are not
|
|
|
- // pushed here -- 0x87 (lizard-off/settings) reaches the controller through the feature-0x01 passthrough path.
|
|
|
+ // Steam OUTPUT reports 0x80-0x89. The haptic/actuator reports (0x80-0x86) are relayed to the controller,
|
|
|
+ // and ONLY when they arrive on the CONNECTED slot's interface: we have one controller but expose 4 puck
|
|
|
+ // slots, and a report aimed at a DIFFERENT slot made the controller buzz at random (the slot gate below
|
|
|
+ // fixes that). Must NOT clamp to 0x82-only: ping/grip/test haptics ride other report IDs (0x85/0x86). The
|
|
|
+ // 63-byte settings/config reports 0x87/0x88/0x89 are NOT haptics and reach the controller via the
|
|
|
+ // feature-0x01 passthrough path instead.
|
|
|
if (type == HID_REPORT_TYPE_OUTPUT) {
|
|
|
if (rid >= 0x80 && rid <= 0x89) {
|
|
|
// capture ALL OUTPUT reports (even un-relayed) for the 'H' dump
|
|
|
hapLogAdd((uint8_t)slot, rid, b, n);
|
|
|
|
|
|
- // ANY Steam OUTPUT report (not just the 0x87 heartbeat) means Steam is present and
|
|
|
- // driving -> leave lizard for gamepad NOW, so a haptic that arrives before the
|
|
|
- // first 0x87 doesn't get relayed while we're still presenting lizard (-> buzz loop).
|
|
|
+ // ANY Steam OUTPUT report (not just the 0x87 heartbeat) means Steam is present and driving ->
|
|
|
+ // leave lizard for gamepad NOW, so a haptic arriving before the first 0x87 isn't relayed while
|
|
|
+ // we're still presenting lizard (-> buzz loop).
|
|
|
hostStampAlive();
|
|
|
}
|
|
|
// Post-resume mute also gates haptics: while onReport45 is muted Steam reads NO 0x45 back, which is the
|
|
|
- // exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop). Same logic
|
|
|
- // as the lizard gate; the mute is short (1.5s), so a dropped wake chime is the cheap end of the trade.
|
|
|
+ // exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
|
|
|
bool muted = g_resumeMs &&
|
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
|
if (rid >= 0x80 && rid <= 0x86 && n >= 1 &&
|
|
|
- hapticRelaySlotOk(slot) && !lizardActive() &&
|
|
|
- !muted) { // wrap as a SET sub-TLV like the report-01 path
|
|
|
+ hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
|
|
|
if (!haptic82Blocked()) {
|
|
|
relayEnqueue(rid, b,
|
|
|
(uint8_t)(n > RELAY_MAXP ?
|
|
|
RELAY_MAXP :
|
|
|
n));
|
|
|
- // Track on/off from what was actually RELAYED (= the controller's believed state), not from every host
|
|
|
- // report: a BLOCKED stop must leave "on" set (the controller may be latched -> reconnect stop-burst),
|
|
|
- // and a blocked ON must not set it (nothing reached the controller -> no spurious burst clicks).
|
|
|
+ // Track on/off from what was actually RELAYED (= the controller's believed state): a
|
|
|
+ // BLOCKED stop must leave "on" set (controller may be latched -> reconnect stop-burst),
|
|
|
+ // a blocked ON must not set it (nothing reached the controller -> no spurious clicks).
|
|
|
if (rid == 0x82)
|
|
|
haptic82HostReport(b, n);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // log so we can see what Steam actually sends (e.g. glide haptics)
|
|
|
if (Serial.availableForWrite() > 80) {
|
|
|
Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
|
|
|
n);
|
|
|
@@ -199,24 +193,21 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
|
|
|
// report 0x01 = raw passthrough -> queue for RF relay to the controller
|
|
|
if (rid == 1 && n >= 2) {
|
|
|
- // capture EVERY relayed feature-1 command for the WebUSB capture view
|
|
|
- // (haptics, LED SET_LED_COLOR, 0x87 settings, 0x9F power-off). The log
|
|
|
- // shows cmd as "rid"; bytes start [cmd][len]... Was gated to 0x80-0x89,
|
|
|
- // which hid exactly the LED/power-off frames we now need to see.
|
|
|
+ // capture EVERY relayed feature-1 command for the WebUSB capture view (haptics, LED SET_LED_COLOR,
|
|
|
+ // 0x87 settings, 0x9F power-off). Log shows cmd as "rid"; bytes start [cmd][len]...
|
|
|
hapLogAdd((uint8_t)slot, cmd, b, n);
|
|
|
bool haptic82 = (cmd == 0x82 && len <= pln);
|
|
|
|
|
|
- // see the OUTPUT path: no haptics while Steam can't read 0x45 back
|
|
|
bool muted = g_resumeMs &&
|
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
|
|
|
|
- // never push haptics to the controller while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
|
|
|
+ // never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop)
|
|
|
bool relayOk = hapticRelaySlotOk(slot) &&
|
|
|
!(haptic82 && (lizardActive() || muted));
|
|
|
if (relayOk && (!haptic82 || !haptic82Blocked())) {
|
|
|
- // Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's multi-register
|
|
|
- // 0x87 settings blocks (LED brightness) and calibration writes are longer than the old 18B cap, and the
|
|
|
- // chopped frames were why those settings never landed on the controller.
|
|
|
+ // Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
|
|
|
+ // multi-register 0x87 settings blocks (LED brightness) and calibration writes exceed the old
|
|
|
+ // 18B cap, and the chopped frames were why those settings never landed on the controller.
|
|
|
uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
|
|
|
if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
|
|
|
Serial.printf(
|
|
|
@@ -229,8 +220,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
haptic82HostReport(pl, len);
|
|
|
}
|
|
|
}
|
|
|
- if (Serial.availableForWrite() >
|
|
|
- 80) { // log host feature writes (non-blocking)
|
|
|
+ if (Serial.availableForWrite() > 80) {
|
|
|
Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
|
|
|
cmd, len);
|
|
|
for (uint16_t i = 0; i < n && i < 14; i++)
|
|
|
@@ -359,10 +349,10 @@ void SteamPuckController::begin()
|
|
|
{
|
|
|
USBDevice.setID(0x28DE, 0x1304);
|
|
|
// Distinct bcdDevice so Windows keys a FRESH usbflags entry (cache is VID:PID:bcdDevice) and actually runs
|
|
|
- // MS OS 2.0 / WinUSB binding for the WebUSB vendor interface -- instead of reusing a stale "no WinUSB" entry
|
|
|
- // tied to the real Steam Controller (28DE:1304), which has no WebUSB interface. The normal (wake-mouse) and
|
|
|
- // one-shot debug (CDC) boots present DIFFERENT interface sets, so they use DIFFERENT bcdDevice values --
|
|
|
- // otherwise Windows would serve one's cached descriptor for the other when you reboot between them.
|
|
|
+ // MS OS 2.0 / WinUSB binding for the WebUSB vendor interface, instead of reusing a stale "no WinUSB" entry
|
|
|
+ // tied to the real Steam Controller (28DE:1304, no WebUSB interface). The normal (wake-mouse) and one-shot
|
|
|
+ // debug (CDC) boots present DIFFERENT interface sets, so they need DIFFERENT bcdDevice values or Windows
|
|
|
+ // serves one's cached descriptor for the other across a reboot.
|
|
|
USBDevice.setDeviceVersion(
|
|
|
g_debugCdcThisBoot ?
|
|
|
0x0212 :
|
|
|
@@ -385,19 +375,18 @@ void SteamPuckController::begin()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Forward the controller's report 0x45 to Steam, or drive lizard kb/mouse when Steam is closed. This is a
|
|
|
-// PURELY USB-SIDE decision -- it changes nothing about the RF poll or the host->controller relay.
|
|
|
+// Forward the controller's report 0x45 to Steam, or drive lizard kb/mouse when Steam is closed. PURELY a
|
|
|
+// USB-side decision -- changes nothing about the RF poll or the host->controller relay.
|
|
|
void SteamPuckController::onReport45(const uint8_t *rep, bool fresh,
|
|
|
uint8_t bodyTlen)
|
|
|
{
|
|
|
- // Host asleep -> forward NOTHING. While suspended, every sendReport attempt (a lizard-mouse report from a
|
|
|
- // trackpad graze, a 0x45 forward) can translate into a host wake -- which made the PC wake on any controller
|
|
|
- // movement and nearly impossible to keep asleep. Waking is an explicit gesture only (Steam-button short press
|
|
|
- // / controller connect), handled in rf_link.cpp via the device-level USB resume signal.
|
|
|
+ // Host asleep -> forward NOTHING. While suspended, every sendReport attempt can translate into a host wake,
|
|
|
+ // making the PC wake on any controller movement. Waking is an explicit gesture only (Steam-button short
|
|
|
+ // press / controller connect), handled in rf_link.cpp via the device-level USB resume signal.
|
|
|
if (USBDevice.suspended())
|
|
|
return;
|
|
|
- // Just woke? Hold off forwarding for a beat so the wake gesture's residual controller input doesn't click /
|
|
|
- // type into the freshly-woken desktop (it was launching Edge from the Start menu on every wake).
|
|
|
+ // Just woke? Hold off forwarding so the wake gesture's residual controller input doesn't click/type into
|
|
|
+ // the freshly-woken desktop.
|
|
|
if (g_resumeMs && millis() - g_resumeMs < POST_RESUME_MUTE_MS)
|
|
|
return;
|
|
|
if (g_connSlot < 0 || g_connSlot >= NSLOT)
|
|
|
@@ -408,9 +397,9 @@ void SteamPuckController::onReport45(const uint8_t *rep, bool fresh,
|
|
|
uint8_t blen = bodyTlen - 1;
|
|
|
if (blen > 45)
|
|
|
blen = 45; // body after the 0x45 id byte
|
|
|
- // forward the puck's raw pad coords untouched -- Steam does its own interpolation/smoothing. Forward only
|
|
|
- // FRESH reports (the real puck dedupes -> Steam gets a clean unique stream; sending stale repeats makes
|
|
|
- // Steam's velocity/smoothing stair-step). g_fwdNewOnly toggles for A/B.
|
|
|
+ // forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
|
|
|
+ // only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
|
|
|
+ // stair-step. g_fwdNewOnly toggles for A/B.
|
|
|
if ((fresh || !g_fwdNewOnly) && g_slot[g_connSlot].used &&
|
|
|
hid[g_connSlot].ready())
|
|
|
hid[g_connSlot].sendReport(
|
|
|
@@ -420,9 +409,8 @@ void SteamPuckController::onReport45(const uint8_t *rep, bool fresh,
|
|
|
}
|
|
|
|
|
|
// Forward the controller's NON-input status reports (0x43 power/battery, 0x44) to Steam verbatim -- the real
|
|
|
-// puck does this and it's how Steam reads battery. OpenPuck used to drop everything but 0x45, so Steam never got
|
|
|
-// the power report (-> default/unknown battery). Same host-asleep / post-resume gating as 0x45; no lizard path
|
|
|
-// (status reports aren't input, so they forward regardless of the lizard decision).
|
|
|
+// puck does this and it's how Steam reads battery. Same host-asleep / post-resume gating as 0x45; no lizard
|
|
|
+// path (status reports aren't input, so they forward regardless of the lizard decision).
|
|
|
void SteamPuckController::onAuxReport(uint8_t rid, const uint8_t *data,
|
|
|
uint8_t n)
|
|
|
{
|
|
|
@@ -436,13 +424,11 @@ void SteamPuckController::onAuxReport(uint8_t rid, const uint8_t *data,
|
|
|
hid[g_connSlot].sendReport(rid, data, n);
|
|
|
}
|
|
|
|
|
|
-// ---- wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they
|
|
|
-// only wake when actual mouse/keyboard input follows. So on a deliberate wake gesture we play a HARMLESS mouse
|
|
|
-// JIGGLE (move a few px right, then back -- NET ZERO cursor, NO button): real mouse activity wakes the host,
|
|
|
-// but it clicks nothing and doesn't close/activate anything (an open Start menu stays open). Earlier versions
|
|
|
-// also sent a click / space / Ctrl; those either activated the focused item or weren't needed. Queued by
|
|
|
-// wakeEvent() (rf_link, on a Steam short press / controller connect while suspended); reports can't cross a
|
|
|
-// suspended bus, so it's delivered once the bus has resumed.
|
|
|
+// wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they only
|
|
|
+// wake when actual mouse/keyboard input follows. So on a deliberate wake gesture we play a HARMLESS mouse
|
|
|
+// JIGGLE (move a few px right, then back -- NET ZERO cursor, NO button): real mouse activity wakes the host
|
|
|
+// but clicks/activates nothing (an open Start menu stays open). Queued by wakeEvent() (rf_link, on a Steam
|
|
|
+// short press / controller connect while suspended); delivered once the suspended bus has resumed.
|
|
|
static uint8_t g_nudgeStep = 0; // 0=idle; 1=jiggle+, 2=jiggle-
|
|
|
static unsigned long g_nudgeMs = 0;
|
|
|
#define NUDGE_JIGGLE_PX 10
|
|
|
@@ -502,10 +488,10 @@ void SteamPuckController::task()
|
|
|
bool conn = (g_connSlot >= 0 && millis() - g_connReplyMs < 300);
|
|
|
if (g_connSlot >= 0 && g_connSlot < NSLOT && hid[g_connSlot].ready()) {
|
|
|
// 0x79 connection state: on edge, then repeated every 750ms ONLY until Steam reacts (its first OUTPUT/
|
|
|
- // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; the old
|
|
|
- // unconditional forever-resend could re-trigger Steam's connect handling (connect chime) every 750ms
|
|
|
- // before Steam started consuming 0x45 -> the connect-time loop of haptic buzzes that only stopped on the
|
|
|
- // first real input (the Steam button). Resending until acked still covers "Steam missed the edge".
|
|
|
+ // settings write after the edge -- g_steamAliveMs). The real puck sends 0x79 ONCE, edge-triggered; an
|
|
|
+ // unconditional forever-resend re-triggers Steam's connect handling (connect chime) every 750ms before
|
|
|
+ // Steam consumes 0x45 -> a loop of connect-time haptic buzzes. Resending until acked still covers
|
|
|
+ // "Steam missed the edge".
|
|
|
bool steamAcked = g_steamAliveMs &&
|
|
|
(int32_t)(g_steamAliveMs - connEdgeMs) >= 0;
|
|
|
if (conn != usbConn ||
|
|
|
@@ -517,16 +503,15 @@ void SteamPuckController::task()
|
|
|
usbConn = conn;
|
|
|
last79 = millis();
|
|
|
} else if (conn && millis() - last7B >= 2000) {
|
|
|
- // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed dBm
|
|
|
- // (capture showed 0xDD = -35; replaying it verbatim is why Steam pinned -35dBm forever) -- patch in the
|
|
|
- // smoothed RSSI the radio now samples on each controller reply (rf_link). 0 = no sample yet -> keep the
|
|
|
- // capture value rather than report garbage.
|
|
|
+ // 0x7B status, live-captured template. Byte 8 is the controller->puck signal strength as signed
|
|
|
+ // dBm (capture showed 0xDD = -35) -- patch in the smoothed RSSI the radio samples on each
|
|
|
+ // controller reply (rf_link). 0 = no sample yet -> keep the capture value rather than garbage.
|
|
|
//
|
|
|
- // CALIBRATION: our raw RSSISAMPLE reads ~RSSI_DBM_OFFSET dB lower than the real Valve puck at the same
|
|
|
- // distance (the Pro Micro's PCB-trace antenna vs Valve's tuned front-end). The 2Mbit ESB link has ~55dB
|
|
|
- // of margin, so a -75dBm reading still works across a house -- but Steam's bar maps raw dBm to "weak"
|
|
|
- // long before that. Adding the offset lines our close-range value up with the puck's captured -35 so the
|
|
|
- // bar tracks usable range instead of antenna gain. Clamp keeps it in a sane window (no wrap/garbage).
|
|
|
+ // CALIBRATION: our raw RSSISAMPLE reads ~RSSI_DBM_OFFSET dB lower than the real Valve puck at the
|
|
|
+ // same distance (Pro Micro PCB-trace antenna vs Valve's tuned front-end). The 2Mbit ESB link has
|
|
|
+ // ~55dB margin, so a -75dBm reading still works across a house, but Steam's bar maps raw dBm to
|
|
|
+ // "weak" long before that. The offset lines our close-range value up with the puck's captured -35
|
|
|
+ // so the bar tracks usable range, not antenna gain. Clamp keeps it in a sane window.
|
|
|
uint8_t s7b[12] = { 0xF7, 0x01, 0x89, 0x00, 0x00, 0x00,
|
|
|
0x03, 0x00, 0xDD, 0x00, 0x3A, 0x02 };
|
|
|
if (g_linkRssi) {
|