From 0cf4562d1e3da9befb9b14db97d2668137f432ba Mon Sep 17 00:00:00 2001 From: George Sexton Date: Mon, 25 Nov 2024 10:57:38 -0700 Subject: [PATCH 1/4] Initial create --- hdc302x/example_test.go | 0 hdc302x/hdc302x.go | 69 +++++++++++++++++++++++++++++++++++++++++ hdc302x/hdc302x_test.go | 0 3 files changed, 69 insertions(+) create mode 100644 hdc302x/example_test.go create mode 100644 hdc302x/hdc302x.go create mode 100644 hdc302x/hdc302x_test.go diff --git a/hdc302x/example_test.go b/hdc302x/example_test.go new file mode 100644 index 0000000..e69de29 diff --git a/hdc302x/hdc302x.go b/hdc302x/hdc302x.go new file mode 100644 index 0000000..e4206f8 --- /dev/null +++ b/hdc302x/hdc302x.go @@ -0,0 +1,69 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. +// +// This package provides a driver for the Texas Instruments HDC3021/3022 +// I2C Temperature/Humidity Sensors +// +// The datasheet is available at: +// +// https://www.ti.com/lit/ds/symlink/hdc3022.pdf +package hdc302x + +import ( + "errors" + "fmt" + "sync" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/physic" +) + +// NewI2C returns a new HDC302x sensor using the specified bus and address. +// If opts is not supplied, the configuration of the sensor is set to the +// default on startup. +func NewI2C(b i2c.Bus, addr uint16, opts *Opts) (*Dev, error) { + if opts == nil { + opts = &Opts{SampleRate: RateFourHertz, AlertSetting: ModeComparator} + } + d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, opts: opts, shutdown: nil} + return d, d.start() +} + +// Halt shuts down the device. If a SenseContinuous operation is in progress, +// its aborted. Implements conn.Resource +func (dev *Dev) Halt() error { +} + +// Sense reads temperature from the device and writes the value to the specified +// env variable. Implements physic.SenseEnv. +func (dev *Dev) Sense(env *physic.Env) error { +} + +// SenseContinuous continuously reads from the device and writes the value to +// the returned channel. Implements physic.SenseEnv. To terminate the +// continuous read, call Halt(). +func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) { +} + +// Precision returns the sensor's precision, or minimum value between steps the +// device can make. The specified precision is 0.0625 degrees Celsius. Note +// that the accuracy of the device is +/- 0.5 degrees Celsius. +func (dev *Dev) Precision(env *physic.Env) { + env.Temperature = _DEGREES_RESOLUTION + env.Pressure = 0 + env.Humidity = 0 +} + +func (dev *Dev) String() string { + return fmt.Sprintf("hdc302x: %s", dev.d.String()) +} + + + + + +var _ conn.Resource = &Dev{} +var _ physic.SenseEnv = &Dev{} diff --git a/hdc302x/hdc302x_test.go b/hdc302x/hdc302x_test.go new file mode 100644 index 0000000..e69de29 From c75349154b6dbddae1cea680447e4959ac7b2329 Mon Sep 17 00:00:00 2001 From: George Sexton Date: Sat, 7 Dec 2024 13:43:33 -0700 Subject: [PATCH 2/4] Initial add --- hdc302x/example_test.go | 41 +++ hdc302x/hdc302x.go | 652 ++++++++++++++++++++++++++++++++++++++-- hdc302x/hdc302x_test.go | 615 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1287 insertions(+), 21 deletions(-) diff --git a/hdc302x/example_test.go b/hdc302x/example_test.go index e69de29..b8464bf 100644 --- a/hdc302x/example_test.go +++ b/hdc302x/example_test.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package hdc302x_test + +import ( + "fmt" + "log" + + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/hdc302x" + "periph.io/x/host/v3" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create the Sensor + sensor, err := hdc302x.NewI2C(bus, hdc302x.DefaultSensorAddress, hdc302x.RateFourHertz) + if err != nil { + log.Fatal(err) + } + // Take a reading + env := physic.Env{} + err = sensor.Sense(&env) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Sensor Output: %s\n", env) +} diff --git a/hdc302x/hdc302x.go b/hdc302x/hdc302x.go index e4206f8..f16ee10 100644 --- a/hdc302x/hdc302x.go +++ b/hdc302x/hdc302x.go @@ -1,18 +1,20 @@ // Copyright 2024 The Periph Authors. All rights reserved. // Use of this source code is governed under the Apache License, Version 2.0 // that can be found in the LICENSE file. -// + // This package provides a driver for the Texas Instruments HDC3021/3022 -// I2C Temperature/Humidity Sensors +// I2C Temperature/Humidity Sensors. This is a high accuracy sensor with +// very good resolution. // -// The datasheet is available at: +// Datasheet // -// https://www.ti.com/lit/ds/symlink/hdc3022.pdf +// https://www.ti.com/lit/ds/symlink/hdc3022.pdf package hdc302x import ( "errors" "fmt" + "math" "sync" "time" @@ -21,49 +23,657 @@ import ( "periph.io/x/conn/v3/physic" ) -// NewI2C returns a new HDC302x sensor using the specified bus and address. -// If opts is not supplied, the configuration of the sensor is set to the -// default on startup. -func NewI2C(b i2c.Bus, addr uint16, opts *Opts) (*Dev, error) { - if opts == nil { - opts = &Opts{SampleRate: RateFourHertz, AlertSetting: ModeComparator} - } - d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, opts: opts, shutdown: nil} - return d, d.start() +type SampleRate uint16 + +const ( + // Constants for the sample rate to use for the measurements. The datasheet + // recommends not sampling more often than once per second to avoid self-heating + // of the sensor. + // + // Every other second + RateHalfHertz SampleRate = iota + // Sample 1x Second. + RateHertz + RateTwoHertz + RateFourHertz + Rate10Hertz +) + +// Dev represents a hdc302x sensor. +type Dev struct { + d *i2c.Dev + shutdown chan struct{} + mu sync.Mutex + sampleRate SampleRate + halted bool +} + +// The alert function works with pairs of values Temperature/Humidity. A +// Threshold is a set of humidity/temperature values defining an upper or +// lower limit for alerts. +type Threshold struct { + Humidity physic.RelativeHumidity + Temperature physic.Temperature +} + +// For alert and clear, there is a pair of temperatures. For example, +// a low alert value, and a clear low alert value. There's a value +// for each measurement parameter. +type ThresholdPair struct { + Low Threshold + High Threshold +} + +type StatusWord uint16 + +const ( + // Status flags returned by ReadStatus() + StatusActiveAlerts StatusWord = 1 << 15 + StatusHeaterEnabled StatusWord = 1 << 13 + // Mirrored on the alert pin. + StatusRHTrackingAlert StatusWord = 1 << 11 + // Also reflected on alert pin + StatusTempTrackingAlert StatusWord = 1 << 10 + StatusRHHighTrackingAlert StatusWord = 1 << 9 + StatusRHLowTrackingAlert StatusWord = 1 << 8 + StatusTempHighTrackingAlert StatusWord = 1 << 7 + StatusTempLowTrackingAlert StatusWord = 1 << 6 + StatusDeviceReset StatusWord = 1 << 4 + // Set if there was a CRC error on the last write command. + StatusLastWriteCRCFailure StatusWord = 1 << 0 +) + +// Configuration provides information about the running device's config. +type Configuration struct { + // Device unique ID. Read-Only + SerialNumber int64 + // Numeric vendor ID. Read-Only + VendorID uint16 + // Status Word. Refer to the Status* constants above, and the datasheet for + // usage. + Status StatusWord + // refer to the Rate constants. Read-Only + SampleRate SampleRate + // Offset for RH calculation. Note that these offsets are approximate, + // so a request to set the offset to -5%rH may result in an offset of + // -4.8%rH. This is an artifact of the device's offset implementation. + // Refer to the datasheet for more information. + HumidityOffset physic.RelativeHumidity + // Offset for Temp result. Note that the data sheet states this is not + // used in the RH calculation. + TemperatureOffset physic.Temperature + + // High/Low thresholds for triggering alerts. As with the offsets, + // written values are not precise. + AlertThresholds ThresholdPair + // High/Low threshold for clearing alerts. + ClearThresholds ThresholdPair +} + +const ( + // The default i2c bus address for this device. + DefaultSensorAddress uint16 = 0x44 +) + +type HeaterPower uint16 + +const ( + // Constants for setting the heater's power setting. + PowerFull HeaterPower = 0x3fff + PowerHalf HeaterPower = 0x03ff + PowerQuarter HeaterPower = 0x9f + PowerOff HeaterPower = 0 +) + +type devCommand []byte + +// Sample Rate commands +var measure2Seconds = devCommand{0x20, 0x32} +var measureSecond = devCommand{0x21, 0x30} +var measure2xSecond = devCommand{0x22, 0x36} +var measure4xSecond = devCommand{0x23, 0x34} +var measure10xSecond = devCommand{0x27, 0x37} + +var sampleRateCommands = []devCommand{measure2Seconds, measureSecond, measure2xSecond, measure4xSecond, measure10xSecond} +var sampleRateDurations = []time.Duration{2 * time.Second, time.Second, 500 * time.Millisecond, 250 * time.Millisecond, 100 * time.Millisecond} + +// Other device commands +var clearStatus = devCommand{0x30, 0x41} +var disableHeater = devCommand{0x30, 0x66} +var enableHeater = devCommand{0x30, 0x6d} +var read = devCommand{0xe0, 0x0} +var readSetHeater = devCommand{0x30, 0x6e} +var readSetOffsets = devCommand{0xa0, 0x04} +var readStatus = devCommand{0xf3, 0x2d} +var readVendorID = devCommand{0x37, 0x81} +var reset = devCommand{0x30, 0xa2} +var stopContinuousReadings = devCommand{0x30, 0x93} + +// read/write alert threshold commands. +var readLowAlertThresholds = devCommand{0xe1, 0x02} +var readHighAlertThresholds = devCommand{0xe1, 0x1f} +var readLowClearThresholds = devCommand{0xe1, 0x09} +var readHighClearThresholds = devCommand{0xe1, 0x14} +var writeLowAlertThresholds = devCommand{0x61, 0x00} +var writeHighAlertThresholds = devCommand{0x61, 0x1d} +var writeLowClearThresholds = devCommand{0x61, 0x0b} +var writeHighClearThresholds = devCommand{0x61, 0x16} + +var invalidCRCError = errors.New("hdc302x: invalid crc") + +const ( + // Magic numbers for count to value conversions. + temperatureOffset float64 = -45.0 + temperatureScalar float64 = 175.0 + humidityScalar float64 = 100.0 + scaleDivisor float64 = 65535.0 +) + +// NewI2C returns a new HDC302x sensor using the specified bus, address, and +// sample rate. +func NewI2C(b i2c.Bus, addr uint16, sampleRate SampleRate) (*Dev, error) { + dev := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, shutdown: nil, sampleRate: sampleRate} + return dev, dev.start() +} + +// send continuous measurement start command. +func (dev *Dev) start() error { + if err := dev.d.Tx(sampleRateCommands[dev.sampleRate], nil); err != nil { + return fmt.Errorf("hdc302x: init %w", err) + } + // Sleep for a minimum of one sample acquisition period. If you + // read before a sample has acquired, you get remote I/O error. + time.Sleep(sampleRateDurations[dev.sampleRate]) + dev.halted = false + return nil +} + +// Convert the raw count to a temperature. +func countToTemperature(bytes []byte) physic.Temperature { + count := (uint16(bytes[0]) << 8) | uint16(bytes[1]) + f := float64(count)/float64(scaleDivisor)*temperatureScalar + temperatureOffset + t := physic.ZeroCelsius + physic.Temperature(f*float64(physic.Celsius)) + return t +} + +// convert the raw count to a humidity value. +func countToHumidity(bytes []byte) physic.RelativeHumidity { + count := (uint16(bytes[0]) << 8) | uint16(bytes[1]) + f := float64(count) / float64(scaleDivisor) * humidityScalar + return physic.RelativeHumidity(f * float64(physic.PercentRH)) +} + +func crc8(bytes []byte) byte { + var crc byte = 0xff + for _, val := range bytes { + crc ^= val + for range 8 { + if (crc & 0x80) == 0 { + crc <<= 1 + } else { + crc = (byte)((crc << 1) ^ 0x31) + } + } + } + return crc } // Halt shuts down the device. If a SenseContinuous operation is in progress, // its aborted. Implements conn.Resource func (dev *Dev) Halt() error { + dev.mu.Lock() + defer dev.mu.Unlock() + if dev.shutdown != nil { + close(dev.shutdown) + } + var err error + if !dev.halted { + dev.halted = true + err = dev.d.Tx(stopContinuousReadings, nil) + } + return err } -// Sense reads temperature from the device and writes the value to the specified -// env variable. Implements physic.SenseEnv. +// Sense reads temperature and humidity from the device and writes the value to +// the specified env variable. Implements physic.SenseEnv. func (dev *Dev) Sense(env *physic.Env) error { + env.Temperature = 0 + env.Pressure = 0 + env.Humidity = 0 + res := make([]byte, 6) + dev.mu.Lock() + defer dev.mu.Unlock() + if dev.halted { + if err := dev.start(); err != nil { + return err + } + } + if err := dev.d.Tx(read, res); err != nil { + return fmt.Errorf("hdc302x: %w", err) + } + if crc8(res[:2]) != res[2] || crc8(res[3:5]) != res[5] { + return invalidCRCError + } + env.Temperature = countToTemperature(res) + env.Humidity = countToHumidity(res[3:]) + return nil +} + +func temperatureToFloat64(temp physic.Temperature) float64 { + return float64(temp) / float64(physic.Celsius) +} + +func humidityToFloat64(humidity physic.RelativeHumidity) float64 { + return float64(humidity) / float64(physic.PercentRH) } // SenseContinuous continuously reads from the device and writes the value to // the returned channel. Implements physic.SenseEnv. To terminate the // continuous read, call Halt(). +// +// If interval is less than the device sample period, an error is returned. func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) { + + if dev.shutdown != nil { + return nil, errors.New("hdc302x: SenseContinuous already running") + } + + if interval < sampleRateDurations[dev.sampleRate] { + return nil, errors.New("hdc302x: sample interval is < device sample rate") + } + + dev.shutdown = make(chan struct{}) + chResult := make(chan physic.Env, 16) + go func(ch chan physic.Env) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + defer close(ch) + for { + select { + case <-dev.shutdown: + dev.shutdown = nil + return + case <-ticker.C: + env := physic.Env{} + if err := dev.Sense(&env); err == nil { + ch <- env + } + } + } + }(chResult) + return chResult, nil } // Precision returns the sensor's precision, or minimum value between steps the -// device can make. The specified precision is 0.0625 degrees Celsius. Note -// that the accuracy of the device is +/- 0.5 degrees Celsius. +// device can measure. Refer to the datasheet for information on limits and +// accuracy. func (dev *Dev) Precision(env *physic.Env) { - env.Temperature = _DEGREES_RESOLUTION - env.Pressure = 0 - env.Humidity = 0 + env.Temperature = physic.Temperature(math.Round(temperatureScalar / scaleDivisor * float64(physic.Celsius))) + env.Humidity = physic.RelativeHumidity(math.Round((float64(physic.PercentRH) * humidityScalar) / float64(scaleDivisor))) + env.Pressure = 0 +} + +func (dev *Dev) readSerialNumber() int64 { + var result int64 + cmd := []byte{0x36, 0x83} + r := make([]byte, 3) + // this is a 6 byte value read in 3 parts + for range 3 { + err := dev.d.Tx(cmd, r) + if err != nil || (crc8(r[:2]) != r[2]) { + return result + } + result = result<<16 | (int64(r[0])<<8 | int64(r[1])) + cmd[1] += 1 // Increment the register to the next one. + } + return result +} + +// read the alert / clear threshold values from the device. +func (dev *Dev) readAlertValues(cfg *Configuration) error { + + // Pair + // Low + // Temp + // Humidity + // High + // Temp + // Humidity + + var cmds = []devCommand{readLowAlertThresholds, readHighAlertThresholds, readLowClearThresholds, readHighClearThresholds} + var pairs = [2]*ThresholdPair{&cfg.AlertThresholds, &cfg.ClearThresholds} + var threshold *Threshold + + r := make([]byte, 3) + + for ix, cmd := range cmds { + pair := pairs[ix>>1] + if ix%2 == 0 { + threshold = &pair.Low + } else { + threshold = &pair.High + } + + err := dev.d.Tx(cmd, r) + if err != nil { + return err + } + if crc8(r[:2]) != r[2] { + return invalidCRCError + } + wValue := uint16(r[0])<<8 | uint16(r[1]) + // The alert value is returned as a 16 bit words, where bits 0-8 are the + // Temperature value, and bits 9-15 are the Humidity. The temperature + // bits correspond to bits 7-15 of the temperature, and bits 9-15 of the + // humidity. Refer to the datasheet. + temp := &threshold.Temperature + humidity := &threshold.Humidity + *temp = physic.Temperature(((float64(uint16(wValue<<7)) * temperatureScalar) / scaleDivisor) * float64(physic.Celsius)) + *humidity = physic.RelativeHumidity(((float64(wValue&0xfe00) * humidityScalar) / scaleDivisor) * float64(physic.PercentRH)) + } + + return nil +} + +// readOffsets returns temperature/humidity offset values stored to the device. +func (dev *Dev) readOffsets(cfg *Configuration) error { + r := make([]byte, 3) + if err := dev.d.Tx(readSetOffsets, r); err != nil { + return fmt.Errorf("hdc302x: %w", err) + } + if crc8(r[:2]) != r[2] { + return invalidCRCError + } + + // The result comes back as the humidity offset, followed by + // the temperature offset. The offsets are computed by summing + // the bits and applying the partial algorithm. + + h := uint16((r[0] & 0x7f)) << 7 + t := uint16((r[1] & 0x7f)) << 6 + rh := float64(h) / scaleDivisor * humidityScalar + temp := (float64(t) * temperatureScalar) / scaleDivisor + + if r[0]&0x80 == 0x00 { + rh *= -1.0 + } + cfg.HumidityOffset = physic.RelativeHumidity(rh * float64(physic.PercentRH)) + + if r[1]&0x80 == 0x00 { + temp *= -1.0 + } + cfg.TemperatureOffset = physic.Temperature(temp * float64(physic.Celsius)) + + return nil +} + +func (dev *Dev) readVendorID() (uint16, error) { + r := make([]byte, 3) + err := dev.d.Tx(readVendorID, r) + if err == nil { + vid := uint16(r[0])<<8 | uint16(r[1]) + return vid, nil + } + return 0, err +} + +// ReadStatus returns the device's status word, and if successful, clears the +// status. Refer to the Status* constants and the datasheet for interpretation. +func (dev *Dev) ReadStatus() (StatusWord, error) { + r := make([]byte, 3) + if err := dev.d.Tx(readStatus, r); err != nil { + return 0, err + } + if crc8(r[:2]) != r[2] { + return 0, invalidCRCError + } + _ = dev.d.Tx(clearStatus, nil) + return StatusWord(r[0])<<8 | StatusWord(r[1]), nil +} + +// Return the device's configuration settings. Includes alert values, offset +// values, and other information about the device. +func (dev *Dev) Configuration() (*Configuration, error) { + cfg := &Configuration{SampleRate: dev.sampleRate} + cfg.SerialNumber = dev.readSerialNumber() + err := dev.readOffsets(cfg) + if err != nil { + return cfg, err + } + if cfg.VendorID, err = dev.readVendorID(); err != nil { + return cfg, err + } + if cfg.Status, err = dev.ReadStatus(); err != nil { + return cfg, err + } + + err = dev.readAlertValues(cfg) + + return cfg, err +} + +// setOffsets writes temperature and humidity offsets to the device. +// Refer to the datasheet for information on offsets. The critical +// thing to know is that the smallest offsets are ~0.2%RH, and ~ +// 0.2 degrees C. +func (dev *Dev) setOffsets(cfg *Configuration) error { + var w = []byte{readSetOffsets[0], + readSetOffsets[1], + computeHumidityOffsetByte(cfg.HumidityOffset), + computeTemperatureOffsetByte(cfg.TemperatureOffset), + 0, + } + w[4] = crc8(w[2:4]) + return dev.d.Tx(w, nil) +} + +// Refer to the datasheet. Essentially, the offsets are only a specific set of +// bit ranges. +func computeTemperatureOffsetByte(temp physic.Temperature) byte { + var res byte + fTemp := temperatureToFloat64(temp) + if fTemp >= 0 { + res |= 0x80 + } else { + fTemp *= -1.0 + } + for bit := 12; bit > 5; bit-- { + offset := (float64(int64(1)<= offset { + fTemp -= offset + res |= (1 << (bit - 6)) + } + } + return res +} + +func computeHumidityOffsetByte(humidity physic.RelativeHumidity) byte { + var res byte + fHumidity := humidityToFloat64(humidity) + if fHumidity >= 0 { + res |= 0x80 + } else { + fHumidity *= -1.0 + } + for bit := 13; bit > 6; bit-- { + offset := (float64(int64(1)<= offset { + fHumidity -= offset + res |= (1 << (bit - 7)) + } + } + return res +} + +// Reset performs a soft-reset of the device. +func (dev *Dev) Reset() error { + dev.mu.Lock() + defer dev.mu.Unlock() + err := dev.d.Tx(reset, nil) + time.Sleep(time.Second) + return err +} + +// setThreshold sets a threshold pair for either alert, or clear alert. +// if typeAlert is true, it indicates the pair type is alert, otherwise +// it's clear alert. +func (dev *Dev) setThresholds(typeAlert bool, tp *ThresholdPair) error { + var cmds = [][]devCommand{{writeLowAlertThresholds, writeHighAlertThresholds}, + {writeLowClearThresholds, writeHighClearThresholds}} + + pair := 1 + if typeAlert { + pair = 0 + } + var th *Threshold + for ix := range 2 { + if ix == 0 { + th = &tp.Low + } else { + th = &tp.High + } + temp := temperatureToFloat64(th.Temperature) + tempBits := uint16(0) + for bit := 15; bit >= 0; bit-- { + bitVal := (float64(uint16(1<= bitVal { + temp -= bitVal + tempBits |= (1 << bit) + } + } + humidity := humidityToFloat64(th.Humidity) + humBits := uint16(0) + for bit := 15; bit >= 0; bit-- { + bitVal := (float64(uint16(1<= bitVal { + humidity -= bitVal + humBits |= (1 << bit) + } + } + wval := uint16(0) + wval = (humBits & 0xfe00) | tempBits>>7 + w := []byte{cmds[pair][ix][0], cmds[pair][ix][1], byte(wval >> 8), byte(wval & 0xff), 0} + w[4] = crc8(w[2:4]) + err := dev.d.Tx(w, nil) + if err != nil { + return err + } + } + + return nil +} + +// SetConfiguration takes a modified configuration struct and +// applies it to the device. +func (dev *Dev) SetConfiguration(cfg *Configuration) error { + _ = dev.Halt() + dev.mu.Lock() + defer dev.mu.Unlock() + current, err := dev.Configuration() + if err != nil { + return err + } + if current.HumidityOffset != cfg.HumidityOffset || current.TemperatureOffset != cfg.TemperatureOffset { + if err := dev.setOffsets(cfg); err != nil { + return err + } + } + + if !current.AlertThresholds.Equals(&cfg.AlertThresholds) { + if err := dev.setThresholds(true, &cfg.AlertThresholds); err != nil { + return err + } + } + + if !current.ClearThresholds.Equals(&cfg.ClearThresholds) { + if err := dev.setThresholds(false, &cfg.ClearThresholds); err != nil { + return err + } + } + return nil +} + +// The hdc302x sensors have a built in heater element for operating in environments +// where the humidity/temperature level is condensing. SetHeater allows you to turn +// the heater element on and off at specified power levels. Refer to the datasheet +// for instructions on how the heater can be used in those environments. +func (dev *Dev) SetHeater(powerLevel HeaterPower) error { + if powerLevel > PowerFull { + return fmt.Errorf("hdc302x: invalid value for powerLevel: 0x%x", powerLevel) + } + if powerLevel == PowerOff { + return dev.d.Tx(disableHeater, nil) + } + var setValue = []byte{readSetHeater[0], + readSetHeater[1], + byte((powerLevel >> 8) & 0xff), + byte(powerLevel & 0xff), + 0} + setValue[4] = crc8(setValue[2:4]) + err := dev.d.Tx(setValue, nil) + if err != nil { + return err + } + return dev.d.Tx(enableHeater, nil) } func (dev *Dev) String() string { - return fmt.Sprintf("hdc302x: %s", dev.d.String()) + return fmt.Sprintf("hdc302x: %s", dev.d.String()) +} + +func (cfg *Configuration) String() string { + return fmt.Sprintf(`{ + SerialNumber: 0x%x, + VendorID: 0x%x, + Status: 0x%x, + SampleRate: %d, + HumidityOffset: %s, + TemperatureOffset: %s, + AlertThresholds: %s, + ClearThresholds: %s + }`, + cfg.SerialNumber, + cfg.VendorID, + cfg.Status, + cfg.SampleRate, + cfg.HumidityOffset, + cfg.TemperatureOffset+physic.ZeroCelsius, + &cfg.AlertThresholds, + &cfg.ClearThresholds) } +func (t *Threshold) String() string { + return fmt.Sprintf("{ Humidity: %s, Temperature: %s }", t.Humidity, t.Temperature+physic.ZeroCelsius) +} + +func (tp *ThresholdPair) String() string { + return fmt.Sprintf("{ Low: %s, High: %s }", + &tp.Low, + &tp.High) +} +func (t *Threshold) Equals(tCompare *Threshold) bool { + return t.Temperature == tCompare.Temperature && t.Humidity == tCompare.Humidity +} +// For thresholds, you can only set a truncated value. For temperature, that means +// the 9 high bits, and for humidity, the 7 high bits. This means a comparison of +// a written value with the resulting value can be off. This method encapsulates +// the comparison of a threshold pair to make sure they're approximately equal. +func (t *Threshold) ApproximatelyEquals(tCompare *Threshold) bool { + t1 := temperatureToFloat64(t.Temperature) + h1 := humidityToFloat64(t.Humidity) + t2 := temperatureToFloat64(tCompare.Temperature) + h2 := humidityToFloat64(tCompare.Humidity) + tLimit := float64(uint16(1<<8)) * temperatureScalar / scaleDivisor + hLimit := float64(uint16(1<<9)) * humidityScalar / scaleDivisor + return math.Abs(t1-t2) < tLimit && + math.Abs(h1-h2) < hLimit +} +func (tp *ThresholdPair) Equals(tpCompare *ThresholdPair) bool { + return tp.Low.Equals(&tpCompare.Low) && tp.High.Equals(&tpCompare.High) +} var _ conn.Resource = &Dev{} var _ physic.SenseEnv = &Dev{} diff --git a/hdc302x/hdc302x_test.go b/hdc302x/hdc302x_test.go index e69de29..761b25d 100644 --- a/hdc302x/hdc302x_test.go +++ b/hdc302x/hdc302x_test.go @@ -0,0 +1,615 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package hdc302x + +import ( + "fmt" + "math" + "os" + "sync/atomic" + "testing" + "time" + + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/conn/v3/i2c/i2ctest" + "periph.io/x/conn/v3/physic" + "periph.io/x/host/v3" +) + +var bus i2c.Bus +var liveDevice bool + +// Playback values for a single sense operation. +var pbSense = []i2ctest.IO{ + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + // 26.621 C, 23.2%RH + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x68, 0xc5, 0x51, 0x3b, 0x82, 0x31}}, +} + +// Playback for heater testing. +var pbHeater = []i2ctest.IO{ + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x64, 0x93, 0x3d, 0x45, 0x3a, 0x61}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x6e, 0x3f, 0xff, 0x6}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x6d}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x9d, 0xb1, 0x2, 0x9, 0xc6, 0xa3}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x66}}} + +// Playback for modifying configuration. +var pbConfiguration = []i2ctest.IO{ + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x80, 0x80, 0xd8}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}} + +// playback for offset modification +var pbOffsets = []i2ctest.IO{ + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x70, 0x90, 0x83, 0x42, 0x10, 0x92}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x19, 0xba, 0x48}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x93}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x19, 0xba, 0x48}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4, 0x32, 0xf4, 0xac}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x32, 0xf4, 0xac}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}, + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x7f, 0x28, 0x1c, 0x37, 0x8d, 0xab}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0xa2}}} + +var pbAlerts = []i2ctest.IO{ + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x70, 0xe0, 0x7b, 0x3f, 0xf7, 0xbf}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x19, 0xba, 0x48}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x80, 0x10, 0xe1}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x93}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x19, 0xba, 0x48}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x34, 0x66, 0xad}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xcd, 0x33, 0xfd}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x38, 0x69, 0x37}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xc9, 0x2d, 0x22}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4, 0x80, 0x80, 0xd8}}, + {Addr: DefaultSensorAddress, W: []uint8{0x61, 0x0, 0x4c, 0x1d, 0xb3}}, + {Addr: DefaultSensorAddress, W: []uint8{0x61, 0x1d, 0xbe, 0xdb, 0x93}}, + {Addr: DefaultSensorAddress, W: []uint8{0x61, 0xb, 0x58, 0x2b, 0x3d}}, + {Addr: DefaultSensorAddress, W: []uint8{0x61, 0x16, 0xb2, 0xcc, 0xf3}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x83}, R: []uint8{0xc2, 0x95, 0x3e}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x84}, R: []uint8{0xb1, 0x49, 0x51}}, + {Addr: DefaultSensorAddress, W: []uint8{0x36, 0x85}, R: []uint8{0x15, 0x21, 0x2f}}, + {Addr: DefaultSensorAddress, W: []uint8{0xa0, 0x4}, R: []uint8{0x80, 0x80, 0xd8}}, + {Addr: DefaultSensorAddress, W: []uint8{0x37, 0x81}, R: []uint8{0x30, 0x0, 0x33}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x0, 0x0, 0x81}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x2}, R: []uint8{0x4c, 0x1d, 0xb3}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x1f}, R: []uint8{0xbe, 0xdb, 0x93}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x9}, R: []uint8{0x58, 0x2b, 0x3d}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe1, 0x14}, R: []uint8{0xb2, 0xcc, 0xf3}}, + {Addr: DefaultSensorAddress, W: []uint8{0x23, 0x34}}, + {Addr: DefaultSensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x62, 0x77, 0x62, 0x4a, 0x94, 0x1b}}, + {Addr: DefaultSensorAddress, W: []uint8{0xf3, 0x2d}, R: []uint8{0x89, 0x0, 0x61}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x41}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0x93}}, + {Addr: DefaultSensorAddress, W: []uint8{0x30, 0xa2}}} + +func init() { + var err error + + liveDevice = os.Getenv("HDC302X") != "" + + // Make sure periph is initialized. + if _, err = host.Init(); err != nil { + fmt.Println(err) + } + + if liveDevice { + bus, err = i2creg.Open("") + if err != nil { + fmt.Println(err) + } + // Add the recorder to dump the data stream when we're using a live device. + bus = &i2ctest.Record{Bus: bus} + } else { + bus = &i2ctest.Playback{DontPanic: true} + } +} + +// getDev returns a configured device using either an i2c bus, or a playback bus. +func getDev(t *testing.T, playbackOps ...[]i2ctest.IO) (*Dev, error) { + if liveDevice { + if recorder, ok := bus.(*i2ctest.Record); ok { + // Clear the operations buffer. + recorder.Ops = make([]i2ctest.IO, 0, 32) + } + + } else { + if len(playbackOps) == 1 { + pb := bus.(*i2ctest.Playback) + pb.Ops = playbackOps[0] + pb.Count = 0 + } + } + dev, err := NewI2C(bus, DefaultSensorAddress, RateFourHertz) + + if err != nil { + t.Log("error constructing dev") + t.Fatal(err) + } + + return dev, err +} + +// shutdown dumps the recorder values if we we're running a live device. +func shutdown(t *testing.T) { + if recorder, ok := bus.(*i2ctest.Record); ok { + t.Logf("%#v", recorder.Ops) + } +} + +func TestCRC(t *testing.T) { + var tests = []struct { + bytes []byte + result byte + }{ + {bytes: []byte{0xbe, 0xef}, result: 0x92}, + {bytes: []byte{0xab, 0xcd}, result: 0x6f}, + } + for _, test := range tests { + res := crc8(test.bytes) + if res != test.result { + t.Errorf("crc8(%#v)!=0x%d receieved 0x%d", test.bytes, test.result, res) + } + } +} + +// TestConversions tests the various temperature/humidity functions +// for correct operation. +func TestConversions(t *testing.T) { + envPrecision := physic.Env{} + dev := Dev{} + dev.Precision(&envPrecision) + t.Logf("Precision: Temperature: %d nanoKelvin Humidity: %d TenthMicroPercent RH", envPrecision.Temperature, envPrecision.Humidity) + + temp := countToTemperature([]byte{0x00, 0x00}) + expected := physic.ZeroCelsius - 45*physic.Celsius + if temp != expected { + t.Errorf("unexpected countToTemperature. expected %s, received %s %d", expected, temp, temp) + } + temp = countToTemperature([]byte{0xd4, 0x1c}) + expected = physic.ZeroCelsius + 100*physic.Celsius + if math.Abs(float64(expected-temp)) > float64(envPrecision.Temperature) { + t.Errorf("unexpected countToTemperature. expected %s (%d), received %s (%d)", expected, expected, temp, temp) + } + + var hTests = []struct { + bytes []byte + result physic.RelativeHumidity + }{ + {bytes: []byte{0x0, 0x0}, result: physic.RelativeHumidity(0)}, + {bytes: []byte{0x80, 0x0}, result: 50 * physic.PercentRH}, + {bytes: []byte{0xff, 0xff}, result: 100 * physic.PercentRH}, + } + for _, hTest := range hTests { + humidity := countToHumidity(hTest.bytes) + if (humidity - hTest.result) > envPrecision.Humidity { + t.Errorf("unexpected humidity got %s (%d) expected %s (%d)", humidity, humidity, hTest.result, hTest.result) + } + } + +} + +// Tests the conversions of offsets to binary offset values used by the device. +func TestOffsetConversions(t *testing.T) { + // These are a subtly off from the datasheet because it's using floating point representation, + var tempOffsets = []struct { + temperature physic.Temperature + expectedResult byte + }{ + {temperature: 170_902 * physic.MicroKelvin, expectedResult: 0x81}, + {temperature: -170_902 * physic.MicroKelvin, expectedResult: 0x01}, // Test sign handling + {temperature: 7_178_000 * physic.MicroKelvin, expectedResult: 0xaa}, + {temperature: 10_937_700 * physic.MicroKelvin, expectedResult: 0xc0}, + {temperature: 21_704_500 * physic.MicroKelvin, expectedResult: 0xff}, + } + for _, test := range tempOffsets { + res := computeTemperatureOffsetByte(test.temperature) + if res != test.expectedResult { + t.Errorf("computeTemperatureOffsetByte() Offset: %s Expected Value: 0x%x Received: 0x%x", test.temperature, test.expectedResult, res) + } + } + + // Now repeat for humidity + var humidityOffsets = []struct { + humidity physic.RelativeHumidity + expectedResult byte + }{ + {humidity: 19532 * physic.TenthMicroRH, expectedResult: 0x81}, + {humidity: -19532 * physic.TenthMicroRH, expectedResult: 0x01}, + {humidity: 820326 * physic.TenthMicroRH, expectedResult: 0xaa}, + {humidity: -24_805_1 * physic.MicroRH, expectedResult: 0x7f}, + } + for _, test := range humidityOffsets { + res := computeHumidityOffsetByte(test.humidity) + if res != test.expectedResult { + t.Errorf("computeHumidityOffsetByte() Offset: %s Expected Value: 0x%x Received: 0x%x", test.humidity, test.expectedResult, res) + } + } + +} + +func TestBasic(t *testing.T) { + dev, err := getDev(t, []i2ctest.IO{pbSense[0]}) + if err != nil { + t.Fatal(err) + } + env := &physic.Env{} + dev.Precision(env) + if env.Pressure != 0 { + t.Error("this device doesn't measure pressure") + } + if env.Temperature != (2670329 * physic.NanoKelvin) { + t.Errorf("incorrect temperature precision value got %d expected %d", env.Temperature, 2670329*physic.NanoKelvin) + } + if env.Humidity != 153*physic.TenthMicroRH { + t.Errorf("incorrect humidity precision got %d expected %d", env.Humidity, 153*physic.TenthMicroRH) + } + + s := dev.String() + if len(s) == 0 { + t.Error("invalid value for String()") + } +} + +func TestSense(t *testing.T) { + d, err := getDev(t, pbSense) + + if err != nil { + t.Fatalf("failed to initialize hdc302x: %v", err) + } + defer shutdown(t) + + // Read temperature and humidity from the sensor + e := physic.Env{} + if err := d.Sense(&e); err != nil { + t.Error(err) + } + t.Logf("%8s %9s", e.Temperature, e.Humidity) + + if !liveDevice { + // The playback temp is 26.621C Ensure that's what we got. + expected := physic.Temperature(299770889600) + if e.Temperature != expected { + t.Errorf("incorrect temperature value read. Expected: %s (%d) Found: %s (%d)", + expected.String(), + expected, + e.Temperature.String(), + e.Temperature, + ) + } + + // 23.2% expected. + expectedRH := 2324559 * physic.TenthMicroRH + if e.Humidity != expectedRH { + t.Errorf("incorrect humidity value read. Expected: %s (%d) Found: %s (%d)", + expectedRH.String(), + expectedRH, + e.Humidity.String(), + e.Humidity, + ) + } + } + +} + +func TestSenseContinuous(t *testing.T) { + + readCount := int32(10) + + // make 10 copies of the single reading playback data. + pb := make([]i2ctest.IO, 0, readCount+1) + pb = append(pb, pbSense[0]) + for range readCount { + pb = append(pb, pbSense[1]) + } + // Add in the halt + pb = append(pb, i2ctest.IO{Addr: DefaultSensorAddress, + W: []uint8{stopContinuousReadings[0], stopContinuousReadings[1]}}) + + dev, err := getDev(t, pb) + if err != nil { + t.Error(fmt.Errorf("failed to initialize hd302x: %w", err)) + } + defer shutdown(t) + + _, err = dev.SenseContinuous(100 * time.Millisecond) + if err == nil { + t.Error("expected error for sense continuous interval < sample interval") + } + + ch, err := dev.SenseContinuous(time.Second) + if err != nil { + t.Fatal(err) + } + + _, err = dev.SenseContinuous(time.Second) + if err == nil { + t.Error("expected an error for attempting concurrent SenseContinuous") + } + + counter := atomic.Int32{} + tEnd := time.Now().UnixMilli() + int64(readCount+2)*1000 + go func() { + for { + time.Sleep(100 * time.Millisecond) + // Stay here until we get the expected number of reads, or the time + // has expired. + if counter.Load() == readCount || time.Now().UnixMilli() > tEnd { + err := dev.Halt() + if err != nil { + t.Error(err) + } + return + } + } + }() + + for e := range ch { + counter.Add(1) + t.Log(time.Now(), e) + } + if counter.Load() != readCount { + t.Errorf("expected %d readings. received %d", readCount, counter.Load()) + } +} + +func TestConfiguration(t *testing.T) { + dev, err := getDev(t, pbConfiguration) + if err != nil { + t.Fatalf("failed to initialize hd302x: %v", err) + } + defer shutdown(t) + + cfg, err := dev.Configuration() + if err != nil { + t.Error(err) + } + s := cfg.String() + t.Log("configuration: ", s) + if len(s) == 0 { + t.Errorf("invalid Configuration.String()") + } + if cfg.SerialNumber == 0 { + t.Error("invalid serial number") + } + if cfg.VendorID != 0x3000 { + t.Errorf("invalid manufacturer id 0x%x", cfg.VendorID) + } +} + +// Tests applying offsets for temperature and humidity and checking that +// the values are applied during a subsequent read. +func TestOffsetModification(t *testing.T) { + dev, err := getDev(t, pbOffsets) + if err != nil { + t.Fatalf("failed to initialize hd302x: %v", err) + } + defer shutdown(t) + env := physic.Env{} + err = dev.Sense(&env) + if err != nil { + t.Fatal(err) + } + t.Logf("Initial Readings: %s", env) + cfg, err := dev.Configuration() + if err != nil { + t.Fatal(err) + } + cfg.TemperatureOffset += 10 * physic.Celsius + cfg.HumidityOffset -= 5 * physic.PercentRH + t.Log("writing configuration: ", cfg) + err = dev.SetConfiguration(cfg) + + if err != nil { + t.Fatal(err) + } + cfg2, _ := dev.Configuration() + t.Logf("read configuration=%s", cfg2) + env2 := physic.Env{} + err = dev.Sense(&env2) + if err != nil { + t.Error(err) + } + t.Log("Second Readings (post offset): ", env2) + if env2.Temperature < (env.Temperature+(9_500*physic.MilliKelvin)) || + env2.Temperature > (env.Temperature+(10_500*physic.MilliKelvin)) { + t.Errorf("offset temperature invalid. Expected ~ %s + 10C Got: %s", env.Temperature, env2.Temperature) + } + + lLow := env.Humidity - 6*physic.PercentRH + lHigh := env.Humidity - 4*physic.PercentRH + if (env2.Humidity < lLow) || + (env2.Humidity > lHigh) { + t.Errorf("offset humidity invalid. Expected ~ %s - 5%% Got: %s Lower Limit: %s, Upper Limit: %s", env.Humidity, env2.Humidity, lLow, lHigh) + } + + // Issue a soft reset to make sure any alterations are undone. + _ = dev.Reset() +} + +// Tests using alert values. +func TestAlerts(t *testing.T) { + + dev, err := getDev(t, pbAlerts) + if err != nil { + t.Fatalf("failed to initialize hd302x: %v", err) + } + defer shutdown(t) + env := physic.Env{} + err = dev.Sense(&env) + if err != nil { + t.Fatal(err) + } + t.Logf("Initial Temperature/Humidity Readings: %s", env) + cfg, err := dev.Configuration() + if err != nil { + t.Fatal(err) + } + + cfg.TemperatureOffset = 0 + cfg.HumidityOffset = 0 + cfg.AlertThresholds.Low.Temperature = 10 * physic.Celsius + cfg.AlertThresholds.High.Temperature = 75 * physic.Celsius + // Purposely set the low limit so it will trigger an alert on the status bit. + cfg.AlertThresholds.Low.Humidity = env.Humidity + 5*physic.PercentRH + cfg.AlertThresholds.High.Humidity = 75 * physic.PercentRH + + cfg.ClearThresholds.Low.Temperature = 15 * physic.Celsius + cfg.ClearThresholds.High.Temperature = 70 * physic.Celsius + cfg.ClearThresholds.Low.Humidity = cfg.AlertThresholds.Low.Humidity + 5*physic.PercentRH + cfg.ClearThresholds.High.Humidity = 70 * physic.PercentRH + t.Logf("Writing Configuration:\n%s", cfg) + // write the Alert levels + err = dev.SetConfiguration(cfg) + if err != nil { + t.Fatal(err) + } + // Re-read the configuration. + cfg2, err := dev.Configuration() + if err != nil { + t.Fatal(err) + } + t.Logf("re-read configuration = \n%s", cfg2) + // Trigger a read and then get the status word. We should have a humidity alert set... + _ = dev.Sense(&env) + status, err := dev.ReadStatus() + if err != nil { + t.Error(err) + } + _ = dev.Halt() + + // Verify things are within one lsb + if !cfg.AlertThresholds.Low.ApproximatelyEquals(&cfg2.AlertThresholds.Low) { + t.Errorf("error in low alert thresholds set: %s read: %s", cfg.AlertThresholds.Low.String(), cfg2.AlertThresholds.Low.String()) + } + if !cfg.AlertThresholds.High.ApproximatelyEquals(&cfg2.AlertThresholds.High) { + t.Errorf("error in high alert thresholds set: %s read: %s", cfg.AlertThresholds.High.String(), cfg2.AlertThresholds.High.String()) + } + if !cfg.ClearThresholds.Low.ApproximatelyEquals(&cfg2.ClearThresholds.Low) { + t.Errorf("error in low clear thresholds set: %s read: %s", cfg.ClearThresholds.Low.String(), cfg2.ClearThresholds.Low.String()) + } + if !cfg.ClearThresholds.High.ApproximatelyEquals(&cfg2.ClearThresholds.High) { + t.Errorf("error in high clear thresholds set: %s read: %s", cfg.ClearThresholds.High.String(), cfg2.ClearThresholds.High.String()) + } + + if status&StatusActiveAlerts != StatusActiveAlerts { + t.Error("expected status active alerts to be set") + } + if status&StatusRHTrackingAlert != StatusRHTrackingAlert { + t.Error("expected rh tracking alerts to be set") + } + if status&StatusRHLowTrackingAlert != StatusRHLowTrackingAlert { + t.Error("expected RH Low Tracking alert status bit to be set") + } + + // Issue a soft reset to make sure any alterations are undone. + _ = dev.Reset() +} + +// TestHeater turns on the sensor's integrated heater. The heater +// can be used to remove condensation from the sensor. +func TestHeater(t *testing.T) { + dev, err := getDev(t, pbHeater) + if err != nil { + t.Fatalf("failed to initialize hd302x: %v", err) + } + defer shutdown(t) + + err = dev.SetHeater(PowerFull + 1) + if err == nil { + t.Error("expected error with invalid power value") + } + env := physic.Env{} + env2 := physic.Env{} + err = dev.Sense(&env) + if err != nil { + t.Fatal(err) + } + t.Logf("initial temperature: %s Humidity: %s", env.Temperature, env.Humidity) + err = dev.SetHeater(PowerFull) + defer func() { + if err := dev.SetHeater(PowerOff); err != nil { + t.Error(err) + } + }() + if err != nil { + t.Fatal(err) + } + if liveDevice { + for range 5 { + t.Log("Sleeping 5 seconds to test heater...") + time.Sleep(time.Second) + } + } + err = dev.Sense(&env2) + + if err != nil { + t.Error(err) + } + t.Logf("final temperature after heater enabled: %s Humidity: %s", env2.Temperature, env2.Humidity) + if env2.Temperature <= env.Temperature { + t.Errorf("expected heater to increase sensor temperature. Initial: %s Final: %s", env.Temperature, env2.Temperature) + } +} From a94de04d96e243d7374dbd5a420dddde1dd9220e Mon Sep 17 00:00:00 2001 From: George Sexton Date: Sat, 7 Dec 2024 13:59:33 -0700 Subject: [PATCH 3/4] Fix test failures --- hdc302x/hdc302x.go | 10 +++++----- hdc302x/hdc302x_test.go | 9 +++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/hdc302x/hdc302x.go b/hdc302x/hdc302x.go index f16ee10..c74266e 100644 --- a/hdc302x/hdc302x.go +++ b/hdc302x/hdc302x.go @@ -159,7 +159,7 @@ var writeHighAlertThresholds = devCommand{0x61, 0x1d} var writeLowClearThresholds = devCommand{0x61, 0x0b} var writeHighClearThresholds = devCommand{0x61, 0x16} -var invalidCRCError = errors.New("hdc302x: invalid crc") +var errInvalidCRC = errors.New("hdc302x: invalid crc") const ( // Magic numbers for count to value conversions. @@ -252,7 +252,7 @@ func (dev *Dev) Sense(env *physic.Env) error { return fmt.Errorf("hdc302x: %w", err) } if crc8(res[:2]) != res[2] || crc8(res[3:5]) != res[5] { - return invalidCRCError + return errInvalidCRC } env.Temperature = countToTemperature(res) env.Humidity = countToHumidity(res[3:]) @@ -359,7 +359,7 @@ func (dev *Dev) readAlertValues(cfg *Configuration) error { return err } if crc8(r[:2]) != r[2] { - return invalidCRCError + return errInvalidCRC } wValue := uint16(r[0])<<8 | uint16(r[1]) // The alert value is returned as a 16 bit words, where bits 0-8 are the @@ -382,7 +382,7 @@ func (dev *Dev) readOffsets(cfg *Configuration) error { return fmt.Errorf("hdc302x: %w", err) } if crc8(r[:2]) != r[2] { - return invalidCRCError + return errInvalidCRC } // The result comes back as the humidity offset, followed by @@ -425,7 +425,7 @@ func (dev *Dev) ReadStatus() (StatusWord, error) { return 0, err } if crc8(r[:2]) != r[2] { - return 0, invalidCRCError + return 0, errInvalidCRC } _ = dev.d.Tx(clearStatus, nil) return StatusWord(r[0])<<8 | StatusWord(r[1]), nil diff --git a/hdc302x/hdc302x_test.go b/hdc302x/hdc302x_test.go index 761b25d..eb512d2 100644 --- a/hdc302x/hdc302x_test.go +++ b/hdc302x/hdc302x_test.go @@ -8,6 +8,7 @@ import ( "fmt" "math" "os" + "sync" "sync/atomic" "testing" "time" @@ -391,6 +392,8 @@ func TestSenseContinuous(t *testing.T) { counter := atomic.Int32{} tEnd := time.Now().UnixMilli() + int64(readCount+2)*1000 + wg := sync.WaitGroup{} + wg.Add(1) go func() { for { time.Sleep(100 * time.Millisecond) @@ -401,6 +404,7 @@ func TestSenseContinuous(t *testing.T) { if err != nil { t.Error(err) } + wg.Done() return } } @@ -413,6 +417,7 @@ func TestSenseContinuous(t *testing.T) { if counter.Load() != readCount { t.Errorf("expected %d readings. received %d", readCount, counter.Load()) } + wg.Wait() } func TestConfiguration(t *testing.T) { @@ -590,8 +595,8 @@ func TestHeater(t *testing.T) { t.Logf("initial temperature: %s Humidity: %s", env.Temperature, env.Humidity) err = dev.SetHeater(PowerFull) defer func() { - if err := dev.SetHeater(PowerOff); err != nil { - t.Error(err) + if errOff := dev.SetHeater(PowerOff); errOff != nil { + t.Error(errOff) } }() if err != nil { From 890d624f55673e35c054cf5517e41d0132c36a49 Mon Sep 17 00:00:00 2001 From: George Sexton Date: Sat, 7 Dec 2024 14:19:42 -0700 Subject: [PATCH 4/4] Fix race conditions in test SenseContinuous() --- scd4x/scd4x_test.go | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/scd4x/scd4x_test.go b/scd4x/scd4x_test.go index 76bc555..95b3f00 100644 --- a/scd4x/scd4x_test.go +++ b/scd4x/scd4x_test.go @@ -12,6 +12,8 @@ package scd4x import ( "fmt" "os" + "sync" + "sync/atomic" "testing" "time" @@ -235,7 +237,7 @@ func TestSense(t *testing.T) { } func TestSenseContinuous(t *testing.T) { - readings := 6 + readings := int32(6) timeBase := time.Second if liveDevice { timeBase *= 10 @@ -244,26 +246,36 @@ func TestSenseContinuous(t *testing.T) { if err != nil { t.Fatal(err) } - defer func() { _ = dev.Halt() }() + defer shutdown(t) t.Log("dev.sensing=", dev.sensing) ch, err := dev.SenseContinuous(timeBase) if err != nil { t.Error(err) } - + received := atomic.Int32{} + wg := sync.WaitGroup{} + wg.Add(1) + tEnd := time.Now().UnixMilli() + int64(readings+2)*1000 go func() { - time.Sleep(time.Duration(readings) * timeBase) - _ = dev.Halt() + for { + if received.Load() == readings || time.Now().UnixMilli() > tEnd { + _ = dev.Halt() + wg.Done() + break + } + } }() - received := 0 + for env := range ch { + received.Add(1) t.Log(env.String()) - received += 1 + } - if received < (readings-1) || received > readings { - t.Errorf("SenseContinuous() expected at least %d readings, got %d", readings-1, received) + if received.Load() != readings { + t.Errorf("SenseContinuous() expected at least %d readings, got %d", readings-1, received.Load()) } + wg.Wait() } @@ -335,7 +347,7 @@ func TestGetSetConfiguration(t *testing.T) { // previously programmed into the device. func TestPersistAndResetFactory(t *testing.T) { if !liveDevice || os.Getenv("SCDRESET") == "" { - t.Skip("using live device and SCDRESET not defined. skipping") + t.Skip("not using live device or SCDRESET not defined. skipping") } dev, err := getDev(t) if err != nil {