fermentord/internal/controllers/chamber.go

219 lines
5.6 KiB
Go
Raw Normal View History

2021-08-30 20:46:38 +00:00
package controllers
import (
2021-11-16 05:19:24 +00:00
"context"
2022-07-25 08:55:25 +00:00
"fmt"
2022-02-18 20:59:32 +00:00
"log"
2021-08-30 20:46:38 +00:00
"sync"
"time"
2024-06-15 14:31:02 +00:00
"git.joco.dk/snr/fermentord/internal/configuration"
"git.joco.dk/snr/fermentord/pkg/temperature"
2022-03-05 19:42:17 +00:00
"github.com/getsentry/sentry-go"
2021-08-30 20:46:38 +00:00
)
type ChamberState int
const (
ChamberStateIdle ChamberState = iota
ChamberStateCooling
ChamberStateHeating
)
2022-02-18 20:59:32 +00:00
var ChamberStateMap = map[ChamberState]string{
ChamberStateIdle: "IDLE",
ChamberStateCooling: "COOLING",
ChamberStateHeating: "HEATING",
}
2021-08-30 20:46:38 +00:00
type ChamberController struct {
2022-03-15 16:20:47 +00:00
ConfigUpdates chan configuration.Configuration
config configuration.Configuration
2021-08-30 20:46:38 +00:00
// Current state.
chamberState ChamberState
lastChamberStateChange time.Time
2021-11-16 05:19:24 +00:00
lastCoolerStateChange time.Time
2021-08-30 20:46:38 +00:00
C chan ChamberState
isPaused bool
2021-08-30 20:46:38 +00:00
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
lastTemperatureUpdate time.Time
2021-08-30 20:46:38 +00:00
chTemp chan temperature.TemperatureReading
chPause chan bool
2021-11-16 05:19:24 +00:00
pid *PIDController
2022-03-05 19:42:17 +00:00
hub *sentry.Hub
2021-08-30 20:46:38 +00:00
}
2022-07-24 06:25:02 +00:00
func NewChamberController(config configuration.Configuration) *ChamberController {
2021-08-30 20:46:38 +00:00
return &ChamberController{
2021-11-16 05:19:24 +00:00
C: make(chan ChamberState),
config: config,
pid: NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd),
2022-07-19 09:24:39 +00:00
chTemp: make(chan temperature.TemperatureReading),
chPause: make(chan bool, 1),
2021-11-16 05:19:24 +00:00
chamberState: ChamberStateIdle,
2022-03-15 16:20:47 +00:00
ConfigUpdates: make(chan configuration.Configuration, 1),
2022-03-05 19:42:17 +00:00
hub: sentry.CurrentHub().Clone(),
2021-08-30 20:46:38 +00:00
}
}
2021-11-16 05:19:24 +00:00
func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
2022-03-06 22:13:00 +00:00
defer wg.Done()
2022-07-25 08:55:25 +00:00
defer p.hub.Flush(10 * time.Second)
2021-11-16 05:19:24 +00:00
ticker := time.NewTicker(1 * time.Second)
2021-08-30 20:46:38 +00:00
for {
2021-11-16 05:19:24 +00:00
select {
case <-ticker.C:
state := p.computeChamberState()
p.setChamberState(state)
2022-03-15 20:07:53 +00:00
case t := <-p.chTemp:
p.ambientTemperature = t.Ambient
p.chamberTemperature = t.Chamber
p.wortTemperature = t.Wort
p.lastTemperatureUpdate = time.Now()
2021-08-30 20:46:38 +00:00
case pause := <-p.chPause:
p.setPause(pause)
2021-11-16 05:19:24 +00:00
case c := <-p.ConfigUpdates:
p.config = c
2022-08-02 05:26:41 +00:00
log.Printf("Fermentation temperature set to %v", p.config.FermentationTemperature)
2021-08-30 20:46:38 +00:00
2021-11-16 05:19:24 +00:00
case <-ctx.Done():
ticker.Stop()
p.isPaused = false
2022-07-19 09:24:39 +00:00
2021-11-16 05:19:24 +00:00
return
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
}
}
2022-07-19 09:24:39 +00:00
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)
}
}
2021-11-16 05:19:24 +00:00
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return
}
if p.isPaused {
return
}
2022-07-25 07:35:02 +00:00
//log.Printf("State changed from %v to %v", p.chamberState, state)
2021-08-30 20:46:38 +00:00
2021-11-16 05:19:24 +00:00
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 {
2022-07-25 07:35:02 +00:00
//offset := p.pid.Compute(p.chamberTemperature, p.config.FermentationTemperature)
//chamberTargetTemp := p.config.FermentationTemperature + offset
2021-11-16 05:19:24 +00:00
// Stay in idle mode until first temperature update is received
if p.lastTemperatureUpdate.IsZero() {
return ChamberStateIdle
}
2021-11-16 05:19:24 +00:00
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
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
// Limit chamber min. temp.
if p.chamberTemperature < p.config.Limits.MinChamberTemperature {
return ChamberStateIdle
2021-08-30 20:46:38 +00:00
}
2022-03-05 19:42:17 +00:00
// Limit chamber max. temp.
if p.chamberTemperature > p.config.Limits.MaxChamberTemperature {
return ChamberStateIdle
}
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
var next ChamberState
heater := p.config.HeaterEnabled && p.chamberTemperature < p.config.FermentationTemperature-p.config.DeltaTemperatureHeat //&&
2022-07-25 07:35:02 +00:00
//p.chamberTemperature < chamberTargetTemp-p.config.DeltaTemperatureHeat
cooler := p.config.CoolerEnabled && p.chamberTemperature > p.config.FermentationTemperature+p.config.DeltaTemperatureCool //&&
2022-07-25 07:35:02 +00:00
//p.chamberTemperature > chamberTargetTemp+p.config.DeltaTemperatureCool
2021-11-16 05:19:24 +00:00
if cooler && heater {
2022-07-25 08:55:25 +00:00
// This should NOT happen!
err := fmt.Errorf("heater and cooler activated at same time")
p.hub.CaptureException(err)
log.Print(err)
2021-11-16 05:19:24 +00:00
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 {
2021-11-16 05:19:24 +00:00
return ChamberStateIdle
}
// Force idle mode when temperature updates are missing
if time.Since(p.lastTemperatureUpdate) > 1*time.Minute {
err := fmt.Errorf("no temperature update on controller for 1 minute - setting IDLE mode")
p.hub.CaptureException(err)
log.Print(err)
next = ChamberStateIdle
}
2021-11-16 05:19:24 +00:00
return next
2021-08-30 20:46:38 +00:00
}