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..ab85c9be --- /dev/null +++ b/lib/bme280/README.md @@ -0,0 +1,320 @@ +# 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_one_shot() + + 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 | + +--- + +## Comparison with other MicroPython BME280 drivers + +| Feature | **STeaMi** | **robert-hh** | **Adafruit** | **neliogodoi** | **Pimoroni** | **RandomNerd** | +|---|---|---|---|---|---|---| +| 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: + +* [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) diff --git a/lib/bme280/examples/basic_reader.py b/lib/bme280/examples/basic_reader.py new file mode 100644 index 00000000..33964615 --- /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_one_shot() + + 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..bce19124 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -416,3 +416,58 @@ tests: result = standby == STANDBY_500_MS and filt == FILTER_4 expect_true: true mode: [mock] + + # ----- Hardware tests ----- + + - name: "Temperature in plausible range" + 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: script + script: | + _, p, _ = dev.read_one_shot() + result = 900.0 <= p <= 1100.0 + expect_true: true + mode: [hardware] + + - name: "Humidity in plausible range" + action: script + script: | + _, _, h = dev.read_one_shot() + result = 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: "Continuous mode produces plausible values" + action: script + script: | + 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] + + - name: "Readings feel correct" + action: manual + display: + - method: read_one_shot + label: "T, P, H" + prompt: "Do the values look reasonable?" + expect_true: true + mode: [hardware]