Maintenance, installation, engineering and repair.
Post Reply
User avatar
!
35%
Posts: 3545
Joined: 2013-02-25 18:36

2026-04-28 10:17 »

I thought it's worth caching. Seems kind'a cool. Looks like they used A.I. to do it. :thumbup:

https://github.com/Hugo2049/alienware-16x-fan-control
A fan speed controller for the Alienware 16X Aurora (AC16251) on Linux, achieved through reverse engineering of the ACPI/WMI interface.
https://github.com/Hugo2049/alienware-16x-fan-control wrote:Skip to content
Navigation Menu
Sign in
Hugo2049
/
alienware-16x-fan-control
Public
Code
Issues
Pull requests
Actions
Projects
Security and quality
Hugo2049/alienware-16x-fan-control
Name
Hugo2049
Hugo2049
Update README.md
d56b8ec
·
8 hours ago
README.md
Update README.md
8 hours ago
fan_control.py
Initial release: Alienware 16X Aurora fan control via ACPI RE
14 hours ago
fan_helper.sh
Initial release: Alienware 16X Aurora fan control via ACPI RE
14 hours ago
Repository files navigation
README
Alienware 16X Aurora Fan Control for Linux
A fan speed controller for the Alienware 16X Aurora (AC16251) on Linux, achieved through reverse engineering of the ACPI/WMI interface.

image
Hardware
Machine: Alienware 16X Aurora AC16251
CPU: Intel Core Ultra 9 275HX
GPU: NVIDIA GeForce RTX 5070 Laptop
Tested on: Arch Linux, kernel 6.18.24-1-lts
How it works
Dell/Alienware exposes fan control through a proprietary WMI interface (AWCCWmiMethodFunction) implemented in ACPI SSDT table AWCCTABL.

By decompiling the ACPI tables and reverse engineering the WMAX method, we discovered the following protocol:

Fan Control Commands (via /proc/acpi/call)
CPU fan speed (0-100%): echo '_SB.AMWW.WMAX 0 0x15 {0x02,0x32,SPEED,0x00}' > /proc/acpi/call

GPU fan speed (0-100%): echo '_SB.AMWW.WMAX 0 0x15 {0x02,0x33,SPEED,0x00}' > /proc/acpi/call

Thermal profiles: echo '_SB.AMWW.WMAX 0 0x15 {0x01,0xA0,0x00,0x00}' > /proc/acpi/call # Balanced echo '_SB.AMWW.WMAX 0 0x15 {0x01,0xA1,0x00,0x00}' > /proc/acpi/call # Performance echo '_SB.AMWW.WMAX 0 0x15 {0x01,0xA3,0x00,0x00}' > /proc/acpi/call # Quiet echo '_SB.AMWW.WMAX 0 0x15 {0x01,0xAB,0x00,0x00}' > /proc/acpi/call # Game Shift

Where SPEED is a hex value from 0x00 (0%) to 0x64 (100%).

Fan IDs
0x32 = CPU fan
0x33 = GPU fan
Dependencies
Arch Linux: sudo pacman -S acpi_call-lts python-gobject gtk4 libadwaita

Load module: sudo modprobe acpi_call

Auto-load on boot: echo 'acpi_call' | sudo tee /etc/modules-load.d/acpi_call.conf

Installation
git clone https://github.com/Hugo2049/alienware-16x-fan-control cd alienware-16x-fan-control

Add sudoers rule: echo "$USER ALL=(ALL) NOPASSWD: $(pwd)/fan_helper.sh" | sudo tee /etc/sudoers.d/fancontroller sudo chmod 440 /etc/sudoers.d/fancontroller chmod +x fan_helper.sh

Run: python fan_control.py

CLI Usage
sudo ./fan_helper.sh cpu 75 sudo ./fan_helper.sh gpu 50 sudo ./fan_helper.sh both 80 60 sudo ./fan_helper.sh profile performance

Discovery Method
Dumped ACPI tables with acpidump
Decompiled SSDT tables with iasl
Found AWCCTABL SSDT containing AWCCWmiMethodFunction implementation
Reverse engineered WMAX method and AX24/AX26 sub-functions
Identified EC register writes via ECW1(0x21, speed) and ECW1(0x39, fan_id)
Confirmed via acpi_call kernel module
Credits
Developed by Hugo (Hugo2049) with assistance from Claude (Anthropic). The reverse engineering methodology, ACPI analysis, and protocol discovery were worked out collaboratively through an iterative process of dumping tables, reading ASL source, probing WMI interfaces on Windows, and testing on Linux via acpi_call.

Claude: https://claude.ai Anthropic: https://anthropic.com

License
GPL-2.0

About
Fan speed control for Alienware 16X Aurora on Linux via ACPI reverse engineering

Resources
Readme
Activity
Stars
1 star
Watchers
0 watching
Forks
0 forks
Report repository
Releases
No releases published
Packages
No packages published
Contributors
1
@Hugo2049
Hugo2049 Hb_W
Languages
Python
91.4%

Shell
8.6%
Footer
© 2026 GitHub, Inc.
Footer navigation
Terms
Privacy
Security
Status
Community
Docs
Contact
Manage cookies
Do not share my personal information
584512571-4e1d98ca-fec4-41cd-a11b-a1cf585dfed1.png
584512571-4e1d98ca-fec4-41cd-a11b-a1cf585dfed1.png (72.55 KiB) Viewed 4 times
alienware-16x-fan-control-main.zip
(5.33 KiB) Not downloaded yet

User avatar
!
35%
Posts: 3545
Joined: 2013-02-25 18:36

2026-04-28 13:02 »

fan_helper.sh

Code: Select all

#!/bin/bash

ACPI_CALL=/proc/acpi/call

call_acpi() {
    echo "$1" > $ACPI_CALL
}

case "$1" in
  cpu)
    call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x02,0x32,0x$(printf '%02X' $2),0x00}"
    ;;
  gpu)
    call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x02,0x33,0x$(printf '%02X' $2),0x00}"
    ;;
  both)
    call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x02,0x32,0x$(printf '%02X' $2),0x00}"
    call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x02,0x33,0x$(printf '%02X' $3),0x00}"
    ;;
  profile)
    case "$2" in
      balanced)    call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x01,0xA0,0x00,0x00}" ;;
      performance) call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x01,0xA1,0x00,0x00}" ;;
      quiet)       call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x01,0xA3,0x00,0x00}" ;;
      gameshift)   call_acpi "\_SB.AMWW.WMAX 0 0x15 {0x01,0xAB,0x00,0x00}" ;;
    esac
    ;;
  *)
    echo "Usage: fan_helper.sh cpu <0-100>"
    echo "       fan_helper.sh gpu <0-100>"
    echo "       fan_helper.sh both <cpu%> <gpu%>"
    echo "       fan_helper.sh profile <balanced|performance|quiet|gameshift>"
    exit 1
    ;;
esac
fan_control.py

Code: Select all

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GLib
import subprocess, math

HELPER   = '/home/hbwal/Desktop/fanController/fan_helper.sh'
CPU_COLOR = (0.216, 0.540, 0.867)
GPU_COLOR = (0.114, 0.620, 0.459)
MAX_RPM   = 7000

def read_hwmon(path):
    try:
        with open(path) as f: return int(f.read().strip())
    except: return 0

def run_helper(*args):
    try: subprocess.run(['sudo', HELPER] + list(args), check=True)
    except Exception as e: print(f"Helper error: {e}")

def get_cpu_temp():
    best = 0
    for i in range(30):
        v = read_hwmon(f'/sys/class/hwmon/hwmon8/temp{i}_input')
        if v > best: best = v
    return best // 1000 if best > 1000 else best

def get_gpu_temp():
    v = read_hwmon('/sys/class/hwmon/hwmon3/temp2_input')
    return v // 1000 if v > 1000 else v

def get_fan_rpm(fan):
    for hwmon in range(10):
        try:
            name = open(f'/sys/class/hwmon/hwmon{hwmon}/name').read().strip()
            if name == 'alienware_wmi':
                return read_hwmon(f'/sys/class/hwmon/hwmon{hwmon}/fan{fan}_input')
        except: pass
    return 0

class FanGauge(Gtk.DrawingArea):
    def __init__(self, color):
        super().__init__()
        self.color   = color
        self.percent = 0
        self.rpm     = 0
        self.set_size_request(170, 170)
        self.set_draw_func(self.draw)

    def set_value(self, percent, rpm):
        self.percent = max(0, min(100, percent))
        self.rpm     = rpm
        self.queue_draw()

    def draw(self, widget, cr, w, h):
        cx, cy = w/2, h/2
        r      = min(w,h)/2 - 18
        start  = math.pi * 0.75
        end    = math.pi * 2.25
        span   = end - start

        cr.set_line_width(11)
        cr.set_line_cap(1)

        cr.set_source_rgba(0.5, 0.5, 0.5, 0.18)
        cr.arc(cx, cy, r, start, end)
        cr.stroke()

        if self.percent > 0:
            r2, g2, b2 = self.color
            cr.set_source_rgba(r2, g2, b2, 0.85)
            cr.arc(cx, cy, r, start, start + span * self.percent / 100)
            cr.stroke()

        cr.select_font_face('Sans', 0, 1)
        cr.set_font_size(22)
        cr.set_source_rgba(0.95, 0.95, 0.95, 1)
        t = f'{self.rpm:,}'
        e = cr.text_extents(t)
        cr.move_to(cx - e.width/2 - e.x_bearing, cy - 6)
        cr.show_text(t)

        cr.set_font_size(11)
        cr.set_source_rgba(0.55, 0.55, 0.55, 1)
        e2 = cr.text_extents('RPM')
        cr.move_to(cx - e2.width/2 - e2.x_bearing, cy + 13)
        cr.show_text('RPM')

        cr.set_font_size(13)
        cr.set_source_rgb(*self.color)
        pt = f'{self.percent}%'
        e3 = cr.text_extents(pt)
        cr.move_to(cx - e3.width/2 - e3.x_bearing, cy + 33)
        cr.show_text(pt)


class FanApp(Adw.Application):
    def __init__(self):
        super().__init__(application_id='com.hbwal.fancontrol')
        self.connect('activate', self.on_activate)
        self.manual_mode = False

    def on_activate(self, app):
        self.win = Adw.ApplicationWindow(application=app)
        self.win.set_title('Fan Control')
        self.win.set_default_size(500, 620)
        self.win.set_resizable(False)

        tb = Adw.ToolbarView()
        header = Adw.HeaderBar()
        tb.add_top_bar(header)

        root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        root.set_margin_start(16)
        root.set_margin_end(16)
        root.set_margin_top(8)
        root.set_margin_bottom(16)

        # Title row
        tr = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        tb2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        tb2.set_hexpand(True)
        t1 = Gtk.Label(label='Fan control')
        t1.set_halign(Gtk.Align.START)
        t1.add_css_class('title-2')
        t2 = Gtk.Label(label='Alienware 16X Aurora · RTX 5070')
        t2.set_halign(Gtk.Align.START)
        t2.add_css_class('caption')
        t2.add_css_class('dim-label')
        tb2.append(t1)
        tb2.append(t2)
        self.mode_badge = Gtk.Label(label='Auto')
        self.mode_badge.add_css_class('tag')
        tr.append(tb2)
        tr.append(self.mode_badge)
        root.append(tr)

        # Gauges row
        gbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        for side in ('cpu', 'gpu'):
            card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
            card.add_css_class('card')
            card.set_hexpand(True)

            hdr = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
            hdr.set_margin_start(12)
            hdr.set_margin_end(12)
            hdr.set_margin_top(12)

            lbl = Gtk.Label(label='CPU fan' if side=='cpu' else 'GPU fan')
            lbl.set_halign(Gtk.Align.START)
            lbl.set_hexpand(True)
            lbl.add_css_class('caption')
            lbl.add_css_class('dim-label')

            badge = Gtk.Label(label='--°C')
            badge.add_css_class('tag')
            badge.add_css_class('success')

            hdr.append(lbl)
            hdr.append(badge)
            card.append(hdr)

            gauge = FanGauge(CPU_COLOR if side=='cpu' else GPU_COLOR)
            gauge.set_halign(Gtk.Align.CENTER)
            card.append(gauge)

            slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
            slider.set_value(50)
            slider.set_margin_start(12)
            slider.set_margin_end(12)
            slider.set_margin_bottom(12)
            slider.add_mark(0, Gtk.PositionType.BOTTOM, '0%')
            slider.add_mark(50, Gtk.PositionType.BOTTOM, '50%')
            slider.add_mark(100, Gtk.PositionType.BOTTOM, '100%')
            slider.connect('value-changed', self.on_cpu_slider if side=='cpu' else self.on_gpu_slider)
            card.append(slider)

            gbox.append(card)

            if side == 'cpu':
                self.cpu_badge  = badge
                self.cpu_gauge  = gauge
                self.cpu_slider = slider
            else:
                self.gpu_badge  = badge
                self.gpu_gauge  = gauge
                self.gpu_slider = slider

        root.append(gbox)

        # Presets
        pcard = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        pcard.add_css_class('card')
        pl = Gtk.Label(label='Presets')
        pl.set_halign(Gtk.Align.START)
        pl.add_css_class('caption')
        pl.add_css_class('dim-label')
        pl.set_margin_start(12)
        pl.set_margin_top(12)
        pcard.append(pl)

        prow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        prow.set_homogeneous(True)
        prow.set_margin_start(12)
        prow.set_margin_end(12)
        prow.set_margin_bottom(12)

        self.preset_btns = {}
        for key, label in [('auto','Auto'),('quiet','Quiet'),('balanced','Balanced'),('performance','Performance'),('gameshift','Game Shift')]:
            btn = Gtk.Button(label=label)
            btn.connect('clicked', self.on_preset, key)
            if key == 'balanced':
                btn.add_css_class('suggested-action')
            prow.append(btn)
            self.preset_btns[key] = btn

        pcard.append(prow)
        root.append(pcard)

        # Stats row
        scard = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
        scard.add_css_class('card')
        self.stat_labels = {}
        for i, (key, label) in enumerate([('cpu_rpm','CPU RPM'),('gpu_rpm','GPU RPM'),('cpu_temp','CPU temp'),('gpu_temp','GPU temp')]):
            sb = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
            sb.set_hexpand(True)
            sb.set_margin_top(12)
            sb.set_margin_bottom(12)
            if i > 0:
                scard.append(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL))
            l = Gtk.Label(label=label)
            l.add_css_class('caption')
            l.add_css_class('dim-label')
            l.set_halign(Gtk.Align.CENTER)
            v = Gtk.Label(label='--')
            v.add_css_class('title-4')
            v.set_halign(Gtk.Align.CENTER)
            sb.append(l)
            sb.append(v)
            scard.append(sb)
            self.stat_labels[key] = v
        root.append(scard)

        tb.set_content(root)
        self.win.set_content(tb)
        self.win.present()

        self.update_sensors()
        GLib.timeout_add(2000, self.update_sensors)

    def set_manual(self):
        for b in self.preset_btns.values():
            b.remove_css_class('suggested-action')
        self.mode_badge.set_label('Manual')
        self.manual_mode = True

    def on_cpu_slider(self, s):
        self.set_manual()
        pct = int(s.get_value())
        run_helper('cpu', str(pct))

    def on_gpu_slider(self, s):
        self.set_manual()
        pct = int(s.get_value())
        run_helper('gpu', str(pct))

    def on_preset(self, btn, key):
        for b in self.preset_btns.values():
            b.remove_css_class('suggested-action')
        btn.add_css_class('suggested-action')
        self.manual_mode = False

        presets = {
            'auto':        (None, None, 'balanced', 'Auto'),
            'quiet':       (20,   20,   'quiet',    'Quiet'),
            'balanced':    (50,   50,   'balanced', 'Balanced'),
            'performance': (80,   80,   'performance', 'Performance'),
            'gameshift':   (100,  100,  'gameshift', 'Game Shift'),
        }

        cv, gv, profile, badge_label = presets[key]
        self.mode_badge.set_label(badge_label)
        run_helper('profile', profile)

        if cv is not None:
            self.cpu_slider.set_value(cv)
            self.gpu_slider.set_value(gv)
            run_helper('both', str(cv), str(gv))

    def update_badge(self, badge, temp):
        badge.remove_css_class('success')
        badge.remove_css_class('warning')
        badge.remove_css_class('error')
        if temp < 70:   badge.add_css_class('success')
        elif temp < 85: badge.add_css_class('warning')
        else:           badge.add_css_class('error')
        badge.set_label(f'{temp}°C')

    def update_sensors(self):
        cpu_rpm  = get_fan_rpm(1)
        gpu_rpm  = get_fan_rpm(2)
        cpu_temp = get_cpu_temp()
        gpu_temp = get_gpu_temp()

        if self.manual_mode:
            cpu_pct = int(self.cpu_slider.get_value())
            gpu_pct = int(self.gpu_slider.get_value())
        else:
            cpu_pct = min(100, int(cpu_rpm / MAX_RPM * 100))
            gpu_pct = min(100, int(gpu_rpm / MAX_RPM * 100))

        self.cpu_gauge.set_value(cpu_pct, cpu_rpm)
        self.gpu_gauge.set_value(gpu_pct, gpu_rpm)
        self.update_badge(self.cpu_badge, cpu_temp)
        self.update_badge(self.gpu_badge, gpu_temp)

        self.stat_labels['cpu_rpm'].set_label(f'{cpu_rpm:,}')
        self.stat_labels['gpu_rpm'].set_label(f'{gpu_rpm:,}')
        self.stat_labels['cpu_temp'].set_label(f'{cpu_temp}°C')
        self.stat_labels['gpu_temp'].set_label(f'{gpu_temp}°C')
        return True

FanApp().run()