mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-09-01 20:39:04 +00:00
354 lines
8.7 KiB
Go
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:])
|
|
}
|