Initial release of Predator Control 1.0.0

PredatorSense replacement for the Acer Predator Helios 16 (PH16-71) on Linux:

- predatord: root service for profiles, fans (incl. temperature curves),
  keyboard RGB, lightbar and battery health; restores settings at boot
  and after resume, switches profiles on AC/battery changes
- Qt desktop app with dashboard, fan curve editor and per-key RGB studio
- Plasma 6 panel widget with quick controls
- predatorctl command line tool
- predator_wmi kernel module (DKMS) as a minimal WMI method bridge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 18:38:14 +02:00
co-authored by Claude Opus 5.5
commit ea7ce49452
34 changed files with 3774 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
"""Physical keyboard layouts of the PH16-71 mapped to the 128 LED cells of the keyboard MCU.
cell = column * 6 + row (row 0 = bottom row). The mapping is based on the keymap
recorded by the Venator project (GPL-2.0). Corrections can be placed in
~/.config/predator-control/layout.json as {"<label>": <cell>}.
Each key: (label, cell, x, y, width, height) in key units.
"""
import json
import locale
import os
def _row(y, keys, x0=0.0, h=1.0):
out, x = [], x0
for label, cell, w in keys:
if label is not None:
out.append((label, cell, x, y, w, h))
x += w
return out
def _build(variant):
iso = variant == "iso-de"
keys = []
fw = 15.0 / 16
keys += _row(0, [("Esc", 5, fw)] + [("F%d" % i, 5 + 6 * i, fw) for i in range(1, 13)] +
[("PrtSc", 83, fw), ("Ins", 89, fw), ("Del", 95, fw)], h=0.8)
y = 0.9
nums = [str((i + 1) % 10) for i in range(10)]
keys += _row(y, [("^" if iso else "`", 4, 1)] + [(n, 10 + 6 * i, 1) for i, n in enumerate(nums)] +
[("ß" if iso else "-", 70, 1), ("´" if iso else "=", 76, 1), ("⌫", 82, 2)])
y += 1
top = "QWERTZUIOP" if iso else "QWERTYUIOP"
if iso:
keys += _row(y, [("Tab", 3, 1.5)] + [(c, 15 + 6 * i, 1) for i, c in enumerate(top)] +
[("Ü", 75, 1), ("+", 81, 1), ("↵", 86, 1.5)])
else:
keys += _row(y, [("Tab", 3, 1.5)] + [(c, 15 + 6 * i, 1) for i, c in enumerate(top)] +
[("[", 75, 1), ("]", 81, 1), ("\\", 87, 1.5)])
y += 1
if iso:
keys += _row(y, [("Caps", 2, 1.75)] + [(c, 20 + 6 * i, 1) for i, c in enumerate("ASDFGHJKL")] +
[("Ö", 74, 1), ("Ä", 80, 1), ("#", 87, 1), ("↵", 86, 1.25)])
else:
keys += _row(y, [("Caps", 2, 1.75)] + [(c, 20 + 6 * i, 1) for i, c in enumerate("ASDFGHJKL")] +
[(";", 74, 1), ("'", 80, 1), ("Enter", 86, 2.25)])
y += 1
if iso:
keys += _row(y, [("⇧", 1, 1.25), ("<", 13, 1)] + [(c, 19 + 6 * i, 1) for i, c in enumerate("YXCVBNM")] +
[(",", 61, 1), (".", 67, 1), ("-", 73, 1), ("⇧", 85, 2.75)])
else:
keys += _row(y, [("⇧", 1, 2.25)] + [(c, 19 + 6 * i, 1) for i, c in enumerate("ZXCVBNM")] +
[(",", 61, 1), (".", 67, 1), ("/", 73, 1), ("⇧", 85, 2.75)])
y += 1
keys += _row(y, [("Ctrl", 0, 1.25), ("Fn", 6, 1), ("⊞", 12, 1), ("Alt", 18, 1),
("", 24, 5), ("AltGr" if iso else "Alt", 60, 1), ("☰", 72, 1), ("Ctrl", 78, 1),
("←", 84, 1)])
keys.append(("↑", 90, 13.25, y, 1, 0.5))
keys.append(("↓", 96, 13.25, y + 0.5, 1, 0.5))
keys.append(("→", 102, 14.25, y, 1, 1))
# column of special keys between the main block and the numpad
sx = 15.6
for i, (lbl, cell) in enumerate([("⏻", 107), ("P", 106), ("⏯", 105), ("⏮", 104), ("⏭", 103)]):
keys.append((lbl, cell, sx, 0 if i == 0 else 0.9 + (i - 1), 1, 0.8 if i == 0 else 1))
# numpad
nx = 16.85
y = 0.9
keys += _row(y, [("Num", 112, 1), ("/", 118, 1), ("*", 124, 1), ("−", 123, 1)], x0=nx)
keys += _row(y + 1, [("7", 99, 1), ("8", 111, 1), ("9", 117, 1)], x0=nx)
keys.append(("+", 122, nx + 3, y + 1, 1, 2))
keys += _row(y + 2, [("4", 101, 1), ("5", 110, 1), ("6", 116, 1)], x0=nx)
keys += _row(y + 3, [("1", 100, 1), ("2", 109, 1), ("3", 115, 1)], x0=nx)
keys.append(("↵", 120, nx + 3, y + 3, 1, 2))
keys += _row(y + 4, [("0", 108, 2), ("," if iso else ".", 114, 1)], x0=nx)
return keys
VARIANTS = {"iso-de": "German (ISO)", "ansi-us": "US (ANSI)"}
def default_variant():
lang = (os.environ.get("LANG") or locale.getlocale()[0] or "").lower()
return "iso-de" if lang.startswith(("de", "at", "ch")) else "ansi-us"
def keys(variant=None):
ks = _build(variant or default_variant())
path = os.path.expanduser("~/.config/predator-control/layout.json")
try:
with open(path) as f:
over = json.load(f)
ks = [(l, over.get(l, c), x, y, w, h) for (l, c, x, y, w, h) in ks]
except (OSError, ValueError):
pass
return ks
def size(ks):
return max(k[2] + k[4] for k in ks), max(k[3] + k[5] for k in ks)
# cell groups for quick selection in the RGB studio
GROUPS = {
"WASD": [21, 20, 26, 32],
"Arrows": [84, 90, 96, 102],
"F-row": [5 + 6 * i for i in range(13)] + [83, 89, 95],
"Number row": [4] + [10 + 6 * i for i in range(10)] + [70, 76, 82],
"Numpad": [112, 118, 124, 123, 99, 111, 117, 122, 101, 110, 116, 100, 109, 115, 120, 108, 114],
"Special keys": [107, 106, 105, 104, 103],
"Modifiers": [0, 6, 12, 18, 60, 72, 78, 1, 85, 2, 3],
}