From 8b142fcd11238bd1cd6e4e953c5e11e3afb874ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 12:41:04 +0200 Subject: [PATCH 1/2] feat(bme280): Add measurement modes and sensor configuration. --- lib/bme280/bme280/device.py | 71 +++++++++++++++++++ tests/scenarios/bme280.yaml | 132 ++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index bd253fef..95970dff 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -7,8 +7,10 @@ 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, @@ -16,6 +18,7 @@ REG_CALIB_HUM, REG_CALIB_TEMP_PRESS, REG_CHIP_ID, + REG_CONFIG, REG_CTRL_HUM, REG_CTRL_MEAS, REG_DATA_START, @@ -23,6 +26,7 @@ REG_STATUS, RESET_DELAY_MS, SOFT_RESET_CMD, + STANDBY_SHIFT, STATUS_IM_UPDATE, STATUS_MEASURING, ) @@ -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: + 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 # -------------------------------------------------- diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index 46f7c3f9..c8b078fb 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -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 + 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] From f63099a70ae2fab57851620bfc6cef435c31ed33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 12:45:47 +0200 Subject: [PATCH 2/2] fix(bme280): Rewrite ctrl_meas after ctrl_hum to latch humidity config. --- lib/bme280/bme280/device.py | 15 ++++++++------- tests/scenarios/bme280.yaml | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 95970dff..7e12e834 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -182,13 +182,14 @@ def set_oversampling(self, temperature=None, pressure=None, humidity=None): """ if humidity is not None: self._write_reg(REG_CTRL_HUM, humidity) - if temperature is not None or pressure is not None: - 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) + 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) + # ctrl_meas must always be rewritten: changes to ctrl_hum are only + # latched when ctrl_meas is written (datasheet section 5.4.3). + self._write_reg(REG_CTRL_MEAS, ctrl) def set_iir_filter(self, coefficient): """Configure the IIR filter coefficient. diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index c8b078fb..10e7361c 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -356,7 +356,7 @@ tests: expect_true: true mode: [mock] - - name: "set_oversampling configures humidity" + - name: "set_oversampling configures humidity and latches via ctrl_meas" action: script script: | from bme280.const import OSRS_X8 @@ -364,7 +364,17 @@ tests: 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 + # ctrl_meas must be rewritten after ctrl_hum for the change to take effect + ctrl_writes = [data[0] for reg, data in log if reg == 0xF4] + # Verify write order: ctrl_hum (0xF2) must come before ctrl_meas (0xF4) + hum_idx = next(i for i, (reg, _) in enumerate(log) if reg == 0xF2) + meas_idx = next(i for i, (reg, _) in enumerate(log) if reg == 0xF4) + result = ( + len(hum_writes) == 1 + and hum_writes[0] == OSRS_X8 + and len(ctrl_writes) == 1 + and hum_idx < meas_idx + ) expect_true: true mode: [mock]