|
|
// 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{}
|