Pārlūkot izejas kodu

not really working

Jari 3 mēneši atpakaļ
vecāks
revīzija
d30e2f2b02

+ 1 - 0
OpenPuck/OpenPuck.ino

@@ -311,6 +311,7 @@ void loop()
 	faultDiagStackTick(); // per-task stack headroom (self-gated ~1Hz) -- usbd-overflow hypothesis check
 	faultDiagFlightTick(); // flight recorder: refresh vitals + heartbeat crumb + live CDC vitals line
 	hapticStabTask(); // stability-test keepalive buzz (no-op unless armed via WebUSB)
+	hapticRumblePwmTask(); // duty-cycle host rumble for the strength setting (no-op at 0%/>=100%)
 	// cross-check HFCLK(micros) vs LFCLK(millis) once a second -- cheap, both builds (clone clock diagnostic)
 	clockDiagTick();
 	if (g_dirty) {

+ 4 - 4
OpenPuck/config.cpp

@@ -24,8 +24,8 @@ 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. rumbleScale 100 = native strength
-// (default), 200 = double (max), 0 = off -- per emulated type.
+// rumble 1 = enabled (default), 0 = host rumble silenced for that type. rumbleScale 100 = full/native
+// strength (default), 0 = off; below 100 the motor is PWM-attenuated -- per emulated type.
 TypeCfg g_type[ET_COUNT] = {
 	/* ET_XBOX   */ { { 5, 6, 7, 8 }, 0, 0, 1, 0, 1, 100 },
 	/* ET_SWITCH */ { { 5, 6, 7, 8 }, 18, 1, 0, 0, 1, 100 },
@@ -70,8 +70,8 @@ void applyActiveType()
 	g_ledBright = t.ledBright;
 	g_rumbleScale = t.rumbleScale;
 }
-// Live mirror of the active type's rumble strength % (100 = native default, 200 = double max). Set by
-// applyActiveType(); the per-type source of truth is g_type[et].rumbleScale, edited from the WebUSB panel.
+// Live mirror of the active type's rumble strength % (100 = full/native default; below 100 = PWM-attenuated).
+// Set by applyActiveType(); the per-type source of truth is g_type[et].rumbleScale, edited from the panel.
 uint8_t g_rumbleScale = 100;
 
 // poll rate defaults to POLL_US_DEFAULT (250 Hz), matching the real Valve puck (see config.h). The

+ 7 - 6
OpenPuck/config.h

@@ -122,9 +122,10 @@ 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
-// default, 200 = double max). Applied in hapticSteamRumble() -- the choke point every emulated mode's rumble
-// passes through -- so each emulated controller can be tuned independently.
+// rumbleScale = host-rumble strength for this type, percent (0 = off, 100 = full/native default). The paired
+// controller renders relayed rumble at a fixed amplitude (ignores magnitude), so below 100 the motor is
+// duty-cycled (PWM) to feel weaker -- see hapticRumblePwmTask(). 100 is the native ceiling; it attenuates
+// only. Per emulated type so each emulated controller can be tuned independently.
 struct TypeCfg {
 	uint8_t back[4];
 	uint8_t qamMap;
@@ -152,9 +153,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();
-// Live mirror of the active emulated type's rumble strength (percent of decoded amplitude; 100 = 1x
-// default, 200 = 2x max). Read by hapticSteamRumble(); refreshed by applyActiveType() (100 for puck modes,
-// which forward rumble verbatim and never scale). Edit the per-type value in g_type[et].rumbleScale.
+// Live mirror of the active emulated type's rumble strength (percent; 100 = full/native default, below
+// which the rumble is PWM-attenuated). Read by hapticSteamRumble()/hapticRumblePwmTask(); refreshed by
+// applyActiveType() (100 for puck modes, which forward rumble verbatim). Source: 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.

+ 83 - 13
OpenPuck/haptics.cpp

@@ -79,6 +79,23 @@ static unsigned long g_rumble80Ms[NSLOT] = { 0 };
 // stuck-rumble watchdog is independent
 static bool g_rumble80On[NSLOT] = { false, false, false, false };
 
+// ---- Host-rumble strength via duty-cycle PWM ---------------------------------------------------------
+// The paired controller renders relayed rumble (report 0x80) at a FIXED amplitude: it turns the motor
+// on/off and IGNORES the intensity/speed magnitude we send. Only Steam's own 0x87 haptic-amplifier config
+// (absent in emulated modes -- there is no Steam) makes native strength vary. So to give every emulated
+// mode a working strength control we PULSE the motor on/off: the LRA mechanically averages a fast on/off
+// stream into a lower PERCEIVED strength. Duty = g_rumbleScale% (0 = off, 100 = full/native; the motor's
+// fixed level is the ceiling, so >100 can't boost and is clamped to steady-on). The host's desired state
+// is recorded here and the PWM task modulates it between host updates (mode decoders only call
+// hapticSteamRumble on CHANGE, so nothing else refreshes the stream while a steady rumble is held).
+#define RUMBLE_PWM_PERIOD_MS 20u // one on+off cycle; the motor smooths it (~5 poll cycles @250Hz)
+static bool g_rumbleWantOn[NSLOT] = { false, false, false, false };
+static uint16_t g_rumbleWantLo[NSLOT] = { 0 };
+static uint16_t g_rumbleWantHi[NSLOT] = { 0 };
+static bool g_rumblePwmOn[NSLOT] = { false, false, false,
+				     false }; // last phase we drove
+static unsigned long g_rumblePwmStart[NSLOT] = { 0 }; // current PWM period start
+
 // ---- relay rings: one per bond slot. Multi-producer (USB ISR + loop-context console/xinput), one consumer
 // per slot (rfConnFlushRelay on that slot's poll turn). Producers serialize under PRIMASK.
 struct RelayMsg {
@@ -308,21 +325,12 @@ void haptic82HostReport(const uint8_t *p, uint16_t n)
 	// 0x82 is a discrete pad click -- the spurious end-of-movement "click"/buzz the real puck never produces.
 	g_haptic82On = haptic82PayloadOn(p, n);
 }
-bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
+// Build + enqueue ONE Steam/Triton 0x80 rumble frame at the given (native, un-attenuated) amplitude. This is
+// the physical drive; strength attenuation is done by the PWM task toggling this on/off, NOT by scaling the
+// amplitude (the controller ignores magnitude -- see the PWM notes above).
+static bool sendRumble80(uint8_t slot, uint16_t lowFreq, uint16_t highFreq)
 {
-	if (slot >= NSLOT)
-		return false;
-	// user rumble-strength scale (percent; 200 = double). Clamp to 16-bit.
-	if (g_rumbleScale != 100) {
-		uint32_t l = (uint32_t)lowFreq * g_rumbleScale / 100,
-			 h = (uint32_t)highFreq * g_rumbleScale / 100;
-		lowFreq = (l > 0xFFFF) ? 0xFFFF : (uint16_t)l;
-		highFreq = (h > 0xFFFF) ? 0xFFFF : (uint16_t)h;
-	}
 	bool on = lowFreq || highFreq;
-	// per-type rumble disable: drop ON commands; zero/stop still pass to clear any queued relay
-	if (on && !g_rumble)
-		return false;
 	// Per-slot settle gate (the per-slot reconnect block + link-up check). 0x82 haptics in Steam mode use the
 	// same gate; for XInput, the host only sends a stream while a controller is connected, so this also doubles
 	// as "no controller here, no relay".
@@ -353,6 +361,62 @@ bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
 	g_rumble80On[slot] = on;
 	return true;
 }
+bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot)
+{
+	if (slot >= NSLOT)
+		return false;
+	bool on = lowFreq || highFreq;
+	// per-type rumble disable: drop ON commands; zero/stop still pass to clear any queued relay
+	if (on && !g_rumble)
+		return false;
+	// user strength 0% -> silence entirely (treat as a stop, still relays the zero to clear any latch)
+	if (g_rumbleScale == 0)
+		on = false;
+	// Record the host's desired state; hapticRumblePwmTask() modulates the physical motor between the
+	// (change-only) host updates.
+	g_rumbleWantOn[slot] = on;
+	g_rumbleWantLo[slot] = lowFreq;
+	g_rumbleWantHi[slot] = highFreq;
+	if (!on) { // host stop (or 0% mute): drive off now, reset the PWM phase
+		g_rumblePwmOn[slot] = false;
+		g_rumblePwmStart[slot] = 0;
+		return sendRumble80(slot, 0, 0);
+	}
+	// on: start this PWM period in the ON phase and drive it now for immediate response. At >=100% the task
+	// leaves it steady-on (no toggling); at 1..99% the task pulses it off/on for the rest of the period.
+	g_rumblePwmStart[slot] = millis();
+	g_rumblePwmOn[slot] = true;
+	return sendRumble80(slot, lowFreq, highFreq);
+}
+// Duty-cycle the latched rumble so g_rumbleScale% controls PERCEIVED strength on a controller that renders
+// relayed rumble as a fixed-amplitude on/off. Called once per poll cycle (from loop, alongside the other
+// haptic housekeeping). No-op at 0% (already silenced at command time) and at >=100% (steady-on, no pulsing).
+void hapticRumblePwmTask()
+{
+	uint8_t duty = g_rumbleScale;
+	if (duty == 0 || duty >= 100)
+		return; // off / full: nothing to modulate
+	unsigned long now = millis();
+	uint32_t period = RUMBLE_PWM_PERIOD_MS;
+	uint32_t onMs = period * duty / 100; // motor-driven time per period
+	for (uint8_t s = 0; s < NSLOT; s++) {
+		if (!g_rumbleWantOn[s])
+			continue;
+		uint32_t el = (uint32_t)(now - g_rumblePwmStart[s]);
+		if (el >= period) { // wrap to a fresh period (ON phase first)
+			g_rumblePwmStart[s] = now;
+			el = 0;
+		}
+		bool phaseOn = el < onMs;
+		if (phaseOn == g_rumblePwmOn[s])
+			continue; // already in this phase
+		g_rumblePwmOn[s] = phaseOn;
+		if (phaseOn)
+			sendRumble80(s, g_rumbleWantLo[s], g_rumbleWantHi[s]);
+		else
+			sendRumble80(s, 0, 0);
+	}
+}
 // Queue a pending test-haptic / stop relay (runs inside the poll cadence -- never at raw loop rate). Test
 // haptics broadcast to all connected slots (slot 0xFF); the stop frame is broadcast too (a stuck latch can
 // affect any controller, and the haptic-engine clear-re-init is settings-only so it's harmless on healthy
@@ -596,6 +660,9 @@ void hapticInit()
 		g_rqHead[s] = g_rqTail[s] = 0;
 		g_rumble80On[s] = false;
 		g_rumble80Ms[s] = 0;
+		g_rumbleWantOn[s] = false;
+		g_rumblePwmOn[s] = false;
+		g_rumblePwmStart[s] = 0;
 		// post-connect haptic block is permanently disabled (not armed, not configurable)
 		g_hapticBlockUntil[s] = 0;
 	}
@@ -612,6 +679,9 @@ void hapticOnReconnect(int slot)
 	g_haptic82On = false;
 	g_rumble80On[slot] = false;
 	g_rumble80Ms[slot] = 0;
+	g_rumbleWantOn[slot] = false;
+	g_rumblePwmOn[slot] = false;
+	g_rumblePwmStart[slot] = 0;
 	// Scrub haptics queued before the link came up (stale across the reconnect) -- this slot only.
 	hapticCancelPendingOn(slot);
 	// NO automatic haptic re-init. hapticReinit lands 0x81 (CLEAR DIGITAL MAPPINGS -- rid < 0x87 so it

+ 4 - 0
OpenPuck/haptics.h

@@ -131,6 +131,10 @@ bool hapticSteamRumble(uint16_t lowFreq, uint16_t highFreq, uint8_t slot = 0);
 // unattended uptime-until-hang measurement. hapticStabTask() is called from loop().
 extern bool g_stabTest;
 void hapticStabTask();
+// Duty-cycle the active host rumble so the per-type rumble-strength setting (g_rumbleScale%) controls
+// PERCEIVED strength on a controller that renders relayed rumble as a fixed-amplitude on/off. Called once
+// per poll cycle from loop(); no-op at 0% and >=100%.
+void hapticRumblePwmTask();
 
 void rfConnQueueHapticRelay();
 // returns true if a relay frame was actually transmitted this call (queue had an entry), so the poll loop can

+ 7 - 6
OpenPuck/webusb_config.cpp

@@ -257,7 +257,7 @@ 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
+	// v18: per-emulated-type rumble strength % (100 = full/native, below = PWM-attenuated). 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;
@@ -934,24 +934,25 @@ void webusbPoll()
 					}
 					break;
 
-				// LEGACY rumble strength % (0=off, 100=1x native, 200=double max): edits the ACTIVE
-				// emulated type (rumble strength is now per-type; kept so old panels/backups still apply).
+				// LEGACY rumble strength % (0=off, 100=full/native): edits the ACTIVE emulated type
+				// (rumble strength is now per-type; kept so old panels/backups still apply). PWM only
+				// attenuates below the controller's fixed level, so 100 is the ceiling -> clamp there.
 				case 22:
 					if (g_etype < ET_COUNT) {
 						g_type[g_etype].rumbleScale =
-							v > 200 ? 200 : v;
+							v > 100 ? 100 : v;
 						applyActiveType();
 					}
 					break;
 
 				// per-emulated-type rumble strength % (v18): fields 30..33 = ET_XBOX/SWITCH/DS4/DS5
-				// (0=off, 100=native default, 200=double max).
+				// (0=off, 100=full/native default; below 100 = PWM-attenuated, 100 is the ceiling).
 				case 30:
 				case 31:
 				case 32:
 				case 33: {
 					uint8_t et = (uint8_t)(f - 30);
-					g_type[et].rumbleScale = v > 200 ? 200 : v;
+					g_type[et].rumbleScale = v > 100 ? 100 : v;
 					if (et == g_etype)
 						applyActiveType();
 					break;

+ 3 - 3
docs/index.html

@@ -541,16 +541,16 @@ 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.
+    // Rumble strength (per-type; write fields 30..33, blob v18 at JS p[180+et]). 0=off, 100=full/native; below 100 = PWM-attenuated.
     { 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=200; sl.step=5; row.appendChild(sl);
+      const sl=document.createElement("input"); sl.type="range"; sl.min=0; sl.max=100; 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 default, 200% = double max, 0% = off). Use the toggle above to disable rumble entirely.";
+      note.textContent="Rumble strength for this controller type (100% = full/native default, 0% = off). Below 100% the motor is duty-cycled (pulsed) to feel weaker; the controller's native level is the ceiling, so this attenuates only. Use the toggle above to disable rumble entirely.";
       sec.appendChild(note);
       rec.rumbleScale=sl; rec.rumbleScaleV=vspan; }
     host.appendChild(sec); typeEls.push(rec);