162 lines
4 KiB
Go
162 lines
4 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.joco.dk/sng/fermentord/pkg/temperature"
|
|
)
|
|
|
|
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 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.Printf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.MilliDegrees)
|
|
}
|
|
}
|
|
|
|
func (p *ChamberController) setChamberState(state ChamberState) {
|
|
if state == p.chamberState {
|
|
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.wortTemperature, 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
|
|
}
|
|
}
|
|
|
|
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!
|
|
log.Print("Trying to set cooler and heater on at the same time. 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
|
|
}
|