Skip to content

Latest commit

 

History

History
295 lines (238 loc) · 11.2 KB

File metadata and controls

295 lines (238 loc) · 11.2 KB

Tài liệu Hướng dẫn Kết nối & Đặc tả Kỹ thuật dành cho Đội IoT (ESP32)

Tài liệu này cung cấp các thông số cấu hình, đặc tả giao thức truyền tải âm thanh qua HTTP Upload, cấu trúc điều khiển qua MQTT JSON, và mã nguồn C++ mẫu để đội IoT cập nhật Firmware cho thiết bị ESP32-S3 tương thích 100% với hệ thống Backend/AI.


1. Thông số kết nối MQTT Broker (EMQX Cloud Serverless)

Thiết bị IoT kết nối trực tiếp đến Cloud Broker qua giao thức bảo mật SSL/TLS:

  • MQTT Host: s0b7d811.ala.asia-southeast1.emqxsl.com
  • MQTT Port: 8883 (Bắt buộc sử dụng MQTTS qua SSL/TLS)
  • Quy định Tài khoản Đăng nhập (Dành cho từng thiết bị):
    • Mỗi thiết bị khi xuất xưởng sẽ được cấp một mã thiết bị duy nhất (ví dụ: esp32-01, esp32-02).
    • MQTT Username: device_{device_code} (Ví dụ: device_esp32-01)
    • MQTT Password: (Mật khẩu do hệ thống cấp riêng)
    • Client ID: {device_code} (Ví dụ: esp32-01, trùng khớp với mã thiết bị)

2. Quy tắc Phân quyền ACL & Topics của Thiết bị

Để đảm bảo bảo mật giữa các hộ gia đình/thiết bị khác nhau, EMQX Broker cấu hình phân quyền ACL nghiêm ngặt. Thiết bị chỉ được phép tương tác với các topics của chính mình:

Chức năng Topic tương ứng Quyền hạn Mô tả
Trạng thái thiết bị device/status/{device_code} Publish Gửi trạng thái online/offline (kết hợp LWT)
Dữ liệu cảm biến sensor/data/{device_code} Publish Gửi nhiệt độ, độ ẩm, dung lượng pin (JSON)
Nhận lệnh điều khiển cmd/{device_code} Subscribe Nhận lệnh điều khiển còi hú, đèn LED từ Backend
Nhận cấu hình config/{device_code} Subscribe Nhận tham số cấu hình từ xa (độ nhạy mic, v.v.)

Warning

Thay đổi quan trọng: Hệ thống không sử dụng MQTT để truyền âm thanh (không dùng topic audio/raw/{device_code} hay gửi chuỗi CSV thô qua MQTT). Việc truyền âm thanh hiện tại được thực hiện hoàn toàn qua HTTP POST Upload để tránh gây nghẽn băng thông MQTT Broker.


3. Đặc tả Định dạng Tin nhắn & Giao thức

3.1. Luồng Truyền tải Âm thanh (HTTP POST Upload)

Khi thiết bị phát hiện âm lượng micro vượt ngưỡng $\rightarrow$ Thực hiện ghi âm đoạn âm thanh (khoảng 1.5 - 3 giây) ➜ Đóng gói thành file .wav ➜ Gửi yêu cầu HTTP POST lên API của Backend.

  • Method: POST
  • Endpoint: http://[backend-ip]:3000/api/upload
  • Content-Type: multipart/form-data
  • Body Fields:
    • file: Dữ liệu nhị phân của tệp ghi âm .wav (tên file: {device_code}_{timestamp}.wav).
    • device_code: Mã thiết bị (ví dụ: esp32-01).
  • Phản hồi thành công từ Backend (201 Created):
    {
      "success": true,
      "event_id": "84a3b8d9-2045-4dfc-a492-c2e59178bfd2",
      "audio_url": "https://[project-id].supabase.co/storage/v1/object/public/audio-uploads/esp32-01_1780656604256.wav"
    }

3.2. Nhận Lệnh Điều khiển Cảnh báo từ Backend (MQTT cmd/{device_code})

Thiết bị Subscribe topic cmd/{device_code} để nhận lệnh rung và nháy LED cảnh báo khi AI phát hiện âm thanh nguy hiểm. Payload nhận được ở định dạng JSON:

  • Payload mẫu (Bật cảnh báo):
    {
      "action": "alarm_on",
      "reason": "Phát hiện phá cửa",
      "severity": "critical", // Mức độ nguy hiểm: critical, warning, info
      "led": {
        "color": "#ff0000",   // Mã màu HEX gửi xuống
        "mode": "blink_fast"  // Chế độ nháy: blink_fast, blink_slow, solid
      },
      "vibration": {
        "mode": "strong"      // Cường độ rung: strong, medium, weak, off
      },
      "duration_seconds": 15  // Thời gian duy trì cảnh báo
    }
  • Payload mẫu (Tắt cảnh báo chủ động):
    {
      "action": "alarm_off"
    }

4. Mã nguồn C++ tham khảo dành cho ESP32-S3 (PlatformIO / Arduino)

Dưới đây là khung code mẫu đã tối ưu hóa cho ESP32-S3 sử dụng thư viện WiFiClientSecure, PubSubClient và ArduinoJson (v6 hoặc v7).

4.1. File cấu hình platformio.ini

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
board_build.arduino.memory_type = qio_opi ; Kích hoạt PSRAM cho chip N8R8
lib_deps =
    knolleary/PubSubClient @ ^2.8
    bblanchon/ArduinoJson @ ^7.0.4

4.2. File mã nguồn chính main.cpp

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <FS.h>
#include <SPIFFS.h>

// Cấu hình Wi-Fi
const char* ssid     = "Ten_WiFi_Cua_Ban";
const char* password = "Mat_Khau_WiFi";

// Cấu hình MQTT Broker (EMQX Cloud)
const char* mqtt_server   = "s0b7d811.ala.asia-southeast1.emqxsl.com";
const int mqtt_port       = 8883; 
const char* device_code   = "esp32-01"; // Client ID
const char* mqtt_user     = "device_esp32-01";
const char* mqtt_password = "dev_esp32_pass";

// API URL để upload file âm thanh
const char* upload_url = "http://[IP-BACKEND]:3000/api/upload";

// Các topics
String topic_cmd    = "cmd/" + String(device_code);
String topic_status = "device/status/" + String(device_code);

WiFiClientSecure espClient;
PubSubClient client(espClient);

// Hàm xử lý khi nhận lệnh JSON từ Backend
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("📩 Nhận tin nhắn trên topic: ");
  Serial.println(topic);

  // Parse gói tin JSON nhận được
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, payload, length);

  if (error) {
    Serial.print("❌ Lỗi giải mã JSON: ");
    Serial.println(error.c_str());
    return;
  }

  const char* action = doc["action"];
  if (strcmp(action, "alarm_on") == 0) {
    const char* reason = doc["reason"];
    const char* severity = doc["severity"];
    const char* led_color = doc["led"]["color"];
    const char* led_mode = doc["led"]["mode"];
    const char* vib_mode = doc["vibration"]["mode"];
    int duration = doc["duration_seconds"] | 10;

    Serial.printf("🚨 Bật còi! Lý do: %s | Mức độ: %s | LED: %s (%s) | Rung: %s | Thời gian: %ds\n", 
                  reason, severity, led_color, led_mode, vib_mode, duration);

    // TODO: Viết code điều khiển chân đèn LED và mô-tơ rung thực tế tại đây
    // Ví dụ: Bật mô-tơ rung dựa trên vib_mode và led_color
  } 
  else if (strcmp(action, "alarm_off") == 0) {
    Serial.println("📴 Tắt còi báo động.");
    // TODO: Viết code tắt còi và LED thực tế tại đây
  }
}

// Gửi file ghi âm .wav lên Backend qua HTTP POST
bool upload_wav_file(const char* filepath) {
  if (WiFi.status() != WL_CONNECTED) return false;

  File file = SPIFFS.open(filepath, FILE_READ);
  if (!file) {
    Serial.println("❌ Không tìm thấy file âm thanh để upload!");
    return false;
  }

  HTTPClient http;
  http.begin(upload_url);

  // Khởi tạo định dạng Multipart Form-Data
  String boundary = "----ESP32Boundary" + String(millis());
  http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

  // Tạo header multipart
  String startBoundary = "--" + boundary + "\r\n";
  String endBoundary = "\r\n--" + boundary + "--\r\n";
  
  String fieldDevice = startBoundary + 
                       "Content-Disposition: form-data; name=\"device_code\"\r\n\r\n" + 
                       String(device_code) + "\r\n";
                       
  String fileHeader = startBoundary + 
                      "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n" + 
                      "Content-Type: audio/wav\r\n\r\n";

  int contentLength = fieldDevice.length() + fileHeader.length() + file.size() + endBoundary.length();
  
  // Tiến hành POST stream dữ liệu
  int httpResponseCode = http.sendRequest("POST", 
    (uint8_t*)NULL, 0, // Không gửi body tĩnh
    [&](WiFiClient* client) { // Gửi body động qua Stream lambda
      client->write((uint8_t*)fieldDevice.c_str(), fieldDevice.length());
      client->write((uint8_t*)fileHeader.c_str(), fileHeader.length());
      
      uint8_t buffer[512];
      while (file.available()) {
        int bytesRead = file.read(buffer, sizeof(buffer));
        client->write(buffer, bytesRead);
      }
      
      client->write((uint8_t*)endBoundary.c_str(), endBoundary.length());
      return contentLength;
    }, 
    contentLength
  );

  file.close();
  
  if (httpResponseCode == 201) {
    Serial.println("✅ Tải file lên Cloud qua Backend thành công!");
    String response = http.getString();
    Serial.println("Response: " + response);
    http.end();
    return true;
  } else {
    Serial.printf("❌ Tải lên thất bại. HTTP Code: %d\n", httpResponseCode);
    http.end();
    return false;
  }
}

// Kết nối hoặc tự động kết nối lại MQTT
void reconnect() {
  while (!client.connected()) {
    Serial.print("🔌 Đang kết nối tới MQTT Broker...");
    
    // Cấu hình di chúc Last Will (LWT) báo trạng thái Offline tự động khi ngắt kết nối đột ngột
    String willTopic = topic_status;
    String willPayload = "{\"status\":\"offline\"}";
    
    if (client.connect(device_code, mqtt_user, mqtt_password, 
                       willTopic.c_str(), 1, true, willPayload.c_str())) {
      Serial.println("Đã kết nối thành công!");
      
      // Báo trạng thái Online
      client.publish(topic_status.c_str(), "{\"status\":\"online\"}", true);
      
      // Đăng ký nhận kênh điều khiển
      client.subscribe(topic_cmd.c_str(), 1);
    } else {
      Serial.print("Thất bại, lỗi rc=");
      Serial.print(client.state());
      Serial.println(". Thử lại sau 5 giây...");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // Khởi tạo SPIFFS để lưu tệp âm thanh tạm thời
  if (!SPIFFS.begin(true)) {
    Serial.println("❌ Lỗi khi khởi tạo bộ nhớ tạm SPIFFS!");
  }

  // Kết nối WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi Connected!");

  // Cấu hình bảo mật SSL/TLS (Bỏ qua CA validation để tránh tràn bộ nhớ RAM của chip)
  espClient.setInsecure();

  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  
  // TODO: Viết code quét I2S, nếu phát hiện âm thanh lớn thì:
  // 1. Ghi âm đoạn âm thanh 2 giây lưu vào "/temp_audio.wav" trên SPIFFS.
  // 2. Gọi hàm upload_wav_file("/temp_audio.wav").
}