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 }