Viet Dev

Cover image for Firmware update over the air (FOTA)
Tuan for Unicloud Group

Posted on

Firmware update over the air (FOTA)

Firmware update over the air (FOTA)

Các phương pháp phát triển phần mềm và sản phẩm phổ biến hiện nay, thì
xuất bản kết quả từng giai đoạn thường mang lại hiệu quả cao, sản phẩm
có thể đến tay người dùng sớm, nhận được phản hồi sớm từ khách hàng, và
được điều chỉnh để hợp lý hơn. Chính việc phát hành sản phẩm sớm thường
sẽ ít tính năng và cần cập nhật thêm tính năng, nâng cao chất lượng sản
phẩm trong tương lai.

Cập nhật ứng dụng từ xa trên các phần mềm điện thoại, máy tính đã rất
phổ biến. Các phần mềm được cập nhật hàng tuần, tháng…​ để sửa lỗi, nâng
cấp tính năng.

Đối với các sản phẩm phần cứng cũng tương tự, chúng ta nên bổ sung các
tính năng cập nhật từ xa ngay từ giai đoạn phát triển sản phẩm. Ngoài
việc giúp nâng cấp các tính năng trong tương lai một cách dễ dàng, thì
vấn để sửa lỗi, nâng cấp hệ thống từ xa sẽ giúp tiết kiệm được rất nhiều
chi phí và nguồn lực.

Trong phần này, chúng ta sẽ tìm hiểu các phương pháp cập nhật từ xa cho
ESP8266, làm sao để nạp Firmware không dây cho module, làm sao để
ESP8266 có thể tự tải Firmware về, làm sao để ESP8266 có thể tự khởi
động 1 HTTP Server để có giao diện Web upload firmware lên chip.

Cập nhật firmware từ xa

Cập nhật firmware OTA (Over the Air) là tiến trình tải firmware mới vào
ESP8266 module thay vì sử dụng cổng Serial. Tính năng này thực sự rất
hữu dụng trong nhiều trường hợp giới hạn về kết nối vật lý đến ESP
Module.

OTA có thể thực hiện với:

  • Arduino IDE

  • Web Browser

  • HTTP Server

Sử dụng OTA với tùy chọn dùng Arduino IDE trong quá trình phát triển,
thử nghiệm, 2 tùy chọn còn lại phù hợp cho việc triển khai ứng dụng thực
tế, cung cấp tính năng cập nhật OTA thông qua web hay sử dụng HTTP
Server.

Trong tất cả các trường hợp, thì Firmware hỗ trợ OTA phải được nạp lần
đầu tiên qua cổng Serial, nếu mọi thứ hoạt động trơn tru, logic ứng dụng
OTA hoạt động đúng thì có thể thực hiện việc cập nhật firmware thông qua
OTA.

Sẽ không có đảm bảo an ninh đối với quá trình cập nhật OTA bị hack. Nó
phụ thuộc vào nhà phát triển đảm bảo việc cập nhật được phép từ nguồn
hợp pháp, đáng tin cậy. Khi cập nhật hoàn tất, ESP8266 sẽ khởi động lại
và thực thi code mới. Nhà phát triển phải đảm bảo ứng dụng thực trên
module phải được tắt và khởi động lại 1 cách an toàn. Nội dung bên dưới
cung cấp bổ sung các thông tin về an ninh, và an toàn cho tiến trình cập
nhật OTA.

Bảo mật

Khi ESP8266 được phép thực thi OTA, có nghĩa nó được kết nối mạng không
dây và có khả năng được cập nhập Sketch mới. Cho nên khả năng ESP8266 bị
tấn công sẽ nhiều hơn và bị nạp bởi mã thực thi khác là rất cao. Để giảm
khả năng bị tấn công cần xem xét bảo vệ cập nhật của bạn với một mật
khẩu, cổng sử dụng cố định khác biệt, v.v…

Kiểm tra những tính năng được cung cấp bởi thư viện ArduinoOTA thường
xuyên, có thể được nâng cấp khả năng bảo vệ an toàn:

void setPort(uint16_t port);
void setHostname(const char* hostname);
void setPassword(const char* password);
Enter fullscreen mode Exit fullscreen mode

Một số chức năng bảo vệ đã được xây dựng trong và không yêu cầu bất kỳ
mã hóa nào cho nhà phát triển. ArduinoOTA và espota.py sử dụng
Digest-MD5 để chứng thực việc tải firmware lên. Đơn giản là đảm bảo tính
toàn vẹn của firmware bằng việc tính MD5.

Hãy phân tích rủi ro cho riêng ứng dụng của bạn và tùy thuộc vào ứng
dụng mà quyết định những chức năng cũng như thư viện để thực hiện. Nếu
cần thiết, có thể xem xét việc thực hiện các phương thức bảo vệ khỏi bị
hack, ví dụ như cập nhật OTA chỉ cho tải lên chỉ theo lịch trình cụ thể,
kích hoạt OTA chỉ được người dùng nhấn nút chuyên dụng “Cập nhật”, v.v…

An toàn

Quá trình OTA tiêu tốn nguồn tài nguyên và băng thông của ESP8266 khi
tải lên. Sau đó, ESP8266 được khởi động lại và một Sketch mới được thực
thi. Cần phân tích và kiểm tra ảnh hưởng của quá trình này tới các chức
năng cũ và sketch mới của ESP module.

Nếu ESP được đặt ở xa và điều khiển một vài thiết bị, ta nên chú ý tới
hoạt động của thiết bị nếu thiết bị ngừng hoạt động đột xuất do quá
trình cập nhật. Do đó, ta cần phải xác định được trạng thái làm việc an
toàn của thiết bị trước quá trình cập nhật. Ví dụ, module được dùng để
điều khiển hệ thống tưới nước tự động trong vườn. Nếu trong quá trình
hoạt động mà hệ thống điều khiển bị tắt đột ngột và các van bị mở, thì
cả vườn sẽ bị ngập nước.

Các hàm sau đây được cung cấp bởi thư viện ArduinoOTA và được dùng để xử
lý ứng dụng trong quá trình cập nhật OTA hoặc để xử lý khi OTA gặp lỗi:

void onStart(OTA_CALLBACK(fn));
void onEnd(OTA_CALLBACK(fn));
void onProgress(OTA_CALLBACK_PROGRESS(fn));
void onError(OTA_CALLBACK_ERROR (fn));
Enter fullscreen mode Exit fullscreen mode

Yêu cầu căn bản

Bộ nhớ Flash phải có đủ dung lượng để lưu cả sketch cũ (đang vận hành
trên hệ thống) và sketch mới (cập nhật OTA).

Hệ thống File và EEPROM cũng cần dung lượng để lưu trữ.

Hàm ESP.getFreeSketchSpace(); được dùng để kiểm tra dung lượng
trống cho sketch mới.

Update process - memory view

  • Sketch mới sẽ được chứa trong dung lượng trống gĩưa sketch cũ và
    spiff.

  • Trong lần reboot tiếp theo thì “eboot” bootloader kiểm tra các câu
    lệnh.

  • Sketch mới sẽ được copy.

  • Sketch mới khởi động.

update

OTA sử dụng Arduino IDE

Thưc hiện OTA với Arduino IDE chỉ nên thưc hiện khi:

  • Sử dụng nạp firmware cho module ESP8266 thông qua WiFi mà không dùng
    cổng Serial

  • ESP8266 và máy tính chạy Arduino IDE sử dụng chung mạng WiFi nội bộ

  • ESP8266 đã kết nối thành công vào mạng WiFi

  • Nạp firmware đã hỗ trợ OTA thông qua cổng Serial trước đó

Trước khi bắt đầu, cần phải chắc chắn Arduino IDE đã được cài đặt phiên
bản mới nhất, bao gồm gói ESP8266 cho Arudino, và Python 2.7

Bước 1: nạp firmware hỗ trợ OTA thông qua cổng Serial

Mở sketch ví dụ mẫu BasicOTA.ino. Vào File > Examples > ArduinoOTA.

Mở sketch ví dụ mẫu<br>
OTA

Cung cấp chính xác SSID và mật khẩu mạng WiFi đang dùng để ESP8266 có
thể kết nối vào

**Cung cấp SSID và password**
const char* ssid = "..........";
const char* password = "..........";
Enter fullscreen mode Exit fullscreen mode
Tùy vào phiên bản và board ESP bạn sử dụng, bạn có thể thấy `Upload Using:` trong menu. Lựa chọn này sẽ không hoạt động và không ảnh hưởng tới lựa chọn của bạn. Chức năng này tương thích với các phiên bản OTA cũ và bị gỡ bỏ ở platform package version 2.2.0.

Upload sketch Ctrl+U Khi hoàn thành, mở Serial Monitor Ctrl+Shift+M
và kiểm tra module đã trup cập được WiFi chưa

Kiểm tra module ESP8266 đã truy cập được mạng WiFi nội bộ<br>
chưa

ESP module nên được reset sau khi Upload xong firmware
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

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

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH)
      type = "sketch";
    else // U_SPIFFS
      type = "filesystem";

    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.setPassword((const char *)"ota-pass");
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}
Enter fullscreen mode Exit fullscreen mode

Bước 2: Lựa chọn cổng nạp thông qua OTA

Khi module kết nối tới mạng WiFi thành công, sau vài giây, cổng
esp8266-ota sẽ xuất hiện trên Arduino IDE. Lúc này bạn hoàn toàn có thể
bỏ kết nối Serial từ board mạch đến máy tính. Arduino IDE có thể nạp
firmware mới thông qua WiFi. Chọn port với địa chỉ IP hiện trên cửa sổ
Serial Monitor ở bước trước.

Chọn cổng nạp từ<br>
Network

Nếu cổng OTA không hiện lên, bạn cần thoát Arduino IDE, và mở lại. Kiểm tra lại port OTA. Nếu vẫn tiếp tục không hiển thị cổng OTA, kiểm tra tưòng lửa của máy và các cài đặt trên router.

Bước 3: Sửa firmware mới và nạp lại thông qua WiFi

Sau khi đã chọn đúng cổng nạp OTA, bạn hoàn toàn có thể sửa lại firmware
mới và nạp thông qua WiFi, tuy nhiên cần lưu ý như sau:

  • Firmware mới phải đảm bảo kết nối đến WiFi không bị mất (ví dụ cấp
    sai mật khẩu)

  • Firmware mới phải có các hàm khởi tạo và xử lý OTA như Bước 1.

Nạp firmware thành công thông qua<br>
OTA

Sử dụng mật khẩu

Bảo vệ quá trình upload OTA với password là một quá trình khá đơn gỉản.
Những việc bạn cần làm là bổ sung đoạn mã nguồn:

ArduinoOTA.setPassword((const char *)"your-password");
Enter fullscreen mode Exit fullscreen mode

Sau đó upload lại sketch một lần nữa (dùng OTA). Sau khi biên dịch và
upload xong, cửa sổ sẽ hiện lên yêu cầu nhập password:

Password cho<br>
OTA

Nhập password, nếu đúng, kết quả là thông báo Authenticating…​OK và quá
trình nạp diễn ra bình thường.

Các lần nạp sau Arduino IDE sẽ nhớ mật khẩu và không hỏi lại, trừ khi
bạn thay đổi mật khẩu OTA, và các bước xác thực không thành công,
Arduino IDE sẽ hỏi lại bạn.

Cần lưu ý là password có thể dễ dàng thấy được, nếu IDE không được đóng
sau lần upload cuối cùng. Việc này có thể được thực hiện bằng cách cho
phép Show verbose ouput during: upload trong File > Preferences
upload lên module.

Những sự cố thường gặp

Nếu việc cập nhật OTA thất bại, bước đầu tiên bạn cần làm là kiểm tra
phần báo lỗi hiện trên cửa sổ Log của Arduino IDE. Nếu việc này không
giúp được bạn, hãy upload lại khi kiểm tra các thông tin của ESP hiện
trên serial port.

Các nguyên nhân phổ biến gây lỗi OTA như sau:

  • Không đủ dung lượng bộ nhớ trên chip (ví dụ như ESP01 với 512KB bộ
    nhớ flash không đủ cho OTA).

  • Khu vực dữ liệu cho SPIFFS quá nhiều, không còn đủ để chưa firmware,
    trong trường hợp bạn có 4MB Flash thì trường hợp này không xảy ra.

  • Khu vực bộ nhớ chưa firmware quá ít, tối thiểu là 512KB

  • Không reset module ESP sau lần upload đầu dùng Serial Port.

Cập nhật Firmware dùng Web Browser

Khi thực hiện cập nhật firmware dùng Web Browser, ESP8266 sẽ khởi động 1
HTTP Server, với 1 form upload. Khi truy cập đúng địa chỉ của nó, bạn sẽ
được cung cấp 1 giao diện để chọn file binary, và upload lên Chip. Việc
này hữu dụng khi không dùng Arudino IDE cho việc cập nhật, sử dụng luôn
trình duyệt sẵn có. Hoặc tích hợp vào 1 ứng dụng mà bạn có thể muốn cập
nhật nó trong tương.

Cập nhật với web browser được thực hiện bằng thư viện
ESP8266HTTPUpdateServer cùng với 2 thư viện khác ESP8266WebServer
ESP8266mDNS cho việc nhận diện ESP8266 trong mạng nội bộ.

Thực hiện

Mở ví dụ: File > Examples > ESP8266HTTPUpdateServer > WebUpdater

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>

const char* host = "esp8266-webupdate";
const char* ssid = "...";
const char* password = "...";

ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

void setup(void){

  Serial.begin(115200);
  Serial.println();
  Serial.println("Booting Sketch...");
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while(WiFi.waitForConnectResult() != WL_CONNECTED){
    WiFi.begin(ssid, password);
    Serial.println("WiFi failed, retrying.");
  }

  MDNS.begin(host);
  httpUpdater.setup(&httpServer);
  httpServer.begin();

  MDNS.addService("http", "tcp", 80);
  Serial.printf("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}

void loop(void){
  httpServer.handleClient();
}
Enter fullscreen mode Exit fullscreen mode

Cung cấp đúng SSID và mật khẩu mạng WiFi máy tính bạn đang dùng, nạp
Firmware WebUpdater vào board NodeMCU

Lưu ý là máy tính phải sử dụng mạng WiFi cùng với board NodeMCU

Khi bạn không thể truy cập vào module ESP8266 theo công Serial, thì để
nhận diện được địa chỉ IP của module trong mạng LAN, bạn cần chạy dịch
vụ mDNS trên máy tính. Dịch vụ này sẵn có trong MacOS, tuy nhiên, với
Linux thì bạn cần cài đặt Avahi: avahi.org/, còn Windows thì
Bonjour: support.apple.com/downloads/bonjour_for_windows

Với dịch vụ mDNS chạy trên máy tính, bạn dễ dàng truy cập vào ESP8266
theo đường dẫn esp8266-webupdate.local/update

Giao diện Web cập nhật<br>
firmware

Bằng cách chọn file và nhấn cập nhật, ESP8266 sẽ tiến hành cập nhật
firmware mới do bạn gởi lên. File này có thể được xuất ra bằng cách
Sketch > Export compiled Binary, và file .bin sẽ nằm trong thư mục của
Sketch

Xuất file<br>
Binary

Khi đã nhập [esp8266-webupdate.local/update](#) mà không thưc hiện được, hãy thay esp8266-webupdate với địa chỉ IP của module (bạn có thể xem trong modem/router, hay Serial Terminal). Ví dụ, nếu IP của module là `192.168.1.100` thì URL phải là [192.168.1.100/update](#). Phương pháp này hữu hiệu trong trường hợp host software cài đặt ở bước 1 không hoạt động.

Nếu các bước diễn ra thành công tốt đẹp, thì trên trình duyệt và cửa sổ
Serial Terminal (nếu mở) như hình

Thực hiện thành công cập nhật Firmware sử dụng<br>
WebUpdater

Bảo mật

Nếu bổ sung WebUpdater vào sản phẩm của mình, dĩ nhiên bạn sẽ không muốn
người khác tự do đưa vào thiết bị 1 firmware khác. Hãy sử dụng hàm
httpUpdater.setup(&httpServer, update_path, update_username, update_password);
với các thông số username, password mà bắt buộc bạn phải nhập đúng mới
được phép upload firmware mới.

Mở ví dụ :
File > Examples > ESP8266HTTPUpdateServer > SecureWebUpdater để xem
chi tiết

HTTP Server

Với 2 phương pháp trước, bạn dễ dàng cập nhật Firmware thông qua mạng
WiFi nội bộ. Tuy nhiên, khi triển khai ứng dụng thực tế, chúng ta sẽ cần
cập nhật Firmware từ xa thông qua Internet, và cần 1 Server để lưu trữ
firmware, quản lý các phiên bản.

Một kịch bản phổ thông thường được làm nhất đó là:

  • Khi ESP8266 khởi động (khoảng sau 1 khoảng thời gian - ví dụ như 1
    ngày), nó sẽ kết nối đến Server, cung cấp thông tin phiên bản hiện
    có của nó

  • Server khi nhận thấy phiên bản hiện tại cần phải được nâng cấp, nó
    sẽ trả về firmware mới

  • Nếu phiên bản hiện tại của ESP8266 không cần phải cập nhật, nó sẽ
    trả về mã 304.

Để thực hiện được điều này, chúng ta cần thực hiện trên cả ESP8266 và
trên Server side. Thử nghiệm trong mục này, chúng ta sẽ dùng Node.js làm
server. Bạn hoàn toàn có thể thực thi đoạn code Server này và gán cho nó
domain để có thể truy cập từ bất kỳ đâu.

ESP8266 ESPhttpUpdate

Bằng cách thực thi
ESPhttpUpdate.update("your-domain.com", 8000, "/fimrware.bin");,
ESP8266 sẽ tự động kết nối tới server ở địa chỉ
your-domain.com:8000/fimrware.bin để tải phiên bản firmware mới về.
Mã HTTP Status Code:

  • (Mã) 200: Nếu tồn tại firmware mới, và nội dung file sẽ được gởi kèm
    sau đó

  • (Mã) 304: Thông báo là không có bản update mới.

Đoạn mã có thể dễ dàng tìm thấy ở
File > Examples > ESPhttpUpdate > httpUpdate

Bạn cần cung cấp SSID, mật khẩu WiFi chính xác. Nạp Code vào board
NodeMCU

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>

const char* ssid = "...";
const char* password = "...";
const char *currentVersion = "1.0"; 
const char *serverUrl = "http://192.168.0.106:8000/firmware.bin"; 
void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println();
  Serial.print("ESP8266 http update, current version: ");
  Serial.println(currentVersion);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  t_httpUpdate_return ret = ESPhttpUpdate.update(serverUrl, currentVersion);
  switch (ret) {
    case HTTP_UPDATE_FAILED:
      Serial.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
      break;

    case HTTP_UPDATE_NO_UPDATES:
      Serial.println("HTTP_UPDATE_NO_UPDATES");
      break;
    case HTTP_UPDATE_OK:
      Serial.println("HTTP_UPDATE_OK");
      break;
  }

}

void loop() {
}
Enter fullscreen mode Exit fullscreen mode
  • Phiên bản firmware của bạn, giả sử bạn thay đổi là 2.0 và đặt lên
    server, sau đó bạn đổi lại 1.0 và nạp vào board

  • Đường dẫn đến firmware của bạn, là địa chỉ web, ip, hay domain

Node.js OTA Server

Khi ESP8266 kết nối tới Web Server, thì nó sẽ cung cấp các thông tin
Header để Server căn cứ vào đó đánh giá firmware có cần phải cập nhật
hay không. Ví dụ về các header

[HTTP_USER_AGENT] => ESP8266-http-Update
[HTTP_X_ESP8266_STA_MAC] => 18:FE:AA:AA:AA:AA
[HTTP_X_ESP8266_AP_MAC] => 1A:FE:AA:AA:AA:AA
[HTTP_X_ESP8266_FREE_SPACE] => 671744
[HTTP_X_ESP8266_SKETCH_SIZE] => 373940
[HTTP_X_ESP8266_SKETCH_MD5] => a56f8ef78a0bebd812f62067daf1408a
[HTTP_X_ESP8266_CHIP_SIZE] => 4194304
[HTTP_X_ESP8266_SDK_VERSION] => 1.3.0
[HTTP_X_ESP8266_VERSION] => 1.0
Enter fullscreen mode Exit fullscreen mode

Dựa trên kiến thức phần server-nodejs, chúng ta xây dựng 1 OTA Server
dùng Node.js như sau

server.js

var fs = require('fs');
var url = require('url');
var http = require('http');
var querystring = require('querystring');
var crypto = require('crypto');
// function gửi yêu cầu(response) từ phía server hoặc nhận yêu cầu (request) của client gửi lên
function requestHandler(request, response) {

    // Giả sử địa chỉ yêu cầu firmware http://192.168.1.7:8000/firmware.bin
    var uriData = url.parse(request.url);
    var pathname = uriData.pathname;          // /firmware.bin

    if (pathname == '/firmware.bin') {
        var ver = request.headers['x-esp8266-version'];
        console.log('Client request update, version ', ver);
        if(ver == '1.0') {
            console.log('Send firmware 2.0 to client');
            fs.readFile('./esp8266-firmware-2.0.bin', function(error, content) {
                response.writeHead(200, {
                    'Content-Type': 'binary/octet-stream',
                    'Content-Length': Buffer.byteLength(content),
                    'x-MD5': crypto.createHash('md5').update(content).digest("hex")
                });
                response.end(content);
            });
        } else {
            response.statusCode = 304;
            response.end();
        }
    }
}
var server = http.createServer(requestHandler);
server.listen(8000);
console.log('Server listening on port 8000');
Enter fullscreen mode Exit fullscreen mode

Thực thi node server.js để khởi động Server

Khi bạn làm việc với các ngôn ngữ lập trình khác, luôn phải đảm bảo khi
gởi về client cần có đầy đủ thông tin header Content-Lengthx-MD5

Bạn cần file esp8266-firware-2.0.bin ở mục
??? biên dịch với currentVersion = "2.0" đặt
cùng thư mục với file server.js

Nếu bạn thực hiện đầy đủ các bước như trên, kết quả thực thi sẽ như hình

Kết quả thực hiện OTA sử dụng HTTP<br>
Server

Latest comments (0)