![cover](https://img-blog.csdnimg.cn/c728e3027392443396d6f31a50e0dd74.jpeg)
前端程序员是怎么做物联网开发的
前端如何做物联网开发
概述和基础讲解
该项目用到的技术有:
前端:jq、less、echarts、mqtt.js
后端:eggjs、egg-emqtt
数据库:mysql
服务器:emqx(mqtt broker)
硬件:
板子:wemos D1 R2(设计基于 Arduino Uno R3 , 搭载esp8266 wifi模块)
调试工具:mqttx、Arduino IDE v2.0.3 使用Arduino C开发
必备知识:
nodejs(eggjs框架)能面向业务即可
mysql 能写基本插入查询语句即可
C语言的基本语法了解即可
知道mqtt协议的运作方式即可
arduino 开发板或任何其他电路板的初步了解即可
简单介绍一下上面几个的知识点:
从来没有后端学习经验的同学,推荐一个全栈项目供你参考:vue-xmw-admin-pro ,该项目用到了 前端VUE、后端eggjs、mysql、redis,对全栈学习很有帮助。
mysql 只需要知道最简单的插入和查询语句即可,在本项目中,其实使用mongodb是更合适的,但是我为了方便,直接用了现成的mysql
即使你不知道C语言的基本语法,也可以在一小时内快速了解一下,知道简单的定义变量、函数、返回值即可
MQTT(消息队列遥测传输)是一种网络协议(长连接,意思就是除了客户端可以主动向服务器通信外,服务器也可以主动向客户端发起),也是基于TCP/IP的,适用于算力低下的硬件设备使用,基于发布\订阅范式的消息协议,具体示例如下:
![](https://i-blog.csdnimg.cn/blog_migrate/f63ac44c66159bdbcf379336ad377f17.png)
当某客户端想发布消息时,图大概长这样:
![](https://i-blog.csdnimg.cn/blog_migrate/2dafe0f0e12b857c083069aa61eb4345.png)
由上图可知,当客户端通过验证上线后,还需要订阅主题,当某客户端向某主题发布消息时,只有订阅了该主题的客户端会收到broker的转发。
举一个简单的例子:你和我,还有他,我们把自己的名字、学号报告给门卫大爷(broker),门卫大爷就同意我们在警卫室玩一会,警卫室有无数块黑板(topic),我们每个人都可以向门卫请求:如果某黑板上被人写了字,请转告给我。门卫会记住每个人的要求,比如当你向一块黑板写了字(你向某topic发送了消息),所有要求门卫告诉的人都会被门卫告知你写了什么(如果你也要求被告知,那么也包括你自己)。
开发板可以被写入程序,程序可以使用简单的代码控制某个针脚的高低电平,或者读取某针脚的数据。
开始
购买 wemos d1开发板、DHT11温湿度传感器,共计19.3元。
使用arduino ide(以下简称ide) 对wemos d1编程需要下载esp8266依赖 参见:Arduino IDE安装esp8266 SDK
在ide的菜单栏选择:文件>首选项>其他开发板管理器地址填入:http://arduino.esp8266.com/stable/package_esp8266com_index.json,可以顺便改个中文
安装ch340驱动参见: win10 安装 CH340驱动 实测win11同样可用
使用 micro-usb 线,连接电脑和开发板,在ide菜单中选择:工具>开发板>esp8266>LOLIN(WEMOS) D1 R2 & mini
选择端口,按win+x,打开设备管理器,查看你的ch340在哪个端口,在ide中选择对应的端口
当ide右下角显示LOLIN(WEMOS) D1 R2 & mini 在comXX上时,连接就成功了
打开ide菜单栏 :文件>示例>esp8266>blink,此时ide会打开新窗口,在新窗口点击左上角的上传按钮,等待上传完成,当板子上的灯一闪一闪,就表明:环境、设置、板子都没问题,可以开始编程了,如果报错,那么一定是哪一步出问题了,我相信你能够根据错误提示找出到底是什么问题,如果实在找不出问题,那么可能买到了坏的板子(故障率还是蛮高的)
wemos d1 针脚中有一个 3.3v电源输出,三个或更多的GND接地口,当安装DHT11传感器元件时,需要将正极插入3.3v口,负极插入GND口,中间的数据线插入随便的数字输入口,比如D5口(D5口的PIN值是14,后面会用到)。
使用DHT11传感器,需要安装库:DHT sensor library by Adafruit , 在ide的左侧栏中的库管理中直接搜索安装即可
下面是一个获取DHT11数据的简单示例,如果正常的话,在串口监视器中,会每秒输出温湿度数据
#include"DHT.h"//这是依赖或者叫库,或者叫驱动也行#include"string.h"#defineDHTPIN14// DHT11数据引脚连接到D5引脚 D5引脚的PIN值是14#defineDHTTYPEDHT11 // 定义DHT11传感器
DHT dht(DHTPIN, DHTTYPE);//初始化传感器voidsetup(){
Serial.begin(115200);//wemos d1 的波特率是 115200pinMode(BUILTIN_LED, OUTPUT);//设置一个输出的LED
dht.begin();//启动传感器}char*getDHT11Data(){float h = dht.readHumidity();//获取湿度值float t = dht.readTemperature();//获取温度值staticchar data[100];if(isnan(h)||isnan(t)){
Serial.println("Failed to read from DHT sensor!");sprintf(data,"Temperature: %.1f, Humidity: %.1f",0.0,0.0);//如果任何一个值没有值,直接返回两个0.0,这样我们就知道传感器可能出问题了return data;}sprintf(data,"Temperature: %.1f, Humidity: %.1f", t, h);//正常就取到值,我这里拼成了一句话return data;}voidloop(){char* data =getDHT11Data();//此处去取传感器值
Serial.println("got: "+String(data));// 打印主题内容delay(1000);//每次循环延迟一秒}
继续
到了这一步,如果你用的是普通的arduino uno r3板子,就可以结束了。
取到数据之后,你就可以根据数据做一些其他的事情了,比如打开接在d6引脚上的继电器,而这个继电器控制着一个加湿器。
如果你跟我一样,使用了带wifi网络的板子,就可以继续跟我做。
我们继续分步操作:
设备端:
引入esp8266库(上面已经提到安装过程)
#include"ESP8266WiFi.h"
安装mqtt客户端库 ,直接在库商店搜索 PubSubClient ,下载 PubSubClient by Nick O'Leary 那一项,下载完成后:
#include"PubSubClient.h"
至此,库文件已全部安装引入完毕
设置 wifi ssid(即名字) 和 密码,如:
char* ssid ="2104";char* passwd ="13912428897";
尝试连接 wifi
WiFiClient espClient;int isConnect =0;voidconnectWIFI(){
isConnect =0;
WiFi.mode(WIFI_STA);//不知道什么意思,照着写就完了
WiFi.begin(ssid, passwd);//尝试连接int timeCount =0;//尝试次数while(WiFi.status()!= WL_CONNECTED){//如果没有连上,继续循环for(int i =200; i <=255; i++){analogWrite(BUILTIN_LED, i);delay(2);}for(int i =255; i >=200; i--){analogWrite(BUILTIN_LED, i);delay(2);}// 上两个循环共计200ms左右,在控制LED闪烁而已,你也可以不写
Serial.println("wifi connecting......"+String(timeCount));
timeCount++;
isConnect =1;//每次都需要把连接状态码设置一下,只有连不上时设置为0// digitalWrite(BUILTIN_LED, LOW);if(timeCount >=200){// 当40000毫秒时还没连上,就不连了
isConnect =0;//设置状态码为 0break;}}if(isConnect ==1){
Serial.println("Connect to wifi successfully!"+String("SSID is ")+ WiFi.SSID());
Serial.println(String("mac address is ")+ WiFi.macAddress());// digitalWrite(BUILTIN_LED, LOW);analogWrite(BUILTIN_LED,250);//设置LED常亮,250的亮度对我来说已经很合适了settMqttConfig();//尝试连接mqtt服务器,在下一步有详细代码}else{analogWrite(BUILTIN_LED,255);//设置LED常灭,不要问我为什么255是常灭,因为我的灯是高电平熄灭的//连接wifi失败,等待一分钟重连delay(60000);}}
尝试连接 mqtt
constchar* mqtt_server ="larryblog.top";//这里是我的服务器,当你看到这篇文章的时候,很可能已经没了,因为我的服务器还剩11天到期constchar* TOPIC ="testtopic";// 设置信息主题constchar* client_id ="mqttx_3b2687d2";//client_id不可重复,可以随便取,相当于你的网名
PubSubClient client(espClient);voidsettMqttConfig(){
client.setServer(mqtt_server,1883);//设定MQTT服务器与使用的端口,1883是默认的MQTT端口
client.setCallback(onMessage);//设置收信函数,当订阅的主题有消息进来时,会进这个函数
Serial.println("try connect mqtt broker");
client.connect(client_id,"wemos","aa995231030");//后两个参数是用户名密码
client.subscribe(TOPIC);//订阅主题
Serial.println("mqtt connected");//一切正常的话,就连上了}//收信函数voidonMessage(char* topic, byte* payload,unsignedint length){
Serial.print("Message arrived [");
Serial.print(topic);// 打印主题信息
Serial.print("]:");char* payloadStr =(char*)malloc(length +1);memcpy(payloadStr, payload, length);
payloadStr[length]='\0';
Serial.println(payloadStr);// 打印主题内容if(strcmp(payloadStr,(char*)"getDHTData")==0){char* data =getDHT11Data();
Serial.println("got: "+String(data));// 打印主题内容
client.publish("wemos/dht11", data);}free(payloadStr);// 释放内存}
发送消息
client.publish("home/status/","{device:client_id,'status':'on'}");//注意,这里向另外一个主题发送的消息,消息内容就是设备在线,当有其他的客户端(比如web端)订阅了此主题,便能收到此消息
至此,板子上的代码基本上就写完了,完整代码如下:
#include"ESP8266WiFi.h"#include"PubSubClient.h"#include"DHT.h"#include"string.h"#defineDHTPIN14// DHT11数据引脚连接到D5引脚#defineDHTTYPEDHT11 // DHT11传感器
DHT dht(DHTPIN, DHTTYPE);char* ssid ="2104";char* passwd ="13912428897";constchar* mqtt_server ="larryblog.top";constchar* TOPIC ="testtopic";// 订阅信息主题constchar* client_id ="mqttx_3b2687d2";int isConnect =0;
WiFiClient espClient;
PubSubClient client(espClient);long lastMsg =0;voidsetup(){
Serial.begin(115200);// Set WiFi to station modeconnectWIFI();pinMode(BUILTIN_LED, OUTPUT);
dht.begin();}char*getDHT11Data(){float h = dht.readHumidity();float t = dht.readTemperature();staticchar data[100];if(isnan(h)||isnan(t)){
Serial.println("Failed to read from DHT sensor!");sprintf(data,"Temperature: %.1f, Humidity: %.1f",0.0,0.0);return data;}sprintf(data,"Temperature: %.1f, Humidity: %.1f", t, h);return data;}voidconnectWIFI(){
isConnect =0;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, passwd);int timeCount =0;while(WiFi.status()!= WL_CONNECTED){for(int i =200; i <=255; i++){analogWrite(BUILTIN_LED, i);delay(2);}for(int i =255; i >=200; i--){analogWrite(BUILTIN_LED, i);delay(2);}// 上两个循环共计200ms左右
Serial.println("wifi connecting......"+String(timeCount));
timeCount++;
isConnect =1;// digitalWrite(BUILTIN_LED, LOW);if(timeCount >=200){// 当40000毫秒时还没连上,就不连了
isConnect =0;break;}}if(isConnect ==1){
Serial.println("Connect to wifi successfully!"+String("SSID is ")+ WiFi.SSID());
Serial.println(String("mac address is ")+ WiFi.macAddress());// digitalWrite(BUILTIN_LED, LOW);analogWrite(BUILTIN_LED,250);settMqttConfig();}else{analogWrite(BUILTIN_LED,255);//连接wifi失败,等待一分钟重连delay(60000);}}voidsettMqttConfig(){
client.setServer(mqtt_server,1883);//设定MQTT服务器与使用的端口,1883是默认的MQTT端口
client.setCallback(onMessage);
Serial.println("try connect mqtt broker");
client.connect(client_id,"wemos","aa995231030");
client.subscribe(TOPIC);
Serial.println("mqtt connected");}voidonMessage(char* topic, byte* payload,unsignedint length){
Serial.print("Message arrived [");
Serial.print(topic);// 打印主题信息
Serial.print("]:");char* payloadStr =(char*)malloc(length +1);memcpy(payloadStr, payload, length);
payloadStr[length]='\0';
Serial.println(payloadStr);// 打印主题内容if(strcmp(payloadStr,(char*)"getDHTData")==0){char* data =getDHT11Data();
Serial.println("got: "+String(data));// 打印主题内容
client.publish("wemos/dht11", data);}free(payloadStr);// 释放内存}voidpublishDhtData(){char* data =getDHT11Data();
Serial.println("got: "+String(data));// 打印主题内容
client.publish("wemos/dht11", data);delay(2000);}voidreconnect(){
Serial.print("Attempting MQTT connection...");// Attempt to connectif(client.connect(client_id,"wemos","aa995231030")){
Serial.println("reconnected successfully");// 连接成功时订阅主题
client.subscribe(TOPIC);}else{
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");// Wait 5 seconds before retryingdelay(5000);}}voidloop(){if(!client.connected()&& isConnect ==1){reconnect();}if(WiFi.status()!= WL_CONNECTED){connectWIFI();}
client.loop();publishDhtData();long now =millis();if(now - lastMsg >2000){
lastMsg = now;
client.publish("home/status/","{device:client_id,'status':'on'}");}// Wait a bit before scanning againdelay(1000);}
服务器
刚才的一同操作很可能让人一头雾水,相信大家对上面mqtt的操作还是一知半解的,不过没有关系,通过对服务端的设置,你会对mqtt的机制了解的更加透彻
我们需要在服务端部署 mqtt broker,也就是mqtt的消息中心服务器
在网络上搜索 emqx , 点击 EMQX: 大规模分布式物联网 MQTT 消息服务器 ,这是一个带有可视化界面的软件,而且画面特别精美,操作特别丝滑,功能相当强大,使用起来基本上没有心智负担。点击立即下载,并选择适合你的服务器系统的版本:
![](https://i-blog.csdnimg.cn/blog_migrate/11dc1b40cb85f5163fa44339affc17d3.png)
这里拿 ubuntu和windows说明举例,相信其他系统也都大差不差
在ubuntu上,推荐使用apt下载,按上图步骤操作即可,如中途遇到其他问题,请自行解决
sudo ufw status 查看开放端口,一般情况下,你只会看到几个你手动开放过的端口,或者只有80、443端口
udo ufw allow 18083 此端口是 emqx dashboard 使用的端口,开启此端口后,可以在外网访问 emqx看板控制台
![](https://i-blog.csdnimg.cn/blog_migrate/996f9d761f3d58cb41091912b4442f67.png)
当你看到如图所示的画面,说明已经开启成功了
windows下直接下载安装包,上传到服务器,双击安装即可
打开 “高级安全Windows Defender 防火墙”,点击入站规则>新建规则
点击端口 > 下一步
点击TCP、特定本地端口 、输入18083,点击下一步
一直下一步到最后一步,输入名称,推荐输入 emqx 即可
![](https://i-blog.csdnimg.cn/blog_migrate/f48edb617e9f0aa3f48decef460d881b.png)
当你看到如图所示画面,说明你已经配置成功了。
完成服务端程序安装和防火墙端口配置后,我们需要配置服务器后台的安全策略,这里拿阿里云举例:
如果你是 ESC 云主机,点击实例>点击你的服务器名>安全组>配置规则>手动添加
添加这么一条即可:
![](https://i-blog.csdnimg.cn/blog_migrate/48c4b9cfd1dd9fa53d8b6156fc960cd7.png)
如果你是轻量服务器,点击安全>防火墙>添加规则 即可,跟esc设置大差不差。
完成后,可以在本地浏览器尝试访问你的emqx控制台
![](https://i-blog.csdnimg.cn/blog_migrate/58d63d6e6f03283adde7cad2eb7d8ff4.png)
直接输入域名:18083即可,初始用户名为admin,初始密码为public,登录完成后,你便会看到如下画面
![](https://i-blog.csdnimg.cn/blog_migrate/3c6e336cb41df29fe76c23ad35b2cdad.png)
接下来需要配置 客户端登录名和密码,比如刚刚在设备中写的用户名密码,就是在这个系统中设置的
点击 访问控制>认证 > 创建,然后无脑下一步即可,完成后你会看到如下画面
![](https://i-blog.csdnimg.cn/blog_migrate/e1687a988a6ad894f717d70f6c892ba1.png)
点击用户管理,添加用户即可,用户名和密码都是自定义的,这些用户名密码可以分配给设备端、客户端、服务端、测试端使用,可以参考我的配置
![](https://i-blog.csdnimg.cn/blog_migrate/5471a96f71a2dee238a88755dd646314.png)
userClient是准备给前端页面用的 ,server是给后端用的,995231030是我个人自留的超级用户,wemos是设备用的,即上面设备连接时输入的用户名密码。
至此,emqx 控制台配置完成。
下载 mqttx,作为测试端尝试连接一下
![](https://i-blog.csdnimg.cn/blog_migrate/5a6a596697a27a3e156a033ae1ef12b5.png)
点击连接,你会发现,根本连接不上......
因为,1883(mqtt默认端口)也是没有开启的,当然,和开启18083的方法一样。
同时,还建议你开启:
1803 websocket 默认端口
1804 websockets 默认端口
3306 mysql默认端口
后面这四个端口都会用到。
当你开启完成后,再次尝试使用mqttx连接broker,会发现可以连接了
![](https://i-blog.csdnimg.cn/blog_migrate/98df86974d023d81be383f9575ab9262.png)
这个页面的功能也是很易懂的,我们在左侧添加订阅,右侧的聊天框里会出现该topic的消息
![](https://i-blog.csdnimg.cn/blog_migrate/ff0c2f2e35aecf3c9b3b928efa3bb0af.png)
你是否还记得,在上面的设备代码中,我们在loop中每一秒向 home/status/ 发送一条设备在线的提示,我们现在在这里就收到了。
当你看到这些消息的时候,就说明,你的设备、服务器、emqx控制台已经跑通了。
前后端以及数据库
前端
前端不必多说,我们使用echarts承载展示数据,由于体量较小,我们不使用任何框架,直接使用jq和echarts实现,这里主要讲前端怎么连接mqtt
首先引入mqtt库
<scriptsrc="https://cdn.bootcdn.net/ajax/libs/mqtt/4.1.0/mqtt.min.js"></script>
然后设置连接参数
const options ={
clean:true,// true: 清除会话, false: 保留会话
connectTimeout:4000,// 超时时间
clientId:'userClient_'+generateRandomString(),//前端客户端很可能比较多,所以这里我们生成一个随机的6位字母加数字作为clientId,以保证不会重复
username:'userClient',
password:'aa995231030',}functiongenerateRandomString(){let result ='';let characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let charactersLength = characters.length;for(let i =0; i <6; i++){
result += characters.charAt(Math.floor(Math.random()* charactersLength));}return result;}
连接
// const connectUrl = 'mqtt://larryblog.top/mqtt' 当然你可以使用mqtt协议,但是有可能会遇到 ssl 跨域的问题,如果你不使用 https 可以忽略这一项,直接使用mqtt即可const connectUrl ='wss://larryblog.top/mqtt'//注意,这里使用了nginx进行转发,后面会讲const client = mqtt.connect(connectUrl, options)
因为前端代码不多,我这里直接贴了
html:
index.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><linkrel="stylesheet/less"href="./style.less"><linkrel="stylesheet"href="//at.alicdn.com/t/c/font_3712319_bzaequy11dn.css"><scriptsrc="https://cdn.bootcdn.net/ajax/libs/less.js/4.1.3/less.js"></script><title>wemos d1 test</title></head><body><divclass="app"id="app"><divid="deviceStatus"><spanclass="statusLight"></span><spanid="statusText">Loading device status</span><!-- <span class="iconfont icon-xinxi"></span> --></div><divclass="container"><divclass="Temperature"><divid="echartsViewTemperature"></div><span>Current temperature:</span><spanid="Temperature">loading...</span></div><divclass="Humidity"><divid="echartsViewHumidity"></div><span>Current humidity:</span><spanid="Humidity">loading...</span></div></div></div></body><scriptsrc="./showTip.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script><scriptsrc="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script><scriptsrc="https://cdn.bootcdn.net/ajax/libs/mqtt/4.1.0/mqtt.min.js"></script><scriptsrc="https://cdn.staticfile.org/echarts/4.7.0/echarts.js"></script><scriptsrc="./echarts.js?v=1.0.0"></script><scriptsrc="./mqttController.js"></script></html>
mqttController.js
// const mqtt = require('mqtt')$(document).ready(()=>{// Welcome to request my open interface. When the device is not online, the latest 2000 pieces of data will be returned
$.post("https://larryblog.top/api",{
topic:"getWemosDhtData",
skip:0},(data, textStatus, jqXHR)=>{setData(data.res)// console.log("line:77 data==> ", data)},);// for (let i = 0; i <= 10; i++) {// toast.showToast(1, "test")// }const options ={
clean:true,// true: 清除会话, false: 保留会话
connectTimeout:4000,// 超时时间// Authentication information
clientId:'userClient_'+generateRandomString(),
username:'userClient',
password:'aa995231030',// You are welcome to use my open mqtt broker(My server is weak but come on you). When connecting, remember to give yourself a personalized clientId to prevent being squeezed out// Topic rule:// baseName/deviceId/events}// 连接字符串, 通过协议指定使用的连接方式// ws 未加密 WebSocket 连接// wss 加密 WebSocket 连接// mqtt 未加密 TCP 连接// mqtts 加密 TCP 连接// wxs 微信小程序连接// alis 支付宝小程序连接let timer;let isShowTip =1const connectUrl ='wss://larryblog.top/mqtt'const client = mqtt.connect(connectUrl, options)
client.on('connect',(error)=>{
console.log('已连接:', error)
toast.showToast("Broker Connected")
timer =setTimeout(onTimeout,3500);// 订阅主题
client.subscribe('wemos/dht11',function(err){if(!err){// 发布消息
client.publish('testtopic','getDHTData')}})
client.subscribe('home/status/')
client.publish('testtopic','Hello mqtt')})
client.on('reconnect',(error)=>{
console.log('正在重连:', error)
toast.showToast(3,"reconnecting...")})
client.on('error',(error)=>{
console.log('连接失败:', error)
toast.showToast(2,"connection failed")})
client.on('message',(topic, message)=>{// console.log('收到消息:', topic, message.toString())switch(topic){case"wemos/dht11":const str = message.toString()const arr = str.split(", ");// 分割字符串const obj = Object.fromEntries(arr.map(s=> s.split(": ")));// 转化为对象
document.getElementById("Temperature").innerHTML = obj.Temperature +" ℃"
optionTemperature.xAxis.data.push(moment().format("MM-DD/HH:mm:ss"))
optionTemperature.xAxis.data.length >=100&& optionTemperature.xAxis.data.shift()
optionTemperature.series[0].data.length >=100&& optionTemperature.series[0].data.shift()
optionTemperature.series[0].data.push(parseFloat(obj.Temperature))
ChartTemperature.setOption(optionTemperature,true);
document.getElementById("Humidity").innerHTML = obj.Humidity +" %RH"
optionHumidity.xAxis.data.push(moment().format("MM-DD/HH:mm:ss"))
optionHumidity.xAxis.data.length >=100&& optionHumidity.xAxis.data.shift()
optionHumidity.series[0].data.length >=100&& optionHumidity.series[0].data.shift()
optionHumidity.series[0].data.push(parseFloat(obj.Humidity))
ChartHumidity.setOption(optionHumidity,true);breakcase"home/status/":$("#statusText").text("device online")deviceOnline()$(".statusLight").removeClass("off")$(".statusLight").addClass("on")clearTimeout(timer);
timer =setTimeout(onTimeout,3500);break}})functiondeviceOnline(){if(isShowTip){
toast.showToast(1,"device online")}
isShowTip =0}functionsetData(data){// console.log("line:136 data==> ", data)for(let i = data.length -1; i >=0; i--){let item = data[i]// console.log("line:138 item==> ", item)
optionTemperature.series[0].data.push(item.temperature)
optionHumidity.series[0].data.push(item.humidity)
optionHumidity.xAxis.data.push(moment(item.updateDatetime).format("MM-DD/HH:mm:ss"))
optionTemperature.xAxis.data.push(moment(item.updateDatetime).format("MM-DD/HH:mm:ss"))}
ChartTemperature.setOption(optionTemperature);
ChartHumidity.setOption(optionHumidity);}functiononTimeout(){$("#statusText").text("device offline")
toast.showToast(3,"device offline")
isShowTip =1
document.getElementById("Temperature").innerHTML ="No data"
document.getElementById("Humidity").innerHTML ="No data"$(".statusLight").removeClass("on")$(".statusLight").addClass("off")}functiongenerateRandomString(){let result ='';let characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';let charactersLength = characters.length;for(let i =0; i <6; i++){
result += characters.charAt(Math.floor(Math.random()* charactersLength));}return result;}});
showTip.js 是我发布在npm上的一个包,如果有需要可以自行npm下载
style.less
*{padding: 0;margin: 0;color: #fff;}.app{background: #1b2028;width: 100vw;height: 100vh;display: flex;flex-direction: column;overflow: hidden;#deviceStatus{display: flex;align-items: center;gap: 10px;padding: 20px;.statusLight{display: block;height: 10px;width: 10px;border-radius: 100px;background: #b8b8b8;&.on{background: #00a890;}&.off{background: #b8b8b8;}}}.container{width: 100%;height: 0;flex: 1;display: flex;@media screen and (max-width: 768px){flex-direction: column;}>div{flex: 1;height: 100%;text-align: center;#echartsViewTemperature,
#echartsViewHumidity{width: 80%;height: 50%;margin: 10px auto;// background: #eee;}}}}
echarts.js 这个文件是我自己写的,别学我这种命名方式,这是反例
let optionTemperature =nulllet ChartTemperature =null$(document).ready(()=>{setTimeout(()=>{// waiting
ChartTemperature = echarts.init(document.getElementById('echartsViewTemperature'));
ChartHumidity = echarts.init(document.getElementById('echartsViewHumidity'));// 指定图表的配置项和数据
optionTemperature ={
textStyle:{
color:'#fff'},
tooltip:{
trigger:'axis',// transitionDuration: 0,
backgroundColor:'#fff',
textStyle:{
color:"#333",
align:"left"},},
xAxis:{
min:0,
data:[],
boundaryGap:false,
splitLine:{
show:false},
axisLine:{
lineStyle:{
color:'#fff'}}},
yAxis:{
splitLine:{
show:false},
axisTick:{
show:false// 隐藏 y 轴的刻度线},
axisLine:{
show:false,
lineStyle:{
color:'#fff'}}},
grid:{// 为了让标尺和提示框在图表外面,需要将图表向外扩展一点
left:'10%',
right:'5%',
bottom:'5%',
top:'5%',
containLabel:true,},
series:[{// clipOverflow: false,
name:'温度',
type:'line',
smooth:true,
symbol:'none',
data:[],
itemStyle:{
color:'#00a890'},
areaStyle:{
color:{
type:'linear',
x:0,
y:0,
x2:0,
y2:1,
colorStops:[{
offset:0,
color:'#00a89066'// 0% 处的颜色},{
offset:1,
color:'#00a89000'// 100% 处的颜色}],
global:false// 缺省为 false}},
hoverAnimation:true,
label:{
show:false,},
markLine:{
symbol:['none','none'],
data:[{
type:'average',
name:'平均值',},],},}]};
optionHumidity ={
textStyle:{
color:'#fff'},
tooltip:{
trigger:'axis',
backgroundColor:'#fff',
textStyle:{
color:"#333",
align:"left"},},
xAxis:{
min:0,
data:[],
boundaryGap:false,
splitLine:{
show:false},
axisTick:{//x轴刻度相关设置
alignWithLabel:true,},
axisLine:{
lineStyle:{
color:'#fff'}}},
yAxis:{
splitLine:{
show:false},
axisTick:{
show:false// 隐藏 y 轴的刻度线},
axisLine:{
show:false,
lineStyle:{
color:'#fff'}}},
grid:{// 为了让标尺和提示框在图表外面,需要将图表向外扩展一点
left:'5%',
right:'5%',
bottom:'5%',
top:'5%',
containLabel:true,},// toolbox: {// feature: {// dataZoom: {},// brush: {// type: ['lineX', 'clear'],// },// },// },
series:[{
clipOverflow:false,
name:'湿度',
type:'line',
smooth:true,
symbol:'none',
data:[],
itemStyle:{
color:'#ffa74b'},
areaStyle:{
color:{
type:'linear',
x:0,
y:0,
x2:0,
y2:1,
colorStops:[{
offset:0,
color:'#ffa74b66'// 0% 处的颜色},{
offset:1,
color:'#ffa74b00'// 100% 处的颜色}],
global:false// 缺省为 false}},
hoverAnimation:true,
label:{
show:false,},
markLine:{
symbol:['none','none'],
data:[{
type:'average',
name:'平均值',},],},}]};// 使用刚指定的配置项和数据显示图表。
ChartTemperature.setOption(optionTemperature);
ChartHumidity.setOption(optionHumidity);},100)});
当你看到这里,你应该可以在你的前端页面上展示你的板子发来的每一条消息了,但是还远远做不到首图上那种密密麻麻的数据,我并不是把页面开了一天,而是使用了后端和数据库存储了一部分数据。
后端
后端我们分为了两个部分,一个是nodejs的后端程序,一个是nginx代理,这里先讲代理,因为上一步前端的连接需要走这个代理
nginx
如果你没有使用https连接,那么可以不看本节,直接使用未加密的mqtt协议,如果你有自己的域名,且申请了ssl证书,那么可以参考我的nginx配置,配置如下
http{
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout65;
types_hash_max_size2048;
include/etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
server{listen80;server_name jshub.cn;#将请求转成httpsrewrite^(.*)$ https://$host$1 permanent;
}
server{listen443ssl;
server_name jshub.cn;
location/{
root/larryzhu/web/release/toolbox;
indexindex.html index.htm;
try_files$uri$uri//index.html;
}
location/mqtt {proxy_passhttp://localhost:8083;proxy_http_version1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";
}# SSL 协议版本ssl_protocols TLSv1.2;# 证书ssl_certificate/larryzhu/web/keys/9263126_jshub.cn.pem;# 私钥ssl_certificate_key/larryzhu/web/keys/9263126_jshub.cn.key;# ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;# ssl_ciphers AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256;# 与False Start没关系,默认此项开启,此处减少抓包的干扰而关闭# ssl_session_tickets off;# return 200 "https ok \n";}
注意这只是部分配置,切不可全部覆盖你的配置。
如果你不会使用nginx,说明你无需配置 ssl ,直接使用 mqtt协议即可。
后端程序部分
这里以egg.js框架为例
首先需要下载egg.js的插件 egg-emqtt ,直接使用npm下载即可,详细配置和启用方法 参见 MQTT系列实践二 在EGG中使用mqtt
上面教程的方法并不全面,可以下载我的示例,仿照着写一下,因为内容相对复杂,地址:https://gitee.com/zhu_yongbo/mqttineggjs
其中还包含了 mysql 数据库的连接方法,内有我服务器的地址、mysql开放端口,用户名以及密码,我服务器还剩不到十天到期,有缘人看到我的文章可以对我的服务器为所欲为,没有什么重要数据。
mysql
mysql方面,只需要一个库,一个表即可完成全部工作
![](https://i-blog.csdnimg.cn/blog_migrate/bab361f98143f1877a8f3c7c86a0f6bd.png)
如图所示,不复杂,仿照我的建库即可
有一点,比较重要,因为mysql本身不适用于存储量级太大的数据,我们的数据重复的又比较多,可以考虑一下压缩算法,或者添加一个事件(每次插入时检查数据量是否超过一定值)。像我的板子大概正常累计运行了几天的时间(每两秒一条数据),到目前可以看到已经累计了七十万条数据了,如果不是因为我设置了插入事件,这个数据量已经可以明显影响查询速度了。
可以仿照我的事件,语句如下:
DELIMITER $$
CREATETRIGGER delete_oldest_data
AFTERINSERTON wemosd1_dht11
FOR EACH ROWBEGIN-- 如果数据量超过43200(每两秒插入一条,这是一天的量)条,调用存储过程删除最早的一条数据IF(SELECTCOUNT(*)FROM wemosd1_dht11)>43200THENCALL delete_oldest();ENDIF;END$$
DELIMITER;-- 创建存储过程CREATEPROCEDURE delete_oldest()BEGIN-- 删除最早的一条数据deletefrom wemosd1_dht11 orderby id asclimit1END$$
DELIMITER;
BTW:这是chatGPT教我的,我只进行了一点小小的修改。
这样做会删除id比较小的数据,然后就会导致,id会增长的越来越大,好处是可以看到一共累计了多少条数据。但是如果你不想让id累计,那么可以选择重建id,具体做法,建议你咨询一下chatGPT
结语
至此,我们已经完成了前端、后端、设备端三端连通。
我们整体梳理一下数据是怎么一步一步来到我们眼前的:
首先wemos d1开发板会在DHT11温湿度传感器上读取温湿度值,然后开发板把数据通过mqtt广播给某topic,我们的前后端都订阅了此topic,后端收到后,把处理过的数据存入mysql,前端直接使用echarts进行展示,当前端启动时,还可以向后端程序查询历史数据,比如前8000条数据,之后的变化由在线的开发板提供,我们就得到了一个实时的,并且能看到历史数据的温湿度在线大屏。
更多推荐
所有评论(0)