You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/scd4x/scd4x_test.go

452 lines
14 KiB
Go

// Copyright 2024 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.
//
// Unit tests for the package. Note that this supports running on a live
// sensor, or using playback mode to simulate a live device.
//
// To use a live device, define the environment variable SCD4X and run go test.
package scd4x
import (
"fmt"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/i2c/i2ctest"
"periph.io/x/conn/v3/physic"
"periph.io/x/host/v3"
)
var bus i2c.Bus
var liveDevice bool = false
// playback values for TestSense
var sensePlayback = []i2ctest.IO{
{Addr: SensorAddress, W: []uint8{0x36, 0xf6}},
{Addr: SensorAddress, W: []uint8{0x21, 0xb1}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x0, 0xa2}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x0, 0xa2}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x2c, 0xa3, 0x67, 0xd, 0x36, 0x4d, 0x8, 0xf1}}}
var senseContinuousPlayback = []i2ctest.IO{
{Addr: SensorAddress, W: []uint8{0x36, 0xf6}},
{Addr: SensorAddress, W: []uint8{0x21, 0xb1}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x1f, 0x35, 0x65, 0x82, 0xbb, 0x53, 0x5e, 0x2a}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x22, 0xbc, 0x65, 0x39, 0xee, 0x55, 0x4b, 0xc6}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x1c, 0x66, 0x64, 0xeb, 0x7c, 0x56, 0xd1, 0x9}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x1f, 0x35, 0x64, 0xad, 0xe7, 0x58, 0x2f, 0xf9}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x1f, 0x35, 0x64, 0x79, 0x27, 0x59, 0x71, 0x6c}},
{Addr: SensorAddress, W: []uint8{0xe4, 0xb8}, R: []uint8{0x80, 0x6, 0x4}},
{Addr: SensorAddress, W: []uint8{0xec, 0x5}, R: []uint8{0x2, 0x1f, 0x35, 0x64, 0x46, 0xcc, 0x5a, 0x8d, 0xbe}}}
var getSetTestPlayback = []i2ctest.IO{
{Addr: SensorAddress, W: []uint8{0x36, 0xf6}},
{Addr: SensorAddress, W: []uint8{0x21, 0xb1}},
{Addr: SensorAddress, W: []uint8{0x3f, 0x86}},
{Addr: SensorAddress, W: []uint8{0x36, 0x46}},
{Addr: SensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x0, 0x5, 0x74}},
{Addr: SensorAddress, W: []uint8{0x23, 0x13}, R: []uint8{0x0, 0x1, 0xb0}},
{Addr: SensorAddress, W: []uint8{0x23, 0x40}, R: []uint8{0x0, 0x2c, 0x7a}},
{Addr: SensorAddress, W: []uint8{0x23, 0x4b}, R: []uint8{0x0, 0x9c, 0xc5}},
{Addr: SensorAddress, W: []uint8{0x23, 0x3f}, R: []uint8{0x1, 0x90, 0x4c}},
{Addr: SensorAddress, W: []uint8{0x36, 0x82}, R: []uint8{0x73, 0xb1, 0x19, 0xeb, 0x7, 0x7a, 0x3b, 0xc, 0x54}},
{Addr: SensorAddress, W: []uint8{0x20, 0x2f}, R: []uint8{0x4, 0x41, 0xe}},
{Addr: SensorAddress, W: []uint8{0x23, 0x22}, R: []uint8{0x0, 0x0, 0x81}},
{Addr: SensorAddress, W: []uint8{0x23, 0x18}, R: []uint8{0x5, 0xda, 0x29}},
{Addr: SensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x0, 0x5, 0x74}},
{Addr: SensorAddress, W: []uint8{0x23, 0x13}, R: []uint8{0x0, 0x1, 0xb0}},
{Addr: SensorAddress, W: []uint8{0x23, 0x40}, R: []uint8{0x0, 0x2c, 0x7a}},
{Addr: SensorAddress, W: []uint8{0x23, 0x4b}, R: []uint8{0x0, 0x9c, 0xc5}},
{Addr: SensorAddress, W: []uint8{0x23, 0x3f}, R: []uint8{0x1, 0x90, 0x4c}},
{Addr: SensorAddress, W: []uint8{0x36, 0x82}, R: []uint8{0x73, 0xb1, 0x19, 0xeb, 0x7, 0x7a, 0x3b, 0xc, 0x54}},
{Addr: SensorAddress, W: []uint8{0x20, 0x2f}, R: []uint8{0x4, 0x41, 0xe}},
{Addr: SensorAddress, W: []uint8{0x23, 0x22}, R: []uint8{0x0, 0x0, 0x81}},
{Addr: SensorAddress, W: []uint8{0x23, 0x18}, R: []uint8{0x5, 0xda, 0x29}},
{Addr: SensorAddress, W: []uint8{0xe0, 0x0, 0x0, 0xa, 0x5a}},
{Addr: SensorAddress, W: []uint8{0x24, 0x16, 0x0, 0x0, 0x81}},
{Addr: SensorAddress, W: []uint8{0x24, 0x45, 0x0, 0x30, 0x44}},
{Addr: SensorAddress, W: []uint8{0x24, 0x4e, 0x0, 0xa0, 0x7d}},
{Addr: SensorAddress, W: []uint8{0x24, 0x3a, 0x1, 0xa4, 0x4d}},
{Addr: SensorAddress, W: []uint8{0x24, 0x27, 0x6, 0x44, 0x22}},
{Addr: SensorAddress, W: []uint8{0xe0, 0x0}, R: []uint8{0x0, 0xa, 0x5a}},
{Addr: SensorAddress, W: []uint8{0x23, 0x13}, R: []uint8{0x0, 0x0, 0x81}},
{Addr: SensorAddress, W: []uint8{0x23, 0x40}, R: []uint8{0x0, 0x30, 0x44}},
{Addr: SensorAddress, W: []uint8{0x23, 0x4b}, R: []uint8{0x0, 0xa0, 0x7d}},
{Addr: SensorAddress, W: []uint8{0x23, 0x3f}, R: []uint8{0x1, 0xa4, 0x4d}},
{Addr: SensorAddress, W: []uint8{0x36, 0x82}, R: []uint8{0x73, 0xb1, 0x19, 0xeb, 0x7, 0x7a, 0x3b, 0xc, 0x54}},
{Addr: SensorAddress, W: []uint8{0x20, 0x2f}, R: []uint8{0x4, 0x41, 0xe}},
{Addr: SensorAddress, W: []uint8{0x23, 0x22}, R: []uint8{0x6, 0x44, 0x22}},
{Addr: SensorAddress, W: []uint8{0x23, 0x18}, R: []uint8{0x5, 0xda, 0x29}},
{Addr: SensorAddress, W: []uint8{0x36, 0x46}}}
var basicStartup = []i2ctest.IO{
{Addr: SensorAddress, W: []uint8{0x36, 0xf6}},
{Addr: SensorAddress, W: []uint8{0x21, 0xb1}}}
func init() {
var err error
// If the environment variable is set, assume we have a live device on
// the default i2c bus and use it for testing. If the variable is not
// present, then use the playback/read values.
if os.Getenv("SCD4X") != "" {
liveDevice = true
}
if _, err = host.Init(); err != nil {
fmt.Println(err)
}
if liveDevice {
bus, err = i2creg.Open("")
if err != nil {
fmt.Println(err)
}
// Add the recorder to dump the data stream when we're using a live device.
bus = &i2ctest.Record{Bus: bus}
} else {
bus = &i2ctest.Playback{DontPanic: true}
}
}
// getDev returns an scd4x device for testing connected to either a live
// bus, or a playback bus. playbackOps is a slice of i2ctest.IO
// operations to be used for playback mode. Ignored for live device
// testing.
func getDev(t *testing.T, playbackOps ...[]i2ctest.IO) (*Dev, error) {
if liveDevice {
if recorder, ok := bus.(*i2ctest.Record); ok {
// Clear the operations buffer.
recorder.Ops = make([]i2ctest.IO, 0, 32)
}
} else {
if len(playbackOps) == 1 {
pb := bus.(*i2ctest.Playback)
pb.Ops = playbackOps[0]
pb.Count = 0
}
}
dev, err := NewI2C(bus, SensorAddress)
if err != nil {
t.Fatal(err)
}
return dev, err
}
// shutdown dumps the recorder values if we we're running a live device.
func shutdown(t *testing.T) {
if recorder, ok := bus.(*i2ctest.Record); ok {
t.Logf("%#v", recorder.Ops)
}
}
func TestCRC(t *testing.T) {
tests := []struct {
bytes []byte
crc byte
}{
{bytes: []byte{0xbe, 0xef}, crc: 0x92},
{bytes: []byte{0x01, 0xa4}, crc: 0x4d},
}
for _, test := range tests {
res := calcCRC(test.bytes)
if res != test.crc {
t.Error(fmt.Errorf("crc calculation error bytes: %#v, result: 0x%x expected: 0x%x", test.bytes, res, test.crc))
}
}
}
func TestCountToTemperature(t *testing.T) {
tests := []struct {
count uint16
expected physic.Temperature
}{
{count: 0x6667, expected: physic.ZeroCelsius + 25*physic.Celsius},
}
for _, test := range tests {
result := countToTemp(test.count)
// round to 2 sig figs for the floating point comparison.
result -= result % (10 * physic.MilliKelvin)
if result != test.expected {
t.Errorf("received: %.8f expected %.8f", result.Celsius(), test.expected.Celsius())
}
}
}
func TestCountToHumidity(t *testing.T) {
result := countToHumidity(0x5eb9) // from the datasheet
// Truncate to 2 decimals for comparison.
result -= result % physic.MilliRH
expected := physic.RelativeHumidity(37 * physic.PercentRH)
if result != expected {
t.Errorf("unexpected value: %d expected %d", result, expected)
}
}
// Non-device basic functionality.
func TestBasic(t *testing.T) {
dev, err := getDev(t, basicStartup)
if err != nil {
t.Fatal(err)
}
defer func() { _ = dev.Halt() }()
defer shutdown(t)
env := Env{}
dev.Precision(&env)
t.Logf("scd4x.Precision()=%#v\n", env)
if env.CO2 != 1 || env.Humidity != physic.TenthMicroRH || env.Temperature != (15259*physic.NanoKelvin) {
t.Error(fmt.Errorf("incorrect value for Precision(): %#v", env))
}
s := dev.String()
t.Logf("dev.String()=%s", s)
if len(s) == 0 {
t.Error("Dev.String() returned empty value.")
}
}
func TestSense(t *testing.T) {
dev, err := getDev(t, sensePlayback)
if err != nil {
t.Fatal(err)
}
defer func() { _ = dev.Halt() }()
defer shutdown(t)
env := Env{}
err = dev.Sense(&env)
if err != nil {
t.Error(err)
} else {
t.Log(env.String())
}
}
func TestSenseContinuous(t *testing.T) {
readings := int32(6)
timeBase := time.Second
if liveDevice {
timeBase *= 10
}
dev, err := getDev(t, senseContinuousPlayback)
if err != nil {
t.Fatal(err)
}
defer shutdown(t)
t.Log("dev.sensing=", dev.sensing)
ch, err := dev.SenseContinuous(timeBase)
if err != nil {
t.Error(err)
}
received := atomic.Int32{}
wg := sync.WaitGroup{}
wg.Add(1)
tEnd := time.Now().UnixMilli() + int64(readings+2)*1000
go func() {
for {
if received.Load() == readings || time.Now().UnixMilli() > tEnd {
_ = dev.Halt()
wg.Done()
break
}
}
}()
for env := range ch {
received.Add(1)
t.Log(env.String())
}
if received.Load() != readings {
t.Errorf("SenseContinuous() expected at least %d readings, got %d", readings-1, received.Load())
}
wg.Wait()
}
func TestGetSetConfiguration(t *testing.T) {
dev, err := getDev(t, getSetTestPlayback)
if err != nil {
t.Fatal(err)
}
err = dev.Halt()
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
// Baseline our settings
err = dev.Reset(ResetEEPROM)
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
defer shutdown(t)
cfg, err := dev.GetConfiguration()
if err != nil {
t.Error(err)
}
t.Logf("existing configuration: %#v", cfg)
cfg.AmbientPressure += 500 * physic.Pascal
cfg.ASCEnabled = !cfg.ASCEnabled
cfg.ASCInitialPeriod += 4 * time.Hour
cfg.ASCStandardPeriod += 4 * time.Hour
cfg.ASCTarget += 20
cfg.SensorAltitude = 1604 * physic.Metre
err = dev.SetConfiguration(cfg)
if err != nil {
t.Error(err)
}
read, err := dev.GetConfiguration()
if err != nil {
t.Error(err)
}
t.Logf("new configuration: %#v", read)
if read.AmbientPressure != cfg.AmbientPressure {
t.Errorf("scd4x: error setting ambient pressure. found: %s (%d) expected: %s (%d)", read.AmbientPressure.String(), read.AmbientPressure, cfg.AmbientPressure.String(), cfg.AmbientPressure)
}
if read.ASCEnabled != cfg.ASCEnabled {
t.Errorf("scd4x: error setting asc enabled. Found %t expected %t", read.ASCEnabled, cfg.ASCEnabled)
}
if read.ASCInitialPeriod != cfg.ASCInitialPeriod {
t.Errorf("scd4x: error setting initial period. found: %d expected %d", read.ASCInitialPeriod, cfg.ASCInitialPeriod)
}
if read.ASCStandardPeriod != cfg.ASCStandardPeriod {
t.Errorf("scd4x: error setting standard period. found: %d expected %d", read.ASCStandardPeriod, cfg.ASCStandardPeriod)
}
if read.ASCTarget != cfg.ASCTarget {
t.Errorf("scd4x: error setting asc target. found %d expected %d", read.ASCTarget, cfg.ASCTarget)
}
if read.SensorAltitude != cfg.SensorAltitude {
t.Errorf("scd4x: error setting sensor altitude. found %d expected %d", read.SensorAltitude/physic.Metre, cfg.SensorAltitude/physic.Metre)
}
_ = dev.Reset(ResetEEPROM) // and go back to our known state.
}
// Since there are limited read/write cycles, by default DO NOT test persist
// and reset factory. To perform the tests, define the environment variable
// SCDRESET. Running this test will destructively clear customized values
// previously programmed into the device.
func TestPersistAndResetFactory(t *testing.T) {
if !liveDevice || os.Getenv("SCDRESET") == "" {
t.Skip("not using live device or SCDRESET not defined. skipping")
}
dev, err := getDev(t)
if err != nil {
t.Fatal(err)
}
err = dev.Halt()
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
// Read the current running configuration.
cfg, err := dev.GetConfiguration()
if err != nil {
t.Fatal(err)
}
defer shutdown(t)
// Set the altitude to the current Altitude+1000M and write it to the device.
if cfg.SensorAltitude < (2000 * physic.Metre) {
cfg.SensorAltitude += 1000 * physic.Metre
} else {
cfg.SensorAltitude -= (500 * physic.Metre)
}
t.Logf("updating sensor altitude to %s", cfg.SensorAltitude)
err = dev.SetConfiguration(cfg)
if err != nil {
t.Fatal(err)
}
// Now, re-read the configuration to verify the write worked.
updatedCfg, err := dev.GetConfiguration()
if err != nil {
t.Fatal(err)
}
if updatedCfg.SensorAltitude != cfg.SensorAltitude {
t.Fatalf("scd41x: Change sensor altitude failed. Read: %s Expected: %s", updatedCfg.SensorAltitude.String(), cfg.SensorAltitude)
}
// OK, now Persist()
err = dev.Persist()
if err != nil {
t.Error(err)
}
_ = dev.Reset(ResetEEPROM)
time.Sleep(time.Second)
// OK, now write 0
cfg.SensorAltitude = 0
err = dev.SetConfiguration(cfg)
if err != nil {
t.Error(err)
}
// Reset Settings to EEPROM
err = dev.Reset(ResetEEPROM)
if err != nil {
t.Fatal(err)
}
// Sometimes you have to wait for it to come to the party...
for range 5 {
_ = dev.Halt()
// Now, re-read the configuration
cfg, err = dev.GetConfiguration()
if err != nil {
t.Logf("GetConfiguration Failed: %s Sleeping before retry.", err)
time.Sleep(time.Second)
} else {
break
}
}
if err != nil {
t.Fatal(err)
}
// The expected value is the original value +1000M
if cfg.SensorAltitude != updatedCfg.SensorAltitude {
t.Errorf("Error using reset to eeprom. Expected SensorAltitude: %s Found: %s", updatedCfg.SensorAltitude, cfg.SensorAltitude)
}
t.Logf("current configuration: %#v", cfg)
// Almost there. Now, reset to factory and read sensor-altitude.
t.Logf("calling reset factory")
err = dev.Reset(ResetFactory)
if err != nil {
t.Error(err)
}
time.Sleep(time.Second)
cfg, err = dev.GetConfiguration()
if err != nil {
t.Error(err)
}
t.Logf("Reset to factory configuration is now: %#v", cfg)
if cfg.SensorAltitude != 0 {
t.Errorf("Error resetting to factory. Sensor Altitude: %s expected 0m", cfg.SensorAltitude)
}
}