air-sensors

Air Quality Sensor library
git clone https://www.brianlane.com/git/air-sensors
Log | Files | Refs | README | LICENSE

sgp30.go (7006B)


      1 // Copyright 2020 by Brian C. Lane <bcl@brianlane.com>. All rights reserved.
      2 // Use of this source code is governed under the Apache License, Version 2.0
      3 // that can be found in the LICENSE file.
      4 
      5 package sgp30
      6 
      7 import (
      8 	"fmt"
      9 	"io/ioutil"
     10 	"time"
     11 
     12 	"github.com/sigurn/crc8"
     13 	"periph.io/x/periph/conn"
     14 	"periph.io/x/periph/conn/i2c"
     15 )
     16 
     17 var (
     18 	crc8sgp30 = crc8.MakeTable(crc8.Params{
     19 		Poly:   0x31,
     20 		Init:   0xFF,
     21 		RefIn:  false,
     22 		RefOut: false,
     23 		XorOut: 0x00,
     24 		Check:  0xA1,
     25 		Name:   "CRC-8/SGP30",
     26 	})
     27 )
     28 
     29 func checkCRC8(data []byte) bool {
     30 	return crc8.Checksum(data[:], crc8sgp30) == 0x00
     31 }
     32 
     33 // New returns a SGP30 device struct for communicating with the device
     34 //
     35 // If baselineFile is passed the baseline calibration data will be read from the file
     36 // at startup, and new data will be saved at baselineInterval interval when ReadAirQuality
     37 // is called.
     38 //
     39 // eg. pass 30 * time.Second to save the baseline data every 30 seconds
     40 func New(i i2c.Bus, baselineFile string, baselineInterval time.Duration) (*Dev, error) {
     41 	d := &Dev{i2c: &i2c.Dev{Bus: i, Addr: 0x58}}
     42 	if _, err := d.GetSerialNumber(); err != nil {
     43 		return nil, err
     44 	}
     45 
     46 	// Restore the baseline from the saved data if it exists
     47 	if len(baselineFile) > 0 {
     48 		d.baselineFile = baselineFile
     49 		d.baselineInterval = baselineInterval
     50 		d.lastSave = time.Now()
     51 		// Restore the baseline data if it exists, ignore missing file
     52 		if baseline, err := ioutil.ReadFile(baselineFile); err == nil {
     53 			err = d.SetBaseline(baseline)
     54 			if err != nil {
     55 				return nil, err
     56 			}
     57 		}
     58 	}
     59 	return d, nil
     60 }
     61 
     62 // Dev holds the connection and error details for the device
     63 // as well as the path to the baseline file and how often to save it.
     64 type Dev struct {
     65 	i2c              conn.Conn     // i2c device handle for the sgp30
     66 	baselineFile     string        // Path and filename for storing baseline values
     67 	baselineInterval time.Duration // How often to save the baseline data
     68 	lastSave         time.Time     // Last time baseline was saved
     69 	err              error         //nolint
     70 }
     71 
     72 // Halt implements conn.Resource.
     73 func (d *Dev) Halt() error {
     74 	return nil
     75 }
     76 
     77 // GetSerialNumber returns the 48 bit serial number of the device
     78 func (d *Dev) GetSerialNumber() (uint64, error) {
     79 	// Send a 0x3682
     80 	// Receive 3 words + 8 bit CRC on each
     81 	var data [9]byte
     82 	if err := d.i2c.Tx([]byte{0x36, 0x82}, data[:]); err != nil {
     83 		return 0, fmt.Errorf("sgp30: Error while reading serial number: %w", err)
     84 	}
     85 
     86 	if !checkCRC8(data[0:3]) {
     87 		return 0, fmt.Errorf("sgp30: serial number word 1 CRC8 failed on: %v", data[0:3])
     88 	}
     89 	if !checkCRC8(data[3:6]) {
     90 		return 0, fmt.Errorf("sgp30: serial number word 2 CRC8 failed on: %v", data[3:6])
     91 	}
     92 	if !checkCRC8(data[6:9]) {
     93 		return 0, fmt.Errorf("sgp30: serial number word 3 CRC8 failed on: %v", data[6:9])
     94 	}
     95 
     96 	return uint64(word(data[:], 0))<<24 + uint64(word(data[:], 3))<<16 + uint64(word(data[:], 6)), nil
     97 }
     98 
     99 // GetFeatures returns the 8 bit product type, and 8 bit product version
    100 func (d *Dev) GetFeatures() (uint8, uint8, error) {
    101 	// Send a 0x202f
    102 	// Receive 1 word + 8 bit CRC
    103 	var data [3]byte
    104 	if err := d.i2c.Tx([]byte{0x20, 0x2f}, data[:]); err != nil {
    105 		return 0, 0, fmt.Errorf("sgp30: Error while reading features: %w", err)
    106 	}
    107 
    108 	if !checkCRC8(data[0:3]) {
    109 		return 0, 0, fmt.Errorf("sgp30: features CRC8 failed on: %v", data[0:3])
    110 	}
    111 
    112 	return data[0], data[1], nil
    113 }
    114 
    115 // StartMeasurements sends the Inlet Air Quality command to start measuring
    116 // ReadAirQuality needs to be called every second after this has been sent
    117 //
    118 // Note that for 15s after the measurements have started the readings will return
    119 // 400ppm CO2 and 0ppb TVOC
    120 func (d *Dev) StartMeasurements() error {
    121 	// Send a 0x2003
    122 	if err := d.i2c.Tx([]byte{0x20, 0x03}, nil); err != nil {
    123 		return fmt.Errorf("sgp30: Error starting air quality measurements: %w", err)
    124 	}
    125 
    126 	return nil
    127 }
    128 
    129 // ReadAirQuality returns the CO2 and TVOC readings as 16 bit values
    130 // CO2 is in ppm and TVOC is in ppb
    131 //
    132 // If a baselineFile was passed to New the baseline data will be saved to disk every
    133 // baselineInterval
    134 func (d *Dev) ReadAirQuality() (uint16, uint16, error) {
    135 	// Send a 0x2008
    136 	// Receive 2 words with + 8 bit CRC on each
    137 	if err := d.i2c.Tx([]byte{0x20, 0x08}, nil); err != nil {
    138 		return 0, 0, fmt.Errorf("sgp30: Error while requesting air quality: %w", err)
    139 	}
    140 
    141 	// Requires a short delay before reading results
    142 	time.Sleep(10 * time.Millisecond)
    143 	var data [6]byte
    144 	if err := d.i2c.Tx(nil, data[:]); err != nil {
    145 		return 0, 0, fmt.Errorf("sgp30: Error while reading air quality: %w", err)
    146 	}
    147 
    148 	if !checkCRC8(data[0:3]) {
    149 		return 0, 0, fmt.Errorf("sgp30: read air quality word 1 CRC8 failed on: %v", data[0:3])
    150 	}
    151 	if !checkCRC8(data[3:6]) {
    152 		return 0, 0, fmt.Errorf("sgp30: read air quality word 2 CRC8 failed on: %v", data[3:6])
    153 	}
    154 
    155 	if len(d.baselineFile) > 0 && time.Since(d.lastSave) >= d.baselineInterval {
    156 		d.lastSave = time.Now()
    157 		baseline, err := d.ReadBaseline()
    158 		if err != nil {
    159 			return 0, 0, fmt.Errorf("sgp30: Error while reading baseline: %w", err)
    160 		}
    161 		if err = ioutil.WriteFile(d.baselineFile, baseline[:], 0644); err != nil {
    162 			return 0, 0, err
    163 		}
    164 	}
    165 
    166 	return word(data[:], 0), word(data[:], 3), nil
    167 }
    168 
    169 // ReadBaseline returns the 6 data bytes for the measurement baseline
    170 // These values should be saved to disk and restore using SetBaseline when the program
    171 // restarts.
    172 func (d *Dev) ReadBaseline() ([6]byte, error) {
    173 	// Send a 0x2015
    174 	// Receive 2 words + 8 bit CRC on each
    175 	var data [6]byte
    176 	if err := d.i2c.Tx([]byte{0x20, 0x15}, data[:]); err != nil {
    177 		return [6]byte{}, fmt.Errorf("sgp30: Error while reading baseline: %w", err)
    178 	}
    179 
    180 	if !checkCRC8(data[0:3]) {
    181 		return [6]byte{}, fmt.Errorf("sgp30: baseline word 1 CRC8 failed on: %v", data[0:3])
    182 	}
    183 	if !checkCRC8(data[3:6]) {
    184 		return [6]byte{}, fmt.Errorf("sgp30: baseline word 2 CRC8 failed on: %v", data[3:6])
    185 	}
    186 
    187 	return data, nil
    188 }
    189 
    190 // SetBaseline sets the measurement baseline data bytes
    191 // The values should have been previously read from the device using ReadBaseline
    192 //
    193 // NOTE: The data order for setting it is TVOC, CO2 even though the order when
    194 // reading is CO2, TVOC. This assumes that the baseline data passed in is CO2, TVOC
    195 func (d *Dev) SetBaseline(baseline []byte) error {
    196 	if !checkCRC8(baseline[0:3]) {
    197 		return fmt.Errorf("sgp30: set baseline word 1 CRC8 failed on: %v", baseline[0:3])
    198 	}
    199 	if !checkCRC8(baseline[3:6]) {
    200 		return fmt.Errorf("sgp30: set baseline word 2 CRC8 failed on: %v", baseline[3:6])
    201 	}
    202 
    203 	// Send InitAirQuality
    204 	if err := d.StartMeasurements(); err != nil {
    205 		return err
    206 	}
    207 
    208 	// Send a 0x201e + TVOC, CO2 baseline data (2 words + CRCs)
    209 	data := append(append([]byte{0x20, 0x1e}, baseline[3:6]...), baseline[0:3]...)
    210 	if err := d.i2c.Tx(data, nil); err != nil {
    211 		return fmt.Errorf("sgp30: Error while setting baseline: %w", err)
    212 	}
    213 	return nil
    214 }
    215 
    216 // word returns 16 bits from the byte stream, starting at index i
    217 func word(data []byte, i int) uint16 {
    218 	return uint16(data[i])<<8 + uint16(data[i+1])
    219 }