Awesome
BLEOTA description
Library inspired by https://components.espressif.com/components/espressif/ble_ota that implement the firmware and SPIFFS/LittleFS OTA via BLE and writes it to flash, sector by sector, until the upgrade is complete.
1. Services definition
The library add two services:
DIS Service
: Displays software and hardware version information, manufacturer, model and serial numberOTA Service
: It is used for OTA upgrade
Service | UUID |
---|---|
BLE_OTA_SERVICE | 00008018-0000-1000-8000-00805f9b34fb |
DIS_SERVICE_UUID | 180A |
2. DIS Service Characteristics definition
The DIS Service
can contains 5 read only characteristics that can be setted up before BLEOTA.init
, if none of these is assigned
the service is not added.
Characteristics | UUID | Prop | description | Method |
---|---|---|---|---|
DIS_MODEL_CHAR | 0x2A24 | Read | Model Number String | setModel |
DIS_SERIAL_N_CHAR | 0x2A25 | Read | Serial Number String | setSerialNumber |
DIS_FW_VER_CHAR | 0x2A26 | Read | Firmware Revision String | setFWVersion |
DIS_HW_VERSION_CHAR | 0x2A27 | Read | Hardware Revision String | setHWVersion |
DIS_MNF_CHAR | 0x2A29 | Read | Manufacturer Name String | setManufactuer |
3. OTA Service Characteristics definition
The OTA Service
can contains 2 characteristics to perform the OTA process.
Characteristics | UUID | Prop | description |
---|---|---|---|
RECV_FW_CHAR | 00008020-0000-1000-8000-00805f9b34fb | Write, WriteNR, Notify | Firmware received, send ACK |
COMMAND_CHAR | 00008022-0000-1000-8000-00805f9b34fb | Write, WriteNR, Notify | Send the command and ACK |
4. OTA Service data transmission details
4.1 Command package format
unit | Command_ID | PayLoad | CRC16 |
---|---|---|---|
Byte | Byte: 0 ~ 1 | Byte: 2 ~ 17 | Byte: 18 ~ 19 |
Command_ID:
- 0x0001: Start Flash OTA, Payload bytes(2 to 5), indicates the length of the firmware. Other Payload is set to 0 by default. CRC16 calculates bytes(0 to 17).
- 0x0004: Start SPIFFS OTA, Payload bytes(2 to 5), indicates the length of the SPIFFS. Other Payload is set to 0 by default. CRC16 calculates bytes(0 to 17).
- 0x0002: Stop OTA, and the remaining Payload will be set to 0. CRC16 calculates bytes(0 to 17).
- 0x0003: The Payload bytes(2 or 3) is the payload of the Command_ID for which the response will be sent. Payload bytes(4 to 5) is a response to the command. 0x0000 indicates accept, 0x0001 indicates reject, 0x0003 indicate signature error. Other payloads are set to 0. CRC16 computes bytes(0 to 17).
4.2 Firmware package format
The format of the firmware package sent by the client is as follows:
unit | Sector_Index | Packet_Seq | PayLoad |
---|---|---|---|
Byte | Byte: 0 ~ 1 | Byte: 2 | Byte: 3 ~ (MTU_size - 4) |
- Sector_Index:Indicates the number of sectors, sector number increases from 0, cannot jump, must be send 4K data and then start transmit the next sector, otherwise it will immediately send the error ACK for request retransmission.
- Packet_Seq:If Packet_Seq is 0xFF, it indicates that this is the last packet of the sector, and the last 2 bytes of Payload is the CRC16 value of 4K data for the entire sector, the remaining bytes will set to 0x0. Server will check the total length and CRC of the data from the client, reply the correct ACK, and then start receive the next sector of firmware data.
The format of the reply packet is as follows:
unit | Sector_Index | ACK_Status | CRC6 |
---|---|---|---|
Byte | Byte: 0 ~ 1 | Byte: 2 ~ 3 | Byte: 18 ~ 19 |
ACK_Status:
- 0x0000: Success
- 0x0001: CRC error
- 0x0002: Sector_Index error, bytes(4 ~ 5) indicates the desired Sector_Index
- 0x0003:Payload length error
- 0x0005:Can't start OTA
5. Compression
To speed up the OTA process the .bin file for the update can be compressed using zlib, the decompression code is based on the work of https://github.com/vortigont/esp32-flashz where you can find more details.
With compression the time is reduced by approximately 30% for FLASH and much more for SPIFFS.
The compression must be done before eventually sign the file, you can use the [https://github.com/madler/pigz].(https://github.com/madler/pigz). tool
- compress the file
pigz -9kzc file.ino.bin > file.compressed.bin
- use the file.compressed.bin to perform the update or to create the signed file
6. Security
After the creation of BLE server call BLEOTA.begin with the Server pointer
// Create the BLE Device
BLEDevice::init("ESP32");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
// Begin BLE OTA with security
BLEOTA.begin(pServer, true);
Add the public key
// Add pub key
BLEOTA.setKey(pub_key, strlen(pub_key));
that has been previously defined as:
const char pub_key[] = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw/rrOWrykXdTPFwZzljd\nPuuhkRDQUJQu0et5dWNd4ntbh+Qp9qDiZMEj9PcUkw6VUCWFTcSFkOR4i3M+H3g3\nJsKGe5y45DGK8HvgOAnGGUtb0/V2UVZAqiUzJ2cXSK+1688/kWRBSv6OTMXFg2Fa\nGnaIEupUIJZfnBjJmZOhqJll+kxvkxE3CjbnnP8SZ31ybItPV3DyML/7RZ3gMBB5\ngVh44kzAIzPD+NtSSU/RNbWOi3rgNPx1SLzUPjThkHAkVRJ96pWEctiblv2XwoIm\n1ZJEeeda3O46+zCpsI1Ph5oo8mi4QWj1MvkQldo3XtLWRtH/IbMLEgRSR5y054Tg\n0QIDAQAB\n-----END PUBLIC KEY-----";
with the content of the .pub file
6.1 OTA File signature
The process for signing the file is:
- generate the private key (keep this secret) and the corresponding public key:
openssl genrsa -out priv_key.pem 2048
openssl rsa -in priv_key.pem -pubout > rsa_key.pub
- export the compiled sketch or SPIFFS to get the bin file, eventually compress it, and create the digest of the file with SHA256 hash with the private key:
openssl dgst -sign priv_key.pem -keyform PEM -sha256 -out signature.sign -binary file.ino.bin
- throw it all in one file
cat file.ino.bin signature.sign > ota.bin
use the ota.bin to perform the update
7. Sample code
After the creation of BLE server call BLEOTA.begin with the Server pointer
// Create the BLE Device
BLEDevice::init("ESP32");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
// Begin BLE OTA
BLEOTA.begin(pServer);
Eventually set the values of the DIS Service
#ifdef MODEL
BLEOTA.setModel(MODEL);
#endif
#ifdef SERIAL_NUM
BLEOTA.setSerialNumber(SERIAL_NUM);
#endif
#ifdef FW_VERSION
BLEOTA.setFWVersion(FW_VERSION);
#endif
#ifdef HW_VERSION
BLEOTA.setHWVersion(HW_VERSION);
#endif
#ifdef MANUFACTURER
BLEOTA.setManufactuer(MANUFACTURER);
#endif
Call BLEOTA.init to add the service
BLEOTA.init();
If you want to add the OTA Service
to the advertising in the setup add
pAdvertising->addServiceUUID(BLEOTA.getBLEOTAuuid());
Add to loop the process function
BLEOTA.process();
After the creation of BLE server call BLEOTA.begin with the Server pointer and security enabled and set the public key Security
// Create the BLE Device
BLEDevice::init("ESP32");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
// Begin BLE OTA with security
BLEOTA.begin(pServer, true);
// Add pub key
BLEOTA.setKey(pub_key, strlen(pub_key));
8. Callbacks
Thanks to the contribution of @drik a set of callbacks can be added. I choice to defer the execution of the code inside each callback in the process method to avoid to break the BLE communication in case of heavy process inside the callbacks. The callbacks can be defined with a new class which inherits from BLEOTACallbacks
class OTACallbacks : public BLEOTACallbacks {
public:
//This callback method is invoked just before the APP OTA update begins.
void beforeStartOTA() {
Serial.println("beforeStartOTA called!\n");
}
//This callback method is invoked just before the SPIFFS OTA update begins.
void beforeStartSPIFFS() {
Serial.println("beforeStartSPIFFS called!\n");
}
//This callback method is invoked just after the update completes.
void afterStop() {
Serial.println("afterStop called!\n");
}
//This callback method is invoked just after the update abort.
void afterAbort() {
Serial.println("afterAbort called!\n");
}
};
To set the callbacks just call setCallbacks after the begin
// Begin BLE OTA
BLEOTA.begin(pServer);
BLEOTA.setCallbacks(new OTACallbacks());
Since the progress function will reset the ESP32 500ms after the completion of the update is possible that the afterStop will no execute correctly, to handle this scenario is possible to disable the automatic reset and handle it some others part of the code.
//automatic reset
//BLEOTA.process();
//to avoid reset after OTA success and manage it in the callback or somewhere else call
BLEOTA.process(false);
9. WebApp
Small web application that implement the OTA process over BLE with Web Bluetooth