From 53b2be544f6c9501b13fb9704e550221ed5fe38a Mon Sep 17 00:00:00 2001 From: Dymstro Date: Tue, 16 Jul 2024 17:23:42 +0200 Subject: [PATCH] Fix systemd service, add hotplug support, auto detect pw sink --- 50-nova-pro-wireless.rules | 2 +- README.md | 19 +--------------- nova-chatmix.service | 1 + nova.py | 45 ++++++++++++++++++++++++-------------- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/50-nova-pro-wireless.rules b/50-nova-pro-wireless.rules index 79953c2..4938c06 100644 --- a/50-nova-pro-wireless.rules +++ b/50-nova-pro-wireless.rules @@ -1 +1 @@ -SUBSYSTEMS=="usb", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e0", TAG+="uaccess" +SUBSYSTEMS=="usb", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e0", TAG+="uaccess", ENV{SYSTEMD_USER_WANTS}="nova-chatmix.service" diff --git a/README.md b/README.md index 8f0a6be..168833a 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ git clone https://git.dymstro.nl/Dymstro/nova-chatmix-linux.git cd nova-chatmix-linux ``` -To be able to run the script as a non-root user, some udev rules need to be applied. This will allow regular users to access the base station USB device. +To be able to run the script as a non-root user, some udev rules need to be applied. This will allow regular users to access the base station USB device. It also starts the script when it gets plugged in (only when the systemd service is set up). Copy `50-nova-pro-wireless.rules` to `/etc/udev/rules.d` and reload udev rules: ``` @@ -100,23 +100,6 @@ sudo udevadm control --reload-rules sudo udevadm trigger ``` -Check if your audio device matches the one on line 49 of `nova.py` -``` -pactl list sinks short -# The output should look something like this: -# 47 alsa_output.pci-0000_0c_00.4.iec958-stereo PipeWire s32le 2ch 48000Hz SUSPENDED -# 77 alsa_output.pci-0000_0a_00.1.hdmi-stereo PipeWire s32le 2ch 48000Hz SUSPENDED -# 92 alsa_output.usb-SteelSeries_Arctis_Nova_Pro_Wireless-00.iec958-stereo PipeWire s24le 2ch 48000Hz RUNNING -``` - -In `nova.py`: -``` -## Lines 48-50 -# PW_ORIGINAL_SINK = ( -# "alsa_output.usb-SteelSeries_Arctis_Nova_Pro_Wireless-00.7.iec958-stereo" # Edit this line if needed -# ) -``` - If you want to run this script on startup you can add and enable the systemd service ``` ## The systemd service expects the script in .local/bin diff --git a/nova-chatmix.service b/nova-chatmix.service index 5a980ce..a5228b7 100644 --- a/nova-chatmix.service +++ b/nova-chatmix.service @@ -6,6 +6,7 @@ Wants=network-online.target [Service] Restart=no Type=simple +ExecStartPre=/bin/sleep 1 ExecStart=%h/.local/bin/nova.py [Install] diff --git a/nova.py b/nova.py index 707a39d..9fd7ba2 100755 --- a/nova.py +++ b/nova.py @@ -2,11 +2,9 @@ # Licensed under the 0BSD -from subprocess import Popen +from subprocess import Popen, check_output from signal import signal, SIGINT, SIGTERM - - -from usb.core import find, USBTimeoutError +from usb.core import find, USBTimeoutError, USBError class NovaProWireless: @@ -43,11 +41,8 @@ class NovaProWireless: OPT_EQ_PRESET = 46 # PipeWire Names - ## Name of digital sink. - ## PipeWire docs recommend the analog sink, but I've had better results with the digital one. Probably not actually, but whatever. - PW_ORIGINAL_SINK = ( - "alsa_output.usb-SteelSeries_Arctis_Nova_Pro_Wireless-00.7.iec958-stereo" - ) + ## This is automatically detected, can be set manually by overriding this variable + PW_ORIGINAL_SINK = None ## Names of virtual sound devices PW_GAME_SINK = "NovaGame" PW_CHAT_SINK = "NovaChat" @@ -59,7 +54,6 @@ class NovaProWireless: # Keeps track of enabled features for when close() is called CHATMIX_CONTROLS_ENABLED = False SONAR_ICON_ENABLED = False - CHATMIX_ENABLED = False # Stops processes when program exits CLOSE = False @@ -105,9 +99,23 @@ class NovaProWireless: self.ENDPOINT_TX, self._create_msgdata((self.TX, self.OPT_EQ_PRESET, preset)), ) + + # Checks available sinks and select headset + def _detect_original_sink(self): + # If sink is set manually, skip auto detect + if self.PW_ORIGINAL_SINK: + return + sinks = check_output(["pactl", "list", "sinks", "short"]).decode().split("\n") + for sink in sinks: + print(sink) + name = sink.split("\t")[1] + if "SteelSeries_Arctis_Nova_Pro_Wireless" in name: + self.PW_ORIGINAL_SINK = name + break - # Create virtual pipewire loopback sinks, and redirect them to the real headset sink + # Creates virtual pipewire loopback sinks, and redirects them to the real headset sink def _start_virtual_sinks(self): + self._detect_original_sink() cmd = [ "pw-loopback", "-P", @@ -118,12 +126,15 @@ class NovaProWireless: self.PW_LOOPBACK_GAME_PROCESS = Popen(cmd + [self.PW_GAME_SINK]) self.PW_LOOPBACK_CHAT_PROCESS = Popen(cmd + [self.PW_CHAT_SINK]) + def _remove_virtual_sinks(self): + self.PW_LOOPBACK_GAME_PROCESS.terminate() + self.PW_LOOPBACK_CHAT_PROCESS.terminate() + # ChatMix implementation # Continuously read from base station and ignore everything but ChatMix messages (OPT_CHATMIX) # The .read method times out and returns an error. This error is catched and basically ignored. Timeout can be configured, but not turned off (I think). def chatmix(self): self._start_virtual_sinks() - self.CHATMIX_ENABLED = True while not self.CLOSE: try: msg = self.dev.read(self.ENDPOINT_RX, self.MSGLEN) @@ -143,6 +154,12 @@ class NovaProWireless: # Ignore timeout. except USBTimeoutError: continue + except USBError: + print("Device was probably disconnected, exiting..") + self.CLOSE = True + self._remove_virtual_sinks() + # Remove virtual sinks on exit + self._remove_virtual_sinks() # Prints output from base station. `debug` argument enables raw output. def print_output(self, debug: bool = False): @@ -171,11 +188,7 @@ class NovaProWireless: if self.CHATMIX_CONTROLS_ENABLED: self.set_chatmix_controls(False) if self.SONAR_ICON_ENABLED: - print("test") self.set_sonar_icon(False) - if self.CHATMIX_ENABLED: - self.PW_LOOPBACK_GAME_PROCESS.terminate() - self.PW_LOOPBACK_CHAT_PROCESS.terminate() # When run directly, just start the ChatMix implementation. (And activate the icon, just for fun)