Explorar o código

add optional higher polling mode for switch

Jariullah Safi hai 4 meses
pai
achega
ab2fa99a4b
Modificáronse 4 ficheiros con 74 adicións e 10 borrados
  1. 5 0
      OpenPuck/config.h
  2. 41 7
      OpenPuck/mode_switch_pro.cpp
  3. 7 3
      OpenPuck/webusb_config.cpp
  4. 21 0
      docs/index.html

+ 5 - 0
OpenPuck/config.h

@@ -72,6 +72,11 @@ extern uint8_t g_abSwap;        // 1 = swap A/B and X/Y (Nintendo face-button la
 extern uint8_t g_back[4];       // back paddles L4,R4,L5,R5 -> button codes (0..15 standard, 16=PS Touch Click, 17=PS5 Mute)
 extern uint8_t g_qamMap;        // QAM (3 dots) physical button -> same code space (0 = default/unmapped)
 extern uint8_t g_rumbleScale;   // rumble strength, percent of decoded amplitude (100 = 1x, 200 = 2x default), all modes
+// Switch Pro motion settings. Persisted in their OWN flash file (mode_switch_pro.cpp), NOT in Cfg -- so changing
+// them never resets the rest of the config. Set from the WebUSB panel.
+extern uint8_t g_swPro120;      // 0 = 66Hz (15ms) report cadence [genuine, default], 1 = 120Hz (8ms, lower latency)
+extern uint8_t g_swGyroScale10; // Switch Pro gyro sensitivity x10 (10 = 1.0x default; 5/15/20/25/30 = 0.5..3.0x)
+void swProSaveCfg();            // persist g_swPro120 + g_swGyroScale10 to their flash file
 
 #define POLL_US_DEFAULT 4000u   // 250 Hz -- matches SC2 input report rate (1000000/250 = 4000 us)
 #define USB_STREAM_MS   4u      // host-side HID stream cadence for translated modes (~250 Hz)

+ 41 - 7
OpenPuck/mode_switch_pro.cpp

@@ -12,9 +12,39 @@ using namespace Adafruit_LittleFS_Namespace;
 
 SwitchProController g_switchPro;
 
-// Switch full-report (0x30) cadence. MUST be ~15 ms (66 Hz), the genuine 3-samples-per-15ms rate -- NOT the 4 ms
+// Switch full-report (0x30) cadence. Default ~15 ms (66 Hz), the genuine 3-samples-per-15ms rate -- NOT the 4 ms
 // PC streaming rate -- or the console over-integrates the gyro (see task()). 2wiCC uses ~60 Hz; this matches.
-#define SW_PRO_REPORT_MS 15u
+// g_swPro120 switches to 8 ms (~120 Hz) for lower latency (may reintroduce gyro drift on some consoles).
+#define SW_PRO_REPORT_MS    15u
+#define SW_PRO_REPORT_MS_120 8u
+
+uint8_t g_swPro120 = 0;          // 0 = 66Hz (default), 1 = 120Hz
+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_swPro120, g_swGyroScale10 };   // [ver][rate][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_swPro120 = b[1]?1:0;
+      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,
@@ -128,16 +158,18 @@ static void switchProBuild(uint8_t out[63]){
   // 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).
-  int16_t gpitch=(int16_t)(-(int16_t)g_in.gx);
   int16_t aY=(int16_t)(-(int16_t)g_in.ax);
+  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.ay&0xFF;  out[o+1]=g_in.ay>>8;       // accel X (Switch) <- +g_in.ay   (matches gyro +gy)
     out[o+2]=aY&0xFF;       out[o+3]=(aY>>8)&0xFF;      // accel Y (Switch) <- -g_in.ax   (matches gyro -gx)
     out[o+4]=g_in.az&0xFF;  out[o+5]=g_in.az>>8;        // accel Z (Switch) <- +g_in.az   (matches gyro +gz)
-    out[o+6]=g_in.gy&0xFF;  out[o+7]=g_in.gy>>8;        // gyro ROLL  <- +g_in.gy
-    out[o+8]=gpitch&0xFF;   out[o+9]=(gpitch>>8)&0xFF;  // gyro PITCH <- -g_in.gx
-    out[o+10]=g_in.gz&0xFF; out[o+11]=g_in.gz>>8;       // gyro YAW   <- +g_in.gz
+    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
@@ -307,6 +339,7 @@ void SwitchProController::begin(){
   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);
@@ -327,7 +360,8 @@ void SwitchProController::task(){
   // 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.)
-  if(millis()-g_swProLastMs<SW_PRO_REPORT_MS) return; g_swProLastMs=millis();
+  uint32_t interval = g_swPro120 ? 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);
 }

+ 7 - 3
OpenPuck/webusb_config.cpp

@@ -15,14 +15,14 @@ Adafruit_USBD_WebUSB usb_web;
 //                [qos][persistMode][chordBtn B][chordBtn X][chordBtn Y][pollsps_lo][pollsps_hi]
 //                [loopPeriod_lo][loopPeriod_hi][loopWorstIdx][loopWorstUs_lo][loopWorstUs_hi]
 //                [pollPeriod_lo][pollPeriod_hi][logEnabled][battery%][rssi|dBm|]
-//                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale]
-#define WB_PAYLEN 52
+//                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10]
+#define WB_PAYLEN 54
 static void webusbSendBlob(){
   if(!usb_web.connected()) return;
   bool up = (g_connSlot>=0 && (millis()-g_connReplyMs) < 300);
   uint8_t p[2+WB_PAYLEN];
   p[0]=0xA5; p[1]=WB_PAYLEN;
-  p[2]=5;                          // protocol version (5 = +rumbleScale; 4 = +battery/rssi/git; 2 = chordBtn[3])
+  p[2]=6;                          // protocol version (6 = +swPro120/gyroScale; 5 = +rumbleScale; 4 = +battery/rssi/git)
   p[3]=g_usbMode; p[4]=(uint8_t)g_mDiv; p[5]=(uint8_t)g_mFric; p[6]=g_qamMap; p[7]=g_abSwap;
   p[8]=g_back[0]; p[9]=g_back[1]; p[10]=g_back[2]; p[11]=g_back[3];
   p[12]=(g_connSlot>=0)?(uint8_t)g_connSlot:0xFF;
@@ -47,6 +47,8 @@ static void webusbSendBlob(){
   memset(&p[41],0,12);                                                 // 12B ASCII git hash, NUL-padded
   { const char* h=OPK_GIT_HASH; for(uint8_t i=0;i<12 && h[i];i++) p[41+i]=(uint8_t)h[i]; }
   p[53]=g_rumbleScale;                                                 // rumble strength % (protocol v5)
+  p[54]=g_swPro120;                                                    // Switch Pro 120Hz toggle (protocol v6)
+  p[55]=g_swGyroScale10;                                               // Switch Pro gyro sensitivity x10 (protocol v6)
   usb_web.write(p,sizeof p); usb_web.flush();
 }
 #if OPK_LOG
@@ -119,6 +121,8 @@ void webusbPoll(){
           case 20: armDebugCdcNextBoot(); usb_web.flush(); delay(40); NVIC_SystemReset(); break;  // reboot once WITH the CDC serial console (puck mode), then auto-revert
           case 21: g_qamMap = v; break;   // QAM physical button remap code (0=default/unmapped)
           case 22: g_rumbleScale = v; break;   // rumble strength % (0=off, 100=1x, 200=double)
+          case 23: g_swPro120 = v?1:0; swProSaveCfg(); persist=false; break;   // Switch Pro report rate (0=66Hz,1=120Hz)
+          case 24: g_swGyroScale10 = (v>=5&&v<=30)?v:10; swProSaveCfg(); persist=false; break;   // Switch Pro gyro scale x10
         }
         if(persist) saveCfg();
         webusbSendBlob();

+ 21 - 0
docs/index.html

@@ -118,6 +118,16 @@
       <p class="note">Scales rumble amplitude across all modes (100% = native, 200% = double, 0% = off).</p>
     </div>
 
+    <div class="card">
+      <h2>Switch Pro motion</h2>
+      <div class="row"><label>Report rate</label>
+        <button id="swRate">66 Hz</button>
+        <span class="note" style="flex:1">66 Hz matches a real Pro Controller (no gyro drift). 120 Hz is lower-latency but may reintroduce slow gyro drift on some consoles.</span>
+      </div>
+      <div class="row"><label>Gyro scale</label><select id="swGyro"></select></div>
+      <p class="note">Gyro sensitivity multiplier for Switch Pro mode (1.0× = unchanged).</p>
+    </div>
+
 
     <div class="card">
       <h2>Button mapping</h2>
@@ -168,6 +178,11 @@ for(const sel of document.querySelectorAll("select.back")){
 for(const sel of document.querySelectorAll("select.chord")){
   MODE_NAMES.forEach((n,i)=>{ const o=document.createElement("option"); o.value=i; o.textContent=n; sel.appendChild(o); });
 }
+// Switch Pro gyro-scale options (value = scale x10, so it fits in one byte)
+{ const sel=document.getElementById("swGyro");
+  for(const [lbl,v] of [["0.5×",5],["1.0×",10],["1.5×",15],["2.0×",20],["2.5×",25],["3.0×",30]]){
+    const o=document.createElement("option"); o.value=v; o.textContent=lbl; sel.appendChild(o); }
+}
 
 async function connect(){
   try{
@@ -242,6 +257,8 @@ function applyBlob(p){
   // status
   const battery=(p.length>36?p[36]:0);                    // controller battery % (report 0x43); 0 = unknown
   const rumbleScale=(p.length>51?p[51]:200);              // rumble strength % (protocol v5+; older blob -> default)
+  const swPro120=(p.length>52?p[52]:0);                   // Switch Pro 120Hz toggle (protocol v6+)
+  const swGyro10=(p.length>53?p[53]:10);                  // Switch Pro gyro scale x10 (protocol v6+)
   const rssi=(p.length>37?p[37]:0);                       // RAW signal strength |dBm| (0 = no sample)
   $("#stLink").innerHTML = up? '<span class="pill up">up</span>' : '<span class="pill dn">down</span>';
   $("#stBatt").textContent = (up && battery)? battery+"%" : "—";
@@ -266,6 +283,8 @@ function applyBlob(p){
   $("#persistMode").textContent = persistMode? "on":"off"; $("#persistMode").classList.toggle("active",!!persistMode);
   for(const sel of document.querySelectorAll("select.back")){ if(document.activeElement!==sel) sel.value=back[+sel.dataset.i]; }
   { const sel=$("#qamMap"); if(document.activeElement!==sel) sel.value=qamMap; }
+  $("#swRate").textContent = swPro120? "120 Hz":"66 Hz"; $("#swRate").classList.toggle("active",!!swPro120);
+  { const sel=$("#swGyro"); if(document.activeElement!==sel) sel.value=swGyro10; }
   for(const sel of document.querySelectorAll("select.chord")){ if(document.activeElement!==sel) sel.value=chord[+sel.dataset.i]; }
 }
 function fmtSlider(id,v){ return id==="rumble" ? v+"%" : ""+v; }
@@ -380,6 +399,8 @@ for(const sel of document.querySelectorAll("select.back")){
   sel.addEventListener("change", ()=>setField(5 + +sel.dataset.i, +sel.value));
 }
 $("#qamMap").addEventListener("change", ()=>setField(21, +$("#qamMap").value));
+$("#swRate").onclick=()=>{ const on=$("#swRate").classList.contains("active"); setField(23, on?0:1); };
+$("#swGyro").addEventListener("change", ()=>setField(24, +$("#swGyro").value));
 for(const sel of document.querySelectorAll("select.chord")){
   sel.addEventListener("change", ()=>setField(CHORD_FIELD[+sel.dataset.i], +sel.value));
 }