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