gaming: drop arctis-sound-manager, ANC via direct HID script
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
027858ff1e
commit
ee86678cb5
4 changed files with 77 additions and 63 deletions
23
flake.lock
generated
23
flake.lock
generated
|
|
@ -1,27 +1,5 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"arctis-sound-manager": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"dir": "nix",
|
|
||||||
"lastModified": 1785229953,
|
|
||||||
"narHash": "sha256-5oezrSj8OxNeMUN0IxjdKh4t2D9jn1BOainmBRNeXgE=",
|
|
||||||
"owner": "loteran",
|
|
||||||
"repo": "Arctis-Sound-Manager",
|
|
||||||
"rev": "aaa05fd62b7ac037d59802ce9c46b69c5bc84a33",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"dir": "nix",
|
|
||||||
"owner": "loteran",
|
|
||||||
"repo": "Arctis-Sound-Manager",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"base16": {
|
"base16": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"fromYaml": "fromYaml"
|
"fromYaml": "fromYaml"
|
||||||
|
|
@ -394,7 +372,6 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"arctis-sound-manager": "arctis-sound-manager",
|
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"nix-cachyos-kernel": "nix-cachyos-kernel",
|
"nix-cachyos-kernel": "nix-cachyos-kernel",
|
||||||
"nixpkgs": "nixpkgs_2",
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,6 @@
|
||||||
|
|
||||||
proton-cachyos-nix.url = "github:powerofthe69/proton-cachyos-nix";
|
proton-cachyos-nix.url = "github:powerofthe69/proton-cachyos-nix";
|
||||||
|
|
||||||
arctis-sound-manager = {
|
|
||||||
url = "github:loteran/Arctis-Sound-Manager?dir=nix";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
stylix.url = "github:nix-community/stylix/release-26.05";
|
stylix.url = "github:nix-community/stylix/release-26.05";
|
||||||
};
|
};
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -54,7 +49,7 @@
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
FredOS-Gaming = mkHost "FredOS-Gaming" [ inputs.arctis-sound-manager.nixosModules.default ];
|
FredOS-Gaming = mkHost "FredOS-Gaming" [];
|
||||||
FredOS-Mediaserver = mkHost "FredOS-Mediaserver" [];
|
FredOS-Mediaserver = mkHost "FredOS-Mediaserver" [];
|
||||||
FredOS-Macbook = mkHost "FredOS-Macbook" [];
|
FredOS-Macbook = mkHost "FredOS-Macbook" [];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,14 @@
|
||||||
|
|
||||||
programs.geary.enable = true;
|
programs.geary.enable = true;
|
||||||
|
|
||||||
# SteelSeries Nova Pro control (ANC switching via quickshell talks to its
|
# Nova Pro Wireless base station: open up its vendor HID interface so
|
||||||
# asm-daemon over D-Bus)
|
# quickshell's ANC switcher can write to it directly (nova-anc script in
|
||||||
services.arctis-sound-manager.enable = true;
|
# settings/quickshell.nix). Single-user desktop, 0666 is fine.
|
||||||
|
services.udev.extraRules = ''
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e0", MODE="0666"
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e5", MODE="0666"
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="225d", MODE="0666"
|
||||||
|
'';
|
||||||
|
|
||||||
# Force heavy builds off the gaming PC onto the media server so nix updates
|
# Force heavy builds off the gaming PC onto the media server so nix updates
|
||||||
# never compete with games for CPU/RAM. Pairs with the distributed build
|
# never compete with games for CPU/RAM. Pairs with the distributed build
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,58 @@ in
|
||||||
'';
|
'';
|
||||||
# 7-day forecast JSON from Open-Meteo (no API key). Location is
|
# 7-day forecast JSON from Open-Meteo (no API key). Location is
|
||||||
# auto-detected by IP via ipinfo.io, falling back to London.
|
# auto-detected by IP via ipinfo.io, falling back to London.
|
||||||
|
# Nova Pro Wireless ANC, no daemon: talk straight to the base station's
|
||||||
|
# vendor HID interface (iface 4). Report id 0x06 leads every 64-byte
|
||||||
|
# packet; opcode 0xbd sets ANC (0 off / 1 transparent / 2 on), 0xb0
|
||||||
|
# requests full status (anc @ 0x0a, power @ 0x0f, 0x08 = on-head).
|
||||||
|
# Protocol lifted from Arctis-Sound-Manager's nova_pro_wireless.yaml.
|
||||||
|
novaAncScript = pkgs.writeScript "nova-anc" ''
|
||||||
|
#!${pkgs.python3}/bin/python3
|
||||||
|
"""nova-anc [off|transparent|on] — no arg prints current mode."""
|
||||||
|
import glob, os, select, sys, time
|
||||||
|
|
||||||
|
MODES = {"off": 0, "transparent": 1, "on": 2}
|
||||||
|
NAMES = {0: "off", 1: "transparent", 2: "on"}
|
||||||
|
PIDS = {0x12e0, 0x12e5, 0x225d}
|
||||||
|
|
||||||
|
def dongle():
|
||||||
|
for h in glob.glob("/sys/class/hidraw/hidraw*"):
|
||||||
|
try:
|
||||||
|
uevent = open(h + "/device/uevent").read()
|
||||||
|
iface = open(h + "/device/../bInterfaceNumber").read().strip()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
for line in uevent.splitlines():
|
||||||
|
if line.startswith("HID_ID="):
|
||||||
|
_, vid, pid = line.split("=")[1].split(":")
|
||||||
|
if int(vid, 16) == 0x1038 and int(pid, 16) in PIDS and iface == "04":
|
||||||
|
return "/dev/" + os.path.basename(h)
|
||||||
|
return None
|
||||||
|
|
||||||
|
dev = dongle()
|
||||||
|
if not dev:
|
||||||
|
sys.exit(1)
|
||||||
|
fd = os.open(dev, os.O_RDWR)
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
os.write(fd, bytes([0x06, 0xbd, MODES[sys.argv[1]]]).ljust(64, b"\0"))
|
||||||
|
print(sys.argv[1])
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
os.write(fd, bytes([0x06, 0xb0]).ljust(64, b"\0"))
|
||||||
|
deadline = time.time() + 1.0
|
||||||
|
while (left := deadline - time.time()) > 0:
|
||||||
|
if not select.select([fd], [], [], left)[0]:
|
||||||
|
break
|
||||||
|
pkt = os.read(fd, 64)
|
||||||
|
# Skip unsolicited 0x07xx event packets until the status reply
|
||||||
|
if len(pkt) > 0x0f and pkt[0] == 0x06 and pkt[1] == 0xb0:
|
||||||
|
if pkt[0x0f] != 0x08:
|
||||||
|
break # headset off / in cradle -> report nothing
|
||||||
|
print(NAMES.get(pkt[0x0a], "off"))
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(1)
|
||||||
|
'';
|
||||||
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
||||||
loc=$(${pkgs.curl}/bin/curl -sf --max-time 5 https://ipinfo.io/loc 2>/dev/null || true)
|
loc=$(${pkgs.curl}/bin/curl -sf --max-time 5 https://ipinfo.io/loc 2>/dev/null || true)
|
||||||
case "$loc" in
|
case "$loc" in
|
||||||
|
|
@ -418,7 +470,7 @@ in
|
||||||
readonly property string systemctl: "${pkgs.systemd}/bin/systemctl"
|
readonly property string systemctl: "${pkgs.systemd}/bin/systemctl"
|
||||||
readonly property string weatherFetch: "${weatherFetchScript}"
|
readonly property string weatherFetch: "${weatherFetchScript}"
|
||||||
readonly property string ssh: "${pkgs.openssh}/bin/ssh"
|
readonly property string ssh: "${pkgs.openssh}/bin/ssh"
|
||||||
readonly property string gdbus: "${pkgs.glib}/bin/gdbus"
|
readonly property string novaAnc: "${novaAncScript}"
|
||||||
// Unprivileged ping works via ICMP dgram sockets — NixOS sets
|
// Unprivileged ping works via ICMP dgram sockets — NixOS sets
|
||||||
// net.ipv4.ping_group_range for everyone (no wrapper exists).
|
// net.ipv4.ping_group_range for everyone (no wrapper exists).
|
||||||
readonly property string ping: "${pkgs.iputils}/bin/ping"
|
readonly property string ping: "${pkgs.iputils}/bin/ping"
|
||||||
|
|
@ -2934,51 +2986,36 @@ in
|
||||||
}
|
}
|
||||||
|
|
||||||
${lib.optionalString (!isMacbook) ''
|
${lib.optionalString (!isMacbook) ''
|
||||||
// Nova Pro noise cancelling — talks to asm-daemon
|
// Nova Pro noise cancelling — writes straight to the
|
||||||
// (Arctis Sound Manager) over the session bus.
|
// base station's HID interface via the nova-anc
|
||||||
// SetSetting("noise_cancelling", "0|1|2"), status read
|
// script (no daemon). Card hides when the dongle or
|
||||||
// back as off/transparent/on. Card hides when the
|
// headset is gone.
|
||||||
// daemon or headset is gone.
|
|
||||||
Card {
|
Card {
|
||||||
id: ancCard
|
id: ancCard
|
||||||
visible: ancMode !== ""
|
visible: ancMode !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
property string ancMode: ""
|
property string ancMode: ""
|
||||||
|
|
||||||
function setMode(key, value) {
|
function setMode(key) {
|
||||||
if (key === ancMode) return;
|
if (key === ancMode) return;
|
||||||
ancMode = key; // optimistic; next poll corrects if the device disagrees
|
ancMode = key; // optimistic; next poll corrects if the device disagrees
|
||||||
ancSet.mode = value;
|
ancSet.mode = key;
|
||||||
ancSet.running = true;
|
ancSet.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: ancSet
|
id: ancSet
|
||||||
property string mode: "0"
|
property string mode: "off"
|
||||||
command: [Commands.gdbus, "call", "--session",
|
command: [Commands.novaAnc, mode]
|
||||||
"--dest", "name.giacomofurlan.ArctisManager.Next",
|
|
||||||
"--object-path", "/name/giacomofurlan/ArctisManager/Next/Settings",
|
|
||||||
"--method", "name.giacomofurlan.ArctisManager.Next.Settings.SetSetting",
|
|
||||||
"noise_cancelling", mode]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: ancStatus
|
id: ancStatus
|
||||||
command: [Commands.gdbus, "call", "--session",
|
command: [Commands.novaAnc]
|
||||||
"--dest", "name.giacomofurlan.ArctisManager.Next",
|
|
||||||
"--object-path", "/name/giacomofurlan/ArctisManager/Next/Status",
|
|
||||||
"--method", "name.giacomofurlan.ArctisManager.Next.Status.GetStatus"]
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
// gdbus prints a GVariant tuple: ('{"headset": ...}',)
|
const m = text.trim();
|
||||||
const a = text.indexOf("('");
|
ancCard.ancMode = (m === "off" || m === "transparent" || m === "on") ? m : "";
|
||||||
const b = text.lastIndexOf("',");
|
|
||||||
if (a < 0 || b <= a) { ancCard.ancMode = ""; return; }
|
|
||||||
try {
|
|
||||||
const st = JSON.parse(text.slice(a + 2, b));
|
|
||||||
const nc = st.headset ? st.headset.noise_cancelling : undefined;
|
|
||||||
ancCard.ancMode = (nc && nc.value) ? nc.value : "";
|
|
||||||
} catch (e) { ancCard.ancMode = ""; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3010,9 +3047,9 @@ in
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{ key: "off", label: "Off", val: "0" },
|
{ key: "off", label: "Off" },
|
||||||
{ key: "transparent", label: "Transparent", val: "1" },
|
{ key: "transparent", label: "Transparent" },
|
||||||
{ key: "on", label: "ANC", val: "2" }
|
{ key: "on", label: "ANC" }
|
||||||
]
|
]
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -3035,7 +3072,7 @@ in
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: ancCard.setMode(modelData.key, modelData.val)
|
onClicked: ancCard.setMode(modelData.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue