forza-trigger: stop per-packet writes between races (fix periodic clicks)

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).
This commit is contained in:
2026-05-07 15:00:59 -04:00
parent c4c9dd7e50
commit b9cdc6a9b7

View File

@@ -777,17 +777,18 @@ def run(host: str, port: int, exit_on_idle: bool = False) -> int:
if in_race: if in_race:
handle_throttle(controller, pkt, state) handle_throttle(controller, pkt, state)
handle_brake(controller, pkt, state) handle_brake(controller, pkt, state)
else: apply_lightbar(controller, pkt, state, in_race=True)
# On the in-race → menu transition, partial-reset state so elif state.last_in_race:
# the next race resumption gets clean EWMA cells, fresh slip # in-race → menu edge: one-shot reset of state, triggers,
# flags (so the cold-start fix at handle_throttle/brake # and lightbar. We do NOT touch the controller again until
# re-fires), and a redrawn lightbar. Edge-only — repeated # the next in-race packet. Doing per-packet writes between
# menu packets shouldn't keep clearing state. # races causes audible clicks on the trigger actuator —
if state.last_in_race: # every HID OUT report (even the ones that look idempotent
state.reset() # via library dedup) re-encodes the trigger config and the
reset_triggers(controller) # firmware reacts on receipt.
state.reset()
reset_all(controller)
state.last_in_race = in_race state.last_in_race = in_race
apply_lightbar(controller, pkt, state, in_race)
finally: finally:
try: try:
reset_all(controller) reset_all(controller)