Jari 3 mesiacov pred
rodič
commit
2a549a705d

+ 18 - 0
OpenPuck/OpenPuck.ino

@@ -99,6 +99,24 @@ void usbReenumerate(uint8_t k)
 	USBDevice.attach();
 }
 
+// Persist a new USB personality and reboot into it. The clean DETACH before the reset is the point: a bare
+// NVIC_SystemReset drops the D+ pullup only for the ~ms the reset takes -- too briefly for some hosts to
+// register a real unplug -- so when switching between DIFFERENT personalities (e.g. Steam <-> Xbox, which are
+// distinct VID/PID + class drivers), the OUTGOING device can linger as a ghost until a physical replug, and
+// any input held at the instant of the switch stays stuck on it. Detaching first makes the reboot behave like
+// that replug: the host tears the old device down (releasing all its input) before the new one enumerates.
+// Used by every user-facing mode change (back-4 chord, WebUSB panel, CDC console).
+void modeSwitchReboot(uint8_t mode)
+{
+	if (modeValid(mode))
+		saveMode(mode); // 0xFF / invalid => keep the current mode (bond-import reboot)
+	if (USBDevice.mounted())
+		USBDevice.detach();
+	delay(60); // let the host see the disconnect before the pullup returns on reset
+	faultDiagArmIntentionalReset();
+	NVIC_SystemReset();
+}
+
 void setup()
 {
 	// full-board wipe (debug-only "erase everything"): if the panel armed one, this never returns -- it erases

+ 55 - 10
OpenPuck/mode_lizard.cpp

@@ -89,6 +89,20 @@ static uint32_t lizardButtons(const PuckInput &in)
 	return buttons;
 }
 
+// Per-slot glide/scroll integrators + last-sent report state for rfLizard. These are FILE-SCOPE (not
+// function-local statics) so rfLizardRelease() can reset them on a lizard->Steam handoff: without that reset
+// a key/mouse-button that was DOWN the instant Steam takes over is never released (the change-dedup below
+// thinks it is still "down") and sticks on the desktop until a reconnect/power-cycle.
+static int prx[NSLOT] = { 0 }, pry[NSLOT] = { 0 };
+static bool prt[NSLOT] = { false };
+static float vx[NSLOT] = { 0 }, vy[NSLOT] = { 0 }, rmx = 0, rmy = 0;
+static int ply[NSLOT] = { 0 };
+static bool plt[NSLOT] = { false };
+static float sacc = 0;
+static uint8_t pmbtn = 0; // last-sent mouse buttons
+static uint8_t prevCC = 0; // last-sent consumer bits
+static uint8_t pmod = 0, pkc[6] = { 0, 0, 0, 0, 0, 0 }; // last-sent kbd modifier + keycodes
+
 void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 	      uint8_t krid)
 {
@@ -110,10 +124,8 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 
 	// ---- right pad -> mouse motion with glide ----
 	// Per-slot velocity/last-position so each controller's pad integrates independently; the resulting
-	// deltas are SUMMED into one cursor (sub-pixel carry rmx/rmy is shared -- one mouse).
-	static int prx[NSLOT] = { 0 }, pry[NSLOT] = { 0 };
-	static bool prt[NSLOT] = { false };
-	static float vx[NSLOT] = { 0 }, vy[NSLOT] = { 0 }, rmx = 0, rmy = 0;
+	// deltas are SUMMED into one cursor (sub-pixel carry rmx/rmy is shared -- one mouse). State is
+	// file-scope (see the block above rfLizard) so rfLizardRelease() can zero it on a handoff.
 	int dx = 0, dy = 0;
 	if (doRpadMouse) {
 		float sumx = 0, sumy = 0;
@@ -165,9 +177,7 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 
 	// ---- left pad -> scroll wheel ----
 	// Per-slot last-position; accumulator shared (one wheel). Reset only when NO controller is scrolling.
-	static int ply[NSLOT] = { 0 };
-	static bool plt[NSLOT] = { false };
-	static float sacc = 0;
+	// State is file-scope (see the block above rfLizard) so rfLizardRelease() can zero it on a handoff.
 	int dw = 0;
 	if (doLpadScroll) {
 		bool anyTouch = false;
@@ -198,7 +208,6 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 	}
 
 	// ---- mouse report ----
-	static uint8_t pmbtn = 0;
 	if (dx || dy || dw || outMBtn != pmbtn) {
 		pmbtn = outMBtn;
 		hid_mouse_report_t m;
@@ -212,7 +221,6 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 	}
 
 	// ---- consumer control (edge-triggered) ----
-	static uint8_t prevCC = 0;
 	if (g_usbMode == MODE_LIZARD) {
 		if (consumerBits != prevCC) {
 			if (mdev->ready())
@@ -222,7 +230,6 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 	}
 
 	// ---- keyboard report ----
-	static uint8_t pmod = 0, pkc[6] = { 0, 0, 0, 0, 0, 0 };
 	bool chg = (outMod != pmod);
 	for (int i = 0; i < 6; i++)
 		if (outKeys[i] != pkc[i])
@@ -238,3 +245,41 @@ void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 			kdev->sendReport(krid, krep, 8);
 	}
 }
+
+// Release everything lizard may currently be holding on the host, then clear the last-sent dedup + glide
+// state so a later re-entry re-asserts from scratch. Called by puck_hid when Steam TAKES OVER (the
+// lizard->gamepad handoff): that transition is a pure runtime flip with NO USB re-enumeration, so a
+// keyboard key / mouse button / consumer key that was DOWN at that instant would otherwise stay latched on
+// the desktop until a reconnect or power-cycle (the reported "stuck input after switching modes"). Sends the
+// neutral reports unconditionally (not change-gated) because the whole point is to override a stale held
+// state that the dedup would suppress. Same direct-sendReport path rfLizard uses.
+void rfLizardRelease(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev,
+		     uint8_t mrid, uint8_t krid)
+{
+	uint8_t krep[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; // all keys + modifiers up
+	if (kdev->ready())
+		kdev->sendReport(krid, krep, 8);
+	hid_mouse_report_t m = { 0, 0, 0, 0, 0 }; // no buttons, no motion/wheel
+	if (mdev->ready())
+		mdev->sendReport(mrid, &m, sizeof m);
+	if (g_usbMode == MODE_LIZARD) {
+		uint8_t cc = 0; // release any held consumer-control key
+		if (mdev->ready())
+			mdev->sendReport(0x03, &cc, 1);
+	}
+	// Zero the integrators + last-sent shadows so re-entry starts clean (no cursor fling from stale
+	// velocity, and held keys/buttons re-send correctly on the next rfLizard pass).
+	for (int s = 0; s < NSLOT; s++) {
+		prx[s] = pry[s] = 0;
+		prt[s] = false;
+		vx[s] = vy[s] = 0;
+		ply[s] = 0;
+		plt[s] = false;
+	}
+	rmx = rmy = sacc = 0;
+	pmbtn = 0;
+	prevCC = 0;
+	pmod = 0;
+	for (int i = 0; i < 6; i++)
+		pkc[i] = 0;
+}

+ 7 - 0
OpenPuck/mode_lizard.h

@@ -20,3 +20,10 @@
 // object (puck mode sends both reports to hid[0]).
 void rfLizard(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev, uint8_t mrid,
 	      uint8_t krid);
+
+// Release any keyboard/mouse/consumer input lizard is currently holding on the host and reset rfLizard's
+// internal last-sent + glide state. Call on the lizard->Steam handoff (Steam takes over the gamepad with no
+// USB re-enumeration): without it a key/button held at that instant stays latched on the desktop until a
+// reconnect or power-cycle. mdev/kdev may be the same object (puck mode uses hid[0] for both).
+void rfLizardRelease(Adafruit_USBD_HID *mdev, Adafruit_USBD_HID *kdev,
+		     uint8_t mrid, uint8_t krid);

+ 28 - 0
OpenPuck/puck_hid.cpp

@@ -885,6 +885,34 @@ void SteamPuckController::task()
 	// no periodic 0x79/0x7B while the host sleeps -- those sends can wake it too
 	if (USBDevice.suspended())
 		return;
+
+	// Lizard<->Steam handoff: release whatever the OUTGOING path was holding. lizardActive() is a pure
+	// runtime decision -- Steam opening/closing (its ~3s settings heartbeat starting/stopping) flips it with
+	// NO USB re-enumeration -- so a key/mouse-button held when Steam TAKES OVER, or a gamepad button held
+	// when Steam CLOSES, is otherwise never released and sticks on the host until a reconnect or power-cycle
+	// (the reported "stuck input after switching modes"). The link-drop neutral in rf_link only covers an RF
+	// outage, not this handoff. Fire a neutral through the path we're leaving on each edge. Placed after the
+	// suspend early-return: while suspended nothing can be sent, and a flip across a sleep is covered on
+	// resume (this edge fires once, plus the post-resume input mute). MODE_LIZARD never leaves lizard, so
+	// this is inert there.
+	{
+		static bool wasLizard = lizardActive();
+		bool nowLizard = lizardActive();
+		if (nowLizard != wasLizard) {
+			if (wasLizard) {
+				// leaving lizard -> release held desktop keyboard/mouse/consumer
+				rfLizardRelease(&hid[0], &hid[0], 0x40, 0x41);
+			} else {
+				// leaving gamepad-forward -> release held 0x45 buttons/sticks/triggers on every slot
+				static const uint8_t neutral45[45] = { 0 };
+				for (int s = 0; s < NSLOT; s++)
+					if (g_slot[s].used && hid[s].ready())
+						usbTxHid(&hid[s], 0x45, neutral45,
+							 sizeof neutral45);
+			}
+			wasLizard = nowLizard;
+		}
+	}
 	// Per-slot 0x79/0x7B: each connected slot reports its OWN edge and its OWN status. State arrays are
 	// per-slot so each controller's "connected" edge fires once and is re-sent only until Steam acks THAT
 	// slot. The real puck's per-slot edge-triggered 0x79 prevents re-triggering Steam's connect-chime loop.

+ 4 - 4
OpenPuck/rf_link.cpp

@@ -8,6 +8,7 @@
 #include "controllers.h"
 #include "status_led.h"
 #include "fault_diag.h"
+#include "usb_mount.h" // modeSwitchReboot()
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <string.h>
@@ -720,10 +721,9 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 						    want != g_usbMode &&
 						    modeValid(want) &&
 						    !USBDevice.suspended()) {
-							saveMode(want);
-							delay(40);
-							faultDiagArmIntentionalReset();
-							NVIC_SystemReset();
+							// clean detach + reboot into the new mode (releases any held
+							// input on the outgoing device -- see modeSwitchReboot)
+							modeSwitchReboot(want);
 						}
 					} else {
 						chWant[g_curSlot] = want;

+ 3 - 4
OpenPuck/serial_console.cpp

@@ -7,6 +7,7 @@
 #include "config.h"
 #include "puck_hid.h" // g_cmdCapture (feature-command capture toggle)
 #include "fault_diag.h"
+#include "usb_mount.h" // modeSwitchReboot()
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <stdlib.h>
@@ -185,10 +186,8 @@ void serialConsolePoll()
 							"# switch mode %u (reboot)\n",
 							m);
 						delay(20);
-						saveMode(m);
-						delay(40);
-						faultDiagArmIntentionalReset();
-						NVIC_SystemReset();
+						// clean detach + reboot (releases held input on the outgoing device)
+						modeSwitchReboot(m);
 					}
 				}
 			} else if (line[0] == 'c') {

+ 6 - 0
OpenPuck/usb_mount.h

@@ -36,3 +36,9 @@ void usbMountTask();
 // (fixed interfaces -- WebUSB / wake mouse -- replayed too), then re-attach. NO MCU reboot. Call with the map
 // already rebuilt for k connected controllers.
 void usbReenumerate(uint8_t k);
+
+// Persist `mode` and reboot into it, cleanly detaching USB first so the host tears down the outgoing
+// personality (releasing any held input) before the new one enumerates -- otherwise the ~ms reset-disconnect
+// can be too brief for the host to notice, leaving a ghost device with stuck input until a physical replug.
+// Does not return. Call for every user-facing mode switch (chord / panel / console).
+void modeSwitchReboot(uint8_t mode);

+ 8 - 10
OpenPuck/webusb_config.cpp

@@ -10,6 +10,7 @@
 #include "fault_diag.h"
 #include "fw_update.h"
 #include "usb_tx.h"
+#include "usb_mount.h" // modeSwitchReboot()
 #include <Arduino.h>
 #include <string.h>
 
@@ -733,12 +734,10 @@ void webusbPoll()
 			} else if (op == 0x0E) {
 				uint8_t mode = buf[1];
 				saveBonds();
-				if (modeValid(mode))
-					saveMode(mode);
 				usb_web.flush();
-				delay(40);
-				faultDiagArmIntentionalReset();
-				NVIC_SystemReset();
+				// clean detach + reboot into `mode` (0xFF = keep current); regenerates RF
+				// session addrs from the imported bonds and releases any held input.
+				modeSwitchReboot(mode);
 
 				// ---- lizard (desktop) binding map editor ----
 				// Renumbered to 0x11..0x15 on the merge with main: main already owns 0x0D (bond-slot
@@ -977,12 +976,11 @@ void webusbPoll()
 				uint8_t m = buf[1];
 				if (modeValid(m) && !USBDevice.suspended()) {
 					// best-effort status to the panel before the reboot; the SOF drain
-					// sends it during the delay(40) below (no blocking flush on loop()).
+					// sends it during the flush below (no blocking flush on loop()).
 					g_blobRequest = true;
-					saveMode(m);
-					delay(40);
-					faultDiagArmIntentionalReset();
-					NVIC_SystemReset();
+					usb_web.flush();
+					// clean detach + reboot (releases held input on the outgoing device)
+					modeSwitchReboot(m);
 				}
 			}
 			memmove(buf, buf + need, n - need);