Giao tiếp giữa 2 ESP32 qua HTTP trong mạng cục bộ

Lập trình ESP32 giao tiếp với ESP32 khác qua HTTP

Trong bài này chúng ta sẽ làm một việc thú vị hơn các bài Webserver trước: giao tiếp giữa 2 ESP32 qua HTTP, cho 2 board tự nói chuyện với nhau qua WiFi, không cần con người bấm nút trên trình duyệt nữa. Board B sẽ đọc nút bấm của chính nó để điều khiển LED trên Board A từ xa, đồng thời đọc ngược lại trạng thái nút bấm của Board A để phản chiếu lên LED của mình.

Đây là một bài thực hành trong Serie Lập trình ESP32 từ A tới Z.

Tại sao cần giao tiếp giữa 2 ESP32?

Ở Bài 1, Bài 2 và Bài 3 của serie Networking, người điều khiển luôn là con người — mở trình duyệt, bấm nút, ESP32 phản hồi lại. Nhưng trong các hệ thống IoT thực tế, phần lớn thiết bị lại nói chuyện với nhau chứ không phải với người: cảm biến gửi dữ liệu về gateway, bộ điều khiển trung tâm ra lệnh cho từng thiết bị chấp hành…

Bài này là bước đệm để hiểu nguyên lý đó, theo mô hình client – server quen thuộc: 1 ESP32 đóng vai server (đứng yên, mở cổng chờ có ai gọi tới), 1 ESP32 khác đóng vai client (chủ động gọi sang server khi cần). Giống như tổng đài điện thoại luôn túc trực chờ cuộc gọi đến (server), còn người bấm số gọi đi mới là bên chủ động (client) vậy.

Mô hình giao tiếp trong bài

  • Board A (Server): chạy webserver ở cổng 80 y hệt Bài 1, mở 3 route: /led/on, /led/off, /status.
  • Board B (Client): dùng thư viện HTTPClient gửi request GET sang Board A — vừa gửi lệnh điều khiển, vừa hỏi thăm trạng thái.
  • Cả 2 board cùng kết nối vào chung 1 mạng WiFi ở chế độ Station (giống Bài 1, khác Bài 2 dùng AP mode) — bắt buộc phải chung mạng thì 2 board mới nhìn thấy IP của nhau.

Mô hình giao tiếp giữa 2 ESP32 qua HTTP: Board B gửi GET /led/on, /led/off, /status tới Board A, Board A trả về LED=ON;BUTTON=RELEASED

Chuẩn bị phần cứng

Mỗi board cần:
– 1 LED nối chân GPIO26 (qua điện trở hạn dòng, giống các bài Webserver trước)
– 1 nút bấm nối chân GPIO4 xuống GND (dùng điện trở kéo lên nội INPUT_PULLUP, không cần điện trở ngoài)

Sơ đồ đấu nối 2 board ESP32 giao tiếp HTTP: mỗi board có LED GPIO26 qua điện trở 220Ω và nút nhấn GPIO4 xuống GND

Lập trình Board A — đóng vai Webserver

Kết nối WiFi và khởi tạo webserver

const int led = 26;
const int button = 4;

const char* ssid = "Ten_Wifi_Cua_Ban";
const char* password = "Mat_Khau_Wifi";

WiFiServer webServer(80);
String ledStatus = "OFF";

void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
  pinMode(button, INPUT_PULLUP);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.print("IP address cua Board A (Server): ");
  Serial.println(WiFi.localIP());

  webServer.begin();
}

Phần này giống hệt Bài 1, chỉ thêm pinMode(button, INPUT_PULLUP) cho nút bấm cục bộ. Dòng in ra WiFi.localIP() cực kỳ quan trọng — đây là địa chỉ mà Board B sẽ cần để gọi sang, nhớ ghi lại nhé.

Xử lý 3 route và trả lời trạng thái

if (header.indexOf("GET /led/on") >= 0) {
  digitalWrite(led, HIGH);
  ledStatus = "ON";
} else if (header.indexOf("GET /led/off") >= 0) {
  digitalWrite(led, LOW);
  ledStatus = "OFF";
}
// route /status khong lam gi them, chi tra ve trang thai hien tai

String buttonStatus = (digitalRead(button) == LOW) ? "PRESSED" : "RELEASED";

webClient.println("HTTP/1.1 200 OK");
webClient.println("Content-type:text/plain");
webClient.println("Connection: close");
webClient.println();
webClient.println("LED=" + ledStatus + ";BUTTON=" + buttonStatus);

Khác với Bài 1/Bài 2 trả về cả 1 trang HTML để hiển thị trên trình duyệt, ở đây Board A trả về plain text dạng LED=ON;BUTTON=RELEASED — vì người đọc response giờ là một con ESP32 khác chứ không phải mắt người, không cần giao diện đẹp, chỉ cần dễ tách chuỗi bằng indexOf().

Lưu ý: Dù request là /led/on, /led/off hay /status, Board A đều trả về cùng 1 định dạng response chứa đầy đủ cả trạng thái LED lẫn trạng thái nút bấm. Nhờ vậy Board B gọi route nào cũng luôn biết được toàn bộ tình trạng của Board A, không cần gọi thêm lần nữa.

Code đầy đủ Board A

#include <Arduino.h>

#include <WiFi.h>

// Board A dong vai tro Webserver - vua nhan lenh dieu khien Led tu Board B gui toi,
// vua bao cao trang thai nut bam cuc bo de Board B doc ve.

const int led = 26;
const int button = 4; // nut bam cuc bo, mot chan noi GND, dung INPUT_PULLUP

const char* ssid = "Ten_Wifi_Cua_Ban";
const char* password = "Mat_Khau_Wifi";

WiFiServer webServer(80);

String ledStatus = "OFF";
String header;

unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000;

void setup() {
  Serial.begin(115200);

  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
  pinMode(button, INPUT_PULLUP);

  Serial.print("Connecting to wifi ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.print("IP address cua Board A (Server): ");
  Serial.println(WiFi.localIP());

  webServer.begin();
}

void loop() {
  WiFiClient webClient = webServer.available();

  if (webClient)
  {
    currentTime = millis();
    previousTime = currentTime;
    Serial.println("New client ket noi toi Board A");
    String currentLine = "";

    while (webClient.connected() && currentTime - previousTime <= timeoutTime)
    {
      currentTime = millis();
      if (webClient.available())
      {
        char c = webClient.read();
        Serial.write(c);
        header += c;
        if (c == '\n')
        {
          if (currentLine.length() == 0)
          {
            if (header.indexOf("GET /led/on") >= 0)
            {
              digitalWrite(led, HIGH);
              ledStatus = "ON";
              Serial.println("Board A: bat led theo lenh tu Board B");
            }
            else if (header.indexOf("GET /led/off") >= 0)
            {
              digitalWrite(led, LOW);
              ledStatus = "OFF";
              Serial.println("Board A: tat led theo lenh tu Board B");
            }

            String buttonStatus = (digitalRead(button) == LOW) ? "PRESSED" : "RELEASED";

            webClient.println("HTTP/1.1 200 OK");
            webClient.println("Content-type:text/plain");
            webClient.println("Connection: close");
            webClient.println();
            webClient.println("LED=" + ledStatus + ";BUTTON=" + buttonStatus);
            break;
          }
          else
          {
            currentLine = "";
          }
        }
        else if (c != '\r')
        {
          currentLine += c;
        }
      }
    }
    header = "";
    webClient.stop();
    Serial.println("Client ngat ket noi.");
    Serial.println("");
  }
}

Code đầy đủ mình để trên GitHub, các bạn xem tại 03-WebServer/06_Two_Boards/BoardA_Server nhé.

Lập trình Board B — đóng vai Client

Kết nối WiFi và cấu hình IP của Board A

const char* boardA_IP = "192.168.1.100";

Lưu ý: boardA_IP phải là IP thật mà Board A vừa in ra Serial Monitor sau khi kết nối WiFi thành công ở bước trên, không phải để nguyên giá trị mẫu này. Nếu router nhà bạn không đặt DHCP reservation (giữ cố định IP cho từng thiết bị), IP này có thể đổi mỗi lần Board A khởi động lại — lúc đó Board B sẽ gọi vào một địa chỉ trống, không có gì trả lời.

Đọc nút bấm, gửi lệnh sang Board A

int buttonState = digitalRead(button);
if (buttonState == LOW && lastButtonState == HIGH) {
  ledOnBoardA = !ledOnBoardA;
  String path = ledOnBoardA ? "/led/on" : "/led/off";
  String url = "http://" + String(boardA_IP) + path;
  httpGETRequest(url.c_str());
  delay(200);
}
lastButtonState = buttonState;

Mỗi lần Board B phát hiện nút vừa được nhấn (chuyển từ HIGH sang LOW), nó đảo biến ledOnBoardA rồi gọi sang đúng route /led/on hoặc /led/off của Board A — y hệt việc bạn tự tay gõ địa chỉ đó lên trình duyệt, chỉ khác là do code tự làm.

Định kỳ đọc trạng thái Board A, phản chiếu lên LED của Board B

if (millis() - lastPollTime >= pollInterval) {
  lastPollTime = millis();
  String status = httpGETRequest(("http://" + String(boardA_IP) + "/status").c_str());

  if (status.indexOf("BUTTON=PRESSED") >= 0) {
    digitalWrite(led, HIGH);
  } else if (status.indexOf("BUTTON=RELEASED") >= 0) {
    digitalWrite(led, LOW);
  }
}

Cứ mỗi giây, Board B chủ động gọi /status sang Board A, đọc xem nút bấm bên đó đang ở trạng thái nào rồi bật/tắt LED của chính mình theo — đây gọi là polling (hỏi thăm định kỳ), cách đơn giản nhất để 1 thiết bị theo dõi trạng thái của thiết bị khác qua HTTP.

Lưu ý: Vì dùng polling mỗi 1 giây, LED của Board B sẽ phản ứng chậm hơn thực tế tối đa 1 giây so với lúc bạn nhấn nút thật trên Board A — đây không phải lỗi, mà là đánh đổi giữa độ đơn giản của HTTP polling với độ trễ. Muốn phản hồi tức thời hơn, các bạn xem thêm hướng WebSocket ở Bài 3 của serie.

Code đầy đủ Board B

#include <Arduino.h>

#include <WiFi.h>
#include <HTTPClient.h>

// Board B dong vai tro Client - doc nut bam cuc bo, gui lenh dieu khien Led
// sang Board A qua HTTP, dong thoi dinh ky doc trang thai nut bam cua Board A
// ve de "phan chieu" len led cua chinh minh.

const int led = 26;
const int button = 4;

const char* ssid = "Ten_Wifi_Cua_Ban";
const char* password = "Mat_Khau_Wifi";

const char* boardA_IP = "192.168.1.100";

bool ledOnBoardA = false;
int lastButtonState = HIGH;
unsigned long lastPollTime = 0;
const unsigned long pollInterval = 1000;

String httpGETRequest(const char* Url);

void setup() {
  Serial.begin(115200);

  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
  pinMode(button, INPUT_PULLUP);

  Serial.print("Connecting to wifi ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.print("IP address cua Board B (Client): ");
  Serial.println(WiFi.localIP());
}

void loop() {
  int buttonState = digitalRead(button);
  if (buttonState == LOW && lastButtonState == HIGH)
  {
    ledOnBoardA = !ledOnBoardA;
    String path = ledOnBoardA ? "/led/on" : "/led/off";
    String url = "http://" + String(boardA_IP) + path;
    Serial.println("Nut bam! Goi sang Board A: " + url);
    httpGETRequest(url.c_str());
    delay(200);
  }
  lastButtonState = buttonState;

  if (millis() - lastPollTime >= pollInterval)
  {
    lastPollTime = millis();
    String url = "http://" + String(boardA_IP) + "/status";
    String status = httpGETRequest(url.c_str());
    Serial.println("Trang thai Board A: " + status);

    if (status.indexOf("BUTTON=PRESSED") >= 0)
    {
      digitalWrite(led, HIGH);
    }
    else if (status.indexOf("BUTTON=RELEASED") >= 0)
    {
      digitalWrite(led, LOW);
    }
  }
}

String httpGETRequest(const char* Url)
{
  HTTPClient http;
  http.begin(Url);
  int responseCode = http.GET();
  String responseBody = "";
  if (responseCode > 0)
  {
    Serial.print("responseCode: ");
    Serial.println(responseCode);
    responseBody = http.getString();
  }
  else
  {
    Serial.print("Error Code: ");
    Serial.println(responseCode);
  }
  http.end();
  return responseBody;
}

Code đầy đủ mình để trên GitHub, các bạn xem tại 03-WebServer/06_Two_Boards/BoardB_Client nhé.

Kết quả

Nạp code cho cả 2 board (nhớ đổi đúng ssid/password WiFi và boardA_IP trong code Board B). Mở 2 cửa sổ Serial Monitor cùng lúc, mỗi cửa sổ theo dõi 1 board qua cổng COM riêng.

Log Serial Monitor của Board A (mình lược bớt các request /status lặp lại mỗi giây, chỉ giữ 2 lệnh bật/tắt LED từ Board B):

Log Serial Monitor Board A ESP32 webserver nhận lệnh /led/on và /led/off từ Board B

Log Serial Monitor của Board B: nhấn nút 2 lần để bật rồi tắt LED bên Board A, sau đó giữ nút bên Board A khoảng 2 giây thì Board B đọc được BUTTON=PRESSED:

Log Serial Monitor Board B ESP32 gửi lệnh bật tắt LED và đọc trạng thái nút của Board A qua HTTP

  • Nhấn nút trên Board B → LED trên Board A bật/tắt gần như ngay lập tức.
  • Nhấn nút trên Board A → LED trên Board B bật/tắt theo, chậm hơn tối đa 1 giây (do polling).
  • Theo dõi Serial Monitor của cả 2 board, các bạn sẽ thấy log request/response qua lại giữa 2 board, đúng không nào.

Kết

Ghép 2 ESP32 giao tiếp trực tiếp qua HTTP là nền tảng để xây các hệ thống nhiều thiết bị nói chuyện với nhau mà không cần con người can thiệp — sau này khi số thiết bị tăng lên, mô hình gọi trực tiếp point-to-point kiểu này sẽ cồng kềnh, lúc đó các bạn có thể chuyển sang MQTT — nơi mọi thiết bị chỉ cần nói chuyện qua 1 broker trung gian thay vì nhớ IP của nhau.

Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè cùng học. Có thắc mắc gì cứ để lại bình luận, mình sẽ giải đáp nhé!

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *