mirror of https://github.com/periph/devices
Add i2c support for BME680 sensor driver
parent
226f28bde0
commit
f0fd3799b8
@ -0,0 +1,334 @@
|
||||
// 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
|
||||
}
|
||||
@ -0,0 +1,329 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"periph.io/x/conn/v3/i2c"
|
||||
"periph.io/x/conn/v3/physic"
|
||||
)
|
||||
|
||||
// I2CAddr Default I²C address for BME680.
|
||||
const I2CAddr uint16 = 0x77
|
||||
|
||||
// Chip variants
|
||||
const (
|
||||
VariantBME680 uint8 = iota
|
||||
VariantBME688
|
||||
)
|
||||
|
||||
// SensorVariant Variant names
|
||||
type SensorVariant string
|
||||
|
||||
const (
|
||||
VariantNameBME680 SensorVariant = "BME680"
|
||||
VariantNameBME688 SensorVariant = "BME688"
|
||||
)
|
||||
|
||||
// OperatingMode represents the BME680 sensor's power and measurement mode.
|
||||
const (
|
||||
SleepMode uint8 = iota // SleepMode puts the sensor in low-power standby.
|
||||
ForcedMode // ForcedMode triggers a single measurement and then returns to sleep.
|
||||
)
|
||||
|
||||
// Oversampling bit positions.
|
||||
const (
|
||||
tempOSBit uint8 = 0x5
|
||||
pressOSBit uint8 = 0x2
|
||||
humOSBit uint8 = 0x0
|
||||
)
|
||||
|
||||
// Oversampling options for sensor measurements.
|
||||
const (
|
||||
OSSkipped uint8 = iota
|
||||
OS1x
|
||||
OS2x
|
||||
OS4x
|
||||
OS8x
|
||||
OS16x
|
||||
)
|
||||
|
||||
// iirFilterBit Bit position.
|
||||
const iirFilterBit = 0x2
|
||||
|
||||
// IIR filter coefficients for smoothing sensor data.
|
||||
const (
|
||||
NoFilter uint8 = iota
|
||||
C1Filter //nolint:unused
|
||||
C3Filter
|
||||
C7Filter
|
||||
C15Filter
|
||||
C31Filter
|
||||
C63Filter
|
||||
C127Filter
|
||||
)
|
||||
|
||||
// GasResistance is returned in Ohms.
|
||||
type GasResistance uint32
|
||||
|
||||
// GasProfile defines one BME680 gas sensor profile (0-9).
|
||||
type GasProfile struct {
|
||||
TargetTempC uint32 // Heater target temperature in °C
|
||||
HeatingDurationMs uint16 // Heating duration in milliseconds
|
||||
}
|
||||
|
||||
// SensorConfig holds all configurable parameters for the BME680 sensor,
|
||||
// including oversampling, filter, gas sensor profiles, and operating mode.
|
||||
type SensorConfig struct {
|
||||
TempOversampling uint8 // Temperature oversampling setting
|
||||
PressureOversampling uint8 // Pressure oversampling setting
|
||||
HumidityOversampling uint8 // Humidity oversampling setting
|
||||
IIRFilter uint8 // IIR filter coefficient
|
||||
GasEnabled bool // Enable gas measurements
|
||||
GasProfiles [10]GasProfile // Array of Gas sensor profiles
|
||||
AmbientTempC float32 // Ambient temperature for heater calculations
|
||||
OperatingMode uint8 // Sensor operating mode (Sleep/Forced), Default:Sleep
|
||||
}
|
||||
|
||||
var (
|
||||
ErrI2cAddress = errors.New("i2c: provided address is not supported by the device")
|
||||
ErrInvalidChipId = errors.New("bme68x: invalid chip ID")
|
||||
ErrInvalidVariantId = errors.New("bme68x: invalid variant ID")
|
||||
ErrNoGasProfileSelected = errors.New("bme68x: no gas profile selected, but gas measurements are enabled")
|
||||
ErrRunSetupSensor = errors.New("bme68x: gas measurement disabled; run SetupSensor()")
|
||||
ErrNilSensorConfig = errors.New("bme680: nil SensorConfig")
|
||||
)
|
||||
|
||||
// Device represents a handle to a BME680 sensor.
|
||||
type Device struct {
|
||||
d i2c.Dev // I²C device handle
|
||||
variant SensorVariant // Sensor variant identifier
|
||||
mutex sync.Mutex // Mutex for concurrent access
|
||||
cfg SensorConfig // User-provided configuration
|
||||
c SensorCalibration // Calibration data
|
||||
activeGasProfileIndex int8 // Currently active gas profile index; -1 if none selected
|
||||
ops sensorOps // Interface for low-level chip operations (read/write registers, measure)
|
||||
ambientTempC float32 // Ambient temperature used for gas sensor compensation
|
||||
}
|
||||
|
||||
// sensorOps defines low-level operations implemented by a specific BME68x chip variant.
|
||||
type sensorOps interface {
|
||||
prepareGasConfig() []byte // prepares the gas heater configuration buffer
|
||||
sense() (physic.Env, GasResistance, bool, error) // triggers a single measurement and returns TPH optionally gas resistance with validity flag
|
||||
status() (sensorStatus, error) // reads the current sensor status
|
||||
setGasProfile(profile uint8) error // activates a specific gas profile (0-9) without triggering measurement
|
||||
}
|
||||
|
||||
// NewI2C initializes a BME68x sensor over I²C.
|
||||
func NewI2C(b i2c.Bus, addr uint16) (*Device, error) {
|
||||
// Validate I2C Address
|
||||
if addr != I2CAddr {
|
||||
return nil, ErrI2cAddress
|
||||
}
|
||||
device := Device{d: i2c.Dev{Bus: b, Addr: addr}}
|
||||
// Validate Device and Variant ID
|
||||
if err := device.validateDeviceID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Calibration Initialization - Common for both variant
|
||||
if err := device.InitCalibration(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &device, nil
|
||||
}
|
||||
|
||||
// GetSensorVariant return the type of BME68X sensor connected
|
||||
func (dev *Device) GetSensorVariant() SensorVariant {
|
||||
return dev.variant
|
||||
}
|
||||
|
||||
// SetupSensor configures oversampling, filter, and gas heater profiles.
|
||||
// Does NOT trigger measurements; call Sense() for actual data. Copies user-provided SensorConfig and validates values.
|
||||
// Builds a write buffer for control registers and optionally gas heater registers.
|
||||
func (dev *Device) SetupSensor(config *SensorConfig) error {
|
||||
if config == nil {
|
||||
return ErrNilSensorConfig
|
||||
}
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
dev.cfg = *config
|
||||
dev.activeGasProfileIndex = -1 // default: no active profile
|
||||
dev.ambientTempC = dev.cfg.AmbientTempC
|
||||
if err := dev.validateOversampling(); err != nil {
|
||||
return err
|
||||
}
|
||||
writeBuf := []byte{
|
||||
byte(regCtrlHum), dev.cfg.HumidityOversampling << humOSBit,
|
||||
byte(regCtrlMeas), dev.cfg.PressureOversampling<<pressOSBit | dev.cfg.TempOversampling<<tempOSBit,
|
||||
byte(regConfig), dev.cfg.IIRFilter << iirFilterBit,
|
||||
}
|
||||
if dev.cfg.GasEnabled {
|
||||
gasBuf := dev.ops.prepareGasConfig()
|
||||
if len(gasBuf) > 0 {
|
||||
writeBuf = append(writeBuf, gasBuf...)
|
||||
}
|
||||
}
|
||||
return dev.regWrite(writeBuf)
|
||||
}
|
||||
|
||||
// SensorSoftReset performs a software reset of the BME680. (It restores the device to its default state without power cycling)
|
||||
// Writes the reset command to regReset and waits 20ms for the sensor to reboot.
|
||||
func (dev *Device) SensorSoftReset() error {
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
writeBuf := []byte{byte(regReset), byte(DeviceSoftReset)}
|
||||
if err := dev.regWrite(writeBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNewMeasurementReady returns true if a new TPHG measurement is available.
|
||||
// Returns false if reading the status fails.
|
||||
func (dev *Device) IsNewMeasurementReady() (bool, error) {
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
status, err := dev.ops.status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return status.MeasurementReady, nil
|
||||
}
|
||||
|
||||
// ActiveGasProfile returns the currently active gas profile index (0-9).
|
||||
// Returns -1 if no gas profile is active or reading the status fails.
|
||||
func (dev *Device) ActiveGasProfile() (int8, error) {
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
status, err := dev.ops.status()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int8(status.GasProfileInProgress), nil
|
||||
}
|
||||
|
||||
// Sense triggers a single forced-mode measurement and returns the compensated data.
|
||||
// Returns temperature, pressure, humidity, and optionally gas resistance (with validity flag).
|
||||
// If gas measurements are enabled, a gas profile and GasEnabled must be selected beforehand.
|
||||
// Blocks until the measurement is ready.
|
||||
func (dev *Device) Sense() (physic.Env, GasResistance, bool, error) {
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
return dev.ops.sense()
|
||||
}
|
||||
|
||||
// SetGasProfile sets the active gas profile (0-9) on the BME680 sensor.
|
||||
// Does not trigger measurement; call Sense() afterwards
|
||||
// Updates the sensor hardware and the internal activeGasProfileIndex.
|
||||
func (dev *Device) SetGasProfile(profile uint8) error {
|
||||
// Validate profile index
|
||||
if profile > 9 {
|
||||
return fmt.Errorf("bme680: gas profile index must be 0-9, got %d", profile)
|
||||
}
|
||||
// Ensure gas measurements are enabled
|
||||
if !dev.cfg.GasEnabled {
|
||||
return ErrRunSetupSensor
|
||||
}
|
||||
dev.mutex.Lock()
|
||||
defer dev.mutex.Unlock()
|
||||
return dev.ops.setGasProfile(profile)
|
||||
}
|
||||
|
||||
// validateDeviceID verifies the BME68x chip ID and variant ID.
|
||||
// It reads the regID and regVariantID registers and sets the device variant.
|
||||
// Returns an error if the device is not a BME680 or BME688 (unsupported).
|
||||
func (dev *Device) validateDeviceID() error {
|
||||
var id, vid []byte
|
||||
var err error
|
||||
if id, err = dev.regRead(regID, 0x1); err != nil {
|
||||
return err
|
||||
}
|
||||
if id[0] != ChipDeviceID {
|
||||
return fmt.Errorf("bme68x: invalid chip ID (expected=0x%x, got=0x%x): %w", ChipDeviceID, id[0], ErrInvalidChipId)
|
||||
}
|
||||
if vid, err = dev.regRead(regVariantID, 0x1); err != nil {
|
||||
return err
|
||||
}
|
||||
switch vid[0] {
|
||||
case VariantBME680:
|
||||
dev.variant = VariantNameBME680
|
||||
dev.ops = dev.newBME680()
|
||||
case VariantBME688:
|
||||
dev.variant = VariantNameBME688
|
||||
return fmt.Errorf("bme68x: BME688 support not implemented yet")
|
||||
default:
|
||||
return fmt.Errorf("bme68x: invalid variant ID (expected=0 or 1, got=0x%x): %w", vid[0], ErrInvalidVariantId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// triggerForcedMeasurement starts a forced-mode measurement on the BME680,
|
||||
// waits for it to complete based on sensor configuration (TPH oversampling, gas heater, etc.),
|
||||
// and ensures the data is ready to read.
|
||||
func (dev *Device) triggerForcedMeasurement() error {
|
||||
// Ensure device is in forced mode
|
||||
if dev.cfg.OperatingMode != ForcedMode {
|
||||
dev.cfg.OperatingMode = ForcedMode
|
||||
}
|
||||
// Set forced mode to start measurement
|
||||
if err := dev.regWrite([]byte{byte(regCtrlMeas),
|
||||
dev.cfg.PressureOversampling<<pressOSBit | dev.cfg.TempOversampling<<tempOSBit | ForcedMode}); err != nil {
|
||||
return err
|
||||
}
|
||||
// Wait for measurements to complete
|
||||
// time.Sleep(d.forcedModeWaitTime())
|
||||
return nil
|
||||
}
|
||||
|
||||
// forcedModeWaitTime computes the time required for a complete forced-mode measurement.
|
||||
// Takes into account:
|
||||
// - Oversampling for Temperature, Pressure, Humidity
|
||||
// - TPH switching overhead
|
||||
// - Gas measurement overhead
|
||||
// - Gas heater duration
|
||||
// - Wake-up time
|
||||
// - Gas heating duration (if enabled)
|
||||
//
|
||||
// Returns the total wait time as time.Duration
|
||||
func (dev *Device) forcedModeWaitTime() time.Duration {
|
||||
cycles := dev.osCycles(dev.cfg.TempOversampling) +
|
||||
dev.osCycles(dev.cfg.PressureOversampling) + dev.osCycles(dev.cfg.HumidityOversampling)
|
||||
// Reference : Bosch - BME68x_SensorAPI
|
||||
duration := time.Duration(cycles*1963)*time.Microsecond + // TPH switching overhead
|
||||
time.Duration(477*(4+5))*time.Microsecond // TPH switch + gas overhead
|
||||
// Add Gas Heating Duration if applicable
|
||||
if dev.cfg.GasEnabled && dev.activeGasProfileIndex >= 0 {
|
||||
duration += time.Duration(dev.cfg.GasProfiles[dev.activeGasProfileIndex].HeatingDurationMs) * time.Millisecond
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
// validateOversampling validates the oversampling threshold
|
||||
func (dev *Device) validateOversampling() error {
|
||||
if dev.cfg.TempOversampling > OS16x {
|
||||
return fmt.Errorf("invalid temperature oversampling value: %d", dev.cfg.TempOversampling)
|
||||
}
|
||||
if dev.cfg.PressureOversampling > OS16x {
|
||||
return fmt.Errorf("invalid pressure oversampling value: %d", dev.cfg.PressureOversampling)
|
||||
}
|
||||
if dev.cfg.HumidityOversampling > OS16x {
|
||||
return fmt.Errorf("invalid humidity oversampling value: %d", dev.cfg.HumidityOversampling)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// osCycles helper to return the measurement cycles as per oversampling
|
||||
func (dev *Device) osCycles(os uint8) int {
|
||||
osToMeasureCycles := [...]int{0, 1, 2, 4, 8, 16}
|
||||
if int(os) >= len(osToMeasureCycles) {
|
||||
return 0 // safe fallback
|
||||
}
|
||||
return osToMeasureCycles[os]
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
// 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 controls a Bosch BME680 device over I²C
|
||||
//
|
||||
// # More details
|
||||
//
|
||||
// See https://periph.io/device/bme68x/ for more details about the device.
|
||||
//
|
||||
// # Datasheet
|
||||
// BME680: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme680-ds001.pdf
|
||||
|
||||
package bme68x
|
||||
@ -0,0 +1,79 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"periph.io/x/conn/v3/i2c/i2creg"
|
||||
bme680 "periph.io/x/devices/v3/bme68x"
|
||||
"periph.io/x/host/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
i2cBus = "/dev/i2c-1"
|
||||
i2cAddr = 0x77
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := host.Init(); err != nil {
|
||||
fmt.Println("Error: Failed to Host init()")
|
||||
return
|
||||
}
|
||||
|
||||
b, err := i2creg.Open(i2cBus)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to open I2C bus: %v", err)
|
||||
return
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Get the Device handler
|
||||
d, err := bme680.NewI2C(b, i2cAddr)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: failed to initialize BME680 sensor: %v\n", err)
|
||||
}
|
||||
|
||||
// user configuration
|
||||
userCfg := &bme680.SensorConfig{
|
||||
TempOversampling: bme680.OS2x, PressureOversampling: bme680.OS16x,
|
||||
HumidityOversampling: bme680.OS1x, IIRFilter: bme680.NoFilter,
|
||||
GasProfiles: [10]bme680.GasProfile{
|
||||
0: {TargetTempC: 300, HeatingDurationMs: 250},
|
||||
7: {TargetTempC: 150, HeatingDurationMs: 100},
|
||||
},
|
||||
GasEnabled: true,
|
||||
OperatingMode: bme680.ForcedMode,
|
||||
}
|
||||
|
||||
if err := d.SetupSensor(userCfg); err != nil {
|
||||
fmt.Printf("Error: Failed Setup Sensor %v\n", err)
|
||||
}
|
||||
if err := d.SetGasProfile(0); err != nil {
|
||||
fmt.Printf("Error: Failed to select gas profile %v\n", err)
|
||||
}
|
||||
|
||||
// Create a ticker to trigger measurements every 15 seconds
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Infinite loop to continuously read measurements at the specified interval
|
||||
for range ticker.C {
|
||||
env, gasResistance, valid, err := d.Sense()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read sensor: %v", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("[%s] Temp: %.3f C, Humidity: %5s, Pressure: %9s, Gas: %s\n",
|
||||
time.Now().Format("15:04:05"), env.Temperature.Celsius(), env.Humidity, env.Pressure,
|
||||
func() string {
|
||||
if valid {
|
||||
return fmt.Sprintf("%d Ohm", gasResistance)
|
||||
}
|
||||
return "INVALID"
|
||||
}())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
// 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
|
||||
|
||||
const (
|
||||
DeviceSoftReset = 0x38
|
||||
ChipDeviceID = 0x61
|
||||
)
|
||||
|
||||
type reg uint8
|
||||
|
||||
// BME680 Register Addresses
|
||||
const (
|
||||
// Status, ID, reset & Ctl
|
||||
regStatus reg = 0x73 //unused
|
||||
regReset reg = 0xE0
|
||||
regID reg = 0xD0
|
||||
regConfig reg = 0x75
|
||||
regCtrlMeas reg = 0x74
|
||||
regCtrlHum reg = 0x72
|
||||
regCtrlGas1 reg = 0x71
|
||||
regCtrlGas0 reg = 0x70
|
||||
|
||||
// Gas wait time registers
|
||||
regGasWait0 reg = 0x64
|
||||
regGasWait1 reg = 0x65
|
||||
regGasWait2 reg = 0x66
|
||||
regGasWait3 reg = 0x67
|
||||
regGasWait4 reg = 0x68
|
||||
regGasWait5 reg = 0x69
|
||||
regGasWait6 reg = 0x6A
|
||||
regGasWait7 reg = 0x6B
|
||||
regGasWait8 reg = 0x6C
|
||||
regGasWait9 reg = 0x6D
|
||||
|
||||
// Heater registers
|
||||
regResHeat0 reg = 0x5A
|
||||
regResHeat1 reg = 0x5B
|
||||
regResHeat2 reg = 0x5C
|
||||
regResHeat3 reg = 0x5D
|
||||
regResHeat4 reg = 0x5E
|
||||
regResHeat5 reg = 0x5F
|
||||
regResHeat6 reg = 0x60
|
||||
regResHeat7 reg = 0x61
|
||||
regResHeat8 reg = 0x62
|
||||
regResHeat9 reg = 0x63
|
||||
|
||||
// IDAC heater registers : Retained for future use
|
||||
regIDACHeat0 reg = 0x50 //unused
|
||||
regIDACHeat1 reg = 0x51 //unused
|
||||
regIDACHeat2 reg = 0x52 //unused
|
||||
regIDACHeat3 reg = 0x53 //unused
|
||||
regIDACHeat4 reg = 0x54 //unused
|
||||
regIDACHeat5 reg = 0x55 //unused
|
||||
regIDACHeat6 reg = 0x56 //unused
|
||||
regIDACHeat7 reg = 0x57 //unused
|
||||
regIDACHeat8 reg = 0x58 //unused
|
||||
regIDACHeat9 reg = 0x59 //unused
|
||||
|
||||
// Sensor data registers
|
||||
regGasRMsb reg = 0x2A
|
||||
regGasRLsb reg = 0x2B //unused
|
||||
regHumMSB reg = 0x25 //unused
|
||||
regHumLSB reg = 0x26 //unused
|
||||
regTempXLSB reg = 0x24 //unused
|
||||
regTempLSB reg = 0x23 //unused
|
||||
regTempMSB reg = 0x22 //unused
|
||||
regPressXLSB reg = 0x21 //unused
|
||||
regPressLSB reg = 0x20 //unused
|
||||
regPressMSB reg = 0x1F
|
||||
|
||||
// Extended status and variant ID
|
||||
regEASStatus0 reg = 0x1D
|
||||
regVariantID reg = 0xF0
|
||||
|
||||
// Temperature calibration registers
|
||||
regParT1 reg = 0xE9 // uint16, LSB @ 0xE9, MSB @ 0xEA
|
||||
regParT2 reg = 0x8A // int16, LSB @ 0x8A, MSB @ 0x8B
|
||||
regParT3 reg = 0x8C // int8
|
||||
|
||||
// Pressure calibration registers
|
||||
regParP1 reg = 0x8E // uint16, LSB @ 0x8E / MSB @ 0x8F
|
||||
regParP2 reg = 0x90 // int16 LSB @ 0x90 / MSB @ 0x91
|
||||
regParP3 reg = 0x92 // int8
|
||||
regParP4 reg = 0x94 // int16 LSB @ 0x94 / MSB @ 0x95
|
||||
regParP5 reg = 0x96 // int16 LSB @ 0x96 / MSB @ 0x97
|
||||
regParP6 reg = 0x99 // int8
|
||||
regParP7 reg = 0x98 // int8
|
||||
regParP8 reg = 0x9C // int16 LSB @ 0x9C / MSB @ 0x9D
|
||||
regParP9 reg = 0x9E // int16 LSB @ 0x9E / MSB @ 0x9F
|
||||
regParP10 reg = 0xA0 // uint8
|
||||
|
||||
// Humidity calibration (packed) registers
|
||||
regParH2 reg = 0xE1 // H2[11:4] LSB @ 0xE2<7:4> / MSB @ 0xE1
|
||||
regParH1 reg = 0xE3 // H1[11:4] LSB @ 0xE2<3:0> / MSB @ 0xE3
|
||||
regParH3 reg = 0xE4 // int8
|
||||
regParH4 reg = 0xE5 // int8
|
||||
regParH5 reg = 0xE6 // int8
|
||||
regParH6 reg = 0xE7 // uint8
|
||||
regParH7 reg = 0xE8 // int8
|
||||
|
||||
// Gas calibration registers
|
||||
regParG2 reg = 0xEB // int16 LSB @ 0xEB / MSB @ 0xEC
|
||||
regParG1 reg = 0xED // int8
|
||||
regParG3 reg = 0xEE // int8
|
||||
|
||||
// Heater calibration (special registers)
|
||||
regResHeatVal reg = 0x00
|
||||
regResHeatRange reg = 0x02 // <5:4>
|
||||
regRangeSwitchingError reg = 0x04
|
||||
)
|
||||
|
||||
// Global slices for gas sensor registers
|
||||
var gasWaitRegs = []reg{
|
||||
regGasWait0, regGasWait1, regGasWait2, regGasWait3, regGasWait4,
|
||||
regGasWait5, regGasWait6, regGasWait7, regGasWait8, regGasWait9,
|
||||
}
|
||||
|
||||
var resHeatRegs = []reg{
|
||||
regResHeat0, regResHeat1, regResHeat2, regResHeat3, regResHeat4,
|
||||
regResHeat5, regResHeat6, regResHeat7, regResHeat8, regResHeat9,
|
||||
}
|
||||
|
||||
// regWrite writes a sequence of bytes to the device
|
||||
func (dev *Device) regWrite(b []byte) error {
|
||||
if err := dev.d.Tx(b, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// regRead reads a sequence of bytes from a given register
|
||||
func (dev *Device) regRead(addr reg, length uint8) ([]byte, error) {
|
||||
readBuf := make([]byte, length)
|
||||
if err := dev.d.Tx([]byte{byte(addr)}, readBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readBuf, nil
|
||||
}
|
||||
Loading…
Reference in New Issue