Add display code

This commit is contained in:
Søren Rasmussen 2022-03-15 21:07:53 +01:00
parent 7adca39509
commit 37ffb95543
10 changed files with 271 additions and 89 deletions

View file

@ -13,6 +13,7 @@ import (
"git.joco.dk/sng/fermentord/internal/controllers"
"git.joco.dk/sng/fermentord/internal/dwingest"
"git.joco.dk/sng/fermentord/internal/hw"
"git.joco.dk/sng/fermentord/internal/lcd"
"git.joco.dk/sng/fermentord/internal/metrics"
"git.joco.dk/sng/fermentord/pkg/daemon"
"git.joco.dk/sng/fermentord/pkg/temperature"
@ -29,6 +30,14 @@ func mainLoop(ctx context.Context, wg *sync.WaitGroup, js nats.JetStream, config
defer hub.Flush(10 * time.Second)
defer wg.Done()
// Display
display, err := lcd.NewLCD()
if err != nil {
hub.CaptureException(err)
log.Fatal(err)
}
defer display.Close()
// Controller
ctrl := controllers.NewChamberController("Chamber 1", *config)
@ -36,11 +45,17 @@ func mainLoop(ctx context.Context, wg *sync.WaitGroup, js nats.JetStream, config
ingest := dwingest.NewDWIngest()
// Configuration reload
loadConfiguration := func() {
temperature.ConfigUpdate <- *config
ctrl.ConfigUpdates <- *config
display.SetSetpointTemp(config.FermentationTemperature)
}
viper.OnConfigChange(func(in fsnotify.Event) {
controllers.ReloadConfiguration(config, ctrl)
loadConfiguration()
})
loadConfiguration()
viper.WatchConfig()
controllers.ReloadConfiguration(config, ctrl)
gpio, err := hw.NewGpio()
if err != nil {
@ -49,7 +64,8 @@ func mainLoop(ctx context.Context, wg *sync.WaitGroup, js nats.JetStream, config
}
defer gpio.Close()
wg.Add(4)
wg.Add(5)
go display.Run(ctx, wg)
go ctrl.Run(ctx, wg)
go ingest.Run(ctx, wg, js, config)
go temperature.PollSensors(ctx, wg, 1*time.Second, config.Sensors.Weight)
@ -57,13 +73,15 @@ func mainLoop(ctx context.Context, wg *sync.WaitGroup, js nats.JetStream, config
for {
select {
case reading := <-temperature.C:
case reading := <-temperature.Reading:
ctrl.SetTemperature(reading)
ingest.AddReading(reading)
display.SetTemperature(reading)
case state := <-ctrl.C:
gpioSetState(state, gpio)
ingest.AddState(state)
display.SetState(state)
case <-ctx.Done():
return

5
go.mod
View file

@ -4,6 +4,8 @@ go 1.18
require (
github.com/JuulLabs-OSS/ble v0.0.0-20200716215611-d4fcc9d598bb
github.com/d2r2/go-hd44780 v0.0.0-20181002113701-74cc28c83a3e
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc
github.com/fsnotify/fsnotify v1.5.4
github.com/getsentry/sentry-go v0.13.0
github.com/nats-io/nats.go v1.15.0
@ -17,6 +19,8 @@ require (
github.com/JuulLabs-OSS/cbgo v0.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
@ -30,6 +34,7 @@ require (
github.com/nats-io/nats-server/v2 v2.7.1 // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect

8
go.sum
View file

@ -63,6 +63,12 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/d2r2/go-hd44780 v0.0.0-20181002113701-74cc28c83a3e h1:3gLJWdofXjBoecDb9e+giWp77saiF6r2Mtu+edWCksY=
github.com/d2r2/go-hd44780 v0.0.0-20181002113701-74cc28c83a3e/go.mod h1:IruYZr0O1UbQs3rV5N2WPM8CpaT5rRgvPPzksu1+N6o=
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc h1:HLRSIWzUGMLCq4ldt0W1GLs3nnAxa5EGoP+9qHgh6j0=
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc/go.mod h1:AwxDPnsgIpy47jbGXZHA9Rv7pDkOJvQbezPuK1Y+nNk=
github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22 h1:nO+SY4KOMsF/LsZ5EtbSKhiT3M6sv/igo2PEru/xEHI=
github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22/go.mod h1:eSx+YfcVy5vCjRZBNIhpIpfCGFMQ6XSOSQkDk7+VCpg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -211,6 +217,8 @@ github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=

View file

@ -2,7 +2,6 @@ package controllers
import (
"context"
"fmt"
"log"
"sync"
"time"
@ -74,8 +73,10 @@ func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
state := p.computeChamberState()
p.setChamberState(state)
case temp := <-p.chTemp:
p.updateTemperature(temp)
case t := <-p.chTemp:
p.ambientTemperature = t.Ambient
p.chamberTemperature = t.Chamber
p.wortTemperature = t.Wort
case c := <-p.ConfigUpdates:
p.config = c
@ -95,23 +96,6 @@ func (p *ChamberController) SetTemperature(t temperature.TemperatureReading) {
p.chTemp <- t
}
func (p *ChamberController) updateTemperature(t temperature.TemperatureReading) {
switch t.Sensor {
case p.config.Sensors.Ambient:
p.ambientTemperature = t.Degrees
case p.config.Sensors.Chamber:
p.chamberTemperature = t.Degrees
case p.config.Sensors.Wort:
p.wortTemperature = t.Degrees
default:
log.Printf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.Degrees)
p.hub.CaptureMessage(fmt.Sprintf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.Degrees))
}
}
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return

View file

@ -1,20 +0,0 @@
package controllers
import (
"log"
"git.joco.dk/sng/fermentord/internal/configuration"
"git.joco.dk/sng/fermentord/pkg/temperature"
)
func ReloadConfiguration(config *configuration.Configuration, ctrl *ChamberController) {
log.Printf("Reloading configuration")
temperature.ConfigUpdates <- []string{
config.Sensors.Ambient,
config.Sensors.Chamber,
config.Sensors.Wort,
}
ctrl.ConfigUpdates <- *config
}

146
internal/lcd/hd44780.go Normal file
View file

@ -0,0 +1,146 @@
package lcd
import (
"context"
"fmt"
"sync"
"git.joco.dk/sng/fermentord/internal/controllers"
"git.joco.dk/sng/fermentord/pkg/temperature"
device "github.com/d2r2/go-hd44780"
"github.com/d2r2/go-i2c"
)
type LCD struct {
ambient, chamber, setpoint, wort float64
state string
bus *i2c.I2C
lcd *device.Lcd
l1, l2 string
chTemp chan temperature.TemperatureReading
chState chan controllers.ChamberState
chSetpoint chan float64
}
func NewLCD() (*LCD, error) {
var err error
p := &LCD{
chTemp: make(chan temperature.TemperatureReading, 10),
chState: make(chan controllers.ChamberState, 10),
chSetpoint: make(chan float64, 10),
}
p.bus, err = i2c.NewI2C(0x27, 2)
if err != nil {
return nil, err
}
p.lcd, err = device.NewLcd(p.bus, device.LCD_16x2)
if err != nil {
p.bus.Close()
return nil, err
}
err = p.lcd.BacklightOn()
if err != nil {
p.bus.Close()
return nil, err
}
err = p.lcd.ShowMessage("Fermentor", device.SHOW_LINE_1|device.SHOW_BLANK_PADDING)
if err != nil {
p.bus.Close()
return nil, err
}
err = p.lcd.ShowMessage("Initializing", device.SHOW_LINE_2|device.SHOW_BLANK_PADDING)
if err != nil {
p.bus.Close()
return nil, err
}
return p, nil
}
func (p *LCD) Close() error {
return p.bus.Close()
}
func (p *LCD) SetTemperature(t temperature.TemperatureReading) {
p.chTemp <- t
}
func (p *LCD) SetSetpointTemp(t float64) {
p.chSetpoint <- t
}
func (p *LCD) SetState(state controllers.ChamberState) {
p.chState <- state
}
func (p *LCD) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case t := <-p.chTemp:
p.ambient = t.Ambient
p.chamber = t.Chamber
p.wort = t.Wort
p.update()
case state := <-p.chState:
switch state {
case controllers.ChamberStateIdle:
p.state = "Id"
case controllers.ChamberStateHeating:
p.state = "He"
case controllers.ChamberStateCooling:
p.state = "Co"
}
p.update()
case t := <-p.chSetpoint:
p.setpoint = t
p.update()
case <-ctx.Done():
p.lcd.ShowMessage("Fermentor", device.SHOW_LINE_1|device.SHOW_BLANK_PADDING)
p.lcd.ShowMessage("Shutting down", device.SHOW_LINE_2|device.SHOW_BLANK_PADDING)
close(p.chSetpoint)
close(p.chState)
close(p.chTemp)
return
}
}
}
func (p *LCD) update() error {
l1 := fmt.Sprintf("W:%4.1f C:%4.1f %s", p.wort, p.chamber, p.state)
l2 := fmt.Sprintf("S:%4.1f A:%4.1f", p.ambient, p.setpoint)
if l1 != p.l1 {
if err := p.lcd.ShowMessage(l1, device.SHOW_LINE_1|device.SHOW_BLANK_PADDING); err != nil {
return err
}
p.l1 = l1
}
if l2 != p.l2 {
if err := p.lcd.ShowMessage(l2, device.SHOW_LINE_2|device.SHOW_BLANK_PADDING); err != nil {
return err
}
p.l2 = l2
}
return nil
}

View file

@ -14,6 +14,7 @@ import (
"sync"
"time"
"git.joco.dk/sng/fermentord/internal/configuration"
"git.joco.dk/sng/fermentord/internal/metrics"
"github.com/getsentry/sentry-go"
)
@ -21,6 +22,8 @@ import (
type BulkReadBusState int
const (
NaN = -999
BulkReadIdle BulkReadBusState = 0
BulkReadBusy BulkReadBusState = -1
BulkReadReady BulkReadBusState = 1
@ -35,23 +38,26 @@ var (
)
var (
// C receives the temperature readings
C chan TemperatureReading
// Reading receives the temperature readings
Reading chan TemperatureReading
ConfigUpdate chan configuration.Configuration
// ConfigUpdates receives the list of sensors to read
ConfigUpdates chan []string
sensors []string
accErrTrigger int
accErrSensor map[string]int
filters map[string]*EWMAFilter
sensors []Sensor
)
func init() {
sensors = make([]string, 0)
filters = make(map[string]*EWMAFilter)
C = make(chan TemperatureReading, 30)
ConfigUpdates = make(chan []string, 1)
accErrSensor = make(map[string]int)
Reading = make(chan TemperatureReading, 30)
ConfigUpdate = make(chan configuration.Configuration)
sensors = make([]Sensor, 0)
}
func configure(config configuration.Configuration) {
sensors = []Sensor{
NewSensor("Ambient", config.Sensors.Ambient, config.Sensors.Weight),
NewSensor("Chamber", config.Sensors.Chamber, config.Sensors.Weight),
NewSensor("Wort", config.Sensors.Wort, config.Sensors.Weight),
}
}
func PollSensors(ctx context.Context, wg *sync.WaitGroup, readingInterval time.Duration, filterWeight float64) {
@ -76,12 +82,6 @@ func PollSensors(ctx context.Context, wg *sync.WaitGroup, readingInterval time.D
log.Fatal("Thermal bulk read failed 60 times in a row -- terminating")
}
for _, sensor := range sensors {
if accErrSensor[sensor] > 60 {
log.Fatalf("Thermal sensor read of %v failed 60 times in a row -- terminating", sensor)
}
}
// Trigger a bulk read to start conversion on all sensors.
if err := triggerBulkRead(); err != nil {
accErrTrigger++
@ -118,52 +118,57 @@ func PollSensors(ctx context.Context, wg *sync.WaitGroup, readingInterval time.D
readSensors(hub)
}
case c := <-ConfigUpdates:
sensors = c
for k := range filters {
delete(filters, k)
}
for _, x := range sensors {
filters[x] = NewEWMAFilter(filterWeight)
}
case c := <-ConfigUpdate:
configure(c)
case <-ctx.Done():
close(C)
close(Reading)
return
}
}
}
func readSensors(hub *sentry.Hub) {
r := TemperatureReading{
Time: time.Now(),
Ambient: NaN,
Chamber: NaN,
Wort: NaN,
}
for _, sensor := range sensors {
t, err := read(sensor)
t, err := read(sensor.Path)
if err != nil {
accErrSensor[sensor]++
sensor.Fail()
hub.CaptureException(err)
log.Printf("Error reading temperature sensor %v: %v", sensor, err)
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor, "failed").Inc()
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "failed").Inc()
continue
}
accErrSensor[sensor] = 0
tw := filters[sensor].Update(float64(t) / 1000)
tw := sensor.Update(t)
r := TemperatureReading{
Time: time.Now(),
Sensor: sensor,
Degrees: tw,
switch sensor.Name {
case "Ambient":
r.Ambient = tw
case "Chamber":
r.Chamber = tw
case "Wort":
r.Wort = tw
}
// TODO Move metrics out
metrics.TemperatureSensorReadingDegreesCelcius.
WithLabelValues(sensor).
WithLabelValues(sensor.Name).
Set(tw)
C <- r
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor, "ok").Inc()
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "ok").Inc()
}
Reading <- r
}
func triggerBulkRead() error {

View file

@ -8,8 +8,8 @@ type EWMAFilter struct {
isInitialized bool
}
func NewEWMAFilter(weight float64) *EWMAFilter {
return &EWMAFilter{
func NewEWMAFilter(weight float64) EWMAFilter {
return EWMAFilter{
weight: weight,
}
}

35
pkg/temperature/sensor.go Normal file
View file

@ -0,0 +1,35 @@
package temperature
import "log"
type Sensor struct {
Name string
Path string
Filter EWMAFilter
accErrors int
}
func NewSensor(name, path string, weight float64) Sensor {
return Sensor{
Name: name,
Path: path,
Filter: NewEWMAFilter(weight),
}
}
func (p *Sensor) Update(value int64) float64 {
p.accErrors = 0
degrees := float64(value) / 1000
return p.Filter.Update(degrees)
}
func (p *Sensor) Fail() error {
p.accErrors++
if p.accErrors > 60 {
log.Fatalf("Thermal sensor read of %v failed 60 times in a row -- terminating", p.Name)
}
return nil
}

View file

@ -3,7 +3,8 @@ package temperature
import "time"
type TemperatureReading struct {
Sensor string `json:"sensor"`
Time time.Time `json:"time"`
Degrees float64 `json:"degrees"`
Ambient float64 `json:"ambient"`
Chamber float64 `json:"chamber"`
Wort float64 `json:"wort"`
}