Skip to content

Instantly share code, notes, and snippets.

@blazewicz
Last active February 1, 2024 05:30
Show Gist options
  • Save blazewicz/2b9e6a34e4b621f7e3c08d917cd0a4f0 to your computer and use it in GitHub Desktop.
Save blazewicz/2b9e6a34e4b621f7e3c08d917cd0a4f0 to your computer and use it in GitHub Desktop.
Better Fan control for Argon One Raspberry Pi 4 case
## This is config for Argon controllable fan
## Temperature at which fan starts
t_on = 50
## Temperature at which fan has full speed
t_full = 65
## Temperature difference for cooldown curve.
## Basically fan will start to slow down when temperature drops by this amount.
t_delta = 5.0
## FAN minimal speed, lowest value at which fan starts spinning.
s_min = 0
## FAN maximum speed, defaults to full speed i.e. 100, set lower if fan noise bothers you too much.
s_max = 100
## Temperature read interval in seconds
interval = 10
#!/usr/bin/python3
"""
Argon One Fan Controller.
Installation
------------
Put this file in /usr/bin/argonone_fan, remeber to give it execution flag (chmod u+x). Configuration file
goes to /etc/argonone_fan.conf. To enable system daemon copy serive file to /etc/systemd/system/argonone_fan.service
and execute:
sudo systemctl daemon-reload
sudo systemctl enable argonone_fan
sudo systemctl start argonone_fan
To see logs run:
sudo journalctl -fu argonone_fan
Principle of operation
----------------------
Fan starts spinning at starting temperature (ON) speed increases lineary with temperature until reaching max at
end temperature (FULL). If temperature rises and begins to lower PWM will not decrease until temp drops
by configured delta (D). This is most common situation (const), where temperature oscilates around fixed value.
With this type of controll you get quick reaction for temperature increase and minial speed variations
in stable conditions.
^ fan speed
100% -
|
MAX - ---<---------<>--
| / /
| (cooling) / /
| / /
| / /
| / /
| / (const) /
| / ---<>--- /
| / / (heating)
| / <- D -> /
MIN - / /
| | |
| | |
0% +--<>------>-----|----------|------->
ON FULL T
"""
import logging
import time
import smbus
import RPi.GPIO as GPIO
rev = GPIO.RPI_REVISION
if rev == 2 or rev == 3:
bus = smbus.SMBus(1)
else:
bus = smbus.SMBus(0)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format='%(message)s')
def _interp1(val, in0, in1, out0, out1):
"""Linear interpolation"""
return (val - in0) * (out1 - out0) / (in1 - in0) + out0
def _interp1_sat(val, in0, in1, out0, out1, sin0=None, sin1=None, sout0=None, sout1=None):
"""Linear interpolation with saturation.
in0, in1 : input range
out0, out1 : output range
sin0, sin1 : saturation input bounds (default in0, in1)
sout0, sout1 : saturation output bounds (default out0, out1)
out =
sout0 if val < sin0
sout1 if val >= sin1
(out0, out1) if val in (in0, in1)
"""
if sin0 is None:
sin0 = in0
if sin1 is None:
sin1 = in1
if sout0 is None:
sout0 = out0
if sout1 is None:
sout1 = out1
if in0 < in1:
assert sin0 <= sin1
else:
assert sin0 >= sin1
if out0 < out1:
assert sout0 <= sout1
else:
assert sout0 >= sout1
if val < sin0:
return sout0
elif val >= sin1:
return sout1
else:
ret = _interp1(val, in0, in1, out0, out1)
if ret > sout1:
return sout1
elif ret < sout0:
return sout0
else:
return ret
def get_temp():
try:
with open("/sys/class/thermal/thermal_zone0/temp", "r") as tempfp:
temp = tempfp.readline()
return float(int(temp) / 1000)
except IOError:
return 0
def read_config():
config = {
'on': 50,
'full': 60,
'delta': 2,
'min': 10,
'max': 100,
'interval': 10,
}
try:
with open("/etc/argonone_fan.conf") as configfp:
config_raw = configfp.read()
except FileNotFoundError:
pass
else:
for lineno, line in enumerate(config_raw.splitlines()):
line = line.strip()
if line.startswith('#') or len(line) == 0:
continue
try:
key, value = line.split('=', maxsplit=1)
except ValueError:
logger.warning('Invalid syntax in config file on line %d', lineno + 1, exc_info=True)
continue
key = key.strip()
value = value.strip()
try:
if key == 't_on':
config['on'] = float(value)
elif key == 't_full':
config['full'] = float(value)
elif key == 't_delta':
config['delta'] = float(value)
elif key == 's_min':
config['min'] = float(value)
elif key == 's_max':
config['max'] = float(value)
elif key == 'interval':
config['interval'] = float(value)
else:
logger.warning('Unknown config key %s', key)
except ValueError:
logger.warning('Invalid config value for %s', key, exc_info=True)
continue
return config
def set_fanspeed(speed):
address = 0x1a
try:
bus.write_byte(address, speed)
except IOError:
pass
def temp_check():
config = read_config()
logger.info('Starting temperature daemon, %.1f°C/%.1f°C ±%.1f°C %d-%d%%',
config['on'], config['full'], config['delta'], config['min'], config['max'])
logger.info('Current core temperature %.1f°C', get_temp())
set_fanspeed(0)
lasttemp = 0
lastspeed = 0
speeddown_from = None
speedup_from = None
while True:
temp = get_temp()
if temp != lasttemp:
temp0 = config['on']
temp1 = config['full']
speed0 = config['min']
speed1 = config['max']
speedmin = 0
speedmax = config['max']
if temp < lasttemp:
speedup_from = None
if speeddown_from is None:
speeddown_from = lastspeed
speedmax = speeddown_from
temp0 = config['on'] - config['delta']
temp1 = config['full'] - config['delta']
else:
speeddown_from = None
if speedup_from is None:
speedup_from = lastspeed
speedmin = speedup_from
lasttemp = temp
logger.debug('%.1f, %d, %d, %d, %d, %d, %d', temp, temp0, temp1, speed0, speed1, speedmin, speedmax)
speed = round(_interp1_sat(temp, temp0, temp1, speed0, speed1, sout0=speedmin, sout1=speedmax))
if speed != lastspeed:
lastspeed = speed
set_fanspeed(speed)
logger.info('FAN speed updated T=%.1f°C PWM=%d%%', temp, speed)
time.sleep(config['interval'])
def main():
try:
temp_check()
except KeyboardException:
pass
finally:
GPIO.cleanup()
if __name__ == '__main__':
main()
[Unit]
Description=Argon One Fan Service
After=multi-user.target
[Service]
Type=simple
Restart=always
RemainAfterExit=true
ExecStart=/usr/bin/argonone_fan
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment