Add WMI EC kernel modules + toolkit + standalone HID inventory

This commit is contained in:
Kay Türtscher 2026-05-25 12:48:22 +02:00
parent 5adfde1ab9
commit 7954197fdb
5 changed files with 644 additions and 0 deletions

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Shared HID inventory helper for Super X tools.
Imports:
from superx_hid_inventory import hid_inventory
Returns a list of dicts with keys:
hidraw, devnode, vid, pid, iface, driver, report_descriptor_len, mode, uid, gid
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
def _read_sysfs_text(hidraw_name: str, *path_parts: str) -> str | None:
p = Path("/sys/class/hidraw") / hidraw_name / Path(*path_parts)
try:
return p.read_text().strip()
except (OSError, UnicodeDecodeError):
return None
def _read_sysfs_bytes(hidraw_name: str, *path_parts: str) -> bytes | None:
p = Path("/sys/class/hidraw") / hidraw_name / Path(*path_parts)
try:
return p.read_bytes()
except OSError:
return None
def hid_inventory():
"""Return a list of dicts describing every hidraw device."""
devices = []
base = Path("/sys/class/hidraw")
if not base.exists():
return devices
for hidraw_entry in sorted(base.iterdir()):
name = hidraw_entry.name
dev_symlink = hidraw_entry / "device"
if not dev_symlink.is_symlink():
continue
# Resolve the symlink to get the real HID device path
hid_device = dev_symlink.resolve()
# Walk up: hid_device.parent = USB interface, hid_device.parent.parent = USB device
usb_iface = hid_device.parent
usb_dev = usb_iface.parent
# Try to read USB VID/PID from the USB device
vid = None
pid = None
iface = None
try:
vid = (usb_dev / "idVendor").read_text().strip()
pid = (usb_dev / "idProduct").read_text().strip()
iface = (usb_iface / "bInterfaceNumber").read_text().strip()
except (OSError, UnicodeDecodeError):
# Not a USB device — maybe I2C, Bluetooth, etc. Try uevent
pass
# Get driver from uevent
uevent_path = hid_device / "uevent"
driver = None
if uevent_path.exists():
try:
uevent_raw = uevent_path.read_text()
for line in uevent_raw.splitlines():
if line.startswith("DRIVER="):
driver = line.split("=", 1)[1].strip()
except (OSError, UnicodeDecodeError):
pass
# Get report descriptor
desc = _read_sysfs_bytes(name, "device", "report_descriptor")
report_descriptor_len = len(desc) if desc else 0
# Get permissions
devnode = str(Path("/dev") / name)
mode = None
uid = None
gid = None
try:
st = os.stat(devnode)
mode = oct(st.st_mode)[-3:]
uid = st.st_uid
gid = st.st_gid
except OSError:
pass
devices.append({
"hidraw": name,
"devnode": devnode,
"vid": vid,
"pid": pid,
"iface": iface,
"driver": driver,
"report_descriptor_len": report_descriptor_len,
"mode": mode,
"uid": uid,
"gid": gid,
})
return devices
# For standalone use
if __name__ == "__main__":
devs = hid_inventory()
if not devs:
print("NO HIDRAW DEVICES FOUND")
sys.exit(1)
for d in devs:
vid = d.get("vid", "????")
pid = d.get("pid", "????")
iface = d.get("iface", "?")
driver = d.get("driver", "?")
desc_len = d.get("report_descriptor_len", 0)
mode = d.get("mode", "???")
print(f" /dev/{d['hidraw']} vid=0x{vid} pid=0x{pid} if={iface} drv={driver} desc={desc_len}B perms={mode}")
print(f"\nTotal: {len(devs)} hidraw device(s)")

213
tools/superx_wmi_toolkit.sh Normal file
View file

@ -0,0 +1,213 @@
#!/usr/bin/env bash
# Super X WMI EC Toolkit
#
# All-in-one Script für den Super X. Reihenfolge:
# 1. oxp-sensors checken (wenn geladen: hwmon Werte zeigen)
# 2. Kernel-Module bauen (oxp-wmi-ec-read)
# 3. EC Dump 0x00-0xFF durchführen
# 4. RGB-Kandidaten 0x50-0x60 extra anzeigen
# 5. Optional: Write-Test (nur mit --write)
#
# Usage:
# sudo ./superx_wmi_toolkit.sh # read-only dump
# sudo ./superx_wmi_toolkit.sh --candidates # nur RGB-Kandidaten
# sudo ./superx_wmi_toolkit.sh --write # interaktiver Write-Test
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WMI_DIR="$SCRIPT_DIR/wmi"
# ── helpers ──────────────────────────────────────────────────────────────────
green() { printf '\033[32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
red() { printf '\033[31m%s\033[0m\n' "$*"; }
check_root() {
if [[ $EUID -ne 0 ]]; then
red "Need root. Run with: sudo $0 $*"
exit 1
fi
}
# ── oxp-sensors check ───────────────────────────────────────────────────────
check_oxp_sensors() {
echo "=== oxp-sensors ==="
if lsmod | grep -q oxp_sensors; then
green "oxp-sensors loaded"
if [[ -d /sys/class/hwmon ]]; then
for hwmon in /sys/class/hwmon/hwmon*; do
name=$(cat "$hwmon/name" 2>/dev/null || true)
if [[ "$name" == "oxp-sensors" ]]; then
echo " hwmon: $hwmon"
for f in fan1_input fan2_input temp1_input pwm1 pwm2; do
if [[ -f "$hwmon/$f" ]]; then
printf " %-20s = %s\n" "$f" "$(cat "$hwmon/$f")"
fi
done
fi
done
fi
else
yellow "oxp-sensors NOT loaded"
echo " Install: yay -S oxp-sensors-dkms-git"
fi
echo
}
# ── WMI EC Dump via Kernel Module ──────────────────────────────────────────
build_and_dump() {
echo "=== WMI EC Dump ==="
# Check kernel headers
if [[ ! -d "/lib/modules/$(uname -r)/build" ]]; then
red "Kernel headers not found!"
echo " Install: sudo pacman -S linux-headers"
return 1
fi
# Build
echo "Building oxp-wmi-ec-read..."
make -C "$WMI_DIR" clean >/dev/null 2>&1 || true
if ! make -C "$WMI_DIR" 2>&1; then
red "Build failed"
return 1
fi
echo
# Load and dump
echo "Loading oxp-wmi-ec-read (read-only, 256 bytes)..."
dmesg -C
if ! insmod "$WMI_DIR/oxp-wmi-ec-read.ko" 2>&1; then
red "insmod failed. Check dmesg for details."
dmesg | tail -20
return 1
fi
# Extract dump
echo
green "EC Dump (0x00-0xFF):"
dmesg | grep 'oxp-wmi-ec-read: ec\[' | sort -t'[' -k2 -n
rmmod oxp_wmi_ec_read 2>/dev/null || true
echo
}
dump_candidates() {
echo "=== RGB Candidate Offsets (0x50-0x60) ==="
build_and_dump | grep -E 'ec\[0x5[0-9a-f]\]|ec\[0x60\]'
}
# ── WMI Write (guarded) ─────────────────────────────────────────────────────
wmi_write_test() {
echo "=== WMI Write Test ==="
red "WARNING: This WRITES to EC memory. Only proceed if you understand the risks."
red "A bad write could cause instability, fan failure, or hardware damage."
echo
local offset="${1:-}"
local value="${2:-}"
if [[ -z "$offset" ]]; then
read -r -p "EC offset to write (hex, e.g. 0x57): " offset
fi
if [[ -z "$value" ]]; then
read -r -p "Value to write (hex, e.g. 0x02): " value
fi
# Convert hex
offset_dec=$((offset))
value_dec=$((value))
echo
echo "Writing: ec[0x$offset] = 0x$value"
read -r -p "Type 'yes' to confirm: " confirm
if [[ "$confirm" != "yes" ]]; then
echo "Aborted."
return 1
fi
# Build write module
echo "Building oxp-wmi-write-once..."
make -C "$WMI_DIR" clean >/dev/null 2>&1 || true
if ! make -C "$WMI_DIR" 2>&1; then
red "Build failed"
return 1
fi
# Load with parameters
dmesg -C
if ! insmod "$WMI_DIR/oxp-wmi-write-once.ko" \
offset="$offset_dec" \
value="$value_dec" \
yes_i_understand_this_writes_to_hardware=1 2>&1; then
red "insmod failed. Check dmesg:"
dmesg
return 1
fi
echo
dmesg | grep 'oxp-wmi-write-once'
rmmod oxp_wmi_write_once 2>/dev/null || true
}
# ── WMI CMS Scan ────────────────────────────────────────────────────────────
wmi_cms_scan() {
echo "=== WMI CMS Candidate Scan (Method 4) ==="
yellow "CMS = alternate WMI path (WMBB method 4: CMSW)"
yellow "Scanning 0xF0..0xFF with value 0x01, auto-reverting each"
echo
read -r -p "This will write and revert 16 bytes. Continue? [y/N] " answer
if [[ "${answer,,}" != "y" ]]; then
echo "Skipped."
return 0
fi
# We need the CMS write module
if [[ -f "$WMI_DIR/oxp-wmi-cms-write-once.c" ]]; then
echo "CMS write module exists — but this is a simplified scan."
echo "For full CMS scan, use the old repo's tools/oxp-wmi-cms-scan.sh"
else
yellow "CMS write module not in this repo yet."
fi
}
# ── main ─────────────────────────────────────────────────────────────────────
main() {
check_root "$@"
local mode="full"
while [[ $# -gt 0 ]]; do
case "$1" in
--candidates) mode="candidates"; shift ;;
--write)
mode="write"
shift
wmi_write_test "$@"
exit $?
;;
--help|-h)
echo "Usage: sudo $0 [--candidates|--write [offset] [value]]"
exit 0
;;
*) shift ;;
esac
done
check_oxp_sensors
case "$mode" in
full) build_and_dump ;;
candidates) build_and_dump | grep -E 'ec\[0x5[0-9a-f]\b|ec\[0x60\]' ;;
esac
echo
green "Done."
}
main "$@"

12
tools/wmi/Makefile Normal file
View file

@ -0,0 +1,12 @@
# Super X WMI Kernel Module Makefile
KDIR ?= /lib/modules/$(shell uname -r)/build
obj-m += oxp-wmi-ec-read.o
obj-m += oxp-wmi-write-once.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean

146
tools/wmi/oxp-wmi-ec-read.c Normal file
View file

@ -0,0 +1,146 @@
// SPDX-License-Identifier: GPL-2.0
/*
* OneXPlayer Super X WMI EC read-only probe.
*
* This module evaluates only UMAInterface/GetEcValue (WmiMethodId 5) on the
* OneX WMI GUID. It does not call any write-capable WMI method and does not
* scan the whole EC region by default.
*/
#include <linux/acpi.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/types.h>
#define OXP_UMA_WMI_GUID "1F72B0F1-BFEA-4472-9877-6E62937AB616"
#define OXP_WMI_METHOD_GET_EC_VALUE 5
static ushort offsets[256] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64,
0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c,
0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74,
0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c,
0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84,
0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c,
0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94,
0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c,
0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4,
0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac,
0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4,
0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc,
0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc,
0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4,
0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4,
0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc,
0xfd, 0xfe, 0xff,
};
static unsigned int offsets_count = 256;
module_param_array(offsets, ushort, &offsets_count, 0444);
MODULE_PARM_DESC(offsets, "Declared EC offsets to read via OneX UMAInterface.GetEcValue; max 256");
static int oxp_wmi_get_ec_value(u16 offset, u64 *value)
{
/*
* The BMOF declares GetEcValue(Index: uint16), but the DSDT creates byte
* fields at Arg2 offsets 0 and 2. Passing only a packed 16-bit buffer trips
* AE_AML_BUFFER_LIMIT, so provide the 4-byte WMI method argument frame the
* firmware expects. WMBB only uses byte 0 (LH50) for MethodId 5.
*/
u8 inbuf[4] = { offset & 0xff, offset >> 8, 0x00, 0x00 };
struct acpi_buffer input = {
.length = sizeof(inbuf),
.pointer = inbuf,
};
union acpi_object *obj;
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
acpi_status status;
int ret = 0;
status = wmi_evaluate_method(OXP_UMA_WMI_GUID, 0,
OXP_WMI_METHOD_GET_EC_VALUE, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
obj = output.pointer;
if (!obj) {
ret = -ENODATA;
goto out;
}
switch (obj->type) {
case ACPI_TYPE_INTEGER:
*value = obj->integer.value;
break;
case ACPI_TYPE_BUFFER:
if (obj->buffer.length < 1) {
ret = -ENODATA;
break;
}
*value = obj->buffer.pointer[0];
break;
default:
ret = -EPROTO;
break;
}
out:
kfree(output.pointer);
return ret;
}
static int __init oxp_wmi_ec_read_init(void)
{
int instances, i;
instances = wmi_instance_count(OXP_UMA_WMI_GUID);
pr_info("oxp-wmi-ec-read: GUID %s instances=%d offsets=%u\n",
OXP_UMA_WMI_GUID, instances, offsets_count);
if (instances <= 0)
return -ENODEV;
for (i = 0; i < offsets_count && i < ARRAY_SIZE(offsets); i++) {
u64 value = 0;
int ret = oxp_wmi_get_ec_value(offsets[i], &value);
if (ret)
pr_info("oxp-wmi-ec-read: ec[0x%02x] read failed ret=%d\n",
offsets[i], ret);
else
pr_info("oxp-wmi-ec-read: ec[0x%02x]=0x%02llx (%llu)\n",
offsets[i], value & 0xff, value);
}
return 0;
}
static void __exit oxp_wmi_ec_read_exit(void)
{
pr_info("oxp-wmi-ec-read: unloaded\n");
}
module_init(oxp_wmi_ec_read_init);
module_exit(oxp_wmi_ec_read_exit);
MODULE_AUTHOR("Morpheus");
MODULE_DESCRIPTION("Read-only OneX UMAInterface WMI EC value probe");
MODULE_LICENSE("GPL");

View file

@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-2.0
/*
* OneXPlayer Super X guarded one-shot WMI EC-memory writer.
*
* This targets the same OneX UMAInterface path used by oxp-wmi-ec-read.c:
* MethodId 5: M049(0xFE800400, offset) read
* MethodId 6: M04C(0xFE800400, offset, value) write, then readback
*
* It writes exactly one byte and prints the returned readback value.
*/
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/types.h>
#define OXP_UMA_WMI_GUID "1F72B0F1-BFEA-4472-9877-6E62937AB616"
#define OXP_WMI_METHOD_GET_EC_VALUE 5
#define OXP_WMI_METHOD_SET_EC_VALUE 6
static uint offset = 0x57;
static uint value = 0x02;
static bool yes_i_understand_this_writes_to_hardware;
module_param(offset, uint, 0444);
MODULE_PARM_DESC(offset, "WMI EC-memory offset to write, default 0x57");
module_param(value, uint, 0444);
MODULE_PARM_DESC(value, "Byte value to write, default 0x02");
module_param(yes_i_understand_this_writes_to_hardware, bool, 0444);
MODULE_PARM_DESC(yes_i_understand_this_writes_to_hardware, "Required safety acknowledgement");
static const struct dmi_system_id dmi_table[] = {
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER SUPER X"),
},
},
{},
};
static int oxp_wmi_call(u8 method_id, u8 off, u8 val, u64 *out_value)
{
/* Firmware reads byte 0 as offset and byte 2 as value. */
u8 inbuf[4] = { off, 0x00, val, 0x00 };
struct acpi_buffer input = {
.length = sizeof(inbuf),
.pointer = inbuf,
};
union acpi_object *obj;
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
acpi_status status;
int ret = 0;
status = wmi_evaluate_method(OXP_UMA_WMI_GUID, 0, method_id, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
obj = output.pointer;
if (!obj) {
ret = -ENODATA;
goto out;
}
switch (obj->type) {
case ACPI_TYPE_INTEGER:
*out_value = obj->integer.value;
break;
case ACPI_TYPE_BUFFER:
if (obj->buffer.length < 1) {
ret = -ENODATA;
break;
}
*out_value = obj->buffer.pointer[0];
break;
default:
ret = -EPROTO;
break;
}
out:
kfree(output.pointer);
return ret;
}
static int __init oxp_wmi_write_once_init(void)
{
u64 before = 0;
u64 after = 0;
int ret;
if (!dmi_first_match(dmi_table)) {
pr_err("oxp-wmi-write-once: refusing: not ONEXPLAYER SUPER X\n");
return -ENODEV;
}
if (!yes_i_understand_this_writes_to_hardware) {
pr_err("oxp-wmi-write-once: refusing: set yes_i_understand_this_writes_to_hardware=1\n");
return -EPERM;
}
if (offset > 0xff || value > 0xff) {
pr_err("oxp-wmi-write-once: refusing: offset/value must be bytes\n");
return -EINVAL;
}
if (wmi_instance_count(OXP_UMA_WMI_GUID) <= 0) {
pr_err("oxp-wmi-write-once: refusing: OneX WMI GUID not present\n");
return -ENODEV;
}
ret = oxp_wmi_call(OXP_WMI_METHOD_GET_EC_VALUE, (u8)offset, 0, &before);
if (ret) {
pr_err("oxp-wmi-write-once: pre-read [0x%02x] failed ret=%d\n", offset, ret);
return ret;
}
pr_warn("oxp-wmi-write-once: WMI write [0x%02x] 0x%02llx -> 0x%02x\n",
offset, before & 0xff, value);
ret = oxp_wmi_call(OXP_WMI_METHOD_SET_EC_VALUE, (u8)offset, (u8)value, &after);
if (ret) {
pr_err("oxp-wmi-write-once: write [0x%02x]=0x%02x failed ret=%d\n",
offset, value, ret);
return ret;
}
pr_warn("oxp-wmi-write-once: returned/readback [0x%02x]=0x%02llx\n",
offset, after & 0xff);
return 0;
}
static void __exit oxp_wmi_write_once_exit(void)
{
pr_info("oxp-wmi-write-once: unloaded\n");
}
module_init(oxp_wmi_write_once_init);
module_exit(oxp_wmi_write_once_exit);
MODULE_AUTHOR("Hermes");
MODULE_DESCRIPTION("Guarded one-shot OneXPlayer Super X WMI EC-memory byte writer");
MODULE_LICENSE("GPL");