diff --git a/.gitignore b/.gitignore index 671c606..8b65aae 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ Module.symvers modules.order .*.cmd .tmp/ +/contrib/upstream-linux/test-module/ +/contrib/upstream-linux/outgoing-*/ # Python cache __pycache__/ diff --git a/README.md b/README.md index 67c2915..848747e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/contrib/power/10-wifi-powersave.conf b/contrib/power/10-wifi-powersave.conf new file mode 100644 index 0000000..de71dcf --- /dev/null +++ b/contrib/power/10-wifi-powersave.conf @@ -0,0 +1,2 @@ +[connection] +wifi.powersave=3 diff --git a/contrib/power/80-oxp-disable-fingerprint.rules b/contrib/power/80-oxp-disable-fingerprint.rules new file mode 100644 index 0000000..0bb7784 --- /dev/null +++ b/contrib/power/80-oxp-disable-fingerprint.rules @@ -0,0 +1 @@ +ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="2808", ATTR{idProduct}=="5952", RUN+="/usr/local/sbin/oxp-disable-fingerprint %p" diff --git a/contrib/power/README.md b/contrib/power/README.md new file mode 100644 index 0000000..d634313 --- /dev/null +++ b/contrib/power/README.md @@ -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. diff --git a/contrib/power/oxp-cpu-profile b/contrib/power/oxp-cpu-profile new file mode 100755 index 0000000..9830472 --- /dev/null +++ b/contrib/power/oxp-cpu-profile @@ -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 diff --git a/contrib/power/oxp-disable-fingerprint b/contrib/power/oxp-disable-fingerprint new file mode 100755 index 0000000..8c13a3e --- /dev/null +++ b/contrib/power/oxp-disable-fingerprint @@ -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" diff --git a/contrib/power/oxp-idle-power-resume b/contrib/power/oxp-idle-power-resume new file mode 100755 index 0000000..859796b --- /dev/null +++ b/contrib/power/oxp-idle-power-resume @@ -0,0 +1,7 @@ +#!/bin/sh + +case "$1" in + post) + systemctl restart oxp-idle-power.service + ;; +esac diff --git a/contrib/power/oxp-idle-power.service b/contrib/power/oxp-idle-power.service new file mode 100644 index 0000000..b8a3d2d --- /dev/null +++ b/contrib/power/oxp-idle-power.service @@ -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 diff --git a/contrib/power/oxp-idle-power.sh b/contrib/power/oxp-idle-power.sh new file mode 100755 index 0000000..e14b17d --- /dev/null +++ b/contrib/power/oxp-idle-power.sh @@ -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 diff --git a/contrib/power/oxp-turbo-tdp.py b/contrib/power/oxp-turbo-tdp.py new file mode 100755 index 0000000..9d62fcd --- /dev/null +++ b/contrib/power/oxp-turbo-tdp.py @@ -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() diff --git a/contrib/power/oxp-turbo-tdp.service b/contrib/power/oxp-turbo-tdp.service new file mode 100644 index 0000000..84e4a07 --- /dev/null +++ b/contrib/power/oxp-turbo-tdp.service @@ -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 diff --git a/contrib/sensors/61-sensor-onexplayer-super-x.hwdb b/contrib/sensors/61-sensor-onexplayer-super-x.hwdb new file mode 100644 index 0000000..66dee38 --- /dev/null +++ b/contrib/sensors/61-sensor-onexplayer-super-x.hwdb @@ -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 diff --git a/gnome-extension/extension.js b/gnome-extension/extension.js index 24c6dfa..d197e79 100644 --- a/gnome-extension/extension.js +++ b/gnome-extension/extension.js @@ -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); diff --git a/install.sh b/install.sh index 5cba7f6..852b55c 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/oxp-sensors.c b/oxp-sensors.c index 1e814af..26f48b8 100644 --- a/oxp-sensors.c +++ b/oxp-sensors.c @@ -8,11 +8,22 @@ #include #include #include +#include #include #include #include #include #include +#include + +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) diff --git a/uninstall.sh b/uninstall.sh index 720edc4..d78011c 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -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