Skip to content

Commit a0d344d

Browse files
authored
feat(bme280): Implement _ensure_data() auto-trigger pattern (#318)
* feat(bme280): Implement _ensure_data() auto-trigger pattern. * fix(bme280): Avoid double-trigger in read_one_shot and add missing tests. * fix(bme280): Use OSError for timeouts per convention established in #44.
1 parent c041dcc commit a0d344d

2 files changed

Lines changed: 162 additions & 9 deletions

File tree

lib/bme280/bme280/device.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
STATUS_IM_UPDATE,
3131
STATUS_MEASURING,
3232
)
33-
from bme280.exceptions import BME280Error, BME280InvalidDevice, BME280NotFound
33+
from bme280.exceptions import BME280InvalidDevice, BME280NotFound
3434

3535

3636
class BME280(object):
@@ -125,7 +125,7 @@ def _wait_boot(self, timeout_ms=50):
125125
if not (self._read_reg(REG_STATUS) & STATUS_IM_UPDATE):
126126
return
127127
sleep_ms(5)
128-
raise BME280Error("BME280 NVM copy timeout")
128+
raise OSError("BME280 NVM copy timeout")
129129

130130
def device_id(self):
131131
"""Read chip ID register. Expected: 0x60."""
@@ -239,6 +239,21 @@ def humidity_ready(self):
239239
# Forced measurement trigger
240240
# --------------------------------------------------
241241

242+
def _is_sleep_mode(self):
243+
"""Return True if the sensor is in sleep mode."""
244+
return (self._read_reg(REG_CTRL_MEAS) & MODE_MASK) == MODE_SLEEP
245+
246+
def _ensure_data(self):
247+
"""Trigger a forced measurement if the sensor is in sleep mode.
248+
249+
In normal mode this is a no-op. In sleep mode it triggers a
250+
single conversion and waits for completion so that subsequent
251+
register reads return fresh data.
252+
"""
253+
if self._is_sleep_mode():
254+
self.trigger_one_shot()
255+
self._wait_measurement()
256+
242257
def trigger_one_shot(self):
243258
"""Trigger a single forced measurement. Poll data_ready() for completion."""
244259
ctrl = self._read_reg(REG_CTRL_MEAS)
@@ -250,7 +265,7 @@ def _wait_measurement(self, timeout_ms=100):
250265
if self.data_ready():
251266
return
252267
sleep_ms(5)
253-
raise BME280Error("BME280 measurement timeout")
268+
raise OSError("BME280 measurement timeout")
254269

255270
# --------------------------------------------------
256271
# Raw data burst read
@@ -323,32 +338,60 @@ def _compensate_humidity(self, raw_hum):
323338
# --------------------------------------------------
324339

325340
def temperature(self):
326-
"""Return compensated temperature in °C (float)."""
341+
"""Return compensated temperature in °C (float).
342+
343+
If the sensor is in sleep mode, a forced measurement is triggered
344+
automatically before reading.
345+
"""
346+
self._ensure_data()
327347
raw_temp, _, _ = self._read_raw()
328348
return self._compensate_temperature(raw_temp) / 100.0
329349

330350
def pressure_hpa(self):
331-
"""Return compensated pressure in hPa (float)."""
351+
"""Return compensated pressure in hPa (float).
352+
353+
If the sensor is in sleep mode, a forced measurement is triggered
354+
automatically before reading.
355+
"""
356+
self._ensure_data()
332357
raw_temp, raw_press, _ = self._read_raw()
333358
self._compensate_temperature(raw_temp)
334359
return self._compensate_pressure(raw_press) / 25600.0
335360

336361
def humidity(self):
337-
"""Return compensated relative humidity in %RH (float)."""
362+
"""Return compensated relative humidity in %RH (float).
363+
364+
If the sensor is in sleep mode, a forced measurement is triggered
365+
automatically before reading.
366+
"""
367+
self._ensure_data()
338368
raw_temp, _, raw_hum = self._read_raw()
339369
self._compensate_temperature(raw_temp)
340370
return self._compensate_humidity(raw_hum) / 1024.0
341371

342372
def read(self):
343-
"""Return (temperature_c, pressure_hpa, humidity_rh) tuple."""
373+
"""Return (temperature_c, pressure_hpa, humidity_rh) tuple.
374+
375+
If the sensor is in sleep mode, a forced measurement is triggered
376+
automatically before reading.
377+
"""
378+
self._ensure_data()
344379
raw_temp, raw_press, raw_hum = self._read_raw()
345380
temp_c = self._compensate_temperature(raw_temp) / 100.0
346381
press_hpa = self._compensate_pressure(raw_press) / 25600.0
347382
hum_rh = self._compensate_humidity(raw_hum) / 1024.0
348383
return temp_c, press_hpa, hum_rh
349384

350385
def read_one_shot(self):
351-
"""Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh)."""
386+
"""Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh).
387+
388+
Reads registers directly without calling _ensure_data() to avoid
389+
a double trigger (forced mode returns the sensor to sleep).
390+
"""
352391
self.trigger_one_shot()
353392
self._wait_measurement()
354-
return self.read()
393+
raw_temp, raw_press, raw_hum = self._read_raw()
394+
temp_c = self._compensate_temperature(raw_temp) / 100.0
395+
press_hpa = self._compensate_pressure(raw_press) / 25600.0
396+
hum_rh = self._compensate_humidity(raw_hum) / 1024.0
397+
return temp_c, press_hpa, hum_rh

tests/scenarios/bme280.yaml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,113 @@ tests:
471471
prompt: "Do the values look reasonable?"
472472
expect_true: true
473473
mode: [hardware]
474+
475+
# ----- Auto-trigger (_ensure_data) -----
476+
477+
- name: "_is_sleep_mode returns True after init"
478+
action: script
479+
script: |
480+
result = dev._is_sleep_mode()
481+
expect_true: true
482+
mode: [mock]
483+
484+
- name: "_is_sleep_mode returns False in normal mode"
485+
action: script
486+
script: |
487+
dev.power_on()
488+
result = not dev._is_sleep_mode()
489+
expect_true: true
490+
mode: [mock]
491+
492+
- name: "_ensure_data triggers forced mode in sleep"
493+
action: script
494+
script: |
495+
dev.power_off()
496+
i2c.clear_write_log()
497+
dev._ensure_data()
498+
log = i2c.get_write_log()
499+
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
500+
result = triggered
501+
expect_true: true
502+
mode: [mock]
503+
504+
- name: "_ensure_data is no-op in normal mode"
505+
action: script
506+
script: |
507+
dev.power_on()
508+
i2c.clear_write_log()
509+
dev._ensure_data()
510+
log = i2c.get_write_log()
511+
# In normal mode, _ensure_data must not write to ctrl_meas (0xF4)
512+
wrote_forced = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
513+
result = not wrote_forced
514+
expect_true: true
515+
mode: [mock]
516+
517+
- name: "temperature() auto-triggers in sleep mode"
518+
action: script
519+
script: |
520+
dev.power_off()
521+
i2c.clear_write_log()
522+
t = dev.temperature()
523+
log = i2c.get_write_log()
524+
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
525+
result = triggered and abs(t - 25.08) < 0.1
526+
expect_true: true
527+
mode: [mock]
528+
529+
- name: "pressure_hpa() auto-triggers in sleep mode"
530+
action: script
531+
script: |
532+
dev.power_off()
533+
i2c.clear_write_log()
534+
p = dev.pressure_hpa()
535+
log = i2c.get_write_log()
536+
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
537+
result = triggered and abs(p - 1009.21) < 0.5
538+
expect_true: true
539+
mode: [mock]
540+
541+
- name: "humidity() auto-triggers in sleep mode"
542+
action: script
543+
script: |
544+
dev.power_off()
545+
i2c.clear_write_log()
546+
h = dev.humidity()
547+
log = i2c.get_write_log()
548+
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
549+
result = triggered and abs(h - 50.57) < 0.5
550+
expect_true: true
551+
mode: [mock]
552+
553+
- name: "read() auto-triggers in sleep mode"
554+
action: script
555+
script: |
556+
dev.power_off()
557+
i2c.clear_write_log()
558+
t, p, h = dev.read()
559+
log = i2c.get_write_log()
560+
triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log)
561+
result = (
562+
triggered
563+
and abs(t - 25.08) < 0.1
564+
and abs(p - 1009.21) < 0.5
565+
and abs(h - 50.57) < 0.5
566+
)
567+
expect_true: true
568+
mode: [mock]
569+
570+
- name: "read_one_shot does not double-trigger"
571+
action: script
572+
script: |
573+
dev.power_off()
574+
i2c.clear_write_log()
575+
t, p, h = dev.read_one_shot()
576+
log = i2c.get_write_log()
577+
# Should only trigger forced mode once (not twice via _ensure_data)
578+
forced_writes = [
579+
1 for reg, data in log if reg == 0xF4 and (data[0] & 0x03) == 0x01
580+
]
581+
result = len(forced_writes) == 1 and abs(t - 25.08) < 0.1
582+
expect_true: true
583+
mode: [mock]

0 commit comments

Comments
 (0)