Created
May 1, 2022 21:34
-
-
Save xrstf/9315797253b46efe658c4516a27738e6 to your computer and use it in GitHub Desktop.
BME280 RPi Fan Controller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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