Files
drivers/scd30/scd30_test.go
T
2026-08-21 16:51:30 +02:00

354 lines
8.7 KiB
Go

package scd30
import (
"bytes"
"errors"
"math"
"testing"
"tinygo.org/x/drivers"
)
type transaction struct {
write []byte
response []byte
err error
}
type fakeBus struct {
t *testing.T
transactions []transaction
next int
wrongAddress bool
extraTransfer bool
}
func (b *fakeBus) Tx(address uint16, write, read []byte) error {
b.t.Helper()
if address != Address {
b.wrongAddress = true
b.t.Errorf("address = %#x, want %#x", address, Address)
return nil
}
if b.next >= len(b.transactions) {
b.extraTransfer = true
b.t.Errorf("unexpected transaction: write=%#v read-len=%d", write, len(read))
return nil
}
want := b.transactions[b.next]
b.next++
if !bytes.Equal(write, want.write) {
b.t.Errorf("write = %#v, want %#v", write, want.write)
}
if len(read) != len(want.response) {
b.t.Errorf("read length = %d, want %d", len(read), len(want.response))
}
if want.err != nil {
return want.err
}
copy(read, want.response)
return nil
}
func (b *fakeBus) verify(t *testing.T) {
t.Helper()
if b.next != len(b.transactions) {
t.Errorf("completed %d transactions, want %d", b.next, len(b.transactions))
}
if b.wrongAddress {
t.Error("driver used an unexpected address")
}
if b.extraTransfer {
t.Error("driver performed an unexpected transfer")
}
}
func newFakeBus(t *testing.T, transactions ...transaction) *fakeBus {
t.Helper()
bus := &fakeBus{t: t, transactions: transactions}
t.Cleanup(func() { bus.verify(t) })
return bus
}
func TestConfigure(t *testing.T) {
bus := newFakeBus(t,
transaction{write: []byte{0x46, 0x00, 0x00, 0x0a, 0x5a}},
transaction{write: []byte{0x53, 0x06, 0x00, 0x01, 0xb0}},
)
err := New(bus).Configure(Config{
MeasurementInterval: 10,
AutomaticSelfCalibration: true,
})
if err != nil {
t.Fatal(err)
}
}
func TestDefaultConfig(t *testing.T) {
if DefaultConfig.MeasurementInterval != 2 {
t.Errorf("default interval = %d, want 2", DefaultConfig.MeasurementInterval)
}
if DefaultConfig.AutomaticSelfCalibration {
t.Error("automatic self-calibration should be disabled by default")
}
}
func TestMeasurementIntervalBounds(t *testing.T) {
tests := []struct {
name string
seconds uint16
valid bool
}{
{name: "below minimum", seconds: 1},
{name: "minimum", seconds: 2, valid: true},
{name: "maximum", seconds: 1800, valid: true},
{name: "above maximum", seconds: 1801},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var transactions []transaction
if test.valid {
argument := []byte{byte(test.seconds >> 8), byte(test.seconds)}
transactions = append(transactions, transaction{write: []byte{
0x46, 0x00, argument[0], argument[1], crc8(argument),
}})
}
bus := newFakeBus(t, transactions...)
err := New(bus).SetMeasurementInterval(test.seconds)
if test.valid && err != nil {
t.Fatal(err)
}
if !test.valid && !errors.Is(err, ErrInvalidInterval) {
t.Fatalf("error = %v, want %v", err, ErrInvalidInterval)
}
})
}
}
func TestAmbientPressureBounds(t *testing.T) {
tests := []struct {
pressure uint16
valid bool
}{
{pressure: 0, valid: true},
{pressure: 699},
{pressure: 700, valid: true},
{pressure: 1400, valid: true},
{pressure: 1401},
}
for _, test := range tests {
t.Run(stringName(test.pressure), func(t *testing.T) {
var transactions []transaction
if test.valid {
argument := []byte{byte(test.pressure >> 8), byte(test.pressure)}
transactions = append(transactions, transaction{write: []byte{
0x00, 0x10, argument[0], argument[1], crc8(argument),
}})
}
bus := newFakeBus(t, transactions...)
err := New(bus).StartContinuousMeasurement(test.pressure)
if test.valid && err != nil {
t.Fatal(err)
}
if !test.valid && !errors.Is(err, ErrInvalidAmbientPressure) {
t.Fatalf("error = %v, want %v", err, ErrInvalidAmbientPressure)
}
})
}
}
func TestStopContinuousMeasurement(t *testing.T) {
bus := newFakeBus(t, transaction{write: []byte{0x01, 0x04}})
if err := New(bus).StopContinuousMeasurement(); err != nil {
t.Fatal(err)
}
}
func TestDataReady(t *testing.T) {
for _, test := range []struct {
name string
value uint16
ready bool
}{
{name: "not ready", value: 0},
{name: "ready", value: 1, ready: true},
} {
t.Run(test.name, func(t *testing.T) {
bus := newFakeBus(t,
transaction{write: []byte{0x02, 0x02}},
transaction{response: encodeWord(test.value)},
)
ready, err := New(bus).DataReady()
if err != nil {
t.Fatal(err)
}
if ready != test.ready {
t.Errorf("ready = %v, want %v", ready, test.ready)
}
})
}
}
func TestDataReadyRejectsBadCRC(t *testing.T) {
response := encodeWord(1)
response[2]++
bus := newFakeBus(t,
transaction{write: []byte{0x02, 0x02}},
transaction{response: response},
)
_, err := New(bus).DataReady()
if !errors.Is(err, ErrCRC) {
t.Fatalf("error = %v, want %v", err, ErrCRC)
}
}
func TestReadMeasurement(t *testing.T) {
response := appendFloat(nil, 800.5)
response = appendFloat(response, 23.25)
response = appendFloat(response, 48.75)
bus := newFakeBus(t,
transaction{write: []byte{0x03, 0x00}},
transaction{response: response},
)
device := New(bus)
if err := device.ReadMeasurement(); err != nil {
t.Fatal(err)
}
if got := device.CO2(); got != 801 {
t.Errorf("CO2 = %d, want 801 ppm", got)
}
if got := device.Temperature(); got != 23250 {
t.Errorf("temperature = %d, want 23250 mC", got)
}
if got := device.Humidity(); got != 4875 {
t.Errorf("humidity = %d, want 4875 hundredths of a percent", got)
}
}
func TestReadMeasurementRoundsNegativeTemperature(t *testing.T) {
response := appendFloat(nil, 400)
response = appendFloat(response, -10.1236)
response = appendFloat(response, 50)
bus := newFakeBus(t,
transaction{write: []byte{0x03, 0x00}},
transaction{response: response},
)
device := New(bus)
if err := device.ReadMeasurement(); err != nil {
t.Fatal(err)
}
if got := device.Temperature(); got != -10124 {
t.Errorf("temperature = %d, want -10124 mC", got)
}
}
func TestReadMeasurementRejectsBadCRCWithoutChangingCache(t *testing.T) {
for corruptWord := 0; corruptWord < 6; corruptWord++ {
t.Run(stringName(uint16(corruptWord)), func(t *testing.T) {
response := appendFloat(nil, 800.5)
response = appendFloat(response, 23.25)
response = appendFloat(response, 48.75)
response[corruptWord*3+2]++
bus := newFakeBus(t,
transaction{write: []byte{0x03, 0x00}},
transaction{response: response},
)
device := New(bus)
device.co2 = 500
device.temperature = 21000
device.humidity = 4000
err := device.ReadMeasurement()
if !errors.Is(err, ErrCRC) {
t.Fatalf("error = %v, want %v", err, ErrCRC)
}
if device.CO2() != 500 || device.Temperature() != 21000 || device.Humidity() != 4000 {
t.Errorf("cache changed after CRC error: CO2=%d temperature=%d humidity=%d",
device.CO2(), device.Temperature(), device.Humidity())
}
})
}
}
func TestUpdateIgnoresUnsupportedMeasurements(t *testing.T) {
bus := newFakeBus(t)
if err := New(bus).Update(drivers.Pressure); err != nil {
t.Fatal(err)
}
}
func TestBusErrorsAreReturned(t *testing.T) {
wantErr := errors.New("I2C failure")
for _, test := range []struct {
name string
transactions []transaction
action func(*Device) error
}{
{
name: "command write",
transactions: []transaction{{write: []byte{0x02, 0x02}, err: wantErr}},
action: func(device *Device) error {
_, err := device.DataReady()
return err
},
},
{
name: "response read",
transactions: []transaction{
{write: []byte{0x02, 0x02}},
{response: make([]byte, 3), err: wantErr},
},
action: func(device *Device) error {
_, err := device.DataReady()
return err
},
},
} {
t.Run(test.name, func(t *testing.T) {
bus := newFakeBus(t, test.transactions...)
if err := test.action(New(bus)); !errors.Is(err, wantErr) {
t.Fatalf("error = %v, want %v", err, wantErr)
}
})
}
}
func TestCRC8(t *testing.T) {
if got := crc8([]byte{0x00, 0x02}); got != 0xe3 {
t.Fatalf("crc = %#x, want 0xe3", got)
}
}
func appendFloat(destination []byte, value float32) []byte {
bits := math.Float32bits(value)
destination = append(destination, encodeWord(uint16(bits>>16))...)
destination = append(destination, encodeWord(uint16(bits))...)
return destination
}
func encodeWord(value uint16) []byte {
result := []byte{byte(value >> 8), byte(value), 0}
result[2] = crc8(result[:2])
return result
}
func stringName(value uint16) string {
const digits = "0123456789"
if value == 0 {
return "0"
}
var buffer [5]byte
position := len(buffer)
for value > 0 {
position--
buffer[position] = digits[value%10]
value /= 10
}
return string(buffer[position:])
}