0

Lập trình ESP32 OTA (Over-The-Air) thông qua Web browser

ESP32-Lap-trinh-OTA-qua-web-server

Một tính năng tuyệt vời của các vi điều khiển có hỗ trợ WiFi như ESP32 là khả năng cập nhật phần mềm (Firmware) không dây – Lập trình ESP32 Over-The-Air thông qua Web browser. Thường được gọi là lập trình OTA.

ESP32 Over-The-Air là gì?

Lập trình OTA cho phép cập nhật một chương trình mới vào ESP32 bằng WiFi thay vì yêu cầu người dùng kết nối ESP32 với máy tính qua USB để thực hiện.

Chức năng OTA cực kỳ hữu ích trong trường hợp bạn không có quyền truy cập vật lý vào ESP32. Nó còn giúp giảm thời gian cho việc cập nhật từng thiết bị ESP32 tại thời điểm bảo trì.

Một tính năng quan trọng của OTA là thiết bị trung tâm có thể gửi bản cập nhật tới nhiều ESP32 trong cùng một mạng.

Nhược điểm duy nhất là bạn phải thêm một đoạn mã nguồn bổ sung cho OTA ở mỗi bản Firmware được nạp vào ESP32, để bạn có thể sử dụng OTA trong bản cập nhật tiếp theo.

Cách thực hiện OTA trên ESP32

Chúng ta có 2 cách thực hiện:

  • Basic OTA – Cập nhật Over-The-Air thông qua Arduino IDE.
  • Web Updater OTA – Cập nhật Over-The-Air thông qua Web browser.

Mỗi loại trên đều có ưu điểm riêng nên bạn có thể thực hiện loại nào tùy vào yêu cầu dự án của bạn.

Bài viết này sẽ chỉ hướng dẫn các bạn cách cập nhật Over-the-air thông qua Web browser.

3 Bước để lập trình OTA cho ESP32

  1. Nạp Firmware có chứa mã nguồn bổ sung OTA. Đây là bước bắt buộc để bạn có thể thực hiện nạp các bản cập nhật lần sau.
  2. Firmware có chứa mã nguồn bổ sung OTA tạo ra một Web Server ở chế độ STA (Trạm), nó có thể được truy cập thông qua trình duyệt web. Khi bạn đăng nhập vào Web Server, bạn có thể cập nhật một bản Firmware mới.
  3. Nạp các bản Firmware mới vào ESP32 bằng cách BuildUpload file .bin (đã được biên dịch) thông qua Web Server.

ESP32-Nap-code-tu-xa-qua-web-server-nhu-the-nao

Bước 1: Nạp firmware có chứa mã nguồn bổ sung OTA

Firmware từ nhà sản xuất có sẵn trong ESP32 không có khả năng nâng cấp OTA, vì vậy, đầu tiên bạn phải nạp Firmware OTA vào ESP32 theo cách thông thường.

Đây là bước bắt buộc để bạn có thể nạp các bản Firmware cập nhật tiếp theo qua mạng.

ESP32 add-on cài cho Arduino IDE đã cõ sẵn thư viện OTA với ví dụ OTAWebUpdater. Bạn có thể tham khảo nó bằng cách chọn File > Examples > ArduinoOTA > OTAWebUpdater hoặc thông qua Github.

Giao diện người dùng của OTAWebUpdater mặc định này rất xấu, vì vậy, chúng tôi đã sửa đổi lại để trông ngầu hơn và dễ dùng hơn :)).

Bắt đầu, hãy cắm ESP32 vào máy tính của bạn và nạp mã nguồn dưới đây. Nhưng trước tiên, bạn cần sửa thông tin mạng WiFi của mình để ESP32 có thể kết nối với nó.

const char* ssid = "xxx";
const char* password = "xxxx";
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";

WebServer server(80);

/* Style */
String style =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";
/* Login page */
String loginIndex = 
"<form name=loginForm>"
"<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style;
 
/* Server Index Page */
String 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' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'>   Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = '   '+ fileName[fileName.length-1];"
"};"
"$('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) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style;

/* 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ước 2: Truy cập Web Server

Mã nguồn  OTA Web Updater tạo ra một Web Server ở chế độ STA, nó có thể được truy cập thông qua trình duyệt web và được sử dụng để tải các bản Firmware mới vào ESP32.

Để truy cập Web Server, hãy mở Serial Mornitor với tốc độ baud là 115200, nhấn nút EN trên ESP32 và sau khi kêt nối WiFi, nó sẽ xuất hiện địa chỉ IP của ESP32.

Sao-chep-dia-chi-IP-cua-ESP32

Tiếp theo, mở trình duyệt và nhập địa chỉ IP của ESP32. ESP32 sẽ phản hồi một trang web yêu cầu thông tin đăng nhập.

Truy-cap-Web-server

Nhập User ID và the Password:

User ID: admin

Password: admin

Nếu bạn muối thay đổi thông tin đăng nhập, hãy sửa đoạn code dưới dây trong mã nguồn OTA Web Updater.

"if(form.userid.value=='admin' && form.pwd.value=='admin')"

Khi bạn đăng nhập vào hệ thống, bạn sẽ được chuyển hướng đến trang /serverIndex. Trang này cho phép bạn nạp một bản Firmware mới vào ESP32 thông qua mạng.

Bản Firmware mới phải ở định dạng .bin. Chúng ta sẽ xem cách để tạo ra file .bin ở đoạn sau của bài viết.

Chú ý

Trang đăng nhập không có chức năng bảo vệ trang /serverIndex. Vậy nên người dùng có thể xâm nhập vào hệ thống mà không cần đăng nhập.

Bước 3: Nạp firmware mới

Bây giờ, hãy nạp một bản Firmware mới thông qua Web browser.

Hãy nhớ! bạn cần thêm mã nguồn OTA Web Updater trong mỗi bản Firmware mà bạn nạp vào. Nếu không, ESP32 sẽ mất khả năng OTA và sẽ không thể nạp bản Firmware mới qua mạng. Để tránh điều này, chúng tôi khuyên bạn nên viết chương trình mới từ mã nguồn ở trên.

Ví dụ, chúng tôi thêm mã nguồn Blink trong mã nguồn OTA Web Updater.

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

const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";

//variabls for blinking an LED with Millis
const int led = 2; // ESP32 Pin to which onboard LED is connected
unsigned long previousMillis = 0;  // will store last time LED was updated
const long interval = 1000;  // interval at which to blink (milliseconds)
int ledState = LOW;  // ledState used to set the LED
WebServer server(80);

/* Style */
String style =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";

/* Login page */
String loginIndex = 
"<form name=loginForm>"
"<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style;
 
/* Server Index Page */
String 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' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'>   Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = '   '+ fileName[fileName.length-1];"
"};"
"$('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) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style;

/* setup function */
void setup(void) {

  pinMode(led,  OUTPUT);
  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);

  //loop to blink without delay
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the 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);
  }
}

Chú ý

Trong chương trình trên, chúng ta không sử dụng hàm delay() để nhấp nháy LED, vì ESP32 sẽ tạm dừng chương trình trong hàm delay(). Nếu trang Web Server được truy cập trong khi ESP32 bị tạm dừng thì chương trình của bạn sẽ bỏ lỡ truy cập đó.

Tạo file .bin trên Arduino IDE

Để nạp một bản Firmware mới vào ESP32, Firmware đó phải được biên dịch thành file .bin

Để xuất file .bin  chọn Sketch > Export compiled Binary

Xuat-file-binary-tu-Arduino-IDE

Sau khi biên dịch thành công, file .bin sẽ xuất hiện tại thư mục  Sketch > Show Sketch Folder.

Mo-thu-muc-du-an-tren-Arduino-IDE

Nạp firmware mới vào ESP32 – Over-The-Air

Sau khi file .binđược tạo, bạn đã sẵn sàng để nạp bản Firmware mới vào ESP32 qua mạng.

Mở trang /serverIndex trong trình duyệt của bạn. Bấm vào Choose File… Chọn file .bin đã xuất, rồi bấm Update.

Truy-cap-trang-web-server-va-update-ota

Bản Firmware sẽ được nạp trong vài giây.

Upload-file-binary

Kết quả lập trình ESP32 OTA

ESP32-Nap-code-tu-xa-OTA-qua-web-server

thanhbinh89

Trả lời

Email của bạn sẽ không được hiển thị công khai.