diff --git a/cmd/fermentord/main.go b/cmd/fermentord/main.go index 52a1c0d..0575add 100644 --- a/cmd/fermentord/main.go +++ b/cmd/fermentord/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -78,6 +79,35 @@ func mainLoop(ctx context.Context, wg *sync.WaitGroup, js nats.JetStream, config ingest.AddReading(reading) display.SetTemperature(reading) + case ev := <-gpio.C: + switch ev { + case hw.DoorClosed: + gpio.LightsOff() + gpio.StartFan() + ctrl.Resume() + + case hw.DoorOpened: + ctrl.Pause() + gpio.LightsOn() + gpio.StopFan() + } + + b, err := json.Marshal(map[string]interface{}{ + "event": ev, + }) + if err != nil { + hub.CaptureException(err) + log.Printf("Error marshaling JSON: %v", err) + break + } + + // Publish to NATS + _, err = js.Publish(config.NATS.Subject.Event, b) + if err != nil { + hub.CaptureException(err) + log.Printf("Error publishing to NATS: %v", err) + } + case state := <-ctrl.C: gpioSetState(state, gpio) ingest.AddState(state) diff --git a/internal/configuration/config.go b/internal/configuration/config.go index ca8eb75..ce8b39f 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -13,8 +13,9 @@ type Configuration struct { URL string `mapstructure:"url"` Stream string `mapstructure:"stream"` Subject struct { - Temp string `mapstructure:"temp"` + Event string `mapstructure:"event"` State string `mapstructure:"state"` + Temp string `mapstructure:"temp"` } `mapstructure:"subject"` } `mapstructure:"nats"` @@ -55,6 +56,7 @@ func LoadConfiguration() *Configuration { viper.SetDefault("http.port", 8000) viper.SetDefault("nats.stream", "FERMENTOR") + viper.SetDefault("nats.subject.event", "FERMENTOR.event") viper.SetDefault("nats.subject.state", "FERMENTOR.state") viper.SetDefault("nats.subject.temp", "FERMENTOR.temp") viper.SetDefault("nats.url", "nats.service.consul") diff --git a/internal/controllers/chamber.go b/internal/controllers/chamber.go index f87e802..bdc57e1 100644 --- a/internal/controllers/chamber.go +++ b/internal/controllers/chamber.go @@ -34,6 +34,7 @@ type ChamberController struct { lastChamberStateChange time.Time lastCoolerStateChange time.Time C chan ChamberState + isPaused bool name string @@ -42,7 +43,8 @@ type ChamberController struct { chamberTemperature float64 wortTemperature float64 - chTemp chan temperature.TemperatureReading + chTemp chan temperature.TemperatureReading + chPause chan bool pid *PIDController hub *sentry.Hub @@ -55,6 +57,7 @@ func NewChamberController(name string, config configuration.Configuration) *Cham config: config, pid: NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd), chTemp: make(chan temperature.TemperatureReading), + chPause: make(chan bool, 1), chamberState: ChamberStateIdle, ConfigUpdates: make(chan configuration.Configuration, 1), hub: sentry.CurrentHub().Clone(), @@ -78,11 +81,16 @@ func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) { p.chamberTemperature = t.Chamber p.wortTemperature = t.Wort + case pause := <-p.chPause: + p.setPause(pause) + case c := <-p.ConfigUpdates: p.config = c case <-ctx.Done(): ticker.Stop() + close(p.chPause) + p.isPaused = false p.setChamberState(ChamberStateIdle) close(p.chTemp) close(p.C) @@ -96,12 +104,37 @@ 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) + } +} + func (p *ChamberController) setChamberState(state ChamberState) { if state == p.chamberState { return } - //log.Printf("State changed from %v to %v", p.chamberState, state) + if p.isPaused { + return + } + + log.Printf("State changed from %v to %v", p.chamberState, state) if p.chamberState == ChamberStateCooling || state == ChamberStateCooling { p.lastCoolerStateChange = time.Now() diff --git a/internal/hw/gpio.go b/internal/hw/gpio.go index 5fe67a2..be2d6a1 100644 --- a/internal/hw/gpio.go +++ b/internal/hw/gpio.go @@ -1,38 +1,80 @@ package hw import ( + "time" + "github.com/warthog618/gpiod" ) const ( + pinDoorOpen = 17 + pinFanPower = 22 pinCoolerPower = 23 pinHeaterPower = 24 + pinLightsPower = 25 off = 0 on = 1 ) +type HWEvent int + +const ( + NoOp HWEvent = iota + DoorClosed + DoorOpened +) + type Gpio struct { chip *gpiod.Chip + doorOpen *gpiod.Line + lightsPower *gpiod.Line + fanPower *gpiod.Line coolerPower *gpiod.Line heaterPower *gpiod.Line + + C chan HWEvent } func NewGpio() (*Gpio, error) { var err error - p := &Gpio{} + p := &Gpio{ + C: make(chan HWEvent), + } p.chip, err = gpiod.NewChip("gpiochip0", gpiod.WithConsumer("fermentord")) if err != nil { return nil, err } - p.coolerPower, err = p.chip.RequestLine(pinCoolerPower, gpiod.AsOutput(1)) + p.doorOpen, err = p.chip.RequestLine( + pinDoorOpen, + gpiod.AsInput, + gpiod.LineBiasPullUp, + gpiod.WithDebounce(200*time.Millisecond), + gpiod.WithBothEdges, + gpiod.WithEventHandler(p.onDoorStateChanged), + ) if err != nil { return nil, err } - p.heaterPower, err = p.chip.RequestLine(pinHeaterPower, gpiod.AsOutput(1)) + p.lightsPower, err = p.chip.RequestLine(pinLightsPower, gpiod.AsOutput(0)) + if err != nil { + return nil, err + } + + p.fanPower, err = p.chip.RequestLine(pinFanPower, gpiod.AsOutput(0)) + if err != nil { + return nil, err + } + + p.coolerPower, err = p.chip.RequestLine(pinCoolerPower, gpiod.AsOutput(0)) + if err != nil { + return nil, err + } + + p.heaterPower, err = p.chip.RequestLine(pinHeaterPower, gpiod.AsOutput(0)) if err != nil { return nil, err } @@ -43,10 +85,17 @@ func NewGpio() (*Gpio, error) { func (p *Gpio) Close() { p.coolerPower.SetValue(off) p.heaterPower.SetValue(off) + p.fanPower.SetValue(off) + p.lightsPower.SetValue(off) p.heaterPower.Close() p.coolerPower.Close() + p.fanPower.Close() + p.lightsPower.Close() + p.doorOpen.Close() p.chip.Close() + + close(p.C) } func (p *Gpio) StartCooler() { @@ -64,3 +113,29 @@ func (p *Gpio) StartHeater() { func (p *Gpio) StopHeater() { p.heaterPower.SetValue(off) } + +func (p *Gpio) StartFan() { + p.fanPower.SetValue(on) +} + +func (p *Gpio) StopFan() { + p.fanPower.SetValue(off) +} + +func (p *Gpio) LightsOn() { + p.lightsPower.SetValue(on) +} + +func (p *Gpio) LightsOff() { + p.lightsPower.SetValue(off) +} + +func (p *Gpio) onDoorStateChanged(le gpiod.LineEvent) { + switch le.Type { + case gpiod.LineEventFallingEdge: + p.C <- DoorOpened + + case gpiod.LineEventRisingEdge: + p.C <- DoorClosed + } +}