<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端程序員是怎么做物聯(lián)網(wǎng)開(kāi)發(fā)的

          共 46137字,需瀏覽 93分鐘

           ·

          2023-10-28 08:06

          關(guān)于文本,作者:德萊厄斯 

          鏈接:https://juejin.cn/post/7203180003471081531


          image-20230104162825029

          上圖是我歷時(shí)一周做的在線(xiàn)的溫濕度可視化項(xiàng)目,可以查看截至目前往前一天的溫度、濕度變化趨勢(shì),并且實(shí)時(shí)更新當(dāng)前溫濕度

          概述和基礎(chǔ)講解

          該項(xiàng)目用到的技術(shù)有:

          • 前端:jq、less、echarts、mqtt.js
          • 后端:eggjs、egg-emqtt
          • 數(shù)據(jù)庫(kù):mysql
          • 服務(wù)器:emqx(mqtt broker)
          • 硬件:
            • 板子:wemos D1 R2(設(shè)計(jì)基于 Arduino Uno R3 , 搭載esp8266 wifi模塊)
          • 調(diào)試工具:mqttx、Arduino IDE v2.0.3 使用Arduino C開(kāi)發(fā)

          必備知識(shí):

          • nodejs(eggjs框架)能面向業(yè)務(wù)即可
          • mysql 能寫(xiě)基本插入查詢(xún)語(yǔ)句即可
          • C語(yǔ)言的基本語(yǔ)法了解即可
          • 知道m(xù)qtt協(xié)議的運(yùn)作方式即可
          • arduino 開(kāi)發(fā)板或任何其他電路板的初步了解即可

          簡(jiǎn)單介紹一下上面幾個(gè)的知識(shí)點(diǎn):

          1. 從來(lái)沒(méi)有后端學(xué)習(xí)經(jīng)驗(yàn)的同學(xué),推薦一個(gè)全棧項(xiàng)目供你參考:**vue-xmw-admin-pro** ,該項(xiàng)目用到了 前端VUE、后端eggjs、mysql、redis,對(duì)全棧學(xué)習(xí)很有幫助。

          2. mysql 只需要知道最簡(jiǎn)單的插入和查詢(xún)語(yǔ)句即可,在本項(xiàng)目中,其實(shí)使用mongodb是更合適的,但是我為了方便,直接用了現(xiàn)成的mysql

          3. 即使你不知道C語(yǔ)言的基本語(yǔ)法,也可以在一小時(shí)內(nèi)快速了解一下,知道簡(jiǎn)單的定義變量、函數(shù)、返回值即可

          4. MQTT(消息隊(duì)列遙測(cè)傳輸)是一種網(wǎng)絡(luò)協(xié)議(長(zhǎng)連接,意思就是除了客戶(hù)端可以主動(dòng)向服務(wù)器通信外,服務(wù)器也可以主動(dòng)向客戶(hù)端發(fā)起),也是基于TCP/IP的,適用于算力低下的硬件設(shè)備使用,基于發(fā)布\訂閱范式的消息協(xié)議,具體示例如下:

            image-20230104170333941

            當(dāng)某客戶(hù)端想發(fā)布消息時(shí),圖大概長(zhǎng)這樣:

            image-20230104171235368

            由上圖可知,當(dāng)客戶(hù)端通過(guò)驗(yàn)證上線(xiàn)后,還需要訂閱主題,當(dāng)某客戶(hù)端向某主題發(fā)布消息時(shí),只有訂閱了該主題的客戶(hù)端會(huì)收到broker的轉(zhuǎn)發(fā)。

            舉一個(gè)簡(jiǎn)單的例子:你和我,還有他,我們把自己的名字、學(xué)號(hào)報(bào)告給門(mén)衛(wèi)大爺(broker),門(mén)衛(wèi)大爺就同意我們?cè)诰l(wèi)室玩一會(huì),警衛(wèi)室有無(wú)數(shù)塊黑板(topic),我們每個(gè)人都可以向門(mén)衛(wèi)請(qǐng)求:如果某黑板上被人寫(xiě)了字,請(qǐng)轉(zhuǎn)告給我。門(mén)衛(wèi)會(huì)記住每個(gè)人的要求,比如當(dāng)你向一塊黑板寫(xiě)了字(你向某topic發(fā)送了消息),所有要求門(mén)衛(wèi)告訴的人都會(huì)被門(mén)衛(wèi)告知你寫(xiě)了什么(如果你也要求被告知,那么也包括你自己)。

          5. 開(kāi)發(fā)板可以被寫(xiě)入程序,程序可以使用簡(jiǎn)單的代碼控制某個(gè)針腳的高低電平,或者讀取某針腳的數(shù)據(jù)。

          開(kāi)始

          1. 購(gòu)買(mǎi) wemos d1開(kāi)發(fā)板、DHT11溫濕度傳感器,共計(jì)19.3元。
          2. 使用arduino ide(以下簡(jiǎn)稱(chēng)ide) 對(duì)wemos d1編程需要下載esp8266依賴(lài) 參見(jiàn):Arduino IDE安裝esp8266 SDK
          3. 在ide的菜單欄選擇:文件>首選項(xiàng)>其他開(kāi)發(fā)板管理器地址填入:arduino.esp8266.com/stable/pack…,可以順便改個(gè)中文
          4. 安裝ch340驅(qū)動(dòng)參見(jiàn):win10 安裝 CH340驅(qū)動(dòng) 實(shí)測(cè)win11同樣可用
          5. 使用 micro-usb 線(xiàn),連接電腦和開(kāi)發(fā)板,在ide菜單中選擇:工具>開(kāi)發(fā)板>esp8266>LOLIN(WEMOS) D1 R2 & mini
          6. 選擇端口,按win+x,打開(kāi)設(shè)備管理器,查看你的ch340在哪個(gè)端口,在ide中選擇對(duì)應(yīng)的端口
          7. 當(dāng)ide右下角顯示LOLIN(WEMOS) D1 R2 & mini 在comXX上時(shí),連接就成功了
          8. 打開(kāi)ide菜單欄 :文件>示例>esp8266>blink,此時(shí)ide會(huì)打開(kāi)新窗口,在新窗口點(diǎn)擊左上角的上傳按鈕,等待上傳完成,當(dāng)板子上的燈一閃一閃,就表明:環(huán)境、設(shè)置、板子都沒(méi)問(wèn)題,可以開(kāi)始編程了,如果報(bào)錯(cuò),那么一定是哪一步出問(wèn)題了,我相信你能夠根據(jù)錯(cuò)誤提示找出到底是什么問(wèn)題,如果實(shí)在找不出問(wèn)題,那么可能買(mǎi)到了壞的板子(故障率還是蠻高的)

          wemos d1 針腳中有一個(gè) 3.3v電源輸出,三個(gè)或更多的GND接地口,當(dāng)安裝DHT11傳感器元件時(shí),需要將正極插入3.3v口,負(fù)極插入GND口,中間的數(shù)據(jù)線(xiàn)插入隨便的數(shù)字輸入口,比如D5口(D5口的PIN值是14,后面會(huì)用到)。

          使用DHT11傳感器,需要安裝庫(kù):DHT sensor library by Adafruit , 在ide的左側(cè)欄中的庫(kù)管理中直接搜索安裝即可

          下面是一個(gè)獲取DHT11數(shù)據(jù)的簡(jiǎn)單示例,如果正常的話(huà),在串口監(jiān)視器中,會(huì)每秒輸出溫濕度數(shù)據(jù)

          arduino復(fù)制代碼#include "DHT.h"  //這是依賴(lài)或者叫庫(kù),或者叫驅(qū)動(dòng)也行
          #include "string.h"
          #define DHTPIN 14 // DHT11數(shù)據(jù)引腳連接到D5引腳 D5引腳的PIN值是14
          #define DHTTYPE DHT11 // 定義DHT11傳感器
          DHT dht(DHTPIN, DHTTYPE); //初始化傳感器

          void setup() {
          Serial.begin(115200);
          //wemos d1 的波特率是 115200
          pinMode(BUILTIN_LED, OUTPUT); //設(shè)置一個(gè)輸出的LED
          dht.begin(); //啟動(dòng)傳感器
          }

          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); //如果任何一個(gè)值沒(méi)有值,直接返回兩個(gè)0.0,這樣我們就知道傳感器可能出問(wèn)題了
          return data;
          }
          sprintf(data, "Temperature: %.1f, Humidity: %.1f", t, h); //正常就取到值,我這里拼成了一句話(huà)
          return data;
          }

          void loop() {
          char* data = getDHT11Data(); //此處去取傳感器值
          Serial.println("got: " + String(data)); // 打印主題內(nèi)容
          delay(1000); //每次循環(huán)延遲一秒
          }

          繼續(xù)

          到了這一步,如果你用的是普通的arduino uno r3板子,就可以結(jié)束了。

          取到數(shù)據(jù)之后,你就可以根據(jù)數(shù)據(jù)做一些其他的事情了,比如打開(kāi)接在d6引腳上的繼電器,而這個(gè)繼電器控制著一個(gè)加濕器。

          如果你跟我一樣,使用了帶wifi網(wǎng)絡(luò)的板子,就可以繼續(xù)跟我做。

          我們繼續(xù)分步操作:

          設(shè)備端:

          1. 引入esp8266庫(kù)(上面已經(jīng)提到安裝過(guò)程)

            1. arduino
              復(fù)制代碼#include "ESP8266WiFi.h"
          2. 安裝mqtt客戶(hù)端庫(kù) ,直接在庫(kù)商店搜索 PubSubClient ,下載 PubSubClient by Nick O'Leary 那一項(xiàng),下載完成后:

            1. arduino
              復(fù)制代碼#include "PubSubClient.h"
          3. 至此,庫(kù)文件已全部安裝引入完畢

          4. 設(shè)置 wifi ssid(即名字) 和 密碼,如:

            1. ini復(fù)制代碼char* ssid = "2104";
              char* passwd = "13912428897";
          5. 嘗試連接 wifi

            1. scss復(fù)制代碼WiFiClient espClient;
              int isConnect = 0;
              void connectWIFI() {
              isConnect = 0;
              WiFi.mode(WIFI_STA); //不知道什么意思,照著寫(xiě)就完了
              WiFi.begin(ssid, passwd); //嘗試連接
              int timeCount = 0; //嘗試次數(shù)
              while (WiFi.status() != WL_CONNECTED) { //如果沒(méi)有連上,繼續(xù)循環(huán)
              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);
              }
              // 上兩個(gè)循環(huán)共計(jì)200ms左右,在控制LED閃爍而已,你也可以不寫(xiě)
              Serial.println("wifi connecting......" + String(timeCount));
              timeCount++;
              isConnect = 1; //每次都需要把連接狀態(tài)碼設(shè)置一下,只有連不上時(shí)設(shè)置為0
              // digitalWrite(BUILTIN_LED, LOW);
              if (timeCount >= 200) {
              // 當(dāng)40000毫秒時(shí)還沒(méi)連上,就不連了
              isConnect = 0; //設(shè)置狀態(tài)碼為 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); //設(shè)置LED常亮,250的亮度對(duì)我來(lái)說(shuō)已經(jīng)很合適了
              settMqttConfig(); //嘗試連接mqtt服務(wù)器,在下一步有詳細(xì)代碼
              } else {
              analogWrite(BUILTIN_LED, 255); //設(shè)置LED常滅,不要問(wèn)我為什么255是常滅,因?yàn)槲业臒羰歉唠娖较绲?br style="outline: 0px;"> //連接wifi失敗,等待一分鐘重連
              delay(60000);
              }
              }
          6. 嘗試連接 mqtt

            1. arduino復(fù)制代碼const char* mqtt_server = "larryblog.top";  //這里是我的服務(wù)器,當(dāng)你看到這篇文章的時(shí)候,很可能已經(jīng)沒(méi)了,因?yàn)槲业姆?wù)器還剩11天到期
              const char* TOPIC = "testtopic"; // 設(shè)置信息主題
              const char* client_id = "mqttx_3b2687d2"; //client_id不可重復(fù),可以隨便取,相當(dāng)于你的網(wǎng)名
              PubSubClient client(espClient);
              void settMqttConfig() {
              client.setServer(mqtt_server, 1883); //設(shè)定MQTT服務(wù)器與使用的端口,1883是默認(rèn)的MQTT端口
              client.setCallback(onMessage); //設(shè)置收信函數(shù),當(dāng)訂閱的主題有消息進(jìn)來(lái)時(shí),會(huì)進(jìn)這個(gè)函數(shù)
              Serial.println("try connect mqtt broker");
              client.connect(client_id, "wemos", "aa995231030"); //后兩個(gè)參數(shù)是用戶(hù)名密碼
              client.subscribe(TOPIC); //訂閱主題
              Serial.println("mqtt connected"); //一切正常的話(huà),就連上了
              }
              //收信函數(shù)
              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); // 打印主題內(nèi)容
              if (strcmp(payloadStr, (char*)"getDHTData") == 0) {
              char* data = getDHT11Data();
              Serial.println("got: " + String(data)); // 打印主題內(nèi)容
              client.publish("wemos/dht11", data);
              }
              free(payloadStr); // 釋放內(nèi)存
              }
          7. 發(fā)送消息

            1. arduino復(fù)制代碼client.publish("home/status/", "{device:client_id,'status':'on'}");
              //注意,這里向另外一個(gè)主題發(fā)送的消息,消息內(nèi)容就是設(shè)備在線(xiàn),當(dāng)有其他的客戶(hù)端(比如web端)訂閱了此主題,便能收到此消息

          至此,板子上的代碼基本上就寫(xiě)完了,完整代碼如下:

          scss復(fù)制代碼#include "ESP8266WiFi.h"
          #include "PubSubClient.h"
          #include "DHT.h"
          #include "string.h"
          #define DHTPIN 14 // DHT11數(shù)據(jù)引腳連接到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);
          }
          // 上兩個(gè)循環(huán)共計(jì)200ms左右
          Serial.println("wifi connecting......" + String(timeCount));
          timeCount++;
          isConnect = 1;
          // digitalWrite(BUILTIN_LED, LOW);
          if (timeCount >= 200) {
          // 當(dāng)40000毫秒時(shí)還沒(méi)連上,就不連了
          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); //設(shè)定MQTT服務(wù)器與使用的端口,1883是默認(rèn)的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); // 打印主題內(nèi)容
          if (strcmp(payloadStr, (char*)"getDHTData") == 0) {
          char* data = getDHT11Data();
          Serial.println("got: " + String(data)); // 打印主題內(nèi)容
          client.publish("wemos/dht11", data);
          }
          free(payloadStr); // 釋放內(nèi)存
          }
          void publishDhtData() {
          char* data = getDHT11Data();
          Serial.println("got: " + String(data)); // 打印主題內(nèi)容
          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");
          // 連接成功時(shí)訂閱主題
          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);
          }

          服務(wù)器

          剛才的一同操作很可能讓人一頭霧水,相信大家對(duì)上面mqtt的操作還是一知半解的,不過(guò)沒(méi)有關(guān)系,通過(guò)對(duì)服務(wù)端的設(shè)置,你會(huì)對(duì)mqtt的機(jī)制了解的更加透徹

          我們需要在服務(wù)端部署 mqtt broker,也就是mqtt的消息中心服務(wù)器

          在網(wǎng)絡(luò)上搜索 emqx , 點(diǎn)擊 EMQX: 大規(guī)模分布式物聯(lián)網(wǎng) MQTT 消息服務(wù)器 ,這是一個(gè)帶有可視化界面的軟件,而且畫(huà)面特別精美,操作特別絲滑,功能相當(dāng)強(qiáng)大,使用起來(lái)基本上沒(méi)有心智負(fù)擔(dān)。點(diǎn)擊立即下載,并選擇適合你的服務(wù)器系統(tǒng)的版本:

          image-20230223102450653

          這里拿 ubuntu和windows說(shuō)明舉例,相信其他系統(tǒng)也都大差不差

          在ubuntu上,推薦使用apt下載,按上圖步驟操作即可,如中途遇到其他問(wèn)題,請(qǐng)自行解決

          1. sudo ufw status 查看開(kāi)放端口,一般情況下,你只會(huì)看到幾個(gè)你手動(dòng)開(kāi)放過(guò)的端口,或者只有80、443端口
          2. udo ufw allow 18083 此端口是 emqx dashboard 使用的端口,開(kāi)啟此端口后,可以在外網(wǎng)訪(fǎng)問(wèn) emqx看板控制臺(tái)

          當(dāng)你看到如圖所示的畫(huà)面,說(shuō)明已經(jīng)開(kāi)啟成功了

          windows下直接下載安裝包,上傳到服務(wù)器,雙擊安裝即可

          1. 打開(kāi) “高級(jí)安全Windows Defender 防火墻”,點(diǎn)擊入站規(guī)則>新建規(guī)則
          2. 點(diǎn)擊端口 > 下一步
          3. 點(diǎn)擊TCP、特定本地端口 、輸入18083,點(diǎn)擊下一步
          4. 一直下一步到最后一步,輸入名稱(chēng),推薦輸入 emqx 即可
          image-20230223103810837

          當(dāng)你看到如圖所示畫(huà)面,說(shuō)明你已經(jīng)配置成功了。

          完成服務(wù)端程序安裝和防火墻端口配置后,我們需要配置服務(wù)器后臺(tái)的安全策略,這里拿阿里云舉例:

          如果你是 ECS 云主機(jī),點(diǎn)擊實(shí)例>點(diǎn)擊你的服務(wù)器名>安全組>配置規(guī)則>手動(dòng)添加

          添加這么一條即可:

          image-20230223104139442

          如果你是輕量服務(wù)器,點(diǎn)擊安全>防火墻>添加規(guī)則 即可,跟ECS設(shè)置大差不差。

          完成后,可以在本地瀏覽器嘗試訪(fǎng)問(wèn)你的emqx控制臺(tái)

          image-20230223104408482

          直接輸入域名:18083即可,初始用戶(hù)名為admin,初始密碼為public,登錄完成后,你便會(huì)看到如下畫(huà)面

          image-20230223104559151

          接下來(lái)需要配置 客戶(hù)端登錄名和密碼,比如剛剛在設(shè)備中寫(xiě)的用戶(hù)名密碼,就是在這個(gè)系統(tǒng)中設(shè)置的

          點(diǎn)擊 訪(fǎng)問(wèn)控制>認(rèn)證 > 創(chuàng)建,然后無(wú)腦下一步即可,完成后你會(huì)看到如下畫(huà)面

          image-20230223104906488

          點(diǎn)擊用戶(hù)管理,添加用戶(hù)即可,用戶(hù)名和密碼都是自定義的,這些用戶(hù)名密碼可以分配給設(shè)備端、客戶(hù)端、服務(wù)端、測(cè)試端使用,可以參考我的配置

          image-20230223105013597

          userClient是準(zhǔn)備給前端頁(yè)面用的 ,server是給后端用的,995231030是我個(gè)人自留的超級(jí)用戶(hù),wemos是設(shè)備用的,即上面設(shè)備連接時(shí)輸入的用戶(hù)名密碼。

          至此,emqx 控制臺(tái)配置完成。

          下載 mqttx,作為測(cè)試端嘗試連接一下

          image-20230223105505838

          點(diǎn)擊連接,你會(huì)發(fā)現(xiàn),根本連接不上......

          因?yàn)椋?883(mqtt默認(rèn)端口)也是沒(méi)有開(kāi)啟的,當(dāng)然,和開(kāi)啟18083的方法一樣。

          同時(shí),還建議你開(kāi)啟:

          • 1803 websocket 默認(rèn)端口
          • 1804 websockets 默認(rèn)端口
          • 3306 mysql默認(rèn)端口

          后面這四個(gè)端口都會(huì)用到。

          當(dāng)你開(kāi)啟完成后,再次嘗試使用mqttx連接broker,會(huì)發(fā)現(xiàn)可以連接了

          image-20230223105957929

          這個(gè)頁(yè)面的功能也是很易懂的,我們?cè)谧髠?cè)添加訂閱,右側(cè)的聊天框里會(huì)出現(xiàn)該topic的消息

          image-20230223110105586

          你是否還記得,在上面的設(shè)備代碼中,我們?cè)趌oop中每一秒向 home/status/ 發(fā)送一條設(shè)備在線(xiàn)的提示,我們現(xiàn)在在這里就收到了。

          當(dāng)你看到這些消息的時(shí)候,就說(shuō)明,你的設(shè)備、服務(wù)器、emqx控制臺(tái)已經(jīng)跑通了。

          前后端以及數(shù)據(jù)庫(kù)

          前端

          前端不必多說(shuō),我們使用echarts承載展示數(shù)據(jù),由于體量較小,我們不使用任何框架,直接使用jq和echarts實(shí)現(xiàn),這里主要講前端怎么連接mqtt

          首先引入mqtt庫(kù)

          xml
          復(fù)制代碼<script src="https://cdn.bootcdn.net/ajax/libs/mqtt/4.1.0/mqtt.min.js"></script>

          然后設(shè)置連接參數(shù)

          javascript復(fù)制代碼  const options = {
              cleantrue// true: 清除會(huì)話(huà), false: 保留會(huì)話(huà)
              connectTimeout4000// 超時(shí)時(shí)間
              clientId'userClient_' + generateRandomString(),
              //前端客戶(hù)端很可能比較多,所以這里我們生成一個(gè)隨機(jī)的6位字母加數(shù)字作為clientId,以保證不會(huì)重復(fù)
              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;
            }
           

          連接

          ini復(fù)制代碼  // const connectUrl = 'mqtt://larryblog.top/mqtt' 當(dāng)然你可以使用mqtt協(xié)議,但是有可能會(huì)遇到 ssl 跨域的問(wèn)題,如果你不使用 https 可以忽略這一項(xiàng),直接使用mqtt即可
          const connectUrl = 'wss://larryblog.top/mqtt' //注意,這里使用了nginx進(jìn)行轉(zhuǎn)發(fā),后面會(huì)講
          const client = mqtt.connect(connectUrl, options)

          因?yàn)榍岸舜a不多,我這里直接貼了

          html:

          index.html

          xml復(fù)制代碼<!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="http://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 test</title>
          </head>

          <body>
            <div class="app" id="app">
              <div id="deviceStatus">
                <span class="statusLight"></span>
                <span id="statusText">Loading device status</span>
                <!-- <span class="iconfont icon-xinxi"></span> -->
              </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

          javascript復(fù)制代碼// 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",
              skip0
            },
              (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 = {
              cleantrue// true: 清除會(huì)話(huà), false: 保留會(huì)話(huà)
              connectTimeout4000// 超時(shí)時(shí)間
              // 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
            }
            // 連接字符串, 通過(guò)協(xié)議指定使用的連接方式
            // 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) {
                  // 發(fā)布消息
                  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(": "))); // 轉(zhuǎn)化為對(duì)象

                  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 是我發(fā)布在npm上的一個(gè)包,如果有需要可以自行npm下載

          style.less

          css復(fù)制代碼* {
            padding0;
            margin0;
            color#fff;
          }

          .app {
            background#1b2028;
            width100vw;
            height100vh;
            display: flex;
            flex-direction: column;
            overflow: hidden;

            #deviceStatus {
              display: flex;
              align-items: center;
              gap10px;
              padding20px;

              .statusLight {
                display: block;
                height10px;
                width10px;
                border-radius100px;
                background#b8b8b8;

                &.on {
                  background#00a890;
                }

                &.off {
                  background#b8b8b8;
                }
              }
            }

            .container {
              width100%;
              height0;
              flex1;
              display: flex;

              @media screen and (max-width768px) {
                flex-direction: column;
              }

              >div {
                flex1;
                height100%;
                text-align: center;

                #echartsViewTemperature,
                #echartsViewHumidity {
                  width80%;
                  height50%;
                  margin10px auto;
                  // background#eee;
                }
              }
            }
          }

          echarts.js 這個(gè)文件是我自己寫(xiě)的,別學(xué)我這種命名方式,這是反例

          yaml復(fù)制代碼let optionTemperature = null
          let ChartTemperature = null
          $(document).ready(() => {
            setTimeout(() => {
              // waiting
              ChartTemperature = echarts.init(document.getElementById('echartsViewTemperature'));
              ChartHumidity = echarts.init(document.getElementById('echartsViewHumidity'));
              // 指定圖表的配置項(xiàng)和數(shù)據(jù)
              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 軸的刻度線(xiàn)
                  },
                  axisLine: {
                    show: false,
                    lineStyle: {
                      color: '#fff'
                    }
                  }
                },
                grid: {
                  // 為了讓標(biāo)尺和提示框在圖表外面,需要將圖表向外擴(kuò)展一點(diǎn)
                  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軸刻度相關(guān)設(shè)置
                    alignWithLabel: true,
                  },
                  axisLine: {
                    lineStyle: {
                      color: '#fff'
                    }
                  }
                },
                yAxis: {
                  splitLine: {
                    show: false
                  },
                  axisTick: {
                    show: false // 隱藏 y 軸的刻度線(xiàn)
                  },
                  axisLine: {
                    show: false,
                    lineStyle: {
                      color: '#fff'
                    }
                  }
                },
                grid: {
                  // 為了讓標(biāo)尺和提示框在圖表外面,需要將圖表向外擴(kuò)展一點(diǎn)
                  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: '平均值',
                      },
                    ],
                  },
                }]
              };

              // 使用剛指定的配置項(xiàng)和數(shù)據(jù)顯示圖表。
              ChartTemperature.setOption(optionTemperature);
              ChartHumidity.setOption(optionHumidity);
            }, 100)
          });

          當(dāng)你看到這里,你應(yīng)該可以在你的前端頁(yè)面上展示你的板子發(fā)來(lái)的每一條消息了,但是還遠(yuǎn)遠(yuǎn)做不到首圖上那種密密麻麻的數(shù)據(jù),我并不是把頁(yè)面開(kāi)了一天,而是使用了后端和數(shù)據(jù)庫(kù)存儲(chǔ)了一部分?jǐn)?shù)據(jù)。

          后端

          后端我們分為了兩個(gè)部分,一個(gè)是nodejs的后端程序,一個(gè)是nginx代理,這里先講代理,因?yàn)樯弦徊角岸说倪B接需要走這個(gè)代理

          nginx

          如果你沒(méi)有使用https連接,那么可以不看本節(jié),直接使用未加密的mqtt協(xié)議,如果你有自己的域名,且申請(qǐng)了ssl證書(shū),那么可以參考我的nginx配置,配置如下

          ini復(fù)制代碼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;
          #將請(qǐng)求轉(zhuǎn)成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 協(xié)議版本
          ssl_protocols TLSv1.2;
          # 證書(shū)
          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沒(méi)關(guān)系,默認(rèn)此項(xiàng)開(kāi)啟,此處減少抓包的干擾而關(guān)閉
          # ssl_session_tickets off;

          # return 200 "https ok \n";
          }

          注意這只是部分配置,切不可全部覆蓋你的配置。

          如果你不會(huì)使用nginx,說(shuō)明你無(wú)需配置 ssl ,直接使用 mqtt協(xié)議即可。

          后端程序部分

          這里以egg.js框架為例

          首先需要下載egg.js的插件 egg-emqtt ,直接使用npm下載即可,詳細(xì)配置和啟用方法 參見(jiàn) MQTT系列實(shí)踐二 在EGG中使用mqtt

          上面教程的方法并不全面,可以下載我的示例,仿照著寫(xiě)一下,因?yàn)閮?nèi)容相對(duì)復(fù)雜,地址:gitee.com/zhu_yongbo/…

          其中還包含了 mysql 數(shù)據(jù)庫(kù)的連接方法,內(nèi)有我服務(wù)器的地址、mysql開(kāi)放端口,用戶(hù)名以及密碼,我服務(wù)器還剩不到十天到期,有緣人看到我的文章可以對(duì)我的服務(wù)器為所欲為,沒(méi)有什么重要數(shù)據(jù)。

          mysql

          mysql方面,只需要一個(gè)庫(kù),一個(gè)表即可完成全部工作

          image-20230223114608671

          如圖所示,不復(fù)雜,仿照我的建庫(kù)即可

          有一點(diǎn),比較重要,因?yàn)閙ysql本身不適用于存儲(chǔ)量級(jí)太大的數(shù)據(jù),我們的數(shù)據(jù)重復(fù)的又比較多,可以考慮一下壓縮算法,或者添加一個(gè)事件(每次插入時(shí)檢查數(shù)據(jù)量是否超過(guò)一定值)。像我的板子大概正常累計(jì)運(yùn)行了幾天的時(shí)間(每?jī)擅胍粭l數(shù)據(jù)),到目前可以看到已經(jīng)累計(jì)了七十萬(wàn)條數(shù)據(jù)了,如果不是因?yàn)槲以O(shè)置了插入事件,這個(gè)數(shù)據(jù)量已經(jīng)可以明顯影響查詢(xún)速度了。

          可以仿照我的事件,語(yǔ)句如下:

          sql復(fù)制代碼DELIMITER $$
          CREATE TRIGGER delete_oldest_data
          AFTER INSERT ON wemosd1_dht11
          FOR EACH ROW
          BEGIN
              -- 如果數(shù)據(jù)量超過(guò)43200(每?jī)擅氩迦胍粭l,這是一天的量)條,調(diào)用存儲(chǔ)過(guò)程刪除最早的一條數(shù)據(jù)
              IF (SELECT COUNT(*) FROM wemosd1_dht11) > 43200 THEN
                  CALL delete_oldest();
              END IF;
          END$$
          DELIMITER ;

          -- 創(chuàng)建存儲(chǔ)過(guò)程
          CREATE PROCEDURE delete_oldest()
          BEGIN
              -- 刪除最早的一條數(shù)據(jù)
              delete from wemosd1_dht11 order by id asc limit 1
              
          END$$
          DELIMITER ;

          BTW:這是chatGPT教我的,我只進(jìn)行了一點(diǎn)小小的修改。

          這樣做會(huì)刪除id比較小的數(shù)據(jù),然后就會(huì)導(dǎo)致,id會(huì)增長(zhǎng)的越來(lái)越大,好處是可以看到一共累計(jì)了多少條數(shù)據(jù)。但是如果你不想讓id累計(jì),那么可以選擇重建id,具體做法,建議你咨詢(xún)一下chatGPT

          結(jié)語(yǔ)

          至此,我們已經(jīng)完成了前端、后端、設(shè)備端三端連通。

          我們整體梳理一下數(shù)據(jù)是怎么一步一步來(lái)到我們眼前的:

          首先wemos d1開(kāi)發(fā)板會(huì)在DHT11溫濕度傳感器上讀取溫濕度值,然后開(kāi)發(fā)板把數(shù)據(jù)通過(guò)mqtt廣播給某topic,我們的前后端都訂閱了此topic,后端收到后,把處理過(guò)的數(shù)據(jù)存入mysql,前端直接使用echarts進(jìn)行展示,當(dāng)前端啟動(dòng)時(shí),還可以向后端程序查詢(xún)歷史數(shù)據(jù),比如前8000條數(shù)據(jù),之后的變化由在線(xiàn)的開(kāi)發(fā)板提供,我們就得到了一個(gè)實(shí)時(shí)的,并且能看到歷史數(shù)據(jù)的溫濕度在線(xiàn)大屏。

          如果你覺(jué)得牛逼,就給我點(diǎn)個(gè)贊吧。

          作者:德萊厄斯 

          鏈接:https://juejin.cn/post/7203180003471081531

          瀏覽 1049
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  狠狠操B 麻豆传媒肏逼视频 | 婷婷五月丁香狠狠撸 | 超碰国产成人 | 四虎无码人妻三区 | 亚洲欧洲中文日韩免费视频一区二区 |