Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
61 changes: 52 additions & 9 deletions lib/bme280/bme280/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
STATUS_IM_UPDATE,
STATUS_MEASURING,
)
from bme280.exceptions import BME280Error, BME280InvalidDevice, BME280NotFound
from bme280.exceptions import BME280InvalidDevice, BME280NotFound


class BME280(object):
Expand Down Expand Up @@ -125,7 +125,7 @@ def _wait_boot(self, timeout_ms=50):
if not (self._read_reg(REG_STATUS) & STATUS_IM_UPDATE):
return
sleep_ms(5)
raise BME280Error("BME280 NVM copy timeout")
raise OSError("BME280 NVM copy timeout")

def device_id(self):
"""Read chip ID register. Expected: 0x60."""
Expand Down Expand Up @@ -239,6 +239,21 @@ def humidity_ready(self):
# Forced measurement trigger
# --------------------------------------------------

def _is_sleep_mode(self):
"""Return True if the sensor is in sleep mode."""
return (self._read_reg(REG_CTRL_MEAS) & MODE_MASK) == MODE_SLEEP

def _ensure_data(self):
"""Trigger a forced measurement if the sensor is in sleep mode.

In normal mode this is a no-op. In sleep mode it triggers a
single conversion and waits for completion so that subsequent
register reads return fresh data.
"""
if self._is_sleep_mode():
self.trigger_one_shot()
self._wait_measurement()
Comment on lines +246 to +255
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.

_ensure_data() relies on _wait_measurement(), which currently raises BME280Error("BME280 measurement timeout"). The auto-trigger convention in issue #159 calls for raising OSError on timeout; as implemented, callers will see a driver-specific exception type instead. Consider changing the timeout exception to OSError (or making BME280Error inherit from OSError) so timeout handling is consistent with the convention across drivers.

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.

The project convention uses driver-specific exceptions (e.g. WSENPADSTimeout in wsen-pads, BME280Error here), all inheriting from Exception, not OSError. This is consistent across all drivers in the repo. Keeping BME280Error for now to stay aligned.

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.

You were right — fixed in 3e6fd4f. The convention from #44 is indeed OSError for timeouts, and 5 out of 7 drivers already follow it. I had incorrectly aligned with WSEN-PADS/WSEN-HIDS which are the two outliers. Created #319 to harmonize those two remaining drivers.


def trigger_one_shot(self):
"""Trigger a single forced measurement. Poll data_ready() for completion."""
ctrl = self._read_reg(REG_CTRL_MEAS)
Expand All @@ -250,7 +265,7 @@ def _wait_measurement(self, timeout_ms=100):
if self.data_ready():
return
sleep_ms(5)
raise BME280Error("BME280 measurement timeout")
raise OSError("BME280 measurement timeout")

# --------------------------------------------------
# Raw data burst read
Expand Down Expand Up @@ -323,32 +338,60 @@ def _compensate_humidity(self, raw_hum):
# --------------------------------------------------

def temperature(self):
"""Return compensated temperature in °C (float)."""
"""Return compensated temperature in °C (float).

If the sensor is in sleep mode, a forced measurement is triggered
automatically before reading.
"""
self._ensure_data()
raw_temp, _, _ = self._read_raw()
return self._compensate_temperature(raw_temp) / 100.0

def pressure_hpa(self):
"""Return compensated pressure in hPa (float)."""
"""Return compensated pressure in hPa (float).

If the sensor is in sleep mode, a forced measurement is triggered
automatically before reading.
"""
self._ensure_data()
raw_temp, raw_press, _ = self._read_raw()
self._compensate_temperature(raw_temp)
return self._compensate_pressure(raw_press) / 25600.0

def humidity(self):
"""Return compensated relative humidity in %RH (float)."""
"""Return compensated relative humidity in %RH (float).

If the sensor is in sleep mode, a forced measurement is triggered
automatically before reading.
"""
self._ensure_data()
raw_temp, _, raw_hum = self._read_raw()
self._compensate_temperature(raw_temp)
return self._compensate_humidity(raw_hum) / 1024.0

def read(self):
"""Return (temperature_c, pressure_hpa, humidity_rh) tuple."""
"""Return (temperature_c, pressure_hpa, humidity_rh) tuple.

If the sensor is in sleep mode, a forced measurement is triggered
automatically before reading.
"""
self._ensure_data()
raw_temp, raw_press, raw_hum = self._read_raw()
Comment on lines 372 to 379
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.

read_one_shot() calls read(), and read() now calls _ensure_data(). On real BME280 hardware, a forced measurement typically returns the sensor to sleep mode after completion; that would make read() immediately trigger a second forced measurement, so read_one_shot() could perform two conversions instead of one. To avoid double-triggering, consider having read_one_shot() read/compensate directly (bypassing _ensure_data), or add an internal/read parameter that skips auto-trigger when read_one_shot() has already triggered a conversion.

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.

Fixed in d39f467: read_one_shot() now reads and compensates directly instead of calling read(), avoiding the double-trigger. A new test "read_one_shot does not double-trigger" verifies that exactly one forced mode write occurs.

temp_c = self._compensate_temperature(raw_temp) / 100.0
press_hpa = self._compensate_pressure(raw_press) / 25600.0
hum_rh = self._compensate_humidity(raw_hum) / 1024.0
return temp_c, press_hpa, hum_rh

def read_one_shot(self):
"""Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh)."""
"""Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh).

Reads registers directly without calling _ensure_data() to avoid
a double trigger (forced mode returns the sensor to sleep).
"""
self.trigger_one_shot()
self._wait_measurement()
return self.read()
raw_temp, raw_press, raw_hum = self._read_raw()
temp_c = self._compensate_temperature(raw_temp) / 100.0
press_hpa = self._compensate_pressure(raw_press) / 25600.0
hum_rh = self._compensate_humidity(raw_hum) / 1024.0
return temp_c, press_hpa, hum_rh
110 changes: 110 additions & 0 deletions tests/scenarios/bme280.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,113 @@ tests:
prompt: "Do the values look reasonable?"
expect_true: true
mode: [hardware]

# ----- Auto-trigger (_ensure_data) -----

- name: "_is_sleep_mode returns True after init"
action: script
script: |
result = dev._is_sleep_mode()
expect_true: true
mode: [mock]

- name: "_is_sleep_mode returns False in normal mode"
action: script
script: |
dev.power_on()
result = not dev._is_sleep_mode()
expect_true: true
mode: [mock]

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

- name: "_ensure_data is no-op in normal mode"
action: script
script: |
dev.power_on()
i2c.clear_write_log()
dev._ensure_data()
log = i2c.get_write_log()
# In normal mode, _ensure_data must not write to ctrl_meas (0xF4)
wrote_forced = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
result = not wrote_forced
expect_true: true
mode: [mock]

- name: "temperature() auto-triggers in sleep mode"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
t = dev.temperature()
log = i2c.get_write_log()
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
result = triggered and abs(t - 25.08) < 0.1
expect_true: true
mode: [mock]

- name: "pressure_hpa() auto-triggers in sleep mode"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
p = dev.pressure_hpa()
log = i2c.get_write_log()
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
result = triggered and abs(p - 1009.21) < 0.5
expect_true: true
mode: [mock]

- name: "humidity() auto-triggers in sleep mode"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
h = dev.humidity()
log = i2c.get_write_log()
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
result = triggered and abs(h - 50.57) < 0.5
expect_true: true
mode: [mock]

- name: "read() auto-triggers in sleep mode"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
t, p, h = dev.read()
log = i2c.get_write_log()
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
result = (
triggered
and abs(t - 25.08) < 0.1
and abs(p - 1009.21) < 0.5
and abs(h - 50.57) < 0.5
)
expect_true: true
mode: [mock]

- name: "read_one_shot does not double-trigger"
action: script
script: |
dev.power_off()
i2c.clear_write_log()
t, p, h = dev.read_one_shot()
log = i2c.get_write_log()
# Should only trigger forced mode once (not twice via _ensure_data)
forced_writes = [
1 for reg, data in log if reg == 0xF4 and (data[0] & 0x03) == 0x01
]
result = len(forced_writes) == 1 and abs(t - 25.08) < 0.1
expect_true: true
mode: [mock]
Loading