-
Notifications
You must be signed in to change notification settings - Fork 42
/
sgp30.go
192 lines (161 loc) · 4.31 KB
/
sgp30.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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
}