yarn: forza dualsense adaptive trigger bridge
This commit is contained in:
@@ -20,19 +20,6 @@
|
||||
# - in Forza, HUD options → set Data Out: ON, Data Out IP: 127.0.0.1,
|
||||
# Data Out IP Port: 5300, and (FM7 only) Data Out Packet Format: CAR DASH.
|
||||
#
|
||||
# System-interaction notes:
|
||||
# - With multiple DualSense controllers connected, pydualsense picks one
|
||||
# non-deterministically (`# TODO: implement multiple controllers working`
|
||||
# in pydualsense's source). Forza Horizon is single-player so this is
|
||||
# usually fine. If you need to pin a specific controller, the cleanest
|
||||
# route is monkey-patching `pydualsense.__find_device`.
|
||||
# - `pkgs.dualsensectl` is intentionally NOT installed by default
|
||||
# (single-shot writes from it get overwritten by our BG thread within
|
||||
# ~4 ms). Bring it in ad-hoc with `nix-shell -p dualsensectl` and stop
|
||||
# this service first via `systemctl --user stop forza-trigger`.
|
||||
# - Hot-plug recovery happens in-process: the daemon polls pydualsense's BG
|
||||
# thread liveness and re-runs `pydualsense.init()` on disconnect. systemd's
|
||||
# `Restart=on-failure` exists only as a crash-recovery safety net.
|
||||
let
|
||||
cfg = config.services.forzaTrigger;
|
||||
pythonPackages = import ./python-packages.nix { inherit lib pkgs; };
|
||||
@@ -90,16 +77,45 @@ in
|
||||
|
||||
environment.systemPackages = [ forzaTrigger ];
|
||||
|
||||
# User-level service so it inherits the seat-bound uaccess ACL on
|
||||
# /dev/hidraw* and dies cleanly when the user logs out.
|
||||
systemd.user.services.forza-trigger = {
|
||||
# Socket-activated by Forza's Data Out UDP stream on port 5300.
|
||||
# The service starts when Forza Horizon 4/5 sends its first telemetry
|
||||
# packet, runs for the entire session, and exits on 3 s of silence
|
||||
# (--exit-on-idle). systemd restarts automatically on the next launch.
|
||||
#
|
||||
# Runs as a system service (not user service) so there is exactly one
|
||||
# instance regardless of how many users have active sessions. The service
|
||||
# runs as `cfg.user` (primary) for hidraw uaccess compatibility with the
|
||||
# udev TAG+="uaccess" rules.
|
||||
#
|
||||
# Crash-recovery: Restart=on-failure restarts the daemon mid-session if
|
||||
# it dies abnormally (non-zero exit). The clean exit-on-idle path returns
|
||||
# 0, which systemd leaves alone — the socket unit stays listening.
|
||||
systemd.services.forza-trigger = {
|
||||
description = "Forza Horizon → DualSense adaptive trigger bridge";
|
||||
wantedBy = [ "default.target" ];
|
||||
after = [ "graphical-session.target" ];
|
||||
# No wantedBy — socket activation pulls it in when Forza sends UDP.
|
||||
serviceConfig = {
|
||||
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port}";
|
||||
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port} --exit-on-idle";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
User = cfg.user;
|
||||
# KillMode=control-group is the default, but be explicit.
|
||||
# systemd sends SIGTERM by default; Python doesn't route that to
|
||||
# KeyboardInterrupt, so we tell systemd to send SIGINT instead.
|
||||
# The daemon registers handlers for both for robustness.
|
||||
KillMode = "control-group";
|
||||
KillSignal = "SIGINT";
|
||||
TimeoutStopSec = 5;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.sockets.forza-trigger = {
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig = {
|
||||
ListenDatagram = "127.0.0.1:${toString cfg.port}";
|
||||
# Guard against rapid re-triggering. One activation per Forza session
|
||||
# (minutes+) is normal; > 3 within 10 s is pathological (bug).
|
||||
TriggerLimitIntervalSec = 10;
|
||||
TriggerLimitBurst = 3;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
1516
hosts/yarn/forza-trigger/forza_trigger.py
Normal file
1516
hosts/yarn/forza-trigger/forza_trigger.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user