自制智能WIFI空氣淨化器(ARDUINO)

自制智能WIFI空氣淨化器(ARDUINO)

轉載請註明出處http://blog.csdn.net/cuijiyue/article/details/78824768

簡介

  • 成品圖片
    這裏寫圖片描述這裏寫圖片描述
  • web控制端
    這裏寫圖片描述

  • 參考資料

模塊 參考資料
單片機 mega2560 硬件 https://wenku.baidu.com/view/0c37156065ce050877321320.html
編程入門 http://www.51hei.com/bbs/dpj-41336-1.html
https://pan.baidu.com/s/1jIf8IxS
多線程 https://www.cnblogs.com/xiaowuyi/p/4319720.html
溫溼度 DHT11,單總線
http://www.arduino.cn/thread-1429-1-2.html
https://pan.baidu.com/s/1kVaMRzD
PM25 PMS500, 串口
https://pan.baidu.com/s/1eS5zosE
WIFI ESP-12N,串口,內置MCU,LUA編程
https://pan.baidu.com/s/1kVvLCzX
屏幕 12864 OLED, i2c
http://www.arduino.cn/forum.php?mod=viewthread&tid=20080
https://pan.baidu.com/s/1micMO1y
PMW電機調速 L298N,gpio PWM調速
https://pan.baidu.com/s/1eSwBMLk
箱體及電機硬件 https://post.smzdm.com/p/41272/

DH11溫溼度

加載DHT11的lib,就可以讀取溫溼度,每5s讀取一次

dht11 DHT11;
void getTemperature(int pin) {
    int ret = DHT11.read(pin);
    switch (ret)
    {
      case DHTLIB_OK: 
        //log("DHT11 OK");
        temperature = DHT11.temperature;
        humidity = DHT11.humidity;
        PT_SEM_SIGNAL(&screenSem);
        break;
      case DHTLIB_ERROR_CHECKSUM: 
        log("DHT11 Checksum error"); 
        break;
      case DHTLIB_ERROR_TIMEOUT: 
        log("DHT11 Time out error"); 
        break;
      default: 
        log("DHT11 Unknown error"); 
        break;
    }
}
static int temperatureMission(struct pt *pt)
{
  PT_BEGIN(pt);//線程開始
  while(1)
  {
    getTemperature(2);//接入2號管腳
    //dhtDelay.setTimer(5000);//設定5秒
    PT_TIMER_DELAY(pt, 5000);
  }
  PT_END(pt);//線程結束
}

PM25

讀取時需要注意檢查數據幀結構,防止信息錯誤
PM25會存儲上一次讀取的數值,每5s會通過信號量通知屏幕更新,
讀取PM25和電機控制在 pm25PowerMission 中

class PM25 {
private:
  int pm_cf_10 = -1;
  int pm_cf_25 = -1;
  int pm_cf_100 = -1;
  int pm_at_10 = -1;
  int pm_at_25 = -1;
  int pm_at_100 = -1;
  int particulate03 = -1;
  int particulate05 = -1;
  int particulate10 = -1;
  int particulate25 = -1;
  int particulate50 = -1;
  int particulate100 = -1;
  int setGpio = -1;
  int resetGpio = -1;
  int serialNum;


  int prePM25[10] = {0};
  int sleepLevel[4][2] = {{10, 20}, {50, 30}, {100, 1}, {200, 0}}; //pm_val, sleep minutes
  int powerLevel[4][2] = {{10, 0}, {50, 2}, {100, 10}, {200, 24}}; //pm_val, powerlevel
  int beginIndex = 0;
  int endIndex = 0;
  bool pmHighPower = true;
  bool pmRun = true;

  unsigned char rxBuffer[32];
  int frameLen = 32;
  enum FrameStatus{
    HEAD1,
    HEAD2,
    LEN1,
    LEN2,
    DATAREAD,
    VERSION,
    CHECK,
    CRC1,
    CRC2,
  };
  FrameStatus frameStatus;
  int dataLen;

  int freamCheck() {
    int sum = 0;
    for (int i = 0; i <= 29; i++) {
      sum += rxBuffer[i];
    }
    if (rxBuffer[30]*256 + rxBuffer[31] == sum)
      return 0;
    else
      return -1;
  }

  void generateData() {
      pm_cf_10=(int)rxBuffer[4] * 256 + (int)rxBuffer[5];      //大氣環境下PM2.5濃度計算        
      pm_cf_25=(int)rxBuffer[6] * 256 + (int)rxBuffer[7];
      pm_cf_100=(int)rxBuffer[8] * 256 + (int)rxBuffer[9];
      pm_at_10=(int)rxBuffer[10] * 256 + (int)rxBuffer[11];               
      pm_at_25=(int)rxBuffer[12] * 256 + (int)rxBuffer[13];
      pm_at_100=(int)rxBuffer[14] * 256 + (int)rxBuffer[15];
      particulate03=(int)rxBuffer[16] * 256 + (int)rxBuffer[17];
      particulate05=(int)rxBuffer[18] * 256 + (int)rxBuffer[19];
      particulate10=(int)rxBuffer[20] * 256 + (int)rxBuffer[21];
      particulate25=(int)rxBuffer[22] * 256 + (int)rxBuffer[23];
      particulate50=(int)rxBuffer[24] * 256 + (int)rxBuffer[25];
      particulate100=(int)rxBuffer[26] * 256 + (int)rxBuffer[27];
      if (pm_at_25 > 1000)
        pm_at_25 = 1000;
      //Serial.println(String("") + "PM25" + pm_cf_25);
      pmReadTimes++;
  }

  int getG5(unsigned char ucData)//獲取G5的值,返回-1錯誤,0讀取完一幀
  {
    switch(frameStatus) {
      case HEAD1:
        if (ucData == 0x42) {
          rxBuffer[0] = ucData;
          frameStatus = HEAD2;
          dataLen = 0;
        } else {
          frameStatus = HEAD1;
        }
        break;
      case HEAD2:
        if (ucData == 0x4D) {
          rxBuffer[1] = ucData;
          frameStatus = LEN1;
        } else {
          frameStatus = HEAD2;
        }
        break;
      case LEN1:
          rxBuffer[2] = ucData;
          frameStatus = LEN2;
        break;
      case LEN2:
        rxBuffer[3] = ucData;
        if (rxBuffer[2] * 256 + rxBuffer[3] == 0x1c)
          frameStatus = DATAREAD;
        else //長度不對,重新讀幀
          frameStatus = HEAD1;
        break;
      case DATAREAD:
        rxBuffer[4+dataLen] = ucData;
        dataLen++;
        if (dataLen == 26)
          frameStatus = CRC1;
        else 
          frameStatus = DATAREAD;
        break;
      case CRC1:
        rxBuffer[30] = ucData;
        frameStatus = CRC2;
        break;
      case CRC2:
        rxBuffer[31] = ucData;
        if (freamCheck() == 0){
          //Serial.println("PM fream check ok");
          generateData();
          storePM25(pm_cf_25);
          frameStatus = HEAD1;
          return 0;
        } else {
          Serial.println("fream error");
          frameStatus = HEAD1;
        }
        break;
    }
    return -1;
  }

public:
  void init(int serialNum, int setPort, int resetPort) {
    if (serialNum = 1) {
      Serial1.begin(9600);
    }
    setGpio = setPort;
    resetGpio = resetPort;
    this->serialNum = serialNum;
    frameStatus = HEAD1;
    pinMode(setGpio, OUTPUT);
    digitalWrite(setGpio, HIGH);
    //digitalWrite(setGpio, LOW);
    pinMode(resetGpio, OUTPUT);
    digitalWrite(resetGpio, HIGH);
  }

  int read() { //use Serial1
    int status = -1;
    if (serialNum = 1) {
      while (Serial1.available())
      {
          status = getG5(Serial1.read());
      }
    }
    return status;
  }

  int getPM25() {
    return pm_cf_25;
  }

  int getPM10() {
    return pm_cf_100;
  }

  void storePM25(int val) {
    prePM25[endIndex] = val;
    endIndex = (endIndex+1) % 10;
    if (endIndex == beginIndex)
      beginIndex = (beginIndex+1) % 10;

    //log(String("") + "storePM25 begin: " + beginIndex + " end: " + endIndex);
  }

  void setSleep() {
    digitalWrite(setGpio, LOW);
  }
  void setWake() {
    digitalWrite(setGpio, HIGH);
  }

  int getSleepTime() { //minutes
    int maxVal = 0;
    for (int j =beginIndex; j!= endIndex; j = (j+1)%10) {
      if (prePM25[j] > maxVal)
        maxVal = prePM25[j];
    }
    for (int i =0; i <= 3; i++) {
      if (maxVal < sleepLevel[i][0])
        return sleepLevel[i][1];
    }
    return 0;
  }

  int getPowerLevel() { //V
    int maxVal = 0;
    for (int j =beginIndex; j!= endIndex; j = (j+1)%10) {
      if (prePM25[j] > maxVal)
        maxVal = prePM25[j];
    }
    for (int i =0; i <= 3; i++) {
      if (maxVal < powerLevel[i][0])
        return powerLevel[i][1];
    }
    return 24;
  }

  void printToSerail() {
    Serial.print("   PM_CF1.0:");Serial.print(pm_cf_10);Serial.print(" ug/m3");//硬件串口輸出數據
    Serial.print("   PM_CF2.5:");Serial.print(pm_cf_25);Serial.print(" ug/m3");
    Serial.print("   PM_CF10 :");Serial.print(pm_cf_100);Serial.println(" ug/m3");
    Serial.print("   PM_AQI1.0:");Serial.print(pm_at_10);Serial.print(" ug/m3");
    Serial.print("   PM_AQI2.5:");Serial.print(pm_at_25);Serial.print(" ug/m3");
    Serial.print("   PM_AQI10:");Serial.print(pm_at_100);Serial.println(" ug/m3");
    Serial.print("   PM_count03:");Serial.print(particulate03);Serial.print(" pcs/0.01cf");
    Serial.print("   PM_count05:");Serial.print(particulate05);Serial.print(" pcs/0.01cf");
    Serial.print("   PM_count10:");Serial.print(particulate10);Serial.print(" pcs/0.01cf");
    Serial.print("   PM_count25:");Serial.print(particulate25);Serial.print(" pcs/0.01cf");
    Serial.print("   PM_count100:");Serial.print(particulate100);Serial.println(" pcs/0.01cf");       
    Serial.println("*********************************************");
  }
};

//pm25 timer,隔一段(5s)時間,通知一次
static int pm25TimeMission(struct pt *pt)
{
  PT_BEGIN(pt);//線程開始
  //while(1)
  //{
    PT_TIMER_DELAY(pt, 5000);
    if ((pm25Val = pm25.getPM25()) >= 0) {
      pm10Val = pm25.getPM10();
      PT_SEM_SIGNAL(&screenSem);
    //}
  }
  PT_END(pt);//線程結束
}

SCREEN

利用U8G的lib,可以方便的顯示數字,字符的大小需要多調整幾次

//screen u8g
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);    // I2C / TWI
void drawMenu(void) {
  uint8_t i, h;
  u8g_uint_t w, d;

  String temp = String("") + "Temp:" + temperature + "C";
  String humid = String("") + "humid:" + humidity + "%";
  String pm25 = String("") + "pm2.5:" + pm25Val + "ug/m3";
  String time = String("") + "run:" + runTime + "s";

  u8g.setFont(u8g_font_8x13);
  u8g.setFontRefHeightText();
  u8g.setFontPosTop();

  h = u8g.getFontAscent() - u8g.getFontDescent();
  w = u8g.getWidth();
    //d = (w - u8g.getStrWidth(menu_strings[i])) / 2;
  u8g.setDefaultForegroundColor();
  u8g.drawStr(d, 1 * h, temp.c_str()); //前兩個參數爲矩形框的左上角座標
  u8g.drawStr(d, 2 * h, humid.c_str());
  u8g.drawStr(d, 3 * h, pm25.c_str());
  u8g.drawStr(d, 4 * h, time.c_str());  
}

//當有數據更新時,通過信號量 screenSem 更新顯示
static int screenMission(struct pt *pt)
{
  PT_BEGIN(pt);//線程開始
  while(1)
  {   
    PT_SEM_WAIT(pt, &screenSem);
    //update wifi 
    //log(String("")+ "update screenMission");
    //wifi.setStatus();
    u8g.firstPage();
    do  {
      drawMenu();
    } while ( u8g.nextPage() );    
  }
  PT_END(pt);//線程結束
}

電機控制

讀取PM25數值後,判斷當前需要設定的電機電壓,然後設置GPIO的PMW
PM25每隔會根據之前值判斷PMS500的休眠時間,數值越低,休眠時間越長,電機轉速越低,設置信息在以下兩個數組中

  int sleepLevel[4][2] = {{10, 20}, {50, 30}, {100, 1}, {200, 0}}; //pm_val, sleep minutes
  int powerLevel[4][2] = {{10, 0}, {50, 2}, {100, 10}, {200, 24}}; //pm_val, powerlevel
bool pm25PowerMission(struct pt *pt) {
  PT_BEGIN(pt);
  while(1) {
    if(runTime < 100)
      sleepTime = 0;
    else
      sleepTime = pm25.getSleepTime();
    log(String("") + "pm25 sleep " + sleepTime*60 + "s at:" + runTime);
    //sleep
    for (loopIndex = 0; loopIndex < sleepTime*60; loopIndex++) {
      pm25.setSleep();
      PT_TIMER_DELAY(pt, 1000);
    }  
    log(String("") + "pm25 wake at:" + runTime);
    pm25.setWake();
    //run
    missionStartTime = runTime;
    log(String("") + "pm25 run at " + missionStartTime + "s pmreadtimes:" + pmReadTimes);
    while(runTime - missionStartTime < 60) { 
      //log(String("") + "runTime:" + runTime + "missionStartTime:"+ missionStartTime);
      PT_TIMER_DELAY(pt, 200); //每200ms讀取一次
      pm25.read();
      if ((pm25Val = pm25.getPM25()) >= 0) {
        pm10Val = pm25.getPM10();
        PT_SEM_SIGNAL(&screenSem);
        powerLevel = pm25.getPowerLevel();
        power.setV(powerLevel);
      }
    }
    log(String("") + "pm25 run over at " + runTime + "s pmreadtimes:" + pmReadTimes);
    //set 電機電壓
    powerLevel = pm25.getPowerLevel();
    log(String("") + "powerLevel:" + powerLevel);
  }
  PT_END(pt);
}

通過GPIO控制電機開關和轉速
class Power {
  int enGpio, pwmGpio, in1Gpio, in2Gpio;
  bool powerStatus = false;
  int maxPower = 24;
public:
  void init(int enPort, int pwmPort, int in1, int in2) {
    enGpio = enPort;
    pwmGpio = pwmPort;
    in1Gpio = in1;
    in2Gpio = in2;
    pinMode(enGpio,OUTPUT);
    pinMode(in1Gpio,OUTPUT);
    pinMode(in2Gpio,OUTPUT);
    digitalWrite(enGpio, HIGH);
    digitalWrite(in1Gpio, HIGH);
    digitalWrite(in2Gpio, LOW);
    analogWrite(pwmGpio,(int)(12*255/24));
    powerLevel = 12;
  }

  void setPower(bool on) {
    powerStatus = on;
    if(on)
      digitalWrite(enGpio, HIGH);
    else
      digitalWrite(enGpio, LOW);
  }

  void setV(int v) {
    if (powerStatus == false) 
      return;
    if (v == 0) {
      setPower(false);
    }
    else if(v>0 && v<=25) {
      analogWrite(pwmGpio,(int)(v*255/24));
      setPower(true);
      Serial.println(String("") + pwmGpio + "Power:setV: " + v);
    }
    powerLevel = v;
  }
};

WIFI模塊

ESP先刷支持lua的固件,lua中自帶一個webserver,可以製作一個建議的web控制網頁。
ESP支持熱點和wifi同時開啓,可以手機直接連接熱點或者通過雲端平臺控制(ESP這個芯片不好,WIFI連接後,熱點就無法連接了,估計是性能太差,雲端控制後續再製作)

Arduion可以通過串口命令控制ESP,相應的命令可以通過lua腳本中設定

Serial2.println(String("")+"PM25="+pm25Val+"PM10="+pm10Val+"Temp=" + temperature+"humid="+humidity+"FanPower="+powerLevel);

Wifi LUA部分

ESP開機後會自動執行init.lua腳本,初始化的工作要在這裏進行
設定全局變量,開啓熱點和wifi

PowerState = 1
PM25 = 0
PM10 = 0
Temp = 0
humid = 0
FanPower = 0
wifi.setmode(wifi.STATIONAP)
--wifi.sta.config(SSID,PWD)
wifi.sta.autoconnect(1)
wifi.ap.config({ ssid = 'cuiting airclean', auth = AUTH_OPEN })
srv=net.createServer(net.TCP)
dofile("tcpserver.lua")

tcpserver.lua中設置了網頁部分,網頁內容在head.html和wifi.html中
https://pan.baidu.com/s/1geHYmLP

function file_read (name)
    if file.open(name, "r") == true then
        --print(name.." open ok")
        local data = file:read("*a")
        file:close()
        return data
    else
        print(name.." open fail")
        return nil
    end
end

srv:listen(80,function(conn)
    conn:on("receive", function(client,request)
        local buf = "";
        local ssid = "";
        local pwd = "";
        local power = "";
        local _, _, method, path, vars = string.find(request, "([A-Z]+) (.+)?(.+) HTTP");
        if(method == nil)then
            _, _, method, path = string.find(request, "([A-Z]+) (.+) HTTP");
        end
        if (vars ~= nil)then
            --print("vars"..vars)
            for k in string.gmatch(vars, "ssid=(%w+)&") do
                ssid = k
            end
            for k in string.gmatch(vars, "&pwd=(%w+)") do
                pwd = k
            end
            for k in string.gmatch(vars, "power=(%w+)") do
               if(k == "OFF") then
                    PowerState = 0
                    print("PowerState=0")
               elseif (k == "ON") then
                    PowerState = 1
                    print("PowerState=1")
               end
            end
            if (ssid ~= "" and pwd~= "" and wifi.sta.status() == 0) then
                wifi.sta.config(ssid,pwd)
            end
            --print("ssid:"..ssid.."pwd:"..pwd.."power"..power)
        end
        buf = file_read("head")
        if (wifi.sta.status() == 5) then
            --buf = buf.."<p>wifi connect, IP:"..wifi.sta.getip().."</p>"
            buf = buf.."<p>wifi connect, IP:"..wifi.sta.getip().."</p>"
        elseif(wifi.sta.status() == 0) then
            buf = buf.."<p>WIFI STATION_IDLE</p>"
            buf = buf..file_read("wifi")
        elseif(wifi.sta.status() == 1) then
            buf = buf.."<p>WIFI STATION_CONNECTING</p>"
            buf = buf..file_read("wifi")
        elseif(wifi.sta.status() == 2) then
            buf = buf.."<p>WIFI STATION_WRONG_PASSWORD</p>"
            buf = buf..file_read("wifi")
        elseif(wifi.sta.status() == 3) then
            buf = buf.."<p>WIFI STATION_NO_AP_FOUND</p>"
            buf = buf..file_read("wifi")
        elseif(wifi.sta.status() == 4) then
            buf = buf.."<p>WIFI STATION_CONNECT_FAIL</p>"
            buf = buf..file_read("wifi")
        end
        if (PowerState == 1) then
            buf = buf.."<p>Power <a href=\"?power=OFF\"><button style='font-size:100%'>OFF</button></a></p>"
        else
            buf = buf.."<p>Power <a href=\"?power=ON\"><button style='font-size:100%'>ON</button></a></p>"
        end
        buf = buf.."<p>temp : "..Temp.." C</p>"
        buf = buf.."<p>humid : "..humid.."%</p>"
        buf = buf.."<p>PM25 : "..PM25.."ug/m3</p>"
        buf = buf.."<p>PM10 : "..PM10.."ug/m3</p>"
        buf = buf.."<p>Fan : "..FanPower.."V</p>"
        buf = buf.."</body>"
        buf = buf.."</html>"

        --print("web rev")
        client:send(buf)
        client:close()
        collectgarbage()
    end)
end)

CODE

代碼中需要的DHT11的庫和U8G的庫,需要放在libraries目錄下,程序文件爲airclean.ino
https://pan.baidu.com/s/1geEAyOj

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章