Salah Uddin
Published © CC BY-NC

Drone Based Air Quality Signage Map

Drone-Based Air Quality Signage Map is a system that uses drones equipped with air quality sensors to collect real-time environmental data.

ExpertWork in progress3 days64
Drone Based Air Quality Signage Map

Things used in this project

Story

Read more

Schematics

Circuit

Circuit Diagram

Code

Source

Arduino
/*
  LinkIt ONE + Grove Air Quality v1.3 (analog) + GPS  Thinger.io

  WHAT IT DOES
  - Reads analog AirQ (A0), moving-average filters, builds a relative IAQ (0500)
  - Gets GPS (lat, lon, alt) from LinkIt ONE internal GPS
  - Connects to Thinger.io (Wi-Fi by default; optional GPRS)
  - Exposes a resource "airq" and pushes to a data bucket at intervals

  WIRING
  - Grove Air Quality v1.3 SIG -> A0, VCC->5V, GND->GND

  SETUP
  1) Fill in WIFI/GPRS and THINGER credentials.
  2) Create a Data Bucket in Thinger called BUCKET_NAME (or change the name below).
*/

#define USE_WIFI   1          // set 1=Wi-Fi (LWiFi), 0=GPRS (LGPRS)

// ---------- Credentials ----------
#if USE_WIFI
  #include <LWiFi.h>
  #include <LWiFiClient.h>
  const char* WIFI_SSID = "YOUR_WIFI_SSID";
  const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
#else
  #include <LGPRS.h>
  #include <LGPRSClient.h>
  const char* APN       = "YOUR_APN";
  const char* APN_USER  = "";     // if required
  const char* APN_PASS  = "";     // if required
#endif

#include <LGPS.h>                 // LinkIt ONE GPS
#include <ThingerESP8266.h>       // Works with any WiFiClient/Client; we will pass LWiFiClient/LGPRSClient
// NOTE: If your IDE pulls a specific Thinger client (e.g., ThingerWifi101),
//       it still accepts a generic Client reference in the constructor.

#include <Arduino.h>

// Thinger.io account/device
#define TH_USER   "YOUR_THINGER_USERNAME"
#define TH_DEVICE "YOUR_DEVICE_ID"
#define TH_CRED   "YOUR_DEVICE_CREDENTIAL"
#define BUCKET_NAME "airq_bucket"

// Networking client to pass into Thinger
#if USE_WIFI
  LWiFiClient netClient;
#else
  LGPRSClient netClient;
#endif

// Create a generic Thinger client with our netClient
ThingerClient thing(TH_USER, TH_DEVICE, TH_CRED, netClient);

// ---------- AirQ config ----------
#define AIRQ_PIN       A0
#define WARMUP_MS      30000UL
#define CALIB_MS       20000UL
#define SAMPLE_MS       500UL     // 2 Hz
#define MA_WINDOW         15
#define PUSH_PERIOD_MS 10000UL    // write bucket every 10 s

// Moving-average buffer
uint16_t buf[MA_WINDOW];
uint8_t  idx = 0; bool filled = false;

uint32_t t_last = 0;
uint32_t t_last_push = 0;
float baseline = NAN;

// ---------- GPS helpers ----------
static bool gps_powered = false;
struct GPSFix {
  bool   valid;
  double lat, lon, alt;
};
GPSFix readGPS() {
  GPSFix f = {false, 0, 0, 0};
  // LGPS returns NMEA strings in callbacks in some examples; however,
  // LinkIt ONE exposes a convenience API via LLocation on older packages.
  // Here we use a lightweight NMEA parse from LGPS.getData() if available.
  // Fallback: try LLocation (some BSPs include it).
  char buff[256];
  if (!gps_powered) return f;
  if (LGPS.available()) {
    int n = LGPS.read(buff, sizeof(buff)-1);
    buff[n] = '\0';
    // Minimal parse: look for GGA sentence
    // $GPGGA,time,lat,NS,lon,EW,fix, ... ,alt,M,...
    char* p = strstr(buff, "$GPGGA");
    if (p) {
      // tokenize carefully
      // For brevity, use sscanf on a simplified pattern (lat/lon are ddmm.mmmm)
      // If parsing fails, we leave 'valid' false.
      // NOTE: This is a minimal reader; for production, use a proper NMEA parser.
      char ns='N', ew='E';
      double lat_raw=0, lon_raw=0, alt=0;
      int fix=0;
      // Extract fields in a permissive way
      // Warning: sscanf on NMEA strings is brittle; keep it simple:
      // Find 2nd,3rd,4th,5th,10th fields quickly:
      // For non-technical prototype, we accept occasional invalids.
      int commas=0;
      char* q=p;
      char* fields[15]={0};
      while (*q && commas<15){
        if (*q==','){ fields[commas++]=q+1; }
        q++;
        if (*q=='*') break;
      }
      if (commas>=10 && fields[1] && fields[2] && fields[3] && fields[8]){
        lat_raw = atof(fields[1]); ns = fields[2][0];
        lon_raw = atof(fields[3]); ew = fields[4][0];
        fix     = atoi(fields[5]); // quality
        alt     = atof(fields[8]);
        // convert ddmm.mmmm -> decimal degrees
        auto ddmm_to_deg = [](double v){
          int dd = (int)(v/100.0);
          double mm = v - dd*100.0;
          return dd + mm/60.0;
        };
        double latt = ddmm_to_deg(lat_raw);
        double lonn = ddmm_to_deg(lon_raw);
        if (ns=='S') latt = -latt;
        if (ew=='W') lonn = -lonn;
        if (fix>0) { f.valid=true; f.lat=latt; f.lon=lonn; f.alt=alt; }
      }
    }
  }
  return f;
}

// ---------- Math helpers ----------
float movingAverage(uint16_t x){
  buf[idx++] = x;
  if (idx>=MA_WINDOW){ idx=0; filled=true; }
  uint16_t n = filled? MA_WINDOW : idx;
  uint32_t s=0; for(uint16_t i=0;i<n;i++) s+=buf[i];
  return (n>0)? (float)s/n : (float)x;
}

float analogToIAQ(float y, float y0){
  if (y0<=1.0f) return 0;
  float r=(y-y0)/y0;
  if (r<=0.0f)      return 25.0f;
  else if (r<=0.20f) return 25.0f  + (r/0.20f)*(75.0f-25.0f);
  else if (r<=0.50f) return 75.0f  + ((r-0.20f)/0.30f)*(150.0f-75.0f);
  else if (r<=1.00f) return 150.0f + ((r-0.50f)/0.50f)*(250.0f-150.0f);
  else if (r<=2.00f) return 250.0f + ((r-1.00f)/1.00f)*(400.0f-250.0f);
  else               return min(400.0f + (r-2.0f)*300.0f, 500.0f);
}

const char* categoryFromIAQ(float iaq){
  if (iaq<50)  return "Good";
  if (iaq<100) return "Moderate ";
  if (iaq<150) return "Unhealthy";
  if (iaq<200) return "Very Unhealthy";
  if (iaq<300) return "Heavily_Polluted";
  return "Hazardous";
}

// ---------- Setup ----------
void setup(){
  Serial.begin(115200);
  while(!Serial){;}

  analogReadResolution(10);
  pinMode(AIRQ_PIN, INPUT);

  Serial.println(F("# Booting"));

  // Power on GPS
  LGPS.powerOn();
  gps_powered = true;
  Serial.println(F("# GPS powered"));

  // Network connect
#if USE_WIFI
  Serial.print(F("# Wi-Fi connecting"));
  LWiFi.begin();
  if (LWiFi.connect(WIFI_SSID, LWiFiLoginInfo(WIFI_AUTH_WPA, WIFI_PASS)) == 0){
    Serial.println(F("\n! Wi-Fi connect failed"));
  } else {
    Serial.println(F("\n# Wi-Fi connected"));
  }
#else
  Serial.print(F("# GPRS connecting"));
  if (!LGPRS.attachGPRS(APN, APN_USER, APN_PASS)){
    Serial.println(F("\n! GPRS attach failed"));
  } else {
    Serial.println(F("\n# GPRS attached"));
  }
#endif

  // Thinger connection handlers
  thing.add_wifi(WIFI_SSID, WIFI_PASS); // harmless on GPRS; library ignores if not needed

  // Expose a resource "airq"  returns a JSON object
  thing["airq"] >> [](pson &out){
    // We publish the latest sample
    static float last_raw=0, last_filt=0, last_base=0, last_iaq=0;
    static double last_lat=0, last_lon=0, last_alt=0;
    static bool last_valid=false;

    out["raw"]      = last_raw;
    out["filtered"] = last_filt;
    out["baseline"] = last_base;
    out["iaq"]      = last_iaq;
    out["category"] = categoryFromIAQ(last_iaq);

    out["gps_valid"]= last_valid ? 1:0;
    out["lat"]      = last_lat;
    out["lon"]      = last_lon;
    out["alt"]      = last_alt;

    out["millis"]   = millis();
  };

  // Warmup + baseline
  Serial.println(F("# Warming sensor 30s"));
  delay(WARMUP_MS);
  Serial.println(F("# Calibrating baseline 20s"));
  uint32_t start=millis(), n=0, sum=0;
  while(millis()-start<CALIB_MS){ sum += analogRead(AIRQ_PIN); n++; delay(25); }
  baseline = (float)sum/(float)n;
  Serial.print(F("# Baseline ADC = ")); Serial.println(baseline,1);
  Serial.println(F("# Start loop"));
}

// We retain last published values for the Thinger resource
static float g_raw=0, g_filt=0, g_base=0, g_iaq=0;
static double g_lat=0, g_lon=0, g_alt=0;
static bool g_fix=false;

void loop(){
  thing.handle(); // keep session alive

  // Sample sensor
  if (millis() - t_last >= SAMPLE_MS){
    t_last = millis();

    uint16_t raw = analogRead(AIRQ_PIN);
    float filt = movingAverage(raw);

    // slow drift adaptation (if reading is near baseline)
    if (!isnan(baseline) && filt < baseline*1.02f){
      baseline = 0.999f*baseline + 0.001f*filt;
    }

    float iaq = analogToIAQ(filt, baseline);
    iaq = constrain(iaq, 0.0f, 500.0f);

    // GPS (non-blocking, may be invalid while searching satellites)
    GPSFix fix = readGPS();

    // Save globals for resource lambda
    g_raw = raw; g_filt=filt; g_base=baseline; g_iaq=iaq;
    g_fix = fix.valid; g_lat=fix.lat; g_lon=fix.lon; g_alt=fix.alt;

    // Log to serial (CSV)
    Serial.print(millis()); Serial.print(",");
    Serial.print(raw);      Serial.print(",");
    Serial.print(filt,1);   Serial.print(",");
    Serial.print(baseline,1); Serial.print(",");
    Serial.print(iaq,1);    Serial.print(",");
    Serial.print(categoryFromIAQ(iaq)); Serial.print(",");
    if (fix.valid){
      Serial.print(fix.lat,6); Serial.print(",");
      Serial.print(fix.lon,6); Serial.print(",");
      Serial.print(fix.alt,1);
    } else {
      Serial.print("NaN,NaN,NaN");
    }
    Serial.println();
  }

  // Periodic write to Thinger Data Bucket
  if (millis() - t_last_push >= PUSH_PERIOD_MS){
    t_last_push = millis();

    // Create a one-shot resource snapshot and write to bucket
    pson payload;
    payload["raw"]      = g_raw;
    payload["filtered"] = g_filt;
    payload["baseline"] = g_base;
    payload["iaq"]      = g_iaq;
    payload["category"] = categoryFromIAQ(g_iaq);

    payload["gps_valid"]= g_fix ? 1:0;
    payload["lat"]      = g_lat;
    payload["lon"]      = g_lon;
    payload["alt"]      = g_alt;

    payload["millis"]   = millis();

    // Send to bucket
    bool ok = thing.write_bucket(BUCKET_NAME, payload);
    if (!ok) Serial.println(F("! Thinger bucket write failed"));
  }
}

Credits

Salah Uddin
46 projects • 150 followers
Technology and IoT Hacker, Robot Killer and Drone lover.

Comments