stratux/fancontrol_main/fancontrol.go

366 wiersze
9.5 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"math"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/b3nn0/stratux/common"
"github.com/felixge/pidctrl"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/stianeikeland/go-rpio/v4"
"github.com/takama/daemon"
)
import "C"
// Initialize Prometheus metrics.
var (
currentTemp = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "current_temp",
Help: "Current CPU temp.",
})
currentPWM = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "current_pwm",
Help: "Current PWM Value",
})
totalFanOnTime = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "total_fan_on_time",
Help: "Total fan run time.",
},
[]string{"all"},
)
totalUptime = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "total_uptime",
Help: "Total uptime.",
},
[]string{"all"},
)
)
const (
configLocation = "/boot/firmware/stratux.conf"
// CPU temperature target, degrees C
defaultTempTarget = 50.
/* Minimum duty cycle in % is the point below which the fan */
/* stops running nicely from a running situation */
defaultPwmDutyMin = 0
/* Maximum duty for PWM controller */
pwmDutyMax = 100 // Must be kept at 100
defaultPwmFrequency = 64000
// how often to update
updateDelayMS = 5000
// start delay of the fan to start the fan to 80% to give the fan a kick to start spinning
PWMDuty80PStartDelay = 500
// GPIO-1/BCM "18"/Pin 12 on a Rev 2 and 3,4 Raspberry Pi
defaultPin = 18
// name of the service
name = "fancontrol"
description = "cooling fan speed control based on CPU temperature"
// Address on which daemon should be listen.
addr = ":9977"
// When set to true, we do not turn of the fan below targetCPU temperature
alwaysOn = true
)
type FanControl struct {
TempTarget float64
TempCurrent float64
PWMDutyMin uint32
PWMFrequency uint32
PWMDuty80PStartDelay uint32
PWMDutyCurrent uint32
PWMPin int
}
var myFanControl FanControl
var configChan = make(chan bool, 1)
var stdlog, errlog *log.Logger
func updateStats() {
updateTicker := time.NewTicker(1 * time.Second)
for {
<-updateTicker.C
totalUptime.With(prometheus.Labels{"all": "all"}).Inc()
currentTemp.Set(float64(myFanControl.TempCurrent))
currentPWM.Set(float64(myFanControl.PWMDutyCurrent))
if myFanControl.PWMDutyCurrent > 0 {
totalFanOnTime.With(prometheus.Labels{"all": "all"}).Inc()
}
}
}
// Map a incomming range to a outgoing range
func fmap( x, in_min, in_max, out_min, out_max float64) float64 {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
func fanControl() {
myFanControl.PWMDuty80PStartDelay = PWMDuty80PStartDelay
myFanControl.TempCurrent = 0
myFanControl.PWMDutyCurrent = 0
updateControlDelay := time.NewTicker(updateDelayMS * time.Millisecond)
// Monitor Temperature
go common.CpuTempMonitor(func(cpuTemp float32) {
if common.IsCPUTempValid(cpuTemp) {
myFanControl.TempCurrent = float64(cpuTemp)
}
})
// Open Raspberry GPIO pins
err := rpio.Open()
if err != nil {
os.Exit(1)
}
defer rpio.Close()
// Set PWM Mode
pin := rpio.Pin(myFanControl.PWMPin)
pin.Mode(rpio.Pwm)
setFanFrequency := func(frequency uint32) {
if (frequency < 250000) {
frequency = 250000
} else if (frequency > 100000000) {
frequency = 100000000
}
pin.Freq(int(frequency))
}
setFanFrequency(myFanControl.PWMFrequency * 100)
// func to Calculate the dutyCycle to the hardware value that
// is approprate for the HW and minimum fan speed allowed
// Result is appropriate duty value for the fan
dutyCycleToFan := func(dutyCycle float64) float64 {
mappedMinimum := fmap(float64(myFanControl.PWMDutyMin), 0.0, 100.0, 0, float64(pwmDutyMax))
return fmap(dutyCycle, 0.0, 100.0, mappedMinimum, 100.0)
}
// Setup HW to a specific duty cycle 0..100
setHWDutyCycle := func(value float64) {
if (value<0.0) {
value = 0.0
} else if (value>100.0) {
value = 100.0
}
myFanControl.PWMDutyCurrent = uint32(value)
pin.DutyCycle(uint32(math.Ceil(fmap(value, 0.0, 100.0, 0.0, float64(pwmDutyMax)))), pwmDutyMax)
}
// Fan test function
turnOnFanTest := func () {
setHWDutyCycle(100.0)
time.Sleep(5 * time.Second) // to show user we are running
setHWDutyCycle(float64(myFanControl.PWMDutyMin))
time.Sleep(10 * time.Second)
}
// Power on "test". Allows the user to verify that their fan is working at the selected minimum duty cycle
// Turns on the fan at minimum duty for 10 seconds. User should see that the fan keeps running all the time
turnOnFanTest()
// Start Prometheus
prometheus.MustRegister(currentTemp)
prometheus.MustRegister(currentPWM)
prometheus.MustRegister(totalFanOnTime)
prometheus.MustRegister(totalUptime)
go updateStats()
// Create a PID controller
pidControl := pidctrl.NewPIDController(0.2, 0.2, 0.1)
pidControl.SetOutputLimits(-100, 0.0)
pidControl.Set(myFanControl.TempTarget)
var lastPWMControlValue float64 = 0.0
for {
// Update the PID controller.
pidValueOut := -pidControl.UpdateDuration(myFanControl.TempCurrent, updateDelayMS * time.Millisecond)
// If fan is starting up eg from 0 to some value, start it up for myFanControl.PWMDuty80PStartDelay at 80%
if (lastPWMControlValue <=5.0 && pidValueOut>5.0 && !alwaysOn) {
// log.Println("Starting up fan for" ,myFanControl.PWMDuty80PStartDelay, "ms")
setHWDutyCycle(100.0)
time.Sleep(time.Duration(myFanControl.PWMDuty80PStartDelay) * time.Millisecond)
}
var fanRequiredDuty float64 = 0 // The duty cycle required by the fan
if (pidValueOut > 5.0 || lastPWMControlValue != 0.0) {
lastPWMControlValue = pidValueOut
fanRequiredDuty = dutyCycleToFan(pidValueOut)
} else {
lastPWMControlValue = 0
if (alwaysOn) {
fanRequiredDuty = dutyCycleToFan(1)
} else {
fanRequiredDuty = 0.0
}
}
setHWDutyCycle(fanRequiredDuty)
// log.Println("Temp:", myFanControl.TempCurrent,
// "Current PWM:", myFanControl.PWMDutyCurrent)
select {
case <-updateControlDelay.C:
break;
case <-configChan:
setFanFrequency(myFanControl.PWMFrequency * 100)
pidControl.Set(myFanControl.TempTarget)
}
}
// Default to "ON" when we bail out
pin.DutyCycle(pwmDutyMax, pwmDutyMax)
}
// Service has embedded daemon
type Service struct {
daemon.Daemon
}
// Manage by daemon commands or run the daemon
func (service *Service) Manage() (string, error) {
// initialize defaults or from settings
readSettings()
// potentially override from command line
tempTarget := flag.Float64("temp", myFanControl.TempTarget, "Target CPU Temperature, degrees C")
dutyMin := flag.Int("minduty", int(myFanControl.PWMDutyMin), "Minimum PWM duty cycle")
frequency := flag.Int("frequency", int(myFanControl.PWMFrequency), "PWM Frequency")
pin := flag.Int("pin", myFanControl.PWMPin, "PWM pin (BCM numbering)")
flag.Parse()
usage := "Usage: " + name + " install | remove | start | stop | status"
// if received any kind of command, do it
if flag.NArg() > 0 {
command := os.Args[flag.NFlag()+1]
switch command {
case "install":
return service.Install()
case "remove":
return service.Remove()
case "start":
return service.Start()
case "stop":
return service.Stop()
case "status":
return service.Status()
default:
return usage, nil
}
}
myFanControl.TempTarget = *tempTarget
myFanControl.PWMDutyMin = uint32(*dutyMin)
myFanControl.PWMFrequency = uint32(*frequency)
myFanControl.PWMPin = *pin
go fanControl()
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
http.HandleFunc("/", handleStatusRequest)
http.Handle("/metrics", promhttp.Handler())
go http.ListenAndServe(addr, nil)
// interrupt by system signal
for {
killSignal := <-interrupt
log.Println("Got signal:", killSignal)
if killSignal == syscall.SIGINT {
return "Daemon was interrupted by system signal", nil
} else if (killSignal == syscall.SIGUSR1) {
readSettings()
configChan<-true
} else {
return "Daemon was killed", nil
}
}
return "", nil
}
func readSettings() {
myFanControl.PWMDutyMin = defaultPwmDutyMin
myFanControl.TempTarget = defaultTempTarget
myFanControl.PWMFrequency = defaultPwmFrequency
myFanControl.PWMPin = defaultPin
myFanControl.PWMDuty80PStartDelay = PWMDuty80PStartDelay
fd, err := os.Open(configLocation)
if err != nil {
log.Printf("can't read settings %s: %s\n", configLocation, err.Error())
return
}
defer fd.Close()
buf := make([]byte, 4096)
count, err := fd.Read(buf)
if err != nil {
log.Printf("can't read settings %s: %s\n", configLocation, err.Error())
return
}
err = json.Unmarshal(buf[0:count], &myFanControl)
if err != nil {
log.Printf("can't read settings %s: %s\n", configLocation, err.Error())
return
}
log.Printf("read in settings.\n")
}
func handleStatusRequest(w http.ResponseWriter, r *http.Request) {
statusJSON, _ := json.Marshal(&myFanControl)
w.Write(statusJSON)
}
func init() {
stdlog = log.New(os.Stdout, "", 0)
errlog = log.New(os.Stderr, "", 0)
}
func main() {
srv, err := daemon.New(name, description, daemon.SystemDaemon)
if err != nil {
errlog.Println("Error: ", err)
os.Exit(1)
}
service := &Service{srv}
status, err := service.Manage()
if err != nil {
errlog.Println(status, "\nError: ", err)
os.Exit(1)
}
fmt.Println(status)
}