Last active
February 11, 2025 16:38
-
-
Save blazewicz/2b9e6a34e4b621f7e3c08d917cd0a4f0 to your computer and use it in GitHub Desktop.
Better Fan control for Argon One Raspberry Pi 4 case
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
## 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 |
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
#!/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() |
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
[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