tinygo.org/x/drivers@v0.27.1-0.20240509133757-7dbca2a54349/ndir/ndir.go (about)

     1  package ndir
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"runtime"
     7  	"time"
     8  
     9  	"tinygo.org/x/drivers"
    10  )
    11  
    12  // Addr returns the I2C address given the solder pad configuration on the Sandbox Electronics i2c/uart converter.
    13  // When the resistor is connected between the left and middle pads the bit is said to be set
    14  // and a0 or a1 should be passed in as true.
    15  func Addr(a0, a1 bool) uint8 {
    16  	return 0b1001000 | b2u8(a0) | b2u8(a1)<<2
    17  }
    18  
    19  func b2u8(b bool) uint8 {
    20  	if b {
    21  		return 1
    22  	}
    23  	return 0
    24  }
    25  
    26  // See https://github.com/SandboxElectronics/NDIR/blob/master/NDIR_I2C/NDIR_I2C.cpp
    27  
    28  // General Registers
    29  const (
    30  	addrRHR       = 0x00
    31  	addrTHR       = 0x00
    32  	addrIER       = 0x01
    33  	addrFCR       = 0x02
    34  	addrIIR       = 0x02
    35  	addrLCR       = 0x03
    36  	addrMCR       = 0x04
    37  	addrLSR       = 0x05
    38  	addrMSR       = 0x06
    39  	addrSPR       = 0x07
    40  	addrTCR       = 0x06
    41  	addrTLR       = 0x07
    42  	addrTXLVL     = 0x08
    43  	addrRXLVL     = 0x09
    44  	addrIODIR     = 0x0A
    45  	addrIOSTATE   = 0x0B
    46  	addrIOINTENA  = 0x0C
    47  	addrIOCONTROL = 0x0E // This addr fails on write of 0x08?
    48  	addrEFCR      = 0x0F
    49  )
    50  
    51  // Special registers
    52  const (
    53  	addrDLL = 0x00
    54  	addrDLH = 1
    55  )
    56  
    57  const (
    58  	shortTxCooldown = time.Millisecond
    59  	longTxCooldown  = 10 * time.Millisecond
    60  	rxTimeout       = 100 * time.Millisecond
    61  )
    62  
    63  var (
    64  	cmd_readCO2                = [...]byte{0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}
    65  	cmd_measure                = [...]byte{0xFF, 0x01, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63}
    66  	cmd_calibrateZero          = [...]byte{0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78}
    67  	cmd_enableAutoCalibration  = [...]byte{0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}
    68  	cmd_disableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}
    69  )
    70  
    71  // DevI2C is a handle to a MH-Z16 NDIR CO2 Sensor using the I2C interface.
    72  type DevI2C struct {
    73  	bus             drivers.I2C
    74  	addr            uint8
    75  	nextAvail       time.Time
    76  	initTime        time.Time
    77  	lastMeasurement int32
    78  }
    79  
    80  // NewDevI2C returns a new NDIR device ready for use. It performs no I/O.
    81  func NewDevI2C(bus drivers.I2C, addr uint8) *DevI2C {
    82  	return &DevI2C{
    83  		bus:             bus,
    84  		addr:            addr,
    85  		lastMeasurement: -1,
    86  	}
    87  }
    88  
    89  // PPM returns the CO2 parts per million read in the last Update call.
    90  func (d *DevI2C) PPMCO2() int32 {
    91  	return d.lastMeasurement
    92  }
    93  
    94  var errInitWait = errors.New("ndir: must wait 12 seconds after init before reading concentration")
    95  
    96  // Update reads the CO2 concentration from the NDIR and stores it ready for the
    97  // PPM() method.
    98  func (d *DevI2C) Update(which drivers.Measurement) (err error) {
    99  	if which&drivers.Concentration == 0 {
   100  		return nil // NDIR only measures concentration, so nothing to do here.
   101  	}
   102  	if time.Since(d.initTime) < 12*time.Second {
   103  		// Wait 12 seconds before performing first read.
   104  		return nil
   105  	}
   106  	err = d.writeRegister(addrFCR, 0x07)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	err = d.send(cmd_measure[:])
   111  	if err != nil {
   112  		return fmt.Errorf("sending cmd_measure: %w", err)
   113  	}
   114  	time.Sleep(11 * time.Millisecond)
   115  	var buf [9]byte
   116  	buf, err = d.receive()
   117  
   118  	if err != nil {
   119  		return fmt.Errorf("receiving during measure: %w", err)
   120  	}
   121  	if buf[0] != 0xff && buf[1] != 0x9c {
   122  		return fmt.Errorf("buffer rx bad values: %q", string(buf[:]))
   123  	}
   124  	var sum uint16
   125  	for i := 0; i < len(buf); i++ {
   126  		sum += uint16(buf[i])
   127  	}
   128  	mod := sum % 256
   129  	if mod != 0xff {
   130  		return fmt.Errorf("ndir checksum modulus got %#x, expected 0xff", mod)
   131  	}
   132  	ppm := uint32(buf[2])<<24 | uint32(buf[3])<<16 | uint32(buf[4])<<8 | uint32(buf[5])
   133  	d.lastMeasurement = int32(ppm)
   134  	return nil
   135  }
   136  
   137  func (d *DevI2C) Init() (err error) {
   138  	// AddrIOCONTROL write is always NACKed so ignore
   139  	// error here.
   140  	d.writeRegister(addrIOCONTROL, 0x08)
   141  
   142  	err = d.writeRegister(addrFCR, 0x07)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	err = d.writeRegister(addrLCR, 0x83)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	err = d.writeRegister(addrDLL, 0x60)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	err = d.writeRegister(addrDLH, 0x00)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	err = d.writeRegister(addrLCR, 0x03)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	d.initTime = time.Now()
   163  	return nil
   164  }
   165  
   166  // CalibrateZero calibrates the NDIR to around 412ppm.
   167  func (d *DevI2C) CalibrateZero() error {
   168  	return d.enactCommand(cmd_calibrateZero[:])
   169  }
   170  
   171  // SetAutoCalibration can enable or disable the NDIR's auto calibration mode.
   172  func (d *DevI2C) SetAutoCalibration(enable bool) (err error) {
   173  	if enable {
   174  		err = d.enactCommand(cmd_enableAutoCalibration[:])
   175  	} else {
   176  		err = d.enactCommand(cmd_disableAutoCalibration[:])
   177  	}
   178  	return err
   179  }
   180  
   181  func (d *DevI2C) send(cmd []byte) error {
   182  	txlvl, err := d.ReadRegister(addrTXLVL)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	if int(txlvl) < len(cmd) {
   187  		return fmt.Errorf("txlvl=%d less than length of command %d", txlvl, len(cmd))
   188  	}
   189  	return d.tx(append([]byte{addrTHR}, cmd...), nil)
   190  }
   191  
   192  func (d *DevI2C) receive() (cmd [9]byte, err error) {
   193  	start := time.Now()
   194  	n := uint8(9)
   195  	for n > 0 {
   196  		if time.Since(start) > rxTimeout {
   197  			return [9]byte{}, errors.New("NDIR rx timeout")
   198  		}
   199  		rxlvl, err := d.ReadRegister(addrRXLVL)
   200  		if err != nil {
   201  			return [9]byte{}, err
   202  		}
   203  		if rxlvl > n {
   204  			rxlvl = n
   205  		}
   206  		ptr := 9 - n
   207  		err = d.tx([]byte{addrRHR << 3}, cmd[ptr:ptr+rxlvl])
   208  		n -= rxlvl
   209  		if err != nil {
   210  			return [9]byte{}, err
   211  		}
   212  	}
   213  	return cmd, nil
   214  }
   215  
   216  func (d *DevI2C) enactCommand(cmd []byte) error {
   217  	if len(cmd) > 31 {
   218  		return errors.New("ndir: command too long")
   219  	}
   220  	// Most commands always start with the same FCR write here.
   221  	err := d.writeRegister(addrFCR, 0x07)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	time.Sleep(longTxCooldown)
   226  
   227  	// C++ send method begins here.
   228  	got, err := d.ReadRegister(addrTXLVL)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	if got < uint8(len(cmd)) {
   233  		return fmt.Errorf("ndir: txlevel=%d too low for command of length %d", got, len(cmd))
   234  	}
   235  	var buf [32]byte
   236  	buf[0] = addrTHR
   237  	n := 1 + copy(buf[1:], cmd)
   238  	err = d.tx(buf[:n], nil)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	d.nextAvail.Add(longTxCooldown) // add some extra time.
   243  	return nil
   244  }
   245  
   246  func (d *DevI2C) writeRegister(addr, val uint8) (err error) {
   247  	return d.WriteRegisters(addr, []byte{val})
   248  }
   249  
   250  func (d *DevI2C) WriteRegisters(addr uint8, vals []byte) (err error) {
   251  	var buf [32]byte
   252  	if len(vals) > 31 {
   253  		return errors.New("can only write up to 31 bytes")
   254  	}
   255  	buf[0] = addr << 3
   256  	n := copy(buf[1:], vals)
   257  	err = d.tx(buf[:n+1], nil)
   258  	if err != nil {
   259  		err = fmt.Errorf("NDIR write %#x (%d) to %#x: %w", buf[1], len(vals), buf[0], err)
   260  	}
   261  	return err
   262  }
   263  
   264  func (d *DevI2C) ReadRegister(addr uint8) (uint8, error) {
   265  	var buf [2]byte
   266  	buf[0] = addr << 3
   267  	err := d.tx(buf[:1], buf[1:2])
   268  	if err != nil {
   269  		err = fmt.Errorf("NDIR read from %#x: %w", buf[0], err)
   270  	}
   271  	return buf[1], err
   272  }
   273  
   274  func (d *DevI2C) tx(w, r []byte) error {
   275  	wait := time.Until(d.nextAvail)
   276  	if wait > 0 {
   277  		// Try yielding process first, maybe there's a short time to wait and a schedule call is enough delay.
   278  		runtime.Gosched()
   279  		wait = time.Until(d.nextAvail)
   280  		if wait > 0 {
   281  			// If yielding did not work then perform sleep
   282  			time.Sleep(wait)
   283  		}
   284  	}
   285  	err := d.bus.Tx(uint16(d.addr), w, r)
   286  	d.nextAvail = time.Now().Add(shortTxCooldown)
   287  	return err
   288  }