274 lines
5.6 KiB
Go
274 lines
5.6 KiB
Go
package temperature
|
|
|
|
// Kernel documentation: https://www.kernel.org/doc/html/latest/w1/slaves/w1_therm.html
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.joco.dk/snr/fermentord/internal/configuration"
|
|
"git.joco.dk/snr/fermentord/internal/metrics"
|
|
"github.com/getsentry/sentry-go"
|
|
)
|
|
|
|
type BulkReadBusState int
|
|
|
|
const (
|
|
NaN = -999
|
|
|
|
BulkReadIdle BulkReadBusState = 0
|
|
BulkReadBusy BulkReadBusState = -1
|
|
BulkReadReady BulkReadBusState = 1
|
|
|
|
sensorConversionTime = 750 * time.Millisecond
|
|
busPollDelay = 10 * time.Millisecond
|
|
)
|
|
|
|
var (
|
|
// ErrReadSensor indicates that the sensor could not be read
|
|
ErrReadSensor = errors.New("failed to read sensor")
|
|
)
|
|
|
|
var (
|
|
// Reading receives the temperature readings
|
|
Reading chan TemperatureReading
|
|
ConfigUpdate chan configuration.Configuration
|
|
RequestReset chan bool
|
|
|
|
accErrTrigger int
|
|
sensors []Sensor
|
|
)
|
|
|
|
func init() {
|
|
Reading = make(chan TemperatureReading, 30)
|
|
ConfigUpdate = make(chan configuration.Configuration, 1)
|
|
RequestReset = make(chan bool)
|
|
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) {
|
|
defer wg.Done()
|
|
|
|
hub := sentry.CurrentHub().Clone()
|
|
defer hub.Flush(10 * time.Second)
|
|
|
|
if readingInterval < sensorConversionTime {
|
|
log.Fatalf("Reading interval must be at least %v ms.", sensorConversionTime)
|
|
}
|
|
|
|
ticker := time.NewTicker(readingInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
poll_bus:
|
|
state, err := pollBusState()
|
|
if err != nil {
|
|
accErrTrigger++
|
|
hub.CaptureException(err)
|
|
log.Print(err)
|
|
break
|
|
}
|
|
|
|
switch state {
|
|
case BulkReadBusy:
|
|
time.Sleep(busPollDelay)
|
|
goto poll_bus
|
|
|
|
case BulkReadIdle:
|
|
start := time.Now()
|
|
|
|
// Trigger a bulk read to start conversion on all sensors.
|
|
if err := triggerBulkRead(); err != nil {
|
|
accErrTrigger++
|
|
hub.CaptureException(err)
|
|
log.Print(err)
|
|
break
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
goto poll_bus
|
|
|
|
case BulkReadReady:
|
|
if !readSensors(hub) {
|
|
accErrTrigger++
|
|
} else {
|
|
accErrTrigger = 0
|
|
}
|
|
}
|
|
|
|
case c := <-ConfigUpdate:
|
|
configure(c)
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
if accErrTrigger > 60 {
|
|
select {
|
|
case RequestReset <- true:
|
|
log.Print("Thermal bulk read failed 60 times in a row -- bus reset")
|
|
|
|
// Reset counter to allow 60 more reads
|
|
accErrTrigger = 0
|
|
|
|
default:
|
|
log.Fatal("Thermal bulk read failed 60 times in a row -- terminating")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func readSensors(hub *sentry.Hub) bool {
|
|
r := TemperatureReading{
|
|
Time: time.Now(),
|
|
Ambient: NaN,
|
|
Chamber: NaN,
|
|
Wort: NaN,
|
|
}
|
|
|
|
hasReadFailure := false
|
|
|
|
for _, sensor := range sensors {
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
t, err := read(sensor.Path)
|
|
if err != nil {
|
|
hasReadFailure = true
|
|
sensor.Fail()
|
|
hub.CaptureException(err)
|
|
log.Printf("%v/%v", sensor.Name, err)
|
|
metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "failed").Inc()
|
|
continue
|
|
}
|
|
|
|
tw := sensor.Update(t)
|
|
|
|
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.Name).
|
|
Set(tw)
|
|
|
|
var statusStr string
|
|
if err == nil {
|
|
statusStr = "ok"
|
|
} else {
|
|
statusStr = "fail"
|
|
}
|
|
metrics.TemperatureSensorReadingStatus.
|
|
WithLabelValues(sensor.Name, statusStr).
|
|
Inc()
|
|
}
|
|
|
|
// Throw away reading if any sensor failed
|
|
if hasReadFailure {
|
|
return false
|
|
}
|
|
|
|
select {
|
|
case Reading <- r:
|
|
break
|
|
|
|
default:
|
|
err := fmt.Errorf("channel overflow on ds18b20 temperature channel")
|
|
hub.CaptureException(err)
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func triggerBulkRead() error {
|
|
f, err := os.OpenFile("/sys/bus/w1/devices/w1_bus_master1/therm_bulk_read", os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString("trigger\n")
|
|
return err
|
|
}
|
|
|
|
func pollBusState() (BulkReadBusState, error) {
|
|
b, err := os.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
|
|
}
|
|
|
|
// read returns the temperature of the specified sensor in millidegrees celcius.
|
|
func read(sensor string) (int64, error) {
|
|
path := filepath.Join("/sys/bus/w1/devices", sensor, "w1_slave")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return NaN, err
|
|
}
|
|
|
|
raw := string(data)
|
|
|
|
if raw == "" {
|
|
return NaN, fmt.Errorf("%v: %w: empty file", sensor, ErrReadSensor)
|
|
}
|
|
|
|
if !strings.Contains(raw, " YES") {
|
|
return NaN, fmt.Errorf("%v: %w: checksum failed [%v]", sensor, ErrReadSensor, raw)
|
|
}
|
|
|
|
i := strings.LastIndex(raw, "t=")
|
|
if i == -1 {
|
|
return NaN, fmt.Errorf("%v: %w: t= not found in [%v]", sensor, ErrReadSensor, raw)
|
|
}
|
|
|
|
c, err := strconv.ParseInt(raw[i+2:len(raw)-1], 10, 64)
|
|
if err != nil {
|
|
return NaN, err
|
|
}
|
|
|
|
// Ignore magic value -128
|
|
if c == -128000 {
|
|
return NaN, fmt.Errorf("%v: magic value -128 detected", sensor)
|
|
}
|
|
|
|
return c, nil
|
|
}
|