devices: Add support for AM2320 Temperature/Humidity Sensor (#82)

* AM2320 Temp/Humidity Sensor - Initial Add
pull/86/head
gsexton 1 year ago committed by GitHub
parent 9a938c42a7
commit 7fd42d5ebf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,181 @@
// 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{}

@ -0,0 +1,190 @@
// 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 am2320
import (
"fmt"
"os"
"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: SensorAddress, W: []uint8{0x0}},
{Addr: SensorAddress, W: []uint8{0x3, 0x0, 0x4}, R: []uint8{0x3, 0x4, 0x1, 0x5c, 0x0, 0xef, 0x71, 0x8a}}}
func init() {
var err error
liveDevice = os.Getenv("AM2320") != ""
// 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, SensorAddress)
if err != nil {
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 TestBasic(t *testing.T) {
dev := Dev{}
env := &physic.Env{}
dev.Precision(env)
if env.Pressure != 0 {
t.Error("this device doesn't measure pressure")
}
if 10*env.Temperature != physic.Celsius {
t.Error("incorrect temperature precision value")
}
if env.Humidity != physic.MilliRH {
t.Error("incorrect humidity precision")
}
s := dev.String()
if len(s) == 0 {
t.Error("invalid value for String()")
}
// Check the CRC Calculation algorithm using the data supplied by the vendor.
crcTest := []byte{0x03, 0x04, 0x01, 0xf4, 0x00, 0xfa, 0x31, 0xa5}
if !checkCRC(crcTest) {
t.Error("crc error")
}
// ensure a corruption is detected.
crcTest[0] = crcTest[0] ^ 0xff
if checkCRC(crcTest) {
t.Error("crc error")
}
}
func TestSense(t *testing.T) {
d, err := getDev(t, pbSense)
if err != nil {
t.Fatalf("failed to initialize am2320: %v", err)
}
defer shutdown(t)
// Read temperature and humidity from the sensor
e := physic.Env{}
if err := d.Sense(&e); err != nil {
t.Fatal(err)
}
t.Logf("%8s %9s", e.Temperature, e.Humidity)
if !liveDevice {
// The playback temp is 23.9C Ensure that's what we got.
expected := physic.ZeroCelsius + 23_900*physic.MilliKelvin
if e.Temperature != expected {
t.Errorf("incorrect temperature value read. Expected: %s (%d) Found: %s (%d)",
e.Temperature.String(),
e.Temperature,
expected.String(),
expected)
}
// 34.8% expected.
expectedRH := 34*physic.PercentRH + 8*physic.MilliRH
if e.Humidity != expectedRH {
t.Errorf("incorrect humidity value read. Expected: %s (%d) Found: %s (%d)",
e.Humidity.String(),
e.Humidity,
expectedRH.String(),
expectedRH)
}
}
}
func TestSenseContinuous(t *testing.T) {
readCount := 10
// make 10 copies of the single reading playback data.
pb := make([]i2ctest.IO, 0, len(pbSense)*10)
for range readCount {
pb = append(pb, pbSense...)
}
d, err := getDev(t, pb)
if err != nil {
t.Fatalf("failed to initialize am2320: %v", err)
}
defer shutdown(t)
_, err = d.SenseContinuous(time.Second)
if err == nil {
t.Error("SenseContinuous() accepted invalid reading interval")
}
ch, err := d.SenseContinuous(3 * time.Second)
if err != nil {
t.Fatal(err)
}
go func() {
time.Sleep(3 * time.Duration(readCount) * time.Second)
err := d.Halt()
if err != nil {
t.Error(err)
}
}()
count := 0
for e := range ch {
count += 1
t.Log(time.Now(), e)
}
if count < (readCount-1) || count > (readCount+1) {
t.Errorf("expected %d readings. received %d", readCount, count)
}
}

@ -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 am2320
import (
"fmt"
"log"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/physic"
"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 := NewI2C(bus, SensorAddress)
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)
}
Loading…
Cancel
Save