diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index a53bb9d14..ae87f83e3 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -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 @@ -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 } @@ -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 @@ -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. @@ -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 @@ -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 { @@ -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 { diff --git a/scd4x/scd4x_test.go b/scd4x/scd4x_test.go index 25bd0fb0b..95bb1a7e1 100644 --- a/scd4x/scd4x_test.go +++ b/scd4x/scd4x_test.go @@ -4,6 +4,7 @@ import ( "testing" qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers" "tinygo.org/x/drivers/tester" ) @@ -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)) +}