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.
809 lines
18 KiB
Go
809 lines
18 KiB
Go
// Copyright 2026 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 sen6x
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"periph.io/x/conn/v3/i2c/i2ctest"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
dev := New(&i2ctest.Playback{}, SEN66)
|
|
|
|
if dev == nil {
|
|
t.Fatalf("dev is nil")
|
|
}
|
|
|
|
if dev.model != SEN66 {
|
|
t.Fatalf("got model %v, want %v", dev.model, SEN66)
|
|
}
|
|
}
|
|
|
|
func TestDevString(t *testing.T) {
|
|
d := newTestDev(t, nil, SEN66)
|
|
got := d.String()
|
|
want := "SEN66"
|
|
if got != want {
|
|
t.Fatalf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// sen66MeasurementOps returns the i2ctest.IO entries for a single SEN66
|
|
// measurement cycle. If withGetDataReady is true then the returned ops will
|
|
// start with GetDataReady returning ready=true.
|
|
func sen66MeasurementOps(t *testing.T, withGetDataReady bool) []i2ctest.IO {
|
|
t.Helper()
|
|
|
|
// SEN66 measurement data.
|
|
data := []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x00, 0x64, 0xfe, // VOC
|
|
0x00, 0x46, 0x1a, // NOx
|
|
0x01, 0xf4, 0x33, // CO2
|
|
}
|
|
|
|
ops := []i2ctest.IO{}
|
|
|
|
if withGetDataReady {
|
|
ops = append(ops,
|
|
// GetDataReady write.
|
|
i2ctest.IO{Addr: i2cAddr, W: []byte{0x02, 0x02}},
|
|
// GetDataReady read.
|
|
i2ctest.IO{Addr: i2cAddr, R: []byte{0x00, 0x01, crc8(0x00, 0x01)}},
|
|
)
|
|
}
|
|
|
|
ops = append(ops,
|
|
// ReadMeasuredValues write.
|
|
i2ctest.IO{Addr: i2cAddr, W: []byte{0x03, 0x00}},
|
|
// ReadMeasuredValues read.
|
|
i2ctest.IO{Addr: i2cAddr, R: data},
|
|
)
|
|
|
|
return ops
|
|
}
|
|
|
|
func TestSenseContinuous(t *testing.T) {
|
|
// Set the device's sampling interval to a small value so that tests run
|
|
// quickly. Set it back after the tests finish.
|
|
originalSamplingInterval := deviceSamplingInterval
|
|
defer func() {
|
|
deviceSamplingInterval = originalSamplingInterval
|
|
}()
|
|
deviceSamplingInterval = 2 * time.Millisecond
|
|
|
|
// We'll use intervals shorter and longer than the device's native interval.
|
|
shortTestInterval := deviceSamplingInterval - time.Millisecond
|
|
longTestInterval := deviceSamplingInterval + time.Millisecond
|
|
|
|
cases := []senseContinuousTestCase{
|
|
{
|
|
name: "success short interval",
|
|
model: SEN66,
|
|
// An interval shorter than the device's internal sampling interval
|
|
// will cause GetDataReady to be polled.
|
|
interval: shortTestInterval,
|
|
expectedMeasurementCount: 3,
|
|
ops: func() []i2ctest.IO {
|
|
// StartContinuousMeasurement write.
|
|
ops := []i2ctest.IO{
|
|
{Addr: i2cAddr, W: []byte{0x00, 0x21}},
|
|
}
|
|
|
|
// Three measurements.
|
|
for range 3 {
|
|
ops = append(ops, sen66MeasurementOps(t, true)...)
|
|
}
|
|
|
|
// StopMeasurement from Halt.
|
|
ops = append(ops,
|
|
i2ctest.IO{Addr: i2cAddr, W: []byte{0x01, 0x04}},
|
|
)
|
|
|
|
return ops
|
|
}(),
|
|
},
|
|
{
|
|
name: "success long interval",
|
|
model: SEN66,
|
|
// An interval longer than the device's internal sampling interval will
|
|
// result in GetDataReady not being polled before reading measurements.
|
|
interval: longTestInterval,
|
|
expectedMeasurementCount: 3,
|
|
ops: func() []i2ctest.IO {
|
|
// StartContinuousMeasurement write.
|
|
ops := []i2ctest.IO{
|
|
{Addr: i2cAddr, W: []byte{0x00, 0x21}},
|
|
}
|
|
|
|
// Three measurements.
|
|
for range 3 {
|
|
ops = append(ops, sen66MeasurementOps(t, false)...)
|
|
}
|
|
|
|
// StopMeasurement from Halt.
|
|
ops = append(ops,
|
|
i2ctest.IO{Addr: i2cAddr, W: []byte{0x01, 0x04}},
|
|
)
|
|
|
|
return ops
|
|
}(),
|
|
},
|
|
{
|
|
name: "fails to start",
|
|
model: SEN66,
|
|
interval: shortTestInterval,
|
|
expectedMeasurementCount: 0,
|
|
ops: []i2ctest.IO{
|
|
// StartContinuousMeasurement write has no matching op
|
|
// so i2ctest will return an error, simulating a bus error.
|
|
|
|
// StopMeasurement from Halt.
|
|
{Addr: i2cAddr, W: []byte{0x01, 0x04}},
|
|
},
|
|
dontPanic: true,
|
|
},
|
|
{
|
|
name: "waitOnDataReady fails",
|
|
model: SEN66,
|
|
interval: shortTestInterval,
|
|
expectedMeasurementCount: 0,
|
|
ops: []i2ctest.IO{
|
|
// StartContinuousMeasurement write.
|
|
{Addr: i2cAddr, W: []byte{0x00, 0x21}},
|
|
// GetDataReady write.
|
|
{Addr: i2cAddr, W: []byte{0x02, 0x02}},
|
|
// No GetDataReady read. This simulates a bus error in waitOnDataReady.
|
|
|
|
// StopMeasurement from Halt.
|
|
{Addr: i2cAddr, W: []byte{0x01, 0x04}},
|
|
},
|
|
dontPanic: true,
|
|
},
|
|
|
|
{
|
|
name: "doReadMeasuredValues fails",
|
|
model: SEN66,
|
|
interval: shortTestInterval,
|
|
expectedMeasurementCount: 0,
|
|
ops: []i2ctest.IO{
|
|
// StartContinuousMeasurement write.
|
|
{Addr: i2cAddr, W: []byte{0x00, 0x21}},
|
|
|
|
// A partial waitOnDataReady cycle that omits the measurement itself.
|
|
// GetDataReady write.
|
|
{Addr: i2cAddr, W: []byte{0x02, 0x02}},
|
|
// GetDataReady read.
|
|
{Addr: i2cAddr, R: []byte{0x00, 0x01, crc8(0x00, 0x01)}},
|
|
// ReadMeasuredValues write.
|
|
{Addr: i2cAddr, W: []byte{0x03, 0x00}},
|
|
// No ReadMeasuredValues read. This simulates a bus error in doReadMeasuredValues.
|
|
|
|
// StopMeasurement from Halt.
|
|
{Addr: i2cAddr, W: []byte{0x01, 0x04}},
|
|
},
|
|
dontPanic: true,
|
|
},
|
|
}
|
|
|
|
runSenseContinuousTests(t, cases)
|
|
}
|
|
|
|
func TestDevStartContinuousMeasurement(t *testing.T) {
|
|
cases := []writeTestCase{
|
|
{
|
|
name: "success",
|
|
model: SEN66,
|
|
tx: []byte{0x00, 0x21},
|
|
},
|
|
}
|
|
|
|
runWriteTests(t, cases, (*Dev).StartContinuousMeasurement)
|
|
}
|
|
|
|
func TestDevStopMeasurement(t *testing.T) {
|
|
cases := []writeTestCase{
|
|
{
|
|
name: "success",
|
|
model: SEN66,
|
|
tx: []byte{0x01, 0x04},
|
|
},
|
|
}
|
|
|
|
runWriteTests(t, cases, (*Dev).StopMeasurement)
|
|
}
|
|
|
|
func TestDevGetDataReady(t *testing.T) {
|
|
cmd := []byte{0x02, 0x02}
|
|
|
|
cases := []writeAndReadTestCase[bool]{
|
|
{
|
|
name: "false",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{0x00, 0x00, 0x81},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "true",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{0x00, 0x01, 0xb0},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "bad CRC",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{0x00, 0x01, 0xff},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
runWriteAndReadTests(t, cases, (*Dev).GetDataReady)
|
|
}
|
|
|
|
func TestDevReadMeasuredValues(t *testing.T) {
|
|
cases := []writeAndReadTestCase[*SensorValues]{
|
|
{
|
|
name: "SEN62",
|
|
model: SEN62,
|
|
tx: []byte{0x04, 0xA3},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN63C",
|
|
model: SEN63C,
|
|
tx: []byte{0x04, 0x71},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x01, 0xf4, 0x33, // CO2
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
CO2: ptr(int16(500)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN65",
|
|
model: SEN65,
|
|
tx: []byte{0x04, 0x46},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x00, 0x64, 0xfe, // VOC
|
|
0x00, 0x46, 0x1a, // NOx
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
VOC: ptr(float32(10)),
|
|
NOx: ptr(float32(7)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN66",
|
|
model: SEN66,
|
|
tx: []byte{0x03, 0x00},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x00, 0x64, 0xfe, // VOC
|
|
0x00, 0x46, 0x1a, // NOx
|
|
0x01, 0xf4, 0x33, // CO2
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
VOC: ptr(float32(10)),
|
|
NOx: ptr(float32(7)),
|
|
CO2: ptr(int16(500)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN68",
|
|
model: SEN68,
|
|
tx: []byte{0x04, 0x67},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x00, 0x64, 0xfe, // VOC
|
|
0x00, 0x46, 0x1a, // NOx
|
|
0x01, 0xf4, 0x33, // HCHO
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
VOC: ptr(float32(10)),
|
|
NOx: ptr(float32(7)),
|
|
HCHO: ptr(float32(50)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN69C",
|
|
model: SEN69C,
|
|
tx: []byte{0x04, 0xb5},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0x27, // PM10.0
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
0x00, 0x64, 0xfe, // VOC
|
|
0x00, 0x46, 0x1a, // NOx
|
|
0x01, 0xf4, 0x33, // HCHO
|
|
0x01, 0xf4, 0x33, // CO2
|
|
},
|
|
want: &SensorValues{
|
|
PM1: ptr(float32(0.4)),
|
|
PM25: ptr(float32(0.5)),
|
|
PM4: ptr(float32(0.6)),
|
|
PM10: ptr(float32(0.6)),
|
|
RH: ptr(float32(53.6)),
|
|
Temp: ptr(float32(22.295)),
|
|
VOC: ptr(float32(10)),
|
|
NOx: ptr(float32(7)),
|
|
HCHO: ptr(float32(50)),
|
|
CO2: ptr(int16(500)),
|
|
},
|
|
},
|
|
{
|
|
// This covers the uint16 CO2 encoding used by the SEN66.
|
|
name: "SEN66 all values unknown",
|
|
model: SEN66,
|
|
tx: []byte{0x03, 0x00},
|
|
rx: []byte{
|
|
0xff, 0xff, 0xac, // PM1.0
|
|
0xff, 0xff, 0xac, // PM2.5
|
|
0xff, 0xff, 0xac, // PM4.0
|
|
0xff, 0xff, 0xac, // PM10.0
|
|
0x7f, 0xff, 0x8f, // RH
|
|
0x7f, 0xff, 0x8f, // Temp
|
|
0x7f, 0xff, 0x8f, // VOC
|
|
0x7f, 0xff, 0x8f, // NOx
|
|
0xff, 0xff, 0xac, // CO2 as uint
|
|
},
|
|
want: &SensorValues{},
|
|
},
|
|
{
|
|
// This covers the unset/unknown value for all measurements in the
|
|
// SEN6x family, but notably it uses the int16 CO2 encoding also used
|
|
// by the SEN63C.
|
|
name: "SEN69C all values unknown",
|
|
model: SEN69C,
|
|
tx: []byte{0x04, 0xb5},
|
|
rx: []byte{
|
|
0xff, 0xff, 0xac, // PM1.0
|
|
0xff, 0xff, 0xac, // PM2.5
|
|
0xff, 0xff, 0xac, // PM4.0
|
|
0xff, 0xff, 0xac, // PM10.0
|
|
0x7f, 0xff, 0x8f, // RH
|
|
0x7f, 0xff, 0x8f, // Temp
|
|
0x7f, 0xff, 0x8f, // VOC
|
|
0x7f, 0xff, 0x8f, // NOx
|
|
0xff, 0xff, 0xac, // HCHO
|
|
0x7f, 0xff, 0x8f, // CO2 as int
|
|
},
|
|
want: &SensorValues{},
|
|
},
|
|
{
|
|
name: "bad crc",
|
|
model: SEN62,
|
|
tx: []byte{0x04, 0xA3},
|
|
rx: []byte{
|
|
0x00, 0x04, 0x45, // PM1.0
|
|
0x00, 0x05, 0x74, // PM2.5
|
|
0x00, 0x06, 0x27, // PM4.0
|
|
0x00, 0x06, 0xff, // PM10.0 with incorrect CRC (should be 0x27)
|
|
0x14, 0xf0, 0xee, // RH
|
|
0x11, 0x6b, 0x4a, // Temp
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown model",
|
|
model: Model(-1),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
runWriteAndReadTests(t, cases, (*Dev).ReadMeasuredValues)
|
|
}
|
|
|
|
func TestDevReadMeasuredRawValues(t *testing.T) {
|
|
cases := []writeAndReadTestCase[*RawSensorValues]{
|
|
{
|
|
name: "SEN62",
|
|
model: SEN62,
|
|
tx: []byte{0x04, 0x92},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN63C",
|
|
model: SEN63C,
|
|
tx: []byte{0x04, 0x92},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN65",
|
|
model: SEN65,
|
|
tx: []byte{0x04, 0x55},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
0x72, 0xc9, 0xac, // VOC
|
|
0x49, 0x45, 0x03, // NOx
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
VOC: ptr(uint16(29385)),
|
|
NOx: ptr(uint16(18757)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN66",
|
|
model: SEN66,
|
|
tx: []byte{0x04, 0x05},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
0x72, 0xc9, 0xac, // VOC
|
|
0x49, 0x45, 0x03, // NOx
|
|
0x01, 0xc8, 0x8b, // CO2
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
VOC: ptr(uint16(29385)),
|
|
NOx: ptr(uint16(18757)),
|
|
CO2: ptr(uint16(456)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN68",
|
|
model: SEN68,
|
|
tx: []byte{0x04, 0x55},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
0x72, 0xc9, 0xac, // VOC
|
|
0x49, 0x45, 0x03, // NOx
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
VOC: ptr(uint16(29385)),
|
|
NOx: ptr(uint16(18757)),
|
|
},
|
|
},
|
|
{
|
|
name: "SEN69C",
|
|
model: SEN69C,
|
|
tx: []byte{0x04, 0x55},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
0x72, 0xc9, 0xac, // VOC
|
|
0x49, 0x45, 0x03, // NOx
|
|
},
|
|
want: &RawSensorValues{
|
|
RH: ptr(float32(51.34)),
|
|
Temp: ptr(float32(22.93)),
|
|
VOC: ptr(uint16(29385)),
|
|
NOx: ptr(uint16(18757)),
|
|
},
|
|
},
|
|
{
|
|
// SEN66 covers all raw measurements.
|
|
name: "SEN66 all values unknown",
|
|
model: SEN66,
|
|
tx: []byte{0x04, 0x05},
|
|
rx: []byte{
|
|
0x7f, 0xff, 0x8f, // RH
|
|
0x7f, 0xff, 0x8f, // Temp
|
|
0xff, 0xff, 0xac, // VOC
|
|
0xff, 0xff, 0xac, // NOx
|
|
0xff, 0xff, 0xac, // CO2
|
|
},
|
|
want: &RawSensorValues{},
|
|
},
|
|
{
|
|
name: "SEN66",
|
|
model: SEN66,
|
|
tx: []byte{0x04, 0x05},
|
|
rx: []byte{
|
|
0x14, 0x0e, 0x73, // RH
|
|
0x11, 0xea, 0x01, // Temp
|
|
0x72, 0xc9, 0xac, // VOC
|
|
0x49, 0x45, 0xff, // NOx with incorrect CRC (should be 0x03)
|
|
0x01, 0xc8, 0x8b, // CO2
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown model",
|
|
model: Model(-1),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
runWriteAndReadTests(t, cases, (*Dev).ReadMeasuredRawValues)
|
|
}
|
|
|
|
func TestDevGetProductName(t *testing.T) {
|
|
cmd := []byte{0xd0, 0x14}
|
|
|
|
cases := []writeAndReadTestCase[string]{
|
|
{
|
|
name: "SEN66",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{
|
|
0x53, 0x45, 0x83, // "SE"
|
|
0x4e, 0x36, 0x06, // "N6"
|
|
0x36, 0x00, 0x69, // "6\0"
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
},
|
|
want: "SEN66",
|
|
},
|
|
{
|
|
name: "bad CRC",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{
|
|
0x53, 0x45, 0x83, // "SE"
|
|
0x4e, 0x36, 0xff, // "N6" with incorrect CRC (should be 0x06)
|
|
0x36, 0x00, 0x69, // "6\0"
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
runWriteAndReadTests(t, cases, (*Dev).GetProductName)
|
|
}
|
|
|
|
func TestDevGetSerialNumber(t *testing.T) {
|
|
cmd := []byte{0xd0, 0x33}
|
|
|
|
cases := []writeAndReadTestCase[string]{
|
|
{
|
|
name: "serial number from device",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{
|
|
0x32, 0x34, 0xeb, // "24"
|
|
0x43, 0x34, 0x24, // "C4"
|
|
0x45, 0x45, 0xb7, // "EE"
|
|
0x31, 0x37, 0x95, // "17"
|
|
0x46, 0x41, 0x5e, // "FA"
|
|
0x43, 0x37, 0x77, // "C7"
|
|
0x43, 0x35, 0x15, // "C5"
|
|
0x43, 0x42, 0x7a, // "CB"
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
},
|
|
want: "24C4EE17FAC7C5CB",
|
|
},
|
|
{
|
|
name: "bad CRC",
|
|
model: SEN66,
|
|
tx: cmd,
|
|
rx: []byte{
|
|
0x32, 0x34, 0xeb, // "24"
|
|
0x43, 0x34, 0x24, // "C4"
|
|
0x45, 0x45, 0xb7, // "EE"
|
|
0x31, 0x37, 0xff, // "17" with incorrect CRC (should be 0x95)
|
|
0x46, 0x41, 0x5e, // "FA"
|
|
0x43, 0x37, 0x77, // "C7"
|
|
0x43, 0x35, 0x15, // "C5"
|
|
0x43, 0x42, 0x7a, // "CB"
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
0x00, 0x00, 0x81,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
runWriteAndReadTests(t, cases, (*Dev).GetSerialNumber)
|
|
}
|
|
|
|
func TestDevGetVersion(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
response []byte
|
|
wantMajor uint8
|
|
wantMinor uint8
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "version 4.0",
|
|
response: []byte{0x04, 0x00, 0x2},
|
|
wantMajor: 4,
|
|
wantMinor: 0,
|
|
},
|
|
{
|
|
name: "version 1.2",
|
|
response: []byte{0x01, 0x02, crc8(0x01, 0x02)},
|
|
wantMajor: 1,
|
|
wantMinor: 2,
|
|
},
|
|
{
|
|
name: "bad CRC",
|
|
response: []byte{0x04, 0x00, 0xff},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
bus := i2ctest.Playback{
|
|
Ops: []i2ctest.IO{
|
|
{Addr: i2cAddr, W: []byte{0xd1, 0x00}},
|
|
{Addr: i2cAddr, R: tc.response},
|
|
},
|
|
}
|
|
defer func() {
|
|
if err := bus.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
d := newTestDev(t, &bus, SEN66)
|
|
|
|
gotMajor, gotMinor, err := d.GetVersion()
|
|
|
|
if tc.wantErr && err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
|
|
if !tc.wantErr && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !tc.wantErr {
|
|
if gotMajor != tc.wantMajor {
|
|
t.Errorf("got major version %d, want %d", gotMajor, tc.wantMajor)
|
|
}
|
|
|
|
if gotMinor != tc.wantMinor {
|
|
t.Errorf("got minor version %d, want %d", gotMinor, tc.wantMinor)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDevDeviceReset(t *testing.T) {
|
|
cases := []writeTestCase{
|
|
{
|
|
name: "success",
|
|
model: SEN66,
|
|
tx: []byte{0xd3, 0x04},
|
|
},
|
|
}
|
|
|
|
runWriteTests(t, cases, (*Dev).DeviceReset)
|
|
}
|
|
|
|
func TestDevStartFanCleaning(t *testing.T) {
|
|
cases := []writeTestCase{
|
|
{
|
|
name: "success",
|
|
model: SEN66,
|
|
tx: []byte{0x56, 0x07},
|
|
},
|
|
}
|
|
|
|
runWriteTests(t, cases, (*Dev).StartFanCleaning)
|
|
}
|