From af64beda1a21641c2641af599ab987444e1eed2e Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Mon, 21 Sep 2026 13:16:48 -0400 Subject: [PATCH 1/2] Adhere to the data sheet better - Checks DataReady() before reading. - Returns ErrDataNotReady without restarting the sensor. - Validates CRCs on all returned words. - Waits the required 30 ms after reinit. - Uses the correct 65535 conversion denominator. - Waits 30 ms for power-up before starting measurements. - Removes the unnecessary startup Configure() call. - Adds CRC and conversion tests. --- scd4x/scd4x.go | 42 +++++++++++++++++++++++++++++++++--------- scd4x/scd4x_test.go | 32 +++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index a53bb9d14..315e5cf04 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -5,15 +5,24 @@ // This driver is heavily influenced by the scd4x code from Adafruit for CircuitPython: // https://github.com/adafruit/Adafruit_CircuitPython_SCD4X // Thank you! -package scd4x // import "tinygo.org/x/drivers/scd4x" +package scd4x //import "tinygo.org/x/drivers/scd4x" import ( "encoding/binary" + "errors" "time" "tinygo.org/x/drivers" ) +var ( + // ErrDataNotReady indicates that the sensor has not produced a new sample + // since the previous read. + ErrDataNotReady = errors.New("scd4x: data not ready") + // ErrCRC indicates that data received from the sensor failed its checksum. + ErrCRC = errors.New("scd4x: invalid CRC") +) + type Device struct { bus drivers.I2C tx []byte @@ -43,12 +52,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 } @@ -104,6 +113,13 @@ func (d *Device) ReadData() error { // it for reading in the CO2, Temperature, and Humidity methods. 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 ErrDataNotReady + } return d.ReadData() } return nil @@ -145,8 +161,8 @@ 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) + // temp = -45 + 175 * value / (2¹⁶ - 1) + return -45000 + int32(175000*int64(d.temperature)/65535) } // ReadTempC returns the value in the temperature value in Celsius. @@ -174,8 +190,8 @@ func (d *Device) ReadHumidity() (humidity int32, err error) { if ok { err = d.ReadData() } - // humidity = 100 * value / 2¹⁶ - return (25 * int32(d.humidity)) / 16384, err + // humidity = 100 * value / (2¹⁶ - 1) + return int32(100 * int64(d.humidity) / 65535), err } // Humidity returns the relative humidity in hundredths of a percent (in other @@ -184,7 +200,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 int32(10000 * int64(d.humidity) / 65535) } func (d *Device) sendCommand(command uint16) error { @@ -198,7 +214,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..965a2bc57 100644 --- a/scd4x/scd4x_test.go +++ b/scd4x/scd4x_test.go @@ -2,14 +2,32 @@ package scd4x import ( "testing" - - qt "github.com/frankban/quicktest" - "tinygo.org/x/drivers/tester" ) +type testI2CBus struct{} + +func (testI2CBus) Tx(uint16, []byte, []byte) error { return nil } + func TestDefaultI2CAddress(t *testing.T) { - c := qt.New(t) - bus := tester.NewI2CBus(c) - dev := New(bus) - c.Assert(dev.Address, qt.Equals, uint8(Address)) + dev := New(testI2CBus{}) + if dev.Address != uint8(Address) { + t.Fatalf("Address = %#x, want %#x", dev.Address, Address) + } +} + +func TestCRC8(t *testing.T) { + if got := crc8([]byte{0xbe, 0xef}); got != 0x92 { + t.Fatalf("crc8(0xbeef) = %#x, want 0x92", got) + } +} + +func TestConversionsUseFullScale(t *testing.T) { + dev := &Device{temperature: 0xffff, humidity: 0xffff} + + if got := dev.Temperature(); got != 130000 { + t.Errorf("Temperature() = %d, want 130000", got) + } + if got := dev.Humidity(); got != 10000 { + t.Errorf("Humidity() = %d, want 10000", got) + } } From 917d5f80a37bbe860f2a50d3fd9854b6526ab570 Mon Sep 17 00:00:00 2001 From: Matthew Hiles Date: Thu, 22 Oct 2026 18:27:34 -0400 Subject: [PATCH 2/2] address review comments --- scd4x/scd4x.go | 25 +++++-------- scd4x/scd4x_test.go | 90 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 33 deletions(-) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index 315e5cf04..ae87f83e3 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -5,7 +5,7 @@ // This driver is heavily influenced by the scd4x code from Adafruit for CircuitPython: // https://github.com/adafruit/Adafruit_CircuitPython_SCD4X // Thank you! -package scd4x //import "tinygo.org/x/drivers/scd4x" +package scd4x // import "tinygo.org/x/drivers/scd4x" import ( "encoding/binary" @@ -15,13 +15,8 @@ import ( "tinygo.org/x/drivers" ) -var ( - // ErrDataNotReady indicates that the sensor has not produced a new sample - // since the previous read. - ErrDataNotReady = errors.New("scd4x: data not ready") - // ErrCRC indicates that data received from the sensor failed its checksum. - ErrCRC = errors.New("scd4x: invalid CRC") -) +// ErrCRC indicates that data received from the sensor failed its checksum. +var ErrCRC = errors.New("scd4x: invalid CRC") type Device struct { bus drivers.I2C @@ -109,8 +104,8 @@ 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() @@ -118,7 +113,7 @@ func (d *Device) Update(measurements drivers.Measurement) error { return err } if !ready { - return ErrDataNotReady + return nil } return d.ReadData() } @@ -161,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¹⁶ - 1) - return -45000 + int32(175000*int64(d.temperature)/65535) + return -45000 + 21875*int32(d.temperature)/8192 } // ReadTempC returns the value in the temperature value in Celsius. @@ -190,8 +184,7 @@ func (d *Device) ReadHumidity() (humidity int32, err error) { if ok { err = d.ReadData() } - // humidity = 100 * value / (2¹⁶ - 1) - return int32(100 * int64(d.humidity) / 65535), err + return 25 * int32(d.humidity) / 16384, err } // Humidity returns the relative humidity in hundredths of a percent (in other @@ -200,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 int32(10000 * int64(d.humidity) / 65535) + return 2500 * int32(d.humidity) / 16384 } func (d *Device) sendCommand(command uint16) error { diff --git a/scd4x/scd4x_test.go b/scd4x/scd4x_test.go index 965a2bc57..95bb1a7e1 100644 --- a/scd4x/scd4x_test.go +++ b/scd4x/scd4x_test.go @@ -2,32 +2,88 @@ package scd4x import ( "testing" -) - -type testI2CBus struct{} -func (testI2CBus) Tx(uint16, []byte, []byte) error { return nil } + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/tester" +) func TestDefaultI2CAddress(t *testing.T) { - dev := New(testI2CBus{}) - if dev.Address != uint8(Address) { - t.Fatalf("Address = %#x, want %#x", dev.Address, Address) - } + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + c.Assert(dev.Address, qt.Equals, uint8(Address)) } func TestCRC8(t *testing.T) { - if got := crc8([]byte{0xbe, 0xef}); got != 0x92 { - t.Fatalf("crc8(0xbeef) = %#x, want 0x92", got) - } + 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 TestConversionsUseFullScale(t *testing.T) { - dev := &Device{temperature: 0xffff, humidity: 0xffff} +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)) +} - if got := dev.Temperature(); got != 130000 { - t.Errorf("Temperature() = %d, want 130000", got) +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}, } - if got := dev.Humidity(); got != 10000 { - t.Errorf("Humidity() = %d, want 10000", got) + 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)) }