|
@@ -3,10 +3,12 @@
|
|
|
#include "config.h"
|
|
#include "config.h"
|
|
|
#include "identity.h"
|
|
#include "identity.h"
|
|
|
#include "haptics.h"
|
|
#include "haptics.h"
|
|
|
|
|
+#include "fault_diag.h"
|
|
|
#include "rf_link.h"
|
|
#include "rf_link.h"
|
|
|
#include "triton.h"
|
|
#include "triton.h"
|
|
|
#include "mode_lizard.h"
|
|
#include "mode_lizard.h"
|
|
|
#include "wake_hid.h"
|
|
#include "wake_hid.h"
|
|
|
|
|
+#include "usb_tx.h"
|
|
|
#include <Adafruit_TinyUSB.h>
|
|
#include <Adafruit_TinyUSB.h>
|
|
|
#include <Arduino.h>
|
|
#include <Arduino.h>
|
|
|
#include <string.h>
|
|
#include <string.h>
|
|
@@ -91,6 +93,73 @@ static const uint8_t PUCK_LIZARD_HID_DESC[] = {
|
|
|
|
|
|
|
|
static Adafruit_USBD_HID hid[NSLOT];
|
|
static Adafruit_USBD_HID hid[NSLOT];
|
|
|
|
|
|
|
|
|
|
+// Drop Steam's relayed 0x81 CLEAR_DIGITAL_MAPPINGS in Steam mode (console "S81" toggles; on by default). It's
|
|
|
|
|
+// the confirmed amp-clicker in Steam's per-connect config and OpenPuck doesn't need it (own input translation
|
|
|
|
|
+// + id9=0 keepalive). Reversible from the console if it regresses.
|
|
|
|
|
+bool g_drop81 = true;
|
|
|
|
|
+
|
|
|
|
|
+// Per-slot shadow of the controller's SET_SETTINGS_VALUES array (id-indexed u16, ids 0..0x52). Steam writes
|
|
|
|
|
+// it with 0x87 and READS IT BACK with 0x89 to verify its config "took"; on the real puck the controller
|
|
|
|
|
+// answers 0x89 from this array. OpenPuck can't relay-and-return over the NO-ACK RF link, so it maintains the
|
|
|
|
|
+// shadow here and answers 0x89 from it -- WITHOUT this, Steam's verify never matches and it re-clears (0x81)
|
|
|
|
|
+// + re-writes (0x87) forever = the connect config storm that buzzes the amp and floods the usbd task.
|
|
|
|
|
+static uint16_t g_setShadow[NSLOT][0x53];
|
|
|
|
|
+
|
|
|
|
|
+// ---- feature-command capture (diagnostic) ---------------------------------------------------------------
|
|
|
|
|
+// Log the USB feature command channel (Steam's SET/GET) to serial to see the connect handshake -- WITHOUT a
|
|
|
|
|
+// Serial.printf on the fragile 800B usbd task (that path can blow the stack -> LOCKUP; it's why production
|
|
|
|
|
+// avoids printf here). handleSet/handleGet (usbd task) push a compact record under PRIMASK; puckCmdLogDrain()
|
|
|
|
|
+// prints them from loop() context. When g_cmdCapture is on, the high-rate I45 input stream is also suppressed
|
|
|
|
|
+// (rf_link) so the command sequence is readable. Toggle from the console with "FC".
|
|
|
|
|
+bool g_cmdCapture = true;
|
|
|
|
|
+struct FCmdRec {
|
|
|
|
|
+ uint8_t dir; // 0 = SET (host->puck write), 1 = GET (host read)
|
|
|
|
|
+ uint8_t iface; // HID interface index = bond slot the command hit
|
|
|
|
|
+ uint8_t rid, cmd, len, b[10];
|
|
|
|
|
+};
|
|
|
|
|
+static FCmdRec g_fc[32];
|
|
|
|
|
+static volatile uint8_t g_fcHead, g_fcTail;
|
|
|
|
|
+static void fcPush(uint8_t dir, int iface, uint8_t rid, uint8_t cmd,
|
|
|
|
|
+ uint8_t len, const uint8_t *b, uint8_t n)
|
|
|
|
|
+{
|
|
|
|
|
+ if (!g_cmdCapture)
|
|
|
|
|
+ return;
|
|
|
|
|
+ uint32_t pm = __get_PRIMASK();
|
|
|
|
|
+ __disable_irq();
|
|
|
|
|
+ uint8_t h = g_fcHead, nx = (uint8_t)((h + 1) & 31);
|
|
|
|
|
+ if (nx !=
|
|
|
|
|
+ g_fcTail) { // drop-newest when full (loop drains fast); never block the usbd task
|
|
|
|
|
+ g_fc[h].dir = dir;
|
|
|
|
|
+ g_fc[h].iface = (uint8_t)iface;
|
|
|
|
|
+ g_fc[h].rid = rid;
|
|
|
|
|
+ g_fc[h].cmd = cmd;
|
|
|
|
|
+ g_fc[h].len = len;
|
|
|
|
|
+ for (uint8_t i = 0; i < 10; i++)
|
|
|
|
|
+ g_fc[h].b[i] = (i < n) ? b[i] : 0;
|
|
|
|
|
+ g_fcHead = nx;
|
|
|
|
|
+ }
|
|
|
|
|
+ __set_PRIMASK(pm);
|
|
|
|
|
+}
|
|
|
|
|
+void puckCmdLogDrain(void)
|
|
|
|
|
+{
|
|
|
|
|
+ // boosted: runs at Steam's feature-storm rate and CDC flush enters the same TinyUSB DMA claim window
|
|
|
|
|
+ // as HID sends (the issue-72 livelock; see usb_tx.cpp)
|
|
|
|
|
+ usbTxBoost();
|
|
|
|
|
+ while (g_fcHead != g_fcTail) {
|
|
|
|
|
+ if (Serial.availableForWrite() < 70)
|
|
|
|
|
+ break; // don't stall loop on CDC backpressure; resume next iteration
|
|
|
|
|
+ FCmdRec r = g_fc[g_fcTail];
|
|
|
|
|
+ g_fcTail = (uint8_t)((g_fcTail + 1) & 31);
|
|
|
|
|
+ Serial.printf("# FC %s if=%u rid=%u cmd=%02X len=%u:",
|
|
|
|
|
+ r.dir ? "GET" : "SET", r.iface, r.rid, r.cmd,
|
|
|
|
|
+ r.len);
|
|
|
|
|
+ for (uint8_t i = 0; i < 10; i++)
|
|
|
|
|
+ Serial.printf(" %02X", r.b[i]);
|
|
|
|
|
+ Serial.println();
|
|
|
|
|
+ }
|
|
|
|
|
+ usbTxUnboost();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ===================== seamless LIZARD decision =====================
|
|
// ===================== seamless LIZARD decision =====================
|
|
|
// Steam, while running, re-sends settings report 0x87 (lizard-off) every ~3s as a heartbeat (captured on HW),
|
|
// Steam, while running, re-sends settings report 0x87 (lizard-off) every ~3s as a heartbeat (captured on HW),
|
|
|
// and ANY OUTPUT report likewise stamps g_steamAliveMs. When the heartbeat stops we fall back to lizard, so the
|
|
// and ANY OUTPUT report likewise stamps g_steamAliveMs. When the heartbeat stops we fall back to lizard, so the
|
|
@@ -117,6 +186,11 @@ static inline bool lizardActive()
|
|
|
(g_usbMode == MODE_LIZARD ||
|
|
(g_usbMode == MODE_LIZARD ||
|
|
|
(g_autoLizard && !steamDrivingGamepad()));
|
|
(g_autoLizard && !steamDrivingGamepad()));
|
|
|
}
|
|
}
|
|
|
|
|
+// Public accessor for the haptic layer (haptics.cpp gates the lizard-suppression keepalive on this).
|
|
|
|
|
+bool puckLizardActive()
|
|
|
|
|
+{
|
|
|
|
|
+ return lizardActive();
|
|
|
|
|
+}
|
|
|
static inline void hostStampAlive()
|
|
static inline void hostStampAlive()
|
|
|
{
|
|
{
|
|
|
g_steamAliveMs = millis();
|
|
g_steamAliveMs = millis();
|
|
@@ -152,7 +226,12 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
// exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
|
|
// exact condition under which Steam loops the same haptic command (-> connect/wake buzz loop).
|
|
|
bool muted = g_resumeMs &&
|
|
bool muted = g_resumeMs &&
|
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
|
- if (rid >= 0x80 && rid <= 0x86 && n >= 1 &&
|
|
|
|
|
|
|
+ // Drop the OUTPUT-report form of 0x81 too (Steam's haptic-reset action, a 7-byte report id 0x81 with
|
|
|
|
|
+ // payloads 00../01..). Same amp-clicker as the feature-0x01 0x81; g_drop81 only covered the feature
|
|
|
|
|
+ // path, so this OUTPUT form was still leaking through and clicking. OpenPuck doesn't need it.
|
|
|
|
|
+ bool dropOut81 =
|
|
|
|
|
+ (g_drop81 && g_usbMode == MODE_STEAM && rid == 0x81);
|
|
|
|
|
+ if (rid >= 0x80 && rid <= 0x86 && !dropOut81 && n >= 1 &&
|
|
|
hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
|
|
hapticRelaySlotOk(slot) && !lizardActive() && !muted) {
|
|
|
if (!haptic82Blocked(slot)) {
|
|
if (!haptic82Blocked(slot)) {
|
|
|
relayEnqueue(rid, b,
|
|
relayEnqueue(rid, b,
|
|
@@ -168,6 +247,12 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+#if OPK_LOG
|
|
|
|
|
+ // Per-report Serial echo: gated to the diagnostic build only. handleSet runs on the TinyUSB device
|
|
|
|
|
+ // task, whose FreeRTOS stack is just 800 bytes; a Serial.printf there costs 200-500B and, stacked
|
|
|
|
|
+ // under TinyUSB's control-transfer frames + a preempting USB ISR, can blow the stack -> the exact
|
|
|
|
|
+ // SP-corruption-during-exception-entry that shows up as RR_LOCKUP. Production keeps this path
|
|
|
|
|
+ // printf-free; the same data is already in the OPK_LOG capture ring (hapLogAdd) for the WebUSB panel.
|
|
|
if (Serial.availableForWrite() > 80) {
|
|
if (Serial.availableForWrite() > 80) {
|
|
|
Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
|
|
Serial.printf("# OUT if%d rid=%02X n=%u:", slot, rid,
|
|
|
n);
|
|
n);
|
|
@@ -175,6 +260,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
Serial.printf(" %02X", b[i]);
|
|
Serial.printf(" %02X", b[i]);
|
|
|
Serial.println();
|
|
Serial.println();
|
|
|
}
|
|
}
|
|
|
|
|
+#endif
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (type != HID_REPORT_TYPE_FEATURE || n < 1)
|
|
if (type != HID_REPORT_TYPE_FEATURE || n < 1)
|
|
@@ -187,15 +273,21 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
// Capture EVERY feature SET (the whole cmd channel: 0x83 attr, 0xAE strings, 0xB4 conn, 0xA2/A3 bond, the
|
|
// Capture EVERY feature SET (the whole cmd channel: 0x83 attr, 0xAE strings, 0xB4 conn, 0xA2/A3 bond, the
|
|
|
// feature-1 relay, AND any host battery query) to the WebUSB ring so the panel's capture view shows it.
|
|
// feature-1 relay, AND any host battery query) to the WebUSB ring so the panel's capture view shows it.
|
|
|
hapLogAdd((uint8_t)slot, cmd, b, n);
|
|
hapLogAdd((uint8_t)slot, cmd, b, n);
|
|
|
|
|
+ // ...and to the serial feature-command capture (diagnostic; drained in loop, see puckCmdLogDrain).
|
|
|
|
|
+ fcPush(0, slot, rid, cmd, len, pl, pln > 10 ? 10 : (uint8_t)pln);
|
|
|
|
|
+ // ...and to the flight recorder (survives a watchdog reset). This runs ON the fragile usbd task, so the
|
|
|
|
|
+ // command flood that (theory) overflows it lands in the post-mortem trail rather than being lost with USB.
|
|
|
|
|
+ faultDiagTrace(FR_SET, (uint16_t)((rid << 8) | cmd));
|
|
|
|
|
|
|
|
// settings/haptic/LED report (incl. 0x87 lizard-off heartbeat, SDL Triton lizard-disable)
|
|
// settings/haptic/LED report (incl. 0x87 lizard-off heartbeat, SDL Triton lizard-disable)
|
|
|
if (cmd >= 0x80 && cmd <= 0x89)
|
|
if (cmd >= 0x80 && cmd <= 0x89)
|
|
|
hostStampAlive();
|
|
hostStampAlive();
|
|
|
// Controller power-off: Steam's "turn off controller" is feature-0x01 frame 9F 04 6F 66 66 21 ("off!"),
|
|
// Controller power-off: Steam's "turn off controller" is feature-0x01 frame 9F 04 6F 66 66 21 ("off!"),
|
|
|
- // confirmed from a real puck capture. The feature-0x01 relay below forwards it once; hapticSendShutdown()
|
|
|
|
|
- // bursts it for NO-ACK reliability (the single hook the test button + host-suspend also drive).
|
|
|
|
|
|
|
+ // confirmed from a real puck capture. The feature-0x01 relay below forwards it once; hapticSendShutdown
|
|
|
|
|
+ // bursts it for NO-ACK reliability. Slot-targeted: the command arrived on THIS controller's interface,
|
|
|
|
|
+ // so only this controller powers off (broadcasting killed all connected controllers at once).
|
|
|
if (rid == 1 && cmd == 0x9F)
|
|
if (rid == 1 && cmd == 0x9F)
|
|
|
- hapticSendShutdown();
|
|
|
|
|
|
|
+ hapticSendShutdown((uint8_t)slot);
|
|
|
|
|
|
|
|
// report 0x01 = raw passthrough -> queue for RF relay to the controller
|
|
// report 0x01 = raw passthrough -> queue for RF relay to the controller
|
|
|
if (rid == 1 && n >= 2) {
|
|
if (rid == 1 && n >= 2) {
|
|
@@ -206,18 +298,38 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
bool muted = g_resumeMs &&
|
|
bool muted = g_resumeMs &&
|
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
millis() - g_resumeMs < POST_RESUME_MUTE_MS;
|
|
|
|
|
|
|
|
|
|
+ // Steam-mode: DROP the relayed 0x81 CLEAR_DIGITAL_MAPPINGS (g_drop81, console "S81"). It EXECUTES on
|
|
|
|
|
+ // the controller (rid<0x87, legacy framing) and each one re-arms the haptic amp (non-idempotent, ibex
|
|
|
|
|
+ // func_0x0001bbf0) = the connect click/buzz; Steam sends ~13 in its per-connect config. OpenPuck does
|
|
|
|
|
+ // its OWN input translation and holds mappings cleared via the id9=0 keepalive, so it does NOT need the
|
|
|
|
|
+ // controller's mapping engine -> Steam's 0x81 is pure downside here. The manual "Clear stuck buzz"
|
|
|
|
|
+ // (hapticReinit) sends its own 0x81 via relayEnqueue directly, so that cure path is unaffected.
|
|
|
|
|
+ bool drop =
|
|
|
|
|
+ (g_drop81 && g_usbMode == MODE_STEAM && cmd == 0x81);
|
|
|
|
|
+ // Do NOT relay commands OpenPuck answers LOCALLY (identity/bond/settings READS). Relaying them to the
|
|
|
|
|
+ // controller is pointless (their reply can't come back over the NO-ACK RF link -- we answer from the
|
|
|
|
|
+ // handleSet/handleGet switch), and 0x83 GET_ATTRIBUTES in particular is < 0x87 so it EXECUTES on the
|
|
|
|
|
+ // controller -- captured as the remaining periodic CLICK, fired every time Steam re-polls identity
|
|
|
|
|
+ // (0x83/0xAE on all 4 interfaces). Only actuator/config commands (0x80-0x82/0x84-0x88 haptics+config,
|
|
|
|
|
+ // 0x9F power) should reach the controller.
|
|
|
|
|
+ bool localAnswer = (cmd == 0x83 || cmd == 0x89 || cmd == 0xAE ||
|
|
|
|
|
+ cmd == 0xA2 || cmd == 0xA3 || cmd == 0xAD ||
|
|
|
|
|
+ cmd == 0xB4 || cmd == 0xED || cmd == 0xA4);
|
|
|
// never push haptics 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) &&
|
|
|
|
|
|
|
+ bool relayOk = hapticRelaySlotOk(slot) && !drop &&
|
|
|
|
|
+ !localAnswer &&
|
|
|
!(haptic82 && (lizardActive() || muted));
|
|
!(haptic82 && (lizardActive() || muted));
|
|
|
if (relayOk && (!haptic82 || !haptic82Blocked(slot))) {
|
|
if (relayOk && (!haptic82 || !haptic82Blocked(slot))) {
|
|
|
// Relay the DECLARED length (up to the 60B RF frame ceiling), not a truncation: Steam's
|
|
// 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
|
|
// 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.
|
|
// 18B cap, and the chopped frames were why those settings never landed on the controller.
|
|
|
uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
|
|
uint8_t rl = (len <= pln) ? len : (uint8_t)pln;
|
|
|
|
|
+#if OPK_LOG
|
|
|
if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
|
|
if (len > RELAY_MAXP && Serial.availableForWrite() > 60)
|
|
|
Serial.printf(
|
|
Serial.printf(
|
|
|
"# RELAY TRUNC cmd=%02X len=%u>%u\n",
|
|
"# RELAY TRUNC cmd=%02X len=%u>%u\n",
|
|
|
cmd, len, (unsigned)RELAY_MAXP);
|
|
cmd, len, (unsigned)RELAY_MAXP);
|
|
|
|
|
+#endif
|
|
|
relayEnqueue(cmd, pl, rl, (uint8_t)slot);
|
|
relayEnqueue(cmd, pl, rl, (uint8_t)slot);
|
|
|
|
|
|
|
|
// track from RELAYED frames only (see the OUTPUT path)
|
|
// track from RELAYED frames only (see the OUTPUT path)
|
|
@@ -225,6 +337,9 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
haptic82HostReport(pl, len);
|
|
haptic82HostReport(pl, len);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+#if OPK_LOG
|
|
|
|
|
+ // Diagnostic-build only -- see the OUTPUT-path note: no Serial.printf on the 800B usbd-task stack in
|
|
|
|
|
+ // production (LOCKUP mitigation). The feature SET is already captured to the OPK_LOG ring above.
|
|
|
if (Serial.availableForWrite() > 80) {
|
|
if (Serial.availableForWrite() > 80) {
|
|
|
Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
|
|
Serial.printf("# SET if%d rid=%02X cmd=%02X len=%u:", slot, rid,
|
|
|
cmd, len);
|
|
cmd, len);
|
|
@@ -232,6 +347,7 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
Serial.printf(" %02X", b[i]);
|
|
Serial.printf(" %02X", b[i]);
|
|
|
Serial.println();
|
|
Serial.println();
|
|
|
}
|
|
}
|
|
|
|
|
+#endif
|
|
|
memset(S.resp, 0, sizeof S.resp);
|
|
memset(S.resp, 0, sizeof S.resp);
|
|
|
S.resp_len = 0;
|
|
S.resp_len = 0;
|
|
|
switch (cmd) {
|
|
switch (cmd) {
|
|
@@ -239,18 +355,48 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
S.resp[0] = 0x83;
|
|
S.resp[0] = 0x83;
|
|
|
S.resp[1] = sizeof ATTR83;
|
|
S.resp[1] = sizeof ATTR83;
|
|
|
memcpy(S.resp + 2, ATTR83, sizeof ATTR83);
|
|
memcpy(S.resp + 2, ATTR83, sizeof ATTR83);
|
|
|
|
|
+ // Report-id 1 = an attributes query RELAYED to the bonded CONTROLLER; it must report the CONTROLLER's
|
|
|
|
|
+ // product id (0x1302), NOT the puck/dongle's (0x1304) -- else Steam sees "a controller with a dongle's
|
|
|
|
|
+ // id" (HANDOFF.md) and drops to the LEGACY 0x81 CLEAR_DIGITAL_MAPPINGS init path, whose rapid re-arm
|
|
|
|
|
+ // storm is the connect buzz; with the correct id Steam uses the modern quiet 0x87 id9=0 path. Report-id
|
|
|
|
|
+ // 2 stays the puck (0x1304). ATTR83 stores product as u32 LE at offset 1, so only the low byte flips
|
|
|
|
|
+ // (0x04 -> 0x02); everything else in the blob is identical.
|
|
|
|
|
+ if (rid == 1)
|
|
|
|
|
+ S.resp[2 + 1] =
|
|
|
|
|
+ 0x02; // product 0x1304 -> 0x1302 (controller)
|
|
|
S.resp_len = 63;
|
|
S.resp_len = 63;
|
|
|
break;
|
|
break;
|
|
|
case 0xAE: {
|
|
case 0xAE: {
|
|
|
uint8_t idx = pln > 0 ? pl[0] : 1;
|
|
uint8_t idx = pln > 0 ? pl[0] : 1;
|
|
|
- const char *s = (idx == 0) ? g_board :
|
|
|
|
|
- (idx == 1) ? g_unit :
|
|
|
|
|
- "NA";
|
|
|
|
|
|
|
+ // Report-id 1 = string attributes of the bonded CONTROLLER, not the puck. Steam matches the connected
|
|
|
|
|
+ // controller to its bond by SERIAL: it reads the controller's serial here (rid 1) and compares to the
|
|
|
|
|
+ // 16-byte serial in the bond record it reads via 0xA3. OpenPuck was returning the PUCK's serial (g_unit,
|
|
|
|
|
+ // "FXB..."), which never matches any bond's controller serial ("FXA...") -> Steam can't associate the
|
|
|
|
|
+ // controller with a puck -> "paired to" list is EMPTY -> Steam treats it as unconfigured and re-runs the
|
|
|
|
|
+ // 0x81/0x87 config storm every connect (the buzz). So on rid 1, answer with the CONTROLLER's serial from
|
|
|
|
|
+ // this interface's bond record (rec[8..24], the same 16-byte serial 0xA3 returns). Captured: Steam
|
|
|
|
|
+ // hammered this read (AE x39 on rid1) exactly because the identity never matched. rid 2 = puck (unchanged).
|
|
|
|
|
+ // idx 0/1/4 = board/unit/alt serial (real controller returns the SAME serial for 0 and 4; the clone
|
|
|
|
|
+ // was returning "NA" for idx 4, which failed Steam's read -> retry).
|
|
|
S.resp[0] = 0xAE;
|
|
S.resp[0] = 0xAE;
|
|
|
S.resp[1] = 0x14;
|
|
S.resp[1] = 0x14;
|
|
|
S.resp[2] = idx;
|
|
S.resp[2] = idx;
|
|
|
memset(S.resp + 3, 0, 60);
|
|
memset(S.resp + 3, 0, 60);
|
|
|
- memcpy(S.resp + 3, s, strlen(s));
|
|
|
|
|
|
|
+ if (rid == 1 && slot >= 0 && slot < NSLOT &&
|
|
|
|
|
+ g_slot[slot].used && (idx == 0 || idx == 1 || idx == 4)) {
|
|
|
|
|
+ // THIS slot's paired-controller serial, straight from its bond record (rec[8..24], the same
|
|
|
|
|
+ // 16-byte serial 0xA3 returns) -- per device, nothing hardcoded. Copied without a stack temp
|
|
|
|
|
+ // (this runs on the fragile 800B usbd task; every byte off the stack helps under a Steam
|
|
|
|
|
+ // re-enumeration burst).
|
|
|
|
|
+ memcpy(S.resp + 3, g_slot[slot].rec + 8, 16);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // rid 2 = the puck's own board/unit serials (device-derived). Any other idx -> "NA".
|
|
|
|
|
+ const char *s = (rid == 1) ? "NA" :
|
|
|
|
|
+ (idx == 0 || idx == 4) ? g_board :
|
|
|
|
|
+ (idx == 1) ? g_unit :
|
|
|
|
|
+ "NA";
|
|
|
|
|
+ memcpy(S.resp + 3, s, strlen(s));
|
|
|
|
|
+ }
|
|
|
S.resp_len = 63;
|
|
S.resp_len = 63;
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
@@ -270,7 +416,9 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
break;
|
|
break;
|
|
|
case 0xAD:
|
|
case 0xAD:
|
|
|
g_pairing = (pln > 0 && pl[0] != 0);
|
|
g_pairing = (pln > 0 && pl[0] != 0);
|
|
|
|
|
+#if OPK_LOG
|
|
|
Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
|
|
Serial.printf("# pairing %s\n", g_pairing ? "ON" : "off");
|
|
|
|
|
+#endif
|
|
|
S.resp[0] = 0xAD;
|
|
S.resp[0] = 0xAD;
|
|
|
S.resp[1] = 0;
|
|
S.resp[1] = 0;
|
|
|
S.resp_len = 63;
|
|
S.resp_len = 63;
|
|
@@ -285,8 +433,10 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
S.used = true;
|
|
S.used = true;
|
|
|
}
|
|
}
|
|
|
g_dirty = true;
|
|
g_dirty = true;
|
|
|
|
|
+#if OPK_LOG
|
|
|
Serial.printf("# slot %d %s\n", slot,
|
|
Serial.printf("# slot %d %s\n", slot,
|
|
|
recEmpty(pl) ? "cleared" : "bonded");
|
|
recEmpty(pl) ? "cleared" : "bonded");
|
|
|
|
|
+#endif
|
|
|
}
|
|
}
|
|
|
S.resp[0] = 0xA2;
|
|
S.resp[0] = 0xA2;
|
|
|
S.resp[1] = 0;
|
|
S.resp[1] = 0;
|
|
@@ -300,6 +450,86 @@ static void handleSet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
memcpy(S.resp + 2, S.rec, 24);
|
|
memcpy(S.resp + 2, S.rec, 24);
|
|
|
S.resp_len = 63;
|
|
S.resp_len = 63;
|
|
|
break;
|
|
break;
|
|
|
|
|
+ case 0x87:
|
|
|
|
|
+ // SET_SETTINGS_VALUES: payload = N x [id][val16 LE]. Shadow each into g_setShadow[slot] so the 0x89
|
|
|
|
|
+ // read-back below matches what Steam wrote -- this is THE fix that stops Steam's endless config-verify
|
|
|
|
|
+ // retry (the 0x81/0x87 storm that buzzes the amp). (Still relayed to the controller by the feature-1
|
|
|
|
|
+ // path above; this just maintains the dongle-side shadow the real controller would return on 0x89.)
|
|
|
|
|
+ // ACK with [0x87][0], not the default payload echo (a clean success reply, like the real dongle).
|
|
|
|
|
+ if (slot >= 0 && slot < NSLOT)
|
|
|
|
|
+ for (uint16_t i = 0; i + 2 < pln; i += 3) {
|
|
|
|
|
+ uint8_t id = pl[i];
|
|
|
|
|
+ if (id < 0x53)
|
|
|
|
|
+ g_setShadow[slot][id] =
|
|
|
|
|
+ (uint16_t)(pl[i + 1] |
|
|
|
|
|
+ (pl[i + 2] << 8));
|
|
|
|
|
+ }
|
|
|
|
|
+ S.resp[0] = 0x87;
|
|
|
|
|
+ S.resp[1] = 0;
|
|
|
|
|
+ S.resp_len = 63;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 0x89: {
|
|
|
|
|
+ // GET_SETTINGS_VALUES: Steam reads back setting `id` (payload[0]) from the id-indexed array. Answer
|
|
|
|
|
+ // from the shadow 0x87 populated: [0x89][3][id][val16 LE]. (Steam didn't use 0x89 in the captured
|
|
|
|
|
+ // session -- it provisions via 0xED below -- but this is the correct real-dongle behavior; harmless.)
|
|
|
|
|
+ uint8_t id = (pln > 0) ? pl[0] : 0;
|
|
|
|
|
+ uint16_t v = (slot >= 0 && slot < NSLOT && id < 0x53) ?
|
|
|
|
|
+ g_setShadow[slot][id] :
|
|
|
|
|
+ 0;
|
|
|
|
|
+ S.resp[0] = 0x89;
|
|
|
|
|
+ S.resp[1] = 3;
|
|
|
|
|
+ S.resp[2] = id;
|
|
|
|
|
+ S.resp[3] = (uint8_t)v;
|
|
|
|
|
+ S.resp[4] = (uint8_t)(v >> 8);
|
|
|
|
|
+ S.resp_len = 63;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ case 0xED: {
|
|
|
|
|
+ // GET-SETTING-BY-PATH (keyed). Steam SETs the path string, then GETs the value; the REAL controller
|
|
|
|
|
+ // returns the setting's value, but OpenPuck was ECHOING the path -> Steam sees the controller as
|
|
|
|
|
+ // un-provisioned/un-bonded and re-runs its full LEGACY config every connect (0x81 CLEAR_DIGITAL_MAPPINGS
|
|
|
|
|
+ // = the amp clicks/buzz; captured: 0xED reads of "esb/bond"/"user/wireless_transport" that got echoed).
|
|
|
|
|
+ // Answer the paths Steam checks at connect so it treats the controller as provisioned + bonded:
|
|
|
|
|
+ // esb/bond -> this slot's 24-byte bond record (the controller's record of ITS puck)
|
|
|
|
|
+ // esb/bond_2 -> a 2nd-puck bond; OpenPuck bonds one puck per slot -> absent (empty)
|
|
|
|
|
+ // user/wireless_transport -> 1 byte = the ACTIVE (connected) slot's transport code (see below)
|
|
|
|
|
+ // Anything else -> empty ([0xED][0]) rather than a garbage path echo.
|
|
|
|
|
+ const char *p = (const char *)pl;
|
|
|
|
|
+ uint16_t pl_n = pln;
|
|
|
|
|
+ auto pathIs = [&](const char *k) -> bool {
|
|
|
|
|
+ uint16_t kl = (uint16_t)strlen(k);
|
|
|
|
|
+ return pl_n >= kl && memcmp(p, k, kl) == 0 &&
|
|
|
|
|
+ (pl_n == kl || p[kl] == 0);
|
|
|
|
|
+ };
|
|
|
|
|
+ S.resp[0] = 0xED;
|
|
|
|
|
+ if (slot >= 0 && slot < NSLOT && g_slot[slot].used &&
|
|
|
|
|
+ pathIs("esb/bond")) {
|
|
|
|
|
+ S.resp[1] = 0x18;
|
|
|
|
|
+ memcpy(S.resp + 2, g_slot[slot].rec, 24);
|
|
|
|
|
+ } else if (pathIs("user/wireless_transport")) {
|
|
|
|
|
+ // Steam reads this (a CONTROLLER setting, relayed rid1) to mark which bond slot is ACTIVE in the
|
|
|
|
|
+ // pairing list, mapping active_slot = value XOR 2 (HANDOFF: slot0->0x02, slot1->0x03, slot2->0x00,
|
|
|
|
|
+ // slot3->0x01). It's a single DONGLE-WIDE value = the currently-connected slot, NOT this
|
|
|
|
|
+ // interface's own index (returning the raw index made Steam mark slot^2 active -> the live puck
|
|
|
|
|
+ // showed "Inactive"). Report the connected slot's code so Steam marks the live puck active.
|
|
|
|
|
+ int act = 0;
|
|
|
|
|
+ unsigned long best = 0, now = millis();
|
|
|
|
|
+ for (int s2 = 0; s2 < NSLOT; s2++)
|
|
|
|
|
+ if (g_slot[s2].used && g_connReplyMs[s2] &&
|
|
|
|
|
+ (now - g_connReplyMs[s2]) < 1200u &&
|
|
|
|
|
+ (best == 0 || g_connReplyMs[s2] > best)) {
|
|
|
|
|
+ best = g_connReplyMs[s2];
|
|
|
|
|
+ act = s2;
|
|
|
|
|
+ }
|
|
|
|
|
+ S.resp[1] = 1;
|
|
|
|
|
+ S.resp[2] = (uint8_t)(act ^ 2);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ S.resp[1] =
|
|
|
|
|
+ 0; // absent/empty -- valid "no value" reply, not a path echo
|
|
|
|
|
+ }
|
|
|
|
|
+ S.resp_len = 63;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
default:
|
|
default:
|
|
|
S.resp[0] = cmd;
|
|
S.resp[0] = cmd;
|
|
|
S.resp[1] = len;
|
|
S.resp[1] = len;
|
|
@@ -316,27 +546,46 @@ static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
if (type != HID_REPORT_TYPE_FEATURE)
|
|
if (type != HID_REPORT_TYPE_FEATURE)
|
|
|
return 0;
|
|
return 0;
|
|
|
|
|
|
|
|
- // host reading feature data => gamepad consumer active (Steam/SDL)
|
|
|
|
|
- if (g_usbMode == MODE_STEAM)
|
|
|
|
|
- hostStampAlive();
|
|
|
|
|
|
|
+ // Do NOT treat a feature GET as "Steam is driving." A bare read is weak evidence: on Linux the kernel
|
|
|
|
|
+ // hid-steam driver (and any hidapi enumerator) issues GET_FEATURE probes even with Steam's window closed,
|
|
|
|
|
+ // which pinned steamDrivingGamepad() true forever and blocked the Steam-closed lizard fallback (keyboard/
|
|
|
|
|
+ // mouse dead, and the lizard-off writes those readers pair with the probe also killed the autonomous
|
|
|
|
|
+ // touchpad haptics). Steam actually taking over is detected by its OUTPUT/SET WRITES (the 0x87 lizard-off
|
|
|
|
|
+ // heartbeat ~every 3s, and 0x82 haptics), which stamp g_steamAliveMs on the handleSet paths. So drop the
|
|
|
|
|
+ // GET-based stamp entirely; a read alone no longer suppresses lizard.
|
|
|
Slot &S = g_slot[slot];
|
|
Slot &S = g_slot[slot];
|
|
|
uint16_t n = S.resp_len ? S.resp_len : 63;
|
|
uint16_t n = S.resp_len ? S.resp_len : 63;
|
|
|
if (n > reqlen)
|
|
if (n > reqlen)
|
|
|
n = reqlen;
|
|
n = reqlen;
|
|
|
memcpy(buf, S.resp, n);
|
|
memcpy(buf, S.resp, n);
|
|
|
|
|
+ // Flight recorder (every GET, un-deduped): a read STORM -- e.g. the "AE x39" identity hammering seen before
|
|
|
|
|
+ // the identity fix -- is itself a wedge signal, so we want it filling the post-mortem trail if it happens.
|
|
|
|
|
+ faultDiagTrace(FR_GET, (uint16_t)((rid << 8) | S.resp[0]));
|
|
|
// Battery diagnostic: in gamepad (Steam) mode battery is read host-side via the feature channel, NOT the
|
|
// Battery diagnostic: in gamepad (Steam) mode battery is read host-side via the feature channel, NOT the
|
|
|
// forwarded 0x43 (that path is verbatim-identical to lizard mode, where battery works). Capture what Steam
|
|
// forwarded 0x43 (that path is verbatim-identical to lizard mode, where battery works). Capture what Steam
|
|
|
// GETs so a WebUSB-panel (or CDC) capture in Steam mode shows the report id it polls for battery (then we
|
|
// GETs so a WebUSB-panel (or CDC) capture in Steam mode shows the report id it polls for battery (then we
|
|
|
// answer it with g_battery in handleSet). De-duped by report id so the high-rate polling doesn't flood the
|
|
// answer it with g_battery in handleSet). De-duped by report id so the high-rate polling doesn't flood the
|
|
|
// ring/console -- a freshly-requested id is logged once, then again only after 1s.
|
|
// ring/console -- a freshly-requested id is logged once, then again only after 1s.
|
|
|
{
|
|
{
|
|
|
- static uint8_t lastRid = 0xFF;
|
|
|
|
|
|
|
+ // Dedup by (rid, interface, response-cmd) so each interface's GET is captured (not collapsed to one
|
|
|
|
|
+ // line across all 4 slots) -- the per-interface bond/identity responses are exactly what we need to
|
|
|
|
|
+ // see -- while still not flooding on high-rate same-report polling.
|
|
|
|
|
+ static uint8_t lastRid = 0xFF, lastSlot = 0xFF, lastCmd = 0xFF;
|
|
|
static unsigned long lastMs = 0;
|
|
static unsigned long lastMs = 0;
|
|
|
- if (rid != lastRid || millis() - lastMs > 1000) {
|
|
|
|
|
|
|
+ if (rid != lastRid || (uint8_t)slot != lastSlot ||
|
|
|
|
|
+ S.resp[0] != lastCmd || millis() - lastMs > 1000) {
|
|
|
lastRid = rid;
|
|
lastRid = rid;
|
|
|
|
|
+ lastSlot = (uint8_t)slot;
|
|
|
|
|
+ lastCmd = S.resp[0];
|
|
|
lastMs = millis();
|
|
lastMs = millis();
|
|
|
// ring marker 0xFC = "host feature GET" (panel renders it as "GET rid=.."); payload = what we returned
|
|
// ring marker 0xFC = "host feature GET" (panel renders it as "GET rid=.."); payload = what we returned
|
|
|
hapLogAdd(0xFC, rid, S.resp, n);
|
|
hapLogAdd(0xFC, rid, S.resp, n);
|
|
|
|
|
+ // serial feature-command capture: what Steam READ on this interface + our answer
|
|
|
|
|
+ fcPush(1, slot, rid, S.resp[0], (uint8_t)n, S.resp + 2,
|
|
|
|
|
+ 10);
|
|
|
|
|
+#if OPK_LOG
|
|
|
|
|
+ // Diagnostic build only -- no Serial.printf on the 800B usbd-task stack in production (LOCKUP
|
|
|
|
|
+ // mitigation). The GET is captured to the OPK_LOG ring above for the WebUSB panel.
|
|
|
if (Serial.availableForWrite() > 80)
|
|
if (Serial.availableForWrite() > 80)
|
|
|
Serial.printf(
|
|
Serial.printf(
|
|
|
"# GET if%d rid=%02X reqlen=%u -> %02X %02X %02X (batt=%u%%)\n",
|
|
"# GET if%d rid=%02X reqlen=%u -> %02X %02X %02X (batt=%u%%)\n",
|
|
@@ -345,6 +594,7 @@ static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
(slot >= 0 && slot < NSLOT) ?
|
|
(slot >= 0 && slot < NSLOT) ?
|
|
|
g_battery[slot] :
|
|
g_battery[slot] :
|
|
|
0);
|
|
0);
|
|
|
|
|
+#endif
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return n;
|
|
return n;
|
|
@@ -408,7 +658,8 @@ void SteamPuckController::begin()
|
|
|
// USB-side decision -- changes nothing about the RF poll or the host->controller relay. Per-slot: each
|
|
// USB-side decision -- changes nothing about the RF poll or the host->controller relay. Per-slot: each
|
|
|
// controller's 0x45 goes to its OWN hid[slot], so Steam sees four independent inputs. Lizard, by contrast,
|
|
// controller's 0x45 goes to its OWN hid[slot], so Steam sees four independent inputs. Lizard, by contrast,
|
|
|
// presents ONE mouse + keyboard (a desktop can only consume one): rfLizard merges every bonded controller's
|
|
// presents ONE mouse + keyboard (a desktop can only consume one): rfLizard merges every bonded controller's
|
|
|
-// input onto hid[0], so all connected controllers drive the same cursor/keys together.
|
|
|
|
|
|
|
+// input (g_in[] across all used slots, populated by the per-slot decode) onto hid[0], so all connected
|
|
|
|
|
+// controllers drive the same cursor/keys together -- and it works regardless of which bond slot is live.
|
|
|
void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
|
|
void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
|
|
|
uint8_t bodyTlen)
|
|
uint8_t bodyTlen)
|
|
|
{
|
|
{
|
|
@@ -427,30 +678,45 @@ void SteamPuckController::onReport45(int slot, const uint8_t *rep, bool fresh,
|
|
|
return;
|
|
return;
|
|
|
if (!g_slot[slot].used)
|
|
if (!g_slot[slot].used)
|
|
|
return;
|
|
return;
|
|
|
|
|
+ // Report id: 0x45 = legacy main input; 0x42 = new-firmware (SC2 beta ~2026-07) main input (53B). Forward
|
|
|
|
|
+ // whichever the controller actually sent, verbatim, under its own id -- both are declared in PUCK_HID_DESC
|
|
|
|
|
+ // and this is exactly what the real puck does. rep[0] is the id byte (rep points at it in the F1 TLV).
|
|
|
|
|
+ const uint8_t rid = rep[0];
|
|
|
if (lizardActive()) {
|
|
if (lizardActive()) {
|
|
|
// All bonded controllers share ONE desktop mouse/keyboard (mounted on hid[0] regardless of
|
|
// All bonded controllers share ONE desktop mouse/keyboard (mounted on hid[0] regardless of
|
|
|
// which RF slots are in use). rfLizard merges g_in[] across every used slot internally, so we
|
|
// which RF slots are in use). rfLizard merges g_in[] across every used slot internally, so we
|
|
|
- // fire it ONCE per cycle -- gated on the lowest bonded slot's report so it runs at a steady
|
|
|
|
|
- // cadence (not once per slot, which would multiply the output rate).
|
|
|
|
|
|
|
+ // fire it ONCE per cycle -- gated on the lowest CONNECTED slot's report so it runs at a steady
|
|
|
|
|
+ // cadence (not once per slot, which would multiply the output rate). g_in[] is populated by the
|
|
|
|
|
+ // per-slot decode for BOTH report 0x45 and the beta 0x42 (byte-identical front section), so
|
|
|
|
|
+ // lizard works on new-firmware controllers too without reading rep[] here.
|
|
|
|
|
+ // Gate on the lowest RECENTLY-REPLYING slot, not merely the lowest bonded one: a bonded-but-
|
|
|
|
|
+ // offline low slot (e.g. a stale/phantom bond from a cloned backup) must not starve lizard when
|
|
|
|
|
+ // a higher slot is the live controller (main's #98 fix -- do not regress it). onReport45 only
|
|
|
|
|
+ // runs for a slot that just decoded a report, so the reporting slot is itself connected.
|
|
|
|
|
+ unsigned long now = millis();
|
|
|
int lizSlot = -1;
|
|
int lizSlot = -1;
|
|
|
for (int s = 0; s < NSLOT; s++)
|
|
for (int s = 0; s < NSLOT; s++)
|
|
|
- if (g_slot[s].used) {
|
|
|
|
|
|
|
+ if (g_slot[s].used && g_connReplyMs[s] &&
|
|
|
|
|
+ (now - g_connReplyMs[s]) < 1200u) {
|
|
|
lizSlot = s;
|
|
lizSlot = s;
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
if (slot == lizSlot)
|
|
if (slot == lizSlot)
|
|
|
rfLizard(&hid[0], &hid[0], 0x40, 0x41);
|
|
rfLizard(&hid[0], &hid[0], 0x40, 0x41);
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // body length after the id byte, clamped to the descriptor's declared size for this report id
|
|
|
|
|
+ // (0x42 = 53B vendor input, 0x45 = 45B input).
|
|
|
|
|
+ uint8_t maxb = (rid == 0x42) ? 53 : 45;
|
|
|
uint8_t blen = bodyTlen - 1;
|
|
uint8_t blen = bodyTlen - 1;
|
|
|
- if (blen > 45)
|
|
|
|
|
- blen = 45; // body after the 0x45 id byte
|
|
|
|
|
|
|
+ if (blen > maxb)
|
|
|
|
|
+ blen = maxb;
|
|
|
// forward the puck's raw pad coords untouched (Steam does its own interpolation/smoothing). Forward
|
|
// 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
|
|
// only FRESH reports: the real puck dedupes, so stale repeats make Steam's velocity/smoothing
|
|
|
// stair-step. g_fwdNewOnly toggles for A/B.
|
|
// stair-step. g_fwdNewOnly toggles for A/B.
|
|
|
if ((fresh || !g_fwdNewOnly) && hid[slot].ready())
|
|
if ((fresh || !g_fwdNewOnly) && hid[slot].ready())
|
|
|
- hid[slot].sendReport(
|
|
|
|
|
- 0x45, rep + 1,
|
|
|
|
|
- blen); // Steam/SDL Triton: input report 0x45
|
|
|
|
|
|
|
+ usbTxHid(
|
|
|
|
|
+ &hid[slot], rid, rep + 1,
|
|
|
|
|
+ blen); // Steam/SDL Triton: input report 0x45 (old) / 0x42 (new fw)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -474,7 +740,7 @@ void SteamPuckController::onAuxReport(int slot, uint8_t rid,
|
|
|
// capture the pushed status report (0x43 battery / 0x44) device->host for the WebUSB panel: this is
|
|
// capture the pushed status report (0x43 battery / 0x44) device->host for the WebUSB panel: this is
|
|
|
// the channel Steam actually reads battery from; marker 0xFB = "->host push".
|
|
// the channel Steam actually reads battery from; marker 0xFB = "->host push".
|
|
|
hapLogAdd(0xFB, rid, data, n);
|
|
hapLogAdd(0xFB, rid, data, n);
|
|
|
- hid[slot].sendReport(rid, data, n);
|
|
|
|
|
|
|
+ usbTxHid(&hid[slot], rid, data, n);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -552,8 +818,8 @@ static void wakeNudgeTask()
|
|
|
m.y = 0;
|
|
m.y = 0;
|
|
|
m.wheel = 0;
|
|
m.wheel = 0;
|
|
|
m.pan = 0;
|
|
m.pan = 0;
|
|
|
- hid[s].sendReport(0x40, &m,
|
|
|
|
|
- sizeof m); // jiggle right, then back
|
|
|
|
|
|
|
+ usbTxHid(&hid[s], 0x40, &m,
|
|
|
|
|
+ sizeof m); // jiggle right, then back
|
|
|
g_nudgeStep[s] = (g_nudgeStep[s] >= 2) ?
|
|
g_nudgeStep[s] = (g_nudgeStep[s] >= 2) ?
|
|
|
0 :
|
|
0 :
|
|
|
(uint8_t)(g_nudgeStep[s] + 1);
|
|
(uint8_t)(g_nudgeStep[s] + 1);
|
|
@@ -603,7 +869,7 @@ void SteamPuckController::task()
|
|
|
connEdgeMs[s] = millis();
|
|
connEdgeMs[s] = millis();
|
|
|
uint8_t st = conn ? 0x02 : 0x01;
|
|
uint8_t st = conn ? 0x02 : 0x01;
|
|
|
hapLogAdd(0xFB, 0x79, &st, 1); // ->host push (capture)
|
|
hapLogAdd(0xFB, 0x79, &st, 1); // ->host push (capture)
|
|
|
- hid[s].sendReport(0x79, &st, 1);
|
|
|
|
|
|
|
+ usbTxHid(&hid[s], 0x79, &st, 1);
|
|
|
usbConn[s] = conn;
|
|
usbConn[s] = conn;
|
|
|
last79[s] = millis();
|
|
last79[s] = millis();
|
|
|
} else if (conn && millis() - last7B[s] >= 2000) {
|
|
} else if (conn && millis() - last7B[s] >= 2000) {
|
|
@@ -627,7 +893,7 @@ void SteamPuckController::task()
|
|
|
s7b[8] = (uint8_t)(0u - (uint8_t)mag);
|
|
s7b[8] = (uint8_t)(0u - (uint8_t)mag);
|
|
|
}
|
|
}
|
|
|
hapLogAdd(0xFB, 0x7B, s7b, 12); // ->host push (capture)
|
|
hapLogAdd(0xFB, 0x7B, s7b, 12); // ->host push (capture)
|
|
|
- hid[s].sendReport(0x7B, s7b, 12);
|
|
|
|
|
|
|
+ usbTxHid(&hid[s], 0x7B, s7b, 12);
|
|
|
last7B[s] = millis();
|
|
last7B[s] = millis();
|
|
|
}
|
|
}
|
|
|
// Synthesized 0x43 = ID_TRITON_BATTERY_STATUS for SDL/Steam's gamepad driver. The verbatim forward of
|
|
// Synthesized 0x43 = ID_TRITON_BATTERY_STATUS for SDL/Steam's gamepad driver. The verbatim forward of
|
|
@@ -645,7 +911,7 @@ void SteamPuckController::task()
|
|
|
b43[0] = st; // ucChargeState
|
|
b43[0] = st; // ucChargeState
|
|
|
b43[1] = g_battery[s]; // ucBatteryLevel (percent)
|
|
b43[1] = g_battery[s]; // ucBatteryLevel (percent)
|
|
|
hapLogAdd(0xFB, 0x43, b43, 14); // ->host push (capture)
|
|
hapLogAdd(0xFB, 0x43, b43, 14); // ->host push (capture)
|
|
|
- hid[s].sendReport(0x43, b43, sizeof b43);
|
|
|
|
|
|
|
+ usbTxHid(&hid[s], 0x43, b43, sizeof b43);
|
|
|
last43[s] = millis();
|
|
last43[s] = millis();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|