|
Air Quality Monitor
Portable C application for collecting, storing, and analyzing air quality sensor readings
|
[
](LICENSE)

Air Quality Monitor is a portable C application that collects, stores, and analyzes air quality–style readings (PM2.5, PM10, CO, NO2, O3, SO2). It uses SQLite for storage, optional libharu for PDF export, and runs on Linux, macOS, and Windows (e.g. MSYS2 / MinGW-w64).
The core design problem this project solves: sensor hardware isn't always available (in CI, on a dev machine, or during a demo), but the rest of the system – database, alerts, exports, statistics – still needs to be built and tested against realistic data. The solution is a runtime-loadable plugin architecture: sensors are .so/.dylib/.dll modules loaded via dlopen/LoadLibrary, validated against a versioned ABI, and each one falls back to simulated data automatically when real hardware isn't reachable. The same binary and menu work identically with or without a sensor plugged in.
Legacy code (pre-refactor snapshot) is preserved on the **legacy** branch; active development targets **main**.
Every plugin exports a single SensorPlugin sensor_plugin symbol matching the ABI defined in sensor.h. sensor_module_load() rejects any plugin whose api_version doesn't match SENSOR_PLUGIN_API_VERSION, or that's missing a required function pointer, before the plugin is ever called – a stale or incompatible module fails loudly at load time instead of crashing later. See docs/plugins.md for the full plugin authoring guide.
sensors + readings tables, foreign keys, indexes, migration from the old single-table SensorData schema when present../data/ by default (air_quality.db, config.cfg). Override with AIR_QUALITY_DATA_DIR.cp / copy shell commands).dlopen on Linux/macOS, LoadLibrary on Windows) via AQM_SENSOR_PLUGIN.HAVE_HPDF and linked against libharu.aqm_parse_int/aqm_parse_float/aqm_trim_crlf). Run with make test.include/. See Documentation below.main. See the badge at the top of this file.Full API reference generated with Doxygen: https://lnpotter.github.io/AirQualityMonitor/
To regenerate locally:
Output lands in docs/api/html/ (gitignored).
| Component | Required | Notes |
|---|---|---|
| C compiler | Yes | GCC or Clang |
| SQLite 3 | Yes | Development headers (libsqlite3-dev, sqlite-devel, MSYS pacman -S mingw-w64-x86_64-sqlite, etc.) |
| libharu | Optional | For PDF menu item; omit with make HAVE_HPDF=0 |
Optional dependencies: the default build runs without GPIO libraries; wiringPi and dynamic sensor plugins are optional capabilities.
Build without PDF if libhpdf is unavailable:
Produces air_quality_monitor (on Windows with MinGW, the file may appear as air_quality_monitor.exe).
The collector supports portable mock mode and plugin-backed sensors.
config.cfg.sensor_mode: mock|dht22|bme680|pms5003|mh-z19|mhz19sensor_plugins_enabled: 1|0 (enable/disable plugins globally)sensor_plugin_path: optional explicit module path overrideEach reading now stores model in the database (for example DHT22, MH-Z19, mock).
The application supports simultaneous data collection from multiple sensors (up to 8 sensors).
config.cfg with sensor_N_mode, sensor_N_path, sensor_N_enabled entriesMenu option 12. Auto-detect sensors automatically scans for available sensor plugins:
plugins/ directoryRuntime flow:
Program -> load shared module -> resolve sensor_plugin symbol -> init/read/shutdown -> unload module
Cross-platform loader:
dlopen / dlsym / dlcloseLoadLibrary / GetProcAddress / FreeLibraryABI contract is defined in sensor.h (SensorPlugin). A plugin must export:
ABI safety:
api_version is required and validated by the loader (SENSOR_PLUGIN_API_VERSION).name, plugin_version, description.Detailed authoring guide: docs/plugins.md.
These examples run in simulated mode (no hardware required), useful for CI/testing/portfolio demos. Hardware support in current plugins:
dht22_plugin: real GPIO read on Linux/Raspberry Pi with HAVE_WIRINGPI=1; fallback mock otherwise.mhz19_plugin: real UART read on Linux/macOS (MHZ19_DEVICE, default /dev/ttyS0); fallback mock when device/read fails.pms5003_plugin: real UART frame read on Linux/macOS (PMS5003_DEVICE, default /dev/ttyUSB0); fallback mock when device/read fails.bme680_plugin: real read from Linux IIO/sysfs (BME680_IIO_PATH optional, otherwise auto-scan /sys/bus/iio/devices); fallback mock on unsupported systems or missing driver.Build plugin shared libraries:
On Linux this produces .so, on macOS .dylib, on Windows .dll.
For DHT22 hardware mode:
Run with a plugin:
Plugins are configured via the menu system (option 11) and persisted in config.cfg. The application will automatically load the appropriate plugin based on the configured sensor mode. No environment variables are required for plugin loading.
DHT22_PIN (wiringPi pin number), default 7.TX/RX/GND) on serial adapter/UART pins. Set MHZ19_DEVICE (e.g. /dev/ttyUSB0).TX/RX/GND + power). Set PMS5003_DEVICE (e.g. /dev/ttyUSB0).BME680_IIO_PATH directly (e.g. /sys/bus/iio/devices/iio:device0).If real reading fails, plugin automatically falls back to mock data so the app stays operational.
On first run, if no config exists, the program prompts for limits and settings, then saves them under ./data/config.cfg.
data/backup_<timestamp>_air_quality.db)After each action, the program waits for Enter before returning to the menu.
stdin); there's no way to script a single action (e.g. "collect 10 samples and exit") without driving the menu. This is also why config_persistence.c and the menu-handling functions in main.c aren't unit tested – they're coupled to live terminal I/O.generate_statistics.c, export_to_csv.c's update_sensor_stats) and alert evaluation (alert_system.c) are not yet covered.insert_data() is dead code.** Superseded by insert_data_sqlite() (which takes an already-open connection), but kept for now – see the @deprecated note in include/data/insert_data.h.globals.c), not passed through a context struct. Workable at this scale; would need to change if the collection logic were ever made concurrent.See [LICENSE](LICENSE) in the repository.