|
|
// 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 (
|
|
|
"fmt"
|
|
|
|
|
|
"periph.io/x/conn/v3/physic"
|
|
|
)
|
|
|
|
|
|
// bme680 represents a BME680 sensor and provides methods to perform measurements.
|
|
|
type bme680 struct {
|
|
|
dev *Device // Pointer to device context (configuration, registers, and helper methods)
|
|
|
}
|
|
|
|
|
|
// adcData stores raw ADC readings and gas sensor status from the BME680.
|
|
|
type adcData struct {
|
|
|
tRaw, pRaw, hRaw, gRaw uint32 // Raw temperature, pressure, humidity, and gas ADC values
|
|
|
gResRange gasRangeR // Gas resistance range (from GAS_R_LSB register)
|
|
|
gasValid, heaterStable bool // gas measurement validity and heater stability
|
|
|
}
|
|
|
|
|
|
// sensorStatus represents the current measurement status of the BME680.
|
|
|
type sensorStatus struct {
|
|
|
MeasurementReady bool // True, New TPHG data available (bit 7) of eas_status_0 register
|
|
|
GasInProgress bool // True, Gas measurement in progress (bit 6) of eas_status_0 register
|
|
|
TPHInProgress bool // True, Temp/Pressure/Humidity measurement in progress (bit 5) of eas_status_0 register
|
|
|
GasProfileInProgress uint8 // Index of Current gas profile (bits 0-2) of eas_status_0 register
|
|
|
}
|
|
|
|
|
|
func (dev *Device) newBME680() sensorOps {
|
|
|
return &bme680{dev}
|
|
|
}
|
|
|
|
|
|
// Sense triggers a single forced-mode measurement and returns the compensated data(Temperature, pressure, humidity, and optionally gas resistance)
|
|
|
// The BME680 automatically returns to sleep after each measurement.
|
|
|
// If gas measurements are enabled, a gas profile must be selected beforehand.
|
|
|
func (b *bme680) sense() (physic.Env, GasResistance, bool, error) {
|
|
|
// Check Gas Profile selection
|
|
|
if !b.dev.cfg.GasEnabled || b.dev.activeGasProfileIndex < 0 {
|
|
|
return physic.Env{}, 0, false, ErrNoGasProfileSelected
|
|
|
}
|
|
|
// Trigger one-shot forced measurement (Blocking until complete)
|
|
|
if err := b.dev.triggerForcedMeasurement(); err != nil {
|
|
|
return physic.Env{}, 0, false, err
|
|
|
}
|
|
|
return b.readSensorData() // return new measurement
|
|
|
}
|
|
|
|
|
|
// setGasProfile activates a specific gas profile (0-9)
|
|
|
// It does NOT trigger a measurement; call Sense() to measure
|
|
|
func (b *bme680) setGasProfile(profile uint8) error {
|
|
|
// Enable Gas Point
|
|
|
writeBuf := []byte{byte(regCtrlGas1), (1 << 4) | (profile)}
|
|
|
writeBuf = append(writeBuf,
|
|
|
byte(gasWaitRegs[profile]), b.dev.gasWaitXCalculationForcedMode(profile),
|
|
|
byte(resHeatRegs[profile]), b.dev.resHeatXCalculation(profile),
|
|
|
)
|
|
|
if err := b.dev.regWrite(writeBuf); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
b.dev.activeGasProfileIndex = int8(profile) // Update active profile index
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// readSensorData reads the latest measurement data from the BME680.
|
|
|
// It returns temperature, pressure, humidity, and optionally gas resistance
|
|
|
// along with a validity flag for the gas measurement.
|
|
|
func (b *bme680) readSensorData() (physic.Env, GasResistance, bool, error) {
|
|
|
// Read raw Temperature, Pressure, Humidity registers (8 bytes)
|
|
|
tph, err := b.dev.regRead(regPressMSB, 0x8)
|
|
|
if err != nil {
|
|
|
return physic.Env{}, 0, false, err
|
|
|
}
|
|
|
// Read raw Gas registers (2 bytes)
|
|
|
g, err := b.dev.regRead(regGasRMsb, 0x2)
|
|
|
if err != nil {
|
|
|
return physic.Env{}, 0, false, err
|
|
|
}
|
|
|
// Validate raw data length
|
|
|
if len(tph) < 8 || len(g) < 2 {
|
|
|
return physic.Env{}, 0, false, fmt.Errorf("bme680: raw sensor data incomplete")
|
|
|
}
|
|
|
// Parse raw TPH & gas data into structured adc values
|
|
|
adc := b.parseRawSensorData(tph, g)
|
|
|
// Compensate raw sensor values to human-readable units
|
|
|
return b.compensateSensorValues(adc)
|
|
|
}
|
|
|
|
|
|
// compensateSensorValues converts raw ADC values into compensated
|
|
|
// temperature, pressure, humidity, and gas resistance readings.
|
|
|
func (b *bme680) compensateSensorValues(adc adcData) (physic.Env, GasResistance, bool, error) {
|
|
|
var env physic.Env
|
|
|
var gas GasResistance
|
|
|
// Compensated Temperature (°C)
|
|
|
tComp := float32(b.dev.compensatedTemperature(adc.tRaw)) // Deg C
|
|
|
env.Temperature = physic.Temperature(tComp)*10*physic.MilliCelsius + physic.ZeroCelsius
|
|
|
// Initialize ambient temperature if not given in the user config
|
|
|
if b.dev.cfg.AmbientTempC == 0.0 {
|
|
|
b.dev.ambientTempC = float32(env.Temperature.Celsius())
|
|
|
}
|
|
|
// Compensate humidity (%RH)
|
|
|
hComp := b.dev.compensatedHumidity(adc.hRaw) // %RH × 1000
|
|
|
env.Humidity = physic.RelativeHumidity(float64(hComp)/1000.0) * physic.PercentRH // %rH
|
|
|
// Clamp humidity to valid range
|
|
|
if env.Humidity < 0*physic.PercentRH {
|
|
|
env.Humidity = 0 * physic.PercentRH
|
|
|
} else if env.Humidity > 100*physic.PercentRH {
|
|
|
env.Humidity = 100 * physic.PercentRH
|
|
|
}
|
|
|
// Compensate Pressure (Pa)
|
|
|
pComp := b.dev.compensatedPressure(adc.pRaw)
|
|
|
env.Pressure = physic.Pressure(pComp) * physic.Pascal
|
|
|
// Validate Gas measurement
|
|
|
gasValid := false
|
|
|
if b.dev.cfg.GasEnabled && adc.gasValid && adc.heaterStable {
|
|
|
gas = GasResistance(b.dev.compensatedGasSensor(adc.gRaw, adc.gResRange))
|
|
|
gasValid = true
|
|
|
}
|
|
|
// Return compensated sensor readings (temperature, humidity, pressure, and gas resistance)
|
|
|
return env, gas, gasValid, nil
|
|
|
}
|
|
|
|
|
|
// parseRawSensorData extracts raw ADC fields from register bytes.
|
|
|
func (b *bme680) parseRawSensorData(tph, g []byte) adcData {
|
|
|
var adc adcData
|
|
|
adc.tRaw = uint32(tph[3])<<12 | uint32(tph[4])<<4 | (uint32(tph[5])&0xF0)>>4
|
|
|
adc.pRaw = uint32(tph[0])<<12 | uint32(tph[1])<<4 | (uint32(tph[2])&0xF0)>>4
|
|
|
adc.hRaw = uint32(tph[6])<<8 | uint32(tph[7])
|
|
|
adc.gRaw = uint32(g[0])<<2 | uint32(g[1]&0xC0)>>6
|
|
|
adc.gResRange = gasRangeR(g[1] & 0x0F)
|
|
|
adc.gasValid = ((g[1] & 0x20) >> 5) == 1
|
|
|
adc.heaterStable = ((g[1] & 0x10) >> 4) == 1
|
|
|
return adc
|
|
|
}
|
|
|
|
|
|
// status reads the BME680 status register and returns the current sensor state.
|
|
|
func (b *bme680) status() (sensorStatus, error) {
|
|
|
var status sensorStatus
|
|
|
// Read a byte from status register
|
|
|
s, err := b.dev.regRead(regEASStatus0, 0x1)
|
|
|
if err != nil {
|
|
|
return status, err
|
|
|
}
|
|
|
// Status bit positions
|
|
|
const (
|
|
|
measReadyBit = 7
|
|
|
gasInProgBit = 6
|
|
|
tphInProgBit = 5
|
|
|
gasProfMask = 0x07 // bits 0-2
|
|
|
)
|
|
|
// Mask and shift bits properly
|
|
|
status.MeasurementReady = ((s[0] >> measReadyBit) & 0x01) == 1
|
|
|
status.GasInProgress = ((s[0] >> gasInProgBit) & 0x01) == 1
|
|
|
status.TPHInProgress = ((s[0] >> tphInProgBit) & 0x01) == 1
|
|
|
status.GasProfileInProgress = s[0] & gasProfMask
|
|
|
return status, nil
|
|
|
}
|
|
|
|
|
|
// prepareGasConfig constructs the gas heater configuration register sequence for the current configuration.
|
|
|
func (b *bme680) prepareGasConfig() []byte {
|
|
|
var firstProfileFound = -1
|
|
|
var gasBuf []byte
|
|
|
for idx, profile := range b.dev.cfg.GasProfiles {
|
|
|
if profile.TargetTempC > 0 && firstProfileFound == -1 {
|
|
|
firstProfileFound = idx // Select the First valid profile
|
|
|
}
|
|
|
gasBuf = append(gasBuf, byte(resHeatRegs[idx]), b.dev.resHeatXCalculation(uint8(idx)))
|
|
|
if b.dev.cfg.OperatingMode == ForcedMode {
|
|
|
gasBuf = append(gasBuf, byte(gasWaitRegs[idx]), b.dev.gasWaitXCalculationForcedMode(uint8(idx)))
|
|
|
}
|
|
|
}
|
|
|
// Activate the first valid profile if any
|
|
|
if firstProfileFound != -1 {
|
|
|
gasBuf = append(gasBuf, byte(regCtrlGas1), byte((1<<4)|(firstProfileFound)))
|
|
|
b.dev.activeGasProfileIndex = int8(firstProfileFound)
|
|
|
}
|
|
|
return gasBuf
|
|
|
}
|