|
|
@@ -4,11 +4,50 @@
|
|
|
#include "config.h"
|
|
|
#include "haptics.h"
|
|
|
#include <Adafruit_TinyUSB.h>
|
|
|
+#include <Adafruit_LittleFS.h>
|
|
|
+#include <InternalFileSystem.h>
|
|
|
#include <Arduino.h>
|
|
|
#include <string.h>
|
|
|
+using namespace Adafruit_LittleFS_Namespace;
|
|
|
|
|
|
SwitchProController g_switchPro;
|
|
|
|
|
|
+// Switch full-report (0x30) cadence. The console integrates the 3 IMU samples per report assuming ~5 ms/sample
|
|
|
+// (3 samples / 15 ms genuine); too high a rate over-integrates the gyro. With the accel scale fixed (gravity-
|
|
|
+// correction now works) 120 Hz is drift-free and lower-latency, so it is the DEFAULT. 66 Hz is the genuine
|
|
|
+// 2wiCC-matching compat fallback; "full" is the 4 ms PC rate for those who want lowest latency and tolerate it.
|
|
|
+#define SW_PRO_REPORT_MS 15u // 66 Hz
|
|
|
+#define SW_PRO_REPORT_MS_120 8u // ~120 Hz
|
|
|
+#define SW_ACCEL_DIV 4 // SC2 accel is +/-2g (16384/g); /4 -> the genuine Pro +/-8g (4096/g) the Switch cal expects
|
|
|
+
|
|
|
+uint8_t g_swProRate = 1; // 0 = 66Hz, 1 = 120Hz (default), 2 = full (~250Hz / USB_STREAM_MS)
|
|
|
+uint8_t g_swGyroScale10 = 10; // gyro sensitivity x10 (10 = 1.0x)
|
|
|
+
|
|
|
+// Persist the two Switch Pro motion settings in their own tiny file so they never trigger a global-Cfg reset.
|
|
|
+#define SWPRO_CFG_FILE "/swprocfg.bin"
|
|
|
+void swProSaveCfg(){
|
|
|
+ uint8_t b[3] = { 0x01, g_swProRate, g_swGyroScale10 }; // [ver][rate 0/1/2][gyroScale x10]
|
|
|
+ InternalFS.remove(SWPRO_CFG_FILE); File f(InternalFS);
|
|
|
+ if(f.open(SWPRO_CFG_FILE, FILE_O_WRITE)){ f.write(b,sizeof b); f.close(); }
|
|
|
+}
|
|
|
+static void swProLoadCfg(){
|
|
|
+ File f(InternalFS); uint8_t b[3];
|
|
|
+ if(f.open(SWPRO_CFG_FILE, FILE_O_READ)){
|
|
|
+ if(f.read(b,3)==3 && b[0]==0x01){
|
|
|
+ g_swProRate = (b[1]<=2) ? b[1] : 1; // 0=66Hz, 1=120Hz, 2=full; bad value -> default 120Hz
|
|
|
+ g_swGyroScale10 = (b[2]>=5 && b[2]<=30) ? b[2] : 10; // sane bounds (0.5x..3.0x)
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
static const uint8_t SWPRO_HID_DESC[]={
|
|
|
0x05,0x01,0x15,0x00,0x09,0x04,0xA1,0x01,0x85,0x30,0x05,0x01,0x05,0x09,0x19,0x01,
|
|
|
0x29,0x0A,0x15,0x00,0x25,0x01,0x75,0x01,0x95,0x0A,0x55,0x00,0x65,0x00,0x81,0x02,
|
|
|
@@ -52,15 +91,22 @@ static Adafruit_USBD_HID g_swPro;
|
|
|
static uint8_t g_swProReportMode=0; // 0 until the host's subcommand 0x03 selects 0x30 -> THEN we stream input
|
|
|
static const uint8_t JC_MAC[6]={0x7C,0xBB,0x8A,0x00,0x00,0x01}; // synthetic but stable controller MAC
|
|
|
static uint16_t jcRumbleAmp(const uint8_t r[4]){
|
|
|
- // Nintendo's 4-byte rumble encoding is frequency+amplitude packed through tables. For translation to the
|
|
|
- // Steam controller, use the amplitude-bearing fields directly and scale them into a 16-bit motor speed.
|
|
|
- if((r[0]==0x00 && r[1]==0x01 && r[2]==0x40 && r[3]==0x40) || (r[0]==0x00 && r[1]==0x00 && r[2]==0x01 && r[3]==0x40)) return 0;
|
|
|
- uint16_t amp = ((uint16_t)(r[1] & 0xFE) << 8) | r[3];
|
|
|
- return amp ? amp : 0x0800;
|
|
|
+ // Nintendo packs a high band (r[0],r[1]) and low band (r[2],r[3]) of frequency+amplitude. We only need a
|
|
|
+ // magnitude for the Steam motor, so pull the two amplitude fields: HF amp = r[1]>>1, LF amp = r[3]&0x3F.
|
|
|
+ // Every canonical idle/neutral frame -- 00 01 40 40, 00 00 01 40, and all-zero -- decodes to 0 this way, so a
|
|
|
+ // steady idle rumble stream maps to "off" instead of latching the motor on. NO forced floor: 0 amplitude -> 0.
|
|
|
+ uint8_t hf = r[1] >> 1; // high-band amplitude (neutral 0x01 -> 0)
|
|
|
+ uint8_t lf = r[3] & 0x3F; // low-band amplitude (neutral 0x40 -> 0)
|
|
|
+ uint8_t a = hf > lf ? hf : lf; // 0..0x7F
|
|
|
+ return (uint16_t)a << 9; // scale to ~16-bit motor speed
|
|
|
}
|
|
|
static void jcRumble(const uint8_t* p, uint16_t pn){
|
|
|
if(pn<9) return; // [timer][left rumble x4][right rumble x4]
|
|
|
- hapticSteamRumble(jcRumbleAmp(p+1), jcRumbleAmp(p+5));
|
|
|
+ uint16_t lo=jcRumbleAmp(p+1), hi=jcRumbleAmp(p+5);
|
|
|
+ static uint16_t lastLo=0, lastHi=0;
|
|
|
+ if(lo==lastLo && hi==lastHi) return; // only relay on change: the Switch streams rumble every frame, so
|
|
|
+ lastLo=lo; lastHi=hi; // re-sending unchanged values would flood the RF relay and loop the motor
|
|
|
+ hapticSteamRumble(lo, hi);
|
|
|
}
|
|
|
static int jcStick12(int16_t v, bool inv){ // steam int16 (center 0) -> 12-bit (center 0x800), clamped
|
|
|
int a = 2048 + (inv ? -((int)v>>4) : ((int)v>>4));
|
|
|
@@ -93,23 +139,46 @@ static void jcInputPrefix(uint8_t* out){
|
|
|
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);
|
|
|
out[0]=g_jcTimer++;
|
|
|
- out[1]=0x81; // full battery + USB powered (hid-nintendo bat_con)
|
|
|
+ out[1]=0x91; // 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. (2wiCC parity)
|
|
|
out[2]=(uint8_t)(jc); out[3]=(uint8_t)(jc>>8); out[4]=(uint8_t)(jc>>16);
|
|
|
+ out[3]|=0x80; // 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.
|
|
|
jcPackStick(out+5, g_in.lx, g_in.ly);
|
|
|
jcPackStick(out+8, g_in.rx, g_in.ry);
|
|
|
- out[11]=0;
|
|
|
+ out[11]=0x09; // rumble_input_report echo: genuine pad emits 0x09..0x0C; some Switch firmware expects this nonzero.
|
|
|
}
|
|
|
static void switchProBuild(uint8_t out[63]){
|
|
|
memset(out,0,63);
|
|
|
jcInputPrefix(out);
|
|
|
+ // Gyro slot order follows hid-nintendo: raw+6 = ROLL, raw+8 = PITCH, raw+10 = YAW. Steam-validated source
|
|
|
+ // routing (a proper rotation, det +1): SwitchX<-+gy, SwitchY<--gx, SwitchZ<-+gz.
|
|
|
+ //
|
|
|
+ // CRITICAL: 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 -- and if the
|
|
|
+ // accel frame has the opposite handedness, the fusion's gravity-correction term points the wrong way and the
|
|
|
+ // attitude estimate latches into a rotated solution (the intermittent ~45deg axis-mixing, per-unit roll bias,
|
|
|
+ // and "only a replug clears it" behaviour). So accel Y must be -ax to match the gyro's -gx pitch axis; with
|
|
|
+ // that, accel is also det +1 and resting gravity lands cleanly on Switch Z (the up axis).
|
|
|
+ // The SC2 accelerometer is +/-2g full-scale (~16384 counts = 1g; measured |a|=16364 at rest). 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 of gravity. Without this the console reads gravity as ~4g, REJECTS the accel for
|
|
|
+ // drift correction (it must be linear accel, not gravity), and the gyro's roll error accumulates into the slow
|
|
|
+ // ~45deg lean. Gyro is left at native scale (aiming sensitivity is right); only the accel magnitude was wrong.
|
|
|
+ int16_t aX=(int16_t)(g_in.ay/SW_ACCEL_DIV); // accel X <- +g_in.ay /4 (matches gyro +gy)
|
|
|
+ int16_t aY=(int16_t)((-(int16_t)g_in.ax)/SW_ACCEL_DIV);// accel Y <- -g_in.ax /4 (matches gyro -gx)
|
|
|
+ int16_t aZ=(int16_t)(g_in.az/SW_ACCEL_DIV); // accel Z <- +g_in.az /4 (matches gyro +gz)
|
|
|
+ int16_t groll =gscale((int16_t)g_in.gy); // gyro outputs scaled by the user sensitivity factor
|
|
|
+ int16_t gpitch=gscale((int16_t)(-(int16_t)g_in.gx));
|
|
|
+ int16_t gyaw =gscale((int16_t)g_in.gz);
|
|
|
for(int k=0;k<3;k++){
|
|
|
int o=12+k*12;
|
|
|
- out[o+0]=g_in.ax&0xFF; out[o+1]=g_in.ax>>8;
|
|
|
- out[o+2]=g_in.ay&0xFF; out[o+3]=g_in.ay>>8;
|
|
|
- out[o+4]=g_in.az&0xFF; out[o+5]=g_in.az>>8;
|
|
|
- out[o+6]=g_in.gx&0xFF; out[o+7]=g_in.gx>>8;
|
|
|
- out[o+8]=g_in.gy&0xFF; out[o+9]=g_in.gy>>8;
|
|
|
- out[o+10]=g_in.gz&0xFF; out[o+11]=g_in.gz>>8;
|
|
|
+ out[o+0]=aX&0xFF; out[o+1]=(aX>>8)&0xFF; // accel X (Switch)
|
|
|
+ out[o+2]=aY&0xFF; out[o+3]=(aY>>8)&0xFF; // accel Y (Switch)
|
|
|
+ out[o+4]=aZ&0xFF; out[o+5]=(aZ>>8)&0xFF; // accel Z (Switch)
|
|
|
+ out[o+6]=groll&0xFF; out[o+7]=(groll>>8)&0xFF; // gyro ROLL <- +g_in.gy (scaled)
|
|
|
+ out[o+8]=gpitch&0xFF; out[o+9]=(gpitch>>8)&0xFF; // gyro PITCH <- -g_in.gx (scaled)
|
|
|
+ out[o+10]=gyaw&0xFF; out[o+11]=(gyaw>>8)&0xFF; // gyro YAW <- +g_in.gz (scaled)
|
|
|
}
|
|
|
}
|
|
|
// --- Canonical factory SPI dumps the host reads for calibration. Neutral IMU + centered sticks so a fresh
|
|
|
@@ -133,6 +202,49 @@ static void jcBuildStickCal(){
|
|
|
uint16_t Rr[6]={C,C, R,R, R,R}; // right order: center(x,y), min(x,y), max(x,y)
|
|
|
jcPack12(&g_spiStickCal[0], L); jcPack12(&g_spiStickCal[9], Rr);
|
|
|
}
|
|
|
+// 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 never do, which is why this was previously unneeded). The data is the
|
|
|
+// canonical hid handshake blob; the Switch only validates the shape, not the key contents. Each is the 31-byte
|
|
|
+// reply body that follows the 0x21 input prefix + ack(0x81) + echoed-subcommand(0x01). (adapted from 2wiCC)
|
|
|
+static const uint8_t BT_PAIR_1[31]={ // type 1: echo type + controller BT addr + "Pro Controller" name
|
|
|
+ 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
|
|
|
+ 0x02, 0xE5,0xC8,0xE4,0x92,0x05,0xFF,0xC9,0x8A,0x7D,0xEA,0x15,0xF6,0x19,0xBA,0x82,0x13,
|
|
|
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
|
|
|
+static const uint8_t BT_PAIR_3[31]={0x03}; // type 3: save pairing (all zero body)
|
|
|
+// 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
|
|
|
+// resting IMU/neutral offset gets cancelled. We previously DISCARDED SPI writes and returned 0xFF (blank), so the
|
|
|
+// Switch's calibration never stuck (it appeared to do nothing and reverted on replug) -- which is why calibrating
|
|
|
+// did not fix the Mario Kart roll/neutral offset. Now 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];
|
|
|
+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(); }
|
|
|
+}
|
|
|
+static void saveUserCal(){
|
|
|
+ InternalFS.remove(SWCAL_FILE); File f(InternalFS);
|
|
|
+ if(f.open(SWCAL_FILE, FILE_O_WRITE)){ f.write(g_userCal,sizeof g_userCal); f.close(); }
|
|
|
+}
|
|
|
+// 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
|
|
|
+// 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){
|
|
|
+ bool changed=false;
|
|
|
+ for(uint8_t i=0;i<len && i<avail;i++){
|
|
|
+ uint32_t a=addr+i;
|
|
|
+ if(a>=0x8000 && a<0x8100){ if(g_userCal[a-0x8000]!=data[i]){ g_userCal[a-0x8000]=data[i]; changed=true; } }
|
|
|
+ }
|
|
|
+ if(changed){ g_userCalDirty=true; g_userCalDirtyMs=millis(); } // defer the flash write to task()
|
|
|
+}
|
|
|
static void spiRead(uint32_t addr, uint8_t len, uint8_t* dst){
|
|
|
for(uint8_t i=0;i<len;i++){
|
|
|
uint32_t a=addr+i; uint8_t v=0xFF;
|
|
|
@@ -141,7 +253,8 @@ static void spiRead(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];
|
|
|
- dst[i]=v; // unmapped (incl. user-cal 0x80xx) -> 0xFF = "blank flash" -> host uses factory blocks above
|
|
|
+ else if(a>=0x8000 && a<0x8100) v=g_userCal[a-0x8000]; // persisted user cal (0xFF blank -> factory fallback)
|
|
|
+ dst[i]=v;
|
|
|
}
|
|
|
}
|
|
|
// Reply FIFO: the host's handshake/subcommand reports arrive in the USB ISR (jcSet); we enqueue the canonical
|
|
|
@@ -167,14 +280,20 @@ static void jcSubcmd(uint8_t sub, const uint8_t* args, uint16_t alen){
|
|
|
jcInputPrefix(p);
|
|
|
p[13]=sub;
|
|
|
switch(sub){
|
|
|
+ case 0x01: { // manual BT pairing: 3-stage key exchange a real Switch runs over USB
|
|
|
+ const uint8_t* d=BT_PAIR_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
|
|
|
+ memcpy(&p[14], d, 31);
|
|
|
+ break; }
|
|
|
case 0x02: // request device info
|
|
|
p[12]=0x82;
|
|
|
- p[14]=0x04; p[15]=0x21; // firmware version (4.33)
|
|
|
+ p[14]=0x03; p[15]=0x48; // firmware version 3.72 (genuine Pro Controller value)
|
|
|
p[16]=0x03; // controller type: Pro Controller
|
|
|
p[17]=0x02; // fixed
|
|
|
memcpy(&p[18], JC_MAC, 6);
|
|
|
p[24]=0x01; // colors stored in SPI (0x6050)
|
|
|
- p[25]=0x02; // fixed
|
|
|
+ p[25]=0x01; // fixed
|
|
|
break;
|
|
|
case 0x10: { // SPI flash read -> echo [addr][len] then the data
|
|
|
if(alen<5){ p[12]=0x80; break; }
|
|
|
@@ -184,10 +303,19 @@ static void jcSubcmd(uint8_t sub, const uint8_t* args, uint16_t alen){
|
|
|
p[14]=args[0]; p[15]=args[1]; p[16]=args[2]; p[17]=args[3]; p[18]=rl;
|
|
|
spiRead(a, rl, &p[19]);
|
|
|
break; }
|
|
|
+ case 0x11: // SPI flash write -> persist user calibration so Switch motion-cal sticks
|
|
|
+ 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);
|
|
|
+ jcSpiWrite(a, args[4], &args[5], (alen>5)?(uint16_t)(alen-5):0);
|
|
|
+ }
|
|
|
+ p[12]=0x80; break; // write ACK
|
|
|
case 0x03: // set input report mode (0x30 = standard full) -> begin streaming
|
|
|
if(alen>=1 && args[0]==0x30) g_swProReportMode=0x30;
|
|
|
p[12]=0x80; break;
|
|
|
- case 0x04: p[12]=0x83; break; // trigger buttons elapsed time
|
|
|
+ case 0x04: // trigger buttons elapsed time -> canned reply (genuine pad returns data)
|
|
|
+ p[12]=0x83;
|
|
|
+ p[14]=0x00; p[15]=0xCC; p[16]=0x00; p[17]=0xEE; p[18]=0x00; p[19]=0xFF;
|
|
|
+ break;
|
|
|
case 0x21: p[12]=0xA0; break; // set NFC/IR config
|
|
|
default: p[12]=0x80; break; // 0x06/0x08/0x30/0x38/0x40/0x41/0x48/... generic positive ACK
|
|
|
}
|
|
|
@@ -221,25 +349,36 @@ static void jcSet(uint8_t rid, hid_report_type_t type, uint8_t const* b, uint16_
|
|
|
|
|
|
void SwitchProController::begin(){
|
|
|
USBDevice.setID(0x057E, 0x2009);
|
|
|
- USBDevice.setDeviceVersion(0x0211); // bumped from 0x0210 for the added wake-mouse interface (Windows caches config by VID:PID:bcdDevice)
|
|
|
+ USBDevice.setDeviceVersion(0x0212); // bumped from 0x0211 for 1ms poll interval (Windows caches config by VID:PID:bcdDevice)
|
|
|
USBDevice.setManufacturerDescriptor("Nintendo Co., Ltd.");
|
|
|
USBDevice.setProductDescriptor("Pro Controller");
|
|
|
jcBuildStickCal();
|
|
|
+ loadUserCal(); // restore any persisted Switch motion calibration (per-unit IMU bias correction)
|
|
|
+ swProLoadCfg(); // restore report-rate + gyro-scale settings
|
|
|
g_swPro.enableOutEndpoint(true);
|
|
|
g_swPro.setReportCallback(NULL, jcSet); // answer the Nintendo USB handshake + subcommands (else Steam never binds it)
|
|
|
g_swPro.setReportDescriptor(SWPRO_HID_DESC, sizeof SWPRO_HID_DESC);
|
|
|
- g_swPro.setPollInterval(4);
|
|
|
+ g_swPro.setPollInterval(1); // 1ms bInterval so the RF rate is the only latency limit (matches Xbox)
|
|
|
g_swPro.begin();
|
|
|
}
|
|
|
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
|
|
|
}
|
|
|
if(g_swProReportMode!=0x30) return; // not until the host has finished init + selected 0x30
|
|
|
- if(millis()-g_swProLastMs<USB_STREAM_MS) return; g_swProLastMs=millis();
|
|
|
+ // Stream the standard full report at ~66 Hz (15 ms), NOT the 250 Hz USB_STREAM_MS the PC modes use. 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). At 250 Hz x 3 samples = 750 samples/s it credits ~3.75 s of
|
|
|
+ // gyro rotation per real second, so any residual gyro bias accumulates ~3.75x too fast -> the slow orientation
|
|
|
+ // lean that builds over minutes and resets on replug. 15 ms matches the genuine push (and 2wiCC's ~60 Hz /
|
|
|
+ // MissionControl), making integration 1:1. (Buttons/sticks on the Switch are genuine-rate-limited anyway.)
|
|
|
+ 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);
|
|
|
}
|