POC import
This commit is contained in:
commit
b1f112b2a4
2 changed files with 158 additions and 0 deletions
24
config.json.example
Normal file
24
config.json.example
Normal file
|
@ -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]
|
||||||
|
]
|
||||||
|
}
|
134
fand.py
Executable file
134
fand.py
Executable file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue