Add Super X tablet and power integration

This commit is contained in:
ps1x 2026-07-28 16:24:31 +03:00
parent e75643f2c4
commit f5eae5c97e
17 changed files with 657 additions and 3 deletions

2
.gitignore vendored
View file

@ -8,6 +8,8 @@ Module.symvers
modules.order
.*.cmd
.tmp/
/contrib/upstream-linux/test-module/
/contrib/upstream-linux/outgoing-*/
# Python cache
__pycache__/

View file

@ -7,6 +7,8 @@ Linux control stack for **OneXPlayer Super X** with:
- GNOME Shell extension
- RGB control
- TDP control
- automatic tablet-mode reporting and display rotation calibration
- selectable CPU power policies
- battery charge-limit backend
If you are searching for **OneXPlayer Super X Linux fan control**, **OneXPlayer
@ -43,6 +45,10 @@ under `local/` and are excluded from Git.
- GNOME Shell top-bar plugin for daily use
- RGB preset and color control helpers
- TDP presets through `ryzenadj`
- Turbo-button TDP synchronization
- Adaptive and Ultra Saver CPU power modes
- detachable-keyboard tablet-mode reporting
- accelerometer mounting calibration for automatic rotation
- battery status and charge-limit backend used by the plugin
## Compatibility
@ -66,6 +72,8 @@ This project is based on the original `oxp-sensors` work:
- `gnome-extension/`: GNOME Shell top-bar plugin
- `oxp-rgb`, `oxp-rgb-hid.py`: RGB control helpers
- `oxp-tdp`: TDP helper based on `ryzenadj`
- `contrib/power/`: CPU policy, Turbo-button TDP, and idle-power helpers
- `contrib/sensors/`: Super X accelerometer hardware database entry
- `oxp-battery-probe.py`, `oxp-battery-ec-probe.c`: battery status / charge-limit backend for the plugin
## Build DKMS Module
@ -90,6 +98,9 @@ Install the client tools, fan-control service, and GNOME extension:
./install.sh
```
The TDP helpers require `ryzenadj` to be installed and available in `PATH`.
The CPU power menu requires `power-profiles-daemon` and `powerprofilesctl`.
## Uninstall
Remove the installed userland components with:
@ -127,6 +138,7 @@ sudo /usr/local/bin/oxp-fan-profile set balanced
The GNOME Shell extension adds a top-bar menu for:
- fan profile switching
- CPU power modes
- RGB presets and custom color
- TDP presets
- battery charge-limit / bypass controls when the backend is available

View file

@ -0,0 +1,2 @@
[connection]
wifi.powersave=3

View file

@ -0,0 +1 @@
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="2808", ATTR{idProduct}=="5952", RUN+="/usr/local/sbin/oxp-disable-fingerprint %p"

33
contrib/power/README.md Normal file
View file

@ -0,0 +1,33 @@
# OneXPlayer idle-power tuning
This host policy combines aggressive runtime idle power management with full
AMD CPU boost availability. The three locally identified USB input devices are
left alone so the built-in controller, keyboard, and receiver wake instantly.
The system service applies PCI runtime PM, a 625 MHz CPU floor, the full 5.19
GHz boost ceiling, `balance_power` EPP, and less frequent dirty-page wakeups.
The resume hook reapplies those settings after suspend.
Power Profiles reports `performance` because that firmware profile is required
to expose the full boost ceiling. The service then replaces its CPU floor and
EPP values, so the effective policy is the custom low-idle configuration above.
The NetworkManager drop-in enables Wi-Fi power saving by default. Installation
reloads NetworkManager configuration; reconnect Wi-Fi to apply it to an
already-active connection.
`oxp-cpu-profile` exposes the effective custom mode as `adaptive` and is used
by the OXP Control GNOME extension's CPU Power menu. Selecting Adaptive asks
for administrator authorization and reapplies `oxp-idle-power.service`.
Ultra Saver keeps the 625 MHz floor, caps the CPU at its 2.0 GHz lowest
non-linear (most efficient) frequency, and selects the `power` EPP hint. The
other modes restore the CPU's full hardware frequency range before applying
their profile, so leaving Ultra Saver cannot retain its 2.0 GHz ceiling.
The selected OXP mode is stored in `/var/lib/oxp-power-mode` and restored at boot
and after resume.
The internal unsupported FocalTech `2808:5952` fingerprint reader is disabled
at its dedicated USB port. A device-specific udev rule reapplies the setting
after USB controller re-enumeration; the power service also reapplies it after
resume.

48
contrib/power/oxp-cpu-profile Executable file
View file

@ -0,0 +1,48 @@
#!/bin/sh
set -eu
usage() {
echo "Usage: $0 status | set {power-saver|balanced|performance|adaptive|ultra}" >&2
exit 2
}
effective_profile() {
policy=/sys/devices/system/cpu/cpufreq/policy0
min=$(cat "$policy/scaling_min_freq")
max=$(cat "$policy/scaling_max_freq")
epp=$(cat "$policy/energy_performance_preference")
expected_min=$(cat "$policy/cpuinfo_min_freq")
ultra_max=$(cat "$policy/amd_pstate_lowest_nonlinear_freq")
if [ -r "$policy/amd_pstate_max_freq" ]; then
expected_max=$(cat "$policy/amd_pstate_max_freq")
else
expected_max=$(cat "$policy/cpuinfo_max_freq")
fi
if [ "$min" = "$expected_min" ] && [ "$max" = "$expected_max" ] && [ "$epp" = balance_power ]; then
echo adaptive
elif [ "$min" = "$expected_min" ] && [ "$max" = "$ultra_max" ] && [ "$epp" = power ]; then
echo ultra
else
powerprofilesctl get
fi
}
case "${1:-}" in
status)
effective_profile
;;
set)
case "${2:-}" in
power-saver|balanced|performance|adaptive|ultra)
/usr/local/sbin/oxp-idle-power "$2"
;;
*)
usage
;;
esac
;;
*)
usage
;;
esac

View file

@ -0,0 +1,21 @@
#!/bin/sh
# Disable the internal FocalTech fingerprint reader at its USB port.
set -eu
[ "$#" -eq 1 ] || exit 2
device="/sys$1"
[ -r "$device/idVendor" ] || exit 0
[ "$(cat "$device/idVendor")" = 2808 ] || exit 0
[ "$(cat "$device/idProduct")" = 5952 ] || exit 0
usb_node=$(basename "$device")
bus=${usb_node%%-*}
port=${usb_node#*-}
case "$port" in
''|*[!0-9]*) exit 1 ;;
esac
usb_root=$(dirname "$device")
disable="$usb_root/$bus-0:1.0/usb$bus-port$port/disable"
[ -w "$disable" ] || exit 1
printf '%s\n' 1 > "$disable"

View file

@ -0,0 +1,7 @@
#!/bin/sh
case "$1" in
post)
systemctl restart oxp-idle-power.service
;;
esac

View file

@ -0,0 +1,11 @@
[Unit]
Description=OneXPlayer low-idle-power tuning with full CPU boost
After=power-profiles-daemon.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/oxp-idle-power
RemainAfterExit=yes
[Install]
WantedBy=graphical.target

71
contrib/power/oxp-idle-power.sh Executable file
View file

@ -0,0 +1,71 @@
#!/bin/sh
# Apply the selected OXP CPU policy and common idle-power savings.
set -eu
state_file=/var/lib/oxp-power-mode
mode=${1:-}
if [ -n "$mode" ]; then
case "$mode" in
power-saver|balanced|performance|adaptive|ultra) ;;
*) echo "Unknown OXP power mode: $mode" >&2; exit 2 ;;
esac
printf '%s\n' "$mode" > "$state_file"
elif [ -r "$state_file" ]; then
mode=$(cat "$state_file")
else
mode=adaptive
fi
# Performance is the only firmware profile that exposes the full CPPC boost
# range needed by Adaptive. Ultra starts from the standard saver profile.
case "$mode" in
adaptive) powerprofilesctl set performance ;;
ultra) powerprofilesctl set power-saver ;;
*) powerprofilesctl set "$mode" ;;
esac
# The unsupported internal FocalTech fingerprint reader otherwise remains on
# the USB bus. Disable its dedicated port; udev applies the same rule at boot.
for fingerprint in /sys/bus/usb/devices/*; do
[ -r "$fingerprint/idVendor" ] || continue
[ "$(cat "$fingerprint/idVendor")" = 2808 ] || continue
[ "$(cat "$fingerprint/idProduct")" = 5952 ] || continue
/usr/local/sbin/oxp-disable-fingerprint "${fingerprint#/sys}"
done
# Let every PCI driver runtime-suspend its device when the driver considers it
# idle. Active devices remain awake and resume automatically on demand.
for control in /sys/bus/pci/devices/*/power/control; do
[ -w "$control" ] && printf '%s\n' auto > "$control"
done
for policy in /sys/devices/system/cpu/cpufreq/policy*; do
[ -d "$policy" ] || continue
# power-profiles-daemon does not reset cpufreq limits previously written by
# Ultra Saver. Restore the hardware range on every profile transition before
# applying any custom ceiling, otherwise Performance can remain stuck at
# 2 GHz (or lower) indefinitely.
if [ -r "$policy/amd_pstate_max_freq" ]; then
cat "$policy/amd_pstate_max_freq" > "$policy/scaling_max_freq"
else
cat "$policy/cpuinfo_max_freq" > "$policy/scaling_max_freq"
fi
cat "$policy/cpuinfo_min_freq" > "$policy/scaling_min_freq"
if [ "$mode" = adaptive ] || [ "$mode" = ultra ]; then
# amd-pstate's powersave governor is dynamic, not a fixed low frequency.
printf '%s\n' powersave > "$policy/scaling_governor"
if [ "$mode" = adaptive ]; then
printf '%s\n' balance_power > "$policy/energy_performance_preference"
else
# This is the efficient ceiling; lower clocks can increase total
# energy by making work take disproportionately longer.
cat "$policy/amd_pstate_lowest_nonlinear_freq" > "$policy/scaling_max_freq"
printf '%s\n' power > "$policy/energy_performance_preference"
fi
fi
done
# Batch periodic dirty-page writeback to reduce otherwise needless wakeups.
sysctl -q -w vm.dirty_writeback_centisecs=1500

136
contrib/power/oxp-turbo-tdp.py Executable file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Set a TDP preset when the OneXPlayer Super X Turbo key is pressed."""
from __future__ import annotations
import argparse
import os
import select
import signal
import shutil
import struct
import subprocess
import time
from pathlib import Path
INPUT_NAME = "OneXPlayer Super X Turbo Button"
FIRMWARE_TDP = Path("/sys/devices/platform/oxp-platform/firmware_tdp")
EV_KEY = 0x01
KEY_PROG1 = 148
INPUT_EVENT = struct.Struct("llHHI")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--watts", type=int, default=80)
parser.add_argument("--normal-stapm", type=int, default=10)
parser.add_argument("--normal-fast", type=int, default=15)
parser.add_argument("--normal-slow", type=int, default=10)
return parser.parse_args()
def wait_for(path: Path, timeout: float = 15.0) -> None:
deadline = time.monotonic() + timeout
while not path.exists():
if time.monotonic() >= deadline:
raise TimeoutError(f"timed out waiting for {path}")
time.sleep(0.25)
def find_input_device() -> Path:
for event in Path("/sys/class/input").glob("event*"):
try:
name = (event / "device/name").read_text(encoding="utf-8").strip()
except OSError:
continue
if name == INPUT_NAME:
return Path("/dev/input") / event.name
raise FileNotFoundError(f"input device {INPUT_NAME!r} was not found")
def read_firmware_tdp() -> tuple[int, int, int]:
values = tuple(
int(value) for value in FIRMWARE_TDP.read_text(encoding="ascii").split()
)
if len(values) != 3:
raise RuntimeError(f"invalid firmware TDP tuple: {values}")
return values
def apply_limits(stapm: int, fast: int, slow: int) -> None:
ryzenadj = shutil.which("ryzenadj")
if not ryzenadj:
raise FileNotFoundError("ryzenadj was not found in PATH")
subprocess.run(
[
ryzenadj,
"--stapm-limit", str(stapm * 1000),
"--fast-limit", str(fast * 1000),
"--slow-limit", str(slow * 1000),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def main() -> None:
args = parse_args()
limits = (args.watts, args.normal_stapm, args.normal_fast, args.normal_slow)
if any(not 3 <= watts <= 80 for watts in limits):
raise ValueError("TDP limits must be between 3 and 80 W")
running = True
def stop(_signum: int, _frame: object) -> None:
nonlocal running
running = False
signal.signal(signal.SIGINT, stop)
signal.signal(signal.SIGTERM, stop)
wait_for(FIRMWARE_TDP)
input_device = find_input_device()
def sync_from_firmware() -> None:
firmware = read_firmware_tdp()
turbo_enabled = firmware[0] == firmware[1] == firmware[2] and firmware[0] >= 45
if turbo_enabled:
applied = (args.watts, args.watts, args.watts)
mode = "turbo"
else:
applied = (args.normal_stapm, args.normal_fast, args.normal_slow)
mode = "normal"
apply_limits(*applied)
print(
f"Firmware {firmware[0]}/{firmware[1]}/{firmware[2]} W: "
f"applied {applied[0]}/{applied[1]}/{applied[2]} W ({mode})",
flush=True,
)
sync_from_firmware()
fd = os.open(input_device, os.O_RDONLY | os.O_NONBLOCK)
poller = select.poll()
poller.register(fd, select.POLLIN)
try:
while running:
for _fd, event_mask in poller.poll(1000):
if event_mask & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
raise OSError(f"input device poll failed: mask={event_mask:#x}")
data = os.read(fd, INPUT_EVENT.size * 64)
for offset in range(0, len(data), INPUT_EVENT.size):
_sec, _usec, event_type, code, value = \
INPUT_EVENT.unpack_from(data, offset)
if event_type == EV_KEY and code == KEY_PROG1 and value == 1:
sync_from_firmware()
finally:
os.close(fd)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,15 @@
[Unit]
Description=OneXPlayer Super X Turbo button TDP preset
After=systemd-modules-load.service
StartLimitIntervalSec=60
StartLimitBurst=3
[Service]
Type=simple
ExecStartPre=/usr/sbin/modprobe oxp-sensors
ExecStart=/usr/local/bin/oxp-turbo-tdp --watts 80
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,5 @@
# One-Netbook OneXPlayer Super X
# The BMI260 ACPI node uses the BMI0160 ID and is mounted with its X and Y
# axes exchanged relative to the built-in display.
sensor:modalias:acpi:BMI0160:*:dmi:*:svnONE-NETBOOK:pnONEXPLAYERSUPERX:*
ACCEL_MOUNT_MATRIX=0, -1, 0; 1, 0, 0; 0, 0, 1

View file

@ -10,6 +10,7 @@ import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
const PROFILE_HELPER = '/usr/local/bin/oxp-fan-profile';
const CPU_PROFILE_HELPER = '/usr/local/bin/oxp-cpu-profile';
const RGB_HELPER = '/usr/local/bin/oxp-rgb';
const RGB_HID_HELPER = '/usr/local/bin/oxp-rgb-hid';
const TDP_HELPER = '/usr/local/bin/oxp-tdp';
@ -196,6 +197,9 @@ class OXPFanProfilesButton extends PanelMenu.Button {
this._profilesMenu = new PopupMenu.PopupSubMenuMenuItem('Fan Profiles');
this.menu.addMenuItem(this._profilesMenu);
this._cpuProfilesMenu = new PopupMenu.PopupSubMenuMenuItem('CPU Power');
this.menu.addMenuItem(this._cpuProfilesMenu);
this._tdpMenu = new PopupMenu.PopupSubMenuMenuItem('TDP');
this.menu.addMenuItem(this._tdpMenu);
@ -570,6 +574,38 @@ class OXPFanProfilesButton extends PanelMenu.Button {
});
}
_getCpuPowerMode() {
const result = this._runChecked([CPU_PROFILE_HELPER, 'status']);
return result.ok ? result.stdout : null;
}
_setCpuPowerMode(mode, label) {
const command = ['pkexec', CPU_PROFILE_HELPER, 'set', mode];
this._spawn(command, `CPU power: ${label}`);
this._runLater(1200, () => this._refresh());
}
_buildCpuProfilesMenu() {
const current = this._getCpuPowerMode();
const profiles = [
['power-saver', 'Power Saver'],
['balanced', 'Balanced'],
['performance', 'Performance'],
['adaptive', 'Adaptive'],
['ultra', 'Ultra Saver'],
];
this._cpuProfilesMenu.menu.removeAll();
for (const [mode, label] of profiles) {
const item = new PopupMenu.PopupMenuItem(label);
if (mode === current) {
item.setOrnament(PopupMenu.Ornament.DOT);
}
item.connect('activate', () => this._setCpuPowerMode(mode, label));
this._cpuProfilesMenu.menu.addMenuItem(item);
}
}
_setTdp(watts) {
const power = this._readPowerSourceInfo();
if (power.onBattery && watts > MAX_BATTERY_TDP_WATTS) {
@ -897,6 +933,7 @@ class OXPFanProfilesButton extends PanelMenu.Button {
const status = this._runStatus();
this._enforceBatteryTdpSafety();
this._profilesMenu.menu.removeAll();
this._buildCpuProfilesMenu();
this._buildTdpMenu();
this._refreshBatteryInfo(false);

View file

@ -13,14 +13,26 @@ sudo install -Dm755 "$REPO_DIR/oxp-fan-profile.py" /usr/local/bin/oxp-fan-profil
sudo install -Dm755 "$REPO_DIR/oxp-rgb-hid.py" /usr/local/bin/oxp-rgb-hid
sudo install -Dm755 "$REPO_DIR/oxp-rgb" /usr/local/bin/oxp-rgb
sudo install -Dm755 "$REPO_DIR/oxp-tdp" /usr/local/bin/oxp-tdp
sudo install -Dm755 "$REPO_DIR/contrib/power/oxp-turbo-tdp.py" /usr/local/bin/oxp-turbo-tdp
sudo install -Dm755 "$REPO_DIR/contrib/power/oxp-cpu-profile" /usr/local/bin/oxp-cpu-profile
sudo install -Dm755 "$REPO_DIR/contrib/power/oxp-idle-power.sh" /usr/local/sbin/oxp-idle-power
sudo install -Dm755 "$REPO_DIR/contrib/power/oxp-disable-fingerprint" /usr/local/sbin/oxp-disable-fingerprint
sudo install -Dm755 "$REPO_DIR/oxp-battery-probe.py" /usr/local/bin/oxp-battery-probe
gcc -O2 -Wall -Wextra -o /tmp/oxp-battery-ec-probe "$REPO_DIR/oxp-battery-ec-probe.c"
sudo install -Dm755 /tmp/oxp-battery-ec-probe /usr/local/bin/oxp-battery-ec-probe
sudo chmod 4755 /usr/local/bin/oxp-battery-ec-probe
sudo install -Dm644 "$REPO_DIR/oxp-fan-control.service" /etc/systemd/system/oxp-fan-control.service
sudo install -Dm644 "$REPO_DIR/contrib/power/oxp-turbo-tdp.service" /etc/systemd/system/oxp-turbo-tdp.service
sudo install -Dm644 "$REPO_DIR/contrib/power/oxp-idle-power.service" /etc/systemd/system/oxp-idle-power.service
sudo install -Dm755 "$REPO_DIR/contrib/power/oxp-idle-power-resume" /usr/lib/systemd/system-sleep/oxp-idle-power
sudo install -Dm644 "$REPO_DIR/oxp-sensors.conf" /etc/modules-load.d/oxp-sensors.conf
sudo install -Dm644 "$REPO_DIR/oxp-fan-profile.rules" /etc/polkit-1/rules.d/49-oxp-fan-profile.rules
sudo install -Dm644 "$REPO_DIR/oxp-rgb-hid.rules" /etc/udev/rules.d/99-oxp-rgb-hid.rules
sudo install -Dm644 "$REPO_DIR/contrib/power/80-oxp-disable-fingerprint.rules" /etc/udev/rules.d/80-oxp-disable-fingerprint.rules
sudo install -Dm644 "$REPO_DIR/contrib/power/10-wifi-powersave.conf" /etc/NetworkManager/conf.d/10-wifi-powersave.conf
sudo install -Dm644 \
"$REPO_DIR/contrib/sensors/61-sensor-onexplayer-super-x.hwdb" \
/etc/udev/hwdb.d/61-sensor-onexplayer-super-x.hwdb
if ! sudo test -f /etc/oxp-fan-control.conf; then
echo "Installing fresh profile config..."
@ -40,10 +52,18 @@ sudo install -m644 -o "$TARGET_USER" -g "$TARGET_GROUP" \
"$EXTENSION_DIR/extension.js"
sudo systemctl daemon-reload
sudo systemd-hwdb update
if command -v nmcli >/dev/null 2>&1; then
sudo nmcli general reload conf || true
fi
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=iio --action=change || true
sudo udevadm trigger --attr-match=idVendor=1a2c --attr-match=idProduct=b001 || true
sudo modprobe oxp-sensors || true
sudo systemctl try-restart iio-sensor-proxy.service
sudo systemctl enable --now oxp-fan-control.service
sudo systemctl enable --now oxp-turbo-tdp.service
sudo systemctl enable --now oxp-idle-power.service
if command -v gnome-extensions >/dev/null 2>&1; then
sudo -u "$TARGET_USER" gnome-extensions enable "$EXTENSION_UUID" 2>/dev/null || true

View file

@ -8,11 +8,22 @@
#include <linux/dmi.h>
#include <linux/hwmon.h>
#include <linux/init.h>
#include <linux/input.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/processor.h>
#include <linux/usb.h>
struct acpi_ec;
typedef int (*acpi_ec_query_func)(void *data);
extern struct acpi_ec *first_ec;
extern int acpi_ec_add_query_handler(struct acpi_ec *ec, u8 query_bit,
acpi_handle handle,
acpi_ec_query_func func, void *data);
extern void acpi_ec_remove_query_handler(struct acpi_ec *ec, u8 query_bit);
/* Handle ACPI lock mechanism */
static u32 oxp_mutex;
@ -46,6 +57,12 @@ static bool unlock_global_acpi_lock(void)
#define OXP_TURBO_TAKE_VAL 0x40
#define OXP_TURBO_RETURN_VAL 0x00
#define OXP_TURBO_QUERY 0x49
#define OXP_TDP_STAPM_REG 0x31
#define OXP_TDP_FAST_REG 0x32
#define OXP_TDP_SLOW_REG 0x33
#define OXP_TURBO_QUERY_METHOD "\\_SB.PCI0.SBRG.EC0._Q49"
#define OXP_LED_ENABLE_VAL 0x01
#define OXP_LED_DISABLE_VAL 0x00
@ -75,6 +92,156 @@ MODULE_PARM_DESC(led_blue_reg, "EC register for LED blue channel");
static bool led_regs_valid;
#define OXP_SUPER_X_KEYBOARD_VID 0x1a86
#define OXP_SUPER_X_KEYBOARD_PID 0x1305
static struct input_dev *oxp_tablet_mode_input;
static acpi_handle oxp_turbo_query_method;
static bool oxp_is_detachable_keyboard(const struct usb_device *udev)
{
return le16_to_cpu(udev->descriptor.idVendor) == OXP_SUPER_X_KEYBOARD_VID &&
le16_to_cpu(udev->descriptor.idProduct) == OXP_SUPER_X_KEYBOARD_PID;
}
static void oxp_report_keyboard_attached(bool attached)
{
if (!oxp_tablet_mode_input)
return;
/* SW_TABLET_MODE is set when the detachable keyboard is absent. */
input_report_switch(oxp_tablet_mode_input, SW_TABLET_MODE, !attached);
input_sync(oxp_tablet_mode_input);
}
static int oxp_find_detachable_keyboard(struct usb_device *udev, void *data)
{
bool *attached = data;
if (oxp_is_detachable_keyboard(udev))
*attached = true;
return 0;
}
static int oxp_usb_notify(struct notifier_block *nb,
unsigned long action, void *data)
{
struct usb_device *udev = data;
if (!oxp_is_detachable_keyboard(udev))
return NOTIFY_DONE;
switch (action) {
case USB_DEVICE_ADD:
oxp_report_keyboard_attached(true);
break;
case USB_DEVICE_REMOVE:
oxp_report_keyboard_attached(false);
break;
default:
return NOTIFY_DONE;
}
return NOTIFY_OK;
}
static struct notifier_block oxp_usb_notifier = {
.notifier_call = oxp_usb_notify,
};
static void oxp_unregister_usb_notifier(void *unused)
{
usb_unregister_notify(&oxp_usb_notifier);
oxp_tablet_mode_input = NULL;
}
static int oxp_register_tablet_mode_switch(struct device *dev)
{
struct input_dev *input;
bool keyboard_attached = false;
int ret;
input = devm_input_allocate_device(dev);
if (!input)
return -ENOMEM;
input->name = "OneXPlayer Super X Tablet Mode Switch";
input->phys = "oxp-platform/input0";
input->id.bustype = BUS_HOST;
input_set_capability(input, EV_SW, SW_TABLET_MODE);
ret = input_register_device(input);
if (ret)
return ret;
oxp_tablet_mode_input = input;
usb_register_notify(&oxp_usb_notifier);
ret = devm_add_action_or_reset(dev, oxp_unregister_usb_notifier, NULL);
if (ret)
return ret;
usb_for_each_dev(&keyboard_attached, oxp_find_detachable_keyboard);
oxp_report_keyboard_attached(keyboard_attached);
return 0;
}
static int oxp_turbo_query_handler(void *data)
{
struct input_dev *input = data;
/* Preserve the firmware _Q49 power-limit notification. */
acpi_evaluate_object(oxp_turbo_query_method, NULL, NULL, NULL);
input_report_key(input, KEY_PROG1, 1);
input_sync(input);
input_report_key(input, KEY_PROG1, 0);
input_sync(input);
return 0;
}
static void oxp_unregister_turbo_query(void *unused)
{
acpi_ec_remove_query_handler(first_ec, OXP_TURBO_QUERY);
}
static int oxp_register_turbo_query(struct device *dev)
{
struct input_dev *input;
acpi_status status;
int ret;
if (!first_ec)
return -ENODEV;
status = acpi_get_handle(NULL, OXP_TURBO_QUERY_METHOD,
&oxp_turbo_query_method);
if (ACPI_FAILURE(status))
return -ENODEV;
input = devm_input_allocate_device(dev);
if (!input)
return -ENOMEM;
input->name = "OneXPlayer Super X Turbo Button";
input->phys = "oxp-platform/input1";
input->id.bustype = BUS_HOST;
input_set_capability(input, EV_KEY, KEY_PROG1);
ret = input_register_device(input);
if (ret)
return ret;
ret = acpi_ec_add_query_handler(first_ec, OXP_TURBO_QUERY, NULL,
oxp_turbo_query_handler, input);
if (ret)
return ret;
return devm_add_action_or_reset(dev, oxp_unregister_turbo_query, NULL);
}
static const struct dmi_system_id dmi_table[] = {
{
.matches = {
@ -184,6 +351,37 @@ static ssize_t tt_toggle_show(struct device *dev,
static DEVICE_ATTR_RW(tt_toggle);
static ssize_t firmware_tdp_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
long stapm, fast, slow;
int ret;
ret = read_from_ec(OXP_TDP_STAPM_REG, 1, &stapm);
if (ret)
return ret;
ret = read_from_ec(OXP_TDP_FAST_REG, 1, &fast);
if (ret)
return ret;
ret = read_from_ec(OXP_TDP_SLOW_REG, 1, &slow);
if (ret)
return ret;
return sysfs_emit(buf, "%ld %ld %ld\n", stapm, fast, slow);
}
static DEVICE_ATTR_RO(firmware_tdp);
static struct attribute *oxp_tt_attrs[] = {
&dev_attr_tt_toggle.attr,
&dev_attr_firmware_tdp.attr,
NULL
};
static const struct attribute_group oxp_tt_group = {
.attrs = oxp_tt_attrs,
};
/* LED control functions */
static int led_enable(void)
{
@ -473,7 +671,6 @@ static const struct hwmon_channel_info * const oxp_platform_sensors[] = {
};
static struct attribute *oxp_ec_attrs[] = {
&dev_attr_tt_toggle.attr,
&dev_attr_led_enable.attr,
&dev_attr_led_mode.attr,
&dev_attr_led_brightness.attr,
@ -481,7 +678,9 @@ static struct attribute *oxp_ec_attrs[] = {
NULL
};
ATTRIBUTE_GROUPS(oxp_ec);
static const struct attribute_group oxp_ec_group = {
.attrs = oxp_ec_attrs,
};
static const struct hwmon_ops oxp_ec_hwmon_ops = {
.is_visible = oxp_ec_hwmon_is_visible,
@ -506,6 +705,18 @@ static int oxp_platform_probe(struct platform_device *pdev)
led_regs_valid = led_map_complete();
ret = oxp_register_tablet_mode_switch(dev);
if (ret)
return ret;
ret = oxp_register_turbo_query(dev);
if (ret)
return ret;
ret = devm_device_add_group(dev, &oxp_tt_group);
if (ret)
return ret;
if (led_regs_valid) {
ret = devm_device_add_group(dev, &oxp_ec_group);
if (ret)

View file

@ -17,6 +17,11 @@ fi
echo "Stopping OXP fan service..."
sudo systemctl disable --now oxp-fan-control.service 2>/dev/null || true
sudo systemctl disable --now oxp-turbo-tdp.service 2>/dev/null || true
sudo systemctl disable --now oxp-idle-power.service 2>/dev/null || true
if [[ -x /usr/local/bin/oxp-cpu-profile ]]; then
sudo /usr/local/bin/oxp-cpu-profile set balanced 2>/dev/null || true
fi
echo "Removing installed binaries..."
for path in \
@ -25,6 +30,10 @@ for path in \
/usr/local/bin/oxp-rgb-hid \
/usr/local/bin/oxp-rgb \
/usr/local/bin/oxp-tdp \
/usr/local/bin/oxp-turbo-tdp \
/usr/local/bin/oxp-cpu-profile \
/usr/local/sbin/oxp-idle-power \
/usr/local/sbin/oxp-disable-fingerprint \
/usr/local/bin/oxp-battery-probe \
/usr/local/bin/oxp-battery-ec-probe
do
@ -34,9 +43,15 @@ done
echo "Removing service and rules..."
for path in \
/etc/systemd/system/oxp-fan-control.service \
/etc/systemd/system/oxp-turbo-tdp.service \
/etc/systemd/system/oxp-idle-power.service \
/usr/lib/systemd/system-sleep/oxp-idle-power \
/etc/modules-load.d/oxp-sensors.conf \
/etc/polkit-1/rules.d/49-oxp-fan-profile.rules \
/etc/udev/rules.d/99-oxp-rgb-hid.rules
/etc/udev/rules.d/99-oxp-rgb-hid.rules \
/etc/udev/rules.d/80-oxp-disable-fingerprint.rules \
/etc/NetworkManager/conf.d/10-wifi-powersave.conf \
/etc/udev/hwdb.d/61-sensor-onexplayer-super-x.hwdb
do
sudo rm -f "$path"
done
@ -50,11 +65,18 @@ if [[ $PURGE -eq 1 ]]; then
echo "Purging local configuration..."
sudo rm -f /etc/oxp-fan-control.conf
sudo rm -f "$TARGET_HOME/.config/oxp-control.json"
sudo rm -f /var/lib/oxp-power-mode
fi
sudo systemctl daemon-reload
sudo systemd-hwdb update
if command -v nmcli >/dev/null 2>&1; then
sudo nmcli general reload conf || true
fi
sudo udevadm control --reload-rules || true
sudo udevadm trigger --subsystem-match=iio --action=change || true
sudo udevadm trigger --attr-match=idVendor=1a2c --attr-match=idProduct=b001 || true
sudo systemctl try-restart iio-sensor-proxy.service
sudo modprobe -r oxp-sensors 2>/dev/null || true
if command -v gnome-extensions >/dev/null 2>&1; then