From 8cddc220e3165f7c71bfdadbbfebc9ff04eca3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 13:25:08 +0200 Subject: [PATCH 1/4] docs(bme280): Add README, examples, and hardware test scenarios. --- README.md | 2 +- lib/bme280/README.md | 281 +++++++++++++++++++++++++ lib/bme280/examples/basic_reader.py | 24 +++ lib/bme280/examples/weather_station.py | 57 +++++ tests/scenarios/bme280.yaml | 51 +++++ 5 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 lib/bme280/README.md create mode 100644 lib/bme280/examples/basic_reader.py create mode 100644 lib/bme280/examples/weather_station.py diff --git a/README.md b/README.md index 6113ceb2..7f61bdf3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This repository contains all the drivers for the main components of the [STeaMi] | ISM330DL | [`ism330dl`](lib/ism330dl/README.md) | `0x6B` | 6-axis IMU (accel + gyro) | | LIS2MDL | [`lis2mdl`](lib/lis2mdl/README.md) | `0x1E` | 3-axis magnetometer | | IM34DT05 | `im34dt05` *(not yet implemented)* | — (PDM) | Digital microphone | -| BME280 | `bme280` *(not yet implemented)* | `0x76` | Pressure + humidity + temperature | +| BME280 | [`bme280`](lib/bme280/README.md) | `0x76` | Pressure + humidity + temperature | | GC9A01 | `gc9a01` *(not yet implemented)* | — (SPI) | Round color LCD display | | STeaMi Config | [`steami_config`](lib/steami_config/README.md) | — | Persistent board configuration | diff --git a/lib/bme280/README.md b/lib/bme280/README.md new file mode 100644 index 00000000..114f6381 --- /dev/null +++ b/lib/bme280/README.md @@ -0,0 +1,281 @@ +# BME280 MicroPython Driver + +MicroPython driver for the **Bosch BME280** combined pressure, humidity, and temperature sensor. + +This driver provides a simple API to read **pressure**, **humidity**, and **temperature** over **I2C**. + +The BME280 is a high-precision environmental sensor suitable for applications such as: + +* weather monitoring +* indoor air quality +* altimetry support +* environmental sensing + +--- + +# Features + +* I2C communication +* device identification +* pressure measurement (hPa) +* temperature measurement (C) +* relative humidity measurement (%RH) +* one-shot acquisition (forced mode) +* continuous measurement mode (normal mode) +* configurable oversampling (temperature, pressure, humidity) +* configurable IIR filter +* configurable standby time +* data-ready status helpers +* sleep mode (power off) +* soft reset and full reset with recalibration + +--- + +# Sensor Overview + +| Feature | Value | +| ---------------------- | ----------------------- | +| Pressure range | 300 hPa - 1100 hPa | +| Pressure resolution | 0.18 Pa (20-bit ADC) | +| Temperature range | -40 C to +85 C | +| Temperature resolution | 0.01 C | +| Humidity range | 0 - 100 %RH | +| Humidity resolution | 0.008 %RH (16-bit ADC) | +| Interface | I2C / SPI | +| Chip ID | 0x60 | + +--- + +# I2C Address + +The sensor can use two I2C addresses depending on the **SDO pin**: + +| SDO | Address | +| ------ | ------- | +| GND | `0x76` | +| VDDIO | `0x77` | + +The default address used by the driver is **0x76**. + +--- + +# Basic Usage + +```python +from machine import I2C +from time import sleep +from bme280 import BME280 + +i2c = I2C(1) + +sensor = BME280(i2c) + +while True: + temperature, pressure, humidity = sensor.read() + + print("T:", temperature, "C") + print("P:", pressure, "hPa") + print("H:", humidity, "%RH") + print() + + sleep(1) +``` + +--- + +# API Reference + +## Initialization + +```python +sensor = BME280(i2c) +``` + +Optional custom address: + +```python +sensor = BME280(i2c, address=0x77) +``` + +The constructor verifies the chip ID, waits for NVM calibration data to be ready, reads factory trimming parameters, and applies a default configuration (1x oversampling, sleep mode). + +--- + +## Measurements + +### Read all channels + +```python +temperature, pressure, humidity = sensor.read() +``` + +Returns a tuple of `(temperature_c, pressure_hpa, humidity_rh)`. + +--- + +### Temperature + +```python +sensor.temperature() +``` + +Returns the temperature in **degrees Celsius**. + +--- + +### Pressure + +```python +sensor.pressure_hpa() +``` + +Returns the pressure in **hPa**. + +--- + +### Humidity + +```python +sensor.humidity() +``` + +Returns the relative humidity in **%RH**. + +--- + +### One-shot measurement + +```python +temperature, pressure, humidity = sensor.read_one_shot() +``` + +Triggers a forced measurement, waits for completion, and returns all three channels. + +--- + +### Trigger forced measurement + +```python +sensor.trigger_one_shot() +``` + +Triggers a single forced measurement. Poll `data_ready()` for completion, then read values with `temperature()`, `pressure_hpa()`, `humidity()`, or `read()`. + +--- + +## Data-Ready Status + +```python +sensor.data_ready() # True when all channels are ready +sensor.temperature_ready() # True when temperature is ready +sensor.pressure_ready() # True when pressure is ready +sensor.humidity_ready() # True when humidity is ready +``` + +--- + +## Configuration + +### Oversampling + +```python +from bme280.const import OSRS_X2, OSRS_X4, OSRS_X16 + +sensor.set_oversampling(temperature=OSRS_X2, pressure=OSRS_X16, humidity=OSRS_X4) +``` + +Available constants: `OSRS_SKIP`, `OSRS_X1`, `OSRS_X2`, `OSRS_X4`, `OSRS_X8`, `OSRS_X16`. + +Pass `None` to keep the current setting for a channel. + +--- + +### IIR Filter + +```python +from bme280.const import FILTER_4 + +sensor.set_iir_filter(FILTER_4) +``` + +Available constants: `FILTER_OFF`, `FILTER_2`, `FILTER_4`, `FILTER_8`, `FILTER_16`. + +--- + +### Standby Time + +```python +from bme280.const import STANDBY_500_MS + +sensor.set_standby(STANDBY_500_MS) +``` + +Available constants: `STANDBY_0_5_MS`, `STANDBY_62_5_MS`, `STANDBY_125_MS`, `STANDBY_250_MS`, `STANDBY_500_MS`, `STANDBY_1000_MS`. + +--- + +## Modes + +### Sleep mode + +```python +sensor.power_off() # enter sleep mode, stop measurements +sensor.power_on() # enter normal mode, continuous measurements +``` + +--- + +### Continuous mode + +```python +from bme280.const import STANDBY_125_MS + +sensor.set_continuous(standby=STANDBY_125_MS) +``` + +Enters normal mode with the specified standby time between measurements. If `standby` is `None`, the current standby setting is kept. + +--- + +## Device Management + +### Device ID + +```python +sensor.device_id() # returns 0x60 +``` + +--- + +### Soft Reset + +```python +sensor.soft_reset() +``` + +Sends the reset command and waits for NVM reload. + +--- + +### Full Reset + +```python +sensor.reset() +``` + +Performs a soft reset, re-reads calibration data, and re-applies default configuration. + +--- + +# Examples + +| Example | Description | +| -------------------- | -------------------------------------------------- | +| `basic_reader.py` | Read temperature, pressure, and humidity | +| `weather_station.py` | Continuous logging with altitude computation | + +--- + +# References + +* [BME280 Datasheet](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf) diff --git a/lib/bme280/examples/basic_reader.py b/lib/bme280/examples/basic_reader.py new file mode 100644 index 00000000..13566f3c --- /dev/null +++ b/lib/bme280/examples/basic_reader.py @@ -0,0 +1,24 @@ +from time import sleep + +from bme280 import BME280 +from machine import I2C + +# Update the I2C bus number and pins to match your board +i2c = I2C(1) + +# Create the sensor object +sensor = BME280(i2c) + +print("BME280 found") +print("Device ID:", hex(sensor.device_id())) + +for _ in range(10): + temperature, pressure, humidity = sensor.read() + + print( + "T: {:.1f} C P: {:.1f} hPa H: {:.1f} %RH".format( + temperature, pressure, humidity + ) + ) + + sleep(1) diff --git a/lib/bme280/examples/weather_station.py b/lib/bme280/examples/weather_station.py new file mode 100644 index 00000000..acfb7aa9 --- /dev/null +++ b/lib/bme280/examples/weather_station.py @@ -0,0 +1,57 @@ +"""Continuous weather monitoring with altitude estimation. + +Reads temperature, pressure, and humidity every 5 seconds in normal mode, +computes approximate altitude from pressure using the barometric formula, +and logs each measurement to the DAPLink flash as CSV. +""" + +from time import sleep, sleep_ms + +from bme280 import BME280 +from bme280.const import FILTER_16, OSRS_X2, OSRS_X16, STANDBY_1000_MS +from daplink_bridge import DaplinkBridge +from daplink_flash import DaplinkFlash +from machine import I2C + +# Sea-level reference pressure in hPa (adjust to local conditions) +SEA_LEVEL_PRESSURE = 1013.25 + +i2c = I2C(1) + +sensor = BME280(i2c) +bridge = DaplinkBridge(i2c) +flash = DaplinkFlash(bridge) + +# Configure for weather monitoring: high pressure resolution, moderate temp/hum +sensor.set_oversampling(temperature=OSRS_X2, pressure=OSRS_X16, humidity=OSRS_X2) +sensor.set_iir_filter(FILTER_16) +sensor.set_continuous(standby=STANDBY_1000_MS) + +# Prepare flash logging +flash.set_filename("WEATHER", "CSV") +flash.clear_flash() +sleep_ms(500) +print("Flash erased.") + +flash.write_line("temperature;pressure;humidity;altitude") + + +def altitude_m(pressure_hpa): + """Estimate altitude in meters from pressure using the barometric formula.""" + return 44330.0 * (1.0 - (pressure_hpa / SEA_LEVEL_PRESSURE) ** 0.1903) + + +while True: + temperature, pressure, humidity = sensor.read() + alt = altitude_m(pressure) + + print( + "T: {:.1f} C P: {:.1f} hPa H: {:.1f} %RH Alt: {:.0f} m".format( + temperature, pressure, humidity, alt + ) + ) + flash.write_line( + "{:.1f};{:.1f};{:.1f};{:.0f}".format(temperature, pressure, humidity, alt) + ) + + sleep(5) diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index 10e7361c..5f617cbc 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -416,3 +416,54 @@ tests: result = standby == STANDBY_500_MS and filt == FILTER_4 expect_true: true mode: [mock] + + # ----- Hardware tests ----- + + - name: "Temperature in plausible range" + action: call + method: temperature + expect_range: [10.0, 45.0] + mode: [hardware] + + - name: "Pressure in plausible range" + action: call + method: pressure_hpa + expect_range: [900.0, 1100.0] + mode: [hardware] + + - name: "Humidity in plausible range" + action: call + method: humidity + expect_range: [5.0, 95.0] + mode: [hardware] + + - name: "read() returns three plausible values" + action: script + script: | + t, p, h = dev.read() + result = 10.0 <= t <= 45.0 and 900.0 <= p <= 1100.0 and 5.0 <= h <= 95.0 + expect_true: true + mode: [hardware] + + - name: "read_one_shot returns three plausible values" + action: script + script: | + t, p, h = dev.read_one_shot() + result = 10.0 <= t <= 45.0 and 900.0 <= p <= 1100.0 and 5.0 <= h <= 95.0 + expect_true: true + mode: [hardware] + + - name: "Readings feel correct" + action: manual + display: + - method: temperature + label: "Temperature" + unit: "C" + - method: pressure_hpa + label: "Pressure" + unit: "hPa" + - method: humidity + label: "Humidity" + unit: "%RH" + prompt: "Do the values look reasonable?" + mode: [hardware] From b660b06aff1d97fe5c20b15cd64716ab006922e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 13:33:06 +0200 Subject: [PATCH 2/4] fix(bme280): Address Copilot review comments on PR 314. --- lib/bme280/README.md | 16 +++++----- lib/bme280/examples/basic_reader.py | 2 +- tests/scenarios/bme280.yaml | 48 ++++++++++++++++------------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index 114f6381..6be21d7f 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -13,7 +13,7 @@ The BME280 is a high-precision environmental sensor suitable for applications su --- -# Features +## Features * I2C communication * device identification @@ -31,7 +31,7 @@ The BME280 is a high-precision environmental sensor suitable for applications su --- -# Sensor Overview +## Sensor Overview | Feature | Value | | ---------------------- | ----------------------- | @@ -46,7 +46,7 @@ The BME280 is a high-precision environmental sensor suitable for applications su --- -# I2C Address +## I2C Address The sensor can use two I2C addresses depending on the **SDO pin**: @@ -59,7 +59,7 @@ The default address used by the driver is **0x76**. --- -# Basic Usage +## Basic Usage ```python from machine import I2C @@ -71,7 +71,7 @@ i2c = I2C(1) sensor = BME280(i2c) while True: - temperature, pressure, humidity = sensor.read() + temperature, pressure, humidity = sensor.read_one_shot() print("T:", temperature, "C") print("P:", pressure, "hPa") @@ -83,7 +83,7 @@ while True: --- -# API Reference +## API Reference ## Initialization @@ -267,7 +267,7 @@ Performs a soft reset, re-reads calibration data, and re-applies default configu --- -# Examples +## Examples | Example | Description | | -------------------- | -------------------------------------------------- | @@ -276,6 +276,6 @@ Performs a soft reset, re-reads calibration data, and re-applies default configu --- -# References +## References * [BME280 Datasheet](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf) diff --git a/lib/bme280/examples/basic_reader.py b/lib/bme280/examples/basic_reader.py index 13566f3c..33964615 100644 --- a/lib/bme280/examples/basic_reader.py +++ b/lib/bme280/examples/basic_reader.py @@ -13,7 +13,7 @@ print("Device ID:", hex(sensor.device_id())) for _ in range(10): - temperature, pressure, humidity = sensor.read() + temperature, pressure, humidity = sensor.read_one_shot() print( "T: {:.1f} C P: {:.1f} hPa H: {:.1f} %RH".format( diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index 5f617cbc..bce19124 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -420,35 +420,45 @@ tests: # ----- Hardware tests ----- - name: "Temperature in plausible range" - action: call - method: temperature - expect_range: [10.0, 45.0] + action: script + script: | + t, _, _ = dev.read_one_shot() + result = 10.0 <= t <= 45.0 + expect_true: true mode: [hardware] - name: "Pressure in plausible range" - action: call - method: pressure_hpa - expect_range: [900.0, 1100.0] + action: script + script: | + _, p, _ = dev.read_one_shot() + result = 900.0 <= p <= 1100.0 + expect_true: true mode: [hardware] - name: "Humidity in plausible range" - action: call - method: humidity - expect_range: [5.0, 95.0] + action: script + script: | + _, _, h = dev.read_one_shot() + result = 5.0 <= h <= 95.0 + expect_true: true mode: [hardware] - - name: "read() returns three plausible values" + - name: "read_one_shot returns three plausible values" action: script script: | - t, p, h = dev.read() + t, p, h = dev.read_one_shot() result = 10.0 <= t <= 45.0 and 900.0 <= p <= 1100.0 and 5.0 <= h <= 95.0 expect_true: true mode: [hardware] - - name: "read_one_shot returns three plausible values" + - name: "Continuous mode produces plausible values" action: script script: | - t, p, h = dev.read_one_shot() + from time import sleep_ms + dev.set_continuous() + sleep_ms(100) + t, p, h = dev.read() + dev.power_off() result = 10.0 <= t <= 45.0 and 900.0 <= p <= 1100.0 and 5.0 <= h <= 95.0 expect_true: true mode: [hardware] @@ -456,14 +466,8 @@ tests: - name: "Readings feel correct" action: manual display: - - method: temperature - label: "Temperature" - unit: "C" - - method: pressure_hpa - label: "Pressure" - unit: "hPa" - - method: humidity - label: "Humidity" - unit: "%RH" + - method: read_one_shot + label: "T, P, H" prompt: "Do the values look reasonable?" + expect_true: true mode: [hardware] From 755f08c777499da1424058a3f5edd273fe9ce78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 13:42:50 +0200 Subject: [PATCH 3/4] docs(bme280): Add feature comparison with other MicroPython drivers. --- lib/bme280/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index 6be21d7f..d5f9b99d 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -276,6 +276,45 @@ Performs a soft reset, re-reads calibration data, and re-applies default configu --- +## Comparison with other MicroPython BME280 drivers + +| Feature | **STeaMi** | **robert-hh** | **Adafruit** | **neliogodoi** | **Pimoroni** | **RandomNerd** | +|---|---|---|---|---|---|---| +| I2C | Yes | Yes | Yes | Yes | Yes | Yes | +| SPI | No | No | Yes | No | No | No | +| Sleep mode | Yes | Yes | Yes | No | No | No | +| Forced mode | Yes | Yes | Yes | Implicit | No | No | +| Normal mode | Yes | Yes | Yes | No | Fixed | No | +| Oversampling (per channel) | Yes | Yes | Yes | Yes | Fixed x16 | Constants only | +| IIR filter | Yes | No | Yes | Yes | Fixed x16 | No | +| Standby time | Yes | No | Yes | No | Fixed 500ms | No | +| Altitude | No | Yes | Yes | Yes | Yes | No | +| Sea-level pressure | No | Yes | Yes | Yes | Yes | No | +| Dew point | No | Yes | No | No | No | No | +| Soft reset | Yes | No | No | No | Yes | No | +| Full reset + recalibration | Yes | No | No | No | No | No | +| power_off / power_on | Yes | No | Via mode | No | No | No | +| data_ready | Yes | No | No | No | No | No | +| read_one_shot | Yes | No | No | No | No | No | +| set_continuous | Yes | No | Via mode | No | No | No | +| Integer compensation | Yes | Yes | No | No | No | Yes | +| Measurement time estimate | No | No | Yes | No | No | No | +| Multi-unit temperature | No | No | No | C/F/K | No | C only | +| BMP280 compatibility | No | No | No | No | Yes | No | +| Dedicated exceptions | Yes | No | No | No | No | No | +| Mock test suite | Yes (39) | No | No | No | No | No | +| Hardware test suite | Yes (6) | No | No | No | No | No | + +Reference implementations: + +* [robert-hh/BME280](https://github.com/robert-hh/BME280) — integer compensation, altitude, dew point +* [Adafruit CircuitPython BME280](https://github.com/adafruit/Adafruit_CircuitPython_BME280) — I2C + SPI, basic/advanced split +* [neliogodoi/MicroPython-BME280](https://github.com/neliogodoi/MicroPython-BME280) — configurable oversampling and IIR +* [Pimoroni envirobit](https://github.com/pimoroni/micropython-envirobit) — Micro:bit driver, BMP280 alias +* [RandomNerdTutorials](https://randomnerdtutorials.com/micropython-bme280-esp32-esp8266/) — ESP32/ESP8266 tutorial + +--- + ## References * [BME280 Datasheet](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf) From 64d0ca1e6298f5a1ac736f4c7cee9131950da4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 13:47:15 +0200 Subject: [PATCH 4/4] docs(bme280): Add emoji to comparison table for readability. --- lib/bme280/README.md | 48 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index d5f9b99d..ab85c9be 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -280,30 +280,30 @@ Performs a soft reset, re-reads calibration data, and re-applies default configu | Feature | **STeaMi** | **robert-hh** | **Adafruit** | **neliogodoi** | **Pimoroni** | **RandomNerd** | |---|---|---|---|---|---|---| -| I2C | Yes | Yes | Yes | Yes | Yes | Yes | -| SPI | No | No | Yes | No | No | No | -| Sleep mode | Yes | Yes | Yes | No | No | No | -| Forced mode | Yes | Yes | Yes | Implicit | No | No | -| Normal mode | Yes | Yes | Yes | No | Fixed | No | -| Oversampling (per channel) | Yes | Yes | Yes | Yes | Fixed x16 | Constants only | -| IIR filter | Yes | No | Yes | Yes | Fixed x16 | No | -| Standby time | Yes | No | Yes | No | Fixed 500ms | No | -| Altitude | No | Yes | Yes | Yes | Yes | No | -| Sea-level pressure | No | Yes | Yes | Yes | Yes | No | -| Dew point | No | Yes | No | No | No | No | -| Soft reset | Yes | No | No | No | Yes | No | -| Full reset + recalibration | Yes | No | No | No | No | No | -| power_off / power_on | Yes | No | Via mode | No | No | No | -| data_ready | Yes | No | No | No | No | No | -| read_one_shot | Yes | No | No | No | No | No | -| set_continuous | Yes | No | Via mode | No | No | No | -| Integer compensation | Yes | Yes | No | No | No | Yes | -| Measurement time estimate | No | No | Yes | No | No | No | -| Multi-unit temperature | No | No | No | C/F/K | No | C only | -| BMP280 compatibility | No | No | No | No | Yes | No | -| Dedicated exceptions | Yes | No | No | No | No | No | -| Mock test suite | Yes (39) | No | No | No | No | No | -| Hardware test suite | Yes (6) | No | No | No | No | No | +| I2C | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| SPI | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| Sleep mode | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| Forced mode | ✅ | ✅ | ✅ | ⚠️ Implicit | ❌ | ❌ | +| Normal mode | ✅ | ✅ | ✅ | ❌ | ⚠️ Fixed | ❌ | +| Oversampling (per channel) | ✅ | ✅ | ✅ | ✅ | ⚠️ Fixed x16 | ⚠️ Constants only | +| IIR filter | ✅ | ❌ | ✅ | ✅ | ⚠️ Fixed x16 | ❌ | +| Standby time | ✅ | ❌ | ✅ | ❌ | ⚠️ Fixed 500ms | ❌ | +| Altitude | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Sea-level pressure | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Dew point | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Soft reset | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| Full reset + recalibration | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| power_off / power_on | ✅ | ❌ | ⚠️ Via mode | ❌ | ❌ | ❌ | +| data_ready | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| read_one_shot | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| set_continuous | ✅ | ❌ | ⚠️ Via mode | ❌ | ❌ | ❌ | +| Integer compensation | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| Measurement time estimate | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| Multi-unit temperature | ❌ | ❌ | ❌ | ✅ C/F/K | ❌ | ❌ | +| BMP280 compatibility | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| Dedicated exceptions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Mock test suite | ✅ (39) | ❌ | ❌ | ❌ | ❌ | ❌ | +| Hardware test suite | ✅ (6) | ❌ | ❌ | ❌ | ❌ | ❌ | Reference implementations: