Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions lib/bme280/bme280/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@
CALIB_H_SIZE,
CALIB_TP_SIZE,
DATA_BLOCK_SIZE,
FILTER_SHIFT,
MODE_FORCED,
MODE_MASK,
MODE_NORMAL,
MODE_SLEEP,
OSRS_P_SHIFT,
OSRS_T_SHIFT,
OSRS_X1,
REG_CALIB_HUM,
REG_CALIB_TEMP_PRESS,
REG_CHIP_ID,
REG_CONFIG,
REG_CTRL_HUM,
REG_CTRL_MEAS,
REG_DATA_START,
REG_SOFT_RESET,
REG_STATUS,
RESET_DELAY_MS,
SOFT_RESET_CMD,
STANDBY_SHIFT,
STATUS_IM_UPDATE,
STATUS_MEASURING,
)
Expand Down Expand Up @@ -139,6 +143,73 @@ def reset(self):
self._read_calibration()
self._configure_default()

# --------------------------------------------------
# Power and mode control
# --------------------------------------------------

def power_off(self):
"""Enter sleep mode. Stops all measurements."""
ctrl = self._read_reg(REG_CTRL_MEAS)
self._write_reg(REG_CTRL_MEAS, ctrl & ~MODE_MASK)

def power_on(self):
"""Enter normal mode. Continuous measurements at configured standby rate."""
ctrl = self._read_reg(REG_CTRL_MEAS)
self._write_reg(REG_CTRL_MEAS, (ctrl & ~MODE_MASK) | MODE_NORMAL)

def set_continuous(self, standby=None):
"""Start continuous measurements in normal mode.

Args:
standby: standby time constant (STANDBY_0_5_MS .. STANDBY_1000_MS).
If None, the current config register value is kept.
"""
if standby is not None:
self.set_standby(standby)
self.power_on()

# --------------------------------------------------
# Sensor configuration
# --------------------------------------------------

def set_oversampling(self, temperature=None, pressure=None, humidity=None):
"""Configure oversampling for one or more channels.

Args:
temperature: OSRS_SKIP .. OSRS_X16 (None = keep current).
pressure: OSRS_SKIP .. OSRS_X16 (None = keep current).
humidity: OSRS_SKIP .. OSRS_X16 (None = keep current).
"""
if humidity is not None:
self._write_reg(REG_CTRL_HUM, humidity)
if temperature is not None or pressure is not None:
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_oversampling() writes REG_CTRL_HUM when humidity is provided, but if only humidity is changed (temperature/pressure are None) it never writes REG_CTRL_MEAS. On BME280, ctrl_hum updates take effect only after a subsequent write to ctrl_meas (your own _configure_default() comment notes this), so humidity-only changes won't actually apply. Fix by performing a (read-modify-)write to REG_CTRL_MEAS after writing REG_CTRL_HUM even when temperature/pressure are unchanged (preserving the existing mode/oversampling bits).

Suggested change
if humidity is not None:
self._write_reg(REG_CTRL_HUM, humidity)
if temperature is not None or pressure is not None:
update_ctrl_meas = False
if humidity is not None:
# On BME280, changes to ctrl_hum take effect only after
# a subsequent write to ctrl_meas.
self._write_reg(REG_CTRL_HUM, humidity)
update_ctrl_meas = True
if temperature is not None or pressure is not None or update_ctrl_meas:

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in f63099a: set_oversampling() now always rewrites ctrl_meas after ctrl_hum, even when only humidity is changed. The comment in the code references datasheet section 5.4.3.

ctrl = self._read_reg(REG_CTRL_MEAS)
if temperature is not None:
ctrl = (ctrl & ~(0x07 << OSRS_T_SHIFT)) | (temperature << OSRS_T_SHIFT)
if pressure is not None:
ctrl = (ctrl & ~(0x07 << OSRS_P_SHIFT)) | (pressure << OSRS_P_SHIFT)
self._write_reg(REG_CTRL_MEAS, ctrl)

def set_iir_filter(self, coefficient):
"""Configure the IIR filter coefficient.

Args:
coefficient: FILTER_OFF .. FILTER_16.
"""
config = self._read_reg(REG_CONFIG)
config = (config & ~(0x07 << FILTER_SHIFT)) | (coefficient << FILTER_SHIFT)
self._write_reg(REG_CONFIG, config)

def set_standby(self, standby):
"""Configure the standby time for normal mode.

Args:
standby: STANDBY_0_5_MS .. STANDBY_1000_MS.
"""
config = self._read_reg(REG_CONFIG)
config = (config & ~(0x07 << STANDBY_SHIFT)) | (standby << STANDBY_SHIFT)
self._write_reg(REG_CONFIG, config)

# --------------------------------------------------
# Status
# --------------------------------------------------
Expand Down
132 changes: 132 additions & 0 deletions tests/scenarios/bme280.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,135 @@ tests:
result = triggered and abs(t - 25.08) < 0.1
expect_true: true
mode: [mock]

# ----- Power and mode control -----

- name: "power_off sets sleep mode"
action: script
script: |
i2c.clear_write_log()
dev.power_off()
log = i2c.get_write_log()
wrote_sleep = any(reg == 0xF4 and (data[0] & 0x03) == 0x00 for reg, data in log)
result = wrote_sleep
expect_true: true
mode: [mock]

- name: "power_on sets normal mode"
action: script
script: |
i2c.clear_write_log()
dev.power_on()
log = i2c.get_write_log()
wrote_normal = any(reg == 0xF4 and (data[0] & 0x03) == 0x03 for reg, data in log)
result = wrote_normal
expect_true: true
mode: [mock]

- name: "power_off preserves oversampling bits"
action: script
script: |
from bme280.const import OSRS_X4, OSRS_T_SHIFT, OSRS_P_SHIFT
dev.set_oversampling(temperature=OSRS_X4, pressure=OSRS_X4)
i2c.clear_write_log()
dev.power_off()
log = i2c.get_write_log()
ctrl = [data[0] for reg, data in log if reg == 0xF4][-1]
osrs_t = (ctrl >> 5) & 0x07
osrs_p = (ctrl >> 2) & 0x07
result = osrs_t == OSRS_X4 and osrs_p == OSRS_X4 and (ctrl & 0x03) == 0x00
expect_true: true
mode: [mock]

- name: "set_continuous enters normal mode"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
dev.set_continuous()
log = i2c.get_write_log()
wrote_normal = any(reg == 0xF4 and (data[0] & 0x03) == 0x03 for reg, data in log)
result = wrote_normal
expect_true: true
mode: [mock]

- name: "set_continuous with standby configures both"
action: script
script: |
from bme280.const import STANDBY_500_MS, STANDBY_SHIFT
i2c.clear_write_log()
dev.set_continuous(standby=STANDBY_500_MS)
log = i2c.get_write_log()
wrote_config = any(reg == 0xF5 for reg, data in log)
wrote_normal = any(reg == 0xF4 and (data[0] & 0x03) == 0x03 for reg, data in log)
result = wrote_config and wrote_normal
expect_true: true
mode: [mock]

# ----- Configuration -----

- name: "set_oversampling configures temperature and pressure"
action: script
script: |
from bme280.const import OSRS_X2, OSRS_X4, OSRS_T_SHIFT, OSRS_P_SHIFT
i2c.clear_write_log()
dev.set_oversampling(temperature=OSRS_X2, pressure=OSRS_X4)
log = i2c.get_write_log()
ctrl_writes = [data[0] for reg, data in log if reg == 0xF4]
ctrl = ctrl_writes[-1]
osrs_t = (ctrl >> OSRS_T_SHIFT) & 0x07
osrs_p = (ctrl >> OSRS_P_SHIFT) & 0x07
result = osrs_t == OSRS_X2 and osrs_p == OSRS_X4
expect_true: true
mode: [mock]

- name: "set_oversampling configures humidity"
action: script
script: |
from bme280.const import OSRS_X8
i2c.clear_write_log()
dev.set_oversampling(humidity=OSRS_X8)
log = i2c.get_write_log()
hum_writes = [data[0] for reg, data in log if reg == 0xF2]
result = len(hum_writes) == 1 and hum_writes[0] == OSRS_X8
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts set_oversampling(humidity=...) only writes REG_CTRL_HUM (0xF2). However, BME280 latches ctrl_hum changes only after a write to REG_CTRL_MEAS (0xF4), so the correct behavior should include a REG_CTRL_MEAS write as well. Update the expectation to also require (at least) one write to 0xF4 following the 0xF2 write.

Suggested change
hum_writes = [data[0] for reg, data in log if reg == 0xF2]
result = len(hum_writes) == 1 and hum_writes[0] == OSRS_X8
hum_index = None
for idx, (reg, data) in enumerate(log):
if reg == 0xF2 and data[0] == OSRS_X8:
hum_index = idx
break
if hum_index is None:
result = False
else:
wrote_ctrl_after = any(reg == 0xF4 for reg, data in log[hum_index + 1:])
result = wrote_ctrl_after

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in f63099a: the test now verifies that both ctrl_hum (0xF2) and ctrl_meas (0xF4) are written, and that 0xF2 comes before 0xF4 in the write log.

expect_true: true
mode: [mock]

- name: "set_iir_filter writes config register"
action: script
script: |
from bme280.const import FILTER_16, FILTER_SHIFT
i2c.clear_write_log()
dev.set_iir_filter(FILTER_16)
log = i2c.get_write_log()
config_writes = [data[0] for reg, data in log if reg == 0xF5]
result = len(config_writes) == 1 and ((config_writes[0] >> FILTER_SHIFT) & 0x07) == FILTER_16
expect_true: true
mode: [mock]

- name: "set_standby writes config register"
action: script
script: |
from bme280.const import STANDBY_250_MS, STANDBY_SHIFT
i2c.clear_write_log()
dev.set_standby(STANDBY_250_MS)
log = i2c.get_write_log()
config_writes = [data[0] for reg, data in log if reg == 0xF5]
result = len(config_writes) == 1 and ((config_writes[0] >> STANDBY_SHIFT) & 0x07) == STANDBY_250_MS
expect_true: true
mode: [mock]

- name: "set_iir_filter preserves standby bits"
action: script
script: |
from bme280.const import STANDBY_500_MS, STANDBY_SHIFT, FILTER_4, FILTER_SHIFT
dev.set_standby(STANDBY_500_MS)
i2c.clear_write_log()
dev.set_iir_filter(FILTER_4)
log = i2c.get_write_log()
config = [data[0] for reg, data in log if reg == 0xF5][-1]
standby = (config >> STANDBY_SHIFT) & 0x07
filt = (config >> FILTER_SHIFT) & 0x07
result = standby == STANDBY_500_MS and filt == FILTER_4
expect_true: true
mode: [mock]
Loading