• 前端程序员是怎么做物联网开发的


    前端程序员是怎么做物联网开发的

    image-20230104162825029

    上图是我历时一周做的在线的温湿度可视化项目,可以查看截至目前往前一天的温度、湿度变化趋势,并且实时更新当前温湿度

    本文可能含有知识诅咒

    概述和基础讲解

    该项目用到的技术有:

    • 前端: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 开发板或任何其他电路板的初步了解即可

    简单介绍一下上面几个的知识点:

    1. 从来没有后端学习经验的同学,推荐一个全栈项目供你参考:vue-xmw-admin-pro ,该项目用到了 前端VUE、后端eggjs、mysql、redis,对全栈学习很有帮助。

    2. mysql 只需要知道最简单的插入和查询语句即可,在本项目中,其实使用mongodb是更合适的,但是我为了方便,直接用了现成的mysql

    3. 即使你不知道C语言的基本语法,也可以在一小时内快速了解一下,知道简单的定义变量、函数、返回值即可

    4. MQTT(消息队列遥测传输)是一种网络协议(长连接,意思就是除了客户端可以主动向服务器通信外,服务器也可以主动向客户端发起),也是基于TCP/IP的,适用于算力低下的硬件设备使用,基于发布\订阅范式的消息协议,具体示例如下:

      image-20230104170333941

      当某客户端想发布消息时,图大概长这样:

      image-20230104171235368

      由上图可知,当客户端通过验证上线后,还需要订阅主题,当某客户端向某主题发布消息时,只有订阅了该主题的客户端会收到broker的转发。

      举一个简单的例子:你和我,还有他,我们把自己的名字、学号报告给门卫大爷(broker),门卫大爷就同意我们在警卫室玩一会,警卫室有无数块黑板(topic),我们每个人都可以向门卫请求:如果某黑板上被人写了字,请转告给我。门卫会记住每个人的要求,比如当你向一块黑板写了字(你向某topic发送了消息),所有要求门卫告诉的人都会被门卫告知你写了什么(如果你也要求被告知,那么也包括你自己)。

    5. 开发板可以被写入程序,程序可以使用简单的代码控制某个针脚的高低电平,或者读取某针脚的数据。

    开始

    1. 购买 wemos d1开发板、DHT11温湿度传感器,共计19.3元。
    2. 使用arduino ide(以下简称ide) 对wemos d1编程需要下载esp8266依赖 参见:Arduino IDE安装esp8266 SDK
    3. 在ide的菜单栏选择:文件>首选项>其他开发板管理器地址填入:http://arduino.esp8266.com/stable/package_esp8266com_index.json,可以顺便改个中文
    4. 安装ch340驱动参见: win10 安装 CH340驱动 实测win11同样可用
    5. 使用 micro-usb 线,连接电脑和开发板,在ide菜单中选择:工具>开发板>esp8266>LOLIN(WEMOS) D1 R2 & mini
    6. 选择端口,按win+x,打开设备管理器,查看你的ch340在哪个端口,在ide中选择对应的端口
    7. 当ide右下角显示LOLIN(WEMOS) D1 R2 & mini 在comXX上时,连接就成功了
    8. 打开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"
    #define DHTPIN 14      // DHT11数据引脚连接到D5引脚 D5引脚的PIN值是14
    #define DHTTYPE DHT11  // 定义DHT11传感器
    DHT dht(DHTPIN, DHTTYPE);  //初始化传感器
    
    void setup() {
      Serial.begin(115200);
      //wemos d1 的波特率是 115200
      pinMode(BUILTIN_LED, OUTPUT); //设置一个输出的LED
      dht.begin();  //启动传感器
    }
    
    char* getDHT11Data() {
      float h = dht.readHumidity();  //获取湿度值
      float t = dht.readTemperature(); //获取温度值
      static char 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;
    }
    
    void loop() {
      char* data = getDHT11Data(); //此处去取传感器值
      Serial.println("got: " + String(data));  // 打印主题内容
      delay(1000); //每次循环延迟一秒
    }
    

    继续

    到了这一步,如果你用的是普通的arduino uno r3板子,就可以结束了。

    取到数据之后,你就可以根据数据做一些其他的事情了,比如打开接在d6引脚上的继电器,而这个继电器控制着一个加湿器。

    如果你跟我一样,使用了带wifi网络的板子,就可以继续跟我做。

    我们继续分步操作:

    设备端:

    1. 引入esp8266库(上面已经提到安装过程)

      1. #include "ESP8266WiFi.h"
        
    2. 安装mqtt客户端库 ,直接在库商店搜索 PubSubClient ,下载 PubSubClient by Nick O'Leary 那一项,下载完成后:

      1. #include "PubSubClient.h"
        
    3. 至此,库文件已全部安装引入完毕

    4. 设置 wifi ssid(即名字) 和 密码,如:

      1. char* ssid = "2104";
        char* passwd = "13912428897";
        
    5. 尝试连接 wifi

      1. WiFiClient espClient;
        int isConnect = 0;
        void connectWIFI() {
          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; //设置状态码为 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); //设置LED常亮,250的亮度对我来说已经很合适了
            settMqttConfig();  //尝试连接mqtt服务器,在下一步有详细代码
          } else {
            analogWrite(BUILTIN_LED, 255); //设置LED常灭,不要问我为什么255是常灭,因为我的灯是高电平熄灭的
            //连接wifi失败,等待一分钟重连
            delay(60000);
          }
        }
        
    6. 尝试连接 mqtt

      1. const char* mqtt_server = "larryblog.top";  //这里是我的服务器,当你看到这篇文章的时候,很可能已经没了,因为我的服务器还剩11天到期
        const char* TOPIC = "testtopic";            // 设置信息主题
        const char* client_id = "mqttx_3b2687d2";   //client_id不可重复,可以随便取,相当于你的网名
        PubSubClient client(espClient);
        void settMqttConfig() {
          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");  //一切正常的话,就连上了
        }
        //收信函数
        void onMessage(char* topic, byte* payload, unsigned int 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);  // 释放内存
        }
        
    7. 发送消息

      1. client.publish("home/status/", "{device:client_id,'status':'on'}");
        //注意,这里向另外一个主题发送的消息,消息内容就是设备在线,当有其他的客户端(比如web端)订阅了此主题,便能收到此消息
        

    至此,板子上的代码基本上就写完了,完整代码如下:

    #include "ESP8266WiFi.h"
    #include "PubSubClient.h"
    #include "DHT.h"
    #include "string.h"
    #define DHTPIN 14      // DHT11数据引脚连接到D5引脚
    #define DHTTYPE DHT11  // DHT11传感器
    DHT dht(DHTPIN, DHTTYPE);
    
    char* ssid = "2104";
    char* passwd = "13912428897";
    const char* mqtt_server = "larryblog.top";
    const char* TOPIC = "testtopic";            // 订阅信息主题
    const char* client_id = "mqttx_3b2687d2";
    int isConnect = 0;
    WiFiClient espClient;
    PubSubClient client(espClient);
    long lastMsg = 0;
    void setup() {
      Serial.begin(115200);
      // Set WiFi to station mode
      connectWIFI();
      pinMode(BUILTIN_LED, OUTPUT);
      dht.begin();
    }
    char* getDHT11Data() {
      float h = dht.readHumidity();
      float t = dht.readTemperature();
      static char 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;
    }
    void connectWIFI() {
      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);
      }
    }
    void settMqttConfig() {
      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");
    }
    void onMessage(char* topic, byte* payload, unsigned int 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);  // 释放内存
    }
    void publishDhtData() {
      char* data = getDHT11Data();
      Serial.println("got: " + String(data));  // 打印主题内容
      client.publish("wemos/dht11", data);
      delay(2000);
    }
    void reconnect() {
      Serial.print("Attempting MQTT connection...");
      // Attempt to connect
      if (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 retrying
        delay(5000);
      }
    }
    void loop() {
      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 again
      delay(1000);
    }
    

    服务器

    刚才的一同操作很可能让人一头雾水,相信大家对上面mqtt的操作还是一知半解的,不过没有关系,通过对服务端的设置,你会对mqtt的机制了解的更加透彻

    我们需要在服务端部署 mqtt broker,也就是mqtt的消息中心服务器

    在网络上搜索 emqx , 点击 EMQX: 大规模分布式物联网 MQTT 消息服务器 ,这是一个带有可视化界面的软件,而且画面特别精美,操作特别丝滑,功能相当强大,使用起来基本上没有心智负担。点击立即下载,并选择适合你的服务器系统的版本:

    image-20230223102450653

    这里拿 ubuntu和windows说明举例,相信其他系统也都大差不差

    在ubuntu上,推荐使用apt下载,按上图步骤操作即可,如中途遇到其他问题,请自行解决

    1. sudo ufw status 查看开放端口,一般情况下,你只会看到几个你手动开放过的端口,或者只有80、443端口
    2. udo ufw allow 18083 此端口是 emqx dashboard 使用的端口,开启此端口后,可以在外网访问 emqx看板控制台

    image-20230223103352676 当你看到如图所示的画面,说明已经开启成功了

    windows下直接下载安装包,上传到服务器,双击安装即可

    1. 打开 “高级安全Windows Defender 防火墙”,点击入站规则>新建规则
    2. 点击端口 > 下一步
    3. 点击TCP、特定本地端口 、输入18083,点击下一步
    4. 一直下一步到最后一步,输入名称,推荐输入 emqx 即可

    image-20230223103810837

    当你看到如图所示画面,说明你已经配置成功了。

    完成服务端程序安装和防火墙端口配置后,我们需要配置服务器后台的安全策略,这里拿阿里云举例:

    如果你是 ESC 云主机,点击实例>点击你的服务器名>安全组>配置规则>手动添加

    添加这么一条即可:

    image-20230223104139442

    如果你是轻量服务器,点击安全>防火墙>添加规则 即可,跟esc设置大差不差。

    完成后,可以在本地浏览器尝试访问你的emqx控制台

    image-20230223104408482

    直接输入域名:18083即可,初始用户名为admin,初始密码为public,登录完成后,你便会看到如下画面

    image-20230223104559151

    接下来需要配置 客户端登录名和密码,比如刚刚在设备中写的用户名密码,就是在这个系统中设置的

    点击 访问控制>认证 > 创建,然后无脑下一步即可,完成后你会看到如下画面

    image-20230223104906488

    点击用户管理,添加用户即可,用户名和密码都是自定义的,这些用户名密码可以分配给设备端、客户端、服务端、测试端使用,可以参考我的配置

    image-20230223105013597

    userClient是准备给前端页面用的 ,server是给后端用的,995231030是我个人自留的超级用户,wemos是设备用的,即上面设备连接时输入的用户名密码。

    至此,emqx 控制台配置完成。

    下载 mqttx,作为测试端尝试连接一下

    image-20230223105505838

    点击连接,你会发现,根本连接不上......

    因为,1883(mqtt默认端口)也是没有开启的,当然,和开启18083的方法一样。

    同时,还建议你开启:

    • 1803 websocket 默认端口
    • 1804 websockets 默认端口
    • 3306 mysql默认端口

    后面这四个端口都会用到。

    当你开启完成后,再次尝试使用mqttx连接broker,会发现可以连接了

    image-20230223105957929

    这个页面的功能也是很易懂的,我们在左侧添加订阅,右侧的聊天框里会出现该topic的消息

    image-20230223110105586

    你是否还记得,在上面的设备代码中,我们在loop中每一秒向 home/status/ 发送一条设备在线的提示,我们现在在这里就收到了。

    当你看到这些消息的时候,就说明,你的设备、服务器、emqx控制台已经跑通了。

    前后端以及数据库

    前端

    前端不必多说,我们使用echarts承载展示数据,由于体量较小,我们不使用任何框架,直接使用jq和echarts实现,这里主要讲前端怎么连接mqtt

    首先引入mqtt库

    <script src="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',
      }
       function generateRandomString() {
        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

    DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <link rel="stylesheet/less" href="./style.less">
      <link rel="stylesheet" href="//at.alicdn.com/t/c/font_3712319_bzaequy11dn.css">
      <script src="https://cdn.bootcdn.net/ajax/libs/less.js/4.1.3/less.js">script>
      <title>wemos d1 testtitle>
    head>
    
    <body>
      <div class="app" id="app">
        <div id="deviceStatus">
          <span class="statusLight">span>
          <span id="statusText">Loading device statusspan>
          
        div>
        <div class="container">
          <div class="Temperature">
            <div id="echartsViewTemperature">div>
            <span>Current temperature:span>
            <span id="Temperature">loading...span>
          div>
          <div class="Humidity">
            <div id="echartsViewHumidity">div>
            <span>Current humidity:span>
            <span id="Humidity">loading...span>
          div>
        div>
      div>
    body>
    <script src="./showTip.js">script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js">script>
    <script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
    <script src="https://cdn.bootcdn.net/ajax/libs/mqtt/4.1.0/mqtt.min.js">script>
    <script src="https://cdn.staticfile.org/echarts/4.7.0/echarts.js">script>
    <script src="./echarts.js?v=1.0.0">script>
    <script src="./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 = 1
      const 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);
            break
          case "home/status/":
            $("#statusText").text("device online")
            deviceOnline()
            $(".statusLight").removeClass("off")
            $(".statusLight").addClass("on")
            clearTimeout(timer);
            timer = setTimeout(onTimeout, 3500);
            break
    
        }
    
      })
    
      function deviceOnline() {
        if (isShowTip) {
          toast.showToast(1, "device online")
        }
        isShowTip = 0
      }
    
      function setData(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);
      }
    
      function onTimeout() {
        $("#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")
      }
    
      function generateRandomString() {
        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 = null
    let 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_timeout 65;
    	types_hash_max_size 2048;
    	include /etc/nginx/mime.types;
    	default_type application/octet-stream;
    
    	##
    	# SSL Settings
    	##
    	server {
        listen 80;
        server_name jshub.cn;
        #将请求转成https
        rewrite ^(.*)$ https://$host$1 permanent;
    	}
    	server {
            listen 443 ssl;
    				server_name jshub.cn;
    				location / {
    					root /larryzhu/web/release/toolbox;
    					index index.html index.htm;
    					try_files $uri $uri/ /index.html;
    				}
    	 location /mqtt {
               proxy_pass http://localhost:8083;
               proxy_http_version 1.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方面,只需要一个库,一个表即可完成全部工作

    image-20230223114608671

    如图所示,不复杂,仿照我的建库即可

    有一点,比较重要,因为mysql本身不适用于存储量级太大的数据,我们的数据重复的又比较多,可以考虑一下压缩算法,或者添加一个事件(每次插入时检查数据量是否超过一定值)。像我的板子大概正常累计运行了几天的时间(每两秒一条数据),到目前可以看到已经累计了七十万条数据了,如果不是因为我设置了插入事件,这个数据量已经可以明显影响查询速度了。

    可以仿照我的事件,语句如下:

    DELIMITER $$
    CREATE TRIGGER delete_oldest_data
    AFTER INSERT ON wemosd1_dht11
    FOR EACH ROW
    BEGIN
        -- 如果数据量超过43200(每两秒插入一条,这是一天的量)条,调用存储过程删除最早的一条数据
        IF (SELECT COUNT(*) FROM wemosd1_dht11) > 43200 THEN
            CALL delete_oldest();
        END IF;
    END$$
    DELIMITER ;
    
    -- 创建存储过程
    CREATE PROCEDURE delete_oldest()
    BEGIN
        -- 删除最早的一条数据
        delete from wemosd1_dht11 order by id asc limit 1
        
    END$$
    DELIMITER ;
    
    

    BTW:这是chatGPT教我的,我只进行了一点小小的修改。

    这样做会删除id比较小的数据,然后就会导致,id会增长的越来越大,好处是可以看到一共累计了多少条数据。但是如果你不想让id累计,那么可以选择重建id,具体做法,建议你咨询一下chatGPT

    结语

    至此,我们已经完成了前端、后端、设备端三端连通。

    我们整体梳理一下数据是怎么一步一步来到我们眼前的:

    首先wemos d1开发板会在DHT11温湿度传感器上读取温湿度值,然后开发板把数据通过mqtt广播给某topic,我们的前后端都订阅了此topic,后端收到后,把处理过的数据存入mysql,前端直接使用echarts进行展示,当前端启动时,还可以向后端程序查询历史数据,比如前8000条数据,之后的变化由在线的开发板提供,我们就得到了一个实时的,并且能看到历史数据的温湿度在线大屏。

    如果你觉得牛逼,就给我点个赞吧。

  • 相关阅读:
    常用linux的命令(持续更新)
    中国金刚石工具市场发展现状及供需格局分析预测报告
    Selenium基础 — TMLTestRunner测试报告
    基于SSM的在线家教管理系统
    字符编码详解
    华为机试题解析018:从单向链表中删除指定值的节点(python)
    BM1反转链表[栈+头插法]
    【全栈计划 —— 编程语言之C#】基础入门知识一文懂
    Javascript抓取京东、淘宝商品数据
    Cocos Creator 场景树与组件实例查找详解
  • 原文地址:https://www.cnblogs.com/Kay-Larry/p/17147584.html