From c4c9dd7e50eb23bd901216260608fbbc2ed5ed76 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 7 May 2026 14:38:12 -0400 Subject: [PATCH] forza-trigger: patch dualsense-controller for vibration freq propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live strace on yarn during gameplay revealed the daemon was correctly calling effect.vibration(freq=35) on slip events, but the OUT report on the wire showed mode=0x26 (VIBRATION) with byte 31 (param9 = frequency) = 0. The controller firmware treats freq=0 as "no oscillation" — trigger sits silent in vibration mode. That's why the user reported "doesn't react to slipping or anything" even after the pedal-off early-return fix. Root cause is in dualsense-controller 0.3.1 itself: - WriteStates.__init__ registers individual write-states for trigger effect params 1-7 only. params 8/9/10 are not registered. - update_out_report copies from per-state values to the OutReport, again only params 1-7. - The OutReport dataclass DEFINES params 1-10 (with defaults of 0) and Usb01OutReport.to_bytes writes all 10 to the wire. - effect.vibration() puts frequency in param9 — silently dropped. - Same hits effect.machine() (params 8,9,10) and effect.galloping() (param10). effect.feedback/weapon/bow only use params 1-7 so they happen to work. Fix is a small upstream-style patch added under patches/dualsense- controller/ and wired into the dualsense-controller derivation in hosts/yarn/forza-trigger/python-packages.nix via the patches attr: in update_out_report, after the param7 assignments, read param8/9/10 directly from the parent TriggerEffect state value (which already carries them correctly from the call site through _set_value). Verified post-patch by source-reading the installed library: out_report.left_trigger_effect_param9 = self.left_trigger_effect.value.param9 out_report.right_trigger_effect_param9 = self.right_trigger_effect.value.param9 (and 4 more for left/right param8/10) Build-sandbox tests (54/54) pass via the forza-trigger-tests build gate; full yarn NixOS closure builds clean. Filing upstream against yesbotics/dualsense-controller-python is the follow-up; until then this is a local patch. --- hosts/yarn/forza-trigger/python-packages.nix | 11 +++++ ...propagate-trigger-effect-params-8-10.patch | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 patches/dualsense-controller/0001-propagate-trigger-effect-params-8-10.patch diff --git a/hosts/yarn/forza-trigger/python-packages.nix b/hosts/yarn/forza-trigger/python-packages.nix index 619ffd7..e7cf7f1 100644 --- a/hosts/yarn/forza-trigger/python-packages.nix +++ b/hosts/yarn/forza-trigger/python-packages.nix @@ -34,6 +34,17 @@ rec { hash = "sha256-yy1MQeRPqaLvoXaAigQd3gPFsFLbwKqrD4mP2zQqcFw="; }; + + # Library bug fix: trigger-effect params 8-10 are never propagated to + # the OUT report. effect.vibration() puts the frequency in param9, so + # without this patch every vibration call ships frequency=0 to the + # controller and the trigger sits silently in vibration mode. The + # patch fixes update_out_report to read params 8/9/10 directly off + # the parent TriggerEffect state. Confirmed live with strace before + # patching: byte 31 (= L2 param9) was 0 every tick during slip events. + patches = [ + ../../../patches/dualsense-controller/0001-propagate-trigger-effect-params-8-10.patch + ]; nativeBuildInputs = [ py.poetry-core ]; # Upstream's pyproject pins pyee ^11 and cffi ^1.15. nixpkgs ships diff --git a/patches/dualsense-controller/0001-propagate-trigger-effect-params-8-10.patch b/patches/dualsense-controller/0001-propagate-trigger-effect-params-8-10.patch new file mode 100644 index 0000000..a7c68fd --- /dev/null +++ b/patches/dualsense-controller/0001-propagate-trigger-effect-params-8-10.patch @@ -0,0 +1,43 @@ +From: forza-trigger maintenance +Date: 2026-05-07 +Subject: [PATCH] WriteStates: propagate trigger-effect params 8-10 + +The library's update_out_report only copies trigger-effect params 1-7 +from per-state values to the OUT report. Params 8/9/10 are never +propagated and stay at their OutReport-dataclass defaults of 0. This +silently breaks effects that use those slots — most painfully +effect.vibration() puts frequency in param9, so vibration mode ships +frequency=0 in the wire and the trigger does nothing while in +vibration mode. Confirmed live with strace on yarn: byte 31 (L2 +param9) wire-byte was 0 every tick despite the daemon calling +effect.vibration(frequency=35). + +Fix: read params 8-10 directly off the parent TriggerEffect state +(which already carries them correctly through _set_value), bypassing +the per-param individual states. No new WriteStateName enum members +needed. + +Upstream: needs filing against yesbotics/dualsense-controller-python. + +--- a/src/dualsense_controller/core/state/write_state/WriteStates.py ++++ b/src/dualsense_controller/core/state/write_state/WriteStates.py +@@ -269,6 +269,19 @@ + WriteStateName.RIGHT_TRIGGER_EFFECT_PARAM6).value_raw + out_report.right_trigger_effect_param7 = self._get_state_by_name( + WriteStateName.RIGHT_TRIGGER_EFFECT_PARAM7).value_raw ++ # Library bug workaround: params 8-10 are never registered as ++ # individual write states so the per-param fan-out in ++ # _on_*_trigger_effect_changed drops them. Read directly off the ++ # parent TriggerEffect state. Without this, effect.vibration(freq=N) ++ # ships frequency=0 to the controller and the trigger sits silent ++ # in vibration mode (confirmed live with strace on yarn before ++ # patching: byte 31 = L2 param9 was 0 every tick). ++ out_report.left_trigger_effect_param8 = self.left_trigger_effect.value.param8 ++ out_report.left_trigger_effect_param9 = self.left_trigger_effect.value.param9 ++ out_report.left_trigger_effect_param10 = self.left_trigger_effect.value.param10 ++ out_report.right_trigger_effect_param8 = self.right_trigger_effect.value.param8 ++ out_report.right_trigger_effect_param9 = self.right_trigger_effect.value.param9 ++ out_report.right_trigger_effect_param10 = self.right_trigger_effect.value.param10 + + def _create_and_register_state( + self,