瀏覽代碼

Hopefully more stable connection

Jariullah Safi 4 月之前
父節點
當前提交
fdb0a81a6a
共有 5 個文件被更改,包括 82 次插入 和 12 次删除
  1. 4 0
      OpenPuck/config.h
  2. 15 2
      OpenPuck/haptics.cpp
  3. 12 2
      OpenPuck/puck_hid.cpp
  4. 19 8
      OpenPuck/rf_link.cpp
  5. 32 0
      docs/index.html

+ 4 - 0
OpenPuck/config.h

@@ -101,6 +101,10 @@ void swProSaveCfg();
 #define POLL_US_DEFAULT 4000u
 // host-side HID stream cadence for translated modes (~250 Hz)
 #define USB_STREAM_MS 4u
+// How long a USB suspend must PERSIST before we power the controllers off. A brief selective-suspend
+// (host idle power-management) resumes in <1s and must not trigger a self-inflicted power-off -> the
+// resulting disconnect/reconnect churn looked like random controller drops. Real host sleep persists.
+#define SUSPEND_OFF_MS 4000u
 // RF poll cadence (us). FIXED -- not configurable (see loadCfg).
 extern const uint32_t g_pollUs;
 

+ 15 - 2
OpenPuck/haptics.cpp

@@ -469,13 +469,26 @@ void hapticTask()
 				     0;
 	}
 	// Power-off on host sleep: only when VBUS is present (genuine sleep, not a cable unplug which also
-	// trips the suspend edge briefly). wasSusp=true at boot suppresses a false fire on boot-into-suspended.
+	// trips the suspend edge briefly) AND the suspend has PERSISTED >= SUSPEND_OFF_MS. A brief USB
+	// selective-suspend (host idle power-management) resumes in <1s; firing the power-off on its edge
+	// powered the controllers off ourselves -> random drop/reconnect churn. Arm only on a genuine
+	// resume->suspend edge (wasSusp=true at boot suppresses a false fire on boot-into-suspended).
 	static bool wasSusp = true;
+	static unsigned long suspSinceMs = 0;
+	static bool suspArmed = false;
 	bool susp = USBDevice.suspended();
 	bool vbus = (NRF_POWER->USBREGSTATUS &
 		     POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
-	if (susp && !wasSusp && vbus)
+	if (susp && !wasSusp) {
+		suspSinceMs = millis();
+		suspArmed = true;
+	}
+	if (!susp)
+		suspArmed = false;
+	if (suspArmed && vbus && (millis() - suspSinceMs) >= SUSPEND_OFF_MS) {
 		hapticSendShutdown();
+		suspArmed = false; // fire once per suspend
+	}
 	wasSusp = susp;
 	// Steam-mode quiet timeout: mark 0x82 stream inactive. No synthesized stop -- Steam forwards its own.
 	if (!g_xbox && g_haptic82On &&

+ 12 - 2
OpenPuck/puck_hid.cpp

@@ -432,8 +432,18 @@ void SteamPuckController::onAuxReport(int slot, uint8_t rid,
 		return;
 	if (slot < 0 || slot >= NSLOT)
 		return;
-	if (g_slot[slot].used && hid[slot].ready())
-		hid[slot].sendReport(rid, data, n);
+	if (!(g_slot[slot].used && hid[slot].ready()))
+		return;
+	// macOS/Steam DROP an input report shorter than its descriptor-declared length, so pad to it
+	// (zero-filled tail). Declared payload lengths excl. report ID: 0x43 = 14, 0x44 = 5. The
+	// controller often sends a compact 0x43; without this pad Steam never reads battery.
+	uint8_t declared = (rid == 0x43) ? 14u : (rid == 0x44) ? 5u : n;
+	uint8_t buf[16];
+	if (declared > sizeof buf)
+		declared = sizeof buf;
+	memset(buf, 0, declared);
+	memcpy(buf, data, (n < declared) ? n : declared);
+	hid[slot].sendReport(rid, buf, declared);
 }
 
 // wake nudge: a bare USB resume signal is NOT enough to wake some hosts (Windows in particular) -- they only

+ 19 - 8
OpenPuck/rf_link.cpp

@@ -274,9 +274,20 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 			}
 			if (rtype == 0xF1)
 				g_stF1++;
-			// controller disconnecting/powering off -> back off 2.5s
-			if (rtype == 0xF2)
-				g_connCooldown = millis();
+			// controller disconnecting/powering off -> back off 2.5s so we don't immediately re-wake it.
+			// BUT only when no OTHER slot is still live: g_connCooldown is global and gates ALL polling +
+			// beacons, so backing off because ONE controller powered off would drop every other connected
+			// controller for 2.5s (a real multi-controller disconnect). The powering-off slot goes silent
+			// and ages out via SLOT_COLD on its own.
+			if (rtype == 0xF2) {
+				int others = 0;
+				for (int i = 0; i < NSLOT; i++)
+					if (i != g_curSlot && g_slot[i].used &&
+					    millis() - g_connReplyMs[i] < 300)
+						others++;
+				if (others == 0)
+					g_connCooldown = millis();
+			}
 			// F3 = controller status/version reply (reply to E7 handshake, byte[6]=version)
 			if (rtype == 0xF3) {
 				g_stF3++;
@@ -612,11 +623,11 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 #define SLOT_COLD_RETRY_MS 2000u
 static unsigned long g_slotLastAttemptMs[NSLOT] = {};
 
-// Drive the connected-mode sequence one step per call. Polls ONE bonded slot per call.
-// Gate fires at g_pollUs / nBonded so each warm slot gets ~250 Hz in round-robin order.
-// "Cold" means the slot WAS connected but has been silent for > SLOT_COLD_MS; slots that
-// have never replied stay warm so new controllers can connect at any time after boot.
-// Cold slots retry every SLOT_COLD_RETRY_MS with a full E7+relay+GET sequence.
+// Drive the connected-mode sequence. The cycle gate fires once per g_pollUs (250 Hz); each fire
+// polls EVERY warm slot back-to-back so all bonded controllers run at full rate regardless of count.
+// "Cold" means the slot WAS connected but has been silent for > SLOT_COLD_MS; slots that have never
+// replied stay warm so new controllers can connect at any time after boot. Cold slots retry every
+// SLOT_COLD_RETRY_MS.
 static void rfConnStep()
 {
 	int firstUsed = -1;

+ 32 - 0
docs/index.html

@@ -111,7 +111,10 @@
         <div class="stat logonly"><div class="k">Slowest stage</div><div class="v" id="stWorst">—</div></div>
         <div class="stat logonly"><div class="k">Poll period act/want</div><div class="v" id="stPollPer">—</div></div>
         <div class="stat"><div class="k">Build (git)</div><div class="v" id="stBuild">—</div></div>
+        <div class="stat"><div class="k">Last reset</div><div class="v" id="stReset">—</div></div>
       </div>
+      <p class="note" style="margin-top:6px"><strong>Reset history</strong> (newest first, persists across power-off): <span id="resetHist" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
+        <em>watchdog</em>/<em>LOCKUP</em> = a fault (loop hang / crash); <em>soft reboot</em> = an intentional reset (mode change); <em>power-on</em> = normal plug-in.</p>
       <p class="note" style="margin-top:8px"><strong>IMU (raw):</strong> <span id="stImu" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
         Raw SC2 accel (before scaling). This controller is a ±2 g sensor (~16384 = 1 g); the Switch report divides it by 4 to present the genuine ±8 g (4096 = 1 g) scale so the console's gravity-correction engages and the gyro stops drifting.</p>
       <p class="note logonly">Polls/s ≈ 250 expected; it's capped by Loop period (≈4000 µs needed). "Slowest stage" is the loop section eating the most time per iteration.</p>
@@ -375,6 +378,35 @@ function applyBlob(p){
   $("#stLoop").textContent = loopUs? loopUs+" µs" : "—";
   $("#stWorst").textContent = loopUs? (WORST[worstIdx]||worstIdx)+" "+worstUs+"µs" : "—";
   $("#stPollPer").textContent = up? (pollUs+" / "+pollIntended+" µs") : "—";
+  // reset-cause history (protocol v9): JS p[73]=count, p[74..83]=last 10 causes (newest first).
+  // Persisted in puck flash, so a reboot that happened on a Switch is still readable here after replugging.
+  function resetReasonText(rr){
+    if (rr === 0) return "power-on";
+    const r = [];
+    if (rr & 0x02) r.push("watchdog");      // DOG: loop wedge / hang
+    if (rr & 0x08) r.push("LOCKUP");        // CPU lockup = HardFault escalation (real crash)
+    if (rr & 0x04) r.push("soft reboot");   // SREQ: NVIC_SystemReset (mode change etc.)
+    if (rr & 0x01) r.push("reset pin");
+    return r.length ? r.join("+") : ("0x"+rr.toString(16));
+  }
+  if (p.length > 73) {
+    const n = Math.min(p[73], 10);
+    const hist = [];
+    for (let i=0; i<n; i++) hist.push(p[74+i]);
+    const fault = (rr)=>!!(rr & 0x0A); // watchdog or lockup = a real fault
+    if (n === 0) { $("#stReset").textContent = "—"; }
+    else {
+      const newest = hist[0];
+      const head = fault(newest) ? ('<span class="pill dn">'+resetReasonText(newest)+'</span>') : resetReasonText(newest);
+      $("#stReset").innerHTML = head;
+      // full history list under the tile (newest -> oldest)
+      const items = hist.map((rr,i)=>{
+        const t = resetReasonText(rr);
+        return (i===0?'<strong>':'')+ '#'+(n-i)+' '+(fault(rr)?('<span style="color:#ff6b6b">'+t+'</span>'):t) +(i===0?' (latest)</strong>':'');
+      });
+      const el = $("#resetHist"); if (el) el.innerHTML = items.join(' &nbsp;·&nbsp; ');
+    }
+  }
   const gitDirty=(p.length>38?p[38]:0); let gitHash="";
   for(let i=39;i<51 && i<p.length && p[i];i++) gitHash+=String.fromCharCode(p[i]);
   $("#stBuild").innerHTML = gitHash