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
+202
View File
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* predator_wmi -- minimal WMI method bridge for Acer Predator laptops.
*
* Binds to the Acer gaming WMI interfaces that mainline acer-wmi does not
* bind as a driver and exposes one root-only sysfs attribute per device:
*
* /sys/bus/wmi/devices/<GUID>-<n>/predator_call
*
* write: "<method-id> <hex-input-bytes>" e.g. "20 0300ff00010000ff0302000000000000"
* read : "ok buf <hex>" | "ok int <hex u64>" | "ok none" | "err <errno>"
*
* The read returns the result of the most recent write. All protocol
* knowledge lives in userspace (predator-control daemon); this module only
* moves bytes between sysfs and wmidev_evaluate_method().
*
* Interfaces (PH16-71):
* 7A4DDFE7-... WMBH AcerGamingFunction (fans, lightbar, misc settings)
* 79772EC5-... WMBE Battery health (80 % limit, calibration)
* 61EF69EA-... WMAA APGE action (USB charging, backlight timeout)
*/
#include <linux/acpi.h>
#include <linux/ctype.h>
#include <linux/device.h>
#include <linux/dmi.h>
#if __has_include(<linux/hex.h>)
#include <linux/hex.h>
#endif
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
#include <linux/wmi.h>
#define PWMI_MAX_IN 64
#define PWMI_MAX_OUT 256
struct pwmi_dev {
struct wmi_device *wdev;
struct mutex lock;
int last_err;
u8 last_type; /* 0 none, 1 buffer, 2 integer */
size_t last_len;
u8 last_out[PWMI_MAX_OUT];
u64 last_int;
};
static ssize_t predator_call_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct pwmi_dev *p = dev_get_drvdata(dev);
struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
struct acpi_buffer in;
union acpi_object *obj;
u8 inbuf[PWMI_MAX_IN];
const char *s = buf, *end = buf + count;
unsigned int method = 0;
size_t n = 0;
acpi_status status;
int hi, lo;
/* method id (decimal) */
while (s < end && isspace(*s))
s++;
if (s >= end || !isdigit(*s))
return -EINVAL;
while (s < end && isdigit(*s)) {
method = method * 10 + (*s - '0');
if (method > 0xffff)
return -EINVAL;
s++;
}
while (s < end && isspace(*s))
s++;
/* hex payload */
while (s + 1 < end && !isspace(*s)) {
hi = hex_to_bin(s[0]);
lo = hex_to_bin(s[1]);
if (hi < 0 || lo < 0 || n >= PWMI_MAX_IN)
return -EINVAL;
inbuf[n++] = (hi << 4) | lo;
s += 2;
}
while (s < end && isspace(*s))
s++;
if (s != end)
return -EINVAL;
in.length = n;
in.pointer = inbuf;
mutex_lock(&p->lock);
p->last_type = 0;
p->last_len = 0;
p->last_err = 0;
status = wmidev_evaluate_method(p->wdev, 0, method, &in, &out);
if (ACPI_FAILURE(status)) {
p->last_err = -EIO;
goto unlock;
}
obj = out.pointer;
if (obj && obj->type == ACPI_TYPE_BUFFER) {
p->last_type = 1;
p->last_len = min_t(size_t, obj->buffer.length, PWMI_MAX_OUT);
memcpy(p->last_out, obj->buffer.pointer, p->last_len);
} else if (obj && obj->type == ACPI_TYPE_INTEGER) {
p->last_type = 2;
p->last_int = obj->integer.value;
}
kfree(out.pointer);
unlock:
mutex_unlock(&p->lock);
return p->last_err ? p->last_err : count;
}
static ssize_t predator_call_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct pwmi_dev *p = dev_get_drvdata(dev);
ssize_t len;
mutex_lock(&p->lock);
if (p->last_err)
len = sysfs_emit(buf, "err %d\n", p->last_err);
else if (p->last_type == 1)
len = sysfs_emit(buf, "ok buf %*phN\n", (int)p->last_len,
p->last_out);
else if (p->last_type == 2)
len = sysfs_emit(buf, "ok int %016llx\n", p->last_int);
else
len = sysfs_emit(buf, "ok none\n");
mutex_unlock(&p->lock);
return len;
}
static DEVICE_ATTR_ADMIN_RW(predator_call);
static struct attribute *pwmi_attrs[] = {
&dev_attr_predator_call.attr,
NULL,
};
ATTRIBUTE_GROUPS(pwmi);
static int pwmi_probe(struct wmi_device *wdev, const void *ctx)
{
struct pwmi_dev *p;
p = devm_kzalloc(&wdev->dev, sizeof(*p), GFP_KERNEL);
if (!p)
return -ENOMEM;
p->wdev = wdev;
mutex_init(&p->lock);
dev_set_drvdata(&wdev->dev, p);
return 0;
}
static const struct wmi_device_id pwmi_id_table[] = {
{ .guid_string = "7A4DDFE7-5B5D-40B4-8595-4408E0CC7F56" },
{ .guid_string = "79772EC5-04B1-4BFD-843C-61E7F77B6CC9" },
{ .guid_string = "61EF69EA-865C-4BC3-A502-A0DEBA0CB531" },
{ }
};
MODULE_DEVICE_TABLE(wmi, pwmi_id_table);
static struct wmi_driver pwmi_driver = {
.driver = {
.name = "predator_wmi",
.dev_groups = pwmi_groups,
},
.id_table = pwmi_id_table,
.probe = pwmi_probe,
.no_singleton = true,
};
static const struct dmi_system_id pwmi_dmi[] = {
{ .matches = { DMI_MATCH(DMI_SYS_VENDOR, "Acer") } },
{ }
};
static int __init pwmi_init(void)
{
if (!dmi_check_system(pwmi_dmi))
return -ENODEV;
return wmi_driver_register(&pwmi_driver);
}
static void __exit pwmi_exit(void)
{
wmi_driver_unregister(&pwmi_driver);
}
module_init(pwmi_init);
module_exit(pwmi_exit);
MODULE_AUTHOR("predator-control");
MODULE_DESCRIPTION("WMI method bridge for Acer Predator laptops");
MODULE_LICENSE("GPL");