Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions scd4x/scd4x.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ package scd4x // import "tinygo.org/x/drivers/scd4x"

import (
"encoding/binary"
"errors"
"time"

"tinygo.org/x/drivers"
)

// ErrCRC indicates that data received from the sensor failed its checksum.
var ErrCRC = errors.New("scd4x: invalid CRC")

type Device struct {
bus drivers.I2C
tx []byte
Expand Down Expand Up @@ -43,12 +47,12 @@ func (d *Device) Configure() (err error) {
}
time.Sleep(500 * time.Millisecond)

// reset the chip
// Reload the persisted settings while the sensor is idle.
if err := d.sendCommand(CmdReinit); err != nil {
return err
}

time.Sleep(20 * time.Millisecond)
time.Sleep(30 * time.Millisecond)
return
}

Expand Down Expand Up @@ -100,10 +104,17 @@ func (d *Device) ReadData() error {
return nil
}

// Update reads new data from the sensor (if new data is available) and caches
// it for reading in the CO2, Temperature, and Humidity methods.
// Update caches a new sample when it is available.
// It keeps the cached sample and returns nil when no new data is ready.
func (d *Device) Update(measurements drivers.Measurement) error {
if measurements&(drivers.Temperature|drivers.Humidity|drivers.Concentration) != 0 {
ready, err := d.DataReady()
if err != nil {
return err
}
if !ready {
return nil
}
return d.ReadData()
}
return nil
Expand Down Expand Up @@ -145,8 +156,7 @@ func (d *Device) ReadTemperature() (temperature int32, err error) {
// Temperature returns the last read temperature in celsius milli degrees
// (°C/1000).
func (d *Device) Temperature() int32 {
// temp = -45 + 175 * value / 2¹⁶
return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192)
return -45000 + 21875*int32(d.temperature)/8192
}

// ReadTempC returns the value in the temperature value in Celsius.
Expand Down Expand Up @@ -174,8 +184,7 @@ func (d *Device) ReadHumidity() (humidity int32, err error) {
if ok {
err = d.ReadData()
}
// humidity = 100 * value / 2¹⁶
return (25 * int32(d.humidity)) / 16384, err
return 25 * int32(d.humidity) / 16384, err
}

// Humidity returns the relative humidity in hundredths of a percent (in other
Expand All @@ -184,7 +193,7 @@ func (d *Device) ReadHumidity() (humidity int32, err error) {
// Warning: the value returned here is of a different scale (more precise) than
// ReadHumidity()!
func (d *Device) Humidity() int32 {
return (2500 * int32(d.humidity)) / 16384
return 2500 * int32(d.humidity) / 16384
}

func (d *Device) sendCommand(command uint16) error {
Expand All @@ -198,7 +207,15 @@ func (d *Device) sendCommandWithResult(command uint16, result []byte) error {
return err
}
time.Sleep(time.Millisecond)
return d.bus.Tx(uint16(d.Address), nil, result)
if err := d.bus.Tx(uint16(d.Address), nil, result); err != nil {
return err
}
for i := 0; i+2 < len(result); i += 3 {
if crc8(result[i:i+2]) != result[i+2] {
return ErrCRC
}
}
return nil
}

func crc8(buf []byte) uint8 {
Expand Down
74 changes: 74 additions & 0 deletions scd4x/scd4x_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

qt "github.com/frankban/quicktest"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/tester"
)

Expand All @@ -13,3 +14,76 @@ func TestDefaultI2CAddress(t *testing.T) {
dev := New(bus)
c.Assert(dev.Address, qt.Equals, uint8(Address))
}

func TestCRC8(t *testing.T) {
c := qt.New(t)
c.Assert(crc8([]byte{0xbe, 0xef}), qt.Equals, uint8(0x92))
}

func TestConversions(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
dev := New(bus)
dev.temperature = 0xffff
dev.humidity = 0xffff

c.Assert(dev.Temperature(), qt.Equals, int32(129997))
c.Assert(dev.Humidity(), qt.Equals, int32(9999))
}

func TestUpdateKeepsCachedDataWhenNotReady(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDeviceCmd(c, Address)
ready := &tester.Cmd{
Command: []byte{0xe4, 0xb8},
Mask: []byte{0xff, 0xff},
Response: []byte{0x00, 0x00, 0x81},
}
fake.Commands = map[uint8]*tester.Cmd{0: ready}
bus.AddDevice(fake)
dev := New(bus)
dev.co2 = 420
dev.temperature = 1000
dev.humidity = 2000

c.Assert(dev.Update(drivers.Concentration), qt.IsNil)
c.Assert(ready.Invocations, qt.Equals, 1)
c.Assert(dev.co2, qt.Equals, uint16(420))
c.Assert(dev.temperature, qt.Equals, uint16(1000))
c.Assert(dev.humidity, qt.Equals, uint16(2000))
}

func TestUpdateReadsDataAndChecksCRC(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDeviceCmd(c, Address)
ready := &tester.Cmd{
Command: []byte{0xe4, 0xb8},
Mask: []byte{0xff, 0xff},
Response: []byte{0x00, 0x01, 0xb0},
}
measurement := &tester.Cmd{
Command: []byte{0xec, 0x05},
Mask: []byte{0xff, 0xff},
Response: []byte{0x01, 0xa4, 0x00, 0x66, 0x66, 0x00, 0x80, 0x00, 0x00},
}
for i := 0; i < len(measurement.Response); i += 3 {
measurement.Response[i+2] = crc8(measurement.Response[i : i+2])
}
fake.Commands = map[uint8]*tester.Cmd{0: ready, 1: measurement}
bus.AddDevice(fake)
dev := New(bus)

c.Assert(dev.Update(drivers.Temperature), qt.IsNil)
c.Assert(measurement.Invocations, qt.Equals, 1)
c.Assert(dev.co2, qt.Equals, uint16(420))
c.Assert(dev.temperature, qt.Equals, uint16(0x6666))
c.Assert(dev.humidity, qt.Equals, uint16(0x8000))

measurement.Response[8] ^= 0xff
c.Assert(dev.Update(drivers.Humidity), qt.Equals, ErrCRC)
c.Assert(dev.co2, qt.Equals, uint16(420))
c.Assert(dev.temperature, qt.Equals, uint16(0x6666))
c.Assert(dev.humidity, qt.Equals, uint16(0x8000))
}