fermentord/internal/controllers/chamber.go
2021-11-17 22:30:18 +01:00

172 lines
4.2 KiB
Go

package controllers
import (
"context"
"sync"
"time"
"git.joco.dk/sng/fermentord/pkg/temperature"
"github.com/rs/zerolog/log"
)
type ChamberState int
const (
ChamberStateIdle ChamberState = iota
ChamberStateCooling
ChamberStateHeating
)
type ChamberController struct {
ConfigUpdates chan ControllerConfig
config ControllerConfig
// Current state.
chamberState ChamberState
lastChamberStateChange time.Time
lastCoolerStateChange time.Time
C chan ChamberState
name string
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
chTemp <-chan temperature.TemperatureReading
pid *PIDController
}
func NewChamberController(name string, config ControllerConfig, chTemp <-chan temperature.TemperatureReading) *ChamberController {
return &ChamberController{
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),
}
}
func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
state := p.computeChamberState()
p.setChamberState(state)
case temp := <-p.chTemp:
p.setTemperature(temp)
case c := <-p.ConfigUpdates:
p.config = c
case <-ctx.Done():
ticker.Stop()
p.setChamberState(ChamberStateIdle)
return
}
}
}
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:
log.Warn().
Str("sensor", t.Sensor).
Int64("temp", t.MilliDegrees).
Msg("Unknown sensor")
}
}
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return
}
log.Info().
Int("from", int(p.chamberState)).
Int("to", int(state)).
Msgf("State changed")
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.wortTemperature, p.config.FermentationTemperature)
chamberTargetTemp := p.config.FermentationTemperature + offset
log.Debug().
Float64("amb_temp", p.ambientTemperature).
Float64("chamber_temp", p.chamberTemperature).
Float64("wort_temp", p.wortTemperature).
Float64("setpoint_temp", p.config.FermentationTemperature).
Float64("offset", offset).
Float64("chamber_target_temp", chamberTargetTemp).
Float64("kp", p.pid.kp).
Float64("ki", p.pid.ki).
Float64("kd", p.pid.kd).
Send()
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
}
}
var next ChamberState
heater := p.wortTemperature < p.config.FermentationTemperature-p.config.DeltaTemperature && p.chamberTemperature < chamberTargetTemp-p.config.DeltaTemperature
cooler := p.wortTemperature > p.config.FermentationTemperature+p.config.DeltaTemperature && p.chamberTemperature > chamberTargetTemp+p.config.DeltaTemperature
if cooler && heater {
// This should not happen!
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
}
return next
}