1
0
Fork 0

Fix systemd service, add hotplug support, auto detect pw sink

This commit is contained in:
Ricardo 2024-07-16 17:23:42 +02:00
parent 50255ba5df
commit 53b2be544f
4 changed files with 32 additions and 35 deletions

View file

@ -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"

View file

@ -90,7 +90,7 @@ git clone https://git.dymstro.nl/Dymstro/nova-chatmix-linux.git
cd nova-chatmix-linux 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: 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 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 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 ## The systemd service expects the script in .local/bin

View file

@ -6,6 +6,7 @@ Wants=network-online.target
[Service] [Service]
Restart=no Restart=no
Type=simple Type=simple
ExecStartPre=/bin/sleep 1
ExecStart=%h/.local/bin/nova.py ExecStart=%h/.local/bin/nova.py
[Install] [Install]

45
nova.py
View file

@ -2,11 +2,9 @@
# Licensed under the 0BSD # Licensed under the 0BSD
from subprocess import Popen from subprocess import Popen, check_output
from signal import signal, SIGINT, SIGTERM from signal import signal, SIGINT, SIGTERM
from usb.core import find, USBTimeoutError, USBError
from usb.core import find, USBTimeoutError
class NovaProWireless: class NovaProWireless:
@ -43,11 +41,8 @@ class NovaProWireless:
OPT_EQ_PRESET = 46 OPT_EQ_PRESET = 46
# PipeWire Names # PipeWire Names
## Name of digital sink. ## This is automatically detected, can be set manually by overriding this variable
## PipeWire docs recommend the analog sink, but I've had better results with the digital one. Probably not actually, but whatever. PW_ORIGINAL_SINK = None
PW_ORIGINAL_SINK = (
"alsa_output.usb-SteelSeries_Arctis_Nova_Pro_Wireless-00.7.iec958-stereo"
)
## Names of virtual sound devices ## Names of virtual sound devices
PW_GAME_SINK = "NovaGame" PW_GAME_SINK = "NovaGame"
PW_CHAT_SINK = "NovaChat" PW_CHAT_SINK = "NovaChat"
@ -59,7 +54,6 @@ class NovaProWireless:
# Keeps track of enabled features for when close() is called # Keeps track of enabled features for when close() is called
CHATMIX_CONTROLS_ENABLED = False CHATMIX_CONTROLS_ENABLED = False
SONAR_ICON_ENABLED = False SONAR_ICON_ENABLED = False
CHATMIX_ENABLED = False
# Stops processes when program exits # Stops processes when program exits
CLOSE = False CLOSE = False
@ -106,8 +100,22 @@ class NovaProWireless:
self._create_msgdata((self.TX, self.OPT_EQ_PRESET, preset)), self._create_msgdata((self.TX, self.OPT_EQ_PRESET, preset)),
) )
# Create virtual pipewire loopback sinks, and redirect them to the real headset sink # 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
# Creates virtual pipewire loopback sinks, and redirects them to the real headset sink
def _start_virtual_sinks(self): def _start_virtual_sinks(self):
self._detect_original_sink()
cmd = [ cmd = [
"pw-loopback", "pw-loopback",
"-P", "-P",
@ -118,12 +126,15 @@ class NovaProWireless:
self.PW_LOOPBACK_GAME_PROCESS = Popen(cmd + [self.PW_GAME_SINK]) self.PW_LOOPBACK_GAME_PROCESS = Popen(cmd + [self.PW_GAME_SINK])
self.PW_LOOPBACK_CHAT_PROCESS = Popen(cmd + [self.PW_CHAT_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 # ChatMix implementation
# Continuously read from base station and ignore everything but ChatMix messages (OPT_CHATMIX) # 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). # 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): def chatmix(self):
self._start_virtual_sinks() self._start_virtual_sinks()
self.CHATMIX_ENABLED = True
while not self.CLOSE: while not self.CLOSE:
try: try:
msg = self.dev.read(self.ENDPOINT_RX, self.MSGLEN) msg = self.dev.read(self.ENDPOINT_RX, self.MSGLEN)
@ -143,6 +154,12 @@ class NovaProWireless:
# Ignore timeout. # Ignore timeout.
except USBTimeoutError: except USBTimeoutError:
continue 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. # Prints output from base station. `debug` argument enables raw output.
def print_output(self, debug: bool = False): def print_output(self, debug: bool = False):
@ -171,11 +188,7 @@ class NovaProWireless:
if self.CHATMIX_CONTROLS_ENABLED: if self.CHATMIX_CONTROLS_ENABLED:
self.set_chatmix_controls(False) self.set_chatmix_controls(False)
if self.SONAR_ICON_ENABLED: if self.SONAR_ICON_ENABLED:
print("test")
self.set_sonar_icon(False) 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) # When run directly, just start the ChatMix implementation. (And activate the icon, just for fun)