Files
drivers/ndir/ndir.go
Patricio Whittingslow a45227590e add Sandbox Electronics NDIR CO2 sensor driver (2) (#580)
add sandbox electronics NDIR CO2 sensor
2023-08-11 11:58:28 +02:00

289 lines
6.9 KiB
Go

package ndir
import (
"errors"
"fmt"
"runtime"
"time"
"tinygo.org/x/drivers"
)
// Addr returns the I2C address given the solder pad configuration on the Sandbox Electronics i2c/uart converter.
// When the resistor is connected between the left and middle pads the bit is said to be set
// and a0 or a1 should be passed in as true.
func Addr(a0, a1 bool) uint8 {
return 0b1001000 | b2u8(a0) | b2u8(a1)<<2
}
func b2u8(b bool) uint8 {
if b {
return 1
}
return 0
}
// See https://github.com/SandboxElectronics/NDIR/blob/master/NDIR_I2C/NDIR_I2C.cpp
// General Registers
const (
addrRHR = 0x00
addrTHR = 0x00
addrIER = 0x01
addrFCR = 0x02
addrIIR = 0x02
addrLCR = 0x03
addrMCR = 0x04
addrLSR = 0x05
addrMSR = 0x06
addrSPR = 0x07
addrTCR = 0x06
addrTLR = 0x07
addrTXLVL = 0x08
addrRXLVL = 0x09
addrIODIR = 0x0A
addrIOSTATE = 0x0B
addrIOINTENA = 0x0C
addrIOCONTROL = 0x0E // This addr fails on write of 0x08?
addrEFCR = 0x0F
)
// Special registers
const (
addrDLL = 0x00
addrDLH = 1
)
const (
shortTxCooldown = time.Millisecond
longTxCooldown = 10 * time.Millisecond
rxTimeout = 100 * time.Millisecond
)
var (
cmd_readCO2 = [...]byte{0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}
cmd_measure = [...]byte{0xFF, 0x01, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63}
cmd_calibrateZero = [...]byte{0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78}
cmd_enableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}
cmd_disableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}
)
// DevI2C is a handle to a MH-Z16 NDIR CO2 Sensor using the I2C interface.
type DevI2C struct {
bus drivers.I2C
addr uint8
nextAvail time.Time
initTime time.Time
lastMeasurement int32
}
// NewDevI2C returns a new NDIR device ready for use. It performs no I/O.
func NewDevI2C(bus drivers.I2C, addr uint8) *DevI2C {
return &DevI2C{
bus: bus,
addr: addr,
lastMeasurement: -1,
}
}
// PPM returns the CO2 parts per million read in the last Update call.
func (d *DevI2C) PPMCO2() int32 {
return d.lastMeasurement
}
var errInitWait = errors.New("ndir: must wait 12 seconds after init before reading concentration")
// Update reads the CO2 concentration from the NDIR and stores it ready for the
// PPM() method.
func (d *DevI2C) Update(which drivers.Measurement) (err error) {
if which&drivers.Concentration == 0 {
return nil // NDIR only measures concentration, so nothing to do here.
}
if time.Since(d.initTime) < 12*time.Second {
// Wait 12 seconds before performing first read.
return nil
}
err = d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
err = d.send(cmd_measure[:])
if err != nil {
return fmt.Errorf("sending cmd_measure: %w", err)
}
time.Sleep(11 * time.Millisecond)
var buf [9]byte
buf, err = d.receive()
if err != nil {
return fmt.Errorf("receiving during measure: %w", err)
}
if buf[0] != 0xff && buf[1] != 0x9c {
return fmt.Errorf("buffer rx bad values: %q", string(buf[:]))
}
var sum uint16
for i := 0; i < len(buf); i++ {
sum += uint16(buf[i])
}
mod := sum % 256
if mod != 0xff {
return fmt.Errorf("ndir checksum modulus got %#x, expected 0xff", mod)
}
ppm := uint32(buf[2])<<24 | uint32(buf[3])<<16 | uint32(buf[4])<<8 | uint32(buf[5])
d.lastMeasurement = int32(ppm)
return nil
}
func (d *DevI2C) Init() (err error) {
// AddrIOCONTROL write is always NACKed so ignore
// error here.
d.writeRegister(addrIOCONTROL, 0x08)
err = d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
err = d.writeRegister(addrLCR, 0x83)
if err != nil {
return err
}
err = d.writeRegister(addrDLL, 0x60)
if err != nil {
return err
}
err = d.writeRegister(addrDLH, 0x00)
if err != nil {
return err
}
err = d.writeRegister(addrLCR, 0x03)
if err != nil {
return err
}
d.initTime = time.Now()
return nil
}
// CalibrateZero calibrates the NDIR to around 412ppm.
func (d *DevI2C) CalibrateZero() error {
return d.enactCommand(cmd_calibrateZero[:])
}
// SetAutoCalibration can enable or disable the NDIR's auto calibration mode.
func (d *DevI2C) SetAutoCalibration(enable bool) (err error) {
if enable {
err = d.enactCommand(cmd_enableAutoCalibration[:])
} else {
err = d.enactCommand(cmd_disableAutoCalibration[:])
}
return err
}
func (d *DevI2C) send(cmd []byte) error {
txlvl, err := d.ReadRegister(addrTXLVL)
if err != nil {
return err
}
if int(txlvl) < len(cmd) {
return fmt.Errorf("txlvl=%d less than length of command %d", txlvl, len(cmd))
}
return d.tx(append([]byte{addrTHR}, cmd...), nil)
}
func (d *DevI2C) receive() (cmd [9]byte, err error) {
start := time.Now()
n := uint8(9)
for n > 0 {
if time.Since(start) > rxTimeout {
return [9]byte{}, errors.New("NDIR rx timeout")
}
rxlvl, err := d.ReadRegister(addrRXLVL)
if err != nil {
return [9]byte{}, err
}
if rxlvl > n {
rxlvl = n
}
ptr := 9 - n
err = d.tx([]byte{addrRHR << 3}, cmd[ptr:ptr+rxlvl])
n -= rxlvl
if err != nil {
return [9]byte{}, err
}
}
return cmd, nil
}
func (d *DevI2C) enactCommand(cmd []byte) error {
if len(cmd) > 31 {
return errors.New("ndir: command too long")
}
// Most commands always start with the same FCR write here.
err := d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
time.Sleep(longTxCooldown)
// C++ send method begins here.
got, err := d.ReadRegister(addrTXLVL)
if err != nil {
return err
}
if got < uint8(len(cmd)) {
return fmt.Errorf("ndir: txlevel=%d too low for command of length %d", got, len(cmd))
}
var buf [32]byte
buf[0] = addrTHR
n := 1 + copy(buf[1:], cmd)
err = d.tx(buf[:n], nil)
if err != nil {
return err
}
d.nextAvail.Add(longTxCooldown) // add some extra time.
return nil
}
func (d *DevI2C) writeRegister(addr, val uint8) (err error) {
return d.WriteRegisters(addr, []byte{val})
}
func (d *DevI2C) WriteRegisters(addr uint8, vals []byte) (err error) {
var buf [32]byte
if len(vals) > 31 {
return errors.New("can only write up to 31 bytes")
}
buf[0] = addr << 3
n := copy(buf[1:], vals)
err = d.tx(buf[:n+1], nil)
if err != nil {
err = fmt.Errorf("NDIR write %#x (%d) to %#x: %w", buf[1], len(vals), buf[0], err)
}
return err
}
func (d *DevI2C) ReadRegister(addr uint8) (uint8, error) {
var buf [2]byte
buf[0] = addr << 3
err := d.tx(buf[:1], buf[1:2])
if err != nil {
err = fmt.Errorf("NDIR read from %#x: %w", buf[0], err)
}
return buf[1], err
}
func (d *DevI2C) tx(w, r []byte) error {
wait := time.Until(d.nextAvail)
if wait > 0 {
// Try yielding process first, maybe there's a short time to wait and a schedule call is enough delay.
runtime.Gosched()
wait = time.Until(d.nextAvail)
if wait > 0 {
// If yielding did not work then perform sleep
time.Sleep(wait)
}
}
err := d.bus.Tx(uint16(d.addr), w, r)
d.nextAvail = time.Now().Add(shortTxCooldown)
return err
}