|
@@ -92,6 +92,69 @@ 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)
|
|
|
|
|
+{
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ===================== 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
|
|
@@ -158,15 +221,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;
|
|
|
- // NEVER relay output report 0x81 (= CLEAR DIGITAL MAPPINGS, ibex FUN_0001f554) to the controller.
|
|
|
|
|
- // It is NOT a haptic despite sitting in the 0x80-0x86 range; it is non-idempotent (each one re-arms a
|
|
|
|
|
- // hardware block -> an audible actuator "deep-inside" click) and OpenPuck does its OWN input
|
|
|
|
|
- // translation, so it never needs the controller's internal mapping engine. Steam sends CLEAR DIGITAL
|
|
|
|
|
- // MAPPINGS as part of its per-controller CONNECT init (this is why the click is seen only in Steam
|
|
|
|
|
- // mode, only with Steam open, and only at connect). sniff1.json was captured mid-session (established
|
|
|
|
|
- // link, no connect handshake), so it never showed the connect init -- absence of 0x81 there is not
|
|
|
|
|
- // evidence Steam never sends it. Dropping the relay is safe (Steam still gets 0x45) and quiets connect.
|
|
|
|
|
- if (rid >= 0x80 && rid <= 0x86 && rid != 0x81 && 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,
|
|
@@ -208,6 +268,8 @@ 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);
|
|
|
|
|
|
|
|
// 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)
|
|
@@ -228,10 +290,26 @@ 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;
|
|
|
|
|
|
|
|
- // never push haptics while presenting lizard (Steam isn't reading 0x45 -> would buzz-loop); and NEVER
|
|
|
|
|
- // relay 0x81 CLEAR DIGITAL MAPPINGS (same reason as the OUTPUT path above -- non-idempotent actuator
|
|
|
|
|
- // click, unused by OpenPuck's own translation, and the source of the Steam-mode connect clicks).
|
|
|
|
|
- bool relayOk = hapticRelaySlotOk(slot) && cmd != 0x81 &&
|
|
|
|
|
|
|
+ // 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)
|
|
|
|
|
+ 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
|
|
@@ -269,13 +347,45 @@ 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). idx 3 = the fixed BT-MAC-style
|
|
|
|
|
+ // string the real controller reports.
|
|
|
|
|
+ char ctrl[17];
|
|
|
|
|
+ const char *s;
|
|
|
|
|
+ if (rid == 1 && slot >= 0 && slot < NSLOT &&
|
|
|
|
|
+ g_slot[slot].used) {
|
|
|
|
|
+ memcpy(ctrl, g_slot[slot].rec + 8, 16);
|
|
|
|
|
+ ctrl[16] = 0;
|
|
|
|
|
+ s = (idx == 0 || idx == 1 || idx == 4) ? ctrl :
|
|
|
|
|
+ (idx == 3) ? "7054257d2da7" :
|
|
|
|
|
+ "NA";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ s = (idx == 0 || idx == 4) ? g_board :
|
|
|
|
|
+ (idx == 1) ? g_unit :
|
|
|
|
|
+ (idx == 3) ? "7054257d2da7" :
|
|
|
|
|
+ "NA";
|
|
|
|
|
+ }
|
|
|
S.resp[0] = 0xAE;
|
|
S.resp[0] = 0xAE;
|
|
|
S.resp[1] = 0x14;
|
|
S.resp[1] = 0x14;
|
|
|
S.resp[2] = idx;
|
|
S.resp[2] = idx;
|
|
@@ -334,6 +444,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;
|
|
@@ -368,13 +558,22 @@ static uint16_t handleGet(int slot, uint8_t rid, hid_report_type_t type,
|
|
|
// 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
|
|
#if OPK_LOG
|
|
|
// Diagnostic build only -- no Serial.printf on the 800B usbd-task stack in production (LOCKUP
|
|
// 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.
|
|
// mitigation). The GET is captured to the OPK_LOG ring above for the WebUSB panel.
|