feat(fan): read hwmon directly — dual-fan support and simulated device tests - #31
Closed
vietanhdev wants to merge 2 commits into
Closed
feat(fan): read hwmon directly — dual-fan support and simulated device tests#31vietanhdev wants to merge 2 commits into
vietanhdev wants to merge 2 commits into
Conversation
…d fan
Dual-fan ThinkPads -- P1, P15, X1 Extreme -- have a second fan the app
could not see. /proc/acpi/ibm/fan has a speed: field and no speed2:, so
the second tachometer is invisible there. hwmon is the only way to read
it.
Verified live on this machine: two thinkpad tachometers at 3813 and 3515
RPM, one control channel.
That one-channel-two-fans shape is the structural point. thinkpad_acpi
writes the same level to both fans, so they cannot be driven
independently -- anything offering per-fan control on these machines
would be lying about what the hardware does.
Replaces shelling out to iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: +46.0°C
thinkpad-isa-0000
Adapter: ISA adapter
fan1: 3491 RPM
fan2: 3236 RPM
CPU: +62.0°C
GPU: +58.0°C
temp3: +63.0°C
temp4: +1.0°C
temp5: +64.0°C
temp6: +60.0°C
temp7: +51.0°C
temp8: N/A
pwm1: 128%
ucsi_source_psy_USBC000:001-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 3.00 A (max = +0.00 A)
BAT0-acpi-0
Adapter: ACPI interface
in0: 16.67 V
power1: 0.00 W
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +68.0°C (high = +100.0°C, crit = +100.0°C)
Core 1: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 2: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 3: +68.0°C (high = +100.0°C, crit = +100.0°C)
Core 4: +65.0°C (high = +100.0°C, crit = +100.0°C)
Core 5: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 6: +69.0°C (high = +100.0°C, crit = +100.0°C)
Core 7: +67.0°C (high = +100.0°C, crit = +100.0°C)
ucsi_source_psy_USBC000:002-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 0.00 A (max = +0.00 A)
nvme-pci-0400
Adapter: PCI adapter
Composite: +59.9°C (low = -273.1°C, high = +81.8°C)
(crit = +84.8°C)
Sensor 1: +59.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +62.9°C (low = -273.1°C, high = +65261.8°C)
acpitz-acpi-0
Adapter: ACPI interface
temp1: +62.0°C , which had three problems:
It required lm-sensors, so temperatures silently vanished without it.
It hardcoded the chip name thinkpad-isa-0000.
It double-counted the first fan: procfs contributed "Fan1" and
iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: +46.0°C
thinkpad-isa-0000
Adapter: ISA adapter
fan1: 3491 RPM
fan2: 3236 RPM
CPU: +62.0°C
GPU: +58.0°C
temp3: +63.0°C
temp4: +1.0°C
temp5: +64.0°C
temp6: +60.0°C
temp7: +51.0°C
temp8: N/A
pwm1: 128%
ucsi_source_psy_USBC000:001-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 3.00 A (max = +0.00 A)
BAT0-acpi-0
Adapter: ACPI interface
in0: 16.67 V
power1: 0.00 W
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +68.0°C (high = +100.0°C, crit = +100.0°C)
Core 1: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 2: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 3: +68.0°C (high = +100.0°C, crit = +100.0°C)
Core 4: +65.0°C (high = +100.0°C, crit = +100.0°C)
Core 5: +67.0°C (high = +100.0°C, crit = +100.0°C)
Core 6: +69.0°C (high = +100.0°C, crit = +100.0°C)
Core 7: +67.0°C (high = +100.0°C, crit = +100.0°C)
ucsi_source_psy_USBC000:002-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 0.00 A (max = +0.00 A)
nvme-pci-0400
Adapter: PCI adapter
Composite: +59.9°C (low = -273.1°C, high = +81.8°C)
(crit = +84.8°C)
Sensor 1: +59.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +62.9°C (low = -273.1°C, high = +65261.8°C)
acpitz-acpi-0
Adapter: ACPI interface
temp1: +62.0°C contributed "fan1", so the UI listed one fan twice under two
names. The procfs entry is dropped now in favour of the complete hwmon
set rather than merged with it.
Dropping the subprocess also removed the last use of the regex crate,
which is gone from the dependency tree.
The capture script had a matching gap: it resolved /sys/class/hwmon
symlinks to their /sys/devices targets and stored only those, so a
profile contained the data but nothing could discover it -- code walks
/sys/class/hwmon, which was absent. It now mirrors the class view as real
directories (not symlinks, which would point outside the profile at the
real machine). The profile was regenerated; redaction re-verified clean.
Tests run against the captured P1 Gen 4i profile and cover both fans,
the single channel, no duplicate labels, temperatures without lm-sensors,
and a machine with no hwmon at all -- which is every container.
The captured profile covers exactly one machine -- a dual-fan P1 with fan_control=1 already on. Every other shape the app must survive was untested, and those are the ones most likely to break because nobody is running the app on them during development. tests/synthetic_hardware.rs builds sysfs trees in temp dirs and points THINKUTILS_HARDWARE_ROOT at them, so the same code path a real machine takes is exercised. Values follow the kernel's documented formats: RPM as a plain integer, temperatures in millidegrees, pwmN_enable as 0/1/2. Thirteen machines and states: single-fan ThinkPad, the common T/X case dual-fan, two tachometers and ONE control channel four-fan, so the reader is not quietly capped at what we own non-ThinkPad laptop -- a dell_smm fan and coretemp still read no hwmon at all, which is every container fan_control=1 absent, the most common broken state, distinguishable from the working one by exactly three commands: lines a stopped fan reporting 0 RPM, which is a reading and not a missing sensor -- dropping it would hide the most safety-relevant state there is pwm1_enable=0, which means FULL SPEED on thinkpad_acpi rather than off empty, non-numeric and out-of-range sysfs contents a chip directory with no name file a sensor with no label file stable ordering across repeated reads Also fixes a real gap the earlier fixture had: the capture script resolved /sys/class/hwmon symlinks to their /sys/devices targets and stored only those, so the data was present but undiscoverable -- code walks /sys/class/hwmon, which was absent. It mirrors the class view now. Suite: 85 -> 99.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
thinkutils | 59a5969 | Jul 19 2026, 03:48 PM |
Owner
Author
|
Superseded — the PPA source build landed on main via #26. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dual-fan ThinkPads (P1, P15, X1 Extreme) had a second fan the app could not see.
/proc/acpi/ibm/fanhas aspeed:field and nospeed2:, so hwmon is the only way to read it.Verified live on a P1 Gen 4i: two tachometers at 3813 and 3515 RPM, one control channel.
The structural point
Tachometers and control channels are not one-to-one.
thinkpad_acpiwrites the same level to both fans, so a dual-fan machine has 2 tachs and 1 PWM. Code assuming one fan per channel is wrong on every P-series — and anything offering per-fan control would be lying about the hardware.Replaces the iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: +46.0°C
thinkpad-isa-0000
Adapter: ISA adapter
fan1: 3486 RPM
fan2: 3238 RPM
CPU: +74.0°C
GPU: +58.0°C
temp3: +63.0°C
temp4: +1.0°C
temp5: +61.0°C
temp6: +56.0°C
temp7: +49.0°C
temp8: N/A
pwm1: 128%
ucsi_source_psy_USBC000:001-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 3.00 A (max = +0.00 A)
BAT0-acpi-0
Adapter: ACPI interface
in0: 16.67 V
power1: 0.00 W
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +77.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +70.0°C (high = +100.0°C, crit = +100.0°C)
Core 1: +70.0°C (high = +100.0°C, crit = +100.0°C)
Core 2: +66.0°C (high = +100.0°C, crit = +100.0°C)
Core 3: +64.0°C (high = +100.0°C, crit = +100.0°C)
Core 4: +58.0°C (high = +100.0°C, crit = +100.0°C)
Core 5: +77.0°C (high = +100.0°C, crit = +100.0°C)
Core 6: +70.0°C (high = +100.0°C, crit = +100.0°C)
Core 7: +74.0°C (high = +100.0°C, crit = +100.0°C)
ucsi_source_psy_USBC000:002-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 0.00 A (max = +0.00 A)
nvme-pci-0400
Adapter: PCI adapter
Composite: +57.9°C (low = -273.1°C, high = +81.8°C)
(crit = +84.8°C)
Sensor 1: +57.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +58.9°C (low = -273.1°C, high = +65261.8°C)
acpitz-acpi-0
Adapter: ACPI interface
temp1: +74.0°C subprocess
Three problems with shelling out:
thinkpad-isa-0000sensorscontributed "fan1", so the UI listed one fan twiceDropping it also removed the last use of the
regexcrate, now gone from the tree.Simulated devices
The captured profile covers exactly one machine.
tests/synthetic_hardware.rsbuilds sysfs trees in temp dirs for thirteen shapes and states — the ones most likely to break precisely because nobody develops on them:dell_smmfan and coretemp still readfan_control=1absentcommands:linespwm1_enable=0namefileA real gap in the earlier fixture
The capture script resolved
/sys/class/hwmonsymlinks to their/sys/devicestargets and stored only those — so the data was present but undiscoverable, because code walks/sys/class/hwmon, which was absent. It mirrors the class view now (as real directories; symlinks would point outside the profile at the real machine). Profile regenerated, redaction re-verified.Suite: 85 → 99.