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.
devices/bme68x/bme680_test.go

335 lines
12 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 bme68x
import (
"errors"
"testing"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/i2c/i2ctest"
"periph.io/x/conn/v3/physic"
)
// TestDev_validateDeviceID tests the validateDeviceID method of the Dev struct.
// It ensures that the device correctly validates both the chip ID and variant ID
// over I2C and returns the expected errors for invalid cases.
func TestDev_validateDeviceID(t *testing.T) {
for _, test := range []struct {
name string
ops []i2ctest.IO
expectErr error
}{
{
name: "success",
ops: []i2ctest.IO{
{Addr: I2CAddr, W: []byte{byte(regID)}, R: []byte{ChipDeviceID}},
{Addr: I2CAddr, W: []byte{byte(regVariantID)}, R: []byte{0x0}},
},
expectErr: nil,
},
{
name: "chipIdFailure",
ops: []i2ctest.IO{{Addr: I2CAddr, W: []byte{byte(regID)}, R: []byte{0x62}}},
expectErr: ErrInvalidChipId,
},
{
name: "variantIdFailure",
ops: []i2ctest.IO{
{Addr: I2CAddr, W: []byte{byte(regID)}, R: []byte{ChipDeviceID}},
{Addr: I2CAddr, W: []byte{byte(regVariantID)}, R: []byte{0x4}},
},
expectErr: ErrInvalidVariantId,
},
} {
t.Run(test.name, func(t *testing.T) {
b := i2ctest.Playback{
Ops: test.ops,
DontPanic: true,
}
dev := Device{}
dev.d = i2c.Dev{Bus: &b, Addr: I2CAddr}
err := dev.validateDeviceID()
if !errors.Is(err, test.expectErr) {
t.Fatalf("Expected error: %v, got: %v", test.expectErr, err)
}
if err := b.Close(); err != nil {
t.Fatal(err)
}
})
}
}
// TestNewI2cAddrErr tests that creating a new Dev with an invalid I2C address
// returns the expected ErrI2cAddress error.
func TestNewI2cAddrErr(t *testing.T) {
var invalidI2cAddr uint16 = 0x60
b := i2ctest.Playback{
Ops: []i2ctest.IO{},
DontPanic: true,
}
_, err := NewI2C(&b, invalidI2cAddr)
if !errors.Is(err, ErrI2cAddress) {
t.Fatalf("Expected error: %v, got: %v", ErrI2cAddress, err)
}
if err := b.Close(); err != nil {
t.Fatal(err)
}
}
// TestDev_IsNewMeasurementReady tests the IsNewMeasurementReady method of Dev.
// It verifies that the method correctly interprets the sensor status register.
func TestDev_IsNewMeasurementReady(t *testing.T) {
for _, test := range []struct {
name string
ops []i2ctest.IO
want bool
expectErr error
}{
{
name: "measurementReady",
ops: []i2ctest.IO{{Addr: I2CAddr, W: []byte{byte(regEASStatus0)}, R: []byte{0x80}}},
expectErr: nil,
want: true,
},
{
name: "measurementNotReady",
ops: []i2ctest.IO{{Addr: I2CAddr, W: []byte{byte(regEASStatus0)}, R: []byte{0x70}}},
expectErr: nil,
want: false,
},
} {
t.Run(test.name, func(t *testing.T) {
b := i2ctest.Playback{
Ops: test.ops,
DontPanic: true,
}
dev := Device{d: i2c.Dev{Bus: &b, Addr: I2CAddr}, variant: VariantNameBME680}
dev.ops = dev.newBME680()
status, err := dev.IsNewMeasurementReady()
if !errors.Is(err, test.expectErr) {
t.Fatalf("Expected error: %v, got: %v", test.expectErr, err)
}
if status != test.want {
t.Fatalf("Expected status: %v, got: %v", test.want, status)
}
if err := b.Close(); err != nil {
t.Fatal(err)
}
})
}
}
// TestDev_Sense tests the Sense() method of the BME680 driver.
// It simulates I2C transactions and verifies that temperature, pressure,
// humidity, and gas resistance readings are correctly returned and compensated.
func TestDev_Sense(t *testing.T) {
for _, test := range []struct {
name string
ops []i2ctest.IO
testCfg SensorConfig
testGasIndex int8
want physic.Env
gasRes GasResistance
gasValid bool
expectErr error
}{
{
name: "success",
ops: []i2ctest.IO{
{Addr: I2CAddr, W: []byte{byte(regCtrlMeas), 0x55}, R: []byte{}},
{Addr: I2CAddr, W: []byte{byte(regPressMSB)}, R: []byte{71, 58, 176, 118, 196, 16, 96, 8}},
{Addr: I2CAddr, W: []byte{byte(regGasRMsb)}, R: []byte{98, 186}},
},
testCfg: SensorConfig{TempOversampling: OS2x, PressureOversampling: OS16x, HumidityOversampling: OS1x,
IIRFilter: NoFilter, GasEnabled: true, OperatingMode: ForcedMode, GasProfiles: defaultGasProfiles(),
},
testGasIndex: 0,
expectErr: nil,
want: physic.Env{Temperature: 2260*10*physic.MilliCelsius + physic.ZeroCelsius, Pressure: 101860.0 * physic.Pascal, Humidity: 67 * physic.PercentRH},
gasRes: 8514,
gasValid: true,
},
{
name: "gas sensor disabled",
testCfg: SensorConfig{
GasEnabled: false,
OperatingMode: ForcedMode,
GasProfiles: defaultGasProfiles(),
},
testGasIndex: 0,
expectErr: ErrNoGasProfileSelected,
want: physic.Env{},
gasRes: 0,
gasValid: false,
},
{
name: "no active gas profile",
testCfg: SensorConfig{GasEnabled: true, OperatingMode: ForcedMode},
testGasIndex: -1,
expectErr: ErrNoGasProfileSelected,
},
} {
t.Run(test.name, func(t *testing.T) {
b := i2ctest.Playback{
Ops: test.ops,
DontPanic: true,
}
dev := Device{d: i2c.Dev{Bus: &b, Addr: I2CAddr},
c: mockCalibration(),
cfg: test.testCfg,
activeGasProfileIndex: test.testGasIndex,
variant: VariantNameBME680,
}
// Assign sensorOps implementation
dev.ops = dev.newBME680()
eExp, gExp, gValid, err := dev.Sense()
if !errors.Is(err, test.expectErr) {
t.Fatalf("Expected error: %v, got: %v", test.expectErr, err)
}
// Compare Env safely with delta tolerance for Humidity
assertEnvEqual(t, eExp, test.want, 0.1, 1.0, 0.01)
// Compare gas sensor
if gExp != test.gasRes {
t.Fatalf("Gas resistance mismatch: got %v, want %v", gExp, test.gasRes)
}
if gValid != test.gasValid {
t.Fatalf("Gas valid mismatch: got %v, want %v", gValid, test.gasValid)
}
})
}
}
// TestDev_InitCalibration tests the initialization of sensor calibration data.
// It uses i2c test Playback to simulate I2C register reads and verifies
// that the Dev.InitCalibration method correctly populates the calibration struct.
func TestDev_InitCalibration(t *testing.T) {
for _, test := range []struct {
name string
ops []i2ctest.IO
want SensorCalibration
expectErr error
}{
{
name: "success",
ops: []i2ctest.IO{
{Addr: I2CAddr, W: []byte{byte(regParT1)}, R: []byte{254, 100}},
{Addr: I2CAddr, W: []byte{byte(regParT2)}, R: []byte{181, 101, 3, 240, 209, 142, 117, 215, 88, 0, 159, 38, 8, 255, 38, 30, 0, 0, 106, 247, 36, 245, 30}},
{Addr: I2CAddr, W: []byte{byte(regParH2)}, R: []byte{63, 91, 47, 0, 45, 20, 120, 156}},
{Addr: I2CAddr, W: []byte{byte(regParG2)}, R: []byte{81, 211, 186, 18}},
{Addr: I2CAddr, W: []byte{byte(regResHeatVal)}, R: []byte{54, 170, 22, 73, 19}},
},
expectErr: nil,
want: mockCalibration(),
},
} {
t.Run(test.name, func(t *testing.T) {
b := i2ctest.Playback{
Ops: test.ops,
DontPanic: true,
}
dev := Device{d: i2c.Dev{Bus: &b, Addr: I2CAddr}, variant: VariantNameBME680}
err := dev.InitCalibration()
if !errors.Is(err, test.expectErr) {
t.Fatalf("Expected error: %v, got: %v", test.expectErr, err)
}
if dev.c != test.want {
t.Fatalf("Expected calibration: %v, got: %v", test.want, dev.c)
}
if err := b.Close(); err != nil {
t.Fatal(err)
}
})
}
}
// TestCompensations verifies that the sensor compensation functions produce correct results
// given known raw ADC readings and a mocked calibration. This ensures the Dev methods
// for temperature, pressure, humidity, and gas sensor calculations are accurate.
func TestCompensations(t *testing.T) {
d := Device{c: mockCalibration(), variant: VariantNameBME680}
// Raw Sensor ADC Readings
var tRaw uint32 = 480355
var pRaw uint32 = 291843
var hRaw uint32 = 24615
var gRaw uint32 = 386
var gasResRange gasRangeR = 10
// Expected compensated results (from datasheet formulas / reference implementation)
var tExp int32 = 2070 // Temperature in hundredths of °C (20.70°C)
var pExp int32 = 101513 // Pressure in Pa
var hExp int32 = 67561 // Humidity in thousandths of %RH (67.561%)
var gExp int32 = 8566 // Gas resistance in Ohms
tComp, pComp, hComp, gComp := expectedCompensated(&d, tRaw, pRaw, hRaw, gRaw, gasResRange)
if tComp != tExp { // °C
t.Fatalf("temp compensation does not match expected value : %v, got: %v", tExp, tComp)
}
if pComp != pExp { //Pa
t.Fatalf("pressure compensation does not match expected = %v, got: %v", pExp, pComp)
}
if hComp != hExp {
t.Fatalf("Humidity compensation does not match expected = %v, got: %v", hExp, hComp)
}
if gComp != gExp { //Ohm
t.Fatalf("Gas compensation does not match expected = %v, got: %v", gExp, gComp)
}
}
// mockCalibration returns a SensorCalibration struct populated with fixed calibration constants.
// These values simulate a real sensor's calibration data and are used in unit tests to
// produce deterministic compensation outputs.
func mockCalibration() SensorCalibration {
return SensorCalibration{
t1: 25854, t2: 26037, t3: 3,
p1: 36561, p2: -10379, p3: 88, p4: 9887,
p5: -248, p6: 30, p7: 38, p8: -2198, p9: -2780, p10: 30,
h1: 763, h2: 1013, h3: 0, h4: 45, h5: 20, h6: 120, h7: -100,
g1: -70, g2: -11439, g3: 18,
resHeatVal: 54, resHeatRange: 2, switchingErr: 19,
tFine: 0, tempComp: 0, pressureComp: 0, humidityComp: 0,
}
}
// expectedCompensated computes the fully compensated sensor readings for temperature, pressure,
// humidity, and gas resistance based on raw ADC values. This helper is used in tests to
// compare actual sensor compensation outputs against expected results.
func expectedCompensated(dev *Device, tRaw, pRaw, hRaw, gRaw uint32, gasRange gasRangeR) (int32, int32, int32, int32) {
return dev.compensatedTemperature(tRaw),
dev.compensatedPressure(pRaw),
dev.compensatedHumidity(hRaw),
dev.compensatedGasSensor(gRaw, gasRange)
}
// assertEnvEqual compares two physic.Env values (got vs want) with specified tolerances.
// deltaTemp, deltaPress, and deltaRH are the allowed differences for temperature, pressure, and humidity respectively.
// This is useful because floating-point conversions or sensor rounding can produce small variations.
func assertEnvEqual(t *testing.T, got, want physic.Env, deltaTemp, deltaPress, deltaRH float64) {
t.Helper()
if diff := float64(got.Temperature - want.Temperature); diff < -deltaTemp || diff > deltaTemp {
t.Fatalf("Temperature mismatch: got %v, want %v", got.Temperature, want.Temperature)
}
if diff := float64(got.Pressure - want.Pressure); diff < -deltaPress || diff > deltaPress {
t.Fatalf("Pressure mismatch: got %v, want %v", got.Pressure, want.Pressure)
}
if diff := float64(got.Humidity - want.Humidity); diff < -deltaRH || diff > deltaRH {
t.Fatalf("Humidity mismatch: got %v, want %v", got.Humidity, want.Humidity)
}
}
// defaultGasProfiles returns a complete default set of GasProfile entries.
// This ensures that tests have a consistent, fully populated array and prevents
// accidental gaps if additional profiles are added later.
func defaultGasProfiles() [10]GasProfile {
var profiles [10]GasProfile
for i := range profiles {
// Default values (can be zero or some safe value)
profiles[i] = GasProfile{TargetTempC: 0, HeatingDurationMs: 0}
}
// Set specific profiles used in tests
profiles[0] = GasProfile{TargetTempC: 300, HeatingDurationMs: 250}
profiles[7] = GasProfile{TargetTempC: 150, HeatingDurationMs: 100}
return profiles
}