From b9cdc6a9b721c1a598ad9bf1da7fbbd9d2ab0f9f Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 7 May 2026 15:00:59 -0400 Subject: [PATCH] forza-trigger: stop per-packet writes between races (fix periodic clicks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reports periodic clicks and LED color changes on the controller "between races". Theory: out-of-race, the run loop was calling reset_triggers(controller) AND apply_lightbar(controller, ...) on every Forza packet (60Hz). Even though both call sites land in library code that nominally dedupes "same state" writes, in practice any state change anywhere — including the lightbar dropping a green channel value as RPM coasts down to idle — triggers a full HID OUT report rewrite. The OUT report carries the trigger configuration too; the controller firmware reacts on receipt and produces an audible click each time. Fix: out-of-race path becomes edge-triggered. On the in-race → menu transition we run state.reset() + reset_all() once (turning both triggers off and the lightbar to (0,0,0)). Subsequent menu packets make no controller calls at all until in_race flips back to True. First in-race packet then re-engages handlers and the RPM-driven lightbar. Side effect: the menu-mode car-class lightbar coloring is gone — the bar stays black between races. If we want it back later, it should be one-shot on the menu transition (NOT updated per-packet). For now keep it simple: in-race only. Build clean; tests unchanged (54/54 still pass — they exercise handlers directly, not the run loop). --- hosts/yarn/forza-trigger/forza_trigger.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/hosts/yarn/forza-trigger/forza_trigger.py b/hosts/yarn/forza-trigger/forza_trigger.py index f20bfd6..ddea9db 100644 --- a/hosts/yarn/forza-trigger/forza_trigger.py +++ b/hosts/yarn/forza-trigger/forza_trigger.py @@ -777,17 +777,18 @@ def run(host: str, port: int, exit_on_idle: bool = False) -> int: if in_race: handle_throttle(controller, pkt, state) handle_brake(controller, pkt, state) - else: - # On the in-race → menu transition, partial-reset state so - # the next race resumption gets clean EWMA cells, fresh slip - # flags (so the cold-start fix at handle_throttle/brake - # re-fires), and a redrawn lightbar. Edge-only — repeated - # menu packets shouldn't keep clearing state. - if state.last_in_race: - state.reset() - reset_triggers(controller) + apply_lightbar(controller, pkt, state, in_race=True) + elif state.last_in_race: + # in-race → menu edge: one-shot reset of state, triggers, + # and lightbar. We do NOT touch the controller again until + # the next in-race packet. Doing per-packet writes between + # races causes audible clicks on the trigger actuator — + # every HID OUT report (even the ones that look idempotent + # via library dedup) re-encodes the trigger config and the + # firmware reacts on receipt. + state.reset() + reset_all(controller) state.last_in_race = in_race - apply_lightbar(controller, pkt, state, in_race) finally: try: reset_all(controller)