fermentord/internal/controllers/chamber.go

202 lines
5.1 KiB
Go

package controllers
import (
"context"
"log"
"sync"
"time"
"git.joco.dk/sng/fermentord/internal/configuration"
"git.joco.dk/sng/fermentord/pkg/temperature"
"github.com/getsentry/sentry-go"
)
type ChamberState int
const (
ChamberStateIdle ChamberState = iota
ChamberStateCooling
ChamberStateHeating
)
var ChamberStateMap = map[ChamberState]string{
ChamberStateIdle: "IDLE",
ChamberStateCooling: "COOLING",
ChamberStateHeating: "HEATING",
}
type ChamberController struct {
ConfigUpdates chan configuration.Configuration
config configuration.Configuration
// Current state.
chamberState ChamberState
lastChamberStateChange time.Time
lastCoolerStateChange time.Time
C chan ChamberState
isPaused bool
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
chTemp chan temperature.TemperatureReading
chPause chan bool
pid *PIDController
hub *sentry.Hub
}
func NewChamberController(config configuration.Configuration) *ChamberController {
return &ChamberController{
C: make(chan ChamberState),
config: config,
pid: NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd),
chTemp: make(chan temperature.TemperatureReading),
chPause: make(chan bool, 1),
chamberState: ChamberStateIdle,
ConfigUpdates: make(chan configuration.Configuration, 1),
hub: sentry.CurrentHub().Clone(),
}
}
func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
defer p.hub.Flush(10 * time.Second)
defer wg.Done()
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
state := p.computeChamberState()
p.setChamberState(state)
case t := <-p.chTemp:
p.ambientTemperature = t.Ambient
p.chamberTemperature = t.Chamber
p.wortTemperature = t.Wort
case pause := <-p.chPause:
p.setPause(pause)
case c := <-p.ConfigUpdates:
p.config = c
case <-ctx.Done():
ticker.Stop()
p.isPaused = false
// TODO Check if the line below blocks on shutdown
p.setChamberState(ChamberStateIdle)
return
}
}
}
func (p *ChamberController) SetTemperature(t temperature.TemperatureReading) {
p.chTemp <- t
}
// Pause is a thread-safe way to pause the controller.
func (p *ChamberController) Pause() {
p.chPause <- true
}
// Resume is a thread-safe way to resume the controller.
func (p *ChamberController) Resume() {
p.chPause <- false
}
func (p *ChamberController) setPause(pause bool) {
if pause {
p.setChamberState(ChamberStateIdle)
p.isPaused = true
} else {
p.isPaused = false
state := p.computeChamberState()
p.setChamberState(state)
}
}
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return
}
if p.isPaused {
return
}
log.Printf("State changed from %v to %v", p.chamberState, state)
if p.chamberState == ChamberStateCooling || state == ChamberStateCooling {
p.lastCoolerStateChange = time.Now()
}
p.chamberState = state
p.lastChamberStateChange = time.Now()
p.C <- state
}
func (p *ChamberController) computeChamberState() ChamberState {
offset := p.pid.Compute(p.chamberTemperature, p.config.FermentationTemperature)
chamberTargetTemp := p.config.FermentationTemperature + offset
runtimeSecs := time.Since(p.lastChamberStateChange).Seconds()
if p.chamberState == ChamberStateCooling {
// Ensure compressor min. runtime
if runtimeSecs < p.config.Limits.MinCoolerRuntimeSecs {
return p.chamberState
}
// Limit compressor runtime
if runtimeSecs > p.config.Limits.MaxCoolerRuntimeSecs {
return ChamberStateIdle
}
// Limit chamber min. temp.
if p.chamberTemperature < p.config.Limits.MinChamberTemperature {
return ChamberStateIdle
}
// Limit chamber max. temp.
if p.chamberTemperature > p.config.Limits.MaxChamberTemperature {
return ChamberStateIdle
}
}
var next ChamberState
heater := p.chamberTemperature < p.config.FermentationTemperature-p.config.DeltaTemperatureHeat &&
p.chamberTemperature < chamberTargetTemp-p.config.DeltaTemperatureHeat
cooler := p.chamberTemperature > p.config.FermentationTemperature+p.config.DeltaTemperatureCool &&
p.chamberTemperature > chamberTargetTemp+p.config.DeltaTemperatureCool
if cooler && heater {
// This should not happen!
log.Print("Trying to set cooler and heater on at the same time. This should NOT happen! Setting IDLE mode.")
p.hub.CaptureMessage("Trying to set cooler and heater on at the same time")
next = ChamberStateIdle
} else if !cooler && !heater {
next = ChamberStateIdle
} else if cooler {
next = ChamberStateCooling
} else if heater {
next = ChamberStateHeating
}
// Ensure compressor cooldown
if p.chamberState != ChamberStateCooling && next == ChamberStateCooling &&
time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.MinCoolerCooldownSecs {
return ChamberStateIdle
}
// Ensure that heater wont run just after cooling cycle
if p.chamberState != ChamberStateHeating && next == ChamberStateHeating && time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.HeaterGraceTimeSecs {
return ChamberStateIdle
}
return next
}