瀏覽代碼

switch pro polling rate options

Jariullah Safi 4 月之前
父節點
當前提交
b1ea128511
共有 4 個文件被更改,包括 49 次插入 和 27 次删除
  1. 2 2
      OpenPuck/config.h
  2. 22 13
      OpenPuck/mode_switch_pro.cpp
  3. 7 5
      OpenPuck/webusb_config.cpp
  4. 18 7
      docs/index.html

+ 2 - 2
OpenPuck/config.h

@@ -74,9 +74,9 @@ extern uint8_t g_qamMap;        // QAM (3 dots) physical button -> same code spa
 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_swProRate;     // Switch Pro report cadence: 0 = 66Hz (15ms, compat), 1 = 120Hz (8ms, DEFAULT), 2 = full (~250Hz)
 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
+void swProSaveCfg();            // persist g_swProRate + 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)

+ 22 - 13
OpenPuck/mode_switch_pro.cpp

@@ -12,19 +12,21 @@ using namespace Adafruit_LittleFS_Namespace;
 
 SwitchProController g_switchPro;
 
-// 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.
-// 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
+// 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_swPro120 = 0;          // 0 = 66Hz (default), 1 = 120Hz
+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_swPro120, g_swGyroScale10 };   // [ver][rate][gyroScale x10]
+  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(); }
 }
@@ -32,7 +34,7 @@ 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_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();
@@ -158,15 +160,22 @@ 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 aY=(int16_t)(-(int16_t)g_in.ax);
+  // 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.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+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)
@@ -360,7 +369,7 @@ 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.)
-  uint32_t interval = g_swPro120 ? SW_PRO_REPORT_MS_120 : SW_PRO_REPORT_MS;
+  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);

+ 7 - 5
OpenPuck/webusb_config.cpp

@@ -4,6 +4,7 @@
 #include "rf_link.h"
 #include "haptics.h"
 #include "puck_hid.h"
+#include "triton.h"   // g_in raw IMU (diagnostic readout)
 #include "build_info.h"
 #include <Arduino.h>
 #include <string.h>
@@ -15,14 +16,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][swPro120][swGyroScale10]
-#define WB_PAYLEN 54
+//                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
+#define WB_PAYLEN 60   // keep the blob under 64 bytes total (readBlob reads one 64-byte packet)
 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]=6;                          // protocol version (6 = +swPro120/gyroScale; 5 = +rumbleScale; 4 = +battery/rssi/git)
+  p[2]=7;                          // protocol version (7 = +raw accel; 6 = +swPro120/gyroScale; 5 = +rumbleScale)
   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,8 +48,9 @@ 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[54]=g_swProRate;                                                   // Switch Pro report rate 0=66/1=120/2=full (protocol v6)
   p[55]=g_swGyroScale10;                                               // Switch Pro gyro sensitivity x10 (protocol v6)
+  { int16_t a[3]={g_in.ax,g_in.ay,g_in.az}; memcpy(&p[56], a, 6); }    // raw accelerometer for scale diagnostics (protocol v7)
   usb_web.write(p,sizeof p); usb_web.flush();
 }
 #if OPK_LOG
@@ -121,7 +123,7 @@ 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 23: g_swProRate = (v<=2)?v:1; swProSaveCfg(); persist=false; break;   // Switch Pro report rate (0=66Hz,1=120Hz,2=full)
           case 24: g_swGyroScale10 = (v>=5&&v<=30)?v:10; swProSaveCfg(); persist=false; break;   // Switch Pro gyro scale x10
         }
         if(persist) saveCfg();

+ 18 - 7
docs/index.html

@@ -92,6 +92,8 @@
         <div class="stat logonly"><div class="k">Poll period act/want</div><div class="v" id="stPollPer">—</div></div>
         <div class="stat"><div class="k">Build (git)</div><div class="v" id="stBuild">—</div></div>
       </div>
+      <p class="note" style="margin-top:8px"><strong>IMU (raw):</strong> <span id="stImu" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
+        Raw SC2 accel (before scaling). This controller is a ±2 g sensor (~16384 = 1 g); the Switch report divides it by 4 to present the genuine ±8 g (4096 = 1 g) scale so the console's gravity-correction engages and the gyro stops drifting.</p>
       <p class="note logonly">Polls/s ≈ 250 expected; it's capped by Loop period (≈4000 µs needed). "Slowest stage" is the loop section eating the most time per iteration.</p>
       <div class="row" style="margin:10px 0 0"><button id="hapClear">Clear stuck buzz</button>
         <span class="note" style="flex:1">If the controller's haptics get stuck/buzzing, this re-inits them (same as pressing Steam).</span>
@@ -120,10 +122,8 @@
 
     <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>Report rate</label><select id="swRate"></select></div>
+      <p class="note" style="margin:0 0 6px">120 Hz (default) is low-latency and drift-free. 66 Hz is a genuine-cadence compatibility fallback. Full rate (~250 Hz) is lowest-latency but non-genuine — use only if your console tolerates it.</p>
       <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>
@@ -178,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 report-rate options (0=66Hz, 1=120Hz default, 2=full ~250Hz)
+{ const sel=document.getElementById("swRate");
+  for(const [lbl,v] of [["120 Hz (default)",1],["66 Hz (compatibility)",0],["Full rate (~250 Hz)",2]]){
+    const o=document.createElement("option"); o.value=v; o.textContent=lbl; 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]]){
@@ -257,7 +262,7 @@ 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 swProRate=(p.length>52?p[52]:1);                  // Switch Pro report rate 0/1/2 (protocol v6+; default 120Hz)
   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>';
@@ -283,7 +288,13 @@ 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);
+  // raw accelerometer diagnostic (protocol v7): 3x s16 LE at p[54..59]
+  if(p.length>=60){ const s16=(o)=>{ let v=p[o]|(p[o+1]<<8); return v>32767?v-65536:v; };
+    const ax=s16(54),ay=s16(56),az=s16(58);
+    const amag=Math.round(Math.sqrt(ax*ax+ay*ay+az*az));
+    $("#stImu").textContent = `a=(${ax}, ${ay}, ${az})  |a|=${amag}`;
+  }
+  { const sel=$("#swRate"); if(document.activeElement!==sel) sel.value=swProRate; }
   { 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]; }
 }
@@ -399,7 +410,7 @@ 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); };
+$("#swRate").addEventListener("change", ()=>setField(23, +$("#swRate").value));
 $("#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));