fermentord/pkg/temperature/ds18b20.go

227 lines
4.6 KiB
Go
Raw Normal View History

2021-08-30 20:46:38 +00:00
package temperature
2021-11-16 05:19:24 +00:00
// Kernel documentation: https://www.kernel.org/doc/html/latest/w1/slaves/w1_therm.html
2021-08-30 20:46:38 +00:00
import (
"context"
"errors"
"io/ioutil"
2022-02-18 20:59:32 +00:00
"log"
2021-11-16 05:19:24 +00:00
"os"
2022-02-18 20:59:32 +00:00
"path/filepath"
2021-08-30 20:46:38 +00:00
"strconv"
"strings"
"sync"
"time"
2022-03-15 20:07:53 +00:00
"git.joco.dk/sng/fermentord/internal/configuration"
2021-08-30 20:46:38 +00:00
"git.joco.dk/sng/fermentord/internal/metrics"
2021-11-16 05:19:24 +00:00
"github.com/getsentry/sentry-go"
2022-02-18 20:59:32 +00:00
)
type BulkReadBusState int
2022-02-18 20:59:32 +00:00
const (
2022-03-15 20:07:53 +00:00
NaN = -999
BulkReadIdle BulkReadBusState = 0
BulkReadBusy BulkReadBusState = -1
BulkReadReady BulkReadBusState = 1
2022-02-18 20:59:32 +00:00
sensorConversionTime = 750 * time.Millisecond
busPollDelay = 10 * time.Millisecond
2021-08-30 20:46:38 +00:00
)
var (
2021-11-16 05:19:24 +00:00
// ErrReadSensor indicates that the sensor could not be read
ErrReadSensor = errors.New("failed to read sensor")
)
2021-08-30 20:46:38 +00:00
2021-11-16 05:19:24 +00:00
var (
2022-03-15 20:07:53 +00:00
// Reading receives the temperature readings
Reading chan TemperatureReading
ConfigUpdate chan configuration.Configuration
2021-08-30 20:46:38 +00:00
accErrTrigger int
2022-03-15 20:07:53 +00:00
sensors []Sensor
2021-08-30 20:46:38 +00:00
)
func init() {
2022-03-15 20:07:53 +00:00
Reading = make(chan TemperatureReading, 30)
2022-07-23 23:16:40 +00:00
ConfigUpdate = make(chan configuration.Configuration, 1)
2022-03-15 20:07:53 +00:00
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),
}
2021-08-30 20:46:38 +00:00
}
2022-03-11 19:46:18 +00:00
func PollSensors(ctx context.Context, wg *sync.WaitGroup, readingInterval time.Duration, filterWeight float64) {
2022-03-05 19:42:17 +00:00
hub := sentry.CurrentHub().Clone()
defer hub.Flush(10 * time.Second)
2021-08-30 20:46:38 +00:00
defer wg.Done()
2022-02-18 20:59:32 +00:00
if readingInterval < sensorConversionTime {
log.Fatalf("Reading interval must be at least %v ms.", sensorConversionTime)
2021-11-16 05:19:24 +00:00
}
ticker := time.NewTicker(readingInterval)
defer ticker.Stop()
2021-08-30 20:46:38 +00:00
for {
work_loop:
2021-08-30 20:46:38 +00:00
select {
case <-ticker.C:
2022-03-05 19:42:17 +00:00
start := time.Now()
if accErrTrigger > 60 {
log.Fatal("Thermal bulk read failed 60 times in a row -- terminating")
}
2022-02-18 20:59:32 +00:00
// Trigger a bulk read to start conversion on all sensors.
if err := triggerBulkRead(); err != nil {
accErrTrigger++
2022-03-05 19:42:17 +00:00
hub.CaptureException(err)
2022-02-18 20:59:32 +00:00
log.Print(err)
break
}
2021-11-16 05:19:24 +00:00
accErrTrigger = 0
2022-03-05 19:42:17 +00:00
// Ensure that we wait for sensors to convert data.
deltaSleep := sensorConversionTime - time.Since(start)
if deltaSleep > 0 {
// Wait for sensors to convert data.
time.Sleep(deltaSleep)
}
2022-02-18 20:59:32 +00:00
// Poll bus state
poll_bus:
state, err := pollBusState()
if err != nil {
accErrTrigger++
hub.CaptureException(err)
log.Print(err)
break work_loop
}
switch state {
case BulkReadBusy:
time.Sleep(busPollDelay)
goto poll_bus
case BulkReadIdle, BulkReadReady:
readSensors(hub)
}
2021-08-30 20:46:38 +00:00
2022-03-15 20:07:53 +00:00
case c := <-ConfigUpdate:
configure(c)
2022-03-11 19:46:18 +00:00
2021-08-30 20:46:38 +00:00
case <-ctx.Done():
return
}
}
}
2022-03-06 21:52:21 +00:00
func readSensors(hub *sentry.Hub) {
2022-03-15 20:07:53 +00:00
r := TemperatureReading{
Time: time.Now(),
Ambient: NaN,
Chamber: NaN,
Wort: NaN,
}
2021-08-30 20:46:38 +00:00
for _, sensor := range sensors {
2022-03-15 20:07:53 +00:00
t, err := read(sensor.Path)
2021-08-30 20:46:38 +00:00
if err != nil {
2022-03-15 20:07:53 +00:00
sensor.Fail()
2022-03-05 19:42:17 +00:00
hub.CaptureException(err)
2022-02-18 20:59:32 +00:00
log.Printf("Error reading temperature sensor %v: %v", sensor, err)
2022-03-15 20:07:53 +00:00
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "failed").Inc()
2021-11-16 05:19:24 +00:00
continue
2021-08-30 20:46:38 +00:00
}
2022-03-15 20:07:53 +00:00
tw := sensor.Update(t)
switch sensor.Name {
case "Ambient":
r.Ambient = tw
2022-03-15 20:07:53 +00:00
case "Chamber":
r.Chamber = tw
case "Wort":
r.Wort = tw
2022-02-18 20:59:32 +00:00
}
2022-03-15 20:07:53 +00:00
// TODO Move metrics out
2021-08-30 20:46:38 +00:00
metrics.TemperatureSensorReadingDegreesCelcius.
2022-03-15 20:07:53 +00:00
WithLabelValues(sensor.Name).
2022-03-11 19:46:18 +00:00
Set(tw)
2021-08-30 20:46:38 +00:00
2022-03-15 20:07:53 +00:00
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "ok").Inc()
2021-08-30 20:46:38 +00:00
}
2022-03-15 20:07:53 +00:00
select {
case Reading <- r:
default:
}
2021-08-30 20:46:38 +00:00
}
2021-11-16 05:19:24 +00:00
func triggerBulkRead() error {
2022-03-05 19:42:17 +00:00
f, err := os.OpenFile("/sys/bus/w1/devices/w1_bus_master1/therm_bulk_read", os.O_WRONLY, 0644)
2021-11-16 05:19:24 +00:00
if err != nil {
return err
}
defer f.Close()
2022-03-06 21:52:21 +00:00
_, err = f.WriteString("trigger\n")
2021-11-16 05:19:24 +00:00
return err
}
func pollBusState() (BulkReadBusState, error) {
b, err := ioutil.ReadFile("/sys/bus/w1/devices/w1_bus_master1/therm_bulk_read")
if err != nil {
return BulkReadIdle, err
}
i, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
return BulkReadIdle, err
}
return BulkReadBusState(i), nil
}
2021-08-30 20:46:38 +00:00
// read returns the temperature of the specified sensor in millidegrees celcius.
func read(sensor string) (int64, error) {
2022-03-05 19:42:17 +00:00
path := filepath.Join("/sys/bus/w1/devices", sensor, "w1_slave")
2021-08-30 20:46:38 +00:00
data, err := ioutil.ReadFile(path)
if err != nil {
return 0.0, err
}
raw := string(data)
if !strings.Contains(raw, " YES") {
return 0.0, ErrReadSensor
}
i := strings.LastIndex(raw, "t=")
if i == -1 {
return 0.0, ErrReadSensor
}
c, err := strconv.ParseInt(raw[i+2:len(raw)-1], 10, 64)
if err != nil {
return 0.0, err
}
return c, nil
}