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
|
2022-03-12 19:51:15 +00:00
|
|
|
isPaused bool
|
2021-08-30 20:46:38 +00:00
|
|
|
|
|
|
|
// Current temperature readings.
|
2022-07-25 20:55:24 +00:00
|
|
|
ambientTemperature float64
|
|
|
|
chamberTemperature float64
|
|
|
|
wortTemperature float64
|
|
|
|
lastTemperatureUpdate time.Time
|
2021-08-30 20:46:38 +00:00
|
|
|
|
2022-03-12 19:51:15 +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),
|
2022-03-12 19:51:15 +00:00
|
|
|
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
|
2022-07-25 20:55:24 +00:00
|
|
|
p.lastTemperatureUpdate = time.Now()
|
2021-08-30 20:46:38 +00:00
|
|
|
|
2022-03-12 19:51:15 +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()
|
2022-03-12 19:51:15 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-12 19:51:15 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-03-12 19:51:15 +00:00
|
|
|
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
|
|
|
|
2022-07-25 20:55: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
|
2022-07-25 21:58:39 +00:00
|
|
|
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
|
2022-07-25 21:58:39 +00:00
|
|
|
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
|
2022-03-06 17:48:19 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-07-25 20:55:24 +00:00
|
|
|
// 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
|
|
|
}
|