forked from tinygo-org/drivers
/
ndir.go
288 lines (261 loc) · 6.94 KB
/
ndir.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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
package ndir
import (
"errors"
"fmt"
"runtime"
"time"
"tinygo.org/x/drivers"
)
// Addr returns the I2C address given the solder pad configuration on the Sandbox Electronics i2c/uart converter.
// When the resistor is connected between the left and middle pads the bit is said to be set
// and a0 or a1 should be passed in as true.
func Addr(a0, a1 bool) uint8 {
return 0b1001000 | b2u8(a0) | b2u8(a1)<<2
}
func b2u8(b bool) uint8 {
if b {
return 1
}
return 0
}
// See https://github.com/SandboxElectronics/NDIR/blob/master/NDIR_I2C/NDIR_I2C.cpp
// General Registers
const (
addrRHR = 0x00
addrTHR = 0x00
addrIER = 0x01
addrFCR = 0x02
addrIIR = 0x02
addrLCR = 0x03
addrMCR = 0x04
addrLSR = 0x05
addrMSR = 0x06
addrSPR = 0x07
addrTCR = 0x06
addrTLR = 0x07
addrTXLVL = 0x08
addrRXLVL = 0x09
addrIODIR = 0x0A
addrIOSTATE = 0x0B
addrIOINTENA = 0x0C
addrIOCONTROL = 0x0E // This addr fails on write of 0x08?
addrEFCR = 0x0F
)
// Special registers
const (
addrDLL = 0x00
addrDLH = 1
)
const (
shortTxCooldown = time.Millisecond
longTxCooldown = 10 * time.Millisecond
rxTimeout = 100 * time.Millisecond
)
var (
cmd_readCO2 = [...]byte{0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}
cmd_measure = [...]byte{0xFF, 0x01, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63}
cmd_calibrateZero = [...]byte{0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78}
cmd_enableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}
cmd_disableAutoCalibration = [...]byte{0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}
)
// DevI2C is a handle to a MH-Z16 NDIR CO2 Sensor using the I2C interface.
type DevI2C struct {
bus drivers.I2C
addr uint8
nextAvail time.Time
initTime time.Time
lastMeasurement int32
}
// NewDevI2C returns a new NDIR device ready for use. It performs no I/O.
func NewDevI2C(bus drivers.I2C, addr uint8) *DevI2C {
return &DevI2C{
bus: bus,
addr: addr,
lastMeasurement: -1,
}
}
// PPM returns the CO2 parts per million read in the last Update call.
func (d *DevI2C) PPMCO2() int32 {
return d.lastMeasurement
}
var errInitWait = errors.New("ndir: must wait 12 seconds after init before reading concentration")
// Update reads the CO2 concentration from the NDIR and stores it ready for the
// PPM() method.
func (d *DevI2C) Update(which drivers.Measurement) (err error) {
if which&drivers.Concentration == 0 {
return nil // NDIR only measures concentration, so nothing to do here.
}
if time.Since(d.initTime) < 12*time.Second {
// Wait 12 seconds before performing first read.
return nil
}
err = d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
err = d.send(cmd_measure[:])
if err != nil {
return fmt.Errorf("sending cmd_measure: %w", err)
}
time.Sleep(11 * time.Millisecond)
var buf [9]byte
buf, err = d.receive()
if err != nil {
return fmt.Errorf("receiving during measure: %w", err)
}
if buf[0] != 0xff && buf[1] != 0x9c {
return fmt.Errorf("buffer rx bad values: %q", string(buf[:]))
}
var sum uint16
for i := 0; i < len(buf); i++ {
sum += uint16(buf[i])
}
mod := sum % 256
if mod != 0xff {
return fmt.Errorf("ndir checksum modulus got %#x, expected 0xff", mod)
}
ppm := uint32(buf[2])<<24 | uint32(buf[3])<<16 | uint32(buf[4])<<8 | uint32(buf[5])
d.lastMeasurement = int32(ppm)
return nil
}
func (d *DevI2C) Init() (err error) {
// AddrIOCONTROL write is always NACKed so ignore
// error here.
d.writeRegister(addrIOCONTROL, 0x08)
err = d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
err = d.writeRegister(addrLCR, 0x83)
if err != nil {
return err
}
err = d.writeRegister(addrDLL, 0x60)
if err != nil {
return err
}
err = d.writeRegister(addrDLH, 0x00)
if err != nil {
return err
}
err = d.writeRegister(addrLCR, 0x03)
if err != nil {
return err
}
d.initTime = time.Now()
return nil
}
// CalibrateZero calibrates the NDIR to around 412ppm.
func (d *DevI2C) CalibrateZero() error {
return d.enactCommand(cmd_calibrateZero[:])
}
// SetAutoCalibration can enable or disable the NDIR's auto calibration mode.
func (d *DevI2C) SetAutoCalibration(enable bool) (err error) {
if enable {
err = d.enactCommand(cmd_enableAutoCalibration[:])
} else {
err = d.enactCommand(cmd_disableAutoCalibration[:])
}
return err
}
func (d *DevI2C) send(cmd []byte) error {
txlvl, err := d.ReadRegister(addrTXLVL)
if err != nil {
return err
}
if int(txlvl) < len(cmd) {
return fmt.Errorf("txlvl=%d less than length of command %d", txlvl, len(cmd))
}
return d.tx(append([]byte{addrTHR}, cmd...), nil)
}
func (d *DevI2C) receive() (cmd [9]byte, err error) {
start := time.Now()
n := uint8(9)
for n > 0 {
if time.Since(start) > rxTimeout {
return [9]byte{}, errors.New("NDIR rx timeout")
}
rxlvl, err := d.ReadRegister(addrRXLVL)
if err != nil {
return [9]byte{}, err
}
if rxlvl > n {
rxlvl = n
}
ptr := 9 - n
err = d.tx([]byte{addrRHR << 3}, cmd[ptr:ptr+rxlvl])
n -= rxlvl
if err != nil {
return [9]byte{}, err
}
}
return cmd, nil
}
func (d *DevI2C) enactCommand(cmd []byte) error {
if len(cmd) > 31 {
return errors.New("ndir: command too long")
}
// Most commands always start with the same FCR write here.
err := d.writeRegister(addrFCR, 0x07)
if err != nil {
return err
}
time.Sleep(longTxCooldown)
// C++ send method begins here.
got, err := d.ReadRegister(addrTXLVL)
if err != nil {
return err
}
if got < uint8(len(cmd)) {
return fmt.Errorf("ndir: txlevel=%d too low for command of length %d", got, len(cmd))
}
var buf [32]byte
buf[0] = addrTHR
n := 1 + copy(buf[1:], cmd)
err = d.tx(buf[:n], nil)
if err != nil {
return err
}
d.nextAvail.Add(longTxCooldown) // add some extra time.
return nil
}
func (d *DevI2C) writeRegister(addr, val uint8) (err error) {
return d.WriteRegisters(addr, []byte{val})
}
func (d *DevI2C) WriteRegisters(addr uint8, vals []byte) (err error) {
var buf [32]byte
if len(vals) > 31 {
return errors.New("can only write up to 31 bytes")
}
buf[0] = addr << 3
n := copy(buf[1:], vals)
err = d.tx(buf[:n+1], nil)
if err != nil {
err = fmt.Errorf("NDIR write %#x (%d) to %#x: %w", buf[1], len(vals), buf[0], err)
}
return err
}
func (d *DevI2C) ReadRegister(addr uint8) (uint8, error) {
var buf [2]byte
buf[0] = addr << 3
err := d.tx(buf[:1], buf[1:2])
if err != nil {
err = fmt.Errorf("NDIR read from %#x: %w", buf[0], err)
}
return buf[1], err
}
func (d *DevI2C) tx(w, r []byte) error {
wait := time.Until(d.nextAvail)
if wait > 0 {
// Try yielding process first, maybe there's a short time to wait and a schedule call is enough delay.
runtime.Gosched()
wait = time.Until(d.nextAvail)
if wait > 0 {
// If yielding did not work then perform sleep
time.Sleep(wait)
}
}
err := d.bus.Tx(uint16(d.addr), w, r)
d.nextAvail = time.Now().Add(shortTxCooldown)
return err
}