Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571cfcca2a | ||
|
|
5b7534560f |
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.0 – 2026-10-07
|
||||
|
||||
### Added
|
||||
- **Power page** with energy settings for the lighting:
|
||||
- toggle for the firmware's automatic keyboard backlight switch-off after 30 s
|
||||
(moved here from the System page)
|
||||
- turn keyboard and lightbar off after a configurable idle time, separately for AC and battery
|
||||
(never, 30 s … 30 min); any keyboard, touchpad or mouse input turns them back on
|
||||
- on battery: reduced keyboard brightness, keyboard lighting off, lightbar off
|
||||
|
||||
## 1.3.0 – 2026-10-07
|
||||
|
||||
### Added
|
||||
- **Predator key opens the app.** The key produces no input event on Linux (its usage code is
|
||||
outside the range the keyboard declares), so predatord listens on the keyboard's vendor HID
|
||||
report instead and opens Predator Control in the active desktop session
|
||||
- Choose the key's action on the System page: open the app, switch to the next performance mode,
|
||||
or do nothing
|
||||
|
||||
## 1.2.0 – 2026-10-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -83,6 +83,17 @@ Keyboard RGB and performance profiles work without the module.
|
||||
|
||||
Uninstall with `sudo pacman -R predator-control` (package) or `sudo ./uninstall.sh` (add `--purge` to also delete your settings).
|
||||
|
||||
## Power settings
|
||||
|
||||
The **Power** page controls when the lighting switches off: the firmware's 30-second backlight
|
||||
timeout can be disabled, a custom idle timeout can be set separately for AC and battery, and on
|
||||
battery the keyboard can be dimmed or turned off and the lightbar disabled.
|
||||
|
||||
## Predator key
|
||||
|
||||
Pressing the **Predator key** opens Predator Control, just like PredatorSense on Windows.
|
||||
On the **System** page you can instead make it switch to the next performance mode, or disable it.
|
||||
|
||||
## Updates
|
||||
|
||||
Installed via the pacman package, Predator Control updates itself: the service checks for new
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# sudo ./install.sh --no-deps skip package installation (non-Arch distros)
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.2.0"
|
||||
VERSION="1.4.0"
|
||||
MODULE="predator-wmi"
|
||||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PREFIX="/usr"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
PACKAGE_NAME="predator-wmi"
|
||||
PACKAGE_VERSION="1.2.0"
|
||||
PACKAGE_VERSION="1.4.0"
|
||||
BUILT_MODULE_NAME[0]="predator_wmi"
|
||||
DEST_MODULE_LOCATION[0]="/kernel/drivers/platform/x86"
|
||||
MAKE[0]="make KVER=${kernelver} KDIR=${kernel_source_dir}"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# sudo pacman -U predator-control-*.pkg.tar.zst
|
||||
|
||||
pkgname=predator-control
|
||||
pkgver=1.2.0
|
||||
pkgver=1.4.0
|
||||
pkgrel=1
|
||||
pkgdesc="PredatorSense replacement for Acer Predator laptops: performance modes, fans, keyboard RGB, lightbar, battery (Qt app + Plasma widget)"
|
||||
arch=('any')
|
||||
@@ -22,7 +22,7 @@ install=predator-control.install
|
||||
source=()
|
||||
|
||||
_src="$(realpath "$startdir/../..")"
|
||||
_modver=1.2.0
|
||||
_modver=1.4.0
|
||||
|
||||
package() {
|
||||
cd "$_src"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Id": "org.predatorcontrol.panel",
|
||||
"License": "GPL-2.0-only",
|
||||
"Name": "Predator Control",
|
||||
"Version": "1.2.0",
|
||||
"Version": "1.4.0",
|
||||
"Website": "https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard"
|
||||
},
|
||||
"X-Plasma-API-Minimum-Version": "6.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
__version__ = "1.2.0"
|
||||
__version__ = "1.4.0"
|
||||
|
||||
APP_NAME = "Predator Control"
|
||||
RUN_DIR = os.environ.get("PREDATOR_CONTROL_RUN_DIR", "/run/predator-control")
|
||||
|
||||
@@ -25,6 +25,8 @@ import traceback
|
||||
from . import CONFIG_DIR, CONFIG_PATH, SOCKET_PATH, STATE_PATH, __version__
|
||||
from . import hardware as hw
|
||||
from . import updater
|
||||
from .hotkey import HotkeyListener, launch_in_session
|
||||
from .idle import IdleWatcher
|
||||
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -49,6 +51,14 @@ DEFAULT_CONFIG = {
|
||||
"battery": {"limit": None}, # None = leave the firmware state alone
|
||||
"keyboard_presets": {},
|
||||
"updates": {"auto_check": True, "interval_hours": 6},
|
||||
"predator_key": "open", # open | cycle_profile | none
|
||||
"power": {
|
||||
"idle_off_ac": 0, # seconds without input until lighting turns off, 0 = never
|
||||
"idle_off_battery": 120,
|
||||
"battery_brightness": 100, # keyboard brightness on battery, % of the normal value
|
||||
"battery_lights_off": False,
|
||||
"battery_lightbar_off": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +108,9 @@ class Daemon:
|
||||
self.update_info = None
|
||||
self.update_error = None
|
||||
self._update_checking = False
|
||||
self.idle = IdleWatcher(self._activity)
|
||||
self.lights_idle = False
|
||||
self.on_ac = True
|
||||
|
||||
# ---------------------------------------------------------- Configuration
|
||||
def _load_config(self):
|
||||
@@ -130,8 +143,35 @@ class Daemon:
|
||||
self.errors[key] = str(e)
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------- Power rules
|
||||
def keyboard_effective(self):
|
||||
k = dict(self.config["keyboard"])
|
||||
p = self.config["power"]
|
||||
if self.lights_idle or (not self.on_ac and p.get("battery_lights_off")):
|
||||
k["effect"] = "off"
|
||||
elif not self.on_ac and p.get("battery_brightness", 100) < 100:
|
||||
k["brightness"] = int(k["brightness"] * p["battery_brightness"] / 100)
|
||||
return k
|
||||
|
||||
def idle_timeout(self):
|
||||
p = self.config["power"]
|
||||
return int(p.get("idle_off_ac" if self.on_ac else "idle_off_battery") or 0)
|
||||
|
||||
def update_idle(self):
|
||||
t = self.idle_timeout()
|
||||
idle = t > 0 and self.idle.idle_seconds() >= t
|
||||
if idle != self.lights_idle:
|
||||
self.lights_idle = idle
|
||||
log("Lighting %s" % ("off (idle)" if idle else "restored"))
|
||||
self.apply_keyboard()
|
||||
|
||||
def _activity(self):
|
||||
if self.lights_idle:
|
||||
with self.lock:
|
||||
self.update_idle()
|
||||
|
||||
def apply_keyboard(self, cfg=None):
|
||||
k = cfg or self.config["keyboard"]
|
||||
k = cfg or self.keyboard_effective()
|
||||
frame = bytes.fromhex(k.get("frame") or "") if k["effect"] == "perkey" else None
|
||||
self._try("keyboard", self.kbd.apply, k["effect"], hex_to_rgb(k["color"]),
|
||||
k["brightness"], frame, int(k.get("effect_id") or 0))
|
||||
@@ -140,8 +180,11 @@ class Daemon:
|
||||
|
||||
def lightbar_effective(self):
|
||||
lb = dict(self.config["lightbar"])
|
||||
if self.lights_idle or (not self.on_ac and self.config["power"].get("battery_lightbar_off")):
|
||||
lb["mode"] = "off"
|
||||
return lb
|
||||
if lb.get("sync_keyboard"):
|
||||
k = self.config["keyboard"]
|
||||
k = self.keyboard_effective()
|
||||
lb["brightness"] = k["brightness"]
|
||||
if k["effect"] == "off":
|
||||
lb["mode"] = "off"
|
||||
@@ -177,6 +220,9 @@ class Daemon:
|
||||
def restore(self):
|
||||
log("Restoring settings")
|
||||
bat = hw.battery()
|
||||
self.on_ac = bat.get("ac_online", True)
|
||||
self.idle.last_input = time.monotonic() # e.g. after resume: lights on
|
||||
self.lights_idle = False
|
||||
if self.config["profile"].get("auto_switch"):
|
||||
self.apply_profile_for_power(bat.get("ac_online", True))
|
||||
self.apply_fans()
|
||||
@@ -235,6 +281,7 @@ class Daemon:
|
||||
"lightbar_effective": self.lightbar_effective(),
|
||||
"fan_curve_applied": self.curve_applied,
|
||||
"battery_limit": self.battery_limit,
|
||||
"lights_idle": self.lights_idle,
|
||||
"update": self.update_info,
|
||||
"update_error": self.update_error,
|
||||
"update_status": updater.status(),
|
||||
@@ -277,6 +324,18 @@ class Daemon:
|
||||
self._last_try = time.time()
|
||||
self.check_update_async()
|
||||
|
||||
def predator_key(self):
|
||||
action = self.config.get("predator_key", "open")
|
||||
if action == "open":
|
||||
if not launch_in_session(["predator-control"]):
|
||||
log("Predator key: no active graphical session to open the app in")
|
||||
elif action == "cycle_profile":
|
||||
with self.lock:
|
||||
ch = hw.profile_choices()
|
||||
cur = hw.get_profile()
|
||||
nxt = ch[(ch.index(cur) + 1) % len(ch)] if cur in ch else ch[0]
|
||||
self._try("profile", hw.set_profile, nxt)
|
||||
|
||||
def loop(self):
|
||||
while True:
|
||||
try:
|
||||
@@ -294,7 +353,11 @@ class Daemon:
|
||||
log("Power source changed:", "AC" if ac else "battery")
|
||||
if self.config["profile"].get("auto_switch"):
|
||||
self.apply_profile_for_power(ac)
|
||||
if ac != self.on_ac:
|
||||
self.on_ac = ac
|
||||
self.apply_keyboard() # battery lighting rules
|
||||
self.last_ac = ac
|
||||
self.update_idle()
|
||||
self.fan_curve_tick()
|
||||
self._ticks += 1
|
||||
if self._ticks % 20 == 1:
|
||||
@@ -328,6 +391,23 @@ class Daemon:
|
||||
merge(self.config["updates"], {k: a[k] for k in ("auto_check", "interval_hours") if k in a})
|
||||
self.save_config()
|
||||
return {}
|
||||
if cmd == "set_power":
|
||||
p = self.config["power"]
|
||||
for key in ("idle_off_ac", "idle_off_battery", "battery_brightness",
|
||||
"battery_lights_off", "battery_lightbar_off"):
|
||||
if key in a:
|
||||
p[key] = a[key]
|
||||
p["battery_brightness"] = max(0, min(100, int(p["battery_brightness"])))
|
||||
self.save_config()
|
||||
self.update_idle()
|
||||
self.apply_keyboard()
|
||||
return {}
|
||||
if cmd == "set_predator_key":
|
||||
if a["action"] not in ("open", "cycle_profile", "none"):
|
||||
raise ValueError("unknown action")
|
||||
self.config["predator_key"] = a["action"]
|
||||
self.save_config()
|
||||
return {}
|
||||
if cmd == "restore":
|
||||
self.restore()
|
||||
return {}
|
||||
@@ -511,6 +591,8 @@ def main():
|
||||
signal.signal(signal.SIGTERM, lambda *_: (server.server_close(), sys.exit(0)))
|
||||
signal.signal(signal.SIGUSR1, lambda *_: threading.Thread(target=lambda: (d.lock.acquire(), d.restore(), d.lock.release())).start())
|
||||
threading.Thread(target=d.loop, daemon=True).start()
|
||||
HotkeyListener(d.predator_key, log).start()
|
||||
d.idle.start()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
|
||||
+106
-2
@@ -26,6 +26,7 @@ PAGES = [
|
||||
("fans", "Fans"),
|
||||
("rgb", "RGB Studio"),
|
||||
("battery", "Battery"),
|
||||
("power", "Power"),
|
||||
("system", "System"),
|
||||
]
|
||||
|
||||
@@ -591,6 +592,92 @@ class RgbPage(Page):
|
||||
self.lb_speed.setValue(lb["speed"])
|
||||
|
||||
|
||||
IDLE_CHOICES = [(0, "Never"), (30, "30 seconds"), (60, "1 minute"), (120, "2 minutes"), (300, "5 minutes"),
|
||||
(600, "10 minutes"), (900, "15 minutes"), (1800, "30 minutes")]
|
||||
|
||||
|
||||
class PowerPage(Page):
|
||||
def __init__(self, win):
|
||||
super().__init__(win)
|
||||
self.lay.addWidget(_title("Power", "Energy settings for the keyboard lighting and lightbar"))
|
||||
|
||||
fw = Card("Firmware backlight timeout")
|
||||
self.fw_timeout = QCheckBox("Turn the keyboard backlight off after 30 s without input (firmware)")
|
||||
self.fw_timeout.clicked.connect(lambda v: (win.backend.run("set_firmware", name="kbd_timeout", value=bool(v)),
|
||||
QTimer.singleShot(800, lambda: win.backend.run("get_firmware"))))
|
||||
fw.body.addWidget(self.fw_timeout)
|
||||
fw.body.addWidget(_muted("This is the built-in automatic switch-off of the laptop. Turn it off to keep the "
|
||||
"lighting on permanently, or use the adjustable timeout below instead."))
|
||||
self.lay.addWidget(fw)
|
||||
|
||||
idle = Card("Turn lighting off when idle")
|
||||
g = QGridLayout()
|
||||
self.idle_ac = QComboBox()
|
||||
self.idle_bat = QComboBox()
|
||||
for combo, key, label in ((self.idle_ac, "idle_off_ac", "On AC power"),
|
||||
(self.idle_bat, "idle_off_battery", "On battery")):
|
||||
for val, text in IDLE_CHOICES:
|
||||
combo.addItem(text, val)
|
||||
combo.activated.connect(lambda _=0, c=combo, k=key: win.backend.run("set_power", **{k: c.currentData()}))
|
||||
g.addWidget(QLabel("On AC power"), 0, 0)
|
||||
g.addWidget(self.idle_ac, 0, 1)
|
||||
g.addWidget(QLabel("On battery"), 1, 0)
|
||||
g.addWidget(self.idle_bat, 1, 1)
|
||||
g.setColumnStretch(2, 1)
|
||||
idle.body.addLayout(g)
|
||||
idle.body.addWidget(_muted("Keyboard and lightbar turn off after this time without keyboard, touchpad or "
|
||||
"mouse input, and come back on with the next input."))
|
||||
self.idle_state = _muted("")
|
||||
idle.body.addWidget(self.idle_state)
|
||||
self.lay.addWidget(idle)
|
||||
|
||||
bat = Card("On battery")
|
||||
h = QHBoxLayout()
|
||||
h.addWidget(QLabel("Keyboard brightness"))
|
||||
self.bat_bright = _slider(10, 100, 100)
|
||||
self.bat_bright.setSingleStep(10)
|
||||
self.bat_bright_l = QLabel("100 %")
|
||||
self.bat_bright.valueChanged.connect(lambda v: self.bat_bright_l.setText("%d %%" % v))
|
||||
self.bat_bright.sliderReleased.connect(lambda: win.backend.run("set_power", battery_brightness=self.bat_bright.value()))
|
||||
h.addWidget(self.bat_bright, 1)
|
||||
h.addWidget(self.bat_bright_l)
|
||||
bat.body.addLayout(h)
|
||||
bat.body.addWidget(_muted("Percentage of your normal keyboard brightness while running on battery."))
|
||||
self.bat_off = QCheckBox("Turn the keyboard lighting off completely on battery")
|
||||
self.bat_off.clicked.connect(lambda v: win.backend.run("set_power", battery_lights_off=bool(v)))
|
||||
bat.body.addWidget(self.bat_off)
|
||||
self.bat_lb = QCheckBox("Turn the lightbar off on battery")
|
||||
self.bat_lb.clicked.connect(lambda v: win.backend.run("set_power", battery_lightbar_off=bool(v)))
|
||||
bat.body.addWidget(self.bat_lb)
|
||||
self.lay.addWidget(bat)
|
||||
self.lay.addStretch(1)
|
||||
|
||||
def showEvent(self, e):
|
||||
super().showEvent(e)
|
||||
self.win.backend.run("get_firmware")
|
||||
|
||||
def firmware(self, fw):
|
||||
v = fw.get("kbd_timeout", {})
|
||||
self.fw_timeout.setEnabled(v.get("value") is not None)
|
||||
self.fw_timeout.setChecked(bool(v.get("value")))
|
||||
self.fw_timeout.setToolTip(v.get("error", "") or "")
|
||||
|
||||
def update_state(self, st):
|
||||
p = st["config"].get("power", {})
|
||||
for combo, key in ((self.idle_ac, "idle_off_ac"), (self.idle_bat, "idle_off_battery")):
|
||||
if not combo.view().isVisible():
|
||||
i = combo.findData(p.get(key, 0))
|
||||
if i < 0:
|
||||
combo.addItem("%d seconds" % p[key], p[key])
|
||||
i = combo.count() - 1
|
||||
combo.setCurrentIndex(i)
|
||||
if not self.bat_bright.isSliderDown():
|
||||
self.bat_bright.setValue(p.get("battery_brightness", 100))
|
||||
self.bat_off.setChecked(bool(p.get("battery_lights_off")))
|
||||
self.bat_lb.setChecked(bool(p.get("battery_lightbar_off")))
|
||||
self.idle_state.setText("Lighting is currently off because the laptop is idle." if st.get("lights_idle") else "")
|
||||
|
||||
|
||||
class BatteryPage(Page):
|
||||
def __init__(self, win):
|
||||
super().__init__(win)
|
||||
@@ -674,6 +761,19 @@ class SystemPage(Page):
|
||||
self.fw_card.body.addWidget(_muted("Values are stored in the laptop firmware (EC/BIOS)."))
|
||||
self.lay.addWidget(self.fw_card)
|
||||
|
||||
key = Card("Predator key")
|
||||
kr = QHBoxLayout()
|
||||
kr.addWidget(QLabel("When the Predator key is pressed"))
|
||||
self.key_action = QComboBox()
|
||||
for val, text in (("open", "Open Predator Control"), ("cycle_profile", "Switch to the next performance mode"),
|
||||
("none", "Do nothing")):
|
||||
self.key_action.addItem(text, val)
|
||||
self.key_action.activated.connect(lambda: win.backend.run("set_predator_key", action=self.key_action.currentData()))
|
||||
kr.addWidget(self.key_action)
|
||||
kr.addStretch(1)
|
||||
key.body.addLayout(kr)
|
||||
self.lay.addWidget(key)
|
||||
|
||||
upd = Card("Updates")
|
||||
self.upd_head = QLabel("")
|
||||
self.upd_head.setObjectName("big")
|
||||
@@ -722,7 +822,7 @@ class SystemPage(Page):
|
||||
|
||||
def firmware(self, fw):
|
||||
for name, info in fw.items():
|
||||
if "label" not in info:
|
||||
if "label" not in info or name == "kbd_timeout": # shown on the Power page
|
||||
continue
|
||||
if name == "usb_charging":
|
||||
w = self.fw_boxes.get(name)
|
||||
@@ -782,6 +882,8 @@ class SystemPage(Page):
|
||||
err = ("\n\n⚠ %s" % st["update_error"]) if st.get("update_error") else ""
|
||||
self.upd_text.setText("Installed version %s · last check %s%s%s" % (st.get("version"), when, err, extra))
|
||||
self.upd_install.hide()
|
||||
if not self.key_action.view().isVisible():
|
||||
self.key_action.setCurrentIndex(max(0, self.key_action.findData(st["config"].get("predator_key", "open"))))
|
||||
self.upd_auto.setChecked(st["config"].get("updates", {}).get("auto_check", True))
|
||||
a = st["available"]
|
||||
yes = lambda v: "✔" if v else "✘"
|
||||
@@ -828,7 +930,8 @@ class MainWindow(QMainWindow):
|
||||
self.nav = QButtonGroup(self)
|
||||
self.pages = {}
|
||||
classes = {"home": HomePage, "performance": PerformancePage, "fans": FansPage,
|
||||
"rgb": RgbPage, "battery": BatteryPage, "system": SystemPage}
|
||||
"rgb": RgbPage, "battery": BatteryPage, "power": PowerPage,
|
||||
"system": SystemPage}
|
||||
for key, label in PAGES:
|
||||
b = QPushButton(label)
|
||||
b.setObjectName("nav")
|
||||
@@ -901,6 +1004,7 @@ class MainWindow(QMainWindow):
|
||||
elif cmd == "get_firmware":
|
||||
self.pages["system"][0].firmware(result)
|
||||
self.pages["battery"][0].firmware(result)
|
||||
self.pages["power"][0].firmware(result)
|
||||
return
|
||||
else:
|
||||
self.statusBar().showMessage("Applied", 2000)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Predator key listener.
|
||||
|
||||
The PH16-71 Predator key produces no input event: its keyboard usage (0xC9) is outside the
|
||||
range the keyboard declares, so the kernel drops it. The key is also reported on the
|
||||
vendor report 4 of USB interface 2: `04 81 ff` on press, `04 00 00` on release.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import select
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .keyboard import HOTKEY_INTERFACE, find_hidraw
|
||||
|
||||
PRESS_PREFIX = b"\x04\x81"
|
||||
|
||||
|
||||
def active_session_user():
|
||||
"""(uid, name) of the active graphical session, or None."""
|
||||
out = subprocess.run(["loginctl", "list-sessions", "--no-legend"],
|
||||
capture_output=True, text=True).stdout
|
||||
for line in out.splitlines():
|
||||
sid = line.split()[0] if line.split() else None
|
||||
if not sid:
|
||||
continue
|
||||
props = dict(l.split("=", 1) for l in subprocess.run(
|
||||
["loginctl", "show-session", sid, "-p", "Active", "-p", "Type", "-p", "User", "-p", "Name"],
|
||||
capture_output=True, text=True).stdout.splitlines() if "=" in l)
|
||||
if props.get("Active") == "yes" and props.get("Type") in ("wayland", "x11"):
|
||||
return int(props["User"]), props["Name"]
|
||||
return None
|
||||
|
||||
|
||||
def launch_in_session(args):
|
||||
sess = active_session_user()
|
||||
if not sess:
|
||||
return False
|
||||
uid, name = sess
|
||||
# runs in the user's systemd manager, which carries WAYLAND_DISPLAY/DISPLAY on Plasma
|
||||
r = subprocess.run(["systemd-run", "--quiet", "--user", "--machine=%s@.host" % name, "--collect",
|
||||
"--unit=predator-control-launch-%d" % int(time.time() * 1000)] + args,
|
||||
capture_output=True, text=True)
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
class HotkeyListener(threading.Thread):
|
||||
def __init__(self, on_press, log):
|
||||
super().__init__(daemon=True)
|
||||
self.on_press = on_press
|
||||
self.log = log
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
path = find_hidraw(HOTKEY_INTERFACE)
|
||||
if not path:
|
||||
time.sleep(5)
|
||||
continue
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY)
|
||||
except OSError as e:
|
||||
self.log("hotkey: cannot open %s: %s" % (path, e))
|
||||
time.sleep(5)
|
||||
continue
|
||||
last = 0.0
|
||||
try:
|
||||
while True:
|
||||
select.select([fd], [], [])
|
||||
data = os.read(fd, 64)
|
||||
if not data:
|
||||
break
|
||||
if data.startswith(PRESS_PREFIX) and time.monotonic() - last > 0.4:
|
||||
last = time.monotonic()
|
||||
try:
|
||||
self.on_press()
|
||||
except Exception as e:
|
||||
self.log("hotkey: %s" % e)
|
||||
except OSError:
|
||||
pass # device vanished (suspend/replug) – reopen
|
||||
finally:
|
||||
os.close(fd)
|
||||
time.sleep(1)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Input activity watcher used for the 'turn lighting off when idle' feature."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class IdleWatcher(threading.Thread):
|
||||
"""Tracks the time of the last keyboard/touchpad/mouse event on any input device."""
|
||||
|
||||
RESCAN = 30 # seconds between scans for new devices (hotplug, resume)
|
||||
|
||||
def __init__(self, on_activity=None):
|
||||
super().__init__(daemon=True)
|
||||
self.last_input = time.monotonic()
|
||||
self.on_activity = on_activity
|
||||
|
||||
def idle_seconds(self):
|
||||
return time.monotonic() - self.last_input
|
||||
|
||||
def _open_all(self, fds):
|
||||
for path in glob.glob("/dev/input/event*"):
|
||||
if path in fds.values():
|
||||
continue
|
||||
try:
|
||||
fds[os.open(path, os.O_RDONLY | os.O_NONBLOCK)] = path
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
fds = {}
|
||||
next_scan = 0.0
|
||||
while True:
|
||||
if time.monotonic() >= next_scan:
|
||||
self._open_all(fds)
|
||||
next_scan = time.monotonic() + self.RESCAN
|
||||
if not fds:
|
||||
time.sleep(5)
|
||||
continue
|
||||
ready, _, _ = select.select(list(fds), [], [], 5)
|
||||
active = False
|
||||
for fd in ready:
|
||||
try:
|
||||
if os.read(fd, 4096):
|
||||
active = True
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except OSError:
|
||||
os.close(fd)
|
||||
fds.pop(fd, None)
|
||||
if active:
|
||||
self.last_input = time.monotonic()
|
||||
if self.on_activity:
|
||||
self.on_activity()
|
||||
@@ -13,6 +13,7 @@ import threading
|
||||
ACER_VID = 0x04F2
|
||||
KNOWN_PIDS = (0x0117, 0x011A)
|
||||
LED_INTERFACE = 3
|
||||
HOTKEY_INTERFACE = 2 # vendor report 4 carries the Predator key
|
||||
NUM_CELLS = 128
|
||||
|
||||
OP_BEGIN = 0x88
|
||||
@@ -57,7 +58,7 @@ def checksum(body7):
|
||||
return (0xFF - (sum(body7) & 0xFF)) & 0xFF
|
||||
|
||||
|
||||
def find_hidraw():
|
||||
def find_hidraw(interface=LED_INTERFACE):
|
||||
"""Path of the hidraw node of the LED interface, or None."""
|
||||
for node in sorted(glob.glob("/sys/class/hidraw/hidraw*")):
|
||||
try:
|
||||
@@ -76,7 +77,7 @@ def find_hidraw():
|
||||
devpath = os.path.realpath(os.path.join(node, "device"))
|
||||
# .../1-7:1.3/0003:04F2:011A.0004 -> interface number from the parent
|
||||
iface = os.path.basename(os.path.dirname(devpath)).rsplit(".", 1)[-1]
|
||||
if iface == str(LED_INTERFACE):
|
||||
if iface == str(interface):
|
||||
return "/dev/" + os.path.basename(node)
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user