Tìm hiểu ESP32 OTA (Over The Air) – Lập trình qua mạng Internet

Bài hướng dẫn chủ đề ESP32 OTA này sẽ tập trung vào hướng dẫn cách lập trình ESP32 qua mạng, thông qua trình cập nhật Web OTA (OTA Web Updater) của Arduino IDE. Điều này có nghĩa là bạn có thể upload code mới vào mạch ESP32 từ Arduino IDE, mà không cần phải kết nối ESP32 với máy tính.

Có thể nói, lập trình OTA khá hữu ích khi chúng ta cần nạp code cho các mạch ESP32 không dễ dàng di chuyển và thuận tiện cho kết nối, chẳng hạn khi ESP32 nằm trong mạng cục bộ.

Tuy nhiên, ESP32 OTA cũng có một nhược điểm bạn cần biết là chúng ta phải thêm mã OTA trong mỗi đoạn code nạp vào ESP32.

Cách hoạt động của ESP32 OTA

  1. Chương trình code đầu tiên được nạp vào ESP32 qua cổng kết nối nối tiếp (USB) bình thường. Trong đoạn code này có sẵn một đoạn mã để khởi tạo OTA Web Updater.
  2. Chương trình code OTA Web Update tạo một Web Server, và bạn có thể truy cập vào chúng để upload một đoạn code mới thông qua trình duyệt Web.
  3. Sau đó, chúng ta triển khai các đoạn mã OTA trong tất cả các đoạn code cần nạp vào ESP32 qua mạng.
  4. Nếu bạn không khai báo mã OTA trong code, bạn không thể truy cập vào Web Server và upload code mới vào ESP32 qua mạng được nữa.

Giới thiệu về OTA Web Updater của ESP32

Đầu tiên, bạn lưu ý rằng cần phải cài đặt tiện ích ESP32 trong Arduino IDE nhé! Đây là điều kiện tiên quyết để Arduino IDE có thể làm việc với ESP32. Bạn có thể xem hướng dẫn tại: Cách lập trình ESP32 bằng Arduino IDE (Windows, Linux, Mac OS X)

Sau khi cài tiện ích ESP32, trong Arduino IDE sẽ tự động cài đặt thư viện Arduino OTA. Bạn có thể đi theo đường dẫn sau để truy cập: File > Examples >ArduinoOTAOTAWebUpdater.

Truy cập vào ESP32 OTA trên Arduino IDE

Sau đó, trên màn hình sẽ tải đoạn code như bên dưới (Lưu ý: trước khi nạp code ESP32 OTA này, bạn cần thay đổi thông tin xác thực WiFi của riêng bạn)

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

WebServer server(80);

/*
 * Login page
 */
const char* loginIndex = 
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<td>Username:</td>"
        "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";
 
/*
 * Server Index Page
 */
 
const char* serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
   "<input type='file' name='update'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('form').submit(function(e){"
  "e.preventDefault();"
  "var form = $('#upload_form')[0];"
  "var data = new FormData(form);"
  " $.ajax({"
  "url: '/update',"
  "type: 'POST',"
  "data: data,"
  "contentType: false,"
  "processData:false,"
  "xhr: function() {"
  "var xhr = new window.XMLHttpRequest();"
  "xhr.upload.addEventListener('progress', function(evt) {"
  "if (evt.lengthComputable) {"
  "var per = evt.loaded / evt.total;"
  "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')" 
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";

/*
 * setup function
 */
void setup(void) {
  Serial.begin(115200);

  // Connect to WiFi network
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
  server.begin();
}

void loop(void) {
  server.handleClient();
  delay(1);
}

Bạn có thể thay đổi thông tin mạng WiFi trong đoạn code ESP32 OTA trên qua đoạn sau, với SSID là tên WiFi và password là mật khẩu WiFi:

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

Đoạn code trên sẽ tạo một Web Server không đồng bộ, cho phép bạn upload code vào mà không cần phải tạo kết nối Serial giữa ESP32 và máy.

Sau khi upload code lên, bạn hãy mở Serial Monitor của Arduino IDE với tốc độ 115200, nhấn nút RST trên ESP32 để lấy địa chỉ IP của mạch:

Lấy địa chỉ IP của mạch ESP32 OTA

Bây giờ, bạn có thể upload code vào ESP32 thông qua mạng, bằng trình duyệt trên mạng cục bộ.

Để kiểm tra OTA Web Updater, bạn có thể ngắt kết nối giữa ESP32 khỏi máy tính và cấp nguồn cho ESP32 bằng sạc dự phòng. Sau đó, bạn nạp code vào thử nhé, theo hướng dẫn bên dưới:

Cách nạp code qua mạng thông qua ESP32 OTA

Đăng nhập vào Web Server

Bạn hãy mở trình duyệt trên máy tính và nhập địa chỉ IP của ESP32 vào thanh tìm kiếm, màn hình sẽ xuất hiện như hình dưới:

Truy cập vào địa chỉ IP của ESP32 để nạp code qua mạng
Truy cập vào địa chỉ IP của ESP32 để nạp code qua mạng

Tại đây, bạn nhập tên người dùng và mật khẩu. Đây là các thông tin đã có sẵn trong đoạn code lúc nãy, bạn có thể thay đổi chúng trong code để không bị trùng với người khác và tăng độ bảo mật cho dự án.

Lưu ý: Sau khi nhập đúng tên người dùng và mật khẩu, Web Server sẽ được chuyển hướng đến URL /serverIndex. Việc truy cập vào URL /serverIndex không cần nhập tên người dùng và mật khẩu nữa. Do đó, có thể xảy ra tình trạng ai đó bên ngoài biết URL và upload code mới vào ESP32, điều này đồng nghĩa với việc tên người dùng và mật khẩu có thể sẽ không bảo vệ tốt trang Web khỏi sự truy cập của người khác.

Một tab mới được mở trên URL /serverIndex. Tại đây, bạn có thể upload code mới vào mạch ESP32 của mình.

Khi upload code thông qua ESP32 OTA, bạn nên upload các tập tin có đuôi là .bin (IoTZone sẽ hướng dẫn bên dưới)

Chuẩn bị code mới

Đương nhiên là bạn cần phải có một đoạn code mới, để có thể demo việc nạp code qua mạng. Lưu ý rằng bạn cần thêm mã OTA trong đoạn code này, để chúng ta có thể ghi đè bất kỳ đoạn code nào bằng một đoạn code mới trong tương lai. Do đó, IoTZone khuyên bạn rằng nên chỉnh sửa lại đoạn code OTA Web Updater, để chúng là đoạn mã riêng và duy nhất của bạn.

Vì mình sẽ minh họa tính năng nạp code qua mạng ESP32 OTA là chính, nên lúc này chúng ta chỉ dùng một đoạn code đơn giản là nhấp nháy đèn LED. Bạn hãy copy đoạn code sau vào Arduino IDE của bạn:

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

//variabls to blink without delay:
const int led = 2;
unsigned long previousMillis = 0;        // Lưu trữ lần cuối LED được updated
const long interval = 1000;           // Khoảng thời gian nhấp nháy(milliseconds)
int ledState = LOW;             // ledState được dùng để cấu hình LED

WebServer server(80);

/*
 * Login page
 */

const char* loginIndex = 
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<td>Username:</td>"
        "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";
 
/*
 * Server Index Page
 */
 
const char* serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
   "<input type='file' name='update'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('form').submit(function(e){"
  "e.preventDefault();"
  "var form = $('#upload_form')[0];"
  "var data = new FormData(form);"
  " $.ajax({"
  "url: '/update',"
  "type: 'POST',"
  "data: data,"
  "contentType: false,"
  "processData:false,"
  "xhr: function() {"
  "var xhr = new window.XMLHttpRequest();"
  "xhr.upload.addEventListener('progress', function(evt) {"
  "if (evt.lengthComputable) {"
  "var per = evt.loaded / evt.total;"
  "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')" 
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";

/*
 * setup function
 */
void setup(void) {
  pinMode(led, OUTPUT);
  
  Serial.begin(115200);

  // Kết nối mạng WiFi
  WiFi.begin(ssid, password);
  Serial.println("");

  // Chờ kết nối
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
  server.begin();
}

void loop(void) {
  server.handleClient();
  delay(1);

  //Vòng lặp nhấp nháy không có delay
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    // Lưu lại thời gian lần cuối cùng nhấp nháy đèn LED
    previousMillis = currentMillis;

    // if the LED is off turn it on and vice-versa:
    ledState = not(ledState);

    // set the LED with the ledState of the variable:
    digitalWrite(led, ledState);
  }
}

Như đoạn code trên mô tả, chúng ta thêm đoạn code “Blink without delay” vào trong code OTA Web Updater, nên chúng ta có thể thực hiện các cập nhật khác sau này.

Dưới đây là hướng dẫn cách tạo tệp .bin:

Tạo tệp .bin trong Arduino IDE

Lưu đoạn code trên của bạn với tên là LED_Web_Updater.

Bạn truy cập vào Sketch > Export compiled Binary như hình:

Tạo tệp .bin trong Arduino IDE - Chuẩn bị cho dự án ESP32 OTA

Khi đó, trong thư mục sketch sẽ tạo ra một tập tin mới. Bạn truy cập vào Sketch > Show Sketch Folder và bạn sẽ thấy 2 tập tin, 1 file là .ino và 1 file là .bin.

Bạn nên upload file .bin lên bằng OTA Web Updater.

Upload file .bin bằng ESP32 OTA Web Updater

Upload code mới vào ESP32 qua mạng

Tại trình duyệt (trang Web Server mà bạn vào bằng địa chỉ IP ESP32 lúc nãy), bạn nhấn vào nút Choose File, chọn tập tin .bin đã tạo trước đó và nhấn Update.

Bạn hãy chờ vài giây để hoàn tất việc upload code mới vào ESP32, thông qua công nghệ ESP32 OTA.

Upload code mới qua mạng, thông qua ESP32 OTA
Upload code mới qua mạng, thông qua ESP32 OTA

Và kết quả là đèn LED trên ESP32 sẽ nhấp nháy:

Hoàn tất upload code mới qua mạng, thông qua ESP32 OTA

Lời kết

Có thể nói, ESP32 OTA là một tính năng khá hữu ích, giúp chúng ta có thể nạp code vào ESP32 dễ dàng mà không cần kết nối dây. Chúng sẽ tự động tạo một Web Server để hỗ trợ việc cập nhật mã mới ngay trên trình duyệt.

Ngoài chủ đề này, IoTZone cũng có thêm các bài hướng dẫn khác thú vị về ESP32, bạn có thể tìm hiểu thêm:

Trả lời

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 *