飘易很久之前就有个想法,自己DIY一套空气质量检测仪,可以实时监测空气的温度、湿度、CO2、TVOC、甲醛、PM2.5等,并通过云端实时记录这些传感器的值,然后通过APP可以方便的查看历史记录,并且通过APP远程控制智能设备上的一些设备比如继电器、电机、全彩LED灯等。
然后2020年春节遇上新冠疫情,得以空出一些时间,飘易就把这个想法落地,开始实施这个空气质量检测仪的DIY方案。
先简单展示一下初步的成果吧。
板子右下角预留的空白地方是留给日后焊接其他元器件用的。
下面我就大概的分几个方面来说,硬件端 、平台端、APP端来说说这样的智能硬件项目怎样去一步步的实现它。
需要的硬件列表(总费用大约200多元):
MEGA 2560单片机(或2560 PRO)
ESP8266-12S wifi模组
0.96寸OLED液晶显示屏或LCD1602
按键一组
温湿度传感器:广州奥松DHT11
CO2&TVOC传感器: 瑞士Sensirion盛思锐 SGP30 (或者瑞典SenseAir森尔CO2二氧化碳传感器 S8-0053,小米新风机用的这款)
甲醛传感器: 英国达特WZ-S(或者国产炜盛ZE08-CH2O,小米有品众筹的霍尼韦尔甲醛监测仪用的是这个传感器)
PM2.5传感器: 日本夏普GP2Y1014AU0F(或者攀藤PM2S-3 PM2.5激光粉尘传感器,小米新风机用的这款)
若干杜邦线
1-2块面包板或万用板
单片机是核心,所有的传感器都归集到MCU这里集中处理。Arduino mega 2560 有2款,一款是 2560(下图左边的板子),还有一款是 贴片版的2560 PRO(下图右边的板子)
怎么选择呢?如果你是打算通过面包板来连接电路的话,那么就选择左边的2560,一般淘宝上有很多卖家在卖的是Arduino MEGA2560 R3改进CH340G 兼容版本,这是国产的板子,一般创客试验可以买这种的。
如果你是打算把电路焊在洞洞板(万用板)上,那么就购买右侧的mega 2560 pro贴片版本,可以直接焊在洞洞板上,缩小体积。
2560 和 2560 PRO 的区别就是一些引脚的区别,功能基本一样,PRO少了一对IIC引脚,只有一组(D20/D21),而2560有2组IIC引脚,但是对于PRO来说,iic是支持多个设备并联的,因此我们可以把多个设备直接接到这个IIC引脚上,通过不同的地址来区分设备即可。
这是一块以ATmega2560为核心的微控制器开发板,本身具有54组数字I/O input/output端(其中14组可做PWM输出),16组模拟比输入端,4组UART(hardware serial ports),使用16 MHz crystal oscillator。由于具有bootloader,因此能夠通过USB直接下载程序而不需经过其他外部烧写器。供电部份可选择由USB直接提供电源,或者使用AC-to-DC adapter及电池作为外部供电。
下面是2560 PRO 的引脚说明:
wifi模组买到手,先刷固件版本到1.6.2,提高下稳定性,关于如何刷固件,可以参考飘易的这篇:乐鑫ESP8266-12S刷固件1.7版本的正确姿势 以及这篇 乐鑫ESP8266烧录固件、升级最新固件、刷MQTT固件 。
刷好了AT固件后,就可以把WIFI模组连接到MCU上去了,我们只需要连接4个线:
RX TX VCC GND
其他线不用连接,除此之外,我们需要设计一个配网按键,当用户按下这个配网按键后,MCU通过AT指令给wifi模组发送配网指令:
//启动smartconfig,支持ESP-Touch和Airkiss智能配网 AT+CWSTARTSMART=3
具体的可以参考:Arduino利用AT指令连接乐鑫ESP8266实现串口通讯、配网
图片就是下面的这种:
引脚定义:
GND: 接地 VCC: 3.3-5V SCL: 串行时钟 SDA: 串行数据
如何编程:
#include "U8glib.h" U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // I2C / TWI // OLED显示,每行16字,共5行,超过80字符不显示 void oledShow(String s) { //u8g.setFont(u8g_font_unifont);// 每行16字母,高10px int l = s.length(); int lineTotal = l / 16;// 共几行 if (l % 16) lineTotal++; String ss = ""; u8g.firstPage(); do { for (int i = 0; i < lineTotal; i++) { if(i >= 5) break; ss = s.substring(i * 16, i * 16 + 16); u8g.setPrintPos(0, (i + 1) * 10 + i * 3);// 坐标,行高10px,行间距3px u8g.print(ss); } } while (u8g.nextPage()); } void setup(void) { u8g.setFont(u8g_font_8x13B);//u8g_font_unifont 高10px }
在需要显示的地方,直接调用
oledShow(F("TEMP:25.00"));
就可以了。
但是,OLED屏在写入数据的时候,会发出滋滋的电流声,这种声音我个人听上去会感觉非常的不舒服,虽然不是很响,但是你调试设备,放在键盘附近的话,这个电流声还是很扰人的。如果你也讨厌这种吱吱的电流声,可以使用LCD屏幕,比如LCD1602,LCD是非常安静的。
DHT11是比较成熟的一种温湿度传感器了。淘宝搜一下一大堆,图片像下面的这种:
引脚定义:
VCC → 3.3V/5V电源正极 GND →电源负极 DATA →单片机IO口
如何编程?
直接引用 arduino IDE 库
#include <DHT.h> // 获取温湿度 void getTempHumi() { // 读取温度或湿度需要约250ms float t = dht.readTemperature();// 温度 float h = dht.readHumidity();// 湿度 // 检查是否读取失败 if (isnan(h) || isnan(t)) { Serial.println(F("Failed to read from DHT sensor!")); return; } temp = String(t); humi = String(h); }
CO2和TVOC的采集我们使用SGP30传感器
这个传感器一般的价格是在50元上下,各位可以自行淘宝下。
SGP30是一款单一芯片上具有多个传感元件的金属氧化物气体传感器,内集成4个气体传感元件,具有完全校准的空气质量输出信号。另外,SGP易于集成,能够将金属氧化物气体传感器集成到移动设备中,为智能家居、家电和物联网应用中的环境监测开辟了新的可能性。
SGP30模块规格:
工作电源电压: 1.8V-5V
功耗:40MA
尺寸: 12 mm x 12 mm x 1.6 mm
接口类型: I2C
如何编程?
我们可以引用Adafruit_SGP30库,里面提供了一些例子,可以从例子里改写程序:
#include "Adafruit_SGP30.h" /* SGP30传感器 温湿度补偿 - return absolute humidity [mg/m^3] with approximation formula @param temperature [°C] @param humidity [%RH] */ uint32_t getAbsoluteHumidity(float temperature, float humidity) { // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15 const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3] const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3] return absoluteHumidityScaled; } // 获取CO2 & TVOC void getCo2Tvoc(float temperature, float humidity) { // 温湿度补偿 - If you have a temperature / humidity sensor, you can set the absolute humidity to enable the humditiy compensation for the air quality signals //float temperature = 22.1; // [°C] //float humidity = 45.2; // [%RH] sgp.setHumidity(getAbsoluteHumidity(temperature, humidity)); if (! sgp.IAQmeasure()) { Serial.println("Measurement failed"); return; } CO2 = sgp.eCO2;// 单位ppm TVOC = sgp.TVOC;// 单位ppb }
甲醛传感器,飘易使用的英国达特的wz-s,这个是带ph2.0 连接线的,还有另外一款是 wz-s-k,这个是插针版本的。这2款的技术指标是一样的,只是对外的引脚方式不同而已。大家可以根据需要自行选择合适的传感器。
WZ-S 型甲醛检测模组是全球甲醛检测专家——英国达特公司的最新力作,采用 升级版达特甲醛传感器结合先进的微检测技术,直接将环境中的甲醛含量转换成 浓度值,标准化数字输出,便于客户集成使用。WZ-S 型甲醛检测模组经过严格 的工厂校准,可直接应用于您的检测体系中。
技术指标如下:
wz-s和单片机通讯是采用串口通讯的,我们只要接四个线就可以了:
VCC GND RX TX
通讯串口协议:
通讯分主动上传和问答式,出厂默认为主动上传,每隔 1 秒发送 1 次浓度值。
飘易采用问答式。切换到问答式,命令行格式如下:
如何编程?
// 获取甲醛 void getCH2O() { int flag_end = false; int flag_start = false; int count = 0; byte buffer[9] = {}; for (int i = 0; i < 9; i++) { CH2OSerial.write(Ask_code[i]);//在问答模式下请求数据 } sleep(10); // 读取 while (flag_end == false) { if (CH2OSerial.available() > 0) {//接收到数据 byte inChar = CH2OSerial.read(); buffer[count] = inChar; if (buffer[count] == 0xFF) { //接收到起始标志 count = 0; flag_start = true; } count++; if (count >= 9 ) { //接收9个byte数据 count = 0; if (flag_start) { // 校验 且 命令号是 0X86 if (buffer[8] == FucCheckSum(buffer, 9) && buffer[1] == 0x86 ) { //校验 flag_end = true; } } } } } if (flag_end) { int h1 = (int)buffer[2] * 256 + (int)buffer[3];// 单位ug/m3,国家标准是80ug/m3(0.08mg/m3) //int h2 = (int)buffer[6] * 256 + (int)buffer[7];// 单位ppb CH2O = String(h1); } } // 甲醛校验函数 unsigned char FucCheckSum(unsigned char *i, unsigned char ln) { unsigned char j, tempq = 0; i += 1; for (j = 0; j < (ln - 2); j++) { tempq += *i; i++; } tempq = (~tempq) + 1; return (tempq); }
PM2.5传感器,飘易目前选用的是日本夏普GP2Y1014AYU0F,这是一款性价比很高的传感器,淘宝上的价格一般在20元左右,它的前一代产品是夏普 GP2Y1010AYU0F,目前已经被1014代替了。这是基于红外的灰尘传感器,如果你希望拥有更高的精确度,可以选用激光类型的传感器。
一般发货清单如下:
1,GP2Y1014AU0F 灰尘传感器(1个)代替已经停产的GP2Y1010AU0F
2,150ohm的电阻(1个)
3,220uF的电容(1个)
4,6pin连接线(1个)
技术参数:
电源电压:5-7V
工作温度:-10-65摄氏度
消耗电流:20mA最大
最小粒子检出值:0.8微米
灵敏度:0.5V/(0.1mg/m3)
清洁空气中电压:0.9V 典型值
工作温度:-10~65℃
存储温度:-20~80℃
使用寿命:5年
尺寸大小:46mm×30mm×17.6mm
重量大小:15g
这个传感器总共有6口,ph2.0接线,怎么连接呢?
实际接线像下面的这样:
注意LED-VCC引脚需要接一个150欧姆的电阻后再到VCC,同时再接一个220uF的电容再到GND,不能直接接VCC哦。
电压和灰尘之间的关系图:
我们需要根据上面的这个关系图,推导一个传感器输出电压和灰尘浓度之间的关系:
float dustDensity = (0.17 * voltage - 0.1) * 1000; // 单位:ug/m3
编程:
// PM2.5传感器 - 夏普GP2Y1014AU0F String PM25 = "0"; int dustPin = A1;//粉尘传感器模拟输入引脚 int dustLedPower = 29;//粉尘传感器led数字引脚 int dustDelayTime = 280;//采样时间为280微秒 int dustDelayTime2 = 40;//测量完后脉冲需要继续保持,保持时间为320-280=40 int dustOffTime = 9680;//LED脉冲周期为10毫秒,故此处为10000-320=9680 // 获取PM2.5 void getPm25() { // 10ms采样周期 digitalWrite(dustLedPower, LOW); delayMicroseconds(dustDelayTime); float dustVal = analogRead(dustPin); delayMicroseconds(dustDelayTime2); digitalWrite(dustLedPower, HIGH); delayMicroseconds(dustOffTime); // 电压 float voltage = dustVal * (5.0 / 1024); // 粉尘密度 0.9v洁净空气,3.6V最大污染-512ug/m3 float dustDensity = (0.17 * voltage - 0.1) * 1000; // 单位:ug/m3 if (dustDensity < 0) dustDensity = 0; int density = dustDensity * 1.0;// 取整 //Serial.println(dustDensity); PM25 = String(density); }
其他还有一些继电器、全彩LED灯(可以实现1600万种颜色变化)、步进电机(正反转、0-100%转速)等设备的远程控制,就不在本篇文章里展开叙述了。
平台端也就是我们的服务器端,该怎么设计呢?
因为我们的终端设备是通过ESP8266 WIFI模组和服务器保持长连接的,因此需要服务器选择支持长链接的的技术实施方案。传统的PHP并不适合用在这样的智能硬件项目里,在java里有大量的实现方案,如果你想采用java来开发服务器端,那么会有很多成熟的套件供你选用。
除了java,我们还可以搭建 MQTT 服务器,参考:源码编译安装EMQX V3.0服务器(MQTT) 以及 EMQX之Kafka插件编译安装
那么PHP这块还有解决方案吗?可以基于SWOOLE,我们实现长链接。
关于Swoole的并发能力测试《并发10万TCP连接的测试》:
本测试启动10个子进程,发起长连接到Swoole的TCP Server.
由于单台机器的原因,ip_local_port_range的范围是32000-60000。 运行到28000个长连接时,由于local port不够用,无法再继续连接。并发测试中使用4台客户端机器同时连接服务器,每台机器与服务器建立2.8W个TCP连接。TCP Server共维持了11.2W长连接。
Swoole使用epoll作为事件轮询,可维持大量TCP连接。只要操作系统的内存足够,就一直可以增加维持的TCP长连接。swoole_server每个连接所占用的内存为220字节,使用数据缓存,如EOF_CHECK/LENGTH_CHECK后可能会增加到每连接8K。
来源:https://wiki.swoole.com/wiki/page/p-c100k.html
服务器和终端设备之间采取TCP长连接,解决了设备和服务器之间的实时通讯 。那么,别忘了,我们还有APP端口,用户在APP上打开页面的时候,需要接收传感器的实时状态,和远程下发控制命令给设备,这又怎么做到呢?也就是如何解决用户(APP)和设备的实时通讯?
幸运的是,swoole也可以方便的创建 websocket 服务器,APP端可以通过websocket和服务器保持长链接,这样即使我们的部分APP页面是基于webview的形式集成的也没关系,html5+js也可以通过 websocket 和服务器保持长链接。
这样,我们只需要在服务器器端做好转发就好了,APP通过websocket下发控制指令的时候先给服务器发送,服务器收到后找到对应的设备,通过TCP长连接转发指令给设备;设备上报状态的时候,先通过TCP上报给服务器,服务器收到后,立即通过websocket再转发给对应的websocket客户端(APP)或h5网页就OK了。
至此,APP(或H5网页) + 服务器 (云端)+ 终端设备 这三者就实现了实时通讯,互联互通了。
APP端我们采用html 5 plus的集成解决方案(标准)。我们分别集成安卓和iOS的离线sdk到我们的app里面。
iOS如何集成离线5+sdk:iOS离线打包5+SDK并编写自定义插件
Android如何集成离线5+SDK:Hbuilder离线打包及蓝牙插件开发Android注意事项
最重要的是我们需要在APP里面实现为ESP8266 WIFI模组配网的功能,乐鑫已经为我们提供了开源的项目demo了,我们只需要把乐鑫的库离线集成到app里面就OK了。
配网采用 ESPTouch 技术方案。乐鑫自主研发的 ESP-TOUCH 协议采用的是 Smart Config(智能配置)技术来帮助用户将嵌入了 ESP8266EX 的设备连接至 Wi-Fi 网络。用户只需在手机上进行简单操作即可实现智能配置。
步骤描述:
1.用户创建任务对象,其中包含参数对象、服务器对象和客户端对象
2.设置超时时间与成功返回设备上限数值
3.获取本地ip地址
4.创建配网对象
5.配网对象包含导向数据对象、主数据对象
6.导向数据对象以固定格式生成,主要用于告知设备解析主数据时的一个参数
7.主数据对象包含:总长度、密码长度、ssid的crckey、bssid的crckey、本机ip地址、密码内容和前面所有数据异或的校验位一个字节
8.如果该ssid是隐藏ssid,则需要在总长度加上ssid的长度,以及主数据对象中包含ssid的数据
9.将导向数据对象和主数据对象,以一定格式转化为两个byte数组
10.使用服务器对象开启监听,设备回复的数据
11.客户端对象开始循环发送导向数据和主数据,导向数据发两秒,主数据发四秒,六秒一个循环,直到收到回复或者超时
12.如果超时还没有成功,则直接返回给用户失败
13.如果服务器对象收到了设备返回的信息,则对比第一个字符与ssid拼接密码的字节长度加9是否相等
14.如果不相等忽略,如果相等则解析出回复数据中的设备bssid和设备ip地址
15.将获取到的所有设备的bssid和ip地址返回给用户
16.清理资源
备注:
1.导向数据实际长度(数据长度+udp+ip层) - 数据长度 = 基准值(udp+ip层)
2.将每个子数据对象变为六个字节的数据后,每两个字节合并成一个uint16的数据并且加上固定值40存起来
3.发送完成导向数据后,每两个字节发送一个包
乐鑫iOS的ESPTouch:https://github.com/EspressifApp/EsptouchForIOS
乐鑫Android的ESPTouch:https://github.com/EspressifApp/EsptouchForAndroid
注意,这一部分需要你同时具备html+js+安卓原生(java)+iOS原生(objective c)的开发能力。
开发完成后,飘易同时发布了安卓版本的app和在苹果app store里上架了这一款应用。
到这里,整个智能硬件项目就可以告一段落了。目前先放在面包板上做演示,这一版本暂时为1.0,后续再用洞洞板把相应的传感器焊接上去。 到时再分享给大家吧。
如果你也有智能硬件的项目,或者希望实现智能硬件 + 平台(云端) + APP(微信或小程序)终端等一整套解决方案,欢迎和我交流哦:
(END)