From 11d283585ca30e9fe9366a070ba92610e9e1ae95 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 7 May 2026 14:18:47 -0400 Subject: [PATCH] forza-trigger: drop pedal-off early-returns (root cause of "no reaction") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live diagnostic on yarn revealed the daemon was receiving 324-byte FH5 packets correctly (5.7MB on the systemd socket; strace showed steady recvfrom + write to /dev/hidraw7) but writing trigger mode 0x05 (no-resistance) on nearly every tick. Cause: `accel` and `brake` are 0 most of the time during normal play (off-throttle on straight sections, off-brake when not braking). Both handlers had: if accel/255 <= THROTTLE_INPUT_THRESHOLD: effect.off(); return if brake/255 <= BRAKE_INPUT_THRESHOLD: effect.off(); return Every off-pedal packet set the trigger to OFF. Brief pedal-on moments set vibration. The result: rapidly oscillating off↔vibration state, imperceptible at 60 Hz packet rate. These early-returns were holdovers from the previous Race-Element 1:1 port (variant A), which IS designed to be silent unless slipping. Variant D's whole point is "always feels something" — Cosmii has no pedal-off gate, and its baseline branch produces feedback even at brake=0/accel=0 with strength clamped to MIN. Fix: remove both early-returns. Foot-off-pedal flows through the baseline branch and produces feedback(strength=MIN_*_RESISTANCE). The user feels light constant resistance instead of silence. Trigger only returns to physical-rest when out-of-race (run-loop's reset_triggers). Also drop the now-dead BRAKE_INPUT_THRESHOLD / THROTTLE_INPUT_THRESHOLD constants. Two tests renamed and updated to assert MIN-strength baseline feedback instead of effect.off() on zero pedal. 54/54 tests pass. Build clean. --- hosts/yarn/forza-trigger/forza_trigger.py | 19 ++++++------------- .../yarn/forza-trigger/test_forza_trigger.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/hosts/yarn/forza-trigger/forza_trigger.py b/hosts/yarn/forza-trigger/forza_trigger.py index 080716b..f20bfd6 100644 --- a/hosts/yarn/forza-trigger/forza_trigger.py +++ b/hosts/yarn/forza-trigger/forza_trigger.py @@ -92,11 +92,6 @@ LOG = logging.getLogger("forza-trigger") # Source citations point to legacy_Program.cs (Cosmii) and fds_Settings.cs # (Patmagauran defaults, inherited by Cosmii via INI file). -# Pedal input gates: avoid noise at zero pedal pressure. Not in upstream; -# carried over from the previous Race-Element port (BRAKE_INPUT_THRESHOLD = 3%). -BRAKE_INPUT_THRESHOLD = 0.03 # 0..1 fraction of pedal travel -THROTTLE_INPUT_THRESHOLD = 0.03 - # --- L2 brake --------------------------------------------------------------- # fds_Settings.cs (Cosmii inherits via appsettings.ini). GRIP_LOSS_VAL = 0.5 # combined-slip threshold for vibration overlay @@ -402,12 +397,12 @@ def handle_throttle(controller: DualSenseController, pkt: ForzaDataPacket, state fall through to feedback-only (avoids audible clicking at very low frequencies). - In-race + no slip: continuous feedback resistance scaled by avgAccel. - - Pedal not pressed: trigger off. + - Pedal at rest: baseline still active at MIN strength so the + trigger always has *some* feel under the finger. + Trigger is only `effect.off()` when out of race + (handled by the run-loop, not here). """ accel = _safe_field(pkt, "accel", 0.0) - if accel / 255.0 <= THROTTLE_INPUT_THRESHOLD: - controller.right_trigger.effect.off() - return avg_a = _avg_accel(pkt) _, combined_front, combined_rear = _combined_slip(pkt) @@ -484,12 +479,10 @@ def handle_brake(controller: DualSenseController, pkt: ForzaDataPacket, state: D - In-race + slip: vibration overlay (freq from slip, amp from brake input). Below MIN_BRAKE_VIBRATION freq → revert to feedback-only. - In-race + no slip: continuous feedback resistance scaled by brake input. - - Pedal not pressed: trigger off. + - Pedal at rest: baseline still active at MIN strength so the + trigger always has some feel. """ brake = _safe_field(pkt, "brake", 0.0) - if brake / 255.0 <= BRAKE_INPUT_THRESHOLD: - controller.left_trigger.effect.off() - return # Use the worst axle's slip for both detection and the freq curve. # Cosmii's all-4 average misses single-axle ABS events (front locks diff --git a/hosts/yarn/forza-trigger/test_forza_trigger.py b/hosts/yarn/forza-trigger/test_forza_trigger.py index bace4b8..b2cbc9d 100644 --- a/hosts/yarn/forza-trigger/test_forza_trigger.py +++ b/hosts/yarn/forza-trigger/test_forza_trigger.py @@ -205,12 +205,16 @@ class TestIsRaceOn(unittest.TestCase): class TestHandleThrottle(unittest.TestCase): - def test_zero_pedal_turns_off(self): + def test_zero_pedal_baseline_min_feedback(self): + # Foot off throttle should NOT turn the trigger off — variant D's + # design is "always feels something". Baseline mode still fires + # with strength = MIN_THROTTLE_RESISTANCE so the user feels light + # constant resistance under the finger. The run-loop is the only + # place that calls effect.off() (out-of-race idle reset). c = FakeController() s = ft.DaemonState() ft.handle_throttle(c, FakePacket(accel=0.0), s) - self.assertEqual(c.calls, [("R2", "off")]) - + self.assertEqual(c.calls, [("R2", "feedback", 0, ft.MIN_THROTTLE_RESISTANCE)]) def test_baseline_under_normal_acceleration(self): c = FakeController() s = ft.DaemonState() @@ -276,11 +280,12 @@ class TestHandleThrottle(unittest.TestCase): class TestHandleBrake(unittest.TestCase): - def test_zero_pedal_turns_off(self): + def test_zero_pedal_baseline_min_feedback(self): + # Same as throttle — foot off brake = light constant resistance, not off. c = FakeController() s = ft.DaemonState() ft.handle_brake(c, FakePacket(brake=0.0), s) - self.assertEqual(c.calls, [("L2", "off")]) + self.assertEqual(c.calls, [("L2", "feedback", 0, ft.MIN_BRAKE_RESISTANCE)]) def test_baseline_under_normal_braking(self): c = FakeController()