From f6271cf4a40238b58740bb7aea8b294645835077 Mon Sep 17 00:00:00 2001 From: Falk Scheerschmidt Date: Fri, 10 Dec 2021 11:39:23 +0100 Subject: [PATCH] add sgp30 air quality sensor --- sgp30/sgp30.go | 192 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 sgp30/sgp30.go diff --git a/sgp30/sgp30.go b/sgp30/sgp30.go new file mode 100644 index 0000000..e431685 --- /dev/null +++ b/sgp30/sgp30.go @@ -0,0 +1,192 @@ +// Copyright 2021 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. + +package sgp30 + +import ( + "context" + "encoding/binary" + "errors" + "log" + "strconv" + "sync" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/i2c" +) + +const ( + initAirQuality uint16 = 0x2003 + measureAirQuality uint16 = 0x2008 + getIAQBaseline uint16 = 0x2015 + setIAQBaseline uint16 = 0x201e + setHumidity uint16 = 0x2061 + measureTest uint16 = 0x2032 + getFeatureSetVersion uint16 = 0x202f + measureRawSignals uint16 = 0x2050 + getTVOCBaseline uint16 = 0x20b3 + setTVOCBaseline uint16 = 0x2077 + + i2CAddress uint16 = 0x58 +) + +// commandDuration maps the defined maximum measurement duration from the sensor +var commandDuration = map[uint16]time.Duration{ + initAirQuality: time.Millisecond * 10, + measureAirQuality: time.Millisecond * 12, + getIAQBaseline: time.Millisecond * 10, + setIAQBaseline: time.Millisecond * 10, + setHumidity: time.Millisecond * 10, + measureTest: time.Millisecond * 220, + getFeatureSetVersion: time.Millisecond * 10, + measureRawSignals: time.Millisecond * 25, + getTVOCBaseline: time.Millisecond * 10, + setTVOCBaseline: time.Millisecond * 10, +} + +// commandResponseLength maps the defined response length including the CRC +var commandResponseLength = map[uint16]int{ + measureAirQuality: 6, + getIAQBaseline: 6, + measureTest: 3, + getFeatureSetVersion: 3, + measureRawSignals: 6, + getTVOCBaseline: 3, +} + +// CO2 represents the current carbon dioxide value in ppm +type CO2 uint16 + +func (c CO2) String() string { + return strconv.Itoa(int(c)) + "ppm" +} + +func (c *CO2) set(b []byte) { + *c = (CO2)(binary.BigEndian.Uint16(b)) +} + +// TVOC represents the current total volatile organic compounds value in ppb +type TVOC uint16 + +func (t TVOC) String() string { + return strconv.Itoa(int(t)) + "ppb" +} + +func (t *TVOC) set(b []byte) { + *t = (TVOC)(binary.BigEndian.Uint16(b)) +} + +// Env represents measurements from an environmental sensor. +type Env struct { + CO2 CO2 + TVOC TVOC +} + +// NewI2C returns an object that communicates over I2C to SGP30 environmental sensor. +// +// The address must be 0x58. +func NewI2C(b i2c.Bus, ctx context.Context) (*Dev, error) { + d := &Dev{ + d: &i2c.Dev{Bus: b, Addr: i2CAddress}, + env: Env{ + CO2: 400, + TVOC: 0, + }, + } + if err := d.makeDev(ctx); err != nil { + return nil, err + } + return d, nil +} + +// Dev is a handle to an initialized SGP30 device. +type Dev struct { + d conn.Conn + mu sync.Mutex + env Env +} + +// AirQuality return the value struct for the sensor +func (d *Dev) AirQuality() Env { + d.mu.Lock() + defer d.mu.Unlock() + + return d.env +} + +func (d *Dev) makeDev(ctx context.Context) error { + // Sending a "sgp30_iaq_init" command starts the air quality measurement + if err := d.initAirQuality(); err != nil { + return err + } + + // After the "sgp30_iaq_init" command, a "sgp30_measure_iaq" command has to be sent in regular + // intervals of 1s to ensure proper operation of the dynamic baseline compensation algorithm. + if err := d.measure(); err != nil { + log.Print(err) + } + + ticker := time.NewTicker(1 * time.Second) + go func() { + for { + select { + case <-ticker.C: + if err := d.measure(); err != nil { + log.Print(err) + } + case <-ctx.Done(): + return + } + } + }() + + return nil +} + +func (d *Dev) initAirQuality() error { + err := d.writeCommand(initAirQuality) + if err == nil { + time.Sleep(time.Second * 20) + } + return err +} + +func (d *Dev) measure() error { + buf := make([]byte, commandResponseLength[measureAirQuality]) + if err := d.readCommand(measureAirQuality, buf); err != nil { + return err + } + + d.mu.Lock() + defer d.mu.Unlock() + + d.env.CO2.set(buf[0:2]) + d.env.TVOC.set(buf[3:5]) + + return nil +} + +func (d *Dev) readCommand(cmd uint16, b []byte) error { + if len(b) != commandResponseLength[cmd] { + return errors.New("response length mismatch") + } + + regAddr := []byte{byte(cmd >> 8), byte(cmd & 0xFF)} + if err := d.d.Tx(regAddr, nil); err != nil { + return err + } + time.Sleep(commandDuration[cmd]) + + return d.d.Tx(nil, b) +} + +func (d *Dev) writeCommand(cmd uint16) error { + regAddr := []byte{byte(cmd >> 8), byte(cmd & 0xFF)} + if err := d.d.Tx(regAddr, nil); err != nil { + return err + } + time.Sleep(commandDuration[cmd]) + return nil +}