Hành Trình Lập Trình IoT của Một Frontend Developer

Tổng Quan và Kiến Thức Cơ Bản

Dự án này sử dụng các công nghệ sau:

  • Frontend: jQuery, Less, ECharts, MQTT.js
  • Backend: Egg.js, Egg-emqtt
  • Cơ sở dữ liệu: MySQL
  • Máy chủ: EMQX (MQTT Broker)
  • Phần cứng:
    • Bo mạch: Wemos D1 R2 (dựa trên Arduino Uno R3, tích hợp module WiFi ESP8266)
  • Công cụ debug: MQTTX, Arduino IDE v2.0.3 (lập trình bằng Arduino C)

Kiến thức cần có:

  • Node.js (framework Egg.js) đủ để xử lý các tác vụ cơ bản.
  • MySQL: viết được câu lệnh INSERT và SELECT cơ bản.
  • C: hiểu các cú pháp cơ bản như khai báo biến, hàm và giá trị trả về.
  • MQTT: hiểu cách thức hoạt động của giao thức này.
  • Arduino: biết sơ qua về bo mạch hoặc các mạch điện tử khác.

Giải thích nhanh các khái niệm trên:

  1. Nếu bạn chưa có kinh nghiệm về backend, hãy tham khảo dự án full-stack vue-xmw-admin-pro. Dự án này sử dụng Vue (frontend), Egg.js (backend), MySQL và Redis, rất hữu ích cho việc học full-stack.

  2. Với MySQL, bạn chỉ cần biết các câu lệnh INSERT và SELECT đơn giản. Trong dự án này, MongoDB có vẻ phù hợp hơn, nhưng tôi chọn MySQL vì sự tiện lợi.

  3. Dù chưa biết C, bạn vẫn có thể nắm các khái niệm cơ bản như biến, hàm và giá trị trả về trong vòng một giờ.

  4. MQTT (Message Queuing Telemetry Transport) là một giao thức mạng (kết nối dài, nghĩa là máy chủ có thể chủ động gửi dữ liệu đến client), hoạt động trên nền TCP/IP. Nó được thiết kế cho các thiết bị phần cứng có tài nguyên hạn chế và sử dụng mô hình publish/subscribe. Ví dụ:

    Minh họa giao thức MQTT

    Khi một client muốn gửi tin nhắn, quy trình như sau:

    Quy trình publish/subscribe trong MQTT

    Như hình trên, sau khi client đăng nhập thành công, nó cần đăng ký (subscribe) một chủ đề (topic). Khi một client khác gửi tin nhắn đến chủ đề đó, broker sẽ chuyển tiếp tin nhắn đến tất cả các client đã đăng ký chủ đề đó.

    Ví dụ đơn giản: Bạn, tôi và một người khác đều báo tên, số báo danh cho bảo vệ (broker). Bảo vệ cho phép chúng ta vào phòng bảo vệ chơi. Trong phòng có vô số bảng đen (topic). Mỗi người có thể yêu cầu bảo vệ: "Nếu có ai viết lên bảng này, hãy báo cho tôi". Bảo vệ sẽ ghi nhớ yêu cầu. Khi bạn viết lên một bảng, bảo vệ sẽ thông báo cho tất cả những người đã yêu cầu (kể cả bạn, nếu bạn cũng yêu cầu được báo).

  5. Bo mạch lập trình có thể chạy các chương trình để điều khiển mức điện áp (HIGH/LOW) trên các chân hoặc đọc dữ liệu từ các chân đó.

Bắt Đầu

  1. Mua bo mạch Wemos D1 và cảm biến nhiệt độ/độ ẩm DHT11 với tổng chi phí khoảng 65.000 VND (19.3 CNY).
  2. Cài đặt phụ thuộc ESP8266 cho Arduino IDE (xem hướng dẫn: Arduino IDE cài đặt SDK ESP8266).
  3. Trong IDE, vào Tệp > Tùy chọn > URL quản lý bo mạch bổ sung và thêm: http://arduino.esp8266.com/stable/package_esp8266com_index.json (có thể chuyển ngôn ngữ sang tiếng Việt nếu muốn).
  4. Cài driver CH340 (xem hướng dẫn: Cài driver CH340 trên Windows 10/11).
  5. Kết nối bo mạch với máy tính qua cáp micro-USB. Trong IDE, chọn Công cụ > Board > ESP8266 > LOLIN(WEMOS) D1 R2 & mini.
  6. Vào Quản lý thiết bị (Windows + X) để kiểm tra cổng COM của CH340, sau đó chọn cổng tương ứng trong IDE.
  7. Khi IDE hiển thị "LOLIN(WEMOS) D1 R2 & mini trên COMxx", kết nối đã thành công.
  8. Vào Tệp > Ví dụ > ESP8266 > Blink, nhấp Tải lên. Nếu đèn trên bo mạch nhấp nháy, môi trường đã sẵn sàng. Nếu lỗi, hãy kiểm tra các bước trên hoặc xem xét khả năng bo mạch bị lỗi.

Bo mạch Wemos D1 có một chân cấp nguồn 3.3V, ba hoặc nhiều chân GND. Khi kết nối cảm biến DHT11, hãy cắm chân dương (VCC) vào 3.3V, chân âm (GND) vào GND, và chân dữ liệu (DATA) vào một chân digital bất kỳ, ví dụ D5 (giá trị PIN là 14, sẽ được sử dụng sau).

Cài đặt thư viện cảm biến DHT: tìm "DHT sensor library by Adafruit" trong trình quản lý thư viện của IDE.

Ví dụ đơn giản để đọc dữ liệu từ DHT11 và in ra Serial Monitor mỗi giây:

#include "DHT.h"
#include "string.h"
#define DHTPIN 14      // Chân dữ liệu DHT11 nối với D5 (PIN 14)
#define DHTTYPE DHT11  // Loại cảm biến DHT11

DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);  // Baud rate của Wemos D1
  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("Lỗi đọc cảm biến DHT!");
    sprintf(data, "Temperature: %.1f, Humidity: %.1f", 0.0, 0.0);
    return data;
  }
  sprintf(data, "Temperature: %.1f, Humidity: %.1f", t, h);
  return data;
}

void loop() {
  char* data = getDHT11Data();
  Serial.println("Nhận: " + String(data));
  delay(1000);
}

Tiếp Theo

Nếu bạn dùng bo mạch Arduino Uno R3 thông thường, bạn có thể dừng lại ở đây. Sau khi có dữ liệu, bạn có thể điều khiển các thiết bị khác, ví dụ như bật rơle nối với chân D6 để bật máy tạo ẩm.

Nếu bạn dùng bo mạch có WiFi như Wemos D1, hãy làm theo các bước tiếp theo.

Phần Thiết Bị (Firmware)

  1. Thêm thư viện WiFi ESP8266 (đã cài đặt ở bước trên):

    #include "ESP8266WiFi.h"
  2. Cài đặt thư viện MQTT client: Tìm "PubSubClient by Nick O'Leary" trong Library Manager và cài đặt. Sau đó thêm:

    #include "PubSubClient.h"
  3. Thiết lập tên và mật khẩu WiFi:

    char* ssid = "TEN_WIFI";
    char* passwd = "MAT_KHAU_WIFI";
  4. Kết nối WiFi:

    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) {
        // Nháy LED để biểu thị đang kết nối
        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);
        }
        Serial.println("Đang kết nối WiFi... " + String(timeCount));
        timeCount++;
        isConnect = 1;
        if (timeCount >= 200) {
          isConnect = 0;
          break;
        }
      }
      if (isConnect == 1) {
        Serial.println("Kết nối WiFi thành công! SSID: " + WiFi.SSID());
        Serial.println("Địa chỉ MAC: " + WiFi.macAddress());
        analogWrite(BUILTIN_LED, 250); // LED sáng
        settMqttConfig(); // Kết nối MQTT
      } else {
        analogWrite(BUILTIN_LED, 255); // LED tắt
        delay(60000); // Chờ 1 phút rồi thử lại
      }
    }
  5. Kết nối MQTT Broker:

    const char* mqtt_server = "larryblog.top";
    const char* TOPIC = "testtopic";
    const char* client_id = "mqttx_3b2687d2";
    
    PubSubClient client(espClient);
    
    void settMqttConfig() {
      client.setServer(mqtt_server, 1883);
      client.setCallback(onMessage);
      Serial.println("Đang kết nối MQTT...");
      client.connect(client_id, "wemos", "aa995231030"); // Tên đăng nhập, mật khẩu
      client.subscribe(TOPIC);
      Serial.println("Đã kết nối MQTT");
    }
    
    // Hàm callback khi nhận tin nhắn
    void onMessage(char* topic, byte* payload, unsigned int length) {
      Serial.print("Tin nhắn đến [");
      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("Nhận: " + String(data));
        client.publish("wemos/dht11", data);
      }
      free(payloadStr);
    }
  6. Gửi tin nhắn MQTT:

    client.publish("home/status/", "{device:client_id,'status':'on'}");

Mã nguồn đầy đủ cho thiết bị:

#include "ESP8266WiFi.h"
#include "PubSubClient.h"
#include "DHT.h"
#include "string.h"

#define DHTPIN 14
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

char* ssid = "TEN_WIFI";
char* passwd = "MAT_KHAU_WIFI";
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);
  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("Lỗi đọc cảm biến DHT!");
    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);
    }
    Serial.println("Đang kết nối WiFi... " + String(timeCount));
    timeCount++;
    isConnect = 1;
    if (timeCount >= 200) {
      isConnect = 0;
      break;
    }
  }
  if (isConnect == 1) {
    Serial.println("Kết nối WiFi thành công! SSID: " + WiFi.SSID());
    Serial.println("Địa chỉ MAC: " + WiFi.macAddress());
    analogWrite(BUILTIN_LED, 250);
    settMqttConfig();
  } else {
    analogWrite(BUILTIN_LED, 255);
    delay(60000);
  }
}

void settMqttConfig() {
  client.setServer(mqtt_server, 1883);
  client.setCallback(onMessage);
  Serial.println("Đang kết nối MQTT...");
  client.connect(client_id, "wemos", "aa995231030");
  client.subscribe(TOPIC);
  Serial.println("Đã kết nối MQTT");
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  Serial.print("Tin nhắn đến [");
  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("Nhận: " + String(data));
    client.publish("wemos/dht11", data);
  }
  free(payloadStr);
}

void publishDhtData() {
  char* data = getDHT11Data();
  Serial.println("Nhận: " + String(data));
  client.publish("wemos/dht11", data);
  delay(2000);
}

void reconnect() {
  Serial.print("Đang thử kết nối lại MQTT...");
  if (client.connect(client_id, "wemos", "aa995231030")) {
    Serial.println("Kết nối lại thành công");
    client.subscribe(TOPIC);
  } else {
    Serial.print("Thất bại, rc=");
    Serial.print(client.state());
    Serial.println(" Thử lại sau 5 giây");
    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'}");
  }
  delay(1000);
}

Phần Máy Chủ (Server)

Chúng ta cần triển khai MQTT broker (máy chủ trung tâm tin nhắn). Hãy tìm kiếm "EMQX" trên mạng. Đây là một phần mềm có giao diện trực quan, đẹp và mạnh mẽ.

Tải xuống phiên bản phù hợp với hệ điều hành máy chủ của bạn:

Trang tải xuống EMQX

Ví dụ trên Ubuntu:

  1. Mở cổng tường lửa: sudo ufw allow 18083 (cổng dashboard của EMQX).
  2. Kiểm tra với sudo ufw status.

Hình ảnh khi mở cổng thành công:

Cổng 18083 đã được mở

Ví dụ trên Windows:

  1. Mở "Windows Defender Firewall with Advanced Security".
  2. Vào "Inbound Rules" > "New Rule".
  3. Chọn "Port" > "Next".
  4. Chọn "TCP" và nhập "18083" vào "Specific local ports".
  5. Tiếp tục đến cuối và đặt tên là "emqx".

Hình ảnh khi cấu hình thành công:

Cấu hình tường lửa Windows

Cấu hình Security Group trên Cloud (Ví dụ: Alibaba Cloud):

  • Với ECS: Vào Instances > Chọn máy chủ > Security Group > Add Rule, thêm cổng 18083.
  • Với Lightweight Server: Vào Security > Firewall > Add Rule.

Sau đó, truy cập EMQX dashboard tại http://<địa-chỉ-máy-chủ>:18083 với tài khoản mặc định: admin/public.

EMQX Dashboard

Cấu hình xác thực người dùng MQTT:

Vào "Access Control" > "Authentication" > "Create", làm theo các bước. Sau đó, thêm người dùng vào "User Management":

Quản lý người dùng MQTT

  • userClient: dùng cho frontend.
  • server: dùng cho backend.
  • wemos: dùng cho thiết bị (như trong code ở trên).

Kiểm tra kết nối với MQTTX:

Tải MQTTX xuống và thử kết nối. Lưu ý: cổng 1883 (MQTT mặc định) cũng cần được mở trên tường lửa và cloud security group. Ngoài ra, nên mở các cổng:

  • 8083: WebSocket mặc định
  • 8084: WebSocket bảo mật
  • 3306: MySQL mặc định

Sau khi mở cổng, bạn có thể kết nối MQTTX và thấy các topic:

MQTTX kết nối thành công

Nếu thấy tin nhắn từ topic home/status/ (do thiết bị gửi), mọi thứ đã hoạt động tốt.

Phần Frontend, Backend và Cơ sở Dữ liệu

Frontend

Frontend sử dụng jQuery, ECharts và MQTT.js. Dưới đây là các file chính:

index.html

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Giám sát Nhiệt độ/Độ ẩm Wemos D1</title>
    <link rel="stylesheet/less" href="./style.less">
    <script src="https://cdn.bootcdn.net/ajax/libs/less.js/4.1.3/less.js"></script>
</head>
<body>
    <div id="app">
        <div id="deviceStatus">
            <span id="statusIndicator"></span>
            <span id="statusText">Đang tải trạng thái thiết bị...</span>
        </div>
        <div class="container">
            <div class="chartWrapper">
                <div id="temperatureChart"></div>
                <span>Nhiệt độ hiện tại: <span id="currentTemperature">Đang tải...</span></span>
            </div>
            <div class="chartWrapper">
                <div id="humidityChart"></div>
                <span>Độ ẩm hiện tại: <span id="currentHumidity">Đang tải...</span></span>
            </div>
        </div>
    </div>
    <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="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
    <script src="./app.js"></script>
</body>
</html>

app.js (kết hợp mqttController.js, echarts.js và logic chính)

let temperatureChart, humidityChart;
let temperatureOption, humidityOption;

$(document).ready(() => {
    // Lấy dữ liệu lịch sử từ API
    $.post("https://larryblog.top/api", { topic: "getWemosDhtData", skip: 0 })
        .done((response) => {
            if (response && response.res) {
                loadHistoricalData(response.res);
            }
        });

    // Khởi tạo biểu đồ
    setTimeout(initCharts, 100);

    // Kết nối MQTT
    const randomSuffix = Math.random().toString(36).substring(2, 8);
    const client = mqtt.connect('wss://larryblog.top/mqtt', {
        clean: true,
        connectTimeout: 4000,
        clientId: 'userClient_' + randomSuffix,
        username: 'userClient',
        password: 'aa995231030'
    });

    let deviceOfflineTimer;

    client.on('connect', () => {
        console.log('Kết nối MQTT thành công');
        showStatusMessage("Kết nối Broker thành công", 1);

        client.subscribe('wemos/dht11');
        client.subscribe('home/status/');
        client.publish('testtopic', 'getDHTData');

        // Thiết lập timer để kiểm tra thiết bị offline
        deviceOfflineTimer = setTimeout(onDeviceOffline, 3500);
    });

    client.on('reconnect', () => {
        showStatusMessage("Đang kết nối lại...", 3);
    });

    client.on('error', (err) => {
        console.error('Lỗi kết nối MQTT:', err);
        showStatusMessage("Kết nối thất bại", 2);
    });

    client.on('message', (topic, message) => {
        const msg = message.toString();

        if (topic === 'wemos/dht11') {
            const parts = msg.split(", ");
            const data = {};
            parts.forEach(part => {
                const [key, value] = part.split(": ");
                data[key.trim()] = value.trim();
            });

            // Cập nhật giá trị hiện tại
            $('#currentTemperature').text(data.Temperature + " ℃");
            $('#currentHumidity').text(data.Humidity + " %RH");

            // Cập nhật biểu đồ
            const now = moment().format("MM-DD/HH:mm:ss");
            updateChart(temperatureOption, temperatureChart, parseFloat(data.Temperature), now);
            updateChart(humidityOption, humidityChart, parseFloat(data.Humidity), now);

        } else if (topic === 'home/status/') {
            $('#statusText').text("Thiết bị đang Online");
            $('#statusIndicator').removeClass('off').addClass('on');
            clearTimeout(deviceOfflineTimer);
            deviceOfflineTimer = setTimeout(onDeviceOffline, 3500);
            showStatusMessage("Thiết bị Online", 1);
        }
    });

    function onDeviceOffline() {
        $('#statusText').text("Thiết bị Offline");
        $('#statusIndicator').removeClass('on').addClass('off');
        $('#currentTemperature').text("Không có dữ liệu");
        $('#currentHumidity').text("Không có dữ liệu");
        showStatusMessage("Thiết bị Offline", 3);
    }

    function loadHistoricalData(data) {
        if (!data || data.length === 0) return;
        for (let i = data.length - 1; i >= 0; i--) {
            const item = data[i];
            const time = moment(item.updateDatetime).format("MM-DD/HH:mm:ss");
            temperatureOption.series[0].data.push(item.temperature);
            temperatureOption.xAxis.data.push(time);
            humidityOption.series[0].data.push(item.humidity);
            humidityOption.xAxis.data.push(time);
        }
        temperatureChart.setOption(temperatureOption);
        humidityChart.setOption(humidityOption);
    }

    function initCharts() {
        const baseConfig = (color) => ({
            textStyle: { color: '#fff' },
            tooltip: {
                trigger: 'axis',
                backgroundColor: '#fff',
                textStyle: { color: '#333' }
            },
            xAxis: {
                data: [],
                boundaryGap: false,
                splitLine: { show: false },
                axisLine: { lineStyle: { color: '#fff' } }
            },
            yAxis: {
                splitLine: { show: false },
                axisTick: { show: false },
                axisLine: { show: false, lineStyle: { color: '#fff' } }
            },
            grid: {
                left: '10%',
                right: '5%',
                bottom: '5%',
                top: '5%',
                containLabel: true
            },
            series: [{
                type: 'line',
                smooth: true,
                symbol: 'none',
                data: [],
                itemStyle: { color: color },
                areaStyle: {
                    color: {
                        type: 'linear',
                        x: 0, y: 0, x2: 0, y2: 1,
                        colorStops: [
                            { offset: 0, color: color + '66' },
                            { offset: 1, color: color + '00' }
                        ]
                    }
                },
                markLine: {
                    symbol: ['none', 'none'],
                    data: [{ type: 'average', name: 'Trung bình' }]
                }
            }]
        });

        temperatureOption = JSON.parse(JSON.stringify(baseConfig('#00a890')));
        humidityOption = JSON.parse(JSON.stringify(baseConfig('#ffa74b')));

        temperatureChart = echarts.init(document.getElementById('temperatureChart'));
        humidityChart = echarts.init(document.getElementById('humidityChart'));

        temperatureChart.setOption(temperatureOption);
        humidityChart.setOption(humidityOption);
    }

    function updateChart(option, chart, value, time) {
        option.xAxis.data.push(time);
        if (option.xAxis.data.length > 100) option.xAxis.data.shift();
        option.series[0].data.push(value);
        if (option.series[0].data.length > 100) option.series[0].data.shift();
        chart.setOption(option, true);
    }

    function showStatusMessage(msg, type) {
        // Có thể thay thế bằng thư viện toast notification
        console.log(`[Status ${type}]: ${msg}`);
    }
});

style.less

* {
    padding: 0;
    margin: 0;
    color: #fff;
    box-sizing: border-box;
}

#app {
    background: #1b2028;
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;

    #deviceStatus {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 20px;

        #statusIndicator {
            display: block;
            height: 10px;
            width: 10px;
            border-radius: 50%;
            background: #b8b8b8;

            &.on { background: #00a890; }
            &.off { background: #b8b8b8; }
        }
    }

    .container {
        width: 100%;
        flex: 1;
        display: flex;

        @media (max-width: 768px) {
            flex-direction: column;
        }

        .chartWrapper {
            flex: 1;
            height: 100%;
            text-align: center;

            #temperatureChart,
            #humidityChart {
                width: 80%;
                height: 50%;
                margin: 10px auto;
            }
        }
    }
}

Backend (Egg.js)

2. Nginx Proxy (nếu dùng HTTPS)

Cấu hình proxy WebSocket MQTT qua Nginx:

server {
    listen 443 ssl;
    server_name jshub.cn;

    location /mqtt {
        proxy_pass http://localhost:8083;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    ssl_certificate /path/to/your/cert.pem;
    ssl_certificate_key /path/to/your/key.key;
}

3. Backend Service (Egg.js)

Cài đặt plugin egg-emqtt. Cấu hình và cách sử dụng chi tiết có thể tham khảo tại repo: https://gitee.com/zhu_yongbo/mqttineggjs.

MySQL

Tạo một database và một bảng đơn giản:

Cấu trúc bảng MySQL

Tối ưu: Tạo trigger và stored procedure để tự động dọn dẹp dữ liệu cũ

DELIMITER $$

CREATE TRIGGER delete_oldest_after_insert
AFTER INSERT ON wemosd1_dht11
FOR EACH ROW
BEGIN
    IF (SELECT COUNT(*) FROM wemosd1_dht11) > 43200 THEN
        CALL delete_oldest_row();
    END IF;
END$$

CREATE PROCEDURE delete_oldest_row()
BEGIN
    DELETE FROM wemosd1_dht11 ORDER BY id ASC LIMIT 1;
END$$

DELIMITER ;

Kết Luận

Chúng ta đã hoàn thành việc kết nối ba phần: frontend, backend và thiết bị phần cứng.

Tổng quan luồng dữ liệu:

  1. Bo mạch Wemos D1 đọc giá trị nhiệt độ/độ ẩm từ cảm biến DHT11.
  2. Bo mạch gửi dữ liệu qua MQTT đến một topic cụ thể.
  3. Cả frontend và backend đều đã đăng ký (subscribe) topic này.
  4. Backend nhận dữ liệu, xử lý và lưu vào MySQL.
  5. Frontend nhận dữ liệu và hiển thị trực tiếp lên biểu đồ ECharts.
  6. Khi frontend khởi động, nó có thể gọi API backend để lấy dữ liệu lịch sử (ví dụ: 8000 bản ghi cuối). Sau đó, các thay đổi mới được cập nhật theo thời gian thực thông qua MQTT.

Kết quả là một màn hình lớn hiển thị thời gian thực và dữ liệu lịch sử về nhiệt độ/độ ẩm.

Thẻ: MQTT ESP8266 Arduino Wemos D1 DHT11

Đăng vào ngày 13 tháng 6 lúc 05:08