|
@@ -3,6 +3,7 @@
|
|
|
#include "gamepad_util.h"
|
|
#include "gamepad_util.h"
|
|
|
#include "config.h"
|
|
#include "config.h"
|
|
|
#include "haptics.h"
|
|
#include "haptics.h"
|
|
|
|
|
+#include "bonds.h"
|
|
|
#include <Adafruit_TinyUSB.h>
|
|
#include <Adafruit_TinyUSB.h>
|
|
|
#include <Adafruit_LittleFS.h>
|
|
#include <Adafruit_LittleFS.h>
|
|
|
#include <InternalFileSystem.h>
|
|
#include <InternalFileSystem.h>
|
|
@@ -140,13 +141,29 @@ static uint32_t codeToJc(uint8_t c, uint32_t fA, uint32_t fB, uint32_t fX,
|
|
|
return 0;
|
|
return 0;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-static unsigned long g_swProLastMs = 0;
|
|
|
|
|
-static Adafruit_USBD_HID g_swPro;
|
|
|
|
|
|
|
+// 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.
|
|
|
|
|
+static Adafruit_USBD_HID g_swPro[NSLOT];
|
|
|
|
|
+static unsigned long g_swProLastMs[NSLOT] = { 0 };
|
|
|
// 0 until the host's subcommand 0x03 selects 0x30 -> THEN we stream input
|
|
// 0 until the host's subcommand 0x03 selects 0x30 -> THEN we stream input
|
|
|
-static uint8_t g_swProReportMode = 0;
|
|
|
|
|
-static const uint8_t JC_MAC[6] = {
|
|
|
|
|
- 0x7C, 0xBB, 0x8A, 0x00, 0x00, 0x01
|
|
|
|
|
-}; // synthetic but stable controller MAC
|
|
|
|
|
|
|
+static uint8_t g_swProReportMode[NSLOT] = { 0 };
|
|
|
|
|
+static uint8_t g_jcTimer[NSLOT] = { 0 };
|
|
|
|
|
+// Per-slot MAC. The console uses the BT addr it reads from subcommand 0x02 to identify the controller;
|
|
|
|
|
+// real Pro Controllers each have their own. Last byte varies per slot.
|
|
|
|
|
+static const uint8_t JC_MAC_BASE[5] = { 0x7C, 0xBB, 0x8A, 0x00, 0x00 };
|
|
|
|
|
+static uint8_t g_jcMac[NSLOT][6];
|
|
|
|
|
+static bool g_jcMacInit = false;
|
|
|
|
|
+static void initJcMacs()
|
|
|
|
|
+{
|
|
|
|
|
+ if (g_jcMacInit)
|
|
|
|
|
+ return;
|
|
|
|
|
+ for (int s = 0; s < NSLOT; s++) {
|
|
|
|
|
+ memcpy(g_jcMac[s], JC_MAC_BASE, 5);
|
|
|
|
|
+ g_jcMac[s][5] = (uint8_t)(0x10 + s);
|
|
|
|
|
+ }
|
|
|
|
|
+ g_jcMacInit = true;
|
|
|
|
|
+}
|
|
|
static uint16_t jcRumbleAmp(const uint8_t r[4])
|
|
static uint16_t jcRumbleAmp(const uint8_t r[4])
|
|
|
{
|
|
{
|
|
|
// Nintendo packs a high band (r[0],r[1]) and low band (r[2],r[3]) of frequency+amplitude. We only need a
|
|
// Nintendo packs a high band (r[0],r[1]) and low band (r[2],r[3]) of frequency+amplitude. We only need a
|
|
@@ -158,19 +175,21 @@ static uint16_t jcRumbleAmp(const uint8_t r[4])
|
|
|
uint8_t a = hf > lf ? hf : lf; // 0..0x7F
|
|
uint8_t a = hf > lf ? hf : lf; // 0..0x7F
|
|
|
return (uint16_t)a << 9; // scale to ~16-bit motor speed
|
|
return (uint16_t)a << 9; // scale to ~16-bit motor speed
|
|
|
}
|
|
}
|
|
|
-static void jcRumble(const uint8_t *p, uint16_t pn)
|
|
|
|
|
|
|
+// Per-slot: each Pro Controller has its own rumble stream, so the "last" relay tracking must be per-slot.
|
|
|
|
|
+static uint16_t g_jcLastLo[NSLOT] = { 0 };
|
|
|
|
|
+static uint16_t g_jcLastHi[NSLOT] = { 0 };
|
|
|
|
|
+static void jcRumble(uint8_t slot, const uint8_t *p, uint16_t pn)
|
|
|
{
|
|
{
|
|
|
if (pn < 9)
|
|
if (pn < 9)
|
|
|
return; // [timer][left rumble x4][right rumble x4]
|
|
return; // [timer][left rumble x4][right rumble x4]
|
|
|
uint16_t lo = jcRumbleAmp(p + 1), hi = jcRumbleAmp(p + 5);
|
|
uint16_t lo = jcRumbleAmp(p + 1), hi = jcRumbleAmp(p + 5);
|
|
|
- static uint16_t lastLo = 0, lastHi = 0;
|
|
|
|
|
// only relay on change: the Switch streams rumble every frame; re-sending
|
|
// only relay on change: the Switch streams rumble every frame; re-sending
|
|
|
// unchanged values would flood the RF relay and loop the motor
|
|
// unchanged values would flood the RF relay and loop the motor
|
|
|
- if (lo == lastLo && hi == lastHi)
|
|
|
|
|
|
|
+ if (lo == g_jcLastLo[slot] && hi == g_jcLastHi[slot])
|
|
|
return;
|
|
return;
|
|
|
- lastLo = lo;
|
|
|
|
|
- lastHi = hi;
|
|
|
|
|
- hapticSteamRumble(lo, hi);
|
|
|
|
|
|
|
+ g_jcLastLo[slot] = lo;
|
|
|
|
|
+ g_jcLastHi[slot] = hi;
|
|
|
|
|
+ hapticSteamRumble(lo, hi, slot);
|
|
|
}
|
|
}
|
|
|
static int jcStick12(int16_t v, bool inv)
|
|
static int jcStick12(int16_t v, bool inv)
|
|
|
{ // steam int16 (center 0) -> 12-bit (center 0x800), clamped
|
|
{ // steam int16 (center 0) -> 12-bit (center 0x800), clamped
|
|
@@ -187,10 +206,9 @@ static void jcPackStick(uint8_t s[3], int16_t x, int16_t y)
|
|
|
}
|
|
}
|
|
|
// Standard input-report prefix [0..11] (timer, battery/conn, 3 button bytes, both packed sticks, vibrator),
|
|
// Standard input-report prefix [0..11] (timer, battery/conn, 3 button bytes, both packed sticks, vibrator),
|
|
|
// shared by the streamed 0x30 report and the 0x21 subcommand-reply reports the host reads during init.
|
|
// shared by the streamed 0x30 report and the 0x21 subcommand-reply reports the host reads during init.
|
|
|
-static uint8_t g_jcTimer = 0;
|
|
|
|
|
-static void jcInputPrefix(uint8_t *out)
|
|
|
|
|
|
|
+static void jcInputPrefix(uint8_t slot, uint8_t *out)
|
|
|
{
|
|
{
|
|
|
- uint32_t b = g_in.buttons;
|
|
|
|
|
|
|
+ uint32_t b = g_in[slot].buttons;
|
|
|
if (g_qamMap && (b & TB_QAM)) {
|
|
if (g_qamMap && (b & TB_QAM)) {
|
|
|
b &= ~(uint32_t)TB_QAM;
|
|
b &= ~(uint32_t)TB_QAM;
|
|
|
b |= tritonFromCode(g_qamMap);
|
|
b |= tritonFromCode(g_qamMap);
|
|
@@ -214,9 +232,9 @@ static void jcInputPrefix(uint8_t *out)
|
|
|
jc |= JC_BTN_L;
|
|
jc |= JC_BTN_L;
|
|
|
if (b & TB_RB)
|
|
if (b & TB_RB)
|
|
|
jc |= JC_BTN_R;
|
|
jc |= JC_BTN_R;
|
|
|
- if ((g_in.lt >= SW_TRIG_ON) || (b & 0x8000000u))
|
|
|
|
|
|
|
+ if ((g_in[slot].lt >= SW_TRIG_ON) || (b & 0x8000000u))
|
|
|
jc |= JC_BTN_ZL;
|
|
jc |= JC_BTN_ZL;
|
|
|
- if ((g_in.rt >= SW_TRIG_ON) || (b & 0x800000u))
|
|
|
|
|
|
|
+ if ((g_in[slot].rt >= SW_TRIG_ON) || (b & 0x800000u))
|
|
|
jc |= JC_BTN_ZR;
|
|
jc |= JC_BTN_ZR;
|
|
|
if (b & TB_VIEW)
|
|
if (b & TB_VIEW)
|
|
|
jc |= JC_BTN_PLUS;
|
|
jc |= JC_BTN_PLUS;
|
|
@@ -244,7 +262,7 @@ static void jcInputPrefix(uint8_t *out)
|
|
|
jc |= codeToJc(g_back[2], fA, fB, fX, fY);
|
|
jc |= codeToJc(g_back[2], fA, fB, fX, fY);
|
|
|
if (b & TB_R5)
|
|
if (b & TB_R5)
|
|
|
jc |= codeToJc(g_back[3], fA, fB, fX, fY);
|
|
jc |= codeToJc(g_back[3], fA, fB, fX, fY);
|
|
|
- out[0] = g_jcTimer++;
|
|
|
|
|
|
|
+ out[0] = g_jcTimer[slot]++;
|
|
|
|
|
|
|
|
// battery full+charging (hi nibble 0x9), connection_info=1 (lo nibble): a wired/charging Pro Controller.
|
|
// battery full+charging (hi nibble 0x9), connection_info=1 (lo nibble): a wired/charging Pro Controller.
|
|
|
// A real Switch reads this to show the pad as connected; Steam/hid-nintendo accept it too.
|
|
// A real Switch reads this to show the pad as connected; Steam/hid-nintendo accept it too.
|
|
@@ -256,15 +274,15 @@ static void jcInputPrefix(uint8_t *out)
|
|
|
// charging_grip bit (button "common" byte, bit7): genuine Pro Controller always sets it on USB; real
|
|
// charging_grip bit (button "common" byte, bit7): genuine Pro Controller always sets it on USB; real
|
|
|
// Switch uses it to recognise a wired controller. Not a button, so hid-nintendo ignores it.
|
|
// Switch uses it to recognise a wired controller. Not a button, so hid-nintendo ignores it.
|
|
|
out[3] |= 0x80;
|
|
out[3] |= 0x80;
|
|
|
- jcPackStick(out + 5, g_in.lx, g_in.ly);
|
|
|
|
|
- jcPackStick(out + 8, g_in.rx, g_in.ry);
|
|
|
|
|
|
|
+ jcPackStick(out + 5, g_in[slot].lx, g_in[slot].ly);
|
|
|
|
|
+ jcPackStick(out + 8, g_in[slot].rx, g_in[slot].ry);
|
|
|
// rumble_input_report echo: genuine pad emits 0x09..0x0C; some Switch firmware expects this nonzero.
|
|
// rumble_input_report echo: genuine pad emits 0x09..0x0C; some Switch firmware expects this nonzero.
|
|
|
out[11] = 0x09;
|
|
out[11] = 0x09;
|
|
|
}
|
|
}
|
|
|
-static void switchProBuild(uint8_t out[63])
|
|
|
|
|
|
|
+static void switchProBuild(uint8_t slot, uint8_t out[63])
|
|
|
{
|
|
{
|
|
|
memset(out, 0, 63);
|
|
memset(out, 0, 63);
|
|
|
- jcInputPrefix(out);
|
|
|
|
|
|
|
+ jcInputPrefix(slot, out);
|
|
|
// Gyro slot order follows hid-nintendo: raw+6 = ROLL, raw+8 = PITCH, raw+10 = YAW. Source routing (a proper
|
|
// 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.
|
|
// rotation, det +1): SwitchX<-+gy, SwitchY<--gx, SwitchZ<-+gz.
|
|
|
//
|
|
//
|
|
@@ -278,14 +296,14 @@ static void switchProBuild(uint8_t out[63])
|
|
|
// ~1g. Without this the console reads gravity as ~4g, REJECTS the accel for drift correction (it must be linear
|
|
// ~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, 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)
|
|
// accel X <- +ay, accel Y <- -ax, accel Z <- +az (signs match gyro)
|
|
|
- int16_t aX = (int16_t)(g_in.ay / SW_ACCEL_DIV);
|
|
|
|
|
- int16_t aY = (int16_t)((-(int16_t)g_in.ax) / SW_ACCEL_DIV);
|
|
|
|
|
- int16_t aZ = (int16_t)(g_in.az / SW_ACCEL_DIV);
|
|
|
|
|
|
|
+ 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);
|
|
|
|
|
|
|
|
// gyro outputs scaled by the user sensitivity factor
|
|
// gyro outputs scaled by the user sensitivity factor
|
|
|
- int16_t groll = gscale((int16_t)g_in.gy);
|
|
|
|
|
- int16_t gpitch = gscale((int16_t)(-(int16_t)g_in.gx));
|
|
|
|
|
- int16_t gyaw = gscale((int16_t)g_in.gz);
|
|
|
|
|
|
|
+ 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++) {
|
|
for (int k = 0; k < 3; k++) {
|
|
|
int o = 12 + k * 12;
|
|
int o = 12 + k * 12;
|
|
|
out[o + 0] = aX & 0xFF;
|
|
out[o + 0] = aX & 0xFF;
|
|
@@ -317,7 +335,8 @@ static const uint8_t SPI_PARAMS2[18] = { 0x0F, 0x30, 0x61, 0x96, 0x30, 0xF3,
|
|
|
0xC7, 0x79, 0x9C, 0x33, 0x36, 0x63 };
|
|
0xC7, 0x79, 0x9C, 0x33, 0x36, 0x63 };
|
|
|
static const uint8_t SPI_COLOR[13] = { 0x32, 0x32, 0x32, 0xE6, 0xE6, 0xE6, 0x32,
|
|
static const uint8_t SPI_COLOR[13] = { 0x32, 0x32, 0x32, 0xE6, 0xE6, 0xE6, 0x32,
|
|
|
0x32, 0x32, 0x32, 0x32, 0x32, 0xFF };
|
|
0x32, 0x32, 0x32, 0x32, 0x32, 0xFF };
|
|
|
-// built at boot: left[9]+right[9] factory stick calibration (packed 12-bit)
|
|
|
|
|
|
|
+// built at boot: left[9]+right[9] factory stick calibration (packed 12-bit). Shared across slots -- it's a
|
|
|
|
|
+// factory stub the host reads to seed its cal, and we present the same neutral stub for all controllers.
|
|
|
static uint8_t g_spiStickCal[18];
|
|
static uint8_t g_spiStickCal[18];
|
|
|
static void jcPack12(uint8_t *o9, const uint16_t v[6])
|
|
static void jcPack12(uint8_t *o9, const uint16_t v[6])
|
|
|
{ // pack 6 12-bit values into 9 bytes (Switch stick-cal format)
|
|
{ // pack 6 12-bit values into 9 bytes (Switch stick-cal format)
|
|
@@ -346,14 +365,6 @@ static void jcBuildStickCal()
|
|
|
// Manual-pairing (subcommand 0x01) reply payloads. A real Switch runs this 3-stage BT key exchange over USB so it
|
|
// Manual-pairing (subcommand 0x01) reply payloads. A real Switch runs this 3-stage BT key exchange over USB so it
|
|
|
// can register the pad (Steam/hid-nintendo do not). The Switch only validates the shape, not the key contents. Each
|
|
// can register the pad (Steam/hid-nintendo do not). The Switch only validates the shape, not the key contents. Each
|
|
|
// is the 31-byte reply body following the 0x21 input prefix + ack(0x81) + echoed-subcommand(0x01).
|
|
// is the 31-byte reply body following the 0x21 input prefix + ack(0x81) + echoed-subcommand(0x01).
|
|
|
-// type 1: echo type + controller BT addr + "Pro Controller" name
|
|
|
|
|
-static const uint8_t BT_PAIR_1[31] = {
|
|
|
|
|
- 0x01, JC_MAC[0], JC_MAC[1], JC_MAC[2], JC_MAC[3], JC_MAC[4], JC_MAC[5],
|
|
|
|
|
- 0x00, 0x25, 0x08, 0x50, 0x72, 0x6F, 0x20,
|
|
|
|
|
- 0x43, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C,
|
|
|
|
|
- 0x6C, 0x65, 0x72, // "Pro Controller"
|
|
|
|
|
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x68
|
|
|
|
|
-};
|
|
|
|
|
static const uint8_t BT_PAIR_2[31] = { // type 2: LTK exchange
|
|
static const uint8_t BT_PAIR_2[31] = { // type 2: LTK exchange
|
|
|
0x02, 0xE5, 0xC8, 0xE4, 0x92, 0x05, 0xFF, 0xC9, 0x8A, 0x7D, 0xEA,
|
|
0x02, 0xE5, 0xC8, 0xE4, 0x92, 0x05, 0xFF, 0xC9, 0x8A, 0x7D, 0xEA,
|
|
|
0x15, 0xF6, 0x19, 0xBA, 0x82, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x15, 0xF6, 0x19, 0xBA, 0x82, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
@@ -364,53 +375,77 @@ static const uint8_t BT_PAIR_3[31] = {
|
|
|
}; // type 3: save pairing (all zero body)
|
|
}; // type 3: save pairing (all zero body)
|
|
|
// User-calibration SPI mirror (0x8000-0x80FF). A real Pro Controller stores the gyro/accel (and stick) calibration
|
|
// User-calibration SPI mirror (0x8000-0x80FF). A real Pro Controller stores the gyro/accel (and stick) calibration
|
|
|
// the Switch writes during "Calibrate Motion Controls" here, then reads it back and applies it -- that's how a
|
|
// the Switch writes during "Calibrate Motion Controls" here, then reads it back and applies it -- that's how a
|
|
|
-// resting IMU/neutral offset gets cancelled. We store writes and serve them back, persisted to flash so it survives
|
|
|
|
|
-// unplug/replug. 0xFF = blank -> Switch falls back to our factory block at 0x6020 (default until cal'd).
|
|
|
|
|
-#define SWCAL_FILE "/swimucal.bin"
|
|
|
|
|
-static uint8_t g_userCal[0x100];
|
|
|
|
|
|
|
+// resting IMU/neutral offset gets cancelled. Per-slot: each Pro Controller has its own SPI mirror, so each
|
|
|
|
|
+// gets its own persisted file (NSLOT files; the same 0x100 bytes as before, partitioned by slot index).
|
|
|
|
|
+#define SWCAL_FILE_BASE "/swimucal"
|
|
|
|
|
+static uint8_t g_userCal[NSLOT][0x100];
|
|
|
|
|
+static bool g_userCalLoaded[NSLOT] = { false, false, false, false };
|
|
|
|
|
+static void swCalFileName(uint8_t slot, char *out)
|
|
|
|
|
+{
|
|
|
|
|
+ // "/swimucal0.bin" .. "/swimucal3.bin"
|
|
|
|
|
+ const char *base = SWCAL_FILE_BASE;
|
|
|
|
|
+ int i = 0;
|
|
|
|
|
+ while (base[i]) {
|
|
|
|
|
+ out[i] = base[i];
|
|
|
|
|
+ i++;
|
|
|
|
|
+ }
|
|
|
|
|
+ out[i++] = (char)('0' + slot);
|
|
|
|
|
+ out[i++] = '.';
|
|
|
|
|
+ out[i++] = 'b';
|
|
|
|
|
+ out[i++] = 'i';
|
|
|
|
|
+ out[i++] = 'n';
|
|
|
|
|
+ out[i] = '\0';
|
|
|
|
|
+}
|
|
|
static void loadUserCal()
|
|
static void loadUserCal()
|
|
|
{
|
|
{
|
|
|
- memset(g_userCal, 0xFF, sizeof g_userCal);
|
|
|
|
|
- File f(InternalFS);
|
|
|
|
|
- if (f.open(SWCAL_FILE, FILE_O_READ)) {
|
|
|
|
|
- f.read(g_userCal, sizeof g_userCal);
|
|
|
|
|
- f.close();
|
|
|
|
|
|
|
+ for (int s = 0; s < NSLOT; s++) {
|
|
|
|
|
+ memset(g_userCal[s], 0xFF, sizeof g_userCal[s]);
|
|
|
|
|
+ char fn[16];
|
|
|
|
|
+ swCalFileName((uint8_t)s, fn);
|
|
|
|
|
+ File f(InternalFS);
|
|
|
|
|
+ if (f.open(fn, FILE_O_READ)) {
|
|
|
|
|
+ f.read(g_userCal[s], sizeof g_userCal[s]);
|
|
|
|
|
+ f.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ g_userCalLoaded[s] = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-static void saveUserCal()
|
|
|
|
|
|
|
+static void saveUserCal(uint8_t slot)
|
|
|
{
|
|
{
|
|
|
- InternalFS.remove(SWCAL_FILE);
|
|
|
|
|
|
|
+ char fn[16];
|
|
|
|
|
+ swCalFileName(slot, fn);
|
|
|
|
|
+ InternalFS.remove(fn);
|
|
|
File f(InternalFS);
|
|
File f(InternalFS);
|
|
|
- if (f.open(SWCAL_FILE, FILE_O_WRITE)) {
|
|
|
|
|
- f.write(g_userCal, sizeof g_userCal);
|
|
|
|
|
|
|
+ if (f.open(fn, FILE_O_WRITE)) {
|
|
|
|
|
+ f.write(g_userCal[slot], sizeof g_userCal[slot]);
|
|
|
f.close();
|
|
f.close();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
// jcSpiWrite runs in the USB ISR (via jcSet). NEVER do flash I/O here -- a blocking LittleFS erase/write in the
|
|
// jcSpiWrite runs in the USB ISR (via jcSet). NEVER do flash I/O here -- a blocking LittleFS erase/write in the
|
|
|
// interrupt wedges USB + the RF poll and corrupts state (device drops into a bad state needing a replug). Only
|
|
// interrupt wedges USB + the RF poll and corrupts state (device drops into a bad state needing a replug). Only
|
|
|
// update the RAM mirror and flag it dirty; task() (main loop) does the actual save, debounced so a calibration
|
|
// update the RAM mirror and flag it dirty; task() (main loop) does the actual save, debounced so a calibration
|
|
|
-// write-burst coalesces into one flash write.
|
|
|
|
|
-static volatile bool g_userCalDirty = false;
|
|
|
|
|
-static volatile unsigned long g_userCalDirtyMs = 0;
|
|
|
|
|
-static void jcSpiWrite(uint32_t addr, uint8_t len, const uint8_t *data,
|
|
|
|
|
- uint16_t avail)
|
|
|
|
|
|
|
+// write-burst coalesces into one flash write. Per-slot: each Pro Controller's ISR write targets its own mirror.
|
|
|
|
|
+static volatile bool g_userCalDirty[NSLOT] = { false, false, false, false };
|
|
|
|
|
+static volatile unsigned long g_userCalDirtyMs[NSLOT] = { 0 };
|
|
|
|
|
+static void jcSpiWrite(uint8_t slot, uint32_t addr, uint8_t len,
|
|
|
|
|
+ const uint8_t *data, uint16_t avail)
|
|
|
{
|
|
{
|
|
|
bool changed = false;
|
|
bool changed = false;
|
|
|
for (uint8_t i = 0; i < len && i < avail; i++) {
|
|
for (uint8_t i = 0; i < len && i < avail; i++) {
|
|
|
uint32_t a = addr + i;
|
|
uint32_t a = addr + i;
|
|
|
if (a >= 0x8000 && a < 0x8100) {
|
|
if (a >= 0x8000 && a < 0x8100) {
|
|
|
- if (g_userCal[a - 0x8000] != data[i]) {
|
|
|
|
|
- g_userCal[a - 0x8000] = data[i];
|
|
|
|
|
|
|
+ if (g_userCal[slot][a - 0x8000] != data[i]) {
|
|
|
|
|
+ g_userCal[slot][a - 0x8000] = data[i];
|
|
|
changed = true;
|
|
changed = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (changed) {
|
|
if (changed) {
|
|
|
- g_userCalDirty = true;
|
|
|
|
|
- g_userCalDirtyMs = millis();
|
|
|
|
|
|
|
+ g_userCalDirty[slot] = true;
|
|
|
|
|
+ g_userCalDirtyMs[slot] = millis();
|
|
|
} // defer the flash write to task()
|
|
} // defer the flash write to task()
|
|
|
}
|
|
}
|
|
|
-static void spiRead(uint32_t addr, uint8_t len, uint8_t *dst)
|
|
|
|
|
|
|
+static void spiRead(uint8_t slot, uint32_t addr, uint8_t len, uint8_t *dst)
|
|
|
{
|
|
{
|
|
|
for (uint8_t i = 0; i < len; i++) {
|
|
for (uint8_t i = 0; i < len; i++) {
|
|
|
uint32_t a = addr + i;
|
|
uint32_t a = addr + i;
|
|
@@ -427,52 +462,74 @@ static void spiRead(uint32_t addr, uint8_t len, uint8_t *dst)
|
|
|
v = SPI_PARAMS2[a - 0x6098];
|
|
v = SPI_PARAMS2[a - 0x6098];
|
|
|
else if (a >= 0x8000 && a < 0x8100)
|
|
else if (a >= 0x8000 && a < 0x8100)
|
|
|
// persisted user cal (0xFF blank -> factory fallback)
|
|
// persisted user cal (0xFF blank -> factory fallback)
|
|
|
- v = g_userCal[a - 0x8000];
|
|
|
|
|
|
|
+ v = g_userCal[slot][a - 0x8000];
|
|
|
dst[i] = v;
|
|
dst[i] = v;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
// Reply FIFO: the host's handshake/subcommand reports arrive in the USB ISR (jcSet); we enqueue the canonical
|
|
// Reply FIFO: the host's handshake/subcommand reports arrive in the USB ISR (jcSet); we enqueue the canonical
|
|
|
-// 0x81/0x21 reply and drain it from task() where sendReport is safe. SPSC ring, volatile indices. Every reply
|
|
|
|
|
-// is sent as a full 63-byte report (64 with the id): macOS IOHIDManager silently DROPS any input report shorter
|
|
|
|
|
-// than the descriptor-declared length (0x3F), so short 0x81/0x21 replies never reached the host.
|
|
|
|
|
|
|
+// 0x81/0x21 reply and drain it from task() where sendReport is safe. SPSC ring, volatile indices. Per-slot: each
|
|
|
|
|
+// Pro Controller runs its own handshake state machine, so each has its own reply queue. Every reply is sent as
|
|
|
|
|
+// a full 63-byte report (64 with the id): macOS IOHIDManager silently DROPS any input report shorter than the
|
|
|
|
|
+// descriptor-declared length (0x3F), so short 0x81/0x21 replies never reached the host.
|
|
|
#define JC_REPLEN 63
|
|
#define JC_REPLEN 63
|
|
|
struct JcRep {
|
|
struct JcRep {
|
|
|
uint8_t rid;
|
|
uint8_t rid;
|
|
|
uint8_t data[JC_REPLEN];
|
|
uint8_t data[JC_REPLEN];
|
|
|
};
|
|
};
|
|
|
#define JCQ_N 8
|
|
#define JCQ_N 8
|
|
|
-static volatile uint8_t g_jcQh = 0, g_jcQt = 0;
|
|
|
|
|
-static JcRep g_jcQ[JCQ_N];
|
|
|
|
|
-static void jcEnq(uint8_t rid, const uint8_t *d, uint8_t len)
|
|
|
|
|
|
|
+static volatile uint8_t g_jcQh[NSLOT] = { 0, 0, 0, 0 };
|
|
|
|
|
+static volatile uint8_t g_jcQt[NSLOT] = { 0, 0, 0, 0 };
|
|
|
|
|
+static JcRep g_jcQ[NSLOT][JCQ_N];
|
|
|
|
|
+static void jcEnq(uint8_t slot, uint8_t rid, const uint8_t *d, uint8_t len)
|
|
|
{
|
|
{
|
|
|
- uint8_t nt = (uint8_t)((g_jcQt + 1) % JCQ_N);
|
|
|
|
|
- if (nt == g_jcQh)
|
|
|
|
|
|
|
+ uint8_t t = g_jcQt[slot];
|
|
|
|
|
+ uint8_t nt = (uint8_t)((t + 1) % JCQ_N);
|
|
|
|
|
+ if (nt == g_jcQh[slot])
|
|
|
return; // full -> drop; the host re-requests on timeout
|
|
return; // full -> drop; the host re-requests on timeout
|
|
|
if (len > JC_REPLEN)
|
|
if (len > JC_REPLEN)
|
|
|
len = JC_REPLEN;
|
|
len = JC_REPLEN;
|
|
|
- g_jcQ[g_jcQt].rid = rid;
|
|
|
|
|
- memset(g_jcQ[g_jcQt].data, 0, JC_REPLEN);
|
|
|
|
|
- memcpy(g_jcQ[g_jcQt].data, d, len); // zero-pad to full report length
|
|
|
|
|
- g_jcQt = nt;
|
|
|
|
|
|
|
+ g_jcQ[slot][t].rid = rid;
|
|
|
|
|
+ memset(g_jcQ[slot][t].data, 0, JC_REPLEN);
|
|
|
|
|
+ memcpy(g_jcQ[slot][t].data, d, len); // zero-pad to full report length
|
|
|
|
|
+ g_jcQt[slot] = nt;
|
|
|
}
|
|
}
|
|
|
// Build the 0x21 reply (standard input prefix + ACK + echoed subcommand id + reply data) for a report-0x01 subcommand.
|
|
// Build the 0x21 reply (standard input prefix + ACK + echoed subcommand id + reply data) for a report-0x01 subcommand.
|
|
|
-static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
|
|
|
|
+static void jcSubcmd(uint8_t slot, uint8_t sub, const uint8_t *args,
|
|
|
|
|
+ uint16_t alen)
|
|
|
{
|
|
{
|
|
|
uint8_t p[JC_REPLEN];
|
|
uint8_t p[JC_REPLEN];
|
|
|
memset(p, 0, sizeof p);
|
|
memset(p, 0, sizeof p);
|
|
|
- jcInputPrefix(p);
|
|
|
|
|
|
|
+ jcInputPrefix(slot, p);
|
|
|
p[13] = sub;
|
|
p[13] = sub;
|
|
|
switch (sub) {
|
|
switch (sub) {
|
|
|
// manual BT pairing: 3-stage key exchange a real Switch runs over USB
|
|
// manual BT pairing: 3-stage key exchange a real Switch runs over USB
|
|
|
case 0x01: {
|
|
case 0x01: {
|
|
|
- const uint8_t *d = BT_PAIR_3;
|
|
|
|
|
uint8_t t = (alen >= 1) ? args[0] : 3;
|
|
uint8_t t = (alen >= 1) ? args[0] : 3;
|
|
|
- if (t == 1)
|
|
|
|
|
- d = BT_PAIR_1;
|
|
|
|
|
- else if (t == 2)
|
|
|
|
|
- d = BT_PAIR_2;
|
|
|
|
|
p[12] = 0x81; // pairing ACK
|
|
p[12] = 0x81; // pairing ACK
|
|
|
- memcpy(&p[14], d, 31);
|
|
|
|
|
|
|
+ if (t == 1) {
|
|
|
|
|
+ // echo type + this slot's MAC + "Pro Controller" name
|
|
|
|
|
+ p[14] = 0x01;
|
|
|
|
|
+ memcpy(&p[15], g_jcMac[slot], 6);
|
|
|
|
|
+ p[21] = 0x00;
|
|
|
|
|
+ p[22] = 0x25;
|
|
|
|
|
+ p[23] = 0x08;
|
|
|
|
|
+ // "Pro Controller" = 50 72 6F 20 43 6F 6E 74 72 6F 6C 6C 65 72
|
|
|
|
|
+ static const uint8_t NAME[14] = {
|
|
|
|
|
+ 0x50, 0x72, 0x6F, 0x20, 0x43, 0x6F, 0x6E,
|
|
|
|
|
+ 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65, 0x72
|
|
|
|
|
+ };
|
|
|
|
|
+ memcpy(&p[24], NAME, 14);
|
|
|
|
|
+ p[38] = 0x00;
|
|
|
|
|
+ p[39] = 0x00;
|
|
|
|
|
+ p[40] = 0x00;
|
|
|
|
|
+ p[41] = 0x00;
|
|
|
|
|
+ p[42] = 0x00;
|
|
|
|
|
+ p[43] = 0x68;
|
|
|
|
|
+ } else if (t == 2) {
|
|
|
|
|
+ memcpy(&p[14], BT_PAIR_2, 31);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ memcpy(&p[14], BT_PAIR_3, 31);
|
|
|
|
|
+ }
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
case 0x02: // request device info
|
|
case 0x02: // request device info
|
|
@@ -483,7 +540,7 @@ static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
p[15] = 0x48;
|
|
p[15] = 0x48;
|
|
|
p[16] = 0x03; // controller type: Pro Controller
|
|
p[16] = 0x03; // controller type: Pro Controller
|
|
|
p[17] = 0x02; // fixed
|
|
p[17] = 0x02; // fixed
|
|
|
- memcpy(&p[18], JC_MAC, 6);
|
|
|
|
|
|
|
+ memcpy(&p[18], g_jcMac[slot], 6);
|
|
|
p[24] = 0x01; // colors stored in SPI (0x6050)
|
|
p[24] = 0x01; // colors stored in SPI (0x6050)
|
|
|
p[25] = 0x01; // fixed
|
|
p[25] = 0x01; // fixed
|
|
|
break;
|
|
break;
|
|
@@ -504,7 +561,7 @@ static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
p[16] = args[2];
|
|
p[16] = args[2];
|
|
|
p[17] = args[3];
|
|
p[17] = args[3];
|
|
|
p[18] = rl;
|
|
p[18] = rl;
|
|
|
- spiRead(a, rl, &p[19]);
|
|
|
|
|
|
|
+ spiRead(slot, a, rl, &p[19]);
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
// SPI flash write -> persist user calibration so Switch motion-cal sticks
|
|
// SPI flash write -> persist user calibration so Switch motion-cal sticks
|
|
@@ -514,7 +571,7 @@ static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
((uint32_t)args[1] << 8) |
|
|
((uint32_t)args[1] << 8) |
|
|
|
((uint32_t)args[2] << 16) |
|
|
((uint32_t)args[2] << 16) |
|
|
|
((uint32_t)args[3] << 24);
|
|
((uint32_t)args[3] << 24);
|
|
|
- jcSpiWrite(a, args[4], &args[5],
|
|
|
|
|
|
|
+ jcSpiWrite(slot, a, args[4], &args[5],
|
|
|
(alen > 5) ? (uint16_t)(alen - 5) : 0);
|
|
(alen > 5) ? (uint16_t)(alen - 5) : 0);
|
|
|
}
|
|
}
|
|
|
p[12] = 0x80; // write ACK
|
|
p[12] = 0x80; // write ACK
|
|
@@ -523,7 +580,7 @@ static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
// set input report mode (0x30 = standard full) -> begin streaming
|
|
// set input report mode (0x30 = standard full) -> begin streaming
|
|
|
case 0x03:
|
|
case 0x03:
|
|
|
if (alen >= 1 && args[0] == 0x30)
|
|
if (alen >= 1 && args[0] == 0x30)
|
|
|
- g_swProReportMode = 0x30;
|
|
|
|
|
|
|
+ g_swProReportMode[slot] = 0x30;
|
|
|
p[12] = 0x80;
|
|
p[12] = 0x80;
|
|
|
break;
|
|
break;
|
|
|
// trigger buttons elapsed time -> canned reply (genuine pad returns data)
|
|
// trigger buttons elapsed time -> canned reply (genuine pad returns data)
|
|
@@ -545,13 +602,12 @@ static void jcSubcmd(uint8_t sub, const uint8_t *args, uint16_t alen)
|
|
|
// 0x06/0x08/0x30/0x38/0x40/0x41/0x48/... generic positive ACK
|
|
// 0x06/0x08/0x30/0x38/0x40/0x41/0x48/... generic positive ACK
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
- jcEnq(0x21, p, JC_REPLEN);
|
|
|
|
|
|
|
+ jcEnq(slot, 0x21, p, JC_REPLEN);
|
|
|
}
|
|
}
|
|
|
-// USB-ISR callback for the Pro Controller's OUT endpoint. Data arrives via the interrupt OUT endpoint, where
|
|
|
|
|
-// this TinyUSB core passes report_id=0 and leaves the real report id in buffer[0] (it does NOT strip it). Also
|
|
|
|
|
-// accept the control SET_REPORT shape (id already in `rid`) for safety. report 0x80 = USB handshake; 0x01 = subcommand.
|
|
|
|
|
-static void jcSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
|
|
|
|
|
- uint16_t n)
|
|
|
|
|
|
|
+// USB-ISR callback for the Pro Controller's OUT endpoint. Per-slot dispatch via per-instance callback
|
|
|
|
|
+// (jcSet##N closes over the slot index). Report 0x80 = USB handshake; 0x01 = subcommand.
|
|
|
|
|
+static void jcSetCommon(uint8_t slot, uint8_t rid, hid_report_type_t type,
|
|
|
|
|
+ uint8_t const *b, uint16_t n)
|
|
|
{
|
|
{
|
|
|
if (type != HID_REPORT_TYPE_OUTPUT || n < 1)
|
|
if (type != HID_REPORT_TYPE_OUTPUT || n < 1)
|
|
|
return;
|
|
return;
|
|
@@ -572,18 +628,24 @@ static void jcSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
|
|
|
if (pn < 1)
|
|
if (pn < 1)
|
|
|
return;
|
|
return;
|
|
|
if (p[0] == 0x01) {
|
|
if (p[0] == 0x01) {
|
|
|
- uint8_t d[9] = { 0x01, 0x00, 0x03,
|
|
|
|
|
- JC_MAC[0], JC_MAC[1], JC_MAC[2],
|
|
|
|
|
- JC_MAC[3], JC_MAC[4], JC_MAC[5] };
|
|
|
|
|
- jcEnq(0x81, d, 9);
|
|
|
|
|
|
|
+ uint8_t d[9] = { 0x01,
|
|
|
|
|
+ 0x00,
|
|
|
|
|
+ 0x03,
|
|
|
|
|
+ g_jcMac[slot][0],
|
|
|
|
|
+ g_jcMac[slot][1],
|
|
|
|
|
+ g_jcMac[slot][2],
|
|
|
|
|
+ g_jcMac[slot][3],
|
|
|
|
|
+ g_jcMac[slot][4],
|
|
|
|
|
+ g_jcMac[slot][5] };
|
|
|
|
|
+ jcEnq(slot, 0x81, d, 9);
|
|
|
} // device type + MAC
|
|
} // device type + MAC
|
|
|
else if (p[0] == 0x02) {
|
|
else if (p[0] == 0x02) {
|
|
|
uint8_t d[1] = { 0x02 };
|
|
uint8_t d[1] = { 0x02 };
|
|
|
- jcEnq(0x81, d, 1);
|
|
|
|
|
|
|
+ jcEnq(slot, 0x81, d, 1);
|
|
|
} // handshake
|
|
} // handshake
|
|
|
else if (p[0] == 0x03) {
|
|
else if (p[0] == 0x03) {
|
|
|
uint8_t d[1] = { 0x03 };
|
|
uint8_t d[1] = { 0x03 };
|
|
|
- jcEnq(0x81, d, 1);
|
|
|
|
|
|
|
+ jcEnq(slot, 0x81, d, 1);
|
|
|
} // set baudrate
|
|
} // set baudrate
|
|
|
// 0x04 force-USB / 0x05 enable-timeout / 0x06 reset: no reply expected
|
|
// 0x04 force-USB / 0x05 enable-timeout / 0x06 reset: no reply expected
|
|
|
return;
|
|
return;
|
|
@@ -591,68 +653,96 @@ static void jcSet(uint8_t rid, hid_report_type_t type, uint8_t const *b,
|
|
|
if (id == 0x01) { // [timer][rumble x8][subcmd][args...]
|
|
if (id == 0x01) { // [timer][rumble x8][subcmd][args...]
|
|
|
if (pn < 10)
|
|
if (pn < 10)
|
|
|
return;
|
|
return;
|
|
|
- jcRumble(p, pn);
|
|
|
|
|
- jcSubcmd(p[9], p + 10, (pn > 10) ? (uint16_t)(pn - 10) : 0);
|
|
|
|
|
|
|
+ jcRumble(slot, p, pn);
|
|
|
|
|
+ jcSubcmd(slot, p[9], p + 10,
|
|
|
|
|
+ (pn > 10) ? (uint16_t)(pn - 10) : 0);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (id == 0x10) {
|
|
if (id == 0x10) {
|
|
|
- jcRumble(p, pn);
|
|
|
|
|
|
|
+ jcRumble(slot, p, pn);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
// report 0x82: ignored
|
|
// report 0x82: ignored
|
|
|
}
|
|
}
|
|
|
|
|
+#define JCCB(N) \
|
|
|
|
|
+ static void jcSet##N(uint8_t r, hid_report_type_t t, uint8_t const *b, \
|
|
|
|
|
+ uint16_t n) \
|
|
|
|
|
+ { \
|
|
|
|
|
+ jcSetCommon(N, r, t, b, n); \
|
|
|
|
|
+ }
|
|
|
|
|
+// clang-format off
|
|
|
|
|
+JCCB(0)
|
|
|
|
|
+JCCB(1)
|
|
|
|
|
+JCCB(2)
|
|
|
|
|
+JCCB(3)
|
|
|
|
|
+// clang-format on
|
|
|
|
|
+typedef void (*jc_setcb_t)(uint8_t, hid_report_type_t, uint8_t const *,
|
|
|
|
|
+ uint16_t);
|
|
|
|
|
+static jc_setcb_t const JC_SETCB[NSLOT] = { jcSet0, jcSet1, jcSet2, jcSet3 };
|
|
|
|
|
|
|
|
void SwitchProController::begin()
|
|
void SwitchProController::begin()
|
|
|
{
|
|
{
|
|
|
USBDevice.setID(0x057E, 0x2009);
|
|
USBDevice.setID(0x057E, 0x2009);
|
|
|
- // bumped from 0x0211 for 1ms poll interval (Windows caches config by VID:PID:bcdDevice)
|
|
|
|
|
- USBDevice.setDeviceVersion(0x0212);
|
|
|
|
|
|
|
+ int n = bondedSlotCount();
|
|
|
|
|
+ USBDevice.setDeviceVersion(
|
|
|
|
|
+ (uint16_t)(0x0220 + (uint16_t)(n > 0 ? n - 1 : 0)));
|
|
|
USBDevice.setManufacturerDescriptor("Nintendo Co., Ltd.");
|
|
USBDevice.setManufacturerDescriptor("Nintendo Co., Ltd.");
|
|
|
USBDevice.setProductDescriptor("Pro Controller");
|
|
USBDevice.setProductDescriptor("Pro Controller");
|
|
|
jcBuildStickCal();
|
|
jcBuildStickCal();
|
|
|
|
|
|
|
|
- // restore any persisted Switch motion calibration (per-unit IMU bias correction)
|
|
|
|
|
loadUserCal();
|
|
loadUserCal();
|
|
|
- swProLoadCfg(); // restore report-rate + gyro-scale settings
|
|
|
|
|
- g_swPro.enableOutEndpoint(true);
|
|
|
|
|
- // answer the Nintendo USB handshake + subcommands (else Steam never binds it)
|
|
|
|
|
- g_swPro.setReportCallback(NULL, jcSet);
|
|
|
|
|
- g_swPro.setReportDescriptor(SWPRO_HID_DESC, sizeof SWPRO_HID_DESC);
|
|
|
|
|
-
|
|
|
|
|
- // 1ms bInterval so the RF rate is the only latency limit (matches Xbox)
|
|
|
|
|
- g_swPro.setPollInterval(1);
|
|
|
|
|
- g_swPro.begin();
|
|
|
|
|
|
|
+ swProLoadCfg();
|
|
|
|
|
+ initJcMacs();
|
|
|
|
|
+ for (int s = 0; s < NSLOT; s++) {
|
|
|
|
|
+ if (n > 0 && !g_slot[s].used)
|
|
|
|
|
+ continue;
|
|
|
|
|
+ if (n == 0 && s > 0)
|
|
|
|
|
+ break;
|
|
|
|
|
+ g_swPro[s].enableOutEndpoint(true);
|
|
|
|
|
+ g_swPro[s].setReportCallback(NULL, JC_SETCB[s]);
|
|
|
|
|
+ g_swPro[s].setReportDescriptor(SWPRO_HID_DESC,
|
|
|
|
|
+ sizeof SWPRO_HID_DESC);
|
|
|
|
|
+ g_swPro[s].setPollInterval(1);
|
|
|
|
|
+ g_swPro[s].begin();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
void SwitchProController::task()
|
|
void SwitchProController::task()
|
|
|
{
|
|
{
|
|
|
- if (!g_swPro.ready())
|
|
|
|
|
- return;
|
|
|
|
|
- // Deferred user-cal flash write (queued by the USB ISR; debounced so a calibration write-burst is one save).
|
|
|
|
|
- if (g_userCalDirty &&
|
|
|
|
|
- (unsigned long)(millis() - g_userCalDirtyMs) > 250u) {
|
|
|
|
|
- g_userCalDirty = false;
|
|
|
|
|
- saveUserCal();
|
|
|
|
|
- }
|
|
|
|
|
- if (g_jcQh !=
|
|
|
|
|
- g_jcQt) { // drain handshake/subcommand replies first (ordered)
|
|
|
|
|
- JcRep *r = &g_jcQ[g_jcQh];
|
|
|
|
|
- if (g_swPro.sendReport(r->rid, r->data, JC_REPLEN))
|
|
|
|
|
- g_jcQh = (uint8_t)((g_jcQh + 1) % JCQ_N);
|
|
|
|
|
- return; // one report per call; the rest next loop
|
|
|
|
|
|
|
+ for (int s = 0; s < NSLOT; s++) {
|
|
|
|
|
+ if (!g_swPro[s].ready())
|
|
|
|
|
+ continue;
|
|
|
|
|
+ // Deferred user-cal flash write (queued by the USB ISR; debounced so a calibration write-burst
|
|
|
|
|
+ // is one save). Per-slot: each Pro Controller saves its own mirror.
|
|
|
|
|
+ if (g_userCalDirty[s] &&
|
|
|
|
|
+ (unsigned long)(millis() - g_userCalDirtyMs[s]) > 250u) {
|
|
|
|
|
+ g_userCalDirty[s] = false;
|
|
|
|
|
+ saveUserCal((uint8_t)s);
|
|
|
|
|
+ }
|
|
|
|
|
+ // drain handshake/subcommand replies first (ordered) -- one report per slot per call so a
|
|
|
|
|
+ // bursty host-init doesn't starve the streamed 0x30
|
|
|
|
|
+ if (g_jcQh[s] != g_jcQt[s]) {
|
|
|
|
|
+ JcRep *r = &g_jcQ[s][g_jcQh[s]];
|
|
|
|
|
+ if (g_swPro[s].sendReport(r->rid, r->data, JC_REPLEN))
|
|
|
|
|
+ g_jcQh[s] = (uint8_t)((g_jcQh[s] + 1) % JCQ_N);
|
|
|
|
|
+ // one report per slot per call; the rest next loop
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ // not until the host has finished init + selected 0x30
|
|
|
|
|
+ if (g_swProReportMode[s] != 0x30)
|
|
|
|
|
+ continue;
|
|
|
|
|
+ // The Switch integrates the report's 3 IMU samples by SAMPLE COUNT at a fixed ~5 ms/sample (it ignores
|
|
|
|
|
+ // the timer byte and assumes a 3-samples-per-15ms genuine cadence). Streaming faster (e.g. 250 Hz x 3
|
|
|
|
|
+ // = 750 samples/s) over-credits gyro rotation ~3.75x, so residual bias accumulates into the slow
|
|
|
|
|
+ // orientation lean that builds over minutes and resets on replug. 15 ms matches the genuine push,
|
|
|
|
|
+ // making integration 1:1.
|
|
|
|
|
+ uint32_t interval = (g_swProRate == 2) ? USB_STREAM_MS :
|
|
|
|
|
+ (g_swProRate == 1) ? SW_PRO_REPORT_MS_120 :
|
|
|
|
|
+ SW_PRO_REPORT_MS;
|
|
|
|
|
+ if (millis() - g_swProLastMs[s] < interval)
|
|
|
|
|
+ continue;
|
|
|
|
|
+ g_swProLastMs[s] = millis();
|
|
|
|
|
+ uint8_t p[63];
|
|
|
|
|
+ switchProBuild((uint8_t)s, p);
|
|
|
|
|
+ g_swPro[s].sendReport(0x30, p, sizeof p);
|
|
|
}
|
|
}
|
|
|
- if (g_swProReportMode != 0x30)
|
|
|
|
|
- return; // not until the host has finished init + selected 0x30
|
|
|
|
|
- // The Switch integrates the report's 3 IMU samples by SAMPLE COUNT at a fixed ~5 ms/sample (it ignores the timer
|
|
|
|
|
- // byte and assumes a 3-samples-per-15ms genuine cadence). Streaming faster (e.g. 250 Hz x 3 = 750 samples/s)
|
|
|
|
|
- // over-credits gyro rotation ~3.75x, so residual bias accumulates into the slow orientation lean that builds over
|
|
|
|
|
- // minutes and resets on replug. 15 ms matches the genuine push, making integration 1:1.
|
|
|
|
|
- uint32_t interval = (g_swProRate == 2) ? USB_STREAM_MS :
|
|
|
|
|
- (g_swProRate == 1) ? SW_PRO_REPORT_MS_120 :
|
|
|
|
|
- SW_PRO_REPORT_MS;
|
|
|
|
|
- if (millis() - g_swProLastMs < interval)
|
|
|
|
|
- return;
|
|
|
|
|
- g_swProLastMs = millis();
|
|
|
|
|
- uint8_t p[63];
|
|
|
|
|
- switchProBuild(p);
|
|
|
|
|
- g_swPro.sendReport(0x30, p, sizeof p);
|
|
|
|
|
}
|
|
}
|