Add TI HDC302x Sensor Support (#83)

* Fix race conditions in test SenseContinuous()

Fixes #84
pull/86/head
gsexton 1 year ago committed by GitHub
parent 7fd42d5ebf
commit 72857e8c3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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)
}

@ -0,0 +1,679 @@
// 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. This is a high accuracy sensor with
// very good resolution.
//
// Datasheet
//
// https://www.ti.com/lit/ds/symlink/hdc3022.pdf
package hdc302x
import (
"errors"
"fmt"
"math"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
)
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 errInvalidCRC = 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 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 errInvalidCRC
}
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 measure. Refer to the datasheet for information on limits and
// accuracy.
func (dev *Dev) Precision(env *physic.Env) {
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 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
// 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 errInvalidCRC
}
// 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, errInvalidCRC
}
_ = 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)<<bit) * temperatureScalar) / scaleDivisor
if fTemp >= 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)<<bit) * humidityScalar) / scaleDivisor
if fHumidity >= 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<<bit)) * temperatureScalar) / scaleDivisor
if temp >= bitVal {
temp -= bitVal
tempBits |= (1 << bit)
}
}
humidity := humidityToFloat64(th.Humidity)
humBits := uint16(0)
for bit := 15; bit >= 0; bit-- {
bitVal := (float64(uint16(1<<bit)) * humidityScalar) / scaleDivisor
if humidity >= 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())
}
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{}

@ -0,0 +1,620 @@
// 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"
"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
wg := sync.WaitGroup{}
wg.Add(1)
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)
}
wg.Done()
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())
}
wg.Wait()
}
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 errOff := dev.SetHeater(PowerOff); errOff != nil {
t.Error(errOff)
}
}()
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)
}
}

@ -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)
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 {

Loading…
Cancel
Save