Преглед на файлове

chord working instantly

Jari преди 3 месеца
родител
ревизия
78778843a3

+ 18 - 34
PicoPuck/src/puck/chord.c

@@ -1,5 +1,10 @@
 // chord.c — button chord → USB mode switch (see chord.h).
 //
+// Checked once per freshly-decoded BT report (raw g_in). The guard is a
+// deliberate 5-button combination (both triggers + both bumpers, or the four
+// back paddles, plus a face button), so it fires immediately on detection — no
+// hold. On-change pads (Xbox) therefore switch on the single press report.
+//
 // SPDX-License-Identifier: AGPL-3.0-or-later
 #include "puck/chord.h"
 #include "puck/triton.h"
@@ -16,11 +21,6 @@
 #define CHORD_X MODE_HIDGYRO
 #define CHORD_Y MODE_SW_HORI
 
-#define CHORD_HOLD 12  // consecutive matching reports before switching
-
-static uint8_t s_cnt[PP_NSLOT];
-static uint8_t s_want[PP_NSLOT];
-
 static void mode_switch_reboot(uint8_t mode)
 {
 	settings_set_mode(mode);
@@ -29,42 +29,26 @@ static void mode_switch_reboot(uint8_t mode)
 	}
 }
 
-void chord_check(int slot, bool is_sc2)
+void chord_note(int slot)
 {
 	if (slot < 0 || slot >= PP_NSLOT)
 		return;
 	uint32_t b = g_in[slot].buttons;
-	uint32_t guard = is_sc2 ? (TB_R4 | TB_L4 | TB_R5 | TB_L5)
-				: (TB_L2 | TB_R2 | TB_LB | TB_RB);
-
-	if ((b & guard) != guard) {
-		s_cnt[slot] = 0;
+	const uint32_t back4 = TB_L4 | TB_R4 | TB_L5 | TB_R5;
+	const uint32_t trig = TB_L2 | TB_R2 | TB_LB | TB_RB;
+	// Either guard works for any controller: the four back paddles (SC2) OR
+	// L2+R2+LB+RB (every pad has these).
+	if (((b & back4) != back4) && ((b & trig) != trig))
 		return;
-	}
 
-	// Guard held → a face button selects the target.
 	uint8_t want = 0xFF;
-	if (b & TB_A)
-		want = MODE_STEAM;
-	else if (b & TB_B)
-		want = CHORD_B;
-	else if (b & TB_X)
-		want = CHORD_X;
-	else if (b & TB_Y)
-		want = CHORD_Y;
+	if (b & TB_A) want = MODE_STEAM;
+	else if (b & TB_B) want = CHORD_B;
+	else if (b & TB_X) want = CHORD_X;
+	else if (b & TB_Y) want = CHORD_Y;
 
-	// Don't leak the chord to the host.
-	g_in[slot].buttons &= ~(guard | TB_A | TB_B | TB_X | TB_Y);
+	if (want == 0xFF || want == settings_mode())
+		return;  // guard held but no (new) target picked → pass through
 
-	if (want == 0xFF || want == settings_mode()) {
-		s_cnt[slot] = 0;
-		return;
-	}
-	if (want != s_want[slot]) {
-		s_want[slot] = want;
-		s_cnt[slot] = 1;
-		return;
-	}
-	if (++s_cnt[slot] >= CHORD_HOLD)
-		mode_switch_reboot(want);
+	mode_switch_reboot(want);  // deliberate combo → switch immediately
 }

+ 5 - 7
PicoPuck/src/puck/chord.h

@@ -6,12 +6,10 @@
 
 #include <stdbool.h>
 
-// Check the mode-switch chord against g_in[slot] (call after each input decode).
-// Guard: an SC2 uses the four back paddles (like OpenPuck); a generic pad uses
-// L2+R2+LB+RB (which every pad has). While the guard is held a face button picks
-// the target (A→Steam, B/X/Y→configured modes) and the guard+face bits are
-// masked out of g_in so they don't leak to the host. A stable selection reboots
-// into the new mode.
-void chord_check(int slot, bool is_sc2);
+// Call ONCE per freshly-decoded BT report (from the input path, on raw g_in).
+// Either guard works for any controller: the four back paddles (SC2) OR
+// L2+R2+LB+RB. A face button picks the target (A→Steam, B→PS5, X→DS4, Y→Switch)
+// and the mode switches immediately (persist + reboot) — no hold needed.
+void chord_note(int slot);
 
 #endif // PICOPUCK_CHORD_H

+ 18 - 20
PicoPuck/src/puck/emu_present.c

@@ -5,6 +5,7 @@
 #include "puck/emu.h"
 #include "puck/slots.h"
 #include "usb/usb_tx.h"
+#include "usb/usb_descriptors.h"
 #include "config/modes.h"
 #include "sys/settings.h"
 
@@ -14,15 +15,9 @@
 
 static uint32_t s_last_ms;
 
-// First connected slot, or 0 (present a neutral controller when nothing is on).
-static int active_slot(void)
-{
-	for (int s = 0; s < PP_NSLOT; s++)
-		if (g_slot[s].connected)
-			return s;
-	return 0;
-}
-
+// Interface i maps 1:1 to controller slot i. Each emulated HID interface streams
+// its slot's g_in (neutral when that slot is disconnected, keeping the interface
+// alive as an idle pad). Report callbacks route the same way.
 void emu_present_task(void)
 {
 	const emu_mode_t *e = emu_mode_for(settings_mode());
@@ -33,26 +28,29 @@ void emu_present_task(void)
 		return;
 	s_last_ms = t;
 
-	uint8_t body[64];
-	uint8_t rid = 0;
-	uint16_t n = e->build(active_slot(), body, &rid);
-	if (n)
-		usb_tx_hid(0, rid, body, n);
+	uint8_t nif = usb_emu_iface_count();
+	for (uint8_t i = 0; i < nif; i++) {
+		uint8_t body[64];
+		uint8_t rid = 0;
+		uint16_t n = e->build(i, body, &rid);
+		if (n)
+			usb_tx_hid(i, rid, body, n);
+	}
 }
 
-uint16_t emu_get_report(uint8_t report_id, uint8_t type, uint8_t *buf,
-			uint16_t reqlen)
+uint16_t emu_get_report(uint8_t instance, uint8_t report_id, uint8_t type,
+			uint8_t *buf, uint16_t reqlen)
 {
 	const emu_mode_t *e = emu_mode_for(settings_mode());
 	if (e && e->get_report)
-		return e->get_report(active_slot(), report_id, type, buf, reqlen);
+		return e->get_report(instance, report_id, type, buf, reqlen);
 	return 0;
 }
 
-void emu_set_report(uint8_t report_id, uint8_t type, const uint8_t *buf,
-		    uint16_t len)
+void emu_set_report(uint8_t instance, uint8_t report_id, uint8_t type,
+		    const uint8_t *buf, uint16_t len)
 {
 	const emu_mode_t *e = emu_mode_for(settings_mode());
 	if (e && e->set_report)
-		e->set_report(active_slot(), report_id, type, buf, len);
+		e->set_report(instance, report_id, type, buf, len);
 }

+ 6 - 5
PicoPuck/src/puck/emu_present.h

@@ -10,10 +10,11 @@
 void emu_present_task(void);
 
 // HID feature/output dispatch for emulated modes (routed from the shared
-// tud_hid callbacks when not in a puck mode).
-uint16_t emu_get_report(uint8_t report_id, uint8_t type, uint8_t *buf,
-			uint16_t reqlen);
-void emu_set_report(uint8_t report_id, uint8_t type, const uint8_t *buf,
-		    uint16_t len);
+// tud_hid callbacks when not in a puck mode). `instance` is the HID interface,
+// which maps 1:1 to the controller slot.
+uint16_t emu_get_report(uint8_t instance, uint8_t report_id, uint8_t type,
+			uint8_t *buf, uint16_t reqlen);
+void emu_set_report(uint8_t instance, uint8_t report_id, uint8_t type,
+		    const uint8_t *buf, uint16_t len);
 
 #endif // PICOPUCK_EMU_PRESENT_H

+ 66 - 59
PicoPuck/src/puck/mode_switch_pro.c

@@ -95,11 +95,17 @@ static uint32_t code_to_jc(uint8_t c, uint32_t fA, uint32_t fB, uint32_t fX,
 	}
 }
 
-// Single controller MAC (one presented interface). OUI 7C:BB:8A like a genuine
-// Pro Controller; the console reads it via subcommand 0x02 to identify the pad.
-static const uint8_t g_jcMac[6] = { 0x7C, 0xBB, 0x8A, 0x00, 0x00, 0x10 };
-static uint8_t g_jcTimer;
-static uint8_t g_reportMode;  // 0 until subcommand 0x03 selects 0x30
+// Per-slot handshake state — each presented Pro Controller runs its own init
+// handshake and needs a distinct MAC (OUI 7C:BB:8A, last byte 0x10+slot) so the
+// console treats them as separate pads.
+static void jc_mac(int slot, uint8_t out[6])
+{
+	static const uint8_t base[5] = { 0x7C, 0xBB, 0x8A, 0x00, 0x00 };
+	memcpy(out, base, 5);
+	out[5] = (uint8_t)(0x10 + slot);
+}
+static uint8_t g_jcTimer[PP_NSLOT];
+static uint8_t g_reportMode[PP_NSLOT];  // 0 until subcommand 0x03 selects 0x30
 
 // ---- HD-rumble amplitude decoder (see OpenPuck for the full protocol notes).
 enum { HDR_AMP_MIN = -256, HDR_AMP_OFF = -256 };
@@ -136,19 +142,16 @@ static void hdr_build_levels(void)
 	}
 	g_hdr_built = true;
 }
-static int16_t g_band_lo[2], g_band_hi[2];  // running amplitudes per motor (0=L,1=R)
-static void hdr_reset(void)
-{
-	g_band_lo[0] = g_band_hi[0] = HDR_AMP_OFF;
-	g_band_lo[1] = g_band_hi[1] = HDR_AMP_OFF;
-}
+// Running amplitudes per slot, per motor (0=L,1=R); packed 5-bit commands are
+// relative, so this must persist between frames — and be independent per pad.
+static int16_t g_band_lo[PP_NSLOT][2], g_band_hi[PP_NSLOT][2];
 static uint8_t hdr_field(uint32_t w, uint8_t shift, uint8_t width)
 {
 	return (uint8_t)((w >> shift) & ((1u << width) - 1u));
 }
-static uint16_t hdr_decode(uint8_t motor, const uint8_t b[4])
+static uint16_t hdr_decode(int slot, uint8_t motor, const uint8_t b[4])
 {
-	int16_t *lo = &g_band_lo[motor], *hi = &g_band_hi[motor];
+	int16_t *lo = &g_band_lo[slot][motor], *hi = &g_band_hi[slot][motor];
 	uint32_t w = (uint32_t)b[0] | ((uint32_t)b[1] << 8) |
 		     ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);
 	uint16_t peak = 0;
@@ -226,16 +229,16 @@ static uint16_t hdr_decode(uint8_t motor, const uint8_t b[4])
 #undef HDR_SAMPLE
 	return peak;
 }
-static uint16_t g_jc_last_lo, g_jc_last_hi;
+static uint16_t g_jc_last_lo[PP_NSLOT], g_jc_last_hi[PP_NSLOT];
 static void jc_rumble(int slot, const uint8_t *p, uint16_t pn)
 {
 	if (pn < 9) return;  // [timer][left x4][right x4]
 	hdr_build_levels();
-	uint16_t lo = hdr_decode(0, p + 1), hi = hdr_decode(1, p + 5);
-	if (lo == g_jc_last_lo && hi == g_jc_last_hi)
+	uint16_t lo = hdr_decode(slot, 0, p + 1), hi = hdr_decode(slot, 1, p + 5);
+	if (lo == g_jc_last_lo[slot] && hi == g_jc_last_hi[slot])
 		return;  // Switch streams rumble every frame; relay only on change
-	g_jc_last_lo = lo;
-	g_jc_last_hi = hi;
+	g_jc_last_lo[slot] = lo;
+	g_jc_last_hi[slot] = hi;
 	puck_rumble(slot, lo, hi);
 }
 
@@ -307,7 +310,7 @@ static void jc_input_prefix(int slot, uint8_t *out)
 	if (b & TB_L5) jc |= code_to_jc(t->back[2], fA, fB, fX, fY);
 	if (b & TB_R5) jc |= code_to_jc(t->back[3], fA, fB, fX, fY);
 	if (qam) jc |= code_to_jc(t->qam, fA, fB, fX, fY);
-	out[0] = g_jcTimer++;
+	out[0] = g_jcTimer[slot]++;
 	uint8_t chg = (g_battery_state[slot] == 1) ? 0x10 : 0x00;
 	out[1] = (uint8_t)((jc_battery_nibble(slot) << 4) | chg);
 	out[2] = (uint8_t)(jc);
@@ -357,22 +360,23 @@ static void jc_build_stick_cal(void)
 	g_stick_cal_built = true;
 }
 
-// User-cal SPI mirror (0x8000-0x80FF), RAM only. 0xFF = blank → factory fallback.
-static uint8_t g_user_cal[0x100];
-static bool g_user_cal_init;
-static void jc_spi_write(uint32_t addr, uint8_t len, const uint8_t *data, uint16_t avail)
+// Per-slot user-cal SPI mirror (0x8000-0x80FF), RAM only. 0xFF = blank → factory
+// fallback. Each Pro Controller has its own motion-cal region.
+static uint8_t g_user_cal[PP_NSLOT][0x100];
+static bool g_user_cal_init[PP_NSLOT];
+static void jc_spi_write(int slot, uint32_t addr, uint8_t len, const uint8_t *data, uint16_t avail)
 {
 	for (uint8_t i = 0; i < len && i < avail; i++) {
 		uint32_t a = addr + i;
 		if (a >= 0x8000 && a < 0x8100)
-			g_user_cal[a - 0x8000] = data[i];
+			g_user_cal[slot][a - 0x8000] = data[i];
 	}
 }
-static void spi_read(uint32_t addr, uint8_t len, uint8_t *dst)
+static void spi_read(int slot, uint32_t addr, uint8_t len, uint8_t *dst)
 {
-	if (!g_user_cal_init) {
-		memset(g_user_cal, 0xFF, sizeof(g_user_cal));
-		g_user_cal_init = true;
+	if (!g_user_cal_init[slot]) {
+		memset(g_user_cal[slot], 0xFF, sizeof(g_user_cal[slot]));
+		g_user_cal_init[slot] = true;
 	}
 	for (uint8_t i = 0; i < len; i++) {
 		uint32_t a = addr + i;
@@ -382,7 +386,7 @@ static void spi_read(uint32_t addr, uint8_t len, uint8_t *dst)
 		else if (a >= 0x6050 && a < 0x6050 + 13) v = SPI_COLOR[a - 0x6050];
 		else if (a >= 0x6080 && a < 0x6080 + 24) v = SPI_PARAMS1[a - 0x6080];
 		else if (a >= 0x6098 && a < 0x6098 + 18) v = SPI_PARAMS2[a - 0x6098];
-		else if (a >= 0x8000 && a < 0x8100) v = g_user_cal[a - 0x8000];
+		else if (a >= 0x8000 && a < 0x8100) v = g_user_cal[slot][a - 0x8000];
 		dst[i] = v;
 	}
 }
@@ -393,27 +397,29 @@ static const uint8_t BT_PAIR_2[31] = {
 	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 };
 
-// ---- reply FIFO (single interface) -----------------------------------------
+// ---- per-slot reply FIFO ----------------------------------------------------
 #define JC_REPLEN 63
 #define JCQ_N 8
 typedef struct { uint8_t rid; uint8_t data[JC_REPLEN]; } jc_rep_t;
-static jc_rep_t g_jcq[JCQ_N];
-static uint8_t g_jcq_h, g_jcq_t;
-static void jc_enq(uint8_t rid, const uint8_t *d, uint8_t len)
+static jc_rep_t g_jcq[PP_NSLOT][JCQ_N];
+static uint8_t g_jcq_h[PP_NSLOT], g_jcq_t[PP_NSLOT];
+static void jc_enq(int slot, uint8_t rid, const uint8_t *d, uint8_t len)
 {
-	uint8_t nt = (uint8_t)((g_jcq_t + 1) % JCQ_N);
-	if (nt == g_jcq_h) return;  // full → drop; host re-requests on timeout
+	uint8_t nt = (uint8_t)((g_jcq_t[slot] + 1) % JCQ_N);
+	if (nt == g_jcq_h[slot]) return;  // full → drop; host re-requests on timeout
 	if (len > JC_REPLEN) len = JC_REPLEN;
-	g_jcq[g_jcq_t].rid = rid;
-	memset(g_jcq[g_jcq_t].data, 0, JC_REPLEN);
-	memcpy(g_jcq[g_jcq_t].data, d, len);
-	g_jcq_t = nt;
+	g_jcq[slot][g_jcq_t[slot]].rid = rid;
+	memset(g_jcq[slot][g_jcq_t[slot]].data, 0, JC_REPLEN);
+	memcpy(g_jcq[slot][g_jcq_t[slot]].data, d, len);
+	g_jcq_t[slot] = nt;
 }
 
 // Build the 0x21 reply (input prefix + ACK + echoed subcommand + reply data).
 static void jc_subcmd(int slot, uint8_t sub, const uint8_t *args, uint16_t alen)
 {
 	uint8_t p[JC_REPLEN];
+	uint8_t mac[6];
+	jc_mac(slot, mac);
 	memset(p, 0, sizeof(p));
 	jc_input_prefix(slot, p);
 	p[13] = sub;
@@ -423,7 +429,7 @@ static void jc_subcmd(int slot, uint8_t sub, const uint8_t *args, uint16_t alen)
 		p[12] = 0x81;
 		if (ty == 1) {
 			p[14] = 0x01;
-			memcpy(&p[15], g_jcMac, 6);
+			memcpy(&p[15], mac, 6);
 			p[21] = 0x00; p[22] = 0x25; p[23] = 0x08;
 			static const uint8_t NAME[14] = {
 				0x50, 0x72, 0x6F, 0x20, 0x43, 0x6F, 0x6E,
@@ -443,7 +449,7 @@ static void jc_subcmd(int slot, uint8_t sub, const uint8_t *args, uint16_t alen)
 		p[15] = 0x48;
 		p[16] = 0x03;  // Pro Controller
 		p[17] = 0x02;
-		memcpy(&p[18], g_jcMac, 6);
+		memcpy(&p[18], mac, 6);
 		p[24] = 0x01;
 		p[25] = 0x01;
 		break;
@@ -456,21 +462,21 @@ static void jc_subcmd(int slot, uint8_t sub, const uint8_t *args, uint16_t alen)
 		p[12] = 0x90;
 		p[14] = args[0]; p[15] = args[1]; p[16] = args[2]; p[17] = args[3];
 		p[18] = rl;
-		spi_read(a, rl, &p[19]);
+		spi_read(slot, a, rl, &p[19]);
 		break;
 	}
 	case 0x11:  // SPI flash write → mirror user cal
 		if (alen >= 5) {
 			uint32_t a = (uint32_t)args[0] | ((uint32_t)args[1] << 8) |
 				     ((uint32_t)args[2] << 16) | ((uint32_t)args[3] << 24);
-			jc_spi_write(a, args[4], &args[5],
+			jc_spi_write(slot, a, args[4], &args[5],
 				     (alen > 5) ? (uint16_t)(alen - 5) : 0);
 		}
 		p[12] = 0x80;
 		break;
 	case 0x03:  // set input report mode
 		if (alen >= 1 && args[0] == 0x30)
-			g_reportMode = 0x30;
+			g_reportMode[slot] = 0x30;
 		p[12] = 0x80;
 		break;
 	case 0x04:  // trigger elapsed time
@@ -484,7 +490,7 @@ static void jc_subcmd(int slot, uint8_t sub, const uint8_t *args, uint16_t alen)
 		p[12] = 0x80;  // generic ACK
 		break;
 	}
-	jc_enq(0x21, p, JC_REPLEN);
+	jc_enq(slot, 0x21, p, JC_REPLEN);
 }
 
 // ---- emu interface ---------------------------------------------------------
@@ -502,15 +508,16 @@ static void swpro_set(int slot, uint8_t rid, uint8_t type, const uint8_t *b, uin
 	if (id == 0x80) {  // USB handshake
 		if (pn < 1) return;
 		if (p[0] == 0x01) {
-			uint8_t d[9] = { 0x01, 0x00, 0x03, g_jcMac[0], g_jcMac[1],
-					 g_jcMac[2], g_jcMac[3], g_jcMac[4], g_jcMac[5] };
-			jc_enq(0x81, d, 9);
+			uint8_t mac[6]; jc_mac(slot, mac);
+			uint8_t d[9] = { 0x01, 0x00, 0x03, mac[0], mac[1],
+					 mac[2], mac[3], mac[4], mac[5] };
+			jc_enq(slot, 0x81, d, 9);
 		} else if (p[0] == 0x02) {
 			uint8_t d[1] = { 0x02 };
-			jc_enq(0x81, d, 1);
+			jc_enq(slot, 0x81, d, 1);
 		} else if (p[0] == 0x03) {
 			uint8_t d[1] = { 0x03 };
-			jc_enq(0x81, d, 1);
+			jc_enq(slot, 0x81, d, 1);
 		}
 		return;
 	}
@@ -554,26 +561,26 @@ static uint16_t swpro_build(int slot, uint8_t *out, uint8_t *rid)
 {
 	jc_build_stick_cal();
 	// Drain one queued handshake/subcommand reply per cycle (ordered), ahead of
-	// any streamed input, so a bursty host-init isn't starved.
-	if (g_jcq_h != g_jcq_t) {
-		jc_rep_t *r = &g_jcq[g_jcq_h];
+	// any streamed input, so a bursty host-init isn't starved. Per-slot FIFO.
+	if (g_jcq_h[slot] != g_jcq_t[slot]) {
+		jc_rep_t *r = &g_jcq[slot][g_jcq_h[slot]];
 		*rid = r->rid;
 		memcpy(out, r->data, JC_REPLEN);
-		g_jcq_h = (uint8_t)((g_jcq_h + 1) % JCQ_N);
+		g_jcq_h[slot] = (uint8_t)((g_jcq_h[slot] + 1) % JCQ_N);
 		return JC_REPLEN;
 	}
-	if (g_reportMode != 0x30)
+	if (g_reportMode[slot] != 0x30)
 		return 0;  // not until the host selects report mode 0x30
 	// Rate-limit the 0x30 stream by the configured Switch cadence (the console
-	// integrates 3 IMU samples/report at a fixed ~5 ms/sample).
-	static uint32_t last_ms;
+	// integrates 3 IMU samples/report at a fixed ~5 ms/sample). Per-slot timer.
+	static uint32_t last_ms[PP_NSLOT];
 	uint8_t rate = settings()->sw_pro_rate;
 	uint32_t interval = (rate == 2) ? SW_STREAM_MS :
 			    (rate == 1) ? SW_PRO_REPORT_MS_120 : SW_PRO_REPORT_MS;
 	uint32_t t = to_ms_since_boot(get_absolute_time());
-	if (t - last_ms < interval)
+	if (t - last_ms[slot] < interval)
 		return 0;
-	last_ms = t;
+	last_ms[slot] = t;
 	*rid = 0x30;
 	swpro_build_30(slot, out);
 	return 63;

+ 4 - 4
PicoPuck/src/puck/personality.c

@@ -106,7 +106,7 @@ void puck_present_synth(int slot)
 {
 	if (slot < 0 || slot >= PP_NSLOT)
 		return;
-	chord_check(slot, false);  // generic pad: L2+R2+LB+RB guard
+	chord_note(slot);  // detect + mask the mode-switch chord on this fresh report
 	s_src[slot] = 1;
 	slot_note_input(slot, now_ms());
 	// Only the puck personality emits the SC2 report; in an emulated mode the
@@ -119,7 +119,7 @@ void puck_present_raw(int slot, const uint8_t *rep, uint8_t len)
 {
 	if (slot < 0 || slot >= PP_NSLOT || len < 1)
 		return;
-	chord_check(slot, true);  // SC2: back-paddle guard
+	chord_note(slot);
 	s_src[slot] = 2;
 	slot_note_input(slot, now_ms());
 	if (mode_is_puck(settings_mode()))
@@ -336,7 +336,7 @@ uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
 			       uint16_t reqlen)
 {
 	if (!mode_is_puck(settings_mode()))
-		return emu_get_report(report_id, report_type, buffer, reqlen);
+		return emu_get_report(instance, report_id, report_type, buffer, reqlen);
 	return handle_get(instance, report_id, report_type, buffer, reqlen);
 }
 
@@ -345,7 +345,7 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
 			   uint16_t bufsize)
 {
 	if (!mode_is_puck(settings_mode())) {
-		emu_set_report(report_id, report_type, buffer, bufsize);
+		emu_set_report(instance, report_id, report_type, buffer, bufsize);
 		return;
 	}
 	handle_set(instance, report_id, report_type, buffer, bufsize);

+ 43 - 28
PicoPuck/src/usb/usb_descriptors.c

@@ -28,47 +28,52 @@ void webusb_set_connected(bool connected);
 static uint8_t s_mode;
 static const emu_mode_t *s_emu;  // NULL for puck/xinput modes
 static bool s_xinput;            // MODE_XBOX: custom vendor class, not HID
+static uint8_t s_emu_ifaces = 1; // # of emulated HID interfaces (one per controller)
 static tusb_desc_device_t s_dev;
-static uint8_t s_emu_cfg[80];
+static uint8_t s_emu_cfg[220];   // PP_NSLOT HID (32 B each) + config + vendor
 static uint8_t s_vendor_itf;
 
+uint8_t usb_emu_iface_count(void) { return s_emu_ifaces; }
+
 #define MS_OS_20_DESC_LEN 0xB2
 static uint8_t s_msos[MS_OS_20_DESC_LEN];
 
-// Build the emulated-mode config descriptor: HID gamepad (itf 0, IN-only) plus
-// the WebUSB vendor interface (itf 1, bulk IN/OUT). Returns total length.
-static uint16_t build_emu_config(uint8_t *b, const emu_mode_t *emu)
+// Build the emulated-mode config descriptor: `nif` HID gamepad interfaces (one
+// per controller slot, each with an IN + OUT endpoint) followed by the WebUSB
+// vendor interface. Presenting several HID interfaces is what lets more than one
+// connected controller appear to the host at once (each interface = one pad).
+// Returns total length. HID endpoints: IN 0x81+i / OUT 0x01+i; vendor 0x85/0x05.
+static uint16_t build_emu_config(uint8_t *b, const emu_mode_t *emu, uint8_t nif)
 {
 	uint16_t rlen = emu->report_desc_len;
 	uint8_t poll = emu->poll_ms ? emu->poll_ms : 1;
-	// itf0 HID (2 endpoints: IN + OUT) + itf1 vendor (2 endpoints).
-	const uint16_t total = 9 + 9 + 9 + 7 + 7 + 9 + 7 + 7;  // 64
+	if (nif < 1) nif = 1;
+	if (nif > PP_NSLOT) nif = PP_NSLOT;
+	const uint16_t total = 9 + nif * (9 + 9 + 7 + 7) + (9 + 7 + 7);
 	uint8_t *p = b;
 	// configuration
 	*p++ = 9; *p++ = TUSB_DESC_CONFIGURATION;
 	*p++ = (uint8_t)(total & 0xFF); *p++ = (uint8_t)(total >> 8);
-	*p++ = 2; *p++ = 1; *p++ = 0; *p++ = 0x80; *p++ = 250;
-	// HID interface (itf 0) — IN + OUT so rumble / Switch-Pro subcommands ride
-	// the interrupt OUT pipe (hosts like hid-nintendo require the OUT endpoint;
-	// output reports still also work via EP0 SET_REPORT).
-	*p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = 0; *p++ = 0; *p++ = 2;
-	*p++ = TUSB_CLASS_HID; *p++ = 0; *p++ = 0; *p++ = 0;
-	// HID descriptor
-	*p++ = 9; *p++ = HID_DESC_TYPE_HID; *p++ = 0x11; *p++ = 0x01; *p++ = 0;
-	*p++ = 1; *p++ = HID_DESC_TYPE_REPORT;
-	*p++ = (uint8_t)(rlen & 0xFF); *p++ = (uint8_t)(rlen >> 8);
-	// HID IN endpoint
-	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x81; *p++ = 0x03;
-	*p++ = 64; *p++ = 0; *p++ = poll;
-	// HID OUT endpoint
-	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x01; *p++ = 0x03;
-	*p++ = 64; *p++ = 0; *p++ = poll;
-	// vendor interface (itf 1) — WebUSB
-	*p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = 1; *p++ = 0; *p++ = 2;
+	*p++ = (uint8_t)(nif + 1); *p++ = 1; *p++ = 0; *p++ = 0x80; *p++ = 250;
+	for (uint8_t i = 0; i < nif; i++) {
+		// HID interface i — IN + OUT (rumble / Switch-Pro subcommands ride the
+		// interrupt OUT pipe; output reports also work via EP0 SET_REPORT).
+		*p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = i; *p++ = 0; *p++ = 2;
+		*p++ = TUSB_CLASS_HID; *p++ = 0; *p++ = 0; *p++ = 0;
+		*p++ = 9; *p++ = HID_DESC_TYPE_HID; *p++ = 0x11; *p++ = 0x01; *p++ = 0;
+		*p++ = 1; *p++ = HID_DESC_TYPE_REPORT;
+		*p++ = (uint8_t)(rlen & 0xFF); *p++ = (uint8_t)(rlen >> 8);
+		*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = (uint8_t)(0x81 + i); *p++ = 0x03;
+		*p++ = 64; *p++ = 0; *p++ = poll;
+		*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = (uint8_t)(0x01 + i); *p++ = 0x03;
+		*p++ = 64; *p++ = 0; *p++ = poll;
+	}
+	// vendor interface (last) — WebUSB
+	*p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = nif; *p++ = 0; *p++ = 2;
 	*p++ = TUSB_CLASS_VENDOR_SPECIFIC; *p++ = 0; *p++ = 0; *p++ = 0;
-	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x02; *p++ = 0x02;
+	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x05; *p++ = 0x02;
 	*p++ = 64; *p++ = 0; *p++ = 0;
-	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x82; *p++ = 0x02;
+	*p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x85; *p++ = 0x02;
 	*p++ = 64; *p++ = 0; *p++ = 0;
 	return total;
 }
@@ -165,8 +170,18 @@ void usb_descriptors_init(void)
 		s_dev.idVendor = s_emu->vid;
 		s_dev.idProduct = s_emu->pid;
 		s_dev.bcdDevice = s_emu->bcd;
-		build_emu_config(s_emu_cfg, s_emu);
-		s_vendor_itf = 1;
+		// One HID interface per paired controller (min 1) so several connected
+		// controllers each appear as their own pad. Bond count is read from the
+		// persisted label table (BT isn't up yet at descriptor-build time). New
+		// pairings made while already in an emulated mode need a replug/reboot to
+		// gain an interface.
+		uint8_t nb = 0;
+		const pp_cfg_t *c = settings();
+		for (int i = 0; i < 4; i++)
+			if (c->bond[i].used) nb++;
+		s_emu_ifaces = nb < 1 ? 1 : (nb > PP_NSLOT ? PP_NSLOT : nb);
+		build_emu_config(s_emu_cfg, s_emu, s_emu_ifaces);
+		s_vendor_itf = s_emu_ifaces;  // vendor is the interface after the HIDs
 	} else {
 		s_dev.idVendor = PP_USB_VID;
 		s_dev.idProduct = PP_USB_PID;

+ 5 - 0
PicoPuck/src/usb/usb_descriptors.h

@@ -43,4 +43,9 @@ enum {
 // Cache the active mode's descriptors. Call once at boot, before tud_init().
 void usb_descriptors_init(void);
 
+// Number of emulated HID interfaces presented in the active emulated mode (one
+// per paired controller; 1 otherwise). Used to fan the report stream + report
+// callbacks across interface==slot.
+uint8_t usb_emu_iface_count(void);
+
 #endif // PICOPUCK_USB_DESCRIPTORS_H

+ 4 - 4
docs/index.html

@@ -148,14 +148,14 @@
         <button id="ppBootBtn" title="Reboot into the UF2 bootloader to flash new firmware">Update firmware</button>
       </div>
       <div style="display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start">
-        <div style="flex:1;min-width:240px">
-          <h3 style="font-size:12px;color:var(--mut);margin:8px 0 6px">Discovered</h3>
-          <div id="ppScanList"><p class="note">Press <em>Scan</em> to look for controllers in pairing mode.</p></div>
-        </div>
         <div style="flex:1;min-width:240px">
           <h3 style="font-size:12px;color:var(--mut);margin:8px 0 6px">Paired controllers</h3>
           <div id="ppSlots"><p class="note">No controllers paired yet.</p></div>
         </div>
+        <div style="flex:1;min-width:240px">
+          <h3 style="font-size:12px;color:var(--mut);margin:8px 0 6px">Discovered</h3>
+          <div id="ppScanList"><p class="note">Press <em>Scan</em> to look for controllers in pairing mode.</p></div>
+        </div>
       </div>
       <p class="note" style="margin-top:8px">This is a <strong>PicoPuck</strong> (Pico W / Pico 2 W). Pair a controller here; <em>everything else</em> — output mode, rumble, mappings — uses the same cards above as a wired puck. Put a controller in pairing mode, press <em>Scan</em>, then pair it. Pairings persist across reboots and mode changes.</p>
     </div>