fermentord/internal/controllers/chamber.go

183 lines
4.7 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-03-05 19:42:17 +00:00
"fmt"
2022-02-18 20:59:32 +00:00
"log"
2021-08-30 20:46:38 +00:00
"sync"
"time"
2021-11-16 05:19:24 +00:00
"git.joco.dk/sng/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 {
2021-11-16 05:19:24 +00:00
ConfigUpdates chan ControllerConfig
config ControllerConfig
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
name string
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
2021-11-16 05:19:24 +00:00
chTemp <-chan temperature.TemperatureReading
pid *PIDController
2022-03-05 19:42:17 +00:00
hub *sentry.Hub
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
func NewChamberController(name string, config ControllerConfig, chTemp <-chan temperature.TemperatureReading) *ChamberController {
2021-08-30 20:46:38 +00:00
return &ChamberController{
2021-11-16 05:19:24 +00:00
C: make(chan ChamberState),
name: name,
config: config,
pid: NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd),
chTemp: chTemp,
chamberState: ChamberStateIdle,
ConfigUpdates: make(chan ControllerConfig, 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-05 19:42:17 +00:00
defer p.hub.Flush(10 * time.Second)
2022-03-06 22:13:00 +00:00
defer wg.Done()
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)
case temp := <-p.chTemp:
p.setTemperature(temp)
2021-08-30 20:46:38 +00:00
2021-11-16 05:19:24 +00:00
case c := <-p.ConfigUpdates:
p.config = c
2021-08-30 20:46:38 +00:00
2021-11-16 05:19:24 +00:00
case <-ctx.Done():
ticker.Stop()
p.setChamberState(ChamberStateIdle)
return
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
}
}
func (p *ChamberController) setTemperature(t temperature.TemperatureReading) {
switch t.Sensor {
case p.config.Sensor.Ambient:
p.ambientTemperature = t.Degrees()
case p.config.Sensor.Chamber:
p.chamberTemperature = t.Degrees()
case p.config.Sensor.Wort:
p.wortTemperature = t.Degrees()
default:
2022-02-18 20:59:32 +00:00
log.Printf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.MilliDegrees)
2022-03-05 19:42:17 +00:00
p.hub.CaptureMessage(fmt.Sprintf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.MilliDegrees))
2021-11-16 05:19:24 +00:00
}
}
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return
}
2022-02-18 20:59:32 +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-03-05 19:42:17 +00:00
offset := p.pid.Compute(p.chamberTemperature, p.config.FermentationTemperature)
2021-11-16 05:19:24 +00:00
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
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-03-05 19:42:17 +00:00
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
2021-11-16 05:19:24 +00:00
if cooler && heater {
// This should not happen!
2022-03-05 19:42:17 +00:00
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")
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
}
return next
2021-08-30 20:46:38 +00:00
}