94 lines
3.8 KiB
Nix
94 lines
3.8 KiB
Nix
# 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;
|
|
};
|
|
};
|
|
};
|
|
}
|