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