Преглед изворни кода

Fix Steam Controller battery reporting in gamepad (Steam) mode

SDL's SC2/Triton HIDAPI driver reads battery only from report 0x43
(ID_TRITON_BATTERY_STATUS) and ignores it unless the report is the full
TritonBatteryStatus_t length (>= 15 bytes incl. id). The controller's
verbatim-forwarded 0x43 is shorter, so SDL fell back to the "wired" glyph
with no battery %, while the kernel hid-steam path (lizard mode) read it
fine.

Push a synthesized full 14-byte 0x43 from g_battery every 2s in puck
task() (keeping the verbatim forward for the lizard/kernel path), with a
valid EChargeState so SDL reports ON_BATTERY + level. rf_link now also
snapshots the charge-state byte (g_batteryState) next to g_battery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jari пре 4 месеци
родитељ
комит
afd4a9f88c
3 измењених фајлова са 29 додато и 3 уклоњено
  1. 18 1
      OpenPuck/puck_hid.cpp
  2. 9 2
      OpenPuck/rf_link.cpp
  3. 2 0
      OpenPuck/rf_link.h

+ 18 - 1
OpenPuck/puck_hid.cpp

@@ -514,7 +514,7 @@ void SteamPuckController::task()
 	// slot. The real puck's per-slot edge-triggered 0x79 prevents re-triggering Steam's connect-chime loop.
 	static bool usbConn[NSLOT] = { 0 };
 	static unsigned long last79[NSLOT] = { 0 }, last7B[NSLOT] = { 0 },
-			     connEdgeMs[NSLOT] = { 0 };
+			     connEdgeMs[NSLOT] = { 0 }, last43[NSLOT] = { 0 };
 	for (int s = 0; s < NSLOT; s++) {
 		if (!g_slot[s].used || !hid[s].ready())
 			continue;
@@ -558,6 +558,23 @@ void SteamPuckController::task()
 			hid[s].sendReport(0x7B, s7b, 12);
 			last7B[s] = millis();
 		}
+		// Synthesized 0x43 = ID_TRITON_BATTERY_STATUS for SDL/Steam's gamepad driver. The verbatim forward of
+		// the controller's own 0x43 (onAuxReport) is what the LIZARD/kernel path reads, but SDL's Triton driver
+		// requires the FULL TritonBatteryStatus_t length (r >= 1 + 14) and lapses to the "wired" glyph without a
+		// fresh one -- so we push a clean 14-byte report from g_battery every 2s. Body: [ucChargeState][ucBattery
+		// Level][voltages/current/temp = 0]; SDL only reads the first two. Map unknown/reset state -> discharging
+		// so it shows ON_BATTERY + % rather than UNKNOWN. Skipped until the controller has reported a level.
+		if (conn && g_battery[s] && millis() - last43[s] >= 2000) {
+			uint8_t st = g_batteryState[s];
+			// EChargeState discharging -> SDL_POWERSTATE_ON_BATTERY
+			if (st != 1 && st != 2 && st != 4)
+				st = 1;
+			uint8_t b43[14] = { 0 };
+			b43[0] = st; // ucChargeState
+			b43[1] = g_battery[s]; // ucBatteryLevel (percent)
+			hid[s].sendReport(0x43, b43, sizeof b43);
+			last43[s] = millis();
+		}
 	}
 	// Reset edge state for slots that are no longer used/ready (so a re-bond sees a fresh edge).
 	for (int s = 0; s < NSLOT; s++)

+ 9 - 2
OpenPuck/rf_link.cpp

@@ -68,6 +68,8 @@ volatile uint8_t g_linkRssi[NSLOT] = { 0 };
 // battery % from the controller's report 0x43 (body[1]); 0 = none yet. Per-slot -- the active controller's
 // battery is the most recently seen one (other slots' values stay in their own array slots).
 volatile uint8_t g_battery[NSLOT] = { 0 };
+// charge state from report 0x43 body[0] (EChargeState: 1=discharging 2=charging 4=charging-done; 0=unknown).
+volatile uint8_t g_batteryState[NSLOT] = { 0 };
 
 // Slot the poll loop is currently driving. Set by rfConnStep before each E7/relay/E3, consumed by the
 // decode (g_in[g_curSlot]), the haptic flush (per-slot session address), and the per-second stat dump.
@@ -534,13 +536,18 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 						// [rid][body...]
 						const uint8_t *rep =
 							&rfrx[idx + 2];
-						// 0x43 body[1] (~0x5e=94) reads as battery % (sniff-derived)
+						// 0x43 body[0] = ucChargeState (EChargeState), body[1] = ucBatteryLevel % (sniff-derived).
+						// rep[0]=rid, rep[1]=body[0], rep[2]=body[1]. Snapshot both for the WebUSB panel and the
+						// synthesized full-length 0x43 the puck pushes for SDL (see puck_hid.cpp task()).
 						if (rep[0] == 0x43 &&
 						    tlen >= 3 &&
 						    g_curSlot >= 0 &&
-						    g_curSlot < NSLOT)
+						    g_curSlot < NSLOT) {
+							g_batteryState[g_curSlot] =
+								rep[1];
 							g_battery[g_curSlot] =
 								rep[2];
+						}
 						if (g_active)
 							g_active->onAuxReport(
 								g_curSlot,

+ 2 - 0
OpenPuck/rf_link.h

@@ -91,6 +91,8 @@ extern volatile uint8_t g_linkRssi[NSLOT];
 // reads g_battery[g_curSlot] (the most recently active one). The raw 0x43 report is also forwarded to
 // Steam verbatim (puck_hid onAuxReport) so Steam reads it itself.
 extern volatile uint8_t g_battery[NSLOT];
+// charge state from report 0x43 body[0] (EChargeState: 1=discharging, 2=charging, 4=charging-done; 0=unknown)
+extern volatile uint8_t g_batteryState[NSLOT];
 
 // TX one connected packet [LEN][S1][payload] on channel ch, then RX the reply into rfrx; decodes 0xF1.
 // rxWinUs overrides the reply-wait window (0 = use g_rxWin). Pass a tiny value for NO-ACK relays that expect