# DualSense rumble/trigger firmware-level attenuation. # # The PS5 controller's haptic actuators are linear resonant actuators (LRAs) — # voice coils, not ERM rotors. When driven near max amplitude they emit an # audible high-pitched whine in addition to the intended rumble. This is a # hardware property of the actuator, not a kernel or Steam Input bug, and # can't be fixed in any rumble path that talks to the LRAs at full strength. # # Sony exposes a firmware knob ("rumble emulation amplitude modifier", in # DS_OUTPUT report 0x05/0x31, gated by valid_flag2 bit RUMBLE_EMULATION_SELECT) # that clamps haptic / trigger amplitude before the actuator is driven. This # module sends that report once on plug-in via dualsensectl. The setting # lives in controller RAM until the next power-cycle (USB unplug or BT # disconnect-with-power-off), so re-applying on every udev `add` covers # every reconnect. # # Coexists with Steam Input's rumble path: Steam writes rumble in the same # output report, but only sets COMPATIBLE_VIBRATION / HAPTICS_SELECT in # valid_flag0 — not RUMBLE_EMULATION_SELECT — so its writes don't reset the # attenuation. Verified live with Steam (gamepadui) and forza-trigger both # holding hidraw concurrently. { config, lib, pkgs, ... }: let cfg = config.services.dualsenseAttenuation; applyScript = pkgs.writeShellScript "dualsense-apply-attenuation" '' set -eu # Retry briefly: the hidraw node exists by udev `add`, but the device # may still be settling its initial HID descriptor exchange. dualsensectl # needs to read the firmware report before it can write attenuation. for _ in 1 2 3 4 5 6 7 8 9 10; do if ${pkgs.dualsensectl}/bin/dualsensectl -l 2>/dev/null | grep -q ":"; then exec ${pkgs.dualsensectl}/bin/dualsensectl attenuation \ ${toString cfg.rumble} ${toString cfg.trigger} fi sleep 0.2 done echo "no DualSense detected; nothing to attenuate" >&2 exit 0 ''; in { options.services.dualsenseAttenuation = { enable = lib.mkEnableOption "Apply DualSense haptic attenuation on plug-in"; rumble = lib.mkOption { type = lib.types.ints.between 0 7; default = 3; description = '' Rumble (LRA) attenuation, 0-7. Higher = weaker. 3 keeps the LRAs below their audible-whine threshold while leaving rumble clearly perceptible. 0 disables attenuation (full strength). ''; }; trigger = lib.mkOption { type = lib.types.ints.between 0 7; default = 0; description = '' Adaptive trigger attenuation, 0-7. Higher = weaker. 0 leaves trigger feedback untouched — forza-trigger drives the triggers with bounded amplitudes already, so the trigger LRAs don't normally hit the audible regime. ''; }; }; config = lib.mkIf cfg.enable { services.udev.extraRules = '' # DualSense (USB) ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" # DualSense Edge (USB) ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0df2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" # DualSense (Bluetooth) ACTION=="add", SUBSYSTEM=="hidraw", KERNELS=="*054C:0CE6*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" # DualSense Edge (Bluetooth) ACTION=="add", SUBSYSTEM=="hidraw", KERNELS=="*054C:0DF2*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" ''; systemd.services.dualsense-attenuation = { description = "Apply DualSense rumble/trigger attenuation in controller firmware"; serviceConfig = { Type = "oneshot"; ExecStart = applyScript; }; }; }; }