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": {
|
||||
"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": {
|
||||
"inputs": {
|
||||
"fromYaml": "fromYaml"
|
||||
|
|
@ -394,7 +372,6 @@
|
|||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"arctis-sound-manager": "arctis-sound-manager",
|
||||
"home-manager": "home-manager",
|
||||
"nix-cachyos-kernel": "nix-cachyos-kernel",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
|
|
|
|||
|
|
@ -20,11 +20,6 @@
|
|||
|
||||
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";
|
||||
};
|
||||
outputs =
|
||||
|
|
@ -54,7 +49,7 @@
|
|||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
FredOS-Gaming = mkHost "FredOS-Gaming" [ inputs.arctis-sound-manager.nixosModules.default ];
|
||||
FredOS-Gaming = mkHost "FredOS-Gaming" [];
|
||||
FredOS-Mediaserver = mkHost "FredOS-Mediaserver" [];
|
||||
FredOS-Macbook = mkHost "FredOS-Macbook" [];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,9 +24,14 @@
|
|||
|
||||
programs.geary.enable = true;
|
||||
|
||||
# SteelSeries Nova Pro control (ANC switching via quickshell talks to its
|
||||
# asm-daemon over D-Bus)
|
||||
services.arctis-sound-manager.enable = true;
|
||||
# Nova Pro Wireless base station: open up its vendor HID interface so
|
||||
# quickshell's ANC switcher can write to it directly (nova-anc script in
|
||||
# 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
|
||||
# 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
|
||||
# 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" ''
|
||||
loc=$(${pkgs.curl}/bin/curl -sf --max-time 5 https://ipinfo.io/loc 2>/dev/null || true)
|
||||
case "$loc" in
|
||||
|
|
@ -418,7 +470,7 @@ in
|
|||
readonly property string systemctl: "${pkgs.systemd}/bin/systemctl"
|
||||
readonly property string weatherFetch: "${weatherFetchScript}"
|
||||
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
|
||||
// net.ipv4.ping_group_range for everyone (no wrapper exists).
|
||||
readonly property string ping: "${pkgs.iputils}/bin/ping"
|
||||
|
|
@ -2934,51 +2986,36 @@ in
|
|||
}
|
||||
|
||||
${lib.optionalString (!isMacbook) ''
|
||||
// Nova Pro noise cancelling — talks to asm-daemon
|
||||
// (Arctis Sound Manager) over the session bus.
|
||||
// SetSetting("noise_cancelling", "0|1|2"), status read
|
||||
// back as off/transparent/on. Card hides when the
|
||||
// daemon or headset is gone.
|
||||
// Nova Pro noise cancelling — writes straight to the
|
||||
// base station's HID interface via the nova-anc
|
||||
// script (no daemon). Card hides when the dongle or
|
||||
// headset is gone.
|
||||
Card {
|
||||
id: ancCard
|
||||
visible: ancMode !== ""
|
||||
width: parent.width
|
||||
property string ancMode: ""
|
||||
|
||||
function setMode(key, value) {
|
||||
function setMode(key) {
|
||||
if (key === ancMode) return;
|
||||
ancMode = key; // optimistic; next poll corrects if the device disagrees
|
||||
ancSet.mode = value;
|
||||
ancSet.mode = key;
|
||||
ancSet.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ancSet
|
||||
property string mode: "0"
|
||||
command: [Commands.gdbus, "call", "--session",
|
||||
"--dest", "name.giacomofurlan.ArctisManager.Next",
|
||||
"--object-path", "/name/giacomofurlan/ArctisManager/Next/Settings",
|
||||
"--method", "name.giacomofurlan.ArctisManager.Next.Settings.SetSetting",
|
||||
"noise_cancelling", mode]
|
||||
property string mode: "off"
|
||||
command: [Commands.novaAnc, mode]
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ancStatus
|
||||
command: [Commands.gdbus, "call", "--session",
|
||||
"--dest", "name.giacomofurlan.ArctisManager.Next",
|
||||
"--object-path", "/name/giacomofurlan/ArctisManager/Next/Status",
|
||||
"--method", "name.giacomofurlan.ArctisManager.Next.Status.GetStatus"]
|
||||
command: [Commands.novaAnc]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
// gdbus prints a GVariant tuple: ('{"headset": ...}',)
|
||||
const a = text.indexOf("('");
|
||||
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 = ""; }
|
||||
const m = text.trim();
|
||||
ancCard.ancMode = (m === "off" || m === "transparent" || m === "on") ? m : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3010,9 +3047,9 @@ in
|
|||
|
||||
Repeater {
|
||||
model: [
|
||||
{ key: "off", label: "Off", val: "0" },
|
||||
{ key: "transparent", label: "Transparent", val: "1" },
|
||||
{ key: "on", label: "ANC", val: "2" }
|
||||
{ key: "off", label: "Off" },
|
||||
{ key: "transparent", label: "Transparent" },
|
||||
{ key: "on", label: "ANC" }
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
|
|
@ -3035,7 +3072,7 @@ in
|
|||
MouseArea {
|
||||
anchors.fill: parent
|
||||
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