Add fan control functionality

This commit is contained in:
NGnius (Graham) 2022-04-27 23:03:23 -04:00
parent d42d95bb8c
commit c7042fa487
4 changed files with 118 additions and 0 deletions

View file

@ -32,6 +32,27 @@ Use `cpupower` (usage: `cpupower --help`).
This isn't strictly how PowerTools does it, but it's a multi-step process which can involve changing the CPU governor.
All that can be done automatically by `cpupower frequency-set --freq {frequency}` where `{frequency}` is `1.7G`, `2.4G` or `2.8G`.
### Set Fan speed
Enable automatic control: `echo 0 > /sys/class/hwmon/hwmon5/recalculate` enables automatic fan control.
Disable automatic control: `echo 1 > /sys/class/hwmon/hwmon5/recalculate` disables automatic (temperature-based) fan control and starts using the set fan voltage instead.
Set the fan voltage: `echo {voltage} > /sys/class/hwmon/hwmon5/fan1_target` where `{voltage}` is a value from 0 to 4000 mV\*.
Read the actual fan voltage: `cat /sys/class/hwmon/hwmon5/fan1_input` gives the fan voltage in mV\*.
\*WARNING: I'm not sure if that's actually fan voltage. I've set it to 6000 (1V higher!!! than the fan's rated 5V) and the fan responded, so it appears to not be limited. Blow up your fans at your own risk; read the license.
NOTE: There's a bug in the fan controller; if you enable automatic fan control it will forget any previously-set target despite it appearing to be set correctly (i.e. `cat /sys/class/hwmon/hwmon5/fan1_target` will display the correct value).
When you disable automatic fan control, you will need to set the fan voltage again.
### Steam Deck kernel patches
This is how I figured out how the fan stuff works.
I've only scratched the surface of what this code allows, I'm sure it has more useful information.
https://lkml.org/lkml/2022/2/5/391
## License
This is licensed under GNU GPLv3.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 151 KiB

35
main.py
View file

@ -3,6 +3,9 @@ import time
class Plugin:
CPU_COUNT = 8
SCALING_FREQUENCIES = [1700000, 2400000, 2800000]
FAN_VOLTAGES = [0, 1000, 2000, 3000, 4000, 5000]
set_fan_voltage = None
# call from main_view.html with setCPUs(count, smt)
async def set_cpus(self, count, smt=True) -> int:
@ -66,6 +69,38 @@ class Plugin:
freq = int(freq_maybe)
return self.SCALING_FREQUENCIES.index(freq)
async def set_fan_tick(self, tick: int):
self.set_fan_voltage = tick # cache voltage set to echo back in get_fan_tick()
if tick >= len(self.FAN_VOLTAGES):
# automatic mode
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 0)
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", 4099) # 4099 is default
else:
# manual voltage
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1)
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", self.FAN_VOLTAGES[tick])
async def get_fan_tick(self) -> int:
fan_target = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_target", amount=-1).strip())
fan_input = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_input", amount=-1).strip())
fan_target_v = float(fan_target) / 1000
fan_input_v = float(fan_input) / 1000
if self.set_fan_voltage is not None:
x = self.set_fan_voltage
self.set_fan_voltage = None
return x
elif fan_target == 4099 or (int(round(fan_target_v)) != int(round(fan_input_v))):
# cannot read /sys/class/hwmon/hwmon5/recalculate, so guess based on available fan info
# NOTE: the fan takes time to ramp up, so fan_target will never approximately equal fan_input
# when fan_target was changed recently (hence set voltage caching)
return len(self.FAN_VOLTAGES)
else:
# quantize voltage to nearest tick (price is right rules; closest without going over)
for i in range(len(self.FAN_VOLTAGES)-1):
if fan_target <= self.FAN_VOLTAGES[i]:
return i
return len(self.FAN_VOLTAGES)-1 # any higher value is considered as highest manual setting
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
async def _main(self):
pass

View file

@ -33,6 +33,14 @@
function getMaxBoost() {
return call_plugin_method("get_max_boost", {});
}
function setFanTick(tick) {
return call_plugin_method("set_fan_tick", {"tick": tick});
}
function getFanTick() {
return call_plugin_method("get_fan_tick", {});
}
// other logic
@ -42,6 +50,7 @@
setToggleState(document.getElementById("smtToggle"), await getSMT());
selectNotch("cpuThreadsNotch", await getCPUs() - 1, 8);
selectNotch("frequencyNotch", await getMaxBoost(), 3);
selectNotch("fanNotch", await getFanTick(), 7);
}
async function setCPUNotch(index) {
@ -93,6 +102,12 @@
selectNotch(ROOT_ID, await getMaxBoost(), 3);
}
async function onSetFanNotch(index) {
const ROOT_ID = "fanNotch";
await setFanTick(index);
selectNotch(ROOT_ID, index, 7);
}
function selectNotch(rootId, index, elements) {
// WARNING: this yeets any style in div of slider
const ENABLED_CLASS = "gamepadslider_TickActive_j418S";
@ -236,5 +251,52 @@
WARNING: This will change the CPU governor.
</div>
</div>
<!-- Fan voltage selector -->
<div class="gamepaddialog_Field_eKmEX gamepaddialog_WithFirstRow_2bDqk gamepaddialog_WithChildrenBelow_37xzV gamepaddialog_InlineWrapShiftsChildrenBelow_3LCXh gamepaddialog_WithBottomSeparator_3YKpU gamepaddialog_ChildrenWidthFixed_ljcbL gamepaddialog_ExtraPaddingOnChildrenBelow_3nLNL gamepaddialog_StandardPadding_xIITX gamepaddialog_HighlightOnFocus_2HFrm Panel Focusable">
<div class="gamepaddialog_FieldLabelRow_2VcTl">
<div class="gamepaddialog_FieldLabel_3jMlJ">Fan Voltage</div>
</div>
<div class="gamepaddialog_FieldChildren_2rhav">
<div id="fanNotch" class="gamepadslider_SliderControlAndNotches_23hjX Focusable" tabindex="0" style="--normalized-slider-value:0.33;">
<div class="gamepadslider_SliderControl_1udlG">
<div class="gamepadslider_SliderTrack_2_vG6 gamepadslider_SliderHasNotches_1Lr71 "></div>
<div class="gamepadslider_SliderHandleContainer_8xNY6">
<div class="gamepadslider_SliderHandle_11PBf"></div>
</div>
</div>
<div class="gamepadslider_SliderNotchContainer_2yM7S Panel Focusable">
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch0" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(0)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">0V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch1" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(1)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">1V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch2" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(2)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">2V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch3" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(3)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">3V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch4" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(4)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">4V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch5" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(5)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">5V</div>
</div>
<div class="gamepadslider_SliderNotch_LYPXt">
<div id="fanNotch6" class="gamepadslider_SliderNotchTick_u8QEa gamepadslider_TickActive_j418S" onclick='onSetFanNotch(6)'></div>
<div class="gamepadslider_SliderNotchLabel_dbACW">Auto</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>