Skip to content

Commit f18aeaf

Browse files
authored
feat(bme280): Add compensated temperature, pressure, humidity readings (#312)
* feat(bme280): Add compensated temperature, pressure, humidity readings. * test(bme280): Add register sequence support and boot polling test. * refactor(bme280): Remove duplicate section header in device.py. * fix(bme280): Address Copilot review comments on PR 312. * test(bme280): Add regression test for humidity at t_fine=76800.
1 parent bfe90c3 commit f18aeaf

3 files changed

Lines changed: 283 additions & 4 deletions

File tree

lib/bme280/bme280/device.py

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
BME280_I2C_DEFAULT_ADDR,
77
CALIB_H_SIZE,
88
CALIB_TP_SIZE,
9+
DATA_BLOCK_SIZE,
10+
MODE_FORCED,
11+
MODE_MASK,
912
MODE_SLEEP,
1013
OSRS_P_SHIFT,
1114
OSRS_T_SHIFT,
@@ -15,11 +18,13 @@
1518
REG_CHIP_ID,
1619
REG_CTRL_HUM,
1720
REG_CTRL_MEAS,
21+
REG_DATA_START,
1822
REG_SOFT_RESET,
1923
REG_STATUS,
2024
RESET_DELAY_MS,
2125
SOFT_RESET_CMD,
2226
STATUS_IM_UPDATE,
27+
STATUS_MEASURING,
2328
)
2429
from bme280.exceptions import BME280Error, BME280InvalidDevice, BME280NotFound
2530

@@ -51,10 +56,6 @@ def _write_reg(self, reg, value):
5156
"""Write a single byte to register."""
5257
self.i2c.writeto_mem(self.address, reg, bytes([value]))
5358

54-
# --------------------------------------------------
55-
# Device identification and initialization
56-
# --------------------------------------------------
57-
5859
# --------------------------------------------------
5960
# Calibration data
6061
# --------------------------------------------------
@@ -137,3 +138,145 @@ def reset(self):
137138
self.soft_reset()
138139
self._read_calibration()
139140
self._configure_default()
141+
142+
# --------------------------------------------------
143+
# Status
144+
# --------------------------------------------------
145+
146+
def _status(self):
147+
"""Return the raw STATUS register value."""
148+
return self._read_reg(REG_STATUS)
149+
150+
def data_ready(self):
151+
"""Return True when all measurements are available."""
152+
return not (self._status() & STATUS_MEASURING)
153+
154+
def temperature_ready(self):
155+
"""Return True when temperature data is available."""
156+
return self.data_ready()
157+
158+
def pressure_ready(self):
159+
"""Return True when pressure data is available."""
160+
return self.data_ready()
161+
162+
def humidity_ready(self):
163+
"""Return True when humidity data is available."""
164+
return self.data_ready()
165+
166+
# --------------------------------------------------
167+
# Forced measurement trigger
168+
# --------------------------------------------------
169+
170+
def trigger_one_shot(self):
171+
"""Trigger a single forced measurement. Poll data_ready() for completion."""
172+
ctrl = self._read_reg(REG_CTRL_MEAS)
173+
self._write_reg(REG_CTRL_MEAS, (ctrl & ~MODE_MASK) | MODE_FORCED)
174+
175+
def _wait_measurement(self, timeout_ms=100):
176+
"""Wait for measurement to complete. Raises on timeout."""
177+
for _ in range(timeout_ms // 5):
178+
if self.data_ready():
179+
return
180+
sleep_ms(5)
181+
raise BME280Error("BME280 measurement timeout")
182+
183+
# --------------------------------------------------
184+
# Raw data burst read
185+
# --------------------------------------------------
186+
187+
def _read_raw(self):
188+
"""Read raw ADC values via burst read (0xF7-0xFE, 8 bytes).
189+
190+
Returns (raw_temp, raw_press, raw_hum) as 20-bit/20-bit/16-bit integers.
191+
"""
192+
data = self._read_block(REG_DATA_START, DATA_BLOCK_SIZE)
193+
raw_press = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
194+
raw_temp = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
195+
raw_hum = (data[6] << 8) | data[7]
196+
return raw_temp, raw_press, raw_hum
197+
198+
# --------------------------------------------------
199+
# Compensation formulas (BME280 datasheet section 4.2.3)
200+
# --------------------------------------------------
201+
202+
def _compensate_temperature(self, raw_temp):
203+
"""Compute compensated temperature in hundredths of °C. Updates t_fine."""
204+
var1 = (((raw_temp >> 3) - (self.dig_T1 << 1)) * self.dig_T2) >> 11
205+
var2 = (
206+
(((raw_temp >> 4) - self.dig_T1) * ((raw_temp >> 4) - self.dig_T1)) >> 12
207+
) * self.dig_T3 >> 14
208+
self.t_fine = var1 + var2
209+
return (self.t_fine * 5 + 128) >> 8
210+
211+
def _compensate_pressure(self, raw_press):
212+
"""Compute compensated pressure in Pa (Q24.8 fixed point)."""
213+
var1 = self.t_fine - 128000
214+
var2 = var1 * var1 * self.dig_P6
215+
var2 = var2 + ((var1 * self.dig_P5) << 17)
216+
var2 = var2 + (self.dig_P4 << 35)
217+
var1 = ((var1 * var1 * self.dig_P3) >> 8) + ((var1 * self.dig_P2) << 12)
218+
var1 = ((1 << 47) + var1) * self.dig_P1 >> 33
219+
if var1 == 0:
220+
return 0
221+
p = 1048576 - raw_press
222+
p = (((p << 31) - var2) * 3125) // var1
223+
var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
224+
var2 = (self.dig_P8 * p) >> 19
225+
return ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)
226+
227+
def _compensate_humidity(self, raw_hum):
228+
"""Compute compensated humidity in Q22.10 fixed point."""
229+
h = self.t_fine - 76800
230+
h = (
231+
(((raw_hum << 14) - (self.dig_H4 << 20) - (self.dig_H5 * h)) + 16384)
232+
>> 15
233+
) * (
234+
(
235+
(
236+
(((h * self.dig_H6) >> 10) * (((h * self.dig_H3) >> 11) + 32768))
237+
>> 10
238+
)
239+
+ 2097152
240+
)
241+
* self.dig_H2
242+
+ 8192
243+
) >> 14
244+
h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.dig_H1) >> 4)
245+
h = max(h, 0)
246+
h = min(h, 419430400)
247+
return h >> 12
248+
249+
# --------------------------------------------------
250+
# Public measurement methods
251+
# --------------------------------------------------
252+
253+
def temperature(self):
254+
"""Return compensated temperature in °C (float)."""
255+
raw_temp, _, _ = self._read_raw()
256+
return self._compensate_temperature(raw_temp) / 100.0
257+
258+
def pressure_hpa(self):
259+
"""Return compensated pressure in hPa (float)."""
260+
raw_temp, raw_press, _ = self._read_raw()
261+
self._compensate_temperature(raw_temp)
262+
return self._compensate_pressure(raw_press) / 25600.0
263+
264+
def humidity(self):
265+
"""Return compensated relative humidity in %RH (float)."""
266+
raw_temp, _, raw_hum = self._read_raw()
267+
self._compensate_temperature(raw_temp)
268+
return self._compensate_humidity(raw_hum) / 1024.0
269+
270+
def read(self):
271+
"""Return (temperature_c, pressure_hpa, humidity_rh) tuple."""
272+
raw_temp, raw_press, raw_hum = self._read_raw()
273+
temp_c = self._compensate_temperature(raw_temp) / 100.0
274+
press_hpa = self._compensate_pressure(raw_press) / 25600.0
275+
hum_rh = self._compensate_humidity(raw_hum) / 1024.0
276+
return temp_c, press_hpa, hum_rh
277+
278+
def read_one_shot(self):
279+
"""Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh)."""
280+
self.trigger_one_shot()
281+
self._wait_measurement()
282+
return self.read()

tests/fake_machine/i2c.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class FakeI2C:
1414

1515
def __init__(self, bus_id=None, *, registers=None, address=None, **kwargs):
1616
self._registers = {}
17+
self._sequences = {}
1718
self._address = address
1819
self._write_log = []
1920
self._read_log = []
@@ -28,6 +29,12 @@ def __init__(self, bus_id=None, *, registers=None, address=None, **kwargs):
2829
def readfrom_mem(self, addr, reg, nbytes, *, addrsize=8):
2930
self._check_address(addr)
3031
self._read_log.append(reg)
32+
seq = self._sequences.get(reg)
33+
if seq:
34+
data = seq.pop(0)
35+
if not seq:
36+
del self._sequences[reg]
37+
return data[:nbytes]
3138
data = self._registers.get(reg, b"\x00" * nbytes)
3239
return data[:nbytes]
3340

@@ -74,6 +81,18 @@ def get_read_log(self):
7481
def clear_read_log(self):
7582
self._read_log.clear()
7683

84+
def set_register_sequence(self, reg, values):
85+
"""Set a sequence of values for a register.
86+
87+
Each read pops the next value from the list. When the list is
88+
exhausted, reads fall back to the static register value.
89+
90+
Args:
91+
reg: register address.
92+
values: list of bytes values to return on successive reads.
93+
"""
94+
self._sequences[reg] = [bytes(v) if not isinstance(v, bytes) else v for v in values]
95+
7796
def _check_address(self, addr):
7897
if self._address is not None and addr != self._address:
7998
raise OSError("I2C device not found at 0x{:02X}".format(addr))

tests/scenarios/bme280.yaml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ mock_registers:
1010
0xD0: 0x60
1111
# Status (not measuring, NVM copy done)
1212
0xF3: 0x00
13+
14+
# Data registers 0xF7..0xFE (8 bytes): raw_press=415148, raw_temp=519888, raw_hum=28680
15+
# Expected compensated: ~25.08°C, ~1009.21 hPa, ~50.57 %RH
16+
0xF7: [0x65, 0x5A, 0xC0, 0x7E, 0xED, 0x00, 0x70, 0x08]
1317
# ctrl_hum (default)
1418
0xF2: 0x00
1519
# ctrl_meas (default sleep mode)
@@ -157,3 +161,116 @@ tests:
157161
)
158162
expect_true: true
159163
mode: [mock]
164+
165+
- name: "wait_boot polls STATUS until IM_UPDATE clears"
166+
action: script
167+
script: |
168+
# Simulate NVM copy in progress: STATUS returns 0x01 twice, then 0x00
169+
i2c.set_register_sequence(0xF3, [bytes([0x01]), bytes([0x01]), bytes([0x00])])
170+
i2c.clear_read_log()
171+
from bme280 import BME280
172+
dev2 = BME280(i2c, address=0x76)
173+
log = i2c.get_read_log()
174+
# STATUS (0xF3) should have been read at least 3 times during boot
175+
status_reads = [r for r in log if r == 0xF3]
176+
result = len(status_reads) >= 3
177+
expect_true: true
178+
mode: [mock]
179+
180+
# ----- Status -----
181+
182+
- name: "data_ready returns True when not measuring"
183+
action: script
184+
script: |
185+
result = dev.data_ready()
186+
expect_true: true
187+
mode: [mock]
188+
189+
- name: "All ready methods delegate to data_ready"
190+
action: script
191+
script: |
192+
result = dev.temperature_ready() and dev.pressure_ready() and dev.humidity_ready()
193+
expect_true: true
194+
mode: [mock]
195+
196+
# ----- Compensated readings -----
197+
198+
- name: "Temperature returns correct value from raw data"
199+
action: script
200+
script: |
201+
temp = dev.temperature()
202+
result = abs(temp - 25.08) < 0.1
203+
expect_true: true
204+
mode: [mock]
205+
206+
- name: "Pressure returns correct value from raw data"
207+
action: script
208+
script: |
209+
press = dev.pressure_hpa()
210+
result = abs(press - 1009.21) < 0.5
211+
expect_true: true
212+
mode: [mock]
213+
214+
- name: "Humidity returns correct value from raw data"
215+
action: script
216+
script: |
217+
hum = dev.humidity()
218+
result = abs(hum - 50.57) < 0.5
219+
expect_true: true
220+
mode: [mock]
221+
222+
- name: "read() returns (temp, press, hum) tuple"
223+
action: script
224+
script: |
225+
t, p, h = dev.read()
226+
result = (
227+
abs(t - 25.08) < 0.1
228+
and abs(p - 1009.21) < 0.5
229+
and abs(h - 50.57) < 0.5
230+
)
231+
expect_true: true
232+
mode: [mock]
233+
234+
- name: "Humidity valid when t_fine equals 76800"
235+
action: script
236+
script: |
237+
# Regression: _compensate_humidity() must not return 0 when t_fine - 76800 == 0
238+
dev.t_fine = 76800
239+
raw_hum = 28680
240+
hum_q22 = dev._compensate_humidity(raw_hum)
241+
result = hum_q22 > 0
242+
expect_true: true
243+
mode: [mock]
244+
245+
- name: "t_fine updated after temperature read"
246+
action: script
247+
script: |
248+
dev.temperature()
249+
result = dev.t_fine == 128422
250+
expect_true: true
251+
mode: [mock]
252+
253+
- name: "trigger_one_shot writes forced mode to ctrl_meas"
254+
action: script
255+
script: |
256+
i2c.clear_write_log()
257+
dev.trigger_one_shot()
258+
log = i2c.get_write_log()
259+
wrote_forced = any(
260+
reg == 0xF4 and (data[0] & 0x03) == 0x01
261+
for reg, data in log
262+
)
263+
result = wrote_forced
264+
expect_true: true
265+
mode: [mock]
266+
267+
- name: "read_one_shot triggers and reads"
268+
action: script
269+
script: |
270+
i2c.clear_write_log()
271+
t, p, h = dev.read_one_shot()
272+
log = i2c.get_write_log()
273+
triggered = any(reg == 0xF4 for reg, data in log)
274+
result = triggered and abs(t - 25.08) < 0.1
275+
expect_true: true
276+
mode: [mock]

0 commit comments

Comments
 (0)