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.
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ị)
- 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ụ:
Để đả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.
Khi thiết bị phát hiện âm lượng micro vượt ngưỡng .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" }
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" }
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).
[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#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").
}