Pomiar zanieczyszczenia powietrza.

By | Styczeń 10, 2021

Po wielu testach różnych czujników, chyba w końcu znalazłem całkiem sensowny czujnik zanieczyszczenia powietrza. Urządzenie nazywa się Plantower PMS7003 i można je kupić w całkiem rozsądnej cenie.

Według ulotki marketingowej Plantower PMS7003 to:

„Laserowy czujnik zanieczyszczeń powietrza cząsteczkami PM1,0, PM2.5 oraz PM10.
Pozwala na precyzyjną analizę ilości zanieczyszczeń w otoczeniu, ma niewielkie wymiary i wbudowany cichy wentylator przetłaczający.
Dane wyprowadzone są na interfejs UART”

Zaskoczony byłem wymiarami tego czujnika który okazał się dużo mniejszy od modeli które testowałem wcześniej. Urządzenie jest skalibrowane fabrycznie i nie wymaga ingerencji użytkownika.
Prace nad programem rozpocząłem od przeszukania sieci, gdzie znalazłem kilka gotowych programów napisanych pod arduino i w pythonie. Z analizy tych programów wynika ze dane są przesyłane w formacie binarnym z sumą kontrolną co zapewnia integralność danych odbieranych z czujnika.

Czujnik podłączony do arduino

W oparciu o te gotowe fragmenty oraz moje wcześniejsze wsady do komunikacji za pomocą protokołu mysensors, przygotowałem program który wysyła dane do Domoticza. Dzięki temu będę mógł gromadzić trendy i przesyłać dane dalej na przykład do systemu APRS.

Poniżej gotowy kod programu:

// **************************************************************//
// AS-500RVD
// ARDUINO 2xVolt meter for solar panel and accumulator
//         PMS7003 dust sensor PM1, PM2.5, PM10
// SQ9MDD @ 2019
// **************************************************************//
// IO
#define ai1 A0                                      //AI1 B solar
#define ai2 A1                                      //AI2 A battery
#define ONE_WIRE_BUS 4
#define pms_RX  3
#define pms_TX  0

// radio 
#define MY_RADIO_NRF24
//#define MY_DEBUG                                  // Enable debug prints
//#define MY_GATEWAY_SERIAL                                         // Enable GW serial
#define MY_REPEATER_FEATURE
#define MY_NODE_ID      7                           // <--- !!! SET NODE ADDRESS HERE !!!
#define MY_RF24_CHANNEL 70                          // channel from 0 to 125. 76 is default

#include 
#include 
#include                                 // https://github.com/PaulStoffregen/Time
#include                       // biblioteka do komunikacji z czujnikami DS18B20
#include                                 // biblioteka do komunikacji z czujnikami one wire

#define CHILD_ID_AI1     1                          // AI1 B solar
#define CHILD_ID_AI2     2                          // AI2 A battery
#define CHILD_ID_AI3     3                          // AI3 PM 1.0
#define CHILD_ID_AI4     4                          // AI4 PM 2.5
#define CHILD_ID_AI5     5                          // AI5 PM 10
#define CHILD_ID_AI6     6                          // AI6 Temp sensor DS18B20

SoftwareSerial pms(pms_RX, 0);                           // Rx, Tx
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

MyMessage msgAI1(CHILD_ID_AI1, V_VOLTAGE);
MyMessage msgAI2(CHILD_ID_AI2, V_VOLTAGE);
MyMessage msgAI3(CHILD_ID_AI3, V_LEVEL);
MyMessage msgAI4(CHILD_ID_AI4, V_LEVEL);
MyMessage msgAI5(CHILD_ID_AI5, V_LEVEL);
MyMessage msgAI6(CHILD_ID_AI6, V_TEMP);             // AI6 Temperature

int last_minute = 0;
int last_second = 0;
int ai1_raw = 0;
int ai2_raw = 0;
int PM_SP_UG_1_0, PM_SP_UG_2_5, PM_SP_UG_10_0;      // standard particles for indoor use
int PM_AE_UG_1_0, PM_AE_UG_2_5, PM_AE_UG_10_0;      // atmospheric enviroment
unsigned int weight = 10;                           // Max 64 min 1, (for optimization must be divide by 2) unsigned int = 0-65535, max from adc 1023 *64 = 65472 
unsigned int ai1_avg = 0;
unsigned int ai2_avg = 0;
unsigned int pm1_avg = 0;
unsigned int pm25_avg = 0;
unsigned int pm10_avg = 0;
float temp = 0.0;
float temp_avg = 0.0;

void receiveTime(uint32_t ts){
  setTime(ts);  
}

bool pms_process(){
  if (!pms.available()) {
    return false;
  }
  uint8_t ch = pms.read();
  static uint8_t dataIndex = 0;
  static uint16_t checksum = 0;
  static uint16_t calculatedChecksum = 0;
  static uint16_t frameLen = 0;
  static uint8_t payload[30];

  switch (dataIndex) {
    case 0:
      if (ch != 0x42) {
        return false;
      }
      calculatedChecksum = ch;
      break;

    case 1:
      if (ch != 0x4D) {
        dataIndex = 0;
        return false;
      }
      calculatedChecksum += ch;
      break;

    case 2:
      frameLen = ch << 8;
      calculatedChecksum += ch;
      break;

    case 3:
      frameLen |= ch;
      calculatedChecksum += ch;
      break;

    default:
      if (dataIndex == frameLen + 2) {
        checksum = ch << 8;
      } else if (dataIndex == frameLen + 2 + 1) {
        checksum |= ch;

        if (calculatedChecksum == checksum) {
          PM_SP_UG_1_0 = (((uint16_t)payload[0]) << 8) | payload[1];
          PM_SP_UG_2_5 = (((uint16_t)payload[2]) << 8) | payload[3];
          PM_SP_UG_10_0 = (((uint16_t)payload[4]) << 8) | payload[5];

          PM_AE_UG_1_0 = (((uint16_t)payload[6]) << 8) | payload[7];
          PM_AE_UG_2_5 = (((uint16_t)payload[8]) << 8) | payload[9];
          PM_AE_UG_10_0 = (((uint16_t)payload[10]) << 8) | payload[11];
        } else {
          //Serial.println("Error checksum");
        }

        dataIndex = 0;
        return true;
      } else {
        calculatedChecksum += ch;
        uint8_t payloadIndex = dataIndex - 4;

        if (payloadIndex < sizeof(payload)) {
          payload[payloadIndex] = ch;
        }
      }
  }
  dataIndex++;
  return false;
}

void presentation(){
  char etykieta[] = "       ";
  int addr = MY_NODE_ID;   
  sendSketchInfo("AS-500RVD", "1.2");
  sprintf(etykieta,"R%02u.AI1",addr);     present(CHILD_ID_AI1, S_MULTIMETER, etykieta);
  sprintf(etykieta,"R%02u.AI2",addr);     present(CHILD_ID_AI2, S_MULTIMETER, etykieta);
  sprintf(etykieta,"R%02u.PM1",addr);     present(CHILD_ID_AI3, S_DUST, etykieta); 
  sprintf(etykieta,"R%02u.PM2.5",addr);   present(CHILD_ID_AI4, S_DUST, etykieta);
  sprintf(etykieta,"R%02u.PM10",addr);    present(CHILD_ID_AI5, S_DUST, etykieta); 
  sprintf(etykieta,"R%02u.AI6",addr);     present(CHILD_ID_AI6, S_TEMP, etykieta);
}

void setup(){  
  pms.begin(9600);
  analogReference(INTERNAL);
  pinMode(ai1, INPUT);
  pinMode(ai2, INPUT);
  delay(1000);
  ai1_avg = analogRead(ai1);
  ai2_avg = analogRead(ai2);
  while(pms_process() == false);                  // wait for data from PMS7003 dust sensor
    pm1_avg = PM_AE_UG_1_0;
    pm25_avg = PM_AE_UG_2_5;
    pm10_avg = PM_AE_UG_10_0;    
  requestTime();
  //sensors.begin();
  
  float ai1_vf = (ai1_avg * 0.019775549434047);  // B solar
  float ai2_vf = (ai2_avg * 0.019930533806591);  // A battery  
  int batteryPcnt = map(ai2_avg,530,683,20,100);
  batteryPcnt = constrain(batteryPcnt,0,100);  

  sensors.requestTemperatures();
  temp = sensors.getTempCByIndex(0);
  temp_avg = temp;
  
  send(msgAI1.set(ai1_vf, 2)); 
  send(msgAI2.set(ai2_vf, 2)); 
  send(msgAI3.set(pm1_avg, 0));
  send(msgAI4.set(pm25_avg, 0));
  send(msgAI5.set(pm10_avg, 0));
  send(msgAI6.set(temp_avg, 1));
  sendBatteryLevel(batteryPcnt);
}

void loop(){
  //filtering readings dust
  if (pms_process()) {
    pm1_avg = (PM_AE_UG_1_0*(weight-1) + pm1_avg) / weight;
    pm25_avg = (PM_AE_UG_2_5*(weight-1) + pm25_avg) / weight;
    pm10_avg = (PM_AE_UG_10_0*(weight-1) + pm10_avg) / weight;
  }  
  if (second() != last_second){
    ai1_raw = analogRead(ai1);
    ai2_raw = analogRead(ai2);
    sensors.requestTemperatures();
    temp = sensors.getTempCByIndex(0);
    

    // filtering readings voltage
    ai1_avg = (ai1_avg*(weight-1) + ai1_raw) / weight;
    ai2_avg = (ai2_avg*(weight-1) + ai2_raw) / weight;  
    temp_avg =  (temp_avg*(weight-1) + temp) / weight;  
    last_second = second(); 
  }
  
  if (minute() != last_minute){    
    // ((1e6+470e3)/470e3)*1.1 = Vmax
    // Vmax/1023 = Volts per bit = 0.0212
    float ai1_vf = (ai1_avg * 0.019775549434047);  // B solar
    float ai2_vf = (ai2_avg * 0.019930533806591);  // A battery

    // calculating battery percent (ADC 519 = 10,5V, ADC 668 = 13,5V)
    // accu: 
    // 13,5V = 100% ADC: 683 ---------
    // 12,5V = 80%                    | 
    // 11,0V = 20%
    // 10,5V = 0%   ADC: 530 -----    |
    // 10,5V = 0% battery dead    |   |
    int batteryPcnt = map(ai2_avg,530,683,20,100);
    batteryPcnt = constrain(batteryPcnt,0,100);     
    
    send(msgAI1.set(ai1_vf, 2)); 
    send(msgAI2.set(ai2_vf, 2)); 
    send(msgAI3.set(pm1_avg, 0));
    send(msgAI4.set(pm25_avg, 0));
    send(msgAI5.set(pm10_avg, 0));
    send(msgAI6.set(temp_avg, 1));
    sendBatteryLevel(batteryPcnt);
    
    last_minute = minute();
  }
}

Czujnik wraz z arduino postanowiłem zainstalować na balkonie w związku z tym do kodu dorzuciłem też odczyt temperatury, oraz dwa pomiary napięcia, które wykorzystam do opomiarowania panela słonecznego.
Oczywiście przygotowałem też projekt estetycznej obudowy by cały projekt zabezpieczyć przed przypadkowym uszkodzeniem. Rysunki przygotowałem w programie Fusion360. A następnie wydrukowałem na drukarce 3D.

Okno programu Fusion 360

Gotowa obudowa łatwo pomieściła wszystkie niezbędne elementy.

Czujnik w obudowie.

Gotową obudowę można ściągnąć z serwisu Thingiverse, pliki znajdują się pod tym adresem:

https://www.thingiverse.com/thing:4716558

Po zamontowaniu ścianek, frontowej i tylnej podłączyłem układ do zasilania i obserwuję w Domoticzu spływające dane.

Odczyt danych w domoticzu.

Dane z Domoticza do systemu APRS przesyłam za pomocą skryptu WxDomoPY który można znaleźć na githubie pod tym adresem:

https://github.com/SQ9MDD/WXDomoPy

R.