mirror of https://github.com/periph/devices
devices: Add support for AM2320 Temperature/Humidity Sensor (#82)
* AM2320 Temp/Humidity Sensor - Initial Addpull/86/head
parent
9a938c42a7
commit
7fd42d5ebf
@ -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…
Reference in New Issue