From 8a85f99127a72c50f67b85ea60bd5363e7324838 Mon Sep 17 00:00:00 2001 From: George Sexton Date: Tue, 27 May 2025 16:03:02 -0600 Subject: [PATCH] Introduces driver for Sensirion SHT-4X Temperature/Humidity Sensors. Move CRC8 logic into common package for resuability. --- common/crc.go | 24 ++ common/crc_test.go | 24 ++ hdc302x/hdc302x.go | 32 +-- hdc302x/hdc302x_test.go | 16 -- scd4x/scd4x.go | 21 +- scd4x/scd4x_test.go | 16 -- sht4x/example_test.go | 44 ++++ sht4x/sht4x.go | 328 ++++++++++++++++++++++++++++ sht4x/sht4x_test.go | 209 ++++++++++++++++++ sht4x/sht4xbustestrecording_test.go | 94 ++++++++ 10 files changed, 735 insertions(+), 73 deletions(-) create mode 100644 common/crc.go create mode 100644 common/crc_test.go create mode 100644 sht4x/example_test.go create mode 100644 sht4x/sht4x.go create mode 100644 sht4x/sht4x_test.go create mode 100644 sht4x/sht4xbustestrecording_test.go diff --git a/common/crc.go b/common/crc.go new file mode 100644 index 0000000..77aa3de --- /dev/null +++ b/common/crc.go @@ -0,0 +1,24 @@ +// Copyright 2025 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 common contains functions used across multiple packages. For +// example, a CRC8 calculation +package common + +// CRC8 calculates the 8-bit CRC of the byte slice parameter and returns the +// calculated value. CRC bytes are used in sensors from TI and Sensirion. +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 +} diff --git a/common/crc_test.go b/common/crc_test.go new file mode 100644 index 0000000..cee944e --- /dev/null +++ b/common/crc_test.go @@ -0,0 +1,24 @@ +// Copyright 2025 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 common + +import "testing" + +func TestCRC8(t *testing.T) { + var tests = []struct { + bytes []byte + result byte + }{ + {bytes: []byte{0xbe, 0xef}, result: 0x92}, + {bytes: []byte{0x01, 0xa4}, result: 0x4d}, + {bytes: []byte{0xab, 0xcd}, result: 0x6f}, + } + for _, test := range tests { + res := CRC8(test.bytes) + if res != test.result { + t.Errorf("CRC8(%#v)!=0x%d received 0x%d", test.bytes, test.result, res) + } + } +} diff --git a/hdc302x/hdc302x.go b/hdc302x/hdc302x.go index c74266e..e36ff8f 100644 --- a/hdc302x/hdc302x.go +++ b/hdc302x/hdc302x.go @@ -21,6 +21,7 @@ import ( "periph.io/x/conn/v3" "periph.io/x/conn/v3/i2c" "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/common" ) type SampleRate uint16 @@ -203,21 +204,6 @@ func countToHumidity(bytes []byte) physic.RelativeHumidity { 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 { @@ -251,7 +237,7 @@ func (dev *Dev) Sense(env *physic.Env) error { 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] { + if common.CRC8(res[:2]) != res[2] || common.CRC8(res[3:5]) != res[5] { return errInvalidCRC } env.Temperature = countToTemperature(res) @@ -320,7 +306,7 @@ func (dev *Dev) readSerialNumber() int64 { // 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]) { + if err != nil || (common.CRC8(r[:2]) != r[2]) { return result } result = result<<16 | (int64(r[0])<<8 | int64(r[1])) @@ -358,7 +344,7 @@ func (dev *Dev) readAlertValues(cfg *Configuration) error { if err != nil { return err } - if crc8(r[:2]) != r[2] { + if common.CRC8(r[:2]) != r[2] { return errInvalidCRC } wValue := uint16(r[0])<<8 | uint16(r[1]) @@ -381,7 +367,7 @@ func (dev *Dev) readOffsets(cfg *Configuration) error { if err := dev.d.Tx(readSetOffsets, r); err != nil { return fmt.Errorf("hdc302x: %w", err) } - if crc8(r[:2]) != r[2] { + if common.CRC8(r[:2]) != r[2] { return errInvalidCRC } @@ -424,7 +410,7 @@ func (dev *Dev) ReadStatus() (StatusWord, error) { if err := dev.d.Tx(readStatus, r); err != nil { return 0, err } - if crc8(r[:2]) != r[2] { + if common.CRC8(r[:2]) != r[2] { return 0, errInvalidCRC } _ = dev.d.Tx(clearStatus, nil) @@ -463,7 +449,7 @@ func (dev *Dev) setOffsets(cfg *Configuration) error { computeTemperatureOffsetByte(cfg.TemperatureOffset), 0, } - w[4] = crc8(w[2:4]) + w[4] = common.CRC8(w[2:4]) return dev.d.Tx(w, nil) } @@ -553,7 +539,7 @@ func (dev *Dev) setThresholds(typeAlert bool, tp *ThresholdPair) error { 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]) + w[4] = common.CRC8(w[2:4]) err := dev.d.Tx(w, nil) if err != nil { return err @@ -609,7 +595,7 @@ func (dev *Dev) SetHeater(powerLevel HeaterPower) error { byte((powerLevel >> 8) & 0xff), byte(powerLevel & 0xff), 0} - setValue[4] = crc8(setValue[2:4]) + setValue[4] = common.CRC8(setValue[2:4]) err := dev.d.Tx(setValue, nil) if err != nil { return err diff --git a/hdc302x/hdc302x_test.go b/hdc302x/hdc302x_test.go index eb512d2..669ac6d 100644 --- a/hdc302x/hdc302x_test.go +++ b/hdc302x/hdc302x_test.go @@ -200,22 +200,6 @@ func shutdown(t *testing.T) { } } -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) { diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index 14128fe..e5690ca 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -12,6 +12,7 @@ import ( "periph.io/x/conn/v3/i2c" "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/common" ) // PPM=Parts Per Million. Units of measure for CO2 concentration. @@ -402,22 +403,6 @@ func (d *Dev) Reset(mode ResetMode) error { return err } -func calcCRC(bytes []byte) byte { - polynomial := byte(0x31) - crc := byte(0xff) - for ix := range len(bytes) { - crc ^= bytes[ix] - for crc_bit := byte(8); crc_bit > 0; crc_bit-- { - if (crc & 0x80) == 0x80 { - crc = (crc << 1) ^ polynomial - } else { - crc = (crc << 1) - } - } - } - return crc -} - // makeWriteData converts the slice of word values into byte values with the // CRC following. func makeWriteData(data []uint16) []byte { @@ -425,7 +410,7 @@ func makeWriteData(data []uint16) []byte { for ix, val := range data { bytes[ix*3] = byte((val >> 8) & 0xff) bytes[ix*3+1] = byte(val & 0xff) - bytes[ix*3+2] = calcCRC(bytes[ix*3 : ix*3+2]) + bytes[ix*3+2] = common.CRC8(bytes[ix*3 : ix*3+2]) } return bytes } @@ -464,7 +449,7 @@ func (d *Dev) sendCommand(cmd command, writeData []uint16) ([]uint16, error) { // verify the CRC as we go. result := make([]uint16, cmd.responseSize/3) for ix := range len(result) { - crc := calcCRC(r[ix*3 : ix*3+2]) + crc := common.CRC8(r[ix*3 : ix*3+2]) if r[ix*3+2] != crc { return nil, fmt.Errorf("scd4x cmd 0x%x: invalid crc", cmd.cmdWord) } diff --git a/scd4x/scd4x_test.go b/scd4x/scd4x_test.go index 95b3f00..fc71b05 100644 --- a/scd4x/scd4x_test.go +++ b/scd4x/scd4x_test.go @@ -154,22 +154,6 @@ func shutdown(t *testing.T) { } } -func TestCRC(t *testing.T) { - tests := []struct { - bytes []byte - crc byte - }{ - {bytes: []byte{0xbe, 0xef}, crc: 0x92}, - {bytes: []byte{0x01, 0xa4}, crc: 0x4d}, - } - for _, test := range tests { - res := calcCRC(test.bytes) - if res != test.crc { - t.Error(fmt.Errorf("crc calculation error bytes: %#v, result: 0x%x expected: 0x%x", test.bytes, res, test.crc)) - } - } -} - func TestCountToTemperature(t *testing.T) { tests := []struct { count uint16 diff --git a/sht4x/example_test.go b/sht4x/example_test.go new file mode 100644 index 0000000..c21f5ed --- /dev/null +++ b/sht4x/example_test.go @@ -0,0 +1,44 @@ +// Copyright 2025 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 sht4x_test + +import ( + "log" + "time" + + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/sht4x" + "periph.io/x/host/v3" +) + +// Example shows creating an SHT-4X sensor and reading from it. +func Example() { + if _, err := host.Init(); err != nil { + log.Fatal("Error calling host.init()") + } + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + + dev, err := sht4x.New(bus, sht4x.DefaultAddress) + if err != nil { + log.Fatal(err) + } + + env := &physic.Env{} + + for range 10 { + err = dev.Sense(env) + if err != nil { + log.Println(err) + } else { + log.Printf("Temperature: %s Humidity: %s\n", env.Temperature, env.Humidity) + } + time.Sleep(time.Second) + } +} diff --git a/sht4x/sht4x.go b/sht4x/sht4x.go new file mode 100644 index 0000000..f590ae0 --- /dev/null +++ b/sht4x/sht4x.go @@ -0,0 +1,328 @@ +// Copyright 2025 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. + +// sht4x is a package for interfacing with the Sensirion SHT-40, SHT-41, and +// SHT-45 sensors. +// +// # Datasheet +// +// https://sensirion.com/media/documents/33FD6951/67EB9032/HT_DS_Datasheet_SHT4x_5.pdf +// +// # Temperature Accuracy +// +// SHT-40 & SHT-41 +// +// Typical accuracy: ±0.2 °C +// +// Response time τ₆₃% ≈ 2 s +// +// SHT-45 +// +// Typical accuracy: ±0.1 °C +// +// Response time τ₆₃% ≈ 2 s +// +// # Humidity Accuracy +// +// SHT-40 (Base‑class) +// +// Typical accuracy at 25 °C: ±1.8 % RH +// +// Maximum accuracy (at 25 °C): up to ±3.5 % RH +// +// SHT-41 (Intermediate‑class) +// +// Typical accuracy at 25 °C: ±1.8 % RH +// +// Maximum accuracy (at 25 °C): up to ±2.5 % RH +// +// SHT-45 (High‑accuracy‑class) +// +// Typical accuracy at 25 °C: ±1.0 % RH +// +// Maximum accuracy (at 25 °C): up to ≈±1.75 % RH +// +// All three share a resolution of 0.01 % RH, a response time τ₆₃% ≈ 4 s, and long‑term drift < 0.2 % RH/year . +// +// All devices have a resolution of 0.01 °C and specified range –40…+125 °C . +package sht4x + +import ( + "errors" + "fmt" + "sync" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/common" +) + +// HeaterPower represents a type for the heater power setting. +type HeaterPower int + +// HeaterDuration represents a duration for turning the heater on. +type HeaterDuration time.Duration + +const ( + // Power settings for the heater element. + Power20mW HeaterPower = iota + Power110mW + Power200mW + + // Durations that you can turn the heater on for. + Duration100ms HeaterDuration = HeaterDuration(time.Duration(100 * time.Millisecond)) + Duration1s HeaterDuration = HeaterDuration(time.Second) + + // Default I2C Address + DefaultAddress i2c.Addr = 0x44 +) + +const ( + // byte commands for device. + cmdHeater200mW1s byte = 0x39 + cmdHeater200mW100ms byte = 0x32 + cmdHeater110mW1s byte = 0x2f + cmdHeater110mW100ms byte = 0x24 + cmdHeater20mW1s byte = 0x1e + cmdHeater20mW100ms byte = 0x15 + + cmdSoftReset byte = 0x94 + // Read at highest precision and repeatability + cmdMeasure byte = 0xfd + cmdReadSerialNumber byte = 0x89 + + countDivisor = float64(65535) + + minTemperature = -40*physic.Kelvin + physic.ZeroCelsius + maxTemperature = 125*physic.Kelvin + physic.ZeroCelsius + + minRH = 0 * physic.PercentRH + maxRH = 100 * physic.PercentRH + + minSampleDuration = 10 * time.Millisecond +) + +// Dev represents a SHT-4X series temperature/humidity sensor +type Dev struct { + d *i2c.Dev + shutdown chan struct{} + mu sync.Mutex +} + +func New(bus i2c.Bus, addr i2c.Addr) (*Dev, error) { + dev := &Dev{d: &i2c.Dev{Bus: bus, Addr: uint16(addr)}} + return dev, nil +} + +// If you try to read immediately after a write with this device, you'll get an +// io error. This just wraps the write and adds a delay before attempting the +// read. +func (dev *Dev) txWithDelay(w, r *[]byte, delay time.Duration) (err error) { + if w != nil { + err = dev.d.Tx(*w, nil) + if err != nil { + err = fmt.Errorf("sht4x: error transmitting %w", err) + return + } + } + time.Sleep(delay) + if r != nil { + err = dev.d.Tx(nil, *r) + if err != nil { + err = fmt.Errorf("sht4x: error reading %w", err) + } + // All calls that return bytes return the same format. 2 bytes + // of data, a CRC, 2 bytes of data, and + // a CRC. Verify them + if common.CRC8((*r)[:2]) != (*r)[2] { + err = errors.New("sht4x: bytes[:2] read crc error") + } + if err == nil && common.CRC8((*r)[3:5]) != (*r)[5] { + err = errors.New("sht4x: bytes[3:5] read crc error") + } + } + return +} + +// convert the count to a temperature value. +func countToTemp(count uint16) physic.Temperature { + // T=-45+175*(count/countDivisor) + val := physic.Temperature(float64(physic.Kelvin)*(-45.0+175.0*(float64(count)/countDivisor))) + physic.ZeroCelsius + if val < minTemperature { + val = minTemperature + } else if val > maxTemperature { + val = maxTemperature + } + return val +} + +func countToHumidity(count uint16) physic.RelativeHumidity { + // RH=-6 + 125*(count/countDivisor) + val := physic.RelativeHumidity((-6.0 + 125.0*(float64(count)/countDivisor)) * float64(physic.PercentRH)) + if val < minRH { + val = minRH + } else if val > maxRH { + val = maxRH + } + return val +} + +// Precision returns the smallest change in readings the device can produce. +// Implements physic.SenseEnv. +func (dev *Dev) Precision(e *physic.Env) { + e.Temperature = physic.Kelvin / 100 + e.Humidity = physic.PercentRH / 100 + e.Pressure = 0 +} + +// Halt shuts down the device and terminates a SenseContinuous +// command if running. Implements conn.Resource +func (dev *Dev) Halt() error { + dev.mu.Lock() + defer dev.mu.Unlock() + if dev.shutdown != nil { + close(dev.shutdown) + } + return nil +} + +// Reset issues a soft-reset to the device +func (dev *Dev) Reset() error { + dev.mu.Lock() + defer dev.mu.Unlock() + err := dev.d.Tx([]byte{cmdSoftReset}, nil) + if err != nil { + err = fmt.Errorf("sht4x: error resetting %w", err) + } + time.Sleep(2 * time.Millisecond) + return err +} + +// Sense reads temperature and humidity from the device. +func (dev *Dev) Sense(e *physic.Env) error { + e.Pressure = 0 + r := make([]byte, 6) + w := []byte{cmdMeasure} + err := dev.txWithDelay(&w, &r, 10*time.Millisecond) + if err != nil { + e.Temperature = minTemperature + e.Humidity = minRH + return fmt.Errorf("sht4x: error reading device %w", err) + } + e.Temperature = countToTemp(uint16(r[0])<<8 | uint16(r[1])) + e.Humidity = countToHumidity(uint16(r[3])<<8 | uint16(r[4])) + return nil +} + +// SenseContinuous continuously reads from the device and sends the output +// to the returned channel. To terminate the read, call Dev.Halt() +func (dev *Dev) SenseContinuous(duration time.Duration) (<-chan physic.Env, error) { + + if dev.shutdown != nil { + return nil, errors.New("sht4x: SenseContinuous already running") + } + + if duration < minSampleDuration { + return nil, errors.New("sht4x: sample interval is < device sample rate") + } + dev.shutdown = make(chan struct{}) + ch := make(chan (physic.Env), 16) + go func(ch chan<- physic.Env) { + ticker := time.NewTicker(duration) + defer ticker.Stop() + defer close(ch) + for { + select { + case <-dev.shutdown: + dev.mu.Lock() + defer dev.mu.Unlock() + dev.shutdown = nil + return + case <-ticker.C: + env := physic.Env{} + if err := dev.Sense(&env); err == nil { + ch <- env + } + } + } + }(ch) + return ch, nil +} + +// SerialNumber returns the device serial number set at the factory. +func (dev *Dev) SerialNumber() (uint32, error) { + r := make([]byte, 6) + w := []byte{cmdReadSerialNumber} + dev.mu.Lock() + defer dev.mu.Unlock() + err := dev.txWithDelay(&w, &r, 10*time.Millisecond) + if err != nil { + return 0, err + } + result := uint32(r[0])<<24 | uint32(r[1])<<16 | uint32(r[3])<<8 | uint32(r[4]) + return result, nil +} + +// SetHeater enables the sensor's heater. You can specify the power level, and +// the duration. After duration has passed, the heater will be turned off +// automatically. Enabling the heater can allow operation in condensing +// environments. +// +// powerLevel is one of the HeaterPower constants, and duration is one of the +// heaterDuration constants, either 100ms, or 1000ms. +// +// Returns the temperature and humidity after the period has completed. Refer to +// section 4.9 of the datasheet. +func (dev *Dev) SetHeater(powerLevel HeaterPower, duration HeaterDuration) (physic.Env, error) { + env := physic.Env{Temperature: minTemperature, Humidity: minRH} + var cmd byte + switch duration { + case Duration100ms: + switch powerLevel { + case Power20mW: + cmd = cmdHeater20mW100ms + case Power110mW: + cmd = cmdHeater110mW100ms + case Power200mW: + cmd = cmdHeater200mW100ms + default: + return env, errors.New("sht4x: invalid heater power") + } + case Duration1s: + switch powerLevel { + case Power20mW: + cmd = cmdHeater20mW1s + case Power110mW: + cmd = cmdHeater110mW1s + case Power200mW: + cmd = cmdHeater200mW1s + default: + return env, errors.New("sht4x: invalid heater power") + } + default: + return env, errors.New("sht4x: invalid heater duration") + } + r := make([]byte, 6) + w := []byte{cmd} + dev.mu.Lock() + defer dev.mu.Unlock() + err := dev.txWithDelay(&w, &r, time.Duration(duration)+10*time.Millisecond) + if err != nil { + return env, fmt.Errorf("sht4x: error setting heater %w", err) + } + env.Temperature = countToTemp(uint16(r[0])<<8 | uint16(r[1])) + env.Humidity = countToHumidity(uint16(r[3])<<8 | uint16(r[4])) + + return env, nil +} + +// String returns a string representation of the device. +func (dev *Dev) String() string { + return "sht4x" +} + +var _ conn.Resource = &Dev{} +var _ physic.SenseEnv = &Dev{} diff --git a/sht4x/sht4x_test.go b/sht4x/sht4x_test.go new file mode 100644 index 0000000..2e6b1ad --- /dev/null +++ b/sht4x/sht4x_test.go @@ -0,0 +1,209 @@ +// Copyright 2025 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 sht4x + +import ( + "math" + "sync" + "sync/atomic" + "testing" + "time" + + "periph.io/x/conn/v3/i2c/i2ctest" + "periph.io/x/conn/v3/physic" +) + +var liveDevice bool + +func getDev(testName string) (*Dev, error) { + return New(&i2ctest.Playback{Ops: recordingData[testName], DontPanic: true}, DefaultAddress) +} + +func TestBasic(t *testing.T) { + t.Logf("liveDevice=%t", liveDevice) + dev, err := getDev(t.Name()) + if err != nil { + t.Fatal(err) + } + + // Test String + s := dev.String() + if len(s) == 0 { + t.Error("string returned empty") + } + + // Test Serial Number + sn, err := dev.SerialNumber() + if err == nil { + if sn == 0 { + t.Error("invalid serial number") + } else { + t.Logf("SerialNumber=0x%x", sn) + } + } else { + t.Error(err) + } +} + +func TestCountToTemp(t *testing.T) { + temp := countToTemp(0) + if temp != minTemperature { + t.Errorf("invalid temperature %s. Expected -40", temp) + } + temp = countToTemp(0xffff) + if temp != maxTemperature { + t.Errorf("invalid temperature %s. Expected 125", temp) + } + temp = countToTemp(0x8000) + tTest := 42.5 + physic.ZeroCelsius.Celsius() + diff := physic.Temperature(math.Abs(tTest-float64(temp.Celsius()))) * physic.Kelvin + if diff > 2*physic.MilliKelvin { + t.Errorf("invalid temperature expected %f. got %s diff=%s", tTest, temp, diff) + } +} + +func TestCountToHumidity(t *testing.T) { + rh := countToHumidity(0) + if rh != minRH { + t.Errorf("received RH %s expected %s", rh, minRH) + } + rh = countToHumidity(0xffff) + if rh != maxRH { + t.Errorf("received RH %s expected %s", rh, maxRH) + } + rh = countToHumidity(0x8000) + expected := physic.RelativeHumidity(56.5 * float64(physic.PercentRH)) + diff := rh - expected + if diff > 2*physic.MilliRH { + t.Errorf("received rh %s expected %s diff=%v", rh, expected, diff) + } +} + +// Test turning on the heater at various power levels and durations. +func TestHeater(t *testing.T) { + dev, err := getDev(t.Name()) + if err != nil { + t.Fatal(err) + } + // Test Invalid parameters + _, err = dev.SetHeater(Power20mW, HeaterDuration(10*time.Second)) + if err == nil { + t.Error("SetHeater() invalid duration did not generate error.") + } + _, err = dev.SetHeater(HeaterPower(500), Duration100ms) + if err == nil { + t.Error("SetHeater() invalid power level did not generate error.") + } + initEnv := &physic.Env{} + // Iterate over the allowed durations + for _, duration := range []HeaterDuration{Duration100ms, Duration1s} { + // Iterate over the supported heater power levels + for _, power := range []HeaterPower{Power20mW, Power110mW, Power200mW} { + // Read the initial temperature at the test start. + err := dev.Sense(initEnv) + if err != nil { + t.Error(err) + continue + } + // Turn the heater on 3 times + var diffLast float64 + for range 3 { + env, err := dev.SetHeater(power, duration) + if err != nil { + t.Error(err) + break + } + // Confirm that the difference between the initial temperature and + // the temperature after the heater was turned on is > 0 + diff := env.Temperature.Celsius() - initEnv.Temperature.Celsius() + t.Logf("initTemp=%s heaterTemp=%s, diff=%f", initEnv.Temperature, env.Temperature, diff) + if diff <= 0 || diff <= diffLast { + t.Errorf("heater error power=%d, Duration=%v diff=%f expected > 0", power, duration, diff) + } + diffLast = diff + } + if liveDevice { + // Give the thermometer core time to cool off + time.Sleep(10 * time.Second) + } + } + } +} + +func TestReset(t *testing.T) { + dev, err := getDev(t.Name()) + if err != nil { + t.Fatal(err) + } + err = dev.Reset() + if err != nil { + t.Error(err) + } +} + +func TestSense(t *testing.T) { + dev, err := getDev(t.Name()) + if err != nil { + t.Fatal(err) + } + env := &physic.Env{} + err = dev.Sense(env) + if err != nil { + t.Error(err) + } + t.Logf("env=%#v temperature=%s humidity=%s", *env, env.Temperature.String(), env.Humidity.String()) +} + +func TestSenseContinuous(t *testing.T) { + readCount := int32(10) + dev, err := getDev(t.Name()) + if err != nil { + t.Fatal(err) + } + _, err = dev.SenseContinuous(time.Millisecond) + if err == nil { + t.Error("SenseContinuous() doesn't return an error on too short a duration.") + } + ch, err := dev.SenseContinuous(100 * time.Millisecond) + 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)*100 + 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 and when we do, Halt the SenseContinuous. + if counter.Load() >= readCount || time.Now().UnixMilli() >= tEnd { + t.Logf("calling halt!") + err := dev.Halt() + t.Logf("halt() returned") + if err != nil { + t.Error(err) + } + wg.Done() + return + } + } + }() + // Iterate over the channel until it's closed. + for e := range ch { + counter.Add(1) + t.Log(time.Now(), e, "count=", counter.Load()) + } + if counter.Load() < readCount || counter.Load() > (readCount+1) { + t.Errorf("expected %d readings. received %d", readCount, counter.Load()) + } + wg.Wait() +} diff --git a/sht4x/sht4xbustestrecording_test.go b/sht4x/sht4xbustestrecording_test.go new file mode 100644 index 0000000..fc3f5eb --- /dev/null +++ b/sht4x/sht4xbustestrecording_test.go @@ -0,0 +1,94 @@ +// Copyright 2025 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 sht4x + +import ( + "periph.io/x/conn/v3/i2c/i2ctest" +) + +// Auto-Generated by i2ctest.BusTest + +var recordingData = map[string][]i2ctest.IO{ + "TestBasic": { + {Addr: 0x44, W: []uint8{0x89}}, + {Addr: 0x44, R: []uint8{0xd, 0x20, 0x47, 0x61, 0x1, 0x11}}}, + "TestHeater": { + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x63, 0x36, 0x9a, 0x5a, 0x7c, 0xd}}, + {Addr: 0x44, W: []uint8{0x15}}, + {Addr: 0x44, R: []uint8{0x67, 0xa0, 0x86, 0x5a, 0xf9, 0x82}}, + {Addr: 0x44, W: []uint8{0x15}}, + {Addr: 0x44, R: []uint8{0x69, 0x28, 0x28, 0x5b, 0x27, 0x6d}}, + {Addr: 0x44, W: []uint8{0x15}}, + {Addr: 0x44, R: []uint8{0x6a, 0x26, 0x1a, 0x5b, 0x16, 0x99}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x63, 0x4a, 0x1f, 0x5a, 0x53, 0xa5}}, + {Addr: 0x44, W: []uint8{0x24}}, + {Addr: 0x44, R: []uint8{0x79, 0x7c, 0xfc, 0x5c, 0x84, 0x6c}}, + {Addr: 0x44, W: []uint8{0x24}}, + {Addr: 0x44, R: []uint8{0x81, 0x14, 0xd1, 0x5c, 0x64, 0xad}}, + {Addr: 0x44, W: []uint8{0x24}}, + {Addr: 0x44, R: []uint8{0x85, 0xd2, 0xb3, 0x5a, 0x5d, 0xba}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x63, 0x7f, 0x2f, 0x5b, 0x84, 0xc2}}, + {Addr: 0x44, W: []uint8{0x32}}, + {Addr: 0x44, R: []uint8{0x89, 0x3c, 0xd9, 0x5e, 0xae, 0xe8}}, + {Addr: 0x44, W: []uint8{0x32}}, + {Addr: 0x44, R: []uint8{0x95, 0xe0, 0x7a, 0x5c, 0xbe, 0x72}}, + {Addr: 0x44, W: []uint8{0x32}}, + {Addr: 0x44, R: []uint8{0x9d, 0xaa, 0xab, 0x55, 0xc0, 0x56}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x63, 0xbb, 0xac, 0x57, 0xed, 0x45}}, + {Addr: 0x44, W: []uint8{0x1e}}, + {Addr: 0x44, R: []uint8{0x6d, 0x4a, 0x72, 0x55, 0x53, 0x3c}}, + {Addr: 0x44, W: []uint8{0x1e}}, + {Addr: 0x44, R: []uint8{0x6e, 0x1e, 0xe5, 0x4e, 0x61, 0xef}}, + {Addr: 0x44, W: []uint8{0x1e}}, + {Addr: 0x44, R: []uint8{0x6e, 0x7e, 0x5e, 0x49, 0x91, 0xc3}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x63, 0xc5, 0x4b, 0x57, 0xeb, 0xe3}}, + {Addr: 0x44, W: []uint8{0x2f}}, + {Addr: 0x44, R: []uint8{0x93, 0x28, 0xde, 0x3b, 0xa5, 0x20}}, + {Addr: 0x44, W: []uint8{0x2f}}, + {Addr: 0x44, R: []uint8{0x97, 0x12, 0x43, 0x25, 0x59, 0xdc}}, + {Addr: 0x44, W: []uint8{0x2f}}, + {Addr: 0x44, R: []uint8{0x99, 0x2, 0x6d, 0x1f, 0x1f, 0x1b}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x65, 0x48, 0x27, 0x51, 0x9e, 0xb4}}, + {Addr: 0x44, W: []uint8{0x39}}, + {Addr: 0x44, R: []uint8{0xb3, 0xb8, 0x3b, 0x21, 0xa1, 0x64}}, + {Addr: 0x44, W: []uint8{0x39}}, + {Addr: 0x44, R: []uint8{0xb9, 0xd0, 0xd7, 0x15, 0x79, 0xe8}}, + {Addr: 0x44, W: []uint8{0x39}}, + {Addr: 0x44, R: []uint8{0xbc, 0xb8, 0xa2, 0x14, 0x4b, 0xbb}}}, + "TestReset": { + {Addr: 0x44, W: []uint8{0x94}}}, + "TestSense": { + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xf4, 0xc8, 0x4c, 0x9b, 0x6f}}}, + "TestSenseContinuous": { + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xee, 0x50, 0x4c, 0xbf, 0x2d}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xda, 0x51, 0x4c, 0xeb, 0x97}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xdb, 0x60, 0x4c, 0xf6, 0x98}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xda, 0x51, 0x4d, 0x24, 0xa}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xd4, 0x4e, 0x4d, 0x3f, 0xa3}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xca, 0x12, 0x4d, 0x50, 0x36}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xba, 0xea, 0x4d, 0x71, 0x81}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xb6, 0x97, 0x4d, 0x99, 0xf9}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xb3, 0x62, 0x4d, 0xb0, 0xf7}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0xb3, 0x62, 0x4d, 0xc3, 0x5c}}, + {Addr: 0x44, W: []uint8{0xfd}}, + {Addr: 0x44, R: []uint8{0x66, 0x9e, 0xa8, 0x4d, 0xce, 0x10}}}, +}