This project is an air quality monitoring system that measures particulate matter (PM2.5 and PM10) using an SDS011 sensor, displays the results on an LCD, transmits the data via an ESP32 microcontroller, and provides real-time monitoring and notifications through a modern Android application.
Android app for monitoring particulate matter (PM2.5 and PM10) from an ESP32 web server. The app periodically fetches readings, displays the latest values and history, and sends notifications when thresholds are exceeded. Built with Kotlin, Jetpack Compose, Retrofit, and Koin.
- Foreground service that periodically polls the ESP32 JSON endpoint
- Notifications when PM thresholds are exceeded
- Editable thresholds and monitoring interval
- Persisted history of successful/failed fetches
- Modern Compose UI
- Android Studio (latest)
- Android device with API 26+ (Oreo) or emulator
- ESP32 running the companion firmware that serves
http://<ESP32_IP>/jsonwith payload:{ "pm2.5": <float>, "pm10": <float> }
- Open this module in Android Studio:
- File → Open → select the
AirQualityMonitorAndroidAppdirectory
- File → Open → select the
- Configure the ESP32 base URL:
- Edit
app/src/main/java/com/mutkuensert/airqualitymonitor/Module.kt - Update
baseUrl("http://192.168.0.184")to your ESP32 IP
- Edit
- Build & Run on a device:
- Connect a device on the same Wi‑Fi network as ESP32
- Run the
appconfiguration - Grant the notification permission when prompted
AirQualityMonitorServiceruns as a foreground service and loops every N seconds (SharedPreferences-driven) to fetch data from ESP32.AirQualityRepositoryperforms the network call viaAirQualityService(Retrofit) and stores a JSON-serialized history of results/failures in SharedPreferences.AirQualityStateManagerexposes a StateFlow consumed byMainViewModelto update UI.AirQualityNotificationposts high-priority alerts when thresholds are exceeded.- Dependencies are provided via Koin in
koinModule.
- Threshold defaults:
- PM2.5:
THRESHOLD_PM_25_DEFAULT = 10 - PM10:
THRESHOLD_PM_10_DEFAULT = 20
- PM2.5:
- Monitoring interval default:
MONITORING_INTERVAL_SECONDS_DEFAULT = 30 - Runtime changes are saved immediately via the UI text fields.
Edit locations:
- Base URL:
app/src/main/java/com/mutkuensert/airqualitymonitor/Module.kt - Defaults/keys:
app/src/main/java/com/mutkuensert/airqualitymonitor/data/AirQualityRepository.kt
Declared in AndroidManifest.xml:
INTERNETPOST_NOTIFICATIONSFOREGROUND_SERVICEFOREGROUND_SERVICE_DATA_SYNCWAKE_LOCK
The app requests the notification permission at runtime on first launch.
- minSdk 26, target/compileSdk 35
- Kotlin 2.0.21, AGP 8.10.1
- Major libraries: Compose BOM 2025.05.01, Retrofit 2.11.0, OkHttp 4.12.0, Koin 4.0.2, kotlinx-serialization 1.8.0, Timber 5.0.1, Chucker 4.1.0
- Endpoint:
GET http://<ESP32_IP>/json - Response:
{ "pm2.5": 12.3, "pm10": 25.7 } - See
AirQualityService.ktandAirQualityResponse.ktfor details.
- Main screen shows current datetime, latest PM2.5/PM10 values, editable threshold/interval fields, and a scrollable history of previous results and errors.
- Ensure the device and ESP32 are on the same network and the ESP32 endpoint is reachable.
- The app uses a foreground service; you can stop it from the ongoing notification action.
presentation/: Compose UI,MainActivity,MainViewModel, UI modeldata/: Repository, models, state manager, Retrofit serviceapplication/: Foreground service and notificationsui/theme/: Compose theme
Reads particulate matter from an SDS011 sensor, displays values on a 16x2 I2C LCD, and streams compact readings over UART to the ESP32. Also toggles a GPIO pulse to let the ESP32 know new data is available.
- Arduino (Uno/Nano or similar)
- SDS011 particulate matter sensor (UART)
- 16x2 LCD with I2C backpack (address 0x27)
- One GPIO connected to the ESP32 as a data-ready signal
- SDS011 ↔ Arduino via
SoftwareSerial:SENSOR_RECEIVER_PIND3 (Arduino RX) ← SDS011 TXSENSOR_TRANSMITTER_PIND4 (Arduino TX) → SDS011 RX
- LCD I2C at address
0x27(SDA/SCL per your board) - Data-ready signal: Arduino D2 → ESP32 GPIO 15
- USB Serial (115200) used for debugging
Constants (see arduino.ino):
DATA_READY_SIGNAL_PIN = 2SENSOR_RECEIVER_PIN = 3SENSOR_TRANSMITTER_PIN = 4ESP32_DATA_HEAD = 0xAA
- Initializes LCD and the SDS011 in active reporting mode
- Cycles: work for up to 60s collecting data, then puts the sensor to sleep for ~60s
- On each measurement, updates the LCD lines:
- Line 1:
PM2.5:<value>ug/m3 - Line 2:
PM10:<value>ug/m3
- Line 1:
- Sends a compact 5-byte frame to the ESP32 over
Serial:- Byte 0:
0xAA(data head) - Bytes 1-2: PM2.5 low, high
- Bytes 3-4: PM10 low, high
- Byte 0:
- Pulses D2 HIGH for ~10ms so ESP32 interrupt can mark the data as ready
arduino.ino: setup/loop, LCD output, signaling, and frame transmitSds011SensorHandler.{h,cpp}: SDS011 command frames and reading/parsingPmResult.h: data container including raw low/high bytes
- Open
arduino/arduino.inoin Arduino IDE - Select your board/port, install dependencies:
SoftwareSerial,LiquidCrystal_I2C,TimeLib - Upload and open Serial Monitor at 115200 for logs
- Ensure the ESP32 shares GND with the Arduino
- If LCD address differs, change
LiquidCrystal_I2C lcd(0x27, 16, 2); - Adjust work/sleep durations in
loop()if needed
Bridges the Arduino SDS011 readings to the network. Listens for a GPIO pulse from Arduino indicating new data, reads the compact frame from UART, and serves the last reading over HTTP (/json) and a simple HTML page (/). Can connect to Wi‑Fi with saved credentials or start an Access Point fallback.
- ESP32 DevKit (e.g.,
esp32dev) - GPIO 15: interrupt input for Arduino data‑ready pulse
- UART0 (USB Serial) connected to Arduino
Serial(ensure common GND)
- Attempts to connect as a station (STA) using saved credentials from NVS (Preferences).
- If connection fails (within ~5 seconds), starts an Access Point (
SSID: AccessPoint, password123456789). - Uses static IP configuration (edit in
src/IpConfig.h).
Credentials sources:
- Default compile‑time values in
Credentials.h - Runtime‑editable via
/savePassword?ssid=...&password=...(stored in NVS)
The Arduino sends 5 bytes whenever a measurement is ready:
- Byte 0:
0xAA(DATA_HEAD) - Bytes 1–2: PM2.5 low, high (value = ((high << 8) | low) / 10.0)
- Bytes 3–4: PM10 low, high (value = ((high << 8) | low) / 10.0)
An interrupt on GPIO 15 (RISING) sets a flag indicating data is ready. The reader then fetches the last full frame present on the serial buffer.
/(HTML):- Shows Wi‑Fi RSSI and the last PM2.5/PM10 values when available
- Includes a simple form to set SSID and password
/json(application/json):- Returns
{ "pm2.5": <float>, "pm10": <float> }if data is available, or an empty body otherwise
- Returns
/savePassword(GET):- Query params:
ssid,password - Saves credentials to NVS for future boots
- Query params:
See src/Presenter.cpp for handlers and src/Sds011Reader.{h,cpp} for the parser.
- Static IP:
src/IpConfig.h - Default credentials:
Credentials.h - Interrupt/Signal pin:
SIGNAL_PINinsrc/main.cpp(default 15) - UART: uses
Serialat 115200
- Open the
esp32/folder in VS Code with PlatformIO platformio.initargetsesp32devwith Arduino framework- Set monitor speed to
115200 - Flash and monitor via PlatformIO tasks
- Ensure ESP32 GND is tied to Arduino GND
- If the ESP32 never receives data, verify the data‑ready wire (Arduino D2 → ESP32 GPIO 15) and serial wiring
- Adjust Wi‑Fi timing or AP credentials in
WirelessConnector.cppif needed
For questions or contributions, please open an issue or pull request on the repository.

