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 }