From b1f112b2a4012cacbabbff2b64b30c9c8ce6a827 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Sun, 12 Apr 2020 18:45:15 +0200 Subject: [PATCH] POC import --- config.json.example | 24 ++++++++ fand.py | 134 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 config.json.example create mode 100755 fand.py diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..32dfa7e --- /dev/null +++ b/config.json.example @@ -0,0 +1,24 @@ +{ + "control": { + "interval": 5, // [s] delay between rounds + "verbose": true + }, + "sensor": { + "source": "/sys/class/thermal/thermal_zone0/temp", + "weight": 0.1 // [0.0-1.0] sample contribution weight + // lower value -> slower response + }, + "pwm": { + "chip": 1, + "channel": 0, + "period": 100000 // [ns] + }, + "map": [ + // each tuple is [m°C, percentage of period] + [25000, 0], + [35000, 50], + [45000, 75], + [50000, 90], + [55000, 100] + ] +} diff --git a/fand.py b/fand.py new file mode 100755 index 0000000..10e3a6e --- /dev/null +++ b/fand.py @@ -0,0 +1,134 @@ +#! /usr/bin/env python3 +# encoding: utf-8 + +""" +Fan control service. + +Measures temperatures and sets fan PWM according to a configured curve. + +See /etc/fand/config.json.example for more. +""" + +import dec8 +import os +import sys +import time + +class Sensor: + def __init__(self, source, weight=0.2): + self.f = open(source, "rb") + self.weight = weight + self.value = None + + def measure(self): + self.f.seek(0) + raw = int(self.f.read()) + + if self.value is None: + self.value = raw + else: + self.value = int(self.weight * raw + (1-self.weight) * self.value) + + return self.value + +class PWM: + def __init__(self, chip, ch, period): + self.chip_base = "/sys/class/pwm/pwmchip%d/" %(chip) + self.ch_base = "/sys/class/pwm/pwmchip%d/pwm%d/" %(chip, ch) + self.period = period + + # export the channel to sysfs if necessary + if not os.path.isdir(self.ch_base): + with open(self.chip_base + "export", "w") as f: + f.write(str(ch)) + + # disable channel + self.set_enabled(False) + + # set period & polarity + with open(self.ch_base + "period", "w") as f: + f.write(str(period)) + + with open(self.ch_base + "polarity", "w") as f: + f.write("normal") + + # acquire a duty cycle file descriptor + self.f_dc = open(self.ch_base + "duty_cycle", "w") + + # set to idle + self.set_percent(0) + + # re-enable channel + self.set_enabled(True) + + def set_enabled(self, enabled): + with open(self.ch_base + "enable", "r+") as f: + current = f.read().strip() + requested = "1" if enabled else "0" + + if current != requested: + f.seek(0) + f.write(requested) + + def set_percent(self, pc): + self.duty_cycle = int(self.period * pc/100) + self.f_dc.seek(0) + self.f_dc.write(str(self.duty_cycle)) + self.f_dc.flush() + +def linlut(a, seq): + """ + Given a value `a` and a sorted list of tuples `seq`, + finds a pair in `seq` whose first elements are closest + to ... well you know what I mean. Read the code. It's + shorter than this paragraph. + """ + + smaller = None + larger = None + + for d in seq: + if d[0] <= a: + smaller = d + + elif d[0] > a: + larger = d + break + + if not smaller: + return seq[0][1] + + if not larger: + return seq[-1][1] + + ratio = (a - smaller[0]) / (larger[0] - smaller[0]) + dy = larger[1] - smaller[1] + + return smaller[1] + ratio * dy + +def printf(fmt, *args, stream=sys.stderr): + print(fmt % args, file=stream) + +if __name__ == "__main__": + try: + cfg = dec8.cfg.Config.from_file("/etc/fand/config.json") + except FileNotFoundError: + printf("!!! missing /etc/fand/config.json") + + s = Sensor(cfg.sensor.source, cfg.sensor.weight) + p = PWM(cfg.pwm.chip, cfg.pwm.channel, cfg.pwm.period) + + while True: + temperature = s.measure() + percentage = linlut(temperature, cfg.map) + p.set_percent(percentage) + + if cfg.control.verbose: + printf("%.01f °C -> %d %% (%d/%d µs)", + temperature, + percentage, + p.duty_cycle // 1000, + p.period // 1000 + ) + + time.sleep(cfg.control.interval)