mirror of https://github.com/periph/devices
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.
440 lines
14 KiB
Go
440 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"
|
|
"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 := 6
|
|
timeBase := time.Second
|
|
if liveDevice {
|
|
timeBase *= 10
|
|
}
|
|
dev, err := getDev(t, senseContinuousPlayback)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = dev.Halt() }()
|
|
defer shutdown(t)
|
|
t.Log("dev.sensing=", dev.sensing)
|
|
ch, err := dev.SenseContinuous(timeBase)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(time.Duration(readings) * timeBase)
|
|
_ = dev.Halt()
|
|
}()
|
|
received := 0
|
|
for env := range ch {
|
|
t.Log(env.String())
|
|
received += 1
|
|
}
|
|
if received < (readings-1) || received > readings {
|
|
t.Errorf("SenseContinuous() expected at least %d readings, got %d", readings-1, received)
|
|
}
|
|
|
|
}
|
|
|
|
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("using live device and 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)
|
|
}
|
|
}
|