Explorar el Código

hopefully final stability fix

Jari hace 3 meses
padre
commit
40ab5d5746
Se han modificado 6 ficheros con 94 adiciones y 35 borrados
  1. 2 6
      OpenPuck/config.cpp
  2. 60 2
      OpenPuck/rf_link.cpp
  3. 9 1
      OpenPuck/rf_link.h
  4. 2 4
      OpenPuck/serial_console.cpp
  5. 8 10
      OpenPuck/webusb_config.cpp
  6. 13 12
      docs/index.html

+ 2 - 6
OpenPuck/config.cpp

@@ -150,12 +150,8 @@ void loadCfg()
 
 			// 0 is a valid setting (rumble off)
 			g_rumbleScale = c.rumbleScale;
-			// Connection tunables: restore the poll RX window (0 = unset/old cfg -> keep boot default) and
-			// the post-connect haptic block (enable + duration in seconds).
-			if (c.rxWin10) {
-				uint32_t w = (uint32_t)c.rxWin10 * 10;
-				g_rxWin = w < 600 ? 600 : (w > 3000 ? 3000 : w);
-			}
+			// Connection tunables: the poll RX window is now FIXED (g_rxWin is const) -- any persisted
+			// rxWin10 is ignored. Restore the post-connect haptic block (enable + duration in seconds).
 			g_hapticBlockOn = c.hapticBlockOn ? 1 : 0;
 			// clamp seconds so s*1000 fits the uint16 ms field (max 60s)
 			g_hapticBlockMs = (uint16_t)((c.hapticBlockS > 60 ?

+ 60 - 2
OpenPuck/rf_link.cpp

@@ -41,8 +41,13 @@ bool g_e7announce =
 bool g_e1keepalive = true;
 
 bool g_connVerbose = false;
-// poll RX-window (us); shorter=more polls/s but may miss DELAYED replies. Persisted + panel-adjustable; 'r'.
-uint32_t g_rxWin = 2000;
+// poll RX-window (us): the poll BUSY-WAITS up to this long for the controller's reply, so it is the dominant
+// per-poll loop cost and directly sets the poll rate -- one slot needs (g_rxWin + overhead) < g_pollUs(4000)
+// to sustain 250 Hz. 1200 is the proven 250 Hz value; it was briefly raised to 2000 (issue-72, delayed-reply
+// tolerance) which dropped the rate to ~220. FIXED + not configurable (like g_pollUs): there is no good
+// reason to raise it in the field, and doing so silently halves the poll rate. Any persisted/old value is
+// ignored.
+const uint32_t g_rxWin = 1200;
 unsigned long g_connCooldown = 0;
 
 uint8_t g_connSt = 0; // 0=announce awake, 1=poll loop
@@ -62,6 +67,15 @@ uint16_t g_f1ps = 0;
 uint16_t g_newps = 0;
 // polls/s (GET+relay TXs) last second -- distinguishes loop-starvation from reply-loss
 uint16_t g_pollsps = 0;
+// last second's CRC-fail and no-reply-in-window counts (wedge diagnosis)
+uint16_t g_crcps = 0, g_norxps = 0;
+uint16_t g_rfStallRecover = 0;
+
+// RF-stall self-heal thresholds. A genuine worst-case reply gap during normal play is ~1.5s (the
+// hapticOnReconnect re-init window); past RF_STALL_MS with us still actively polling, the whole link is wedged,
+// not merely blipping. RF_RECOVER_MS rate-limits the recovery so it can't thrash while a stalled link re-syncs.
+#define RF_STALL_MS 2500u
+#define RF_RECOVER_MS 2000u
 // measured avg us between GET-poll fires (vs intended g_pollUs)
 uint16_t g_pollPeriodUs = 0;
 static uint32_t g_pollDtSum = 0;
@@ -806,6 +820,48 @@ void rfLinkTask()
 	if (g_connOn && millis() - g_connCooldown > 2500) {
 		rfConnStep();
 	} // connected-mode: poll controller, read input
+
+	// ---- RF stall self-heal -------------------------------------------------------------------------------
+	// Field wedge: the controllers stay powered and show a SOLID (connected) light, the puck keeps issuing E3
+	// polls + E1 beacons (radio TX is provably alive -- a power-cycled controller still re-adopts the session
+	// and goes solid against this puck), yet NOT ONE slot decodes a reply for seconds. The link reads down, no
+	// input reaches the host, and it never recovers on its own -- only a puck REPLUG fixes it. rfConfig() already
+	// re-disables (TASKS_DISABLE) the radio before every poll, so whatever this is survives a TASKS_DISABLE; the
+	// only thing that clears it is a full peripheral reset -- exactly what the replug does. So when the WHOLE
+	// link has been silent past RF_STALL_MS while we're still actively polling a slot that WAS connected, do the
+	// replug's job for the radio: power-cycle NRF_RADIO (POWER=0/1 clears every RADIO register; the next
+	// rfConnTx/rfHostFrameOnce re-applies all config via rfConfig()) and drop the connected-mode latches so
+	// polling/beaconing resume and the handshake re-runs from a clean slate. Gated on EVERY used+previously-
+	// connected slot being stalled, so one controller walking off (others still live) never trips it; the
+	// cooldown gate keeps it from firing while a controller is intentionally powering off; rate-limited so it
+	// cannot thrash. g_rfStallRecover is surfaced to the panel so the wedge -- and its recovery -- is observable.
+	if (g_connOn && g_curSlot >= 0 && millis() - g_connCooldown > 2500) {
+		static unsigned long lastRecoverMs = 0;
+		unsigned long nowMs2 = millis();
+		bool anyEverConnected = false, allStalled = true;
+		for (int s = 0; s < NSLOT; s++) {
+			if (!(g_slot[s].used && g_connReplyMs[s] != 0))
+				continue;
+			anyEverConnected = true;
+			if ((uint32_t)(nowMs2 - g_connReplyMs[s]) < RF_STALL_MS)
+				allStalled = false;
+		}
+		if (anyEverConnected && allStalled &&
+		    (uint32_t)(nowMs2 - lastRecoverMs) > RF_RECOVER_MS) {
+			lastRecoverMs = nowMs2;
+			g_rfStallRecover++;
+			NRF_RADIO->POWER = 0;
+			NRF_RADIO->POWER = 1;
+			g_connCooldown = 0;
+			g_connSt = 0;
+			g_connStep = 0;
+			if (Serial.availableForWrite() > 60)
+				Serial.printf(
+					"# RF STALL (#%u) -> radio power-cycle + reconnect\n",
+					g_rfStallRecover);
+		}
+	}
+
 	{
 		// remote wakeup on new RF controller connection (any slot)
 		static bool wasRfConn = false;
@@ -842,6 +898,8 @@ void rfLinkTask()
 		g_f1ps = g_stF1;
 		g_newps = g_stNew;
 		g_pollsps = (uint16_t)g_stPoll;
+		g_crcps = (uint16_t)g_stCrc;
+		g_norxps = (uint16_t)g_stNoRx;
 		g_pollPeriodUs =
 			g_pollDtCnt ? (uint16_t)(g_pollDtSum / g_pollDtCnt) : 0;
 		g_pollDtSum = 0;

+ 9 - 1
OpenPuck/rf_link.h

@@ -43,7 +43,9 @@ extern bool
 
 // full multi-line debug dump vs compact "I45 <hex>" stream
 extern bool g_connVerbose;
-extern uint32_t g_rxWin; // poll RX-window (us): caps poll rate (~1e6/rxWin)
+// poll RX-window (us): caps poll rate (~1e6/rxWin). FIXED, not configurable (like g_pollUs) -- raising it
+// drops the poll rate, so it's pinned at the 250 Hz value rather than exposed as a footgun knob.
+extern const uint32_t g_rxWin;
 
 // set on 0xF2 disconnect; pauses beacon+poll so a powering-off controller can sleep
 extern unsigned long g_connCooldown;
@@ -78,6 +80,12 @@ extern uint16_t g_newps;
 
 // last second's poll TX count (GET+relay) -- vs F1 tells starvation from reply-loss
 extern uint16_t g_pollsps;
+// last second's CRC-fail and no-reply counts: diagnose a wedge -- polls>0 + noRx high = RX dead / wrong
+// channel / controller silent; crc high = corrupt replies (interference); polls==0 = poll loop stopped.
+extern uint16_t g_crcps, g_norxps;
+// Count of RF-stall self-heals (radio power-cycle + reconnect) performed since boot. Non-zero means the
+// firmware caught and recovered from a full-link wedge that previously needed a manual puck replug.
+extern uint16_t g_rfStallRecover;
 
 // MEASURED avg us between GET-poll fires (compare to the intended g_pollUs=4000)
 extern uint16_t g_pollPeriodUs;

+ 2 - 4
OpenPuck/serial_console.cpp

@@ -371,11 +371,9 @@ void serialConsolePoll()
 						g_slot[s].rec[7]);
 				}
 			} else if (line[0] == 'r') {
-				g_rxWin = strtoul(line + 1, 0, 10);
-				if (g_rxWin < 150)
-					g_rxWin = 150;
+				// poll RX-window is FIXED (not configurable) -- report only.
 				Serial.printf(
-					"# poll RX-window=%lu us (poll rate caps ~%lu/s)\n",
+					"# poll RX-window=%lu us (FIXED; poll rate caps ~%lu/s)\n",
 					(unsigned long)g_rxWin,
 					(unsigned long)(1000000 / g_rxWin));
 			} else if (line[0] == 'e') {

+ 8 - 10
OpenPuck/webusb_config.cpp

@@ -30,7 +30,8 @@ static volatile bool g_blobRequest = false;
 //                [v11: resetReason(RR_* code)][resetReas raw u32 LE][rxWin/10 us][hapticBlockOn][hapticBlock s]
 // p[6]/p[7]/p[8..11] mirror the ACTIVE type (legacy display). v11 extends to 113 bytes (115 total incl header);
 // browser reads with transferIn(128) to span the two USB-FS packets.
-#define WB_PAYLEN 113
+//                [diag: crc/s][noRx/s][rfStallRecover count]
+#define WB_PAYLEN 116
 static void webusbSendBlob()
 {
 	if (!usb_web.connected())
@@ -149,6 +150,11 @@ static void webusbSendBlob()
 	p[112] = (uint8_t)(g_rxWin / 10);
 	p[113] = g_hapticBlockOn;
 	p[114] = (uint8_t)(g_hapticBlockMs / 1000);
+	// RF wedge diagnostics (always populated, even when the link reads down): CRC-fail/s and no-reply/s,
+	// capped at 255. With Polls/s this distinguishes poll-loop-stopped vs RX-dead vs corrupt-replies.
+	p[115] = (uint8_t)(g_crcps > 255 ? 255 : g_crcps);
+	p[116] = (uint8_t)(g_norxps > 255 ? 255 : g_norxps);
+	p[117] = (uint8_t)(g_rfStallRecover > 255 ? 255 : g_rfStallRecover);
 	usb_web.write(p, sizeof p);
 	usb_web.flush();
 }
@@ -430,15 +436,7 @@ void webusbPoll()
 					persist = false;
 					break; // Switch Pro gyro scale x10
 
-				// connected-poll RX window, in 10us units: wider window catches a controller reply that
-				// lands late under haptic load; narrower allows a higher poll rate. Clamp 600..3000us.
-				case 25: {
-					uint32_t w = (uint32_t)v * 10;
-					g_rxWin = w < 600  ? 600 :
-						  w > 3000 ? 3000 :
-							     w;
-					break;
-				}
+				// (field 25, poll RX window, removed -- g_rxWin is now FIXED/not configurable)
 
 				// post-connect haptic block enable: drop Steam haptics for g_hapticBlockMs after a connect
 				case 27:

+ 13 - 12
docs/index.html

@@ -106,7 +106,8 @@
       <div class="statgrid">
         <div class="stat"><div class="k">Delivered</div><div class="v" id="stRate">—</div></div>
         <div class="stat"><div class="k">New reports</div><div class="v" id="stNew">—</div></div>
-        <div class="stat logonly"><div class="k">Polls/s</div><div class="v" id="stPolls">—</div></div>
+        <div class="stat"><div class="k">Polls/s</div><div class="v" id="stPolls">—</div></div>
+        <div class="stat"><div class="k">RF fails/s (crc·noRx·heal)</div><div class="v" id="stRfFail">—</div></div>
         <div class="stat logonly"><div class="k">Loop period</div><div class="v" id="stLoop">—</div></div>
         <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>
@@ -121,10 +122,6 @@
       <div class="row" style="margin:14px 0 0;border-top:1px solid #272d3d;padding-top:14px">
         <strong style="flex-basis:100%">Connection tuning <span class="note">(persists across reboot)</span></strong>
       </div>
-      <div class="row" style="margin:8px 0 0"><label style="min-width:150px">Poll RX window</label>
-        <input type="range" id="rxWin" min="60" max="300" step="10"><span class="val" id="rxWinV"></span>
-        <span class="note" style="flex:1">How long each poll waits for the controller's reply. Wider (↑) catches replies that arrive late under heavy haptics (fewer drops); narrower (↓) allows a higher poll rate. Default 2000 µs.</span>
-      </div>
       <div class="row" style="margin:8px 0 0"><label style="min-width:150px">Block haptics on connect</label>
         <button id="hapBlockOn">off</button>
         <span class="note" style="flex:1">Drops all Steam haptics for a short window right after a controller (re)connects, so its haptic engine settles before the first real haptic.</span>
@@ -452,7 +449,11 @@ function applyBlob(p){
   // global stats
   $("#stRate").textContent = up? f1+" /s" : "—";
   $("#stNew").textContent = up? newps+" /s" : "—";
-  $("#stPolls").textContent = up? polls+" /s" : "—";
+  // Poll TX + RF-fail counts: shown ALWAYS (even when the link reads down) so a wedge is diagnosable --
+  // polls>0 here while Delivered is "—" means the puck is still polling but getting no usable replies.
+  $("#stPolls").textContent = polls+" /s";
+  { const crc=(p.length>113?p[113]:0), norx=(p.length>114?p[114]:0), heal=(p.length>115?p[115]:0);
+    $("#stRfFail").textContent = crc+" · "+norx+" · "+heal; }
   $("#stLoop").textContent = loopUs? loopUs+" µs" : "—";
   $("#stWorst").textContent = loopUs? (WORST[worstIdx]||worstIdx)+" "+worstUs+"µs" : "—";
   $("#stPollPer").textContent = up? (pollUs+" / "+pollIntended+" µs") : "—";
@@ -471,8 +472,8 @@ function applyBlob(p){
     if (el) el.textContent = "RESETREAS=0x"+raw.toString(16).padStart(8,"0");
   }
 
-  // Connection tuning: poll RX window /10us (p[110]), haptic-block enable (p[111]), block seconds (p[112]).
-  if (p.length>110) setSlider("rxWin", p[110]);
+  // Connection tuning: haptic-block enable (p[111]), block seconds (p[112]). (poll RX window p[110] is now
+  // fixed in firmware and no longer shown.)
   const hapBlockOn = (p.length>111?p[111]:0);
   $("#hapBlockOn").textContent = hapBlockOn?"on":"off"; $("#hapBlockOn").classList.toggle("active",!!hapBlockOn);
   if (p.length>112) setSlider("hapBlockS", p[112]);
@@ -510,7 +511,7 @@ function applyBlob(p){
   { const sel=$("#swGyro"); if(document.activeElement!==sel) sel.value=swGyro10; }
   for(const sel of document.querySelectorAll("select.chord")){ if(document.activeElement!==sel) sel.value=chord[+sel.dataset.i]; }
 }
-function fmtSlider(id,v){ return id==="rumble" ? v+"%" : id==="rxWin" ? (v*10)+" µs" : id==="hapBlockS" ? v+" s" : ""+v; }
+function fmtSlider(id,v){ return id==="rumble" ? v+"%" : id==="hapBlockS" ? v+" s" : ""+v; }
 function setSlider(id,v){ const el=$("#"+id); if(document.activeElement!==el){ el.value=v; } $("#"+id+"V").textContent=fmtSlider(id,el.value); }
 
 let inflight=false;
@@ -621,10 +622,10 @@ $("#factoryErase").onclick=async()=>{
   log("FACTORY ERASE sent — copycat is reformatting and rebooting to defaults. Re-pair the controller, then reconnect.");
 };
 $("#persistMode").onclick=()=>{ const on=$("#persistMode").classList.contains("active"); setField(16, on?0:1); };
-// Connection tuning: poll RX window /10us (field 25), post-connect haptic block on (field 27) + seconds (field 28)
+// Connection tuning: post-connect haptic block on (field 27) + seconds (field 28). (poll RX window is fixed.)
 $("#hapBlockOn").onclick=()=>{ const on=$("#hapBlockOn").classList.contains("active"); setField(27, on?0:1); };
-for(const id of ["mDiv","mFric","rumble","rxWin","hapBlockS"]){
-  const field={mDiv:1,mFric:2,rumble:22,rxWin:25,hapBlockS:28}[id];
+for(const id of ["mDiv","mFric","rumble","hapBlockS"]){
+  const field={mDiv:1,mFric:2,rumble:22,hapBlockS:28}[id];
   $("#"+id).addEventListener("input", ()=>{ $("#"+id+"V").textContent=fmtSlider(id,$("#"+id).value); });
   $("#"+id).addEventListener("change", ()=>setField(field, +$("#"+id).value));
 }