You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/sht4x/sht4x.go

329 lines
8.6 KiB
Go

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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 τ₆₃% ≈2s
//
// SHT-45
//
// Typical accuracy: ±0.1°C
//
// Response time τ₆₃% ≈2s
//
// # Humidity Accuracy
//
// SHT-40 (Baseclass)
//
// Typical accuracy at 25°C:±1.8%RH
//
// Maximum accuracy (at 25°C): up to ±3.5%RH
//
// SHT-41 (Intermediateclass)
//
// Typical accuracy at 25°C:±1.8%RH
//
// Maximum accuracy (at 25°C): up to ±2.5%RH
//
// SHT-45 (Highaccuracyclass)
//
// 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 τ₆₃% ≈4s, and longterm 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{}