Manual fan control with live temperature and RPM readouts for the Framework Desktop (AMD Ryzen AI MAX), on Linux. A small GTK4 app with a per-fan slider, backed by a minimal privileged daemon.
The Framework Desktop's fans are driven by a Chromium-style embedded controller
(cros_ec), not a standard Super I/O chip. The kernel exposes fan readings
under /sys/class/hwmon, but there are no writable PWM nodes — so tools like
CoolerControl or lm-sensors can only read the fans, never set them. The only
way to control the fans is through the embedded controller via Framework's
official framework_tool.
FanFrame wraps that in a desktop GUI with a safe privilege model.
- Per-fan sliders (0–100% duty), each showing that fan's live RPM.
- Per-fan Auto toggle — switch any single fan back to EC control. When a fan is automatic its slider is disabled (greyed out) and the Auto button is pressed, so the current mode is always obvious. A global button puts every fan back to automatic at once.
- Renamable fans — click a fan's name to rename it (e.g. "CPU", "Intake").
Names are saved per user under
~/.config/fanframe/labels.json. - Live readouts of all
cros_ectemperatures and fan speeds, refreshed once a second straight from the kernel. - Thermal failsafe: while any fan is in manual mode, the daemon restores automatic control if a CPU sensor crosses a critical threshold (default 92 °C).
- GUI never runs as root. The only privileged code is a tiny daemon exposing status/set_duty/auto over a group-restricted Unix socket.
GUI (GTK4, unprivileged)
|- reads temps/RPM directly from sysfs (cros_ec hwmon)
'- sends set_duty / auto --unix socket--> fanframed (root)
|- framework_tool --fansetduty / --autofanctrl
'- thermal failsafe (manual + too hot -> auto)
The embedded-controller fan index is 0-based, so sysfs fan1 ("Ventola 1") maps
to framework_tool --fansetduty 0 <pct>.
- A Framework Desktop (or another Framework device whose fans are exposed via
cros_ec). Developed and tested on the Ryzen AI MAX Desktop, Fedora 43. systemd, Python 3.11+, GTK 4 + PyGObject.- Framework's
framework_toolbinary (FrameworkComputer/framework-system).
# Fedora: GUI dependencies
sudo dnf install -y python3-gobject gtk4
# Get framework_tool (official release binary) into your PATH
mkdir -p ~/.local/bin
curl -fsSL -o ~/.local/bin/framework_tool \
https://github.com/FrameworkComputer/framework-system/releases/latest/download/framework_tool
chmod +x ~/.local/bin/framework_toolgit clone https://github.com/iovis801/Fanframe.git
cd Fanframe
sudo ./install.sh
fanframe-gui # or launch "FanFrame" from your application menuinstall.sh copies framework_tool to /usr/local/bin (if not already there),
installs the sources to /opt/fanframe, adds the fanframe-gui launcher, and
enables the fanframed systemd service. The control socket is restricted to your
login group.
- Drag a fan's slider to set its duty; it is applied ~350 ms after you stop. The RPM next to each slider confirms which physical fan responds.
- Press a fan's Auto toggle to hand just that fan back to the EC (its slider greys out). Use All fans: automatic (EC) to reset every fan at once.
- Click a fan's name to rename it; the name persists across launches.
- The panels keep updating even if the daemon is stopped (reads need no daemon).
Manual duty does not persist across reboot or suspend — the EC reverts to
automatic. While in manual mode the failsafe forces automatic control if the CPU
gets too hot, but a fan controller is still a foot-gun: don't pin a low duty
under sustained load and walk away. Tune the trip point with FANFRAME_CRITICAL_C.
Set in /etc/systemd/system/fanframed.service (Environment=...):
| Variable | Default | Meaning |
|---|---|---|
FANFRAME_SOCKET |
/run/fanframe.sock |
control socket path |
FANFRAME_GROUP |
your login group | group allowed to drive the fans |
FANFRAME_TOOL |
/usr/local/bin/framework_tool |
EC CLI path |
FANFRAME_CRITICAL_C |
92 |
failsafe trip temperature (°C) |
After edits: sudo systemctl daemon-reload && sudo systemctl restart fanframed.
sudo ./uninstall.sh # restores automatic fan control, then removes everythingpython3 -m pytest # unit + socket integration tests
python3 -m pytest --cov=fanframe # coverage (GUI excluded — verified by running it)The logic modules are unit-tested at ~87%; the GTK UI is validated by running
it. The --fansetduty <index> <pct> form is what framework_tool exposes for
per-fan control; if a future firmware changes the indexing, adjust
EcController in src/fanframe/ec.py.
Issues and pull requests are welcome. This is a small, focused tool — keep changes minimal and the privilege boundary (GUI unprivileged, daemon tiny) intact.
MIT — free for anyone to use, modify, and distribute.
This is an unofficial community tool, not affiliated with Framework Computer Inc. It controls cooling hardware; use it at your own risk.