Add i2c support for BME680 sensor driver

pull/115/head
Divya Bhaskaran 3 months ago
parent 226f28bde0
commit f0fd3799b8

@ -0,0 +1,181 @@
// 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
}

@ -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,229 @@
// 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"
)
// gasRangeR represents the gas sensor range index 0 - 15
type gasRangeR uint8
// gasConstant List of gas ranges and corresponding constants used for the resistance calculation
// Table 16 of Datasheet
var gasConstant = map[gasRangeR][]uint32{
0: {2147483647, 4096000000},
1: {2147483647, 2048000000},
2: {2147483647, 1024000000},
3: {2147483647, 512000000},
4: {2147483647, 255744255},
5: {2126008810, 127110228},
6: {2147483647, 64000000},
7: {2130303777, 32258064},
8: {2147483647, 16016016},
9: {2147483647, 8000000},
10: {2143188679, 4000000},
11: {2136746228, 2000000},
12: {2147483647, 1000000},
13: {2126008810, 500000},
14: {2147483647, 250000},
15: {2147483647, 125000},
}
// SensorCalibration holds the calibration coefficients read from a BME680 sensor.
// These values are used internally to convert raw sensor readings into
// accurate temperature, pressure, humidity, and gas measurements.
type SensorCalibration struct {
t1 uint16
t2 int16
t3 int8
p1 uint16
p2, p4, p5, p8, p9 int16
p3, p6, p7 int8
p10 uint8
h1, h2 uint16
h3, h4, h5, h6, h7 int8
g1, g3 int8
g2 int16
resHeatVal, resHeatRange, switchingErr uint8
tFine int32
tempComp int32
pressureComp int32
humidityComp int32
}
// InitCalibration loads the sensor's calibration data for accurate measurements.
func (dev *Device) InitCalibration() error {
// Read all calibration registers
t, err := dev.regRead(regParT1, 2)
if err != nil {
return err
}
tp, err := dev.regRead(regParT2, 23)
if err != nil {
return err
}
h, err := dev.regRead(regParH2, 8)
if err != nil {
return err
}
g, err := dev.regRead(regParG2, 4)
if err != nil {
return err
}
r, err := dev.regRead(regResHeatVal, 5)
if err != nil {
return err
}
// Combined length check
if len(t) < 2 || len(tp) < 23 || len(h) < 8 || len(g) < 4 || len(r) < 5 {
return fmt.Errorf("calibration data incomplete: t=%d, tp=%d, h=%d, g=%d, r=%d", len(t), len(tp), len(h), len(g), len(r))
}
// Populate calibration struct
dev.c = SensorCalibration{}
dev.c.t1 = uint16(t[1])<<8 | uint16(t[0])
dev.c.t2 = int16(tp[1])<<8 | int16(tp[0])
dev.c.t3 = int8(tp[2])
dev.c.p1 = uint16(tp[5])<<8 | uint16(tp[4])
dev.c.p2 = int16(tp[7])<<8 | int16(tp[6])
dev.c.p3 = int8(tp[8])
dev.c.p4 = int16(tp[11])<<8 | int16(tp[10])
dev.c.p5 = int16(tp[13])<<8 | int16(tp[12])
dev.c.p7 = int8(tp[14])
dev.c.p6 = int8(tp[15])
dev.c.p8 = int16(tp[19])<<8 | int16(tp[18])
dev.c.p9 = int16(tp[21])<<8 | int16(tp[20])
dev.c.p10 = tp[22]
dev.c.h1 = uint16(h[2])<<4 | uint16(h[1]&0xF)
dev.c.h2 = uint16(h[0])<<4 | (uint16(h[1]&0xF0) >> 4)
dev.c.h3 = int8(h[3])
dev.c.h4 = int8(h[4])
dev.c.h5 = int8(h[5])
dev.c.h6 = int8(h[6])
dev.c.h7 = int8(h[7])
dev.c.g2 = int16(g[1])<<8 | int16(g[0])
dev.c.g1 = int8(g[2])
dev.c.g3 = int8(g[3])
dev.c.resHeatVal = r[0]
dev.c.resHeatRange = (uint8(r[1]) & 0x30) >> 4
dev.c.switchingErr = r[4]
return nil
}
// compensatedTemperature in degrees Celsius, BME680: Refer Integer Section 3.3.1 of Datasheet
func (dev *Device) compensatedTemperature(tempAdc uint32) int32 {
var1 := (int32(tempAdc) >> 3) - (int32(dev.c.t1) << 1)
var2 := (var1 * int32(dev.c.t2)) >> 11
var3 := ((((var1 >> 1) * (var1 >> 1)) >> 12) * (int32(dev.c.t3) << 4)) >> 14
dev.c.tFine = var2 + var3
tempComp := ((dev.c.tFine * 5) + 128) >> 8
dev.c.tempComp = tempComp
return dev.c.tempComp
}
// compensatedPressure in Pascal, Refer Integer Section 3.3.2 of Datasheet
func (dev *Device) compensatedPressure(pressureAdc uint32) int32 {
var1 := (dev.c.tFine >> 1) - 64000
var2 := ((((var1 >> 2) * (var1 >> 2)) >> 11) * int32(dev.c.p6)) >> 2
var2 = var2 + ((var1 * int32(dev.c.p5)) << 1)
var2 = (var2 >> 2) + (int32(dev.c.p4) << 16)
var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * (int32(dev.c.p3) << 5)) >> 3) + ((int32(dev.c.p2) * var1) >> 1)
var1 = var1 >> 18
var1 = ((32768 + var1) * int32(dev.c.p1)) >> 15
pressComp := 1048576 - int32(pressureAdc)
pressComp = (pressComp - (var2 >> 12)) * (int32(3125))
if pressComp >= (1 << 30) {
pressComp = (pressComp / var1) << 1
} else {
pressComp = (pressComp << 1) / var1
}
var1 = (int32(dev.c.p9) * (((pressComp >> 3) * (pressComp >> 3)) >> 13)) >> 12
var2 = ((pressComp >> 2) * int32(dev.c.p8)) >> 13
var3 := ((pressComp >> 8) * (pressComp >> 8) * (pressComp >> 8) * int32(dev.c.p10)) >> 17
dev.c.pressureComp = pressComp + ((var1 + var2 + var3 + (int32(dev.c.p7) << 7)) >> 4)
return dev.c.pressureComp
}
// compensatedHumidity %RH, Refer Integer Section 3.3.3 of Datasheet
func (dev *Device) compensatedHumidity(humidityAdc uint32) int32 {
tempScaled := dev.c.tempComp
var1 := int32(humidityAdc) - int32(dev.c.h1)<<4 - (((tempScaled * int32(dev.c.h3)) / (int32(100))) >> 1)
var2 := (int32(dev.c.h2) * (((tempScaled * int32(dev.c.h4)) / (int32(100))) + (((tempScaled * ((tempScaled * int32(dev.c.h5)) /
(int32(100)))) >> 6) / (int32(100))) + (int32(1 << 14)))) >> 10
var3 := var1 * var2
var4 := ((int32(dev.c.h6) << 7) + ((tempScaled * int32(dev.c.h7)) / (int32(100)))) >> 4
var5 := ((var3 >> 14) * (var3 >> 14)) >> 10
var6 := (var4 * var5) >> 1
dev.c.humidityComp = (((var3 + var6) >> 10) * (int32(1000))) >> 12
return dev.c.humidityComp
}
// compensatedGasSensor resistance in Ohms, Refer Integer Section 3.4.1 of Datasheet
func (dev *Device) compensatedGasSensor(gasAdc uint32, gasRange gasRangeR) int32 {
if dev.variant == VariantNameBME680 {
var1 := ((1340 + (5 * int64(dev.c.switchingErr))) * (int64(gasConstant[gasRange][0]))) >> 16
var2 := int64(gasAdc<<15) - int64(1<<24) + var1
// Prevent Division by Zero
if var2 == 0 {
return 0
}
gasRes := int32(((int64(gasConstant[gasRange][1]) * var1 >> 9) + (var2 >> 1)) / var2)
return gasRes
}
return 0
}
// multiplicationFactor maps the GAS_WAIT register bits <7:6> to the corresponding
// gas sensor wait time multiplication factor as defined in the datasheet.
// Bits <7:6> | Wait time factor (ms)
var multiplicationFactor = map[uint8]uint8{
1: 0 << 6, // 00 -> factor 1
4: 1 << 6, // 01 -> factor 4
16: 2 << 6, // 10 -> factor 16
64: 3 << 6, // 11 -> factor 64
}
// gasWaitXCalculationForcedMode time between the beginning of the heat phase and the start of gas sensor resistance conversion
// Bits <7:6> = multiplication factor (1, 4, 16, 64), bits <5:0> = wait time in 1 ms steps(0 to 63/252/1008/4032 depending on factor).
// Refer Section 5.3.3.3 of datasheet
func (dev *Device) gasWaitXCalculationForcedMode(profile uint8) uint8 {
var waitMS = dev.cfg.GasProfiles[profile].HeatingDurationMs
var scale uint16 = 0
switch {
case waitMS >= 1 && waitMS <= 63:
scale = 1
case waitMS >= 64 && waitMS <= 252:
scale = 4
case waitMS >= 253 && waitMS <= 1008:
scale = 16
case waitMS >= 1009 && waitMS <= 4032:
scale = 64
default:
// BME680: Section 3.3.5 - In practice, approximately 2030 ms are necessary for the heater to reach
// the intended target temperature
scale = 4
waitMS = 100
}
gasWaitX := multiplicationFactor[uint8(scale)] | uint8((waitMS+scale-1)/scale)
return gasWaitX
}
// resHeatXCalculation convert the target temperature into a device specific target resistance before writing the resulting register code into the sensor
// memory map.
// Refer Section 3.3.5 of datasheet
func (dev *Device) resHeatXCalculation(profile uint8) uint8 {
// Max limit of Target is 400 °C
if dev.cfg.GasProfiles[profile].TargetTempC > 400 {
dev.cfg.GasProfiles[profile].TargetTempC = 400
}
var1 := ((int32(dev.cfg.AmbientTempC) * int32(dev.c.g3)) / 10) << 8
var2 := (int32(dev.c.g1) + 784) * (((((int32(dev.c.g2) + 154009) * int32(dev.cfg.GasProfiles[profile].TargetTempC) * 5) / 100) + 3276800) / 10)
var3 := var1 + (var2 >> 1)
var4 := var3 / (int32(dev.c.resHeatRange) + 4)
var5 := (131 * int32(dev.c.resHeatVal)) + 65536
resHeatX100 := ((var4 / var5) - 250) * 34
resHeatX := uint8((resHeatX100 + 50) / 100) // rounds to the nearest integer.
return resHeatX
}

@ -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…
Cancel
Save