This repository contains the firmware for SAMBA v2, a low-cost indoor environmental quality (IEQ) monitor developed by the IEQ Lab at The University of Sydney. SAMBA runs on ESPHome, an open-source firmware framework for ESP32 microcontrollers. The minimum supported version of ESPHome is 2026.8.1.
ESPHome devices are configured using YAML. This repository is structured so that each sensor or function has its own config file in config/, which are imported as packages by the main samba.yaml.
├── samba.yaml # main config
├── config/
| ├── adc.yaml # analog-to-digital converter
| ├── airspeed.yaml # anemometers
| ├── calibration.yaml # calibration coefficients
| ├── co2.yaml # CO2 sensor
| ├── diagnostics.yaml # device diagnostics
| ├── esp32.yaml # ESP32 board and framework
| ├── fileserver.yaml # SD file server for SAMBA Home
| ├── globals.yaml # global variables
| ├── homeassistant.yaml # Home Assistant API
| ├── illuminance.yaml # illuminance sensor
| ├── influx.yaml # InfluxDB connection
| ├── led.yaml # status LED
| ├── ota.yaml # over-the-air updates
| ├── pm25.yaml # PM2.5 sensor
| ├── rtc.yaml # real-time clock
| ├── sample.yaml # sampling loop
| ├── sd.yaml # SD card logging
| ├── spl.yaml # sound pressure level
| ├── substitutions.yaml # secrets and substitutions
| ├── tags.yaml # InfluxDB location tags
| ├── tair.yaml # air temperature and RH
| ├── tglobe.yaml # globe temperature
| ├── tvoc.yaml # TVOC and NOx sensor
| └── wifi.yaml # wireless networking
├── components/ # custom ESPHome components
| ├── influxdb/ # InfluxDB v2 HTTP upload
| ├── sd_file_server/ # read-only HTTP access to the SD log
| ├── sd_spi_card/ # SPI SD card read/write
| ├── senseair_i2c/ # K30/K33 CO2 over I2C
| └── sound_level_meter/ # I2S audio DSP for SPL
├── firmware/ # compiled binaries for OTA
└── pcb/ # hardware design files (Altium)
The components/ directory contains five external components that extend ESPHome:
sound_level_meter— audio DSP for sound pressure level (LAeq, LA90, LA10)senseair_i2c— I2C driver for the K30 CO2 sensorinfluxdb— HTTP upload to an InfluxDB v2 bucketsd_spi_card— FAT32 SD card logging via SPIsd_file_server— read-only HTTP service over the SD card log for the SAMBA Home app
| Parameter | Sensor | Config | Component |
|---|---|---|---|
| Temperature / RH | Sensirion SHT40 | tair.yaml | sht4x |
| Globe Temperature | NTC Thermistor | tglobe.yaml | ntc |
| Air Speed | Thermal Anemometer | airspeed.yaml | ads1115 |
| CO2 | CO2Meter K30 | co2.yaml | senseair_i2c |
| PM2.5 | Plantower PMS5003T | pm25.yaml | pmsx003 |
| VOC / NOx Index | Sensirion SGP41 | tvoc.yaml | sgp4x |
| Illuminance | TI OPT3001 | illuminance.yaml | opt3001 |
| Sound Pressure Level | ICS-43434 Microphone | spl.yaml | sound_level_meter |
Most sensors are natively supported by ESPHome. The CO2 sensor and sound pressure level measurement use custom external components in components/.
The remote board has two thermal anemometer tips, one on each face. The firmware numbers them the other way round from the schematic, so identify a tip by the face it is on:
| Face | Tip | Schematic sheet | Firmware |
|---|---|---|---|
| RJ45 connector | R9 | Airspeed 1 | as2 (ADS1115 A1) |
| Opposite face | R27 | Airspeed 2 | as1 (ADS1115 A0) |
The published Air Speed is the larger of the two, or the one still reporting if the other has dropped out.
SAMBA continuously measures environmental parameters, applies quality filters and a moving median, then publishes a summary every 5 minutes. The routine is:
- Each sensor measures at its own frequency (see table below).
- Raw readings pass through filters — NaN rejection, clamping, moving median — and calibration functions.
- Every 5 minutes, a cron task on the RTC triggers the
sensor_samplescript. - The script updates all template sensors, then publishes to Home Assistant, InfluxDB, and/or the SD card. The LED flashes white with each upload.
- Publishing is skipped during the first 2 minutes after boot (sensor warm-up).
| Measure | Frequency | Filters |
|---|---|---|
| Air Temperature | 30s | clamp; moving median; linear calibration |
| Relative Humidity | 30s | clamp; moving median; linear calibration |
| Globe Temperature | 30s | clamp; moving median; linear calibration |
| Air Speed | 2s | clamp; moving median; exponential calibration with air temperature |
| CO2 | 30s | filter; clamp; moving median; linear calibration; clamp |
| PM2.5 | ~1s | clamp; moving median |
| VOC Index | 30s | moving median |
| NOx Index | 30s | moving median |
| Illuminance | 20s | clamp; moving median; linear calibration; clamp |
| Sound Pressure Level | 125ms | sos (mic EQ, A-weighting); sliding 5min window of 125ms levels, sent each minute: energy average (LAeq), 0.1dB histogram quantiles (LA90, LA10) |
Published measurements are sent every 5 minutes to one or more of the following backends:
Home Assistant — An open-source home automation platform. Easy to use but requires additional hardware (e.g. Raspberry Pi) and some configuration to retain raw data beyond 10 days. Best suited for projects that also collect other measurements (e.g. energy, window/door state). Communication uses the native ESPHome API component.
InfluxDB — An open-source time series database optimised for IoT. Can be self-hosted or used via InfluxData's cloud service. Requires an active internet connection. Best suited for field deployments of multiple SAMBAs. Communication uses the custom influxdb component with building, level, and zone IDs as tags (set over the native API, see config/tags.yaml). See the InfluxDB key concepts for background.
SD Card — Local CSV logging via the sd_spi_card component. Files are named using the device MAC address and UTC timestamp. No network connection required.
A single RGB LED on the board reports device state. One rule covers the whole scheme: colour identifies the subsystem, and pulsing versus solid identifies severity. A slow pulse is a warning the device expects to recover from on its own; a steady light means the fault is sustained. Only the ADS1115 and K30 escalate to a restart, and only after an hour of uptime. Brightness is deliberately low throughout so a rack of units is not distracting in an occupied office.
| LED | Meaning | What to do |
|---|---|---|
| Green, flashing | Booting | Nothing — clears after startup |
| White, brief flash | 5-minute sample taken and uploaded | Nothing — this is the healthy heartbeat |
| Off | Running normally | Nothing |
| Amber, pulsing | Globe temperature / air speed unresponsive ~3 min | Watch — often transient |
| Amber, solid | Same, ~5 min. Restarts once the fault has covered 35% of the last hour (about 25 min continuous) | Check the RJ45 cable to the remote board, then reseat both ends |
| Blue, pulsing | CO2 (K30) failed 3 consecutive reads | Watch — usually self-recovers |
| Blue, solid | CO2 failed 4 or more; restarts once it has failed 67% of the last hour (about 65 min of near-total failure) | Check the K30; a persistent fault is often board-specific rather than the sensor |
| Magenta, pulsing | VOC / NOx (SGP4x) failed 4 consecutive reads | Watch — usually self-recovers |
| Magenta, solid | VOC / NOx failed 6 or more. Never restarts: the sensor keeps retrying itself, and a reboot would discard its month-long baseline | Check the sensor |
The LED is applied from a single 10-second poll of the error counters rather than from each sensor's error handler, so it survives the sample heartbeat and returns to off within about ten seconds of the sensor recovering. If two subsystems are in trouble at once, the more severe one shows, and the ADS1115 wins a tie.
A lit LED does not mean the device has stopped working. A failed sensor is reported as
nan and left out of the upload, while every other measurement continues to be sampled and
sent. Restarts are rate-limited to at most one per hour precisely so that one dead sensor
cannot take the whole unit off the air.
Every sensor drops out the same way: each stamps the time of its last successful read, and a
measurand goes nan once that stamp is more than 5.5 minutes old — one sample interval plus
margin. A single failed read never costs a reading, because the sensors poll far faster than
they are sampled; it takes eleven consecutive failures for temperature, humidity, globe or
illuminance, and 165 for air speed. What this does not catch is a sensor that keeps
answering with a stuck value.
Amber is the one worth attention in the field: the remote board connects over an RJ45 lead, and a marginal cable is the most common cause of it.
Users are free to modify the SAMBA firmware to suit their needs. We recommend familiarity with ESPHome and microcontroller programming before doing so. To get started:
- Copy
secrets.yaml.exampletosecrets.yamland set your InfluxDB server. - Modify the relevant
.yamlfiles inconfig/. - Compile and upload via USB-C with
esphome run samba.yaml, or wirelessly withesphome run samba.yaml --device <IP_ADDRESS>.
Calibration coefficients are stored as persistent global variables and exposed as number entities over the native API (config/calibration.yaml), so they are set on the device rather than compiled in.
The user is responsible for managing any device running modified firmware.
SAMBA devices check for firmware updates every Monday at 04:00 UTC by comparing against firmware/manifest_v2.json. If a new version is available, the update is applied automatically after a random delay of up to 10 minutes to avoid fleet-wide simultaneous downloads. Units still on 1.x read firmware/manifest.json and never update to 2.x on their own; they move to 2.0 only when the IEQ Lab reflashes them. Automatic updates can be disabled with the Automatic Updates switch (see Deployment).
SAMBA devices are shipped pre-calibrated, with their location tags (building, level, zone) and calibration coefficients already set by the IEQ Lab. Follow these steps to connect a new SAMBA to your network and start sampling:
- Power on the SAMBA. The status LED blinks green while it boots. This firmware has no separate setup colour, so a unit still waiting for a network looks the same as one that is sampling.
- Connect to the hotspot. Using a phone or laptop, join the SAMBA's own open WiFi network — it is named after the device,
samba-xxxxxx, wherexxxxxxis the last three bytes of its MAC address — and open the captive portal at http://192.168.4.1. There is no password on the hotspot. - Enter WiFi credentials. Select the target 2.4 GHz network from the list and enter the password. The SAMBA will connect and begin sampling automatically. The LED will blink green during the warm-up period and then turn off once it enters the normal sampling routine.
Switch it on in the room it will monitor. The VOC sensor's index is relative: it learns a baseline from the air it sees in its first 45 minutes and then holds that baseline for about a month, so the first hour sets the scale every later TVOC reading is reported against. Boot the unit in its final position under normal conditions — not on a bench in another room, and not somewhere unusually clean or unusually solvent-heavy. If a unit's first hour was unrepresentative, power-cycling it within three hours discards the baseline and it learns again from scratch; after three hours only a firmware update clears it. This also means TVOC may step to a new baseline after an automatic update. Nothing else in the unit depends on where it is first switched on.
The location tags, the InfluxDB Upload / SD Card Write / Automatic Updates switches and the calibration coefficients are exposed over the ESPHome native API, so they can be viewed and changed from Home Assistant or with the IEQ Lab's samba laptop client from samba_calibration (its Identify SAMBA button blinks the LED to pick one unit out of a batch). If you need a SAMBA recalibrated or re-tagged, please reach out — see Project Maintenance below.
The API is encrypted with a key that is set on the device by the IEQ Lab during deployment rather than compiled into the firmware, so the published binaries carry no key; until one is set, the device accepts unencrypted connections so it can be provisioned. The same is true of the InfluxDB token and the OTA password (see config/influx.yaml and config/ota.yaml): each is stored on the device, survives updates, and the device only ever reports a fingerprint of it (InfluxDB Token, OTA Password) together with the result of its last upload (InfluxDB Status). The published firmware has no compiled-in fallbacks: a unit that has not been provisioned does not upload to InfluxDB and takes OTA updates without a password, so provision it before it leaves a network you trust. To upload to your own InfluxDB instead, set token: in config/influx.yaml in your build; a provisioned token still takes precedence. A unit going into a home gets a fourth credential, a pairing password that turns on a read-only HTTP service over the SD card log for the SAMBA Home app (see config/fileserver.yaml and docs/home-sync.md); without one the service is off and the unit behaves exactly as a building unit.
This is an active project to build an open research platform for healthy, high-performance buildings. If you're interested in using SAMBA in your project or contributing to its development, start a Github discussion or email Tom.