自制智能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