ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay

ESP32 dùng Firebase Realtime Database không cần thư viện Firebase: đăng nhập ẩn danh, tự làm mới token, đọc lệnh relay, ghi trạng thái và lịch sử, Rules theo uid.

Firebase Realtime Database là cơ sở dữ liệu trên máy chủ Google, lưu dữ liệu dạng cây JSON, có gói miễn phí và bảng điều khiển trên web để xem, sửa dữ liệu. ESP32 thường dùng thư viện Firebase riêng, nhưng mọi thao tác thật ra chỉ là các yêu cầu HTTPS: đăng nhập lấy token, rồi đọc, ghi bằng REST. Bài này làm theo cách đó, chỉ dùng HTTPClient có sẵn trong gói esp32 và ArduinoJson: ESP32 đăng nhập ẩn danh, tự làm mới token mỗi giờ, ghi trạng thái relay 4 kênh, nhận lệnh bật tắt và lưu lịch sử. Viết tay giúp hiểu rõ từng bước, ít phụ thuộc thư viện, và dễ đổi sang dịch vụ khác.

1. Dữ liệu được tổ chức thế nào

Cây dữ liệu thiet_bi, uid, lenh với k1 đến k4, trang_thai với k1 đến k4 rssi giay cap_nhat, lich_su với các khóa do máy chủ sinh; khung Rules chỉ cho thiết bị đọc ghi nhánh mang uid của nó
Cây dữ liệu của một thiết bị và Rules (uid, số liệu là ví dụ).
  • thiet_bi/<uid>: mỗi mạch một nhánh, uid là mã tài khoản ẩn danh của mạch đó.
  • lenh: nơi người dùng sửa k1…k4 thành true/false; ESP32 đọc mỗi 5 giây.
  • trang_thai: ESP32 báo trạng thái relay thật, cường độ Wi-Fi, thời gian chạy; cap_nhat là thời điểm do máy chủ điền.
  • lich_su: mỗi 10 phút thêm một dòng, khóa do máy chủ sinh theo thứ tự thời gian.

2. Tạo dự án Firebase

Tên menu ghi theo giao diện tiếng Anh của bảng điều khiển Firebase (console.firebase.google.com), có thể thay đổi theo thời gian.

  1. Tạo dự án: đăng nhập bằng tài khoản Google, tạo dự án mới (có thể tắt Google Analytics).
  2. Bật đăng nhập ẩn danh: vào Authentication › Sign-in method, bật Anonymous.
  3. Tạo cơ sở dữ liệu: vào Realtime Database › Create Database, chọn vị trí Singapore (asia-southeast1) cho gần Việt Nam, chọn chế độ khóa (locked mode). Chép địa chỉ cơ sở dữ liệu ở đầu trang, dạng https://ten-du-an-default-rtdb.asia-southeast1.firebasedatabase.app; cơ sở dữ liệu đặt ở Mỹ thì địa chỉ có đuôi firebaseio.com.
  4. Dán Rules: trong tab Rules của Realtime Database, dán nội dung như hình ở mục 1 rồi bấm Publish.
  5. Lấy Web API Key: trong Project settings › General. Nếu chưa thấy khóa, bật Authentication trước hoặc thêm một ứng dụng web vào dự án.

3. Chuẩn bị và sơ đồ nối

Sơ đồ nối module relay 4 kênh IN1 IN2 IN3 IN4 vào GPIO32 33 26 27 của ESP32 DevKit V1, VCC vào VIN, GND chung, nút nhấn từ GPIO4 xuống GND; bảng nối dây
Cùng sơ đồ với các bài relay khác trong loạt.

Relay đóng cắt điện 220 V phải lắp trong hộp kín, dây lưới điện có cầu chì và đủ tiết diện; khi chưa quen làm điện, chỉ thử với đèn LED hoặc tải điện áp thấp.

4. Chương trình

Cần thư viện ArduinoJson (Benoit Blanchon, bản 7) trong Library Manager; phần còn lại có sẵn trong gói esp32. Trước khi nạp, sửa tên và mật khẩu Wi-Fi, API_KEY và DB_URL (không có dấu / ở cuối).

// PYWORLD.VN - ESP32 ghi trạng thái và nhận lệnh relay qua Firebase Realtime Database bằng REST, không dùng thư viện Firebase
// Đăng nhập ẩn danh bằng Firebase Auth REST, tự làm mới ID token trước khi hết hạn, lưu refresh token bằng Preferences
// Relay 4 kênh ở GPIO32, 33, 26, 27 (kích mức thấp), nút nhấn GPIO4 xuống GND; thư viện ArduinoJson 7

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Preferences.h>
#include <ArduinoJson.h>

const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *API_KEY = "KHOA_WEB_API";  // Project settings › General › Web API Key
// Địa chỉ cơ sở dữ liệu, chép ở đầu trang Realtime Database (không có dấu / ở cuối)
const char *DB_URL = "https://ten-du-an-default-rtdb.asia-southeast1.firebasedatabase.app";

const uint8_t RELAY_PINS[4] = {32, 33, 26, 27};  // IN1, IN2, IN3, IN4
const bool RELAY_ACTIVE_LOW = true;
const uint8_t NUT = 4;
const unsigned long DOC_LENH_MS = 5000;          // Hỏi lệnh mỗi 5 giây
const unsigned long GHI_TRANG_THAI_MS = 30000;   // Báo trạng thái mỗi 30 giây
const unsigned long GHI_LICH_SU_MS = 600000;     // Thêm một dòng lịch sử mỗi 10 phút

Preferences pref;
WiFiClientSecure tlsDb;  // Kết nối tới máy chủ cơ sở dữ liệu, giữ mở giữa các lần gọi
HTTPClient httpDb;
String idToken, refreshToken, uid;
unsigned long lucCoToken = 0, thoiHanToken = 0;  // ms
bool relayBat[4] = {false, false, false, false};
bool canGhiTrangThai = true;

void datRelay(uint8_t kenh, bool bat) {
  relayBat[kenh] = bat;
  bool muc = RELAY_ACTIVE_LOW ? !bat : bat;
  digitalWrite(RELAY_PINS[kenh], muc ? HIGH : LOW);
  Serial.printf("Relay K%u: %s\n", (unsigned)(kenh + 1), bat ? "BAT" : "TAT");
}

// Gửi một yêu cầu tới máy chủ đăng nhập (identitytoolkit / securetoken); mỗi lần một kết nối riêng
int goiAuth(const String &url, const char *kieu, const String &noiDung, JsonDocument &kq) {
  WiFiClientSecure tls;
  tls.setInsecure();  // Mã hóa nhưng không kiểm chứng chỉ máy chủ, xem giải thích trong bài
  HTTPClient http;
  if (!http.begin(tls, url)) return -1;
  http.addHeader("Content-Type", kieu);
  int ma = http.POST(noiDung);
  String phanHoi = http.getString();
  http.end();
  deserializeJson(kq, phanHoi);
  if (ma != HTTP_CODE_OK) {
    Serial.printf("Firebase Auth tu choi (HTTP %d): %s\n", ma, (const char *)(kq["error"]["message"] | "khong ro"));
  }
  return ma;
}

bool dangKyAnDanh() {
  Serial.println("Dang ky tai khoan an danh moi");
  JsonDocument kq;
  int ma = goiAuth(String("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=") + API_KEY, "application/json",
                   "{\"returnSecureToken\":true}", kq);
  if (ma != HTTP_CODE_OK) return false;
  idToken = kq["idToken"] | "";
  refreshToken = kq["refreshToken"] | "";
  uid = kq["localId"] | "";
  thoiHanToken = String(kq["expiresIn"] | "3600").toInt() * 1000UL;
  return idToken.length() > 0;
}

// Trả về 1 nếu thành công, 0 nếu lỗi mạng (thử lại sau), -1 nếu refresh token không còn dùng được
int lamMoiToken() {
  JsonDocument kq;
  int ma = goiAuth(String("https://securetoken.googleapis.com/v1/token?key=") + API_KEY, "application/x-www-form-urlencoded",
                   "grant_type=refresh_token&refresh_token=" + refreshToken, kq);
  if (ma == HTTP_CODE_OK) {
    idToken = kq["id_token"] | "";
    refreshToken = kq["refresh_token"] | refreshToken.c_str();
    uid = kq["user_id"] | "";
    thoiHanToken = String(kq["expires_in"] | "3600").toInt() * 1000UL;
    return idToken.length() > 0 ? 1 : 0;
  }
  return (ma == 400 || ma == 403) ? -1 : 0;  // TOKEN_EXPIRED, USER_NOT_FOUND, INVALID_REFRESH_TOKEN…
}

bool layToken() {
  int kq = 0;
  if (refreshToken.length()) kq = lamMoiToken();
  if (kq == -1 || refreshToken.length() == 0) {
    if (kq == -1) Serial.println("Refresh token khong con dung duoc, tao tai khoan moi (uid se doi)");
    kq = dangKyAnDanh() ? 1 : 0;
  }
  if (kq != 1) return false;
  if (thoiHanToken < 600000UL) thoiHanToken = 3600000UL;  // Phòng giá trị lạ: coi như 1 giờ
  lucCoToken = millis();
  pref.begin("firebase", false);
  if (pref.getString("rt", "") != refreshToken) pref.putString("rt", refreshToken);  // Giữ uid sau khi khởi động lại
  pref.end();
  Serial.printf("Dang nhap Firebase OK, uid %s, token dung %lu giay\n", uid.c_str(), thoiHanToken / 1000);
  return true;
}

// Gọi REST tới /thiet_bi/<uid><duongDan>.json; ghi thì thêm print=silent để máy chủ không gửi lại dữ liệu
int goiDb(const char *phuongThuc, const String &duongDan, const String &json, String *ketQua = nullptr) {
  String url = String(DB_URL) + "/thiet_bi/" + uid + duongDan + ".json?auth=" + idToken;
  if (ketQua == nullptr) url += "&print=silent";
  if (!httpDb.begin(tlsDb, url)) return -1;
  httpDb.addHeader("Content-Type", "application/json");
  int ma = httpDb.sendRequest(phuongThuc, json);
  if (ketQua) *ketQua = httpDb.getString();
  httpDb.end();
  if (ma == 401) {
    // Token cũ hơn 1 phút: có thể đã hết hạn, lần sau lấy token mới. Token mới mà vẫn bị từ chối: Rules chưa cho phép
    if (millis() - lucCoToken > 60000) lucCoToken = 0;
    else Serial.println("Bi tu choi (401): kiem tra Rules cua Realtime Database");
  }
  return ma;
}

String jsonRelay() {
  JsonDocument d;
  for (int i = 0; i < 4; i++) d["k" + String(i + 1)] = relayBat[i];
  String s;
  serializeJson(d, s);
  return s;
}

void docLenh() {
  String s;
  int ma = goiDb("GET", "/lenh", "", &s);
  if (ma != HTTP_CODE_OK) {
    Serial.printf("Doc lenh loi HTTP %d\n", ma);
    return;
  }
  if (s == "null") {  // Lần đầu: chưa có nhánh lenh, tạo bằng trạng thái hiện tại
    Serial.println("Tao nhanh lenh ban dau");
    goiDb("PUT", "/lenh", jsonRelay());
    return;
  }
  JsonDocument d;
  if (deserializeJson(d, s)) return;
  for (int i = 0; i < 4; i++) {
    JsonVariant v = d["k" + String(i + 1)];
    if (v.is<bool>() && v.as<bool>() != relayBat[i]) {
      datRelay(i, v.as<bool>());
      canGhiTrangThai = true;
    }
  }
}

void ghiTrangThai() {
  JsonDocument d;
  for (int i = 0; i < 4; i++) d["k" + String(i + 1)] = relayBat[i];
  d["rssi"] = WiFi.RSSI();
  d["giay"] = millis() / 1000;
  d["cap_nhat"][".sv"] = "timestamp";  // Máy chủ tự điền thời điểm (mili giây từ 1/1/1970)
  String s;
  serializeJson(d, s);
  int ma = goiDb("PATCH", "/trang_thai", s);
  if (ma != HTTP_CODE_OK && ma != HTTP_CODE_NO_CONTENT) Serial.printf("Ghi trang_thai loi HTTP %d\n", ma);
}

void ghiLichSu() {
  JsonDocument d;
  d["rssi"] = WiFi.RSSI();
  d["relay"] = (relayBat[0] ? 1 : 0) | (relayBat[1] ? 2 : 0) | (relayBat[2] ? 4 : 0) | (relayBat[3] ? 8 : 0);
  d["luc"][".sv"] = "timestamp";
  String s;
  serializeJson(d, s);
  int ma = goiDb("POST", "/lich_su", s);  // POST: máy chủ tự đặt khóa theo thứ tự thời gian
  Serial.printf("Ghi lich_su: HTTP %d\n", ma);
}

// Nút nhấn tại chỗ đảo K1 và ghi lại vào nhánh lenh, để lần đọc lệnh sau không đảo ngược lại
void kiemTraNut() {
  static bool docTruoc = HIGH, onDinh = HIGH;
  static unsigned long lucDoi = 0;
  bool doc = digitalRead(NUT);
  if (doc != docTruoc) {
    docTruoc = doc;
    lucDoi = millis();
  }
  if (millis() - lucDoi > 30 && doc != onDinh) {  // Chống dội 30 ms
    onDinh = doc;
    if (onDinh == LOW) {
      datRelay(0, !relayBat[0]);
      canGhiTrangThai = true;
      if (idToken.length()) goiDb("PATCH", "/lenh", relayBat[0] ? "{\"k1\":true}" : "{\"k1\":false}");
    }
  }
}

void setup() {
  Serial.begin(115200);
  for (uint8_t i = 0; i < 4; i++) {
    pinMode(RELAY_PINS[i], INPUT);                               // Chân chưa xuất tín hiệu
    digitalWrite(RELAY_PINS[i], RELAY_ACTIVE_LOW ? HIGH : LOW);  // Ghi sẵn mức tắt
    pinMode(RELAY_PINS[i], OUTPUT);                              // Bật ngõ ra, relay vẫn tắt
  }
  pinMode(NUT, INPUT_PULLUP);
  pref.begin("firebase", true);
  refreshToken = pref.getString("rt", "");
  pref.end();
  tlsDb.setInsecure();
  httpDb.setReuse(true);
  httpDb.setTimeout(10000);
  WiFi.mode(WIFI_STA);
  WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
  Serial.println("Dang ket noi Wi-Fi...");
}

void loop() {
  static bool daBaoIp = false;
  static unsigned long lanThuToken = 0, lanDocLenh = 0, lanGhiTrangThai = 0, lanGhiLichSu = 0;
  static bool daGhiLichSu = false;

  kiemTraNut();
  if (WiFi.status() != WL_CONNECTED) return;
  if (!daBaoIp) {
    daBaoIp = true;
    Serial.printf("Da vao Wi-Fi, IP %s\n", WiFi.localIP().toString().c_str());
  }

  // Làm mới token 5 phút trước khi hết hạn (token sống 3600 giây)
  bool canToken = idToken.length() == 0 || lucCoToken == 0 || millis() - lucCoToken > thoiHanToken - 300000UL;
  if (canToken) {
    if (lanThuToken != 0 && millis() - lanThuToken < 30000) return;  // Lỗi thì 30 giây sau thử lại
    lanThuToken = millis();
    if (!layToken()) return;
    lanThuToken = 0;
  }

  if (millis() - lanDocLenh > DOC_LENH_MS) {
    lanDocLenh = millis();
    docLenh();
  }
  if (canGhiTrangThai || millis() - lanGhiTrangThai > GHI_TRANG_THAI_MS) {
    canGhiTrangThai = false;
    lanGhiTrangThai = millis();
    ghiTrangThai();
  }
  if (!daGhiLichSu || millis() - lanGhiLichSu > GHI_LICH_SU_MS) {
    daGhiLichSu = true;
    lanGhiLichSu = millis();
    ghiLichSu();
  }
}

5. Đăng nhập ẩn danh và làm mới token

Sơ đồ trình tự giữa ESP32, Firebase Auth và Realtime Database: đăng ký ẩn danh nhận idToken và refreshToken, đọc ghi kèm auth bằng idToken, 55 phút sau đổi refresh token lấy id_token mới
Trình tự đăng nhập, đọc ghi và làm mới token.
  • Lần đầu: ESP32 gửi POST accounts:signUp với {"returnSecureToken":true}. Firebase tạo một tài khoản ẩn danh và trả về idToken (dùng để đọc ghi, sống 3600 giây), refreshToken (dùng lâu dài) và localId (uid).
  • Lưu refresh token vào Preferences. Khởi động lại, ESP32 đổi refresh token lấy ID token mới qua securetoken.googleapis.com/v1/token, nên giữ nguyên uid và nhánh dữ liệu cũ. Nếu mỗi lần khởi động lại đều đăng ký mới, mỗi lần sẽ là một uid khác và một nhánh dữ liệu mới.
  • Làm mới trước hạn: chương trình đổi token khi còn 5 phút. Bị từ chối (HTTP 401) với token cũ hơn 1 phút thì cũng đổi token.
  • Refresh token hỏng (tài khoản bị xóa, bị vô hiệu): chương trình đăng ký tài khoản mới và in uid mới ra Serial. Nếu dự án nâng cấp lên Identity Platform, đừng bật chức năng tự xóa tài khoản ẩn danh quá 30 ngày cho dự án này, vì mạch chạy lâu sẽ bị đổi uid.

6. Đọc, ghi bằng REST

Mọi đường dẫn có đuôi .json và tham số auth= là ID token:

  • GET …/lenh.json: đọc lệnh. Chưa có dữ liệu thì máy chủ trả về null, chương trình tạo nhánh lenh bằng PUT.
  • PATCH …/trang_thai.json: chỉ ghi đè các khóa gửi lên, giữ nguyên khóa khác. PUT thì thay cả nhánh.
  • POST …/lich_su.json: thêm một mục với khóa mới do máy chủ sinh, sắp theo thời gian.
  • {".sv": "timestamp"}: máy chủ thay bằng thời điểm hiện tại (mili giây tính từ 1/1/1970), nên ESP32 không cần biết giờ.
  • print=silent khi ghi: máy chủ trả mã 204 và không gửi lại dữ liệu, bớt lưu lượng.

Các lệnh tới cơ sở dữ liệu dùng chung một WiFiClientSecure và HTTPClient khai báo toàn cục, để giữ kết nối mở giữa các lần gọi thay vì bắt tay TLS lại mỗi 5 giây. Yêu cầu đăng nhập đi tới máy chủ khác nên dùng đối tượng riêng. setInsecure() mã hóa đường truyền nhưng không kiểm tra chứng chỉ máy chủ; muốn chặt hơn thì nạp chứng chỉ gốc bằng setCACert().

7. Rules: ai được đọc, ghi

Khóa $uid trong Rules khớp với mọi nhánh con của thiet_bi; điều kiện auth.uid === $uid nghĩa là chỉ tài khoản có đúng uid đó mới đọc, ghi được nhánh của nó. Mạch khác, hay người lấy được Web API Key rồi tự đăng ký ẩn danh, cũng chỉ thấy nhánh của chính mình. Bảng điều khiển Firebase dùng quyền quản trị nên vẫn xem, sửa được mọi nhánh. Không để cơ sở dữ liệu ở chế độ thử (ai cũng đọc, ghi được) lâu hơn vài phút.

8. Điều khiển và kết quả

Mở Realtime Database › Data, đi tới thiet_bi › <uid> › lenh › k2, sửa thành true: trong vòng 5 giây relay K2 đóng, trang_thai/k2 đổi theo. Bấm nút nhấn tại chỗ thì K1 đảo và lenh/k1 cũng được ghi lại, để lần đọc lệnh sau không đảo ngược. Muốn điều khiển bằng ứng dụng riêng, cần một tài khoản đăng nhập cho người dùng (ví dụ email) và sửa Rules cho phép tài khoản đó vào nhánh của mạch.

Serial Monitor: lần chạy đầu đăng ký tài khoản ẩn danh, đăng nhập OK với uid, tạo nhánh lệnh, ghi lịch sử HTTP 204, relay K2 và K1 bật; khởi động lại giữ nguyên uid; hai lỗi thường gặp OPERATION_NOT_ALLOWED và 401
Serial Monitor ở tốc độ 115200 (uid, IP là ví dụ).
Bảng số lần gọi mỗi ngày: đọc lệnh 17.280, ghi trạng thái 2.880, thêm lịch sử 144, làm mới token 26; ước tính dữ liệu tải về mỗi tháng khoảng 0,3 GB nếu giữ kết nối, khoảng 2,3 GB nếu bắt tay TLS mỗi lần, so với 10 GB của gói Spark
Ước tính cho một thiết bị chạy suốt ngày, so với gói miễn phí Spark.

Gói miễn phí Spark của Realtime Database có 1 GB lưu trữ, 10 GB dữ liệu tải về mỗi tháng và tối đa 100 kết nối đồng thời. Một mạch như bài này dùng một phần nhỏ; nhiều mạch thì nhân lên, và đọc lệnh 5 giây một lần là phần tốn nhất.

9. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
"Firebase Auth tu choi (HTTP 400): OPERATION_NOT_ALLOWED" (có khi là ADMIN_ONLY_OPERATION)Chưa bật đăng nhập ẩn danhAuthentication › Sign-in method › bật Anonymous
Báo "API key not valid"Chép sai Web API KeyChép lại trong Project settings › General
"Doc lenh loi HTTP 401" ngay sau khi đăng nhậpRules chưa đúng hoặc chưa bấm PublishDán lại Rules, Publish; kiểm tra đường dẫn thiet_bi
"Doc lenh loi HTTP 404" hoặc -1Sai DB_URL (tên, đuôi vùng), thừa dấu /, mất InternetChép lại địa chỉ ở đầu trang Realtime Database
Mỗi lần khởi động lại uid đổiRefresh token không lưu được, nạp lại có xóa toàn bộ flash, tài khoản bị xóaTắt Erase All Flash khi nạp; không bật tự xóa tài khoản ẩn danh
Bấm nút tại chỗ, vài giây sau relay đảo lạiLúc bấm mất mạng, chưa ghi được lenh/k1Kiểm tra Wi-Fi; sửa lenh/k1 trên bảng điều khiển cho khớp

10. Xem thêm


Tin tức liên quan

RFID RC522 với ESP32: đọc mã thẻ không cần thư viện, lưu thẻ hợp lệ, mở khóa bằng relay
RFID RC522 với ESP32: đọc mã thẻ không cần thư viện, lưu thẻ hợp lệ, mở khóa bằng relay

Nối RC522 với ESP32 qua SPI, hiểu REQA, chống va chạm, SELECT, HLTA, đọc UID 4 và 7 byte không cần thư viện, lưu thẻ bằng Preferences, đóng relay khi quẹt đúng thẻ.

Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái
Điều khiển relay bằng điện thoại với Blynk IoT và ESP32: template, datastream, đồng bộ trạng thái

Hướng dẫn Blynk IoT với ESP32 từ đầu: tạo template, datastream V0–V5, thiết bị và token, nối relay 4 kênh, chương trình bật tắt bằng điện thoại kèm nút nhấn tại chỗ.

Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32
Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32

Hướng dẫn nối màn hình OLED 1.3 inch SH1106 với ESP32 DevKit V1 qua I2C, cài thư viện Adafruit SH110X, chạy code hiển thị chữ và xử lý lỗi thường gặp.

Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng
Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng

Làm xe robot hai bánh với ESP32 và L298N, điều khiển bằng trình duyệt điện thoại qua Wi-Fi do ESP32 phát ra: bố trí, nối dây, code web, tự dừng khi mất sóng, lái vi sai, STEM.

ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED
ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED

Hướng dẫn dựng mô hình ESP-NOW nhiều node: tối đa bốn ESP32 gửi số đo về một trạm thu, trạm thu hiện điện áp, RSSI và tình trạng từng node trên màn hình OLED 0.96 inch.

Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32
Mạch hạ áp LM2596 và MP1584: cách chọn, chỉnh điện áp và cấp nguồn ESP32

So sánh mạch hạ áp LM2596 và MP1584, cách chỉnh điện áp ra bằng đồng hồ vạn năng trước khi nối tải, cấp nguồn 5 V cho ESP32 DevKit V1 từ nguồn 12 V và xử lý lỗi thường gặp.

Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay
Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay

Hướng dẫn dùng cảm biến chuyển động PIR HC-SR501 với ESP32: nguyên lý, cách chỉnh module, đọc tín hiệu bằng ngắt và ghép relay làm đèn tự bật khi có người, tự tắt sau 60 giây.

Dải LED WS2812B với ESP32: điều khiển từng bóng bằng RMT không cần thư viện, tính dòng và chọn nguồn
Dải LED WS2812B với ESP32: điều khiển từng bóng bằng RMT không cần thư viện, tính dòng và chọn nguồn

Bên trong bóng WS2812B, tín hiệu một dây 24 bit GRB, cách tính dòng để chọn nguồn, nối dải LED với ESP32 và tạo tín hiệu bằng bộ RMT không cần thư viện, kèm ba hiệu ứng.


Bình luận
Đã thêm vào giỏ hàng