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
```
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

View file

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

45
nova.py
View file

@ -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
@ -106,8 +100,22 @@ class NovaProWireless:
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):
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)