mirror of https://github.com/periph/devices
scd4x: SCD4x CO2 Sensor - Initial Add (#81)
* Initial Add * Fix lint issue and inversion of skip testingpull/86/head
parent
5bc0352f3a
commit
9a938c42a7
@ -0,0 +1,56 @@
|
|||||||
|
# Sensirion SCD4x CO<sub>2</sub> Sensors
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides a driver for the Sensirion SCD4x CO<sub>2</sub> sensors. This is a
|
||||||
|
compact sensor that provides temperature, humidity, and CO<sub>2</sub> concentration
|
||||||
|
readings. The datasheet for this device is available at:
|
||||||
|
|
||||||
|
https://sensirion.com/media/documents/48C4B7FB/66E05452/CD_DS_SCD4x_Datasheet_D1.pdf
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The unit tests can function with either a live sensor, or in playback mode. If the
|
||||||
|
environment variable SCD4X is set, then the self test code will use a live
|
||||||
|
sensor on the default I<sup>2</sup>C bus. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$> SCD4X=1 go test -v
|
||||||
|
```
|
||||||
|
If the environment variable is not present, then unit tests will be conducted using
|
||||||
|
playback values.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### Acquisition Time
|
||||||
|
|
||||||
|
The minimum acquisition time for the sensor is 5 seconds. If you call Sense() more
|
||||||
|
frequently, it will block until a reading is ready.
|
||||||
|
|
||||||
|
### Forced Calibration and Self-Test
|
||||||
|
|
||||||
|
These functions are not implemented. From examining the datasheet, and
|
||||||
|
experimenting, it appears that these two calls require the i2c communication
|
||||||
|
driver to wait a specified period before initiating the read. The periph.io
|
||||||
|
I<sup>2</sup>C library doesn't support this functionality. This means that attempts
|
||||||
|
to call these functions will always fail so they're not implemented.
|
||||||
|
|
||||||
|
### Acquisition Mode
|
||||||
|
|
||||||
|
Only certain commands can be issued while the device is running in acquisition
|
||||||
|
mode. If you're working on the low-level code, be aware that attempts to send
|
||||||
|
a non-allowed command while in acquisition mode will return an i2c remote
|
||||||
|
io-error.
|
||||||
|
|
||||||
|
### Automatic Self Calibration
|
||||||
|
|
||||||
|
When Automatic Self Calibration is enabled, and the sensor has run for the
|
||||||
|
required period, it will adjust itself so that the LOWEST recorded reading
|
||||||
|
during the period yields the value set for ASC Target. The factory default
|
||||||
|
target is 400PPM, but the current PPM is ~425PPM. To get a more accurate
|
||||||
|
value for CO2 concentration in Earth's atmosphere, refer to:
|
||||||
|
|
||||||
|
https://www.co2.earth/daily-co2
|
||||||
|
|
||||||
|
For more details, refer to the datasheet.
|
||||||
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This package provides a driver for the Sensiron SCD4x CO2 sensors.
|
||||||
|
// The scd4x family provide a compact sensor that can be used to measure
|
||||||
|
// Temperature, Humidity, and CO2 concentration.
|
||||||
|
//
|
||||||
|
// Refer to the datasheet for more information.
|
||||||
|
//
|
||||||
|
// https://sensirion.com/media/documents/48C4B7FB/66E05452/CD_DS_SCD4x_Datasheet_D1.pdf
|
||||||
|
package scd4x
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
//go:build examples
|
||||||
|
// +build examples
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package scd4x_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3/i2c/i2creg"
|
||||||
|
"periph.io/x/devices/v3/scd4x"
|
||||||
|
"periph.io/x/host/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// basic example program for scd4x sensors using this library.
|
||||||
|
//
|
||||||
|
// To execute this as a stand-alone program:
|
||||||
|
//
|
||||||
|
// Copy the file example_test.go to a new directory.
|
||||||
|
// rename the file to main.go
|
||||||
|
// rename the Example() function to main, and the package to main
|
||||||
|
//
|
||||||
|
// execute:
|
||||||
|
//
|
||||||
|
// go mod init mydomain.com/scd4x
|
||||||
|
// go mod tidy
|
||||||
|
// go build -o main main.go
|
||||||
|
// ./main
|
||||||
|
func Example() {
|
||||||
|
fmt.Println("scd4x example program")
|
||||||
|
if _, err := host.Init(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
bus, err := i2creg.Open("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
dev, err := scd4x.NewI2C(bus, scd4x.SensorAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := scd4x.Env{}
|
||||||
|
err = dev.Sense(&env)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println(env.String())
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := dev.GetConfiguration()
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("Configuration: %#v\n", cfg)
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
// Output: Temperature: 24.845°C Humidity: 32.3%rH CO2: 581 PPM
|
||||||
|
// Configuration: &scd4x.DevConfig{AmbientPressure:0, ASCEnabled:true, ASCInitialPeriod:158400000000000, ASCStandardPeriod:561600000000000, ASCTarget:400, SensorAltitude:0, SerialNumber:127207989525260, TemperatureOffset:4, SensorType:0}
|
||||||
|
}
|
||||||
@ -0,0 +1,623 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package scd4x
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3/i2c"
|
||||||
|
"periph.io/x/conn/v3/physic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PPM=Parts Per Million. Units of measure for CO2 concentration.
|
||||||
|
type PPM int
|
||||||
|
|
||||||
|
// Sensor Variant type
|
||||||
|
type Variant int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SCD40 Variant = iota
|
||||||
|
SCD41
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type of reset to perform.
|
||||||
|
type ResetMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResetFactory ResetMode = iota
|
||||||
|
// Reset to last values stored in EEPROM
|
||||||
|
ResetEEPROM
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// These devices only support this i2c address.
|
||||||
|
SensorAddress uint16 = 0x62
|
||||||
|
)
|
||||||
|
|
||||||
|
type cmd uint16
|
||||||
|
|
||||||
|
// Structure to simplify sending commands to the device.
|
||||||
|
type command struct {
|
||||||
|
// The 16-bit command words.
|
||||||
|
cmdWord cmd
|
||||||
|
// The expected number of bytes returned. 0, 3, or 9.
|
||||||
|
responseSize int
|
||||||
|
// True if this command is permitted while the sensor is running in
|
||||||
|
// acquisition mode.
|
||||||
|
whileSensing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// The various implemented commands.
|
||||||
|
|
||||||
|
var cmdStartMeasurement = command{
|
||||||
|
cmdWord: 0x21b1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdReadMeasurement = command{
|
||||||
|
cmdWord: 0xec05,
|
||||||
|
responseSize: 9,
|
||||||
|
whileSensing: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdStopMeasurement = command{
|
||||||
|
cmdWord: 0x3f86,
|
||||||
|
whileSensing: true,
|
||||||
|
}
|
||||||
|
var cmdGetTemperatureOffset = command{
|
||||||
|
cmdWord: 0x2318,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdSetTemperatureOffset = command{
|
||||||
|
cmdWord: 0x241d,
|
||||||
|
}
|
||||||
|
var cmdGetSensorAltitude = command{
|
||||||
|
cmdWord: 0x2322,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdSetSensorAltitude = command{
|
||||||
|
cmdWord: 0x2427,
|
||||||
|
}
|
||||||
|
var cmdGetAmbientPressure = command{
|
||||||
|
cmdWord: 0xe000,
|
||||||
|
responseSize: 3,
|
||||||
|
whileSensing: true,
|
||||||
|
}
|
||||||
|
var cmdSetAmbientPressure = command{
|
||||||
|
cmdWord: 0xe000,
|
||||||
|
whileSensing: true,
|
||||||
|
}
|
||||||
|
var cmdSetASCEnabled = command{
|
||||||
|
cmdWord: 0x2416,
|
||||||
|
}
|
||||||
|
var cmdGetASCEnabled = command{
|
||||||
|
cmdWord: 0x2313,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdGetASCTarget = command{
|
||||||
|
cmdWord: 0x233f,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdSetASCTarget = command{
|
||||||
|
cmdWord: 0x243a,
|
||||||
|
}
|
||||||
|
var cmdGetDataReadyStatus = command{
|
||||||
|
cmdWord: 0xe4b8,
|
||||||
|
responseSize: 3,
|
||||||
|
whileSensing: true,
|
||||||
|
}
|
||||||
|
var cmdPersistSettings = command{
|
||||||
|
cmdWord: 0x3615,
|
||||||
|
}
|
||||||
|
var cmdGetSerialNumber = command{
|
||||||
|
cmdWord: 0x3682,
|
||||||
|
responseSize: 9,
|
||||||
|
}
|
||||||
|
var cmdPerformFactoryReset = command{
|
||||||
|
cmdWord: 0x3632,
|
||||||
|
}
|
||||||
|
var cmdReinit = command{
|
||||||
|
cmdWord: 0x3646,
|
||||||
|
}
|
||||||
|
var cmdGetSensorVariant = command{
|
||||||
|
cmdWord: 0x202f,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdGetASCInitialPeriod = command{
|
||||||
|
cmdWord: 0x2340,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdSetASCInitialPeriod = command{
|
||||||
|
cmdWord: 0x2445,
|
||||||
|
}
|
||||||
|
var cmdGetASCStandardPeriod = command{
|
||||||
|
cmdWord: 0x234b,
|
||||||
|
responseSize: 3,
|
||||||
|
}
|
||||||
|
var cmdSetASCStandardPeriod = command{
|
||||||
|
cmdWord: 0x244e,
|
||||||
|
}
|
||||||
|
var cmdWakeUp = command{
|
||||||
|
cmdWord: 0x36f6,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevConfig is the current running configuration of the device. Values prefixed
|
||||||
|
// with ASC refer to Auto-Self-Calibration. Use Dev.GetConfiguration() to read
|
||||||
|
// the value, and Dev.SetConfiguration() to apply changes.
|
||||||
|
//
|
||||||
|
// Refer to the datasheet for more information on settings.
|
||||||
|
type DevConfig struct {
|
||||||
|
// Ambient pressure value. Used to adjust operation of sensor.
|
||||||
|
AmbientPressure physic.Pressure
|
||||||
|
// Automatic-Self-Calibration enabled. True or false.
|
||||||
|
ASCEnabled bool
|
||||||
|
// Refer to datasheet for usage.
|
||||||
|
ASCInitialPeriod time.Duration
|
||||||
|
// Refer to datasheet for usage.
|
||||||
|
ASCStandardPeriod time.Duration
|
||||||
|
// Target CO2 concentration for automatic self calibration. To obtain the
|
||||||
|
// current value, visit:
|
||||||
|
//
|
||||||
|
// https://www.co2.earth/daily-co2
|
||||||
|
ASCTarget PPM
|
||||||
|
// Sensor altitude in metres. Alternative method to adjust ambient pressure
|
||||||
|
// for sensor correction.
|
||||||
|
SensorAltitude physic.Distance
|
||||||
|
// The 48 bit unique serial number of the device. Read-Only
|
||||||
|
SerialNumber int64
|
||||||
|
// Offset temperature added to reading. Refer to the datasheet for usage.
|
||||||
|
TemperatureOffset physic.Temperature
|
||||||
|
// The Type of sensor. SCD40 or SCD41. Read-Only
|
||||||
|
SensorType Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev represents an SCD4x device.
|
||||||
|
type Dev struct {
|
||||||
|
// The i2c bus device.
|
||||||
|
d *i2c.Dev
|
||||||
|
// channel to halt SenseContinuous
|
||||||
|
chHalt chan bool
|
||||||
|
mu sync.Mutex
|
||||||
|
// True if the device is in continuous sense mode.
|
||||||
|
sensing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ppm *PPM) String() string {
|
||||||
|
return fmt.Sprintf("%d PPM", *ppm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sensor reading. Returns CO2 PPM, Temperature, and Humidity.
|
||||||
|
type Env struct {
|
||||||
|
physic.Env
|
||||||
|
CO2 PPM
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the sensor readings in string format.
|
||||||
|
func (e *Env) String() string {
|
||||||
|
return fmt.Sprintf("Temperature: %s Humidity: %s CO2: %s", e.Temperature.String(), e.Humidity.String(), e.CO2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewI2c creates a new SCD4x sensor using the supplied bus and address.
|
||||||
|
// The constant value SensorAddress should be supplied as the value for
|
||||||
|
// addr.
|
||||||
|
func NewI2C(b i2c.Bus, addr uint16) (*Dev, error) {
|
||||||
|
d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, chHalt: nil}
|
||||||
|
return d, d.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfiguration returns a structure containing all of the scd4x configuration
|
||||||
|
// variables. You can then alter settings and call SetConfiguration with it.
|
||||||
|
//
|
||||||
|
// To examine the device use:
|
||||||
|
//
|
||||||
|
// cfg, _ :=dev.GetConfiguration()
|
||||||
|
// fmt.Printf("Configuration=%#v\n", cfg)
|
||||||
|
func (d *Dev) GetConfiguration() (*DevConfig, error) {
|
||||||
|
|
||||||
|
cfg := &DevConfig{}
|
||||||
|
var words []uint16
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetAmbientPressure, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.AmbientPressure = physic.Pascal * 100 * physic.Pressure(words[0])
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetASCEnabled, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.ASCEnabled = words[0] != 0
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetASCInitialPeriod, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.ASCInitialPeriod = time.Hour * time.Duration(words[0])
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetASCStandardPeriod, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.ASCStandardPeriod = time.Hour * time.Duration(words[0])
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetASCTarget, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.ASCTarget = PPM(words[0])
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetSerialNumber, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.SerialNumber = int64(words[0])<<32 | int64(words[1])<<16 | int64(words[2])
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetSensorVariant, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if (words[0]>>11)&0x07 == 0 {
|
||||||
|
cfg.SensorType = SCD40
|
||||||
|
} else {
|
||||||
|
cfg.SensorType = SCD41
|
||||||
|
}
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetSensorAltitude, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.SensorAltitude = physic.Distance(words[0]) * physic.Metre
|
||||||
|
|
||||||
|
if words, err = d.sendCommand(cmdGetTemperatureOffset, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.TemperatureOffset = countToOffset(words[0])
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfiguration alters the configuration of the sensor. Note that this call
|
||||||
|
// does not persist the settings to EEPROM. You need to call Persist() to
|
||||||
|
// commit the writes to EEPROM. If you do not persist changes, then those settings
|
||||||
|
// will be lost when the unit is power-cycled.
|
||||||
|
func (d *Dev) SetConfiguration(newCfg *DevConfig) error {
|
||||||
|
|
||||||
|
_ = d.Halt()
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
w := make([]uint16, 1)
|
||||||
|
currentConfig, err := d.GetConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("scd4x GetConfiguration(): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.AmbientPressure != newCfg.AmbientPressure {
|
||||||
|
w[0] = uint16(newCfg.AmbientPressure / (100 * physic.Pascal))
|
||||||
|
_, err := d.sendCommand(cmdSetAmbientPressure, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.ASCEnabled != newCfg.ASCEnabled {
|
||||||
|
|
||||||
|
if newCfg.ASCEnabled {
|
||||||
|
w[0] = 1
|
||||||
|
} else {
|
||||||
|
w[0] = 0
|
||||||
|
}
|
||||||
|
_, err := d.sendCommand(cmdSetASCEnabled, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.ASCInitialPeriod != newCfg.ASCInitialPeriod {
|
||||||
|
if newCfg.ASCInitialPeriod%4 != 0 {
|
||||||
|
return fmt.Errorf("scd4x: invalid initial period %d. must be a multiple of 4", newCfg.ASCInitialPeriod)
|
||||||
|
}
|
||||||
|
w[0] = uint16(newCfg.ASCInitialPeriod / time.Hour)
|
||||||
|
_, err := d.sendCommand(cmdSetASCInitialPeriod, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.ASCStandardPeriod != newCfg.ASCStandardPeriod {
|
||||||
|
if newCfg.ASCStandardPeriod%4 != 0 {
|
||||||
|
return fmt.Errorf("scd4x: invalid standard period %d. must be a multiple of 4", newCfg.ASCStandardPeriod)
|
||||||
|
}
|
||||||
|
w[0] = uint16(newCfg.ASCStandardPeriod / time.Hour)
|
||||||
|
_, err := d.sendCommand(cmdSetASCStandardPeriod, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.ASCTarget != newCfg.ASCTarget {
|
||||||
|
w[0] = uint16(newCfg.ASCTarget)
|
||||||
|
_, err := d.sendCommand(cmdSetASCTarget, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.SensorAltitude != newCfg.SensorAltitude {
|
||||||
|
w[0] = uint16(newCfg.SensorAltitude / physic.Metre)
|
||||||
|
_, err := d.sendCommand(cmdSetSensorAltitude, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentConfig.TemperatureOffset != newCfg.TemperatureOffset {
|
||||||
|
val := float64(newCfg.TemperatureOffset.Celsius()) * (float64(65535) / float64(175))
|
||||||
|
w[0] = uint16(val)
|
||||||
|
_, err := d.sendCommand(cmdSetTemperatureOffset, w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Halt stops continuous sensing if enabled, and if a SenseContinuous operation
|
||||||
|
// is in progress, it too is halted.
|
||||||
|
func (d *Dev) Halt() error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
if d.sensing {
|
||||||
|
if d.chHalt != nil {
|
||||||
|
close(d.chHalt)
|
||||||
|
}
|
||||||
|
d.sensing = false
|
||||||
|
_, err := d.sendCommand(cmdStopMeasurement, nil)
|
||||||
|
time.Sleep(550 * time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist writes the current running configuration to the sensor EEPROM for
|
||||||
|
// use on the next power-up.
|
||||||
|
func (d *Dev) Persist() error {
|
||||||
|
_, err := d.sendCommand(cmdPersistSettings, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset performs either a factory reset, or a re-load of settings from EEPROM
|
||||||
|
// depending on the value of mode. During development, it was noticed that
|
||||||
|
// ResetFactory DOES NOT reset AmbientPressure to 0.
|
||||||
|
func (d *Dev) Reset(mode ResetMode) error {
|
||||||
|
var err error
|
||||||
|
if mode == ResetFactory {
|
||||||
|
_, err = d.sendCommand(cmdPerformFactoryReset, nil)
|
||||||
|
} else if mode == ResetEEPROM {
|
||||||
|
_, err = d.sendCommand(cmdReinit, nil)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("scd4x: invalid reset mode 0x%x", mode)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcCRC(bytes []byte) byte {
|
||||||
|
polynomial := byte(0x31)
|
||||||
|
crc := byte(0xff)
|
||||||
|
for ix := range len(bytes) {
|
||||||
|
crc ^= bytes[ix]
|
||||||
|
for crc_bit := byte(8); crc_bit > 0; crc_bit-- {
|
||||||
|
if (crc & 0x80) == 0x80 {
|
||||||
|
crc = (crc << 1) ^ polynomial
|
||||||
|
} else {
|
||||||
|
crc = (crc << 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeWriteData converts the slice of word values into byte values with the
|
||||||
|
// CRC following.
|
||||||
|
func makeWriteData(data []uint16) []byte {
|
||||||
|
bytes := make([]byte, len(data)*3)
|
||||||
|
for ix, val := range data {
|
||||||
|
bytes[ix*3] = byte((val >> 8) & 0xff)
|
||||||
|
bytes[ix*3+1] = byte(val & 0xff)
|
||||||
|
bytes[ix*3+2] = calcCRC(bytes[ix*3 : ix*3+2])
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// All commands to read or write to the sensor go through this function.
|
||||||
|
func (d *Dev) sendCommand(cmd command, writeData []uint16) ([]uint16, error) {
|
||||||
|
|
||||||
|
if d.sensing && !cmd.whileSensing {
|
||||||
|
// We're in sense mode and this command isn't compatible. Stop sensing.
|
||||||
|
if err := d.Halt(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := make([]byte, 2)
|
||||||
|
w[0] = byte((cmd.cmdWord >> 8) & 0xff)
|
||||||
|
w[1] = byte(cmd.cmdWord & 0xff)
|
||||||
|
if writeData != nil {
|
||||||
|
writeBytes := makeWriteData(writeData)
|
||||||
|
w = append(w, writeBytes...)
|
||||||
|
}
|
||||||
|
var r []byte
|
||||||
|
if cmd.responseSize > 0 {
|
||||||
|
r = make([]byte, cmd.responseSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.d.Tx(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scd4x cmd 0x%x: %w", cmd.cmdWord, err)
|
||||||
|
}
|
||||||
|
if cmd.responseSize == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK, we need to convert the bytes into a slice of words and
|
||||||
|
// verify the CRC as we go.
|
||||||
|
result := make([]uint16, cmd.responseSize/3)
|
||||||
|
for ix := range len(result) {
|
||||||
|
crc := calcCRC(r[ix*3 : ix*3+2])
|
||||||
|
if r[ix*3+2] != crc {
|
||||||
|
return nil, fmt.Errorf("scd4x cmd 0x%x: invalid crc", cmd.cmdWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
word := uint16(r[ix*3])<<8 | uint16(r[ix*3+1])
|
||||||
|
|
||||||
|
result[ix] = word
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// start continuous sensing.
|
||||||
|
func (d *Dev) start() error {
|
||||||
|
if d.sensing {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
_, err := d.sendCommand(cmdWakeUp, nil)
|
||||||
|
if err != nil {
|
||||||
|
// If an SCD4x is in measurement mode, then any non-measurement mode
|
||||||
|
// command will return an error. In that case, send a stop measurement
|
||||||
|
// command, wait the specified time and try sending a re-init.
|
||||||
|
_, _ = d.sendCommand(cmdStopMeasurement, nil)
|
||||||
|
time.Sleep(550 * time.Millisecond)
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
_, err = d.sendCommand(cmdStartMeasurement, nil)
|
||||||
|
if err == nil {
|
||||||
|
d.sensing = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formula used for temperature offset calculation.
|
||||||
|
func countToOffset(count uint16) physic.Temperature {
|
||||||
|
frac := 175.0 / 65535.0
|
||||||
|
return physic.Temperature(frac * float64(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
// countToTemp converts a device count to Temperature
|
||||||
|
func countToTemp(count uint16) physic.Temperature {
|
||||||
|
frac := float64(count) / 65535.0
|
||||||
|
result := -45 + 175*frac
|
||||||
|
return physic.ZeroCelsius + physic.Temperature(float64(physic.Celsius)*result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func countToHumidity(count uint16) physic.RelativeHumidity {
|
||||||
|
frac := float64(count) / 65535.0
|
||||||
|
return physic.RelativeHumidity(frac * 100.0 * float64(physic.PercentRH))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sense returns readings (Temperature, Humidity, and CO2 concentration in PPM)
|
||||||
|
// from the device. Note that in normal acquisition mode, the minimum reading
|
||||||
|
// period is 5 seconds. If you call this function more frequently than this,
|
||||||
|
// it will block until data is ready.
|
||||||
|
func (d *Dev) Sense(env *Env) error {
|
||||||
|
env.Temperature = 0
|
||||||
|
env.Humidity = 0
|
||||||
|
env.CO2 = 0
|
||||||
|
env.Pressure = 0
|
||||||
|
|
||||||
|
if !d.sensing {
|
||||||
|
err := d.start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
ready := false
|
||||||
|
mask := uint16(1<<11 - 1)
|
||||||
|
tCutoff := time.Now().Unix() + 6
|
||||||
|
for !ready && time.Now().Unix() < tCutoff {
|
||||||
|
words, err := d.sendCommand(cmdGetDataReadyStatus, nil)
|
||||||
|
ready = err == nil && (words[0]&mask) > 0
|
||||||
|
if !ready {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ready {
|
||||||
|
return errors.New("scd4x: timeout waiting for data ready status")
|
||||||
|
}
|
||||||
|
words, err := d.sendCommand(cmdReadMeasurement, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
env.CO2 = PPM(words[0])
|
||||||
|
env.Temperature = countToTemp(words[1])
|
||||||
|
env.Humidity = countToHumidity(words[2])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SenseContinuous continuously reads the sensor on the specified duration, and
|
||||||
|
// writes readings to the returned channel. The sense time for the scd4x device
|
||||||
|
// is 5 seconds in normal acquisition mode. If you specify a shorter period than
|
||||||
|
// that, the routine will spin until the device indicates a reading is ready. To
|
||||||
|
// terminate a continuous sense, call Halt().
|
||||||
|
func (d *Dev) SenseContinuous(interval time.Duration) (<-chan Env, error) {
|
||||||
|
if d.chHalt != nil {
|
||||||
|
return nil, errors.New("scd4x: SenseContinuous() running already")
|
||||||
|
}
|
||||||
|
if !d.sensing {
|
||||||
|
if err := d.start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelSize := 16
|
||||||
|
channel := make(chan Env, channelSize)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
defer close(channel)
|
||||||
|
if d.chHalt == nil {
|
||||||
|
d.chHalt = make(chan bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { d.chHalt = nil }()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-d.chHalt:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// do the reading and write to the channel.
|
||||||
|
e := Env{}
|
||||||
|
err := d.Sense(&e)
|
||||||
|
if err == nil && len(channel) < channelSize {
|
||||||
|
channel <- e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precision returns the sensor's resolution, or minimum value between steps the
|
||||||
|
// device can make. The specified precision is 1 PPM for CO2, 1/65535 for temperature
|
||||||
|
// and humidity.
|
||||||
|
func (d *Dev) Precision(env *Env) {
|
||||||
|
countIncrement := float64(1.0) / float64((1<<16)-1)
|
||||||
|
env.Temperature = physic.Temperature(countIncrement * float64(physic.Celsius))
|
||||||
|
env.Pressure = 0
|
||||||
|
env.Humidity = physic.RelativeHumidity(float64(physic.PercentRH) * countIncrement)
|
||||||
|
env.CO2 = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) String() string {
|
||||||
|
return fmt.Sprintf("scd4x: %s", d.d.String())
|
||||||
|
}
|
||||||
@ -0,0 +1,439 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue