mirror of https://github.com/periph/devices
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.
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
// 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 AOSONG AM2320 Temperature/Humidity
|
|
// Sensor. This sensor is a basic, inexpensive i2c sensor with reasonably good
|
|
// accuracy for both temperature and humidity.
|
|
//
|
|
// # Datasheet
|
|
//
|
|
// https://cdn-shop.adafruit.com/product-files/3721/AM2320.pdf
|
|
package am2320
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"periph.io/x/conn/v3"
|
|
"periph.io/x/conn/v3/i2c"
|
|
"periph.io/x/conn/v3/physic"
|
|
)
|
|
|
|
// Dev represents an am2320 temperature/humidity sensor.
|
|
type Dev struct {
|
|
d *i2c.Dev
|
|
mu sync.Mutex
|
|
shutdown chan struct{}
|
|
}
|
|
|
|
const (
|
|
// The address of this device is fixed. Note that the datasheet states
|
|
// the value is 0xb8, which is incorrect.
|
|
SensorAddress uint16 = 0x5c
|
|
|
|
humidityRegisters byte = 0x00
|
|
)
|
|
|
|
// Create a new am2320 device and return it.
|
|
func NewI2C(b i2c.Bus, addr uint16) (*Dev, error) {
|
|
d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}}
|
|
return d, nil
|
|
}
|
|
|
|
// Halt interrupts a running SenseContinuous() operation.
|
|
func (dev *Dev) Halt() error {
|
|
dev.mu.Lock()
|
|
defer dev.mu.Unlock()
|
|
if dev.shutdown != nil {
|
|
close(dev.shutdown)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Algorithm from the datasheet. Returns true if CRC matches check value.
|
|
func checkCRC(bytes []byte) bool {
|
|
crc := uint16(0xffff)
|
|
for ix := range len(bytes) - 2 {
|
|
b := uint16(bytes[ix])
|
|
crc ^= b
|
|
for range 8 {
|
|
if (crc & 0x01) == 0x01 {
|
|
crc = crc >> 1
|
|
crc ^= 0xa001
|
|
} else {
|
|
crc = crc >> 1
|
|
}
|
|
}
|
|
}
|
|
chk := uint16(bytes[len(bytes)-2]) | uint16(bytes[len(bytes)-1])<<8
|
|
return chk == crc
|
|
}
|
|
|
|
// readCommand provides the logic of communicating with the sensor. According
|
|
// to the datasheet, it tries to stay in low-power as much as possible to
|
|
// avoid self-heating the sensors. This makes it finicky to talk to. On success,
|
|
// returns a slice of registerCount bytes starting from registerAddress.
|
|
func (dev *Dev) readCommand(registerAddress, registerCount byte) ([]byte, error) {
|
|
// Send a wake-up call to the device.
|
|
var err error
|
|
for range 5 {
|
|
err = dev.d.Tx([]byte{0}, nil)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
w := []byte{0x3, registerAddress, registerCount}
|
|
// The read return format is:
|
|
//
|
|
// {operation,registerCount,requested registers...,crc low, crc high}
|
|
r := make([]byte, registerCount+4)
|
|
|
|
for range 10 {
|
|
err = dev.d.Tx(w, r)
|
|
if err == nil &&
|
|
w[0] == r[0] && w[2] == r[1] &&
|
|
checkCRC(r) {
|
|
|
|
return r[2 : 2+registerCount], nil
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
if err == nil {
|
|
err = errors.New("invalid return values or crc from sensor")
|
|
}
|
|
return nil, fmt.Errorf("am2320 error sending read command: %w", err)
|
|
}
|
|
|
|
// Sense queries the sensor for the current temperature and humidity. Note that
|
|
// the sensor reports a sample rate of 1/2 hz. It's recommended to not poll
|
|
// the sensor more frequently than once every 3 seconds.
|
|
func (dev *Dev) Sense(env *physic.Env) error {
|
|
env.Temperature = 0
|
|
env.Pressure = 0
|
|
env.Humidity = 0
|
|
|
|
dev.mu.Lock()
|
|
defer dev.mu.Unlock()
|
|
|
|
r, err := dev.readCommand(humidityRegisters, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h := int16(r[0])<<8 | int16(r[1])
|
|
env.Humidity = physic.RelativeHumidity(h) * physic.MilliRH
|
|
t := int16(r[2])<<8 | int16(r[3])
|
|
env.Temperature = physic.ZeroCelsius + (physic.Celsius/10)*physic.Temperature(t)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SenseContinuous returns a channel that can be read to return values from
|
|
// the sensor. The minimum value for interval is 3 seconds. To end the read,
|
|
// call Halt()
|
|
func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) {
|
|
if interval < (3 * time.Second) {
|
|
return nil, errors.New("am2320: invalid duration. minimum 3 seconds")
|
|
}
|
|
if dev.shutdown != nil {
|
|
return nil, errors.New("am2320: sense continuous already running")
|
|
}
|
|
|
|
dev.shutdown = make(chan struct{})
|
|
ch := make(chan physic.Env, 16)
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-dev.shutdown:
|
|
close(ch)
|
|
dev.shutdown = nil
|
|
return
|
|
case <-ticker.C:
|
|
e := physic.Env{}
|
|
err := dev.Sense(&e)
|
|
if err == nil {
|
|
ch <- e
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return ch, nil
|
|
}
|
|
|
|
func (dev *Dev) String() string {
|
|
return fmt.Sprintf("am2320: %s", dev.d)
|
|
}
|
|
|
|
// Precision returns the resolution of the device for it's measured parameters.
|
|
func (dev *Dev) Precision(env *physic.Env) {
|
|
env.Temperature = physic.Celsius / 10
|
|
env.Pressure = 0
|
|
env.Humidity = physic.MilliRH
|
|
}
|
|
|
|
var _ conn.Resource = &Dev{}
|
|
var _ physic.SenseEnv = &Dev{}
|