package temperature // Kernel documentation: https://www.kernel.org/doc/html/latest/w1/slaves/w1_therm.html import ( "context" "errors" "io/ioutil" "log" "os" "path/filepath" "strconv" "strings" "sync" "time" "git.joco.dk/sng/fermentord/internal/configuration" "git.joco.dk/sng/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 accErrTrigger int sensors []Sensor ) func init() { Reading = make(chan TemperatureReading, 30) ConfigUpdate = make(chan configuration.Configuration, 1) 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) { hub := sentry.CurrentHub().Clone() defer hub.Flush(10 * time.Second) defer wg.Done() if readingInterval < sensorConversionTime { log.Fatalf("Reading interval must be at least %v ms.", sensorConversionTime) } ticker := time.NewTicker(readingInterval) defer ticker.Stop() for { work_loop: select { case <-ticker.C: start := time.Now() if accErrTrigger > 60 { log.Fatal("Thermal bulk read failed 60 times in a row -- terminating") } // Trigger a bulk read to start conversion on all sensors. if err := triggerBulkRead(); err != nil { accErrTrigger++ hub.CaptureException(err) log.Print(err) break } accErrTrigger = 0 // 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) } // 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) } case c := <-ConfigUpdate: configure(c) case <-ctx.Done(): 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.Path) if err != nil { sensor.Fail() hub.CaptureException(err) log.Printf("Error reading temperature sensor %v: %v", sensor, 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) metrics.TemperatureSensorReadingStatus.WithLabelValues(sensor.Name, "ok").Inc() } Reading <- r } 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 := 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 } // 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 := 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 }