Skip to content

Instantly share code, notes, and snippets.

@xrstf
Created May 1, 2022 21:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xrstf/9315797253b46efe658c4516a27738e6 to your computer and use it in GitHub Desktop.
Save xrstf/9315797253b46efe658c4516a27738e6 to your computer and use it in GitHub Desktop.
BME280 RPi Fan Controller
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strconv"
"time"
"github.com/maciej/bme280"
"github.com/spf13/pflag"
"github.com/stianeikeland/go-rpio/v4"
"golang.org/x/exp/io/i2c"
)
const (
speedSteps = 10
)
var (
// start with 100% speed and then slowly regulate down when appropriate
fanSpeed = speedSteps
)
type options struct {
temperatureOffset float64
targetTemperature float64
metricsAddress string
i2cDevice string
i2cAddress int // this is in hex before being parsed and translated to decimal
pwmPin int
pwmFrequency int
updateInterval time.Duration
}
func main() {
opt := options{
targetTemperature: 20,
updateInterval: 30 * time.Second,
i2cDevice: "/dev/i2c-1",
i2cAddress: 76,
pwmPin: 12,
pwmFrequency: 25000,
metricsAddress: "0.0.0.0:9090",
}
pflag.Float64VarP(&opt.targetTemperature, "target", "t", opt.targetTemperature, "temperature go aim for when controlling the fan speed")
pflag.Float64VarP(&opt.temperatureOffset, "temp-offset", "o", opt.temperatureOffset, "offset to compensate for temperature sensor drift")
pflag.DurationVarP(&opt.updateInterval, "interval", "i", opt.updateInterval, "how long to wait between temprature updates")
pflag.StringVar(&opt.i2cDevice, "i2c-device", opt.i2cDevice, "device path to the I²C bus")
pflag.IntVar(&opt.i2cAddress, "i2c-address", opt.i2cAddress, "I²C address of the sensor (usually 76 or 77 for BME280)")
pflag.IntVar(&opt.pwmPin, "pwm-pin", opt.pwmPin, "GPIO pin for outputting the PWM control signal")
pflag.IntVar(&opt.pwmFrequency, "pwm-frequency", opt.pwmFrequency, "PWM frequency")
pflag.StringVar(&opt.metricsAddress, "metrics-address", opt.metricsAddress, "HTTP endpoint to listen on and provide Prometheus metrics")
pflag.Parse()
if opt.updateInterval < time.Second {
log.Fatal("-interval cannot be smaller than 1s.")
}
parsed, err := strconv.ParseInt(fmt.Sprintf("%d", opt.i2cAddress), 16, 32)
if err != nil {
log.Fatalf("Invalid -i2c-address %q: %v", opt.i2cAddress, err)
}
opt.i2cAddress = int(parsed)
ctx, cancel := context.WithCancel(context.Background())
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt)
go func() {
<-c
cancel()
log.Println("Shutting down…")
<-c
log.Println("Exiting…")
os.Exit(1) // second signal. Exit directly.
}()
if opt.metricsAddress != "" {
setupMetrics(opt)
}
log.Println("Starting…")
if err := runApplication(ctx, opt); err != nil {
log.Fatalf("Error: %v", err)
}
log.Println("Tschüss!")
}
func runApplication(ctx context.Context, opt options) error {
sensor := getBME280(opt)
defer sensor.Close()
pwmPin := getPWMPin(opt)
defer rpio.Close()
ticker := time.NewTicker(opt.updateInterval)
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if err := work(ctx, sensor, pwmPin, opt); err != nil {
log.Printf("Error: %v", err)
currentHealth.Set(0)
} else {
currentHealth.Set(1)
}
}
}
return nil
}
func work(ctx context.Context, sensor *bme280.Driver, pwmPin rpio.Pin, opt options) error {
response, err := sensor.Read()
if err != nil {
return fmt.Errorf("failed to read sensor: %w", err)
}
temperature := response.Temperature + opt.temperatureOffset
oldSpeed := fanSpeed
if temperature > (opt.targetTemperature + 0.5) {
fanSpeed++
} else if temperature < (opt.targetTemperature - 0.5) {
fanSpeed--
}
if fanSpeed > speedSteps {
fanSpeed = speedSteps
} else if fanSpeed < 0 {
fanSpeed = 0
}
currentTemperature.Set(temperature)
currentHumidity.Set(response.Humidity)
currentFanSpeed.Set(float64(fanSpeed))
if fanSpeed != oldSpeed {
log.Printf("Changing fan speed from %d to %d (temperature is %.2F°C, aiming for %.1F°C)", oldSpeed, fanSpeed, temperature, opt.targetTemperature)
pwmPin.DutyCycle(uint32(fanSpeed), speedSteps)
}
return nil
}
func getBME280(opt options) *bme280.Driver {
device, err := i2c.Open(&i2c.Devfs{Dev: opt.i2cDevice}, opt.i2cAddress)
if err != nil {
log.Fatalf("Failed to open I²C device: %v", err)
}
driver := bme280.New(device)
err = driver.InitWith(bme280.ModeForced, bme280.Settings{
Filter: bme280.FilterOff,
Standby: bme280.StandByTime1000ms,
PressureOversampling: bme280.Oversampling16x,
TemperatureOversampling: bme280.Oversampling16x,
HumidityOversampling: bme280.Oversampling16x,
})
if err != nil {
log.Fatalf("Failed to initialize BME280 sensor: %v", err)
}
return driver
}
func getPWMPin(opt options) rpio.Pin {
err := rpio.Open()
if err != nil {
log.Fatalf("Failed to initialize GPIO system: %v", err)
}
pin := rpio.Pin(opt.pwmPin)
pin.Mode(rpio.Pwm)
pin.Freq(opt.pwmFrequency * speedSteps)
pin.DutyCycle(speedSteps, speedSteps)
return pin
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment