Jari 2 hónapja
szülő
commit
2bb9d9c6d5

+ 6 - 135
OpenPuck/mode_switch_hori.cpp

@@ -1,6 +1,7 @@
 #include "mode_switch_hori.h"
 #include "triton.h"
 #include "gamepad_util.h"
+#include "src/common/report_build.h"
 #include "config.h"
 #include "bonds.h"
 #include "usb_mount.h"
@@ -17,146 +18,16 @@ static Adafruit_USBD_HID g_switch[NSLOT];
 static unsigned long g_swLastMs[NSLOT] = { 0 };
 
 // back-paddle / QAM code (g_back[], g_qamMap) -> Switch button bit. 0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back(Minus) 10=Start(Plus) 11=Guide(Home) 18=Capture
-static uint16_t codeToSwitch(uint8_t c, uint16_t fA, uint16_t fB, uint16_t fX,
-			     uint16_t fY)
-{
-	switch (c) {
-	case 1:
-		return fA;
-	case 2:
-		return fB;
-	case 3:
-		return fX;
-	case 4:
-		return fY;
-	case 5:
-		return 0x10;
-	case 6:
-		return 0x20;
-	case 7:
-		return 0x400;
-	case 8:
-		return 0x800;
-	case 9:
-		return 0x100;
-	case 10:
-		return 0x200;
-	case 11:
-		return 0x1000;
-	case 18:
-		return 0x2000; // Capture / Screenshot (Switch-only target)
-	case 19:
-		return 0x40; // ZL (left trigger)
-	case 20:
-		return 0x80; // ZR (right trigger)
-	default:
-		return 0;
-	}
-}
 // Back-paddle code 12..15 map to D-pad Up/Down/Left/Right; fold them into the hat direction flags.
-static inline void backCodeToHatDirs(uint8_t c, bool &u, bool &d, bool &l,
-				     bool &r)
-{
-	if (c == 12)
-		u = true;
-	else if (c == 13)
-		d = true;
-	else if (c == 14)
-		l = true;
-	else if (c == 15)
-		r = true;
-}
 // HORIPAD/Switch button bits: Y=1 B=2 A=4 X=8 L=10 R=20 ZL=40 ZR=80 Minus=100 Plus=200 LClick=400 RClick=800 Home=1000 Capture=2000
 // Per-slot: each controller's decoded input is in g_in[slot].
 static void switchBuildHoripad(uint8_t slot, uint8_t out[8])
 {
-	uint32_t b = g_in[slot].buttons;
-	uint16_t btn = 0;
-	// QAM (3 dots) remap: the configured code is applied to the Switch output just like a back paddle below
-	// (so Capture(18) and every Switch target work). 0 = unmapped -> QAM does nothing on Switch.
-	bool qam = g_qamMap && (b & TB_QAM);
-	// Mode-switch chord (all 4 back + A/X/Y): don't pass the face press to the console while the back-4 are held.
-	if ((b & CHORD_BACK4) == CHORD_BACK4)
-		b &= ~(uint32_t)(TB_A | TB_X | TB_Y);
-	// face buttons with optional A/B + X/Y swap (Nintendo physical-vs-label layout)
-	uint16_t fY = g_abSwap ? 0x08 : 0x01, fB = g_abSwap ? 0x04 : 0x02,
-		 fA = g_abSwap ? 0x02 : 0x04, fX = g_abSwap ? 0x01 : 0x08;
-	if (b & TB_Y)
-		btn |= fY;
-	if (b & TB_B)
-		btn |= fB;
-	if (b & TB_A)
-		btn |= fA;
-	if (b & TB_X)
-		btn |= fX;
-	if (b & TB_LB)
-		btn |= 0x10;
-	if (b & TB_RB)
-		btn |= 0x20; // L, R
-	// ZL/ZR digital: trip on the analog threshold (activates early) OR the full-press click bit
-	if ((g_in[slot].lt >= SW_TRIG_ON) || (b & 0x8000000u))
-		btn |= 0x40;
-	if ((g_in[slot].rt >= SW_TRIG_ON) || (b & 0x800000u))
-		btn |= 0x80;
-	if (b & TB_MENU)
-		btn |= 0x100;
-	if (b & TB_VIEW)
-		btn |= 0x200; // Minus, Plus
-	if (b & TB_L3)
-		btn |= 0x400;
-	if (b & TB_R3)
-		btn |= 0x800; // LClick, RClick
-	if (b & TB_STEAM)
-		btn |= 0x1000; // Home
-	// back paddles -> configurable mapping (same g_back[] as Xbox: default L4->LB R4->RB L5->L3 R5->R3)
-	if (b & TB_L4)
-		btn |= codeToSwitch(g_back[0], fA, fB, fX, fY);
-	if (b & TB_R4)
-		btn |= codeToSwitch(g_back[1], fA, fB, fX, fY);
-	if (b & TB_L5)
-		btn |= codeToSwitch(g_back[2], fA, fB, fX, fY);
-	if (b & TB_R5)
-		btn |= codeToSwitch(g_back[3], fA, fB, fX, fY);
-	if (qam)
-		btn |= codeToSwitch(g_qamMap, fA, fB, fX, fY);
-	bool u = b & TB_DUP, d = b & TB_DDN, l = b & TB_DLF,
-	     r = b & TB_DRT; // hat: 0=N..7=NW, 8=neutral
-	if (b & TB_L4)
-		backCodeToHatDirs(g_back[0], u, d, l, r);
-	if (b & TB_R4)
-		backCodeToHatDirs(g_back[1], u, d, l, r);
-	if (b & TB_L5)
-		backCodeToHatDirs(g_back[2], u, d, l, r);
-	if (b & TB_R5)
-		backCodeToHatDirs(g_back[3], u, d, l, r);
-	if (qam)
-		backCodeToHatDirs(g_qamMap, u, d, l, r);
-	uint8_t hat = 8;
-	if (u && r)
-		hat = 1;
-	else if (r && d)
-		hat = 3;
-	else if (d && l)
-		hat = 5;
-	else if (l && u)
-		hat = 7;
-	else if (u)
-		hat = 0;
-	else if (r)
-		hat = 2;
-	else if (d)
-		hat = 4;
-	else if (l)
-		hat = 6;
-	out[0] = btn & 0xFF;
-	out[1] = btn >> 8;
-	out[2] = hat;
-	out[3] = swStick(g_in[slot].lx, false);
-	out[4] = swStick(g_in[slot].ly,
-			 true); // HID Y is down-positive -> invert
-	out[5] = swStick(g_in[slot].rx, false);
-	out[6] = swStick(g_in[slot].ry, true);
-	out[7] = 0;
+	// Report layout is in the shared builder (src/common/report_build.c).
+	report_cfg_t cfg = { g_abSwap != 0,
+			     { g_back[0], g_back[1], g_back[2], g_back[3] },
+			     g_qamMap };
+	build_switch_hori(&g_in[slot], &cfg, out);
 }
 
 // Dynamic-mount mode: begin() is unused (setup() calls beginPool()+usbReenumerate instead).

+ 8 - 165
OpenPuck/mode_switch_pro.cpp

@@ -1,6 +1,7 @@
 #include "mode_switch_pro.h"
 #include "triton.h"
 #include "gamepad_util.h"
+#include "src/common/report_build.h"
 #include "config.h"
 #include "haptics.h"
 #include "bonds.h"
@@ -25,7 +26,6 @@ SwitchProController g_switchPro;
 #define SW_PRO_REPORT_MS 15u // 66 Hz
 #define SW_PRO_REPORT_MS_120 8u // ~120 Hz
 // SC2 accel is +/-2g (16384/g); /4 -> the genuine Pro +/-8g (4096/g) the Switch cal expects
-#define SW_ACCEL_DIV 4
 
 uint8_t g_swProRate =
 	2; // 0 = 66Hz, 1 = 120Hz, 2 = full (~250Hz / USB_STREAM_MS, default)
@@ -60,81 +60,7 @@ static void swProLoadCfg()
 		f.close();
 	}
 }
-// Scale a gyro axis by g_swGyroScale10/10, clamped to int16 (3x can saturate at high rates -- expected).
-static int16_t gscale(int16_t v)
-{
-	if (g_swGyroScale10 == 10)
-		return v;
-	int32_t s = (int32_t)v * (int32_t)g_swGyroScale10 / 10;
-	if (s > 32767)
-		s = 32767;
-	else if (s < -32768)
-		s = -32768;
-	return (int16_t)s;
-}
 
-#define JC_BTN_Y (1u << 0)
-#define JC_BTN_X (1u << 1)
-#define JC_BTN_B (1u << 2)
-#define JC_BTN_A (1u << 3)
-#define JC_BTN_R (1u << 6)
-#define JC_BTN_ZR (1u << 7)
-#define JC_BTN_MINUS (1u << 8)
-#define JC_BTN_PLUS (1u << 9)
-#define JC_BTN_RSTICK (1u << 10)
-#define JC_BTN_LSTICK (1u << 11)
-#define JC_BTN_HOME (1u << 12)
-#define JC_BTN_CAPTURE (1u << 13)
-#define JC_BTN_DOWN (1u << 16)
-#define JC_BTN_UP (1u << 17)
-#define JC_BTN_RIGHT (1u << 18)
-#define JC_BTN_LEFT (1u << 19)
-#define JC_BTN_L (1u << 22)
-#define JC_BTN_ZL (1u << 23)
-static uint32_t codeToJc(uint8_t c, uint32_t fA, uint32_t fB, uint32_t fX,
-			 uint32_t fY)
-{
-	switch (c) {
-	case 1:
-		return fA;
-	case 2:
-		return fB;
-	case 3:
-		return fX;
-	case 4:
-		return fY;
-	case 5:
-		return JC_BTN_L;
-	case 6:
-		return JC_BTN_R;
-	case 7:
-		return JC_BTN_LSTICK;
-	case 8:
-		return JC_BTN_RSTICK;
-	case 9:
-		return JC_BTN_MINUS;
-	case 10:
-		return JC_BTN_PLUS;
-	case 11:
-		return JC_BTN_HOME;
-	case 18:
-		return JC_BTN_CAPTURE; // Capture / Screenshot (Switch-only target)
-	case 19:
-		return JC_BTN_ZL; // left trigger
-	case 20:
-		return JC_BTN_ZR; // right trigger
-	case 12:
-		return JC_BTN_UP;
-	case 13:
-		return JC_BTN_DOWN;
-	case 14:
-		return JC_BTN_LEFT;
-	case 15:
-		return JC_BTN_RIGHT;
-	default:
-		return 0;
-	}
-}
 // NSLOT Pro-Controller HIDs (one per bond slot) + per-slot handshake state (timer, report-mode gate,
 // subcommand-reply FIFO), per-slot reply queue indices, per-slot last-stream millis, per-slot MAC, and
 // per-slot user-cal SPI mirror.
@@ -237,60 +163,11 @@ static void jcInputPrefix(uint8_t slot, uint8_t *out)
 {
 	uint8_t bond = jcBondOf(
 		slot); // input data comes from the mapped controller; timer/state stay per USB slot
-	uint32_t b = g_in[bond].buttons;
-	// QAM (3 dots) remap -> applied via codeToJc below like a back paddle (so Capture(18)/any target work).
-	bool qam = g_qamMap && (b & TB_QAM);
-	if ((b & CHORD_BACK4) == CHORD_BACK4)
-		b &= ~(uint32_t)(TB_A | TB_B | TB_X | TB_Y);
-	uint32_t fA = g_abSwap ? JC_BTN_B : JC_BTN_A,
-		 fB = g_abSwap ? JC_BTN_A : JC_BTN_B;
-	uint32_t fX = g_abSwap ? JC_BTN_Y : JC_BTN_X,
-		 fY = g_abSwap ? JC_BTN_X : JC_BTN_Y;
-	uint32_t jc = 0;
-	if (b & TB_Y)
-		jc |= fY;
-	if (b & TB_B)
-		jc |= fB;
-	if (b & TB_A)
-		jc |= fA;
-	if (b & TB_X)
-		jc |= fX;
-	if (b & TB_LB)
-		jc |= JC_BTN_L;
-	if (b & TB_RB)
-		jc |= JC_BTN_R;
-	if ((g_in[bond].lt >= SW_TRIG_ON) || (b & 0x8000000u))
-		jc |= JC_BTN_ZL;
-	if ((g_in[bond].rt >= SW_TRIG_ON) || (b & 0x800000u))
-		jc |= JC_BTN_ZR;
-	if (b & TB_VIEW)
-		jc |= JC_BTN_PLUS;
-	if (b & TB_MENU)
-		jc |= JC_BTN_MINUS;
-	if (b & TB_L3)
-		jc |= JC_BTN_LSTICK;
-	if (b & TB_R3)
-		jc |= JC_BTN_RSTICK;
-	if (b & TB_STEAM)
-		jc |= JC_BTN_HOME;
-	if (b & TB_DDN)
-		jc |= JC_BTN_DOWN;
-	if (b & TB_DUP)
-		jc |= JC_BTN_UP;
-	if (b & TB_DRT)
-		jc |= JC_BTN_RIGHT;
-	if (b & TB_DLF)
-		jc |= JC_BTN_LEFT;
-	if (b & TB_L4)
-		jc |= codeToJc(g_back[0], fA, fB, fX, fY);
-	if (b & TB_R4)
-		jc |= codeToJc(g_back[1], fA, fB, fX, fY);
-	if (b & TB_L5)
-		jc |= codeToJc(g_back[2], fA, fB, fX, fY);
-	if (b & TB_R5)
-		jc |= codeToJc(g_back[3], fA, fB, fX, fY);
-	if (qam)
-		jc |= codeToJc(g_qamMap, fA, fB, fX, fY);
+	// Button field is the shared builder; timer/battery/sticks stay OpenPuck-local.
+	report_cfg_t cfg = { g_abSwap != 0,
+			     { g_back[0], g_back[1], g_back[2], g_back[3] },
+			     g_qamMap };
+	uint32_t jc = switch_pro_buttons(&g_in[bond], &cfg);
 	out[0] = g_jcTimer[slot]++;
 
 	// bat_con byte: [7:5]=capacity, bit4=charging, bit0=host_powered (see jcBatteryNibble). The controllers are
@@ -314,42 +191,8 @@ static void switchProBuild(uint8_t slot, uint8_t out[63])
 		jcBondOf(slot); // IMU data comes from the mapped controller
 	memset(out, 0, 63);
 	jcInputPrefix(slot, out);
-	// Gyro slot order follows hid-nintendo: raw+6 = ROLL, raw+8 = PITCH, raw+10 = YAW. Source routing (a proper
-	// rotation, det +1): SwitchX<-+gy, SwitchY<--gx, SwitchZ<-+gz.
-	//
-	// The accel MUST use the SAME signed permutation as the gyro. Steam reads the gyro raw and ignores this, but the
-	// Switch console FUSES accel (gravity) with gyro to anchor absolute orientation -- if the accel frame has the
-	// opposite handedness, the gravity-correction term points the wrong way and the attitude estimate latches into a
-	// rotated solution (intermittent ~45deg axis-mixing, per-unit roll bias, cleared only by a replug). So accel Y
-	// must be -ax to match the gyro's -gx pitch axis; resting gravity then lands cleanly on Switch Z (the up axis).
-	// The SC2 accelerometer is +/-2g full-scale (~16384 counts = 1g). The factory cal we present (sensitivity 0x4000)
-	// tells the Switch it's a genuine +/-8g Pro Controller (4096 counts = 1g), so divide by SW_ACCEL_DIV=4 to report
-	// ~1g. Without this the console reads gravity as ~4g, REJECTS the accel for drift correction (it must be linear
-	// accel, not gravity), and gyro roll error accumulates into a slow ~45deg lean. Gyro is left at native scale.
-	// accel X <- +ay, accel Y <- -ax, accel Z <- +az (signs match gyro)
-	int16_t aX = (int16_t)(g_in[bond].ay / SW_ACCEL_DIV);
-	int16_t aY = (int16_t)((-(int16_t)g_in[bond].ax) / SW_ACCEL_DIV);
-	int16_t aZ = (int16_t)(g_in[bond].az / SW_ACCEL_DIV);
-
-	// gyro outputs scaled by the user sensitivity factor
-	int16_t groll = gscale((int16_t)g_in[bond].gy);
-	int16_t gpitch = gscale((int16_t)(-(int16_t)g_in[bond].gx));
-	int16_t gyaw = gscale((int16_t)g_in[bond].gz);
-	for (int k = 0; k < 3; k++) {
-		int o = 12 + k * 12;
-		out[o + 0] = aX & 0xFF;
-		out[o + 1] = (aX >> 8) & 0xFF;
-		out[o + 2] = aY & 0xFF;
-		out[o + 3] = (aY >> 8) & 0xFF;
-		out[o + 4] = aZ & 0xFF;
-		out[o + 5] = (aZ >> 8) & 0xFF;
-		out[o + 6] = groll & 0xFF;
-		out[o + 7] = (groll >> 8) & 0xFF;
-		out[o + 8] = gpitch & 0xFF;
-		out[o + 9] = (gpitch >> 8) & 0xFF;
-		out[o + 10] = gyaw & 0xFF;
-		out[o + 11] = (gyaw >> 8) & 0xFF;
-	}
+	// IMU (accel/gyro permutation + user gyro scale) is the shared builder.
+	switch_pro_imu(&g_in[bond], g_swGyroScale10, out + 12);
 }
 // --- Canonical factory SPI dumps the host reads for calibration. Neutral IMU + centered sticks so a fresh
 // "device" calibrates sane; user-cal regions (0x80xx) read 0xFF so the host falls back to these factory blocks.

+ 307 - 0
OpenPuck/src/common/report_build.c

@@ -186,3 +186,310 @@ uint16_t build_ds3(const puck_input_t *in, const report_cfg_t *cfg,
 	ds3_imu(out + 46, in->gz);
 	return 48;
 }
+
+// ---- Switch HORIPAD -------------------------------------------------------
+// Back-paddle / QAM code → Switch button-field bit (dpad codes 12..15 go to the
+// hat via switch_hat_dirs instead, so return 0 here).
+static uint16_t code_to_switch(uint8_t c, uint16_t fA, uint16_t fB, uint16_t fX,
+			       uint16_t fY)
+{
+	switch (c) {
+	case 1:
+		return fA;
+	case 2:
+		return fB;
+	case 3:
+		return fX;
+	case 4:
+		return fY;
+	case 5:
+		return 0x10;
+	case 6:
+		return 0x20;
+	case 7:
+		return 0x400;
+	case 8:
+		return 0x800;
+	case 9:
+		return 0x100;
+	case 10:
+		return 0x200;
+	case 11:
+		return 0x1000;
+	case 18:
+		return 0x2000; // Capture / Screenshot
+	case 19:
+		return 0x40; // ZL
+	case 20:
+		return 0x80; // ZR
+	default:
+		return 0;
+	}
+}
+static void switch_hat_dirs(uint8_t c, bool *u, bool *d, bool *l, bool *r)
+{
+	if (c == 12)
+		*u = true;
+	else if (c == 13)
+		*d = true;
+	else if (c == 14)
+		*l = true;
+	else if (c == 15)
+		*r = true;
+}
+
+uint16_t build_switch_hori(const puck_input_t *in, const report_cfg_t *cfg,
+			   uint8_t out[8])
+{
+	uint32_t b = in->buttons;
+	uint16_t btn = 0;
+	bool qam = cfg->qam && (b & TB_QAM);
+	// Mode-switch chord (all 4 back + a face): don't leak the face to the console.
+	if ((b & CHORD_BACK4) == CHORD_BACK4)
+		b &= ~(uint32_t)(TB_A | TB_X | TB_Y);
+	uint16_t fY = cfg->ab_swap ? 0x08 : 0x01,
+		 fB = cfg->ab_swap ? 0x04 : 0x02,
+		 fA = cfg->ab_swap ? 0x02 : 0x04,
+		 fX = cfg->ab_swap ? 0x01 : 0x08;
+	if (b & TB_Y)
+		btn |= fY;
+	if (b & TB_B)
+		btn |= fB;
+	if (b & TB_A)
+		btn |= fA;
+	if (b & TB_X)
+		btn |= fX;
+	if (b & TB_LB)
+		btn |= 0x10;
+	if (b & TB_RB)
+		btn |= 0x20;
+	if ((in->lt >= SW_TRIG_ON) || (b & TB_L2))
+		btn |= 0x40; // ZL
+	if ((in->rt >= SW_TRIG_ON) || (b & TB_R2))
+		btn |= 0x80; // ZR
+	if (b & TB_MENU)
+		btn |= 0x100; // Minus
+	if (b & TB_VIEW)
+		btn |= 0x200; // Plus
+	if (b & TB_L3)
+		btn |= 0x400;
+	if (b & TB_R3)
+		btn |= 0x800;
+	if (b & TB_STEAM)
+		btn |= 0x1000; // Home
+	if (b & TB_L4)
+		btn |= code_to_switch(cfg->back[0], fA, fB, fX, fY);
+	if (b & TB_R4)
+		btn |= code_to_switch(cfg->back[1], fA, fB, fX, fY);
+	if (b & TB_L5)
+		btn |= code_to_switch(cfg->back[2], fA, fB, fX, fY);
+	if (b & TB_R5)
+		btn |= code_to_switch(cfg->back[3], fA, fB, fX, fY);
+	if (qam)
+		btn |= code_to_switch(cfg->qam, fA, fB, fX, fY);
+
+	bool u = b & TB_DUP, d = b & TB_DDN, l = b & TB_DLF, r = b & TB_DRT;
+	if (b & TB_L4)
+		switch_hat_dirs(cfg->back[0], &u, &d, &l, &r);
+	if (b & TB_R4)
+		switch_hat_dirs(cfg->back[1], &u, &d, &l, &r);
+	if (b & TB_L5)
+		switch_hat_dirs(cfg->back[2], &u, &d, &l, &r);
+	if (b & TB_R5)
+		switch_hat_dirs(cfg->back[3], &u, &d, &l, &r);
+	if (qam)
+		switch_hat_dirs(cfg->qam, &u, &d, &l, &r);
+	uint8_t hat = 8;
+	if (u && r)
+		hat = 1;
+	else if (r && d)
+		hat = 3;
+	else if (d && l)
+		hat = 5;
+	else if (l && u)
+		hat = 7;
+	else if (u)
+		hat = 0;
+	else if (r)
+		hat = 2;
+	else if (d)
+		hat = 4;
+	else if (l)
+		hat = 6;
+
+	out[0] = (uint8_t)(btn & 0xFF);
+	out[1] = (uint8_t)(btn >> 8);
+	out[2] = hat;
+	out[3] = sw_stick(in->lx, false);
+	out[4] = sw_stick(in->ly, true);
+	out[5] = sw_stick(in->rx, false);
+	out[6] = sw_stick(in->ry, true);
+	out[7] = 0;
+	return 8;
+}
+
+// ---- Switch Pro Controller -------------------------------------------------
+enum {
+	JC_BTN_Y = 1u << 0,
+	JC_BTN_X = 1u << 1,
+	JC_BTN_B = 1u << 2,
+	JC_BTN_A = 1u << 3,
+	JC_BTN_R = 1u << 6,
+	JC_BTN_ZR = 1u << 7,
+	JC_BTN_MINUS = 1u << 8,
+	JC_BTN_PLUS = 1u << 9,
+	JC_BTN_RSTICK = 1u << 10,
+	JC_BTN_LSTICK = 1u << 11,
+	JC_BTN_HOME = 1u << 12,
+	JC_BTN_CAPTURE = 1u << 13,
+	JC_BTN_DOWN = 1u << 16,
+	JC_BTN_UP = 1u << 17,
+	JC_BTN_RIGHT = 1u << 18,
+	JC_BTN_LEFT = 1u << 19,
+	JC_BTN_L = 1u << 22,
+	JC_BTN_ZL = 1u << 23,
+};
+static uint32_t code_to_jc(uint8_t c, uint32_t fA, uint32_t fB, uint32_t fX,
+			   uint32_t fY)
+{
+	switch (c) {
+	case 1:
+		return fA;
+	case 2:
+		return fB;
+	case 3:
+		return fX;
+	case 4:
+		return fY;
+	case 5:
+		return JC_BTN_L;
+	case 6:
+		return JC_BTN_R;
+	case 7:
+		return JC_BTN_LSTICK;
+	case 8:
+		return JC_BTN_RSTICK;
+	case 9:
+		return JC_BTN_MINUS;
+	case 10:
+		return JC_BTN_PLUS;
+	case 11:
+		return JC_BTN_HOME;
+	case 18:
+		return JC_BTN_CAPTURE;
+	case 19:
+		return JC_BTN_ZL;
+	case 20:
+		return JC_BTN_ZR;
+	case 12:
+		return JC_BTN_UP;
+	case 13:
+		return JC_BTN_DOWN;
+	case 14:
+		return JC_BTN_LEFT;
+	case 15:
+		return JC_BTN_RIGHT;
+	default:
+		return 0;
+	}
+}
+uint32_t switch_pro_buttons(const puck_input_t *in, const report_cfg_t *cfg)
+{
+	uint32_t b = in->buttons;
+	bool qam = cfg->qam && (b & TB_QAM);
+	if ((b & CHORD_BACK4) == CHORD_BACK4)
+		b &= ~(uint32_t)(TB_A | TB_B | TB_X | TB_Y);
+	uint32_t fA = cfg->ab_swap ? JC_BTN_B : JC_BTN_A;
+	uint32_t fB = cfg->ab_swap ? JC_BTN_A : JC_BTN_B;
+	uint32_t fX = cfg->ab_swap ? JC_BTN_Y : JC_BTN_X;
+	uint32_t fY = cfg->ab_swap ? JC_BTN_X : JC_BTN_Y;
+	uint32_t jc = 0;
+	if (b & TB_Y)
+		jc |= fY;
+	if (b & TB_B)
+		jc |= fB;
+	if (b & TB_A)
+		jc |= fA;
+	if (b & TB_X)
+		jc |= fX;
+	if (b & TB_LB)
+		jc |= JC_BTN_L;
+	if (b & TB_RB)
+		jc |= JC_BTN_R;
+	if ((in->lt >= SW_TRIG_ON) || (b & TB_L2))
+		jc |= JC_BTN_ZL;
+	if ((in->rt >= SW_TRIG_ON) || (b & TB_R2))
+		jc |= JC_BTN_ZR;
+	if (b & TB_VIEW)
+		jc |= JC_BTN_PLUS;
+	if (b & TB_MENU)
+		jc |= JC_BTN_MINUS;
+	if (b & TB_L3)
+		jc |= JC_BTN_LSTICK;
+	if (b & TB_R3)
+		jc |= JC_BTN_RSTICK;
+	if (b & TB_STEAM)
+		jc |= JC_BTN_HOME;
+	if (b & TB_DDN)
+		jc |= JC_BTN_DOWN;
+	if (b & TB_DUP)
+		jc |= JC_BTN_UP;
+	if (b & TB_DRT)
+		jc |= JC_BTN_RIGHT;
+	if (b & TB_DLF)
+		jc |= JC_BTN_LEFT;
+	if (b & TB_L4)
+		jc |= code_to_jc(cfg->back[0], fA, fB, fX, fY);
+	if (b & TB_R4)
+		jc |= code_to_jc(cfg->back[1], fA, fB, fX, fY);
+	if (b & TB_L5)
+		jc |= code_to_jc(cfg->back[2], fA, fB, fX, fY);
+	if (b & TB_R5)
+		jc |= code_to_jc(cfg->back[3], fA, fB, fX, fY);
+	if (qam)
+		jc |= code_to_jc(cfg->qam, fA, fB, fX, fY);
+	return jc;
+}
+
+// SC2 accel is ±2g (16384/g); ÷4 → the genuine Pro ±8g (4096/g) the Switch cal
+// expects. Gyro is scaled by gyro_scale10/10, clamped to int16.
+#define SW_ACCEL_DIV 4
+static int16_t sw_gscale(int16_t v, uint8_t g10)
+{
+	if (g10 == 10)
+		return v;
+	int32_t s = (int32_t)v * (int32_t)g10 / 10;
+	if (s > 32767)
+		s = 32767;
+	else if (s < -32768)
+		s = -32768;
+	return (int16_t)s;
+}
+void switch_pro_imu(const puck_input_t *in, uint8_t gyro_scale10,
+		    uint8_t out36[36])
+{
+	// accel X<-+ay, Y<--ax, Z<-+az (÷4); gyro roll<-+gy, pitch<--gx, yaw<-+gz.
+	// Same signed permutation across both so the console's accel/gyro fusion keeps
+	// a consistent handedness (see the OpenPuck handedness note).
+	int16_t aX = (int16_t)(in->ay / SW_ACCEL_DIV);
+	int16_t aY = (int16_t)((-(int16_t)in->ax) / SW_ACCEL_DIV);
+	int16_t aZ = (int16_t)(in->az / SW_ACCEL_DIV);
+	int16_t groll = sw_gscale((int16_t)in->gy, gyro_scale10);
+	int16_t gpitch = sw_gscale((int16_t)(-(int16_t)in->gx), gyro_scale10);
+	int16_t gyaw = sw_gscale((int16_t)in->gz, gyro_scale10);
+	for (int k = 0; k < 3; k++) {
+		int o = k * 12;
+		out36[o + 0] = aX & 0xFF;
+		out36[o + 1] = (aX >> 8) & 0xFF;
+		out36[o + 2] = aY & 0xFF;
+		out36[o + 3] = (aY >> 8) & 0xFF;
+		out36[o + 4] = aZ & 0xFF;
+		out36[o + 5] = (aZ >> 8) & 0xFF;
+		out36[o + 6] = groll & 0xFF;
+		out36[o + 7] = (groll >> 8) & 0xFF;
+		out36[o + 8] = gpitch & 0xFF;
+		out36[o + 9] = (gpitch >> 8) & 0xFF;
+		out36[o + 10] = gyaw & 0xFF;
+		out36[o + 11] = (gyaw >> 8) & 0xFF;
+	}
+}

+ 11 - 0
OpenPuck/src/common/report_build.h

@@ -43,6 +43,17 @@ uint16_t build_ps5(const puck_input_t *in, const report_cfg_t *cfg,
 		   report_seq_t *seq, uint8_t out[63]); // DualSense
 uint16_t build_ds3(const puck_input_t *in, const report_cfg_t *cfg,
 		   uint8_t out[48]); // DualShock 3 / Sixaxis
+uint16_t build_switch_hori(const puck_input_t *in, const report_cfg_t *cfg,
+			   uint8_t out[8]); // Switch HORIPAD (8-byte, no id)
+
+// Switch Pro Controller pieces (the handshake / battery / timer state stays in
+// each firmware's mode_switch_pro; these are the shared button-field + IMU math).
+// switch_pro_buttons packs the 24-bit JC button field from g_in + remap config.
+uint32_t switch_pro_buttons(const puck_input_t *in, const report_cfg_t *cfg);
+// switch_pro_imu writes the 36-byte IMU region (3 samples of accel+gyro) of the
+// 0x30 report; gyro_scale10 is the user gyro sensitivity ×10 (10 = 1.0×).
+void switch_pro_imu(const puck_input_t *in, uint8_t gyro_scale10,
+		    uint8_t out36[36]);
 
 #ifdef __cplusplus
 }

+ 12 - 61
PicoPuck/src/puck/mode_switch_hori.c

@@ -5,72 +5,23 @@
 //
 // SPDX-License-Identifier: AGPL-3.0-or-later
 #include "puck/emu.h"
-#include "gamepad_util.h"
+#include "sys/settings.h"
 #include "hid_reports.h"
+#include "report_build.h"
 
+#define ET_SWITCH 1 // per-type config index (matches OpenPuck ET_SWITCH)
+
+// Report layout is in the shared builder (common/report_build.c). This also
+// brings PicoPuck's HORIPAD up to OpenPuck parity: back-paddle / QAM remap,
+// hat-from-paddle, and configurable A/B+X/Y swap.
 static uint16_t hori_build(int slot, uint8_t *out, uint8_t *rid)
 {
 	*rid = 0;
-	uint32_t b = g_in[slot].buttons;
-	uint16_t btn = 0;
-	// Nintendo layout (abSwap): A→B(0x02) B→A(0x04) X→Y(0x01) Y→X(0x08).
-	if (b & TB_A)
-		btn |= 0x02;
-	if (b & TB_B)
-		btn |= 0x04;
-	if (b & TB_X)
-		btn |= 0x01;
-	if (b & TB_Y)
-		btn |= 0x08;
-	if (b & TB_LB)
-		btn |= 0x10;
-	if (b & TB_RB)
-		btn |= 0x20;
-	if (g_in[slot].lt >= SW_TRIG_ON || (b & TB_L2))
-		btn |= 0x40; // ZL
-	if (g_in[slot].rt >= SW_TRIG_ON || (b & TB_R2))
-		btn |= 0x80; // ZR
-	if (b & TB_MENU)
-		btn |= 0x100; // Minus
-	if (b & TB_VIEW)
-		btn |= 0x200; // Plus
-	if (b & TB_L3)
-		btn |= 0x400;
-	if (b & TB_R3)
-		btn |= 0x800;
-	if (b & TB_STEAM)
-		btn |= 0x1000; // Home
-	if (b & TB_QAM)
-		btn |= 0x2000; // Capture
-
-	bool u = b & TB_DUP, d = b & TB_DDN, l = b & TB_DLF, r = b & TB_DRT;
-	uint8_t hat = 8;
-	if (u && r)
-		hat = 1;
-	else if (r && d)
-		hat = 3;
-	else if (d && l)
-		hat = 5;
-	else if (l && u)
-		hat = 7;
-	else if (u)
-		hat = 0;
-	else if (r)
-		hat = 2;
-	else if (d)
-		hat = 4;
-	else if (l)
-		hat = 6;
-
-	out[0] = (uint8_t)(btn & 0xFF);
-	out[1] = (uint8_t)(btn >> 8);
-	out[2] = hat;
-	out[3] = sw_stick(g_in[slot].lx, false);
-	out[4] = sw_stick(g_in[slot].ly, true);
-	out[5] = sw_stick(g_in[slot].rx, false);
-	out[6] = sw_stick(g_in[slot].ry, true);
-	out[7] = 0;
-	return 8;
+	const pp_type_cfg_t *t = &settings()->type[ET_SWITCH];
+	report_cfg_t cfg = { t->ab_swap,
+			     { t->back[0], t->back[1], t->back[2], t->back[3] },
+			     t->qam };
+	return build_switch_hori(&g_in[slot], &cfg, out);
 }
 
 const emu_mode_t emu_switch_hori = {

+ 7 - 155
PicoPuck/src/puck/mode_switch_pro.c

@@ -23,78 +23,14 @@
 #include "pico/time.h"
 #include "hid_reports.h"
 #include "hd_rumble.h"
+#include "report_build.h"
 
 #define ET_SWITCH 1 // per-type config index (matches OpenPuck ET_SWITCH)
 #define SW_PRO_REPORT_MS 15u // 66 Hz
 #define SW_PRO_REPORT_MS_120 8u // ~120 Hz
 #define SW_STREAM_MS 4u // "full" (~250 Hz), matches emu_present cadence
-#define SW_ACCEL_DIV 4 // SC2 ±2g (16384/g) → Pro ±8g (4096/g)
 // CHORD_BACK4 comes from the shared triton_masks.h (via emu.h → triton.h).
 
-#define JC_BTN_Y (1u << 0)
-#define JC_BTN_X (1u << 1)
-#define JC_BTN_B (1u << 2)
-#define JC_BTN_A (1u << 3)
-#define JC_BTN_R (1u << 6)
-#define JC_BTN_ZR (1u << 7)
-#define JC_BTN_MINUS (1u << 8)
-#define JC_BTN_PLUS (1u << 9)
-#define JC_BTN_RSTICK (1u << 10)
-#define JC_BTN_LSTICK (1u << 11)
-#define JC_BTN_HOME (1u << 12)
-#define JC_BTN_CAPTURE (1u << 13)
-#define JC_BTN_DOWN (1u << 16)
-#define JC_BTN_UP (1u << 17)
-#define JC_BTN_RIGHT (1u << 18)
-#define JC_BTN_LEFT (1u << 19)
-#define JC_BTN_L (1u << 22)
-#define JC_BTN_ZL (1u << 23)
-
-static uint32_t code_to_jc(uint8_t c, uint32_t fA, uint32_t fB, uint32_t fX,
-			   uint32_t fY)
-{
-	switch (c) {
-	case 1:
-		return fA;
-	case 2:
-		return fB;
-	case 3:
-		return fX;
-	case 4:
-		return fY;
-	case 5:
-		return JC_BTN_L;
-	case 6:
-		return JC_BTN_R;
-	case 7:
-		return JC_BTN_LSTICK;
-	case 8:
-		return JC_BTN_RSTICK;
-	case 9:
-		return JC_BTN_MINUS;
-	case 10:
-		return JC_BTN_PLUS;
-	case 11:
-		return JC_BTN_HOME;
-	case 18:
-		return JC_BTN_CAPTURE;
-	case 19:
-		return JC_BTN_ZL;
-	case 20:
-		return JC_BTN_ZR;
-	case 12:
-		return JC_BTN_UP;
-	case 13:
-		return JC_BTN_DOWN;
-	case 14:
-		return JC_BTN_LEFT;
-	case 15:
-		return JC_BTN_RIGHT;
-	default:
-		return 0;
-	}
-}
-
 // 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.
@@ -163,75 +99,14 @@ static uint8_t jc_battery_nibble(int slot)
 			 << 1); // even nibble {0,2,4,6,8}; leaves bit4 (charging) clear
 }
 
-// Scale a gyro axis by sw_gyro10/10, clamped to int16.
-static int16_t gscale(int16_t v)
-{
-	uint8_t g10 = settings()->sw_gyro10;
-	if (g10 == 10)
-		return v;
-	int32_t s = (int32_t)v * (int32_t)g10 / 10;
-	if (s > 32767)
-		s = 32767;
-	else if (s < -32768)
-		s = -32768;
-	return (int16_t)s;
-}
 static void jc_input_prefix(int slot, uint8_t *out)
 {
 	const pp_type_cfg_t *t = &settings()->type[ET_SWITCH];
-	uint32_t b = g_in[slot].buttons;
-	bool qam = t->qam && (b & TB_QAM);
-	if ((b & CHORD_BACK4) == CHORD_BACK4)
-		b &= ~(uint32_t)(TB_A | TB_B | TB_X | TB_Y);
-	uint32_t fA = t->ab_swap ? JC_BTN_B : JC_BTN_A;
-	uint32_t fB = t->ab_swap ? JC_BTN_A : JC_BTN_B;
-	uint32_t fX = t->ab_swap ? JC_BTN_Y : JC_BTN_X;
-	uint32_t fY = t->ab_swap ? JC_BTN_X : JC_BTN_Y;
-	uint32_t jc = 0;
-	if (b & TB_Y)
-		jc |= fY;
-	if (b & TB_B)
-		jc |= fB;
-	if (b & TB_A)
-		jc |= fA;
-	if (b & TB_X)
-		jc |= fX;
-	if (b & TB_LB)
-		jc |= JC_BTN_L;
-	if (b & TB_RB)
-		jc |= JC_BTN_R;
-	if ((g_in[slot].lt >= SW_TRIG_ON) || (b & TB_L2))
-		jc |= JC_BTN_ZL;
-	if ((g_in[slot].rt >= SW_TRIG_ON) || (b & TB_R2))
-		jc |= JC_BTN_ZR;
-	if (b & TB_VIEW)
-		jc |= JC_BTN_PLUS;
-	if (b & TB_MENU)
-		jc |= JC_BTN_MINUS;
-	if (b & TB_L3)
-		jc |= JC_BTN_LSTICK;
-	if (b & TB_R3)
-		jc |= JC_BTN_RSTICK;
-	if (b & TB_STEAM)
-		jc |= JC_BTN_HOME;
-	if (b & TB_DDN)
-		jc |= JC_BTN_DOWN;
-	if (b & TB_DUP)
-		jc |= JC_BTN_UP;
-	if (b & TB_DRT)
-		jc |= JC_BTN_RIGHT;
-	if (b & TB_DLF)
-		jc |= JC_BTN_LEFT;
-	if (b & TB_L4)
-		jc |= code_to_jc(t->back[0], fA, fB, fX, fY);
-	if (b & TB_R4)
-		jc |= code_to_jc(t->back[1], fA, fB, fX, fY);
-	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);
+	report_cfg_t cfg = { t->ab_swap,
+			     { t->back[0], t->back[1], t->back[2], t->back[3] },
+			     t->qam };
+	uint32_t jc =
+		switch_pro_buttons(&g_in[slot], &cfg); // shared button field
 	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);
@@ -481,30 +356,7 @@ static void swpro_build_30(int slot, uint8_t out[63])
 {
 	memset(out, 0, 63);
 	jc_input_prefix(slot, out);
-	// accel X<-+ay, Y<--ax, Z<-+az (÷4 for ±8g cal); gyro roll<-+gy, pitch<--gx,
-	// yaw<-+gz. Same signed permutation across both so the console's accel/gyro
-	// fusion keeps a consistent handedness (see OpenPuck notes).
-	int16_t aX = (int16_t)(g_in[slot].ay / SW_ACCEL_DIV);
-	int16_t aY = (int16_t)((-(int16_t)g_in[slot].ax) / SW_ACCEL_DIV);
-	int16_t aZ = (int16_t)(g_in[slot].az / SW_ACCEL_DIV);
-	int16_t groll = gscale((int16_t)g_in[slot].gy);
-	int16_t gpitch = gscale((int16_t)(-(int16_t)g_in[slot].gx));
-	int16_t gyaw = gscale((int16_t)g_in[slot].gz);
-	for (int k = 0; k < 3; k++) {
-		int o = 12 + k * 12;
-		out[o + 0] = aX & 0xFF;
-		out[o + 1] = (aX >> 8) & 0xFF;
-		out[o + 2] = aY & 0xFF;
-		out[o + 3] = (aY >> 8) & 0xFF;
-		out[o + 4] = aZ & 0xFF;
-		out[o + 5] = (aZ >> 8) & 0xFF;
-		out[o + 6] = groll & 0xFF;
-		out[o + 7] = (groll >> 8) & 0xFF;
-		out[o + 8] = gpitch & 0xFF;
-		out[o + 9] = (gpitch >> 8) & 0xFF;
-		out[o + 10] = gyaw & 0xFF;
-		out[o + 11] = (gyaw >> 8) & 0xFF;
-	}
+	switch_pro_imu(&g_in[slot], settings()->sw_gyro10, out + 12);
 }
 
 static uint16_t swpro_build(int slot, uint8_t *out, uint8_t *rid)