Abhijit Kar
Created May 12, 2025

Autonomous Indoor Air-Quality Remediation Drone

Self-navigating indoor drone maps VOCs and particulate hotspots, then deploys targeted ozone or HEPA filter modules for real-time, on-demand

25
Autonomous Indoor Air-Quality Remediation Drone

Things used in this project

Hardware components

nRF7002 Development Kit
Nordic Semiconductor nRF7002 Development Kit
For Wi-Fi + Thread/Matter mesh networking
×1
Photon 2
Particle Photon 2
For cloud connectivity and telemetry (integrated with Particle Cloud)
×1
MapleTree Mini - STM32duino STM32F103RB Compatible with Leaf Maple
MapleTree Mini - STM32duino STM32F103RB Compatible with Leaf Maple
Optional for dedicated motor control or sensor processing
×1
Solo Propellers
3DR Solo Propellers
×1
Pixhawk Mini
3DR Pixhawk Mini
For stable autonomous flight
×1
SGP30
VOC sensor : To detect volatile organic compounds
×1
PM 2.5 SENSOR USB POWER/SHT20 (PMSA003)
M5Stack PM 2.5 SENSOR USB POWER/SHT20 (PMSA003)
Particulate Matter sensor :To monitor PM1.0, PM2.5, PM10 levels
×1
Grove - Carbon Dioxide Sensor(MH-Z16)
Seeed Studio Grove - Carbon Dioxide Sensor(MH-Z16)
CO₂ sensor: To detect CO₂ levels for enhanced remediation targeting
×1
DHT22 Temperature Sensor
DHT22 Temperature Sensor
To monitor environmental conditions
×1
Mini Ozone
To break down VOCs in localized hotspots
×1
Mini HEPA filter module with fan
To physically filter particulates from air
×1
Brushless DC motors (4x), Propellers (4x), Battery (LiPo, 3S or 4S), Quad0copter drone frame
×1
6 DOF Sensor - MPU6050
DFRobot 6 DOF Sensor - MPU6050
For orientation and stability
×1
Time-of-Flight (ToF) VL53L0X Laser Ranging Unit (MCP4725/)
M5Stack Time-of-Flight (ToF) VL53L0X Laser Ranging Unit (MCP4725/)
For obstacle avoidance and indoor mapping
×1

Software apps and online services

Windows 10
Microsoft Windows 10
For development environment (VS Code, Fusion 360, SDK tools)
NuttX RTOS
NuttX RTOS
Embedded RTOS (used with nRF SDK or ESP32 for real-time sensor and motor control)
Fusion
Autodesk Fusion
For designing the drone’s 3D frame and modular mounts
VS Code
Microsoft VS Code
Code editor for developing embedded firmware
nRF Connect SDK
Nordic Semiconductor nRF Connect SDK
For configuring and programming the nRF7002
Particle Build Web IDE
Particle Build Web IDE
For coding and deploying firmware to Particle devices
Arduino IDE
Arduino IDE
For sensor integration, prototyping, or alternate MCU programming
QGroundControl
PX4 QGroundControl
For configuring the drone’s flight controller and autonomous navigation settings
KiCad
KiCad
For custom PCB design (before sending to PCBWay)
Particle Cloud
For telemetry logging, remote firmware updates, and live monitoring
PCBWay
For manufacturing the custom PCB used in sensor interfacing and power distribution
GitHub
For version control and collaboration
Google Drive
For documenting, planning, and tracking progress

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Extraction Tool, 6 Piece Screw Extractor & Screwdriver Set
Extraction Tool, 6 Piece Screw Extractor & Screwdriver Set
Mastech MS8217 Autorange Digital Multimeter
Digilent Mastech MS8217 Autorange Digital Multimeter
Hot glue gun (generic)
Hot glue gun (generic)
3D Printer (generic)
3D Printer (generic)

Story

Read more

Schematics

Working Procedure

Working Procedure Circuit

Code

Required Coding

C/C++
Below is a complete example firmware written for a Particle Boron (LTE-M/Wi-Fi) that:

Reads a CCS811 VOC sensor over I²C

Reads a Plantower PMS5003 particulate sensor over UART

Uses the on-board mesh/Wi-Fi (nRF7002) to publish data to Particle Cloud

Maps “hotspots” in a simple grid logic and drives two GPIOs to switch on a HEPA filter or an ozone module

Reports status and allows remote override via Particle Cloud functions/variables

========================================================================================

How it fits together
setup()

Exports Particle Cloud variables & function

Initializes CCS811 and PMS5003 sensors

Configures GPIOs for filter modules

loop()

Every 5 seconds, reads both sensors

Updates latestVOC and latestPM25

In “auto” (no override), applies a simple threshold-based decision:

PM₂.₅ > 35 µg/m³ → HEPA

eCO₂ > 1000 ppm → Ozone

otherwise off

Runs a placeholder grid-walk so the drone “patrols” spots

Cloud & Remote Control

Publishes air‐quality data & current grid position as private events

Exposes a SetModule function so you can remotely force HEPA, Ozone, or off

Next Steps

Integrate with your PX4 flight controller using MAVLink to move between grid cells

Replace advanceGridPosition() with real SLAM or waypoint logic

Fine-tune thresholds, PID loops, and safety interlocks
// main.cpp
#include "Particle.h"
#include "CCS811.h"        // https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library
#include "PMS.h"           // https://github.com/fuho/PMS

// --- Pin assignments (change as needed) ---
#define PIN_HEPA_MODULE   D6
#define PIN_OZONE_MODULE  D7

// CCS811 VOC sensor (I2C on default Wire/BUS)
CCS811 ccs811;

// PMS5003 (UART2 on TX/RX)
PMS pms(Serial1);
PMS::DATA pmsData;

// Grid mapping parameters
const int GRID_ROWS = 3;
const int GRID_COLS = 3;
int currentRow = 0;
int currentCol = 0;

// Timing
unsigned long lastSensorRead = 0;
const unsigned long SENSOR_INTERVAL = 5000; // 5s

// Cloud variables & functions
double latestVOC = 0;
double latestPM25 = 0;
int latestAction = 0; // 0 = none, 1 = HEPA, 2 = Ozone

// Remote override
int remoteCommand = 0; // 0:auto, 1:HEPA, 2:Ozone, 3:off
int cmdHandler(String cmd) {
  int c = cmd.toInt();
  if (c >= 0 && c <= 3) remoteCommand = c;
  return remoteCommand;
}

void setup() {
  // Particle cloud setup
  Particle.variable("VOC", latestVOC);
  Particle.variable("PM25", latestPM25);
  Particle.variable("Action", latestAction);
  Particle.function("SetModule", cmdHandler);

  // Module outputs
  pinMode(PIN_HEPA_MODULE, OUTPUT);
  pinMode(PIN_OZONE_MODULE, OUTPUT);
  digitalWrite(PIN_HEPA_MODULE, LOW);
  digitalWrite(PIN_OZONE_MODULE, LOW);

  // Start serial for PMS5003
  Serial1.begin(9600);

  // Init CCS811
  Wire.begin();
  if (!ccs811.begin()) {
    Serial.println("CCS811 not found");
    Particle.publish("Error", "CCS811 init failed", PRIVATE);
  }
  ccs811.setDriveMode(CCS811_DRIVE_MODE_1SEC);
}

void loop() {
  unsigned long now = millis();
  if (now - lastSensorRead >= SENSOR_INTERVAL) {
    lastSensorRead = now;
    readAndProcessSensors();
    advanceGridPosition();
  }
}

// Read sensors, decide action, publish to cloud
void readAndProcessSensors() {
  // --- VOC reading ---
  if (ccs811.dataAvailable()) {
    ccs811.readAlgorithmResults();
    latestVOC = ccs811.geteCO2();      // equivalent CO2 in ppm
    double tvoc = ccs811.getTVOC();    // total VOC in ppb
    LOG(TRACE, "VOC: %0.1f ppm, TVOC: %0.1f ppb", latestVOC, tvoc);
  }

  // --- Particulate reading ---
  pms.read(pmsData);
  latestPM25 = pmsData.PM_AE_UG_2_5;   // PM2.5 concentration (µg/m³)
  LOG(TRACE, "PM2.5: %0.1f ug/m3", latestPM25);

  // --- Decide action (simple threshold) ---
  int action = 0;
  if (remoteCommand == 0) {
    // automatic mode
    if (latestPM25 > 35.0)      action = 1;  // HEPA
    else if (latestVOC > 1000)  action = 2;  // Ozone
    else                         action = 0;  // none
  } else {
    // remote override
    action = remoteCommand;
  }
  applyAction(action);

  // --- Publish to cloud ---
  latestAction = action;
  Particle.publish("GridPos", String::format("%d,%d", currentRow, currentCol), PRIVATE);
  Particle.publish("AQData", String::format("V%.0f_P%.1f_A%d", latestVOC, latestPM25, action), PRIVATE);
}

// Turn modules on/off
void applyAction(int action) {
  switch (action) {
    case 1: // HEPA
      digitalWrite(PIN_HEPA_MODULE, HIGH);
      digitalWrite(PIN_OZONE_MODULE, LOW);
      break;
    case 2: // Ozone
      digitalWrite(PIN_HEPA_MODULE, LOW);
      digitalWrite(PIN_OZONE_MODULE, HIGH);
      break;
    default: // off
      digitalWrite(PIN_HEPA_MODULE, LOW);
      digitalWrite(PIN_OZONE_MODULE, LOW);
      break;
  }
}

// Simple zig-zag grid mover—replace with your SLAM or waypoint logic
void advanceGridPosition() {
  currentCol++;
  if (currentCol >= GRID_COLS) {
    currentCol = 0;
    currentRow = (currentRow + 1) % GRID_ROWS;
  }
}

Credits

Abhijit Kar
1 project • 0 followers

Comments