Fix systemd service, add hotplug support, auto detect pw sink
This commit is contained in:
parent
50255ba5df
commit
53b2be544f
4 changed files with 32 additions and 35 deletions
|
@ -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"
|
||||||
|
|
19
README.md
19
README.md
|
@ -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
|
||||||
|
|
|
@ -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
45
nova.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue