Standalone MicroPython scripts for Pimoroni Enviro boards. Replaces the stock enviro library with a unified entrypoint (main.py) that loads board-specific sensor code based on config.model. Keeps the same Pimoroni UF2 dependency for sensor drivers.
The generated sensor data is stored locally and uploaded periodically. Once uploaded, the JSON can be processed by ./data-vis in order to render some simple HTML charts.
| Board | Module | Sensors |
|---|---|---|
| Indoor | board_indoor.py |
BME688 (temp, humidity, pressure, gas/AQI), BH1745 (light, colour temperature) |
| Weather | board_weather.py |
BME280 (temp, humidity, pressure), LTR-559 (light), wind vane, anemometer, rain gauge |
| Urban | board_urban.py |
BME280 (temp, humidity, pressure), PMS5003I (PM1, PM2.5, PM10), MEMS microphone (noise) |
Set model in config.py to "indoor", "weather", or "urban".
Tested with version 0.2.0 of the Pimoroni firmware (likely to work fine with later versions).
Follow the guide here: https://github.com/pimoroni/enviro/blob/main/documentation/upgrading-firmware.md
Utilising the releases such as pimoroni-enviro-v1.22.2-micropython-enviro-v0.2.0.uf2.
Once the device has been flashed, connect to it via USB and remove all default files and folders, replacing them with all of the .py files from this repository. Modify the config.py as needed, then run main.py (main.py will also automatically be executed on boot).
Each script runs a loop:
- Clock sync — fetches time from
pool.ntp.orgvia NTP, writes to the PCF85063A RTC. Re-syncs everyresync_frequencyhours - Read sensors — takes a single reading from all onboard sensors. USB temperature compensation is applied when running on USB power
- Save locally — appends a CSV row to
readings/<date>.csv. Column headings stored once inreadings/columns.txt - Cache for upload — writes a JSON file to
uploads/containing the reading, device nickname, model, UID, current power mode (usb/batt), and current free disk percentage - Upload — when cached file count reaches
upload_frequency, connects to WiFi and POSTs each JSON toupload_url. Failed uploads are retried next cycle - Sleep — on battery: sets an RTC alarm and powers off (board re-powers on alarm). On USB:
time.sleep()until the next reading
- VSYS_EN latch — the power rail is latched (
Pin(2, Pin.OUT, value=True)) as the very first action on boot, before any imports or delays. On battery wake from an RTC alarm, a hold capacitor keeps the board alive only briefly — if the latch happens too late, the board powers off before the script can run - Dynamic
vbus_present— the USB power pin is read via a cachedPinobject at the start of every loop cycle, so the device switches between USB and battery behaviour dynamically — including correct sleep mode and temperature compensation - I2C bus reset — 16 SCL toggles run at startup to recover from stuck transactions after an unclean reset (e.g. USB disconnect mid-operation)
When free space drops below 10%, the script attempts to upload cached files. If uploads also fail, it thins the cache by removing every other JSON file — halving the count while preserving coverage across the full time range. Repeated thinning degrades resolution gracefully rather than losing a contiguous block of history.
Each cached/uploaded JSON payload includes:
nickname,model,uid,timestampreadings(sensor values)power_mode(usborbatt)free_space(current filesystem free space percentage)
Create a Cloudflare Worker using the content of worker.js, bind the Worker to an R2 bucket (setting the variable name enviro_r2 to the name of the bucket). The upload_url value in config.py should be the Worker's public URL (optionally, add HTTP auth to the Worker, and add the credentials to config.py).
All settings live in config.py:
| Setting | Description |
|---|---|
model |
Board type: "indoor", "weather", or "urban" |
wifi_ssid / wifi_password |
WiFi credentials |
wifi_country |
ISO 3166-1 alpha-2 code for regulatory domain (e.g. GB, US) |
nickname |
Device name included in upload payloads |
reading_frequency |
Minutes between readings (aligned to clock grid) |
upload_url |
HTTP endpoint for JSON uploads |
upload_frequency |
Number of cached readings before triggering upload |
http_username / http_password |
Optional HTTP Basic Auth |
resync_frequency |
Hours between NTP re-syncs |
usb_power_temperature_offset |
°C subtracted from temp when on USB power |
silent_mode |
True to disable all LEDs (activity + warning) |
| File | Purpose |
|---|---|
main.py |
Unified entrypoint — shared logic (power, WiFi, NTP, CSV, upload, sleep) |
helpers.py |
Stateless utilities (datetime, file ops, disk space) |
board_indoor.py |
Indoor sensor init + read |
board_weather.py |
Weather sensor init + read (incl. wind/rain) |
board_urban.py |
Urban sensor init + read (incl. PMS5003I, mic) |
config.py |
Device configuration (model, WiFi, upload, schedule) |
logging.py |
Minimal file + stdout logger with 4 KB rotation |
worker.js |
Cloudflare Worker that receives JSON payloads and stores them in R2 |
battery.md |
Battery life estimates |
- Pimoroni custom MicroPython UF2 (provides
breakout_bme68x,breakout_bme280,breakout_bh1745,breakout_ltr559,pimoroni_i2c,pcf85063a) - Pico W with Enviro Indoor, Weather, or Urban board
- Cloudflare Worker with an R2 binding