其实这个也是做烂的项目,最近帮人带学生做了一个,在此简单说明下。
1.硬件部分
1.1 包含设备
设备端包括如下设备:
图1 Nodemcu模块1个
图2 12864IIC0.96寸OLED屏幕1个
图3 攀腾G5传感器1个
1.2 接口连线
各设备接线如下表所示:
Nodemcu |
12864 OLED |
3V |
VCC |
GND |
GND |
D6 |
SDA |
D5 |
SCL |
Nodemcu |
12864 OLED |
VU或5V |
VCC |
GND |
GND |
D8 |
PIN4/RXD |
D7 |
PIN5/TXD |
接线部分这里采用最小方式接线,其实攀腾传感器的sleep和rst接口最好也接在单片机上,这样就可以应对传感器死机以及待机的功能。
图4 设备接线
2.软件部分
代码还是采用arduino IDE,使用8266的库编译。所实现的功能主要包括:
- 读取攀腾G5的数据。
- 将读取到的数据用12864屏显示。
- 利用8266的网络功能,联网上传数据。
2.1 攀腾传感器数据的读取
攀腾传感器采用软串口方式读取数据,利用相应的软串口库文件根据传感器手册读取对应的数据即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| int _pm2p5, _pm10; SoftwareSerial SerialG5(13, 15); void ReadSensorData() { byte inbyte[30]; byte state = 0; byte count = 0; while (true) { while (SerialG5.available() > 0) { byte inByte = SerialG5.read(); switch (state) { case 0: if (inByte == 0x42) state = 1; break; case 1: state = (inByte == 0x4d) ? 2 : 0; count = 0; break; case 2: inbyte[count] = inByte; count++; break; default: state = 0; break; } if (count > 29) break; } if (count > 29) { int chkval = 143; for (byte i = 0; i < 28; i++) { chkval += inbyte[i]; } if (chkval == ((int)inbyte[28] * 256 + inbyte[29])) { Serial.println("OK"); _pm2p5 = ((int)inbyte[10]) * 256 + inbyte[11]; _pm10 = ((int)inbyte[12]) * 256 + inbyte[13]; break; } else { Serial.println("error"); state = 0; count = 0; } } } }
|
2.2 12864屏幕显示
屏幕显示主要显示初始化屏幕和实时数据两部分,不做中文显示相对简单,不过多解释,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| SSD1306 display(0x3c, 12, 14); void PrintInitScreen() { display.clear(); display.setTextAlignment(TEXT_ALIGN_LEFT); display.setFont(ArialMT_Plain_24); display.drawString(0, 12, "WIFI"); display.drawString(0, 36, "Connecting..."); display.display(); } void PrintResultScreen(String PM25, String PM10) { display.clear(); display.setTextAlignment(TEXT_ALIGN_LEFT); display.setFont(ArialMT_Plain_24); display.drawString(0, 12, PM25); display.drawString(0, 40, PM10); display.display(); }
|
2.3 数据上传
2.3.1 WIFI操作
WIFI热点的名称和密码最好是保存在EEPROM中,通过读写EEPROM实现配置对修改。本例偷懒直接用变量存储。
首先是判断当前是否包含设备记录的WIFI名称,主要利用了WiFi.scanNetworks()这个库函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| char _ssid[20] = "REPLACE WITH YOURS"; char _pwd[20] = "REPLACE WITH YOURS"; WiFiClient client; int _wifiStatus = WL_IDLE_STATUS; bool IsSSIDExist() { int n = WiFi.scanNetworks(); if (n == 0) return false; else { String ssid = String(_ssid); for (int i = 0; i < n; ++i) { Serial.println(WiFi.SSID(i)); if (ssid.equals(WiFi.SSID(i))) return true; delay(10); } } return false; }
|
发现存在存储的WIFI名后,利用WiFi.begin方法连接到热点。经测试有时会出现一次连不到热点的情况,所以这里用一个循环连续尝试多次连接热点直至成功,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| bool ConnectToWifi() { WiFi.mode(WIFI_STA); if (IsSSIDExist()) { WiFi.begin(_ssid, _pwd); for (int i = 0; i < 25; i++) { delay(5000); Serial.print("Connecting to WPA SSID: "); Serial.println(_ssid); Serial.println(_pwd); Serial.println(_wifiStatus); if (WiFi.status() == WL_CONNECTED) { Serial.println("Wifi connected"); _wifiStatus = WL_CONNECTED; return true; } } } _wifiStatus = WL_DISCONNECTED; return false; }
|
2.3.2 数据上传
数据上传也是采用http post方式,直接用WiFiClient.print方法将http post字符串打印即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #define APIKEY "REPLACE WITH YOURS" #define DEVICEID "REPLACE WITH YOURS" #define LINEBREAK "\r\n"
void UpLoadtoOneNet(char *sensorID, int SensorData) { if (client.connect("api.heclouds.com", 80)) { String strval; int ilength = 56; Serial.println("connecting..."); Serial.println(sensorID); Serial.println(SensorData); client.print("POST http://api.heclouds.com/devices/"); client.print(DEVICEID); Serial.print(DEVICEID); client.print("/datapoints HTTP/1.1"); client.print(LINEBREAK); client.print("Host: "); client.print("api.heclouds.com"); client.print(LINEBREAK); client.print("api-key: "); client.print(APIKEY); client.print(LINEBREAK); client.println("Connection: close"); client.print("Content-Length: "); strval = String(SensorData); ilength += strval.length(); strval = String(sensorID); ilength += strval.length(); client.print(ilength); client.print(LINEBREAK); client.print(LINEBREAK); client.print("{\"datastreams\":[{\"id\":\""); client.print(sensorID); client.print("\",\"datapoints\":[{\"value\":"); client.print(SensorData); client.print("}]}]}"); client.print(LINEBREAK); client.println(LINEBREAK); } else { Serial.println("connection failed"); Serial.println(); Serial.println("disconnecting."); client.stop(); } }
|
2.4 整体框架
程序运行采用状态机的方式,利用定时器每隔60秒完成数据采集、显示和上传,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| Ticker flipper; #define TICK 60 bool _needUpdate = false; void Flip() { _needUpdate = true; }
void SensorDataHandle() { ReadSensorData(); if (_wifiStatus == WL_CONNECTED) { UpLoadtoOneNet((char *)"_pm2p5", _pm2p5); delay(1000); UpLoadtoOneNet((char *)"_pm10", _pm10); } }
void setup() { display.init(); display.flipScreenVertically(); PrintInitScreen(); Serial.begin(9600); SerialG5.begin(9600); if (ConnectToWifi()) { flipper.attach(TICK, Flip); delay(3000); _needUpdate = true; } }
void loop() { if (_needUpdate) { Serial.println("Data Handle"); SensorDataHandle(); char ctmp[6]; String PM25 = "PM2.5:"; sprintf(ctmp, "%d", _pm2p5); PM25 += ctmp; String PM10 = "PM10:"; sprintf(ctmp, "%d", _pm10); PM10 += ctmp; PrintResultScreen(PM25, PM10); _needUpdate = false; } }
|
数据最终上传到了onenet平台,如图5所示。
图5 上传至onenet平台