Bläddra i källkod

Fix rumble scaling and make it per mode

Jari 3 månader sedan
förälder
incheckning
977609fdda
4 ändrade filer med 92 tillägg och 41 borttagningar
  1. 19 13
      OpenPuck/config.cpp
  2. 7 1
      OpenPuck/config.h
  3. 29 6
      OpenPuck/webusb_config.cpp
  4. 37 21
      docs/index.html

+ 19 - 13
OpenPuck/config.cpp

@@ -24,12 +24,13 @@ int g_mDiv = 64, g_mFric = 94;
 // Per-type button config. back default {5,6,7,8} = L4->LB R4->RB L5->L3 R5->R3 (0..11 buttons, 12..15 D-pad,
 // 16/17 PS touch/mute, 18 Switch Capture). Switch differs: QAM defaults to Capture(18), A/B swap on, and
 // trackpad haptics off. qamMap 0 = unmapped (hardcoded per-mode behavior). ledBright 0 = no override.
-// rumble 1 = enabled (default), 0 = host rumble silenced for that type.
+// rumble 1 = enabled (default), 0 = host rumble silenced for that type. rumbleScale 200 = double strength
+// (default), 100 = native, 0 = off -- per emulated type.
 TypeCfg g_type[ET_COUNT] = {
-	/* ET_XBOX   */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1 },
-	/* ET_SWITCH */ { { 5, 6, 7, 8 }, 18, 1, 0, 0, 1 },
-	/* ET_DS4    */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1 },
-	/* ET_DS5    */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1 },
+	/* ET_XBOX   */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1, 200 },
+	/* ET_SWITCH */ { { 5, 6, 7, 8 }, 18, 1, 0, 0, 1, 200 },
+	/* ET_DS4    */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1, 200 },
+	/* ET_DS5    */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1, 200 },
 };
 uint8_t g_etype = ET_NONE;
 
@@ -55,6 +56,8 @@ void applyActiveType()
 		g_padHaptics = 1;
 		g_rumble = 1;
 		g_ledBright = 0;
+		g_rumbleScale =
+			200; // puck modes forward rumble verbatim; scale unused
 		return;
 	}
 	const TypeCfg &t = g_type[g_etype];
@@ -65,8 +68,10 @@ void applyActiveType()
 	g_padHaptics = t.padHaptics;
 	g_rumble = t.rumble;
 	g_ledBright = t.ledBright;
+	g_rumbleScale = t.rumbleScale;
 }
-// rumble strength % (200 = double); adjustable from the WebUSB panel
+// Live mirror of the active type's rumble strength % (200 = double). Set by applyActiveType(); the
+// per-type source of truth is g_type[et].rumbleScale, edited from the WebUSB panel / CDC console.
 uint8_t g_rumbleScale = 200;
 
 // poll rate defaults to POLL_US_DEFAULT (250 Hz), matching the real Valve puck (see config.h). The
@@ -76,12 +81,13 @@ uint8_t g_rumbleScale = 200;
 uint32_t g_pollUs = POLL_US_DEFAULT;
 
 #define CFG_FILE "/cfg.bin"
-// Struct layout/semantics changed (TypeCfg gained rumble byte); bump so old flash format is discarded ->
-// clean defaults once.
-#define CFG_MAGIC 0xCF
+// Struct layout/semantics changed (TypeCfg gained per-type rumbleScale byte; the old global rumbleScale byte
+// is now unused/reserved); bump so old flash format is discarded -> clean defaults once.
+#define CFG_MAGIC 0xD0
 struct Cfg {
+	// rsvd1 = ex-global rumbleScale; rumble strength is now per-emulated-type (see type[].rumbleScale).
 	uint8_t magic, mode, mDiv, mFric, rsvd0, pollU100, persistMode,
-		bootMode, chordBtn[3], rumbleScale;
+		bootMode, chordBtn[3], rsvd1;
 	// rxWin10: legacy RF tunable slot (window now fixed; ignored). lizKeep: the id9=0 hold enable (see
 	// haptics.h LIZKEEP_MS). landAll87: the verbatim-0x87-relay experiment toggle (haptics.h g_landAll87).
 	uint8_t rxWin10, lizKeep, landAll87;
@@ -99,7 +105,7 @@ void saveCfg()
 		  (uint8_t)(g_persistMode ? 1 : 0),
 		  g_bootMode,
 		  { g_chordBtn[0], g_chordBtn[1], g_chordBtn[2] },
-		  g_rumbleScale,
+		  0, // rsvd1 (ex-global rumbleScale; now per-type in type[])
 		  (uint8_t)(g_rxWin / 10),
 		  g_lizKeep,
 		  g_landAll87,
@@ -155,8 +161,8 @@ void loadCfg()
 							c.chordBtn[i] :
 							CHORD_DEF[i];
 
-			// 0 is a valid setting (rumble off)
-			g_rumbleScale = c.rumbleScale;
+			// per-type rumble strength loaded via c.type[] above; applyActiveType() (below)
+			// resolves the active type's rumbleScale into the g_rumbleScale live mirror.
 			// lizard-suppression keepalive enable (0/1; anything else = a pre-0xCE cfg leaked
 			// through -> keep the on default)
 			if (c.lizKeep <= 1)

+ 7 - 1
OpenPuck/config.h

@@ -122,6 +122,9 @@ extern int g_mDiv, g_mFric; // xbox/lizard mouse sensitivity divisor / friction%
 // ledBright = LED brightness sent to the controller on connect: 0 = no override (controller default),
 // 1-100 = brightness %. Steam sets brightness each session; emulated modes never do, so the controller
 // comes up at full brightness. Setting a value here preserves the preferred brightness across mode switches.
+// rumbleScale = host-rumble strength for this type, percent of decoded amplitude (0 = off, 100 = 1x native,
+// 200 = double, default). Applied in hapticSteamRumble() -- the choke point every emulated mode's rumble
+// passes through -- so each emulated controller can be tuned independently.
 struct TypeCfg {
 	uint8_t back[4];
 	uint8_t qamMap;
@@ -129,6 +132,7 @@ struct TypeCfg {
 	uint8_t padHaptics;
 	uint8_t ledBright;
 	uint8_t rumble;
+	uint8_t rumbleScale;
 };
 extern TypeCfg g_type[ET_COUNT];
 extern uint8_t
@@ -148,7 +152,9 @@ extern uint8_t g_ledBright;
 
 // Copy g_type[g_etype] into the live mirrors above (safe defaults when g_etype == ET_NONE).
 void applyActiveType();
-// rumble strength, percent of decoded amplitude (100 = 1x, 200 = 2x default), all modes
+// Live mirror of the active emulated type's rumble strength (percent of decoded amplitude; 100 = 1x,
+// 200 = 2x default). Read by hapticSteamRumble(); refreshed by applyActiveType() (200 for puck modes,
+// which forward rumble verbatim and never scale). Edit the per-type value in g_type[et].rumbleScale.
 extern uint8_t g_rumbleScale;
 // 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.

+ 29 - 6
OpenPuck/webusb_config.cpp

@@ -54,7 +54,9 @@ static void fwupAckPost(uint8_t status)
 //                [v13: per-slot link stats, 4x9B from p[145]: {pollsps u16, f1ps u16, newps u16, crc/s u8,
 //                 noRx/s u8, relay/s u8} -- each controller's own rates (the v4 aggregates are their sums)]
 //                [v14/v17: p[181] landAll87 (verbatim-0x87-relay experiment toggle)]
-#define WB_PAYLEN 180
+//                [v18: p[182..185] per-emulated-type rumble scale % (ET_XBOX/SWITCH/DS4/DS5); rumble
+//                 strength is now per-type -- p[53] mirrors the ACTIVE type for legacy readers]
+#define WB_PAYLEN 184
 // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob
 // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank
 // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without
@@ -79,13 +81,14 @@ static void webusbSendBlob()
 	p[0] = 0xA5;
 	p[1] = WB_PAYLEN;
 
-	// protocol version (17 = per-type rumble field (TypeCfg k=8), per-type stride 8->9; 16 = +configurable
+	// protocol version (18 = per-type rumble SCALE at p[182..185] (rumble strength is now per-emulated-type;
+	// p[53] mirrors the active type); 17 = per-type rumble field (TypeCfg k=8), per-type stride 8->9; 16 = +configurable
 	// lizard-map ops 0x11..0x15 / 0xAA frame, payload unchanged -- the panel MUST see >=16 before it dares
 	// send 0x11, or a blocking readLizard() would hang forever against a firmware that silently drops the
 	// unknown op; 15 = +staged firmware-update ops 0x20..0x24; 14 = +landAll87 toggle; 13 = +per-slot link
 	// stats; 12 = +relay rate + clock fingerprint; 11 = +reset cause; 10 = +ledBright per type; 9 = +per-type
 	// cfg; 8 = +per-slot link status; 7 = +raw accel; 6 = +swPro120/gyroScale)
-	p[2] = 17;
+	p[2] = 18;
 	p[3] = g_usbMode;
 	p[4] = (uint8_t)g_mDiv;
 	p[5] = (uint8_t)g_mFric;
@@ -142,7 +145,7 @@ static void webusbSendBlob()
 		for (uint8_t i = 0; i < 12 && buildId[i]; i++)
 			p[41 + i] = (uint8_t)buildId[i];
 	}
-	p[53] = g_rumbleScale; // rumble strength % (protocol v5)
+	p[53] = g_rumbleScale; // ACTIVE type's rumble strength % (legacy display; per-type at p[182..185], v18)
 
 	// Switch Pro report rate 0=66/1=120/2=full (protocol v6)
 	p[54] = g_swProRate;
@@ -254,6 +257,10 @@ static void webusbSendBlob()
 	}
 	// v14/v17: verbatim-0x87-relay experiment toggle (panel reflects + toggles it)
 	p[181] = g_landAll87;
+	// v18: per-emulated-type rumble strength % (200 = double). One byte per ET_* type; the panel edits these
+	// via write fields 30..33. p[53] above mirrors whichever type is active for legacy/aggregate readers.
+	for (int et = 0; et < ET_COUNT; et++)
+		p[182 + et] = g_type[et].rumbleScale;
 	// CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the
 	// panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a
 	// backgrounded tab, or the host briefly not servicing transferIn under load -- the FIFO never empties and
@@ -927,11 +934,27 @@ void webusbPoll()
 					}
 					break;
 
-				// rumble strength % (0=off, 100=1x, 200=double)
+				// LEGACY rumble strength % (0=off, 100=1x, 200=double): edits the ACTIVE emulated
+				// type (rumble strength is now per-type; kept so old panels / backups still apply).
 				case 22:
-					g_rumbleScale = v;
+					if (g_etype < ET_COUNT) {
+						g_type[g_etype].rumbleScale = v;
+						applyActiveType();
+					}
 					break;
 
+				// per-emulated-type rumble strength % (v18): fields 30..33 = ET_XBOX/SWITCH/DS4/DS5.
+				case 30:
+				case 31:
+				case 32:
+				case 33: {
+					uint8_t et = (uint8_t)(f - 30);
+					g_type[et].rumbleScale = v;
+					if (et == g_etype)
+						applyActiveType();
+					break;
+				}
+
 				// Switch Pro report rate (0=66Hz,1=120Hz,2=full)
 				case 23:
 					g_swProRate = (v <= 2) ? v : 2;

+ 37 - 21
docs/index.html

@@ -205,12 +205,6 @@
       <p class="note">Right-pad cursor in Steam-mode lizard (Steam closed) and in Xbox mode. Sensitivity is a divisor — lower = faster pointer. Glide = how long the cursor coasts after a flick.</p>
     </div>
 
-    <div class="card">
-      <h2>Rumble</h2>
-      <div class="row"><label>Strength</label><input type="range" id="rumble" min="0" max="255" step="5"><span class="val" id="rumbleV"></span></div>
-      <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><select id="swRate"></select></div>
@@ -509,7 +503,7 @@ function setTab(et){
     tab.onclick=()=>setTab(et); bar.appendChild(tab);
     // panel
     const sec=document.createElement("div"); sec.style.cssText="background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:12px 14px;display:none";
-    const rec={sec,tab,activeDot:tab.querySelector(".active-dot"),back:[],qam:null,abSwap:null,pad:null,led:null,ledV:null,rumble:null};
+    const rec={sec,tab,activeDot:tab.querySelector(".active-dot"),back:[],qam:null,abSwap:null,pad:null,led:null,ledV:null,rumble:null,rumbleScale:null,rumbleScaleV:null};
     // A/B swap + trackpad haptics + rumble toggles
     const tog=document.createElement("div"); tog.className="row";
     tog.innerHTML='<label>A/B + X/Y swap</label>';
@@ -547,6 +541,18 @@ function setTab(et){
       sl.addEventListener("change",()=>setField(40+et*9+7, +sl.value));
       sec.appendChild(row);
       rec.led=sl; rec.ledV=vspan; }
+    // Rumble strength (per-type; write fields 30..33, blob v18 at JS p[180+et]). 0=off, 100=native, 200=double.
+    { const row=document.createElement("div"); row.className="row";
+      const lab=document.createElement("label"); lab.textContent="Rumble strength"; row.appendChild(lab);
+      const sl=document.createElement("input"); sl.type="range"; sl.min=0; sl.max=255; sl.step=5; row.appendChild(sl);
+      const vspan=document.createElement("span"); vspan.className="val"; row.appendChild(vspan);
+      sl.addEventListener("input",()=>{ vspan.textContent=sl.value+"%"; });
+      sl.addEventListener("change",()=>setField(30+et, +sl.value));
+      sec.appendChild(row);
+      const note=document.createElement("p"); note.className="note";
+      note.textContent="Scales host rumble for this controller type (100% = native, 200% = double, 0% = off). Use the toggle above to disable rumble entirely.";
+      sec.appendChild(note);
+      rec.rumbleScale=sl; rec.rumbleScaleV=vspan; }
     host.appendChild(sec); typeEls.push(rec);
   });
   setTab(0);
@@ -745,7 +751,6 @@ function applyBlob(p){
   // the built-in default behavior, so the editor is shown only when the device is in Lizard mode.
   $("#lizardCard").style.display = (mode===3 && p[0]>=16)?"":"none";
   const battery=(p.length>36?p[36]:0);
-  const rumbleScale=(p.length>51?p[51]:200);
   const swProRate=(p.length>52?p[52]:2);
   const swGyro10=(p.length>53?p[53]:10);
   const rssi=(p.length>37?p[37]:0);
@@ -926,7 +931,7 @@ function applyBlob(p){
   $("#updInstalled").textContent = buildId || "—"; // mirrored on the Firmware-update tab
   updateFwGate(); // (re)evaluate the firmware-tab gate on every blob — version is at p[0]
   checkUpdateNotice(); // needs BOTH the installed version (this blob) and relCache (fetched on connect)
-  setSlider("mDiv",mDiv); setSlider("mFric",mFric); setSlider("rumble",rumbleScale);
+  setSlider("mDiv",mDiv); setSlider("mFric",mFric);
   $("#persistMode").textContent = persistMode? "on":"off"; $("#persistMode").classList.toggle("active",!!persistMode);
   // land-all-0x87 experiment toggle (blob v14/v17, firmware p[181] -> JS p[179])
   { const la=(p.length>179?p[179]:0); $("#landAll87").textContent = la?"on":"off"; $("#landAll87").classList.toggle("active",!!la); }
@@ -943,6 +948,9 @@ function applyBlob(p){
       rec.pad.textContent=pad?"on":"off"; rec.pad.classList.toggle("active",!!pad);
       rec.rumble.textContent=rum?"on":"off"; rec.rumble.classList.toggle("active",!!rum);
       if(document.activeElement!==rec.led){ rec.led.value=led; rec.ledV.textContent=(led===0)?"Auto":led+"%"; }
+      // per-type rumble strength (blob v18, firmware p[182+et] -> JS p[180+et]; defaults 200 for older fw)
+      { const rs=(p.length>180+et)?p[180+et]:200;
+        if(document.activeElement!==rec.rumbleScale){ rec.rumbleScale.value=rs; rec.rumbleScaleV.textContent=rec.rumbleScale.value+"%"; } }
       // dot marks the type matching the current puck mode
       rec.activeDot.style.display = (et===activeEt) ? "" : "none";
     });
@@ -958,7 +966,7 @@ function applyBlob(p){
   { 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+"%" : id==="hapBlockS" ? v+" s" : ""+v; }
+function fmtSlider(id,v){ return id==="hapBlockS" ? v+" s" : ""+v; }
 function setSlider(id,v){ const el=$("#"+id); if(document.activeElement!==el){ el.value=v; } $("#"+id+"V").textContent=fmtSlider(id,el.value); }
 
 let inflight=false;
@@ -983,9 +991,10 @@ function buildBackup(p, bp){
     bonds.push({slot:s, used:!!((mask>>s)&1), rec:hexEnc(rec)}); }
   const types=[];
   for(let et=0; et<TYPE_DEFS.length; et++){ const q=73+et*9;
-    types.push({back:[p[q],p[q+1],p[q+2],p[q+3]], qam:p[q+4], abSwap:p[q+5], pad:p[q+6], led:p[q+7], rumble:p[q+8]}); }
+    types.push({back:[p[q],p[q+1],p[q+2],p[q+3]], qam:p[q+4], abSwap:p[q+5], pad:p[q+6], led:p[q+7], rumble:p[q+8],
+                rumbleScale:(p.length>180+et?p[180+et]:200)}); }
   const cfg={ mode:p[1], mDiv:p[2], mFric:p[3], persistMode:p[22], chord:[p[23],p[24],p[25]],
-              rumble:p[51], swProRate:p[52], swGyro10:p[53], types };
+              swProRate:p[52], swGyro10:p[53], types };
   // version 2: include the lizard map so it survives a backup/restore cycle.
   // Guard on !lizardBusy to avoid a race with the one-shot lazy load (lizardExchange sets
   // lizardBusy while the initial 0x11 fetch is in flight); if the load is still pending the
@@ -1032,11 +1041,13 @@ async function importBackup(file){
   // driven while the import replays every setting onto the puck, then closes on Done / on error.
   modalOpen("Restore backup");
   const c=obj.config;
-  // Pre-count every write so the bar tracks real progress: settings (9 base fields), per-type config
-  // (9 each), the lizard bindings, 4 bond slots, and the final commit.
+  // Pre-count every write so the bar tracks real progress: 8 base settings (+1 legacy global rumble for
+  // old backups), per-type config (10 each: back/qam/swap/pad/led/rumble/rumbleScale), the lizard bindings,
+  // 4 bond slots, and the final commit.
   const typeN=Array.isArray(c.types)?Math.min(c.types.length,4):0;
   const lizardN=(Array.isArray(c.lizardMap) && lizardCapable())?Math.min(c.lizardMap.length,LZ_MAX):0;
-  const total=9 + typeN*9 + lizardN + 4 + 1;
+  const legacyRumbleN=(c.rumble!==undefined)?1:0;
+  const total=8 + legacyRumbleN + typeN*10 + lizardN + 4 + 1;
   let step=0, stage="";
   const tick=()=>modalStage(stage, Math.min(99,Math.floor(step*100/total)));
   try{
@@ -1058,9 +1069,12 @@ async function importBackup(file){
     const sf=async(f,v)=>{ await w([0x02, f, (v|0)&0xff]); };
     // replay every setting (each persists; none reboot). mode is applied by the 0x0E commit below.
     stage="Restoring settings"; tick();
-    await sf(1,c.mDiv); await sf(2,c.mFric); await sf(22,c.rumble); await sf(16,c.persistMode?1:0);
+    await sf(1,c.mDiv); await sf(2,c.mFric); await sf(16,c.persistMode?1:0);
     await sf(17,c.chord[0]); await sf(18,c.chord[1]); await sf(19,c.chord[2]);
     await sf(23,c.swProRate); await sf(24,c.swGyro10);
+    // Legacy backups stored ONE global rumble strength (field 22 now edits the active type); newer backups
+    // carry it per-type in c.types[].rumbleScale (restored below), so only replay the global one if present.
+    if(legacyRumbleN) await sf(22,c.rumble);
     // c.types is absent in some very old backups created before per-type paddle/haptic config
     // was added to the panel export; guard so a missing field never crashes the import mid-flight.
     if(typeN){
@@ -1068,9 +1082,11 @@ async function importBackup(file){
         for(let k=0;k<4;k++) await sf(40+et*9+k, t.back[k]);
         await sf(40+et*9+4, t.qam); await sf(40+et*9+5, t.abSwap?1:0);
         await sf(40+et*9+6, t.pad?1:0); await sf(40+et*9+7, t.led);
-        await sf(40+et*9+8, t.rumble!==undefined?t.rumble:1); }
-      log("settings replayed ("+(9+typeN*9)+" fields)");
-    } else { log("settings replayed (9 fields — no per-type config in this backup)"); }
+        await sf(40+et*9+8, t.rumble!==undefined?t.rumble:1);
+        // per-type rumble strength (field 30..33); default 200 (2x) for pre-v18 backups
+        await sf(30+et, t.rumbleScale!==undefined?t.rumbleScale:200); }
+      log("settings replayed ("+(8+legacyRumbleN+typeN*10)+" fields)");
+    } else { log("settings replayed ("+(8+legacyRumbleN)+" fields — no per-type config in this backup)"); }
     // restore the lizard map if the backup includes it (version 2+) and the puck speaks v16+.
     // pre-lizard-map backups simply lack c.lizardMap; skip silently so they still import cleanly.
     if(lizardN){
@@ -1939,8 +1955,8 @@ $("#factoryErase").onclick=async()=>{
 $("#persistMode").onclick=()=>{ const on=$("#persistMode").classList.contains("active"); setField(16, on?0:1); };
 $("#landAll87").onclick=async()=>{ const on=$("#landAll87").classList.contains("active"); await setField(29, on?0:1); log("land-all-0x87 "+(on?"disabled":"enabled")); };
 // Connection tuning: post-connect haptic block on (field 27) + seconds (field 28). (poll RX window is fixed.)
-for(const id of ["mDiv","mFric","rumble"]){
-  const field={mDiv:1,mFric:2,rumble:22}[id];
+for(const id of ["mDiv","mFric"]){
+  const field={mDiv:1,mFric:2}[id];
   $("#"+id).addEventListener("input", ()=>{ $("#"+id+"V").textContent=fmtSlider(id,$("#"+id).value); });
   $("#"+id).addEventListener("change", ()=>setField(field, +$("#"+id).value));
 }