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.
118 lines
4.4 KiB
Nix
118 lines
4.4 KiB
Nix
# Python packages forza_trigger.py imports that aren't in nixpkgs. Returns
|
|
# an attrset consumed by ./default.nix.
|
|
#
|
|
# Bumping a version: change `version` and `hash`, then `nix build` — Nix
|
|
# fails with the new sha256 in the error message, paste it back in.
|
|
{
|
|
lib,
|
|
pkgs,
|
|
python ? pkgs.python3,
|
|
}:
|
|
let
|
|
py = python.pkgs;
|
|
in
|
|
rec {
|
|
# DualSense controller library (yesbotics/dualsense-controller-python).
|
|
# Bundles its own hidapi-cffi binding; replaces the previous pydualsense +
|
|
# hidapi-usb pair. The library's transport model writes the HID output
|
|
# report on every input report tick BUT only when an output state actually
|
|
# changed since the last write — so we get on-demand HID writes for free
|
|
# without disabling any background thread (the thing pydualsense made us
|
|
# work around at ~250 Hz). BT CRC32 is computed correctly inside the
|
|
# library's Bt31OutReport.to_bytes.
|
|
dualsense-controller = py.buildPythonPackage rec {
|
|
pname = "dualsense-controller";
|
|
version = "0.3.1";
|
|
pyproject = true;
|
|
|
|
# PyPI normalizes the sdist filename to underscore form (PEP 625) but
|
|
# the project URL slug is dasherized. fetchPypi assumes they match;
|
|
# passing the underscored pname matches the on-disk filename.
|
|
src = py.fetchPypi {
|
|
pname = "dualsense_controller";
|
|
inherit version;
|
|
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
|
|
# pyee 13 and cffi 2. The library only uses stable APIs — pyee
|
|
# `EventEmitter`/`on`/`emit` and cffi `FFI`/`cdef`/`dlopen`/`new`/
|
|
# `buffer` — that work unchanged on the newer versions. Relax both.
|
|
pythonRelaxDeps = [
|
|
"pyee"
|
|
"cffi"
|
|
];
|
|
|
|
propagatedBuildInputs = with py; [
|
|
pyee
|
|
cffi
|
|
deprecated
|
|
];
|
|
|
|
# Same hidapi-binding pattern as our previous hidapi-usb derivation.
|
|
# The library walks a tuple of soname strings via ffi.dlopen() until
|
|
# one resolves; pin the two Linux hidraw entries to absolute store
|
|
# paths so the wrapped Python in writePython3Bin doesn't need
|
|
# LD_LIBRARY_PATH wrapping. The libusb / iohidmanager / dylib / dll
|
|
# entries are dead code on Linux. --replace-fail makes a rename in
|
|
# upstream's tuple a loud build error rather than a silent ImportError
|
|
# at runtime.
|
|
postPatch = ''
|
|
substituteInPlace src/dualsense_controller/core/hidapi/hidapi.py \
|
|
--replace-fail "'libhidapi-hidraw.so'," "'${pkgs.hidapi}/lib/libhidapi-hidraw.so'," \
|
|
--replace-fail "'libhidapi-hidraw.so.0'," "'${pkgs.hidapi}/lib/libhidapi-hidraw.so.0',"
|
|
'';
|
|
|
|
pythonImportsCheck = [ "dualsense_controller" ];
|
|
|
|
meta = {
|
|
description = "Use DualSense Controller with Python";
|
|
homepage = "https://github.com/yesbotics/dualsense-controller-python";
|
|
license = lib.licenses.mit;
|
|
};
|
|
};
|
|
|
|
# Single-file Forza UDP packet parser (nettrom/forza_motorsport). Pinned to
|
|
# a known-good commit; the repo is dormant (last commit 2021) but the FH4
|
|
# packet layout is frozen and FH5 reuses it byte-for-byte.
|
|
fdp = py.buildPythonPackage {
|
|
pname = "fdp";
|
|
version = "0-unstable-2021-05-28";
|
|
format = "other";
|
|
|
|
src = pkgs.fetchurl {
|
|
url = "https://raw.githubusercontent.com/nettrom/forza_motorsport/61845cb7ff4082211292a51ce3c49edbfd2d6503/fdp.py";
|
|
hash = "sha256-osFaVF9VaEzU4dp3x6KN6OF7SXsd9ZBwvilU+xTT7mM=";
|
|
};
|
|
|
|
dontUnpack = true;
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
install -Dm644 $src $out/${python.sitePackages}/fdp.py
|
|
runHook postInstall
|
|
'';
|
|
|
|
pythonImportsCheck = [ "fdp" ];
|
|
|
|
meta = {
|
|
description = "ForzaDataPacket — Forza Motorsport / Horizon UDP packet parser";
|
|
homepage = "https://github.com/nettrom/forza_motorsport";
|
|
license = lib.licenses.mit;
|
|
};
|
|
};
|
|
}
|