其实这个也是做烂的项目,最近帮人带学生做了一个,在此简单说明下。

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; //0x42+0x4d
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 //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平台