Hi Felix,
I try to run the FFat version of the code, but it looks like it is slower to run... also I have modified your code.
I have heavily modified your code to reduce de complexity (of the firmware and of the Python OTA script). Note that I removed all the code stored in the loop. Nearly all is managed in the onWrite
callback and a new task is created to perform the update in the end. On the other side I changed the update sequence, I first send the file size, then MTU/Part size and finally I initiate the update by getting the mode.
It is not perfect but by removing most of the global variable, the code gets simpler:
/*
MIT License
Copyright (c) 2021 Felix Biego
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "FS.h"
#include <Arduino.h>
#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <Update.h>
// #define DEBUG_BLE_OTA_DFU_TX
// #define DEBUG_BLE_OTA_DFU_RX
const uint8_t BUILTIN_LED = 2;
const bool FORMAT_FLASH_IF_MOUNT_FAILED = true;
#define USE_SPIFFS // comment to use FFat
#ifdef USE_SPIFFS
// SPIFFS write is slower
#include "SPIFFS.h"
#define FLASH SPIFFS
const bool FASTMODE = false;
#else
// FFat is faster
#include "FFat.h"
#define FLASH FFat
const bool FASTMODE = true;
#endif
// enum mode { OTA_IDLE = 0, OTA_UPLOAD, OTA_INSTALL };
const char SERVICE_UUID[] = "fe590001-54ae-4a28-9f74-dfccb248601d";
const char CHARACTERISTIC_UUID_RX[] = "fe590002-54ae-4a28-9f74-dfccb248601d";
const char CHARACTERISTIC_UUID_TX[] = "fe590003-54ae-4a28-9f74-dfccb248601d";
BLECharacteristic *pCharacteristicTX;
BLECharacteristic *pCharacteristicRX;
bool device_connected = false;
void reboot_ESP_with_reason(String reason) {
ESP_LOGI(TAG, "Rebooting ESP32 with reason: %s", reason.c_str());
delay(5000);
ESP.restart();
}
uint16_t write_binary(fs::FS &file_system, const char *path, uint8_t *data,
uint16_t length) {
// Append data to the file
ESP_LOGI(TAG, "Write binary file %s\r\n", path);
File file = file_system.open(path, FILE_APPEND);
if (!file) {
ESP_LOGE(TAG, "Failed to open the file to write");
return 0;
}
file.write(data, length);
file.close();
return length;
}
void perform_update(Stream &update_stream, size_t update_size) {
String result = (String)(char)0x0F;
// Init update
if (Update.begin(update_size)) {
// Perform the update
size_t written = Update.writeStream(update_stream);
if (written == update_size) {
ESP_LOGI(TAG, "Written: %d successfully", written);
} else {
ESP_LOGI(TAG, "Written: %d/%d. Retry?", written, update_size);
}
result += "Written : " + String(written) + "/" + String(update_size) +
" [" + String((written / update_size) * 100) + " %] \n";
// Check update
if (Update.end()) {
ESP_LOGI(TAG, "OTA done!");
result += "OTA Done: ";
if (Update.isFinished()) {
ESP_LOGI(TAG, "Update successfully completed. Rebooting...");
result += "Success!\n";
} else {
ESP_LOGE(TAG, "Update not finished? Something went wrong!");
result += "Failed!\n";
}
} else {
Serial.println("Error Occurred. Error #: " + String(Update.getError()));
result += "Error #: " + String(Update.getError());
}
} else {
ESP_LOGE(TAG, "Not enough space to begin BLE OTA DFU");
result += "Not enough space to begin BLE OTA DFU";
}
if (device_connected) {
// Return the result to the client (tells the client if the update was a
// success or not)
pCharacteristicTX->setValue(result.c_str());
pCharacteristicTX->notify();
delay(10);
delay(5000);
}
}
void update_from_FS(fs::FS &file_system) {
// Update the board from the flash.
// Open update.bin file.
File update_binary = file_system.open("/update.bin");
// If the file can be loaded
if (update_binary) {
// Verify that the file is not a directory
if (update_binary.isDirectory()) {
ESP_LOGE(TAG, "Error, update.bin is not a file");
update_binary.close();
return;
}
// Get binary file size
size_t update_size = update_binary.size();
// Proceed to the update if the file is not empty
if (update_size > 0) {
ESP_LOGI(TAG, "Trying to start update");
perform_update(update_binary, update_size);
} else {
ESP_LOGE(TAG, "Error, update file is empty");
}
update_binary.close();
// When finished remove the binary from spiffs
// to indicate the end of the process
ESP_LOGI(TAG, "Removing update file");
file_system.remove("/update.bin");
reboot_ESP_with_reason("complete OTA update");
} else {
ESP_LOGE(TAG, "Could not load update.bin from spiffs root");
}
}
void task_install_update(void *parameters) {
delay(5000);
update_from_FS(FLASH);
ESP_LOGI(TAG, "Installation is complete");
}
class MyServerCallbacks : public BLEServerCallbacks {
// Somehow generic
void onConnect(BLEServer *pServer) { device_connected = true; }
void onDisconnect(BLEServer *pServer) { device_connected = false; }
};
class BLEOverTheAirDeviceFirmwareUpdate : public BLECharacteristicCallbacks {
private:
bool selected_updater = true;
uint8_t updater[2][16384];
uint16_t write_len[2] = {0, 0};
uint16_t parts = 0, MTU = 0;
uint16_t current_progression = 0;
uint32_t received_file_size, expected_file_size;
// void onStatus(BLECharacteristic* pCharacteristic, Status s, uint32_t
// code) {
// Serial.print("Status ");
// Serial.print(s);
// Serial.print(" on characteristic ");
// Serial.print(pCharacteristic->getUUID().toString().c_str());
// Serial.print(" with code ");
// Serial.println(code);
// }
public:
void onNotify(BLECharacteristic *pCharacteristic) {
#ifdef DEBUG_BLE_OTA_DFU_TX
uint8_t *pData;
std::string value = pCharacteristic->getValue();
uint16_t len = value.length();
pData = pCharacteristic->getData();
if (pData != NULL) {
ESP_LOGD(TAG, "Notify callback for characteristic %s of data length %d",
pCharacteristic->getUUID().toString().c_str(), len);
// Print transferred packets
Serial.print("TX ");
for (uint16_t i = 0; i < len; i++) {
Serial.printf("%02X ", pData[i]);
}
Serial.println();
}
#endif
}
void onWrite(BLECharacteristic *pCharacteristic) {
uint8_t *pData;
std::string value = pCharacteristic->getValue();
uint16_t len = value.length();
pData = pCharacteristic->getData();
if (pData != NULL) { // Check that data have been received
#ifdef DEBUG_BLE_OTA_DFU_RX
ESP_LOGD(TAG, "Write callback for characteristic %s of data length %d",
pCharacteristic->getUUID().toString().c_str(), len);
Serial.print("RX ");
for (int i = 0; i < len; i++) {
Serial.printf("%02X ", pData[i]);
}
Serial.println();
#endif
switch (pData[0]) {
case 0xEF: { // Format the flash and send total and used sizes
FLASH.format();
// Send flash size
uint16_t total_size = FLASH.totalBytes();
uint16_t used_size = FLASH.usedBytes();
uint8_t flash_size[] = {0xEF,
(uint8_t)(total_size >> 16),
(uint8_t)(total_size >> 8),
(uint8_t)total_size,
(uint8_t)(used_size >> 16),
(uint8_t)(used_size >> 8),
(uint8_t)used_size};
pCharacteristicTX->setValue(flash_size, 7);
pCharacteristicTX->notify();
delay(10);
} break;
case 0xFB: // Write parts to RAM
// pData[1] is the position of the next part
for (uint16_t index = 0; index < len - 2; index++) {
updater[!selected_updater][(pData[1] * MTU) + index] =
pData[index + 2];
}
break;
case 0xFC: { // Write updater content to the flash
selected_updater = !selected_updater;
write_len[selected_updater] = (pData[1] * 256) + pData[2];
current_progression = (pData[3] * 256) + pData[4];
received_file_size +=
write_binary(FLASH, "/update.bin", updater[selected_updater],
write_len[selected_updater]);
if ((current_progression < parts - 1) && !FASTMODE) {
uint8_t progression[] = {0xF1,
(uint8_t)((current_progression + 1) / 256),
(uint8_t)((current_progression + 1) % 256)};
pCharacteristicTX->setValue(progression, 3);
pCharacteristicTX->notify();
delay(10);
}
ESP_LOGI(TAG, "Upload progress: %d/%d", current_progression + 1, parts);
if (current_progression + 1 == parts) {
// If all the file has been received, send the progression
uint8_t progression[] = {0xF2,
(uint8_t)((current_progression + 1) / 256),
(uint8_t)((current_progression + 1) % 256)};
pCharacteristicTX->setValue(progression, 3);
pCharacteristicTX->notify();
delay(10);
if (received_file_size != expected_file_size) {
received_file_size +=
write_binary(FLASH, "/update.bin", updater[selected_updater],
write_len[selected_updater]);
if (received_file_size > expected_file_size) {
ESP_LOGW(TAG, "Unexpected size:\n Expected: %d\nReceived: %d",
expected_file_size, received_file_size);
}
} else {
ESP_LOGI(TAG, "Installing update");
xTaskCreate(task_install_update, "task_install_update", 8192, NULL,
5, NULL);
}
}
} break;
case 0xFD: // Remove previous file and send transfer mode
{
// Remove previous (failed?) update
if (FLASH.exists("/update.bin")) {
ESP_LOGI(TAG, "Removing previous update");
FLASH.remove("/update.bin");
}
// Send mode ("fast" or "slow")
uint8_t mode[] = {0xAA, FASTMODE};
pCharacteristicTX->setValue(mode, 2);
pCharacteristicTX->notify();
delay(10);
} break;
case 0xFE: // Keep track of the received file and of the expected file
// sizes
received_file_size = 0;
expected_file_size = (pData[1] * 16777216) + (pData[2] * 65536) +
(pData[3] * 256) + pData[4];
ESP_LOGI(TAG, "Available space: %d\nFile Size: %d\n",
FLASH.totalBytes() - FLASH.usedBytes(), expected_file_size);
break;
case 0xFF: // Switch to update mode
parts = (pData[1] * 256) + pData[2];
MTU = (pData[3] * 256) + pData[4];
break;
default:
ESP_LOGW(TAG, "Unknown command: %02X", pData[0]);
break;
}
}
delay(1);
}
};
void initBLE() {
BLEDevice::init("ESP32 OTA");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristicTX = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristicRX = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
pCharacteristicRX->setCallbacks(new BLEOverTheAirDeviceFirmwareUpdate());
pCharacteristicTX->setCallbacks(new BLEOverTheAirDeviceFirmwareUpdate());
pCharacteristicTX->addDescriptor(new BLE2902());
pCharacteristicTX->setNotifyProperty(true);
pService->start();
// BLEAdvertising *pAdvertising = pServer->getAdvertising();
// The above is still working for backward compatibility
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
// functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("Characteristic defined! Now you can read it in your phone!");
}
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE OTA sketch");
pinMode(BUILTIN_LED, OUTPUT);
#ifdef USE_SPIFFS
if (!SPIFFS.begin(FORMAT_FLASH_IF_MOUNT_FAILED)) {
Serial.println("SPIFFS Mount Failed");
return;
}
#else
if (!FFat.begin()) {
Serial.println("FFat Mount Failed");
if (FORMAT_FLASH_IF_MOUNT_FAILED)
FFat.format();
return;
}
#endif
initBLE();
}
void loop() {
// switch (mode) {
// case OTA_IDLE:
// if (device_connected) {
// digitalWrite(BUILTIN_LED, HIGH);
// delay(100);
// // your loop code here (if a client is needed)
// } else {
// digitalWrite(BUILTIN_LED, LOW);
// delay(500);
// }
// // or here (if no client is needed)
// break;
// case OTA_UPLOAD:
// break;
// case OTA_INSTALL:
// break;
// }
delay(100); // Try to increase stability
}
"""
MIT License
Copyright (c) 2021 Felix Biego
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from __future__ import print_function
import os
import asyncio
import math
import sys
import re
from bleak import BleakClient, BleakScanner
# from bleak.exc import BleakError
header = """#####################################################################
------------------------BLE OTA update---------------------
Arduino code @ https://github.com/fbiego/ESP32_BLE_OTA_Arduino
#####################################################################"""
UART_SERVICE_UUID = "fe590001-54ae-4a28-9f74-dfccb248601d"
UART_RX_CHAR_UUID = "fe590002-54ae-4a28-9f74-dfccb248601d"
UART_TX_CHAR_UUID = "fe590003-54ae-4a28-9f74-dfccb248601d"
PART = 16000
MTU = 500
ble_ota_dfu_end = False
global_client = None
file_bytes = None
total = 0
async def start_ota(ble_address: str, filename: str):
device = await BleakScanner.find_device_by_address(ble_address, timeout=20.0)
disconnected_event = asyncio.Event()
def handle_disconnect(_: BleakClient):
print("Device disconnected !")
disconnected_event.set()
async def handle_rx(_: int, data: bytearray):
# print(f'\nReceived: {data = }\n')
match data[0]:
case 0xAA:
print("Starting transfer, mode:", data[1])
print_progress_bar(0, total, prefix='Upload progress:',
suffix='Complete', length=50)
match data[1]:
case 0: # Slow mode
# Send first part
await send_part(0, file_bytes, global_client)
case 1: # Fast mode
for index in range(file_parts):
await send_part(index, file_bytes, global_client)
print_progress_bar(index + 1, total,
prefix='Upload progress:',
suffix='Complete', length=50)
case 0xF1: # Send next part and update progress bar
next_part_to_send = int.from_bytes(
data[2:3], byteorder='little')
# print("Next part:", next_part_to_send, "\n")
await send_part(next_part_to_send, file_bytes, global_client)
print_progress_bar(next_part_to_send + 1, total,
prefix='Upload progress:',
suffix='Complete', length=50)
case 0xF2: # Install firmware
# ins = 'Installing firmware'
# print("Installing firmware")
pass
case 0x0F:
print("OTA result: ", str(data[1:], 'utf-8'))
global ble_ota_dfu_end
ble_ota_dfu_end = True
def print_progress_bar(iteration: int, total: int, prefix: str = '', suffix: str = '', decimals: int = 1, length: int = 100, filler: str = 'โ', print_end: str = "\r"):
"""
Call in a loop to create terminal progress bar
@params:
iteration - Required : current iteration (Int)
total - Required : total iterations (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
filler - Optional : bar fill character (Str)
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
percent = ("{0:." + str(decimals) + "f}").format(100 *
(iteration / float(total)))
filled_length = (length * iteration) // total
bar = filler * filled_length + '-' * (length - filled_length)
print(f'\r{prefix} |{bar}| {percent}ย % {suffix}', end=print_end)
# Print new line upon complete
if iteration == total:
print()
async def send_part(position: int, data: bytearray, client: BleakClient):
start = position * PART
end = (position + 1) * PART
if len(data) < end:
end = len(data)
data_length = end - start
parts = data_length // MTU
for part_index in range(parts):
to_be_sent = bytearray([0xFB, part_index])
for mtu_index in range(MTU):
to_be_sent.append(
data[(position*PART)+(MTU * part_index) + mtu_index])
await send_data(client, to_be_sent)
if data_length % MTU:
remaining = data_length % MTU
to_be_sent = bytearray([0xFB, parts])
for index in range(remaining):
to_be_sent.append(
data[(position*PART)+(MTU * parts) + index])
await send_data(client, to_be_sent)
await send_data(client, bytearray([0xFC, data_length//256, data_length %
256, position//256, position % 256]), True)
async def send_data(client: BleakClient, data: bytearray, response: bool = False):
await client.write_gatt_char(UART_RX_CHAR_UUID, data, response)
if not device:
print("-----------Failed--------------")
print(f"Device with address {ble_address} could not be found.")
return
#raise BleakError(f"A device with address {ble_address} could not be found.")
async with BleakClient(device, disconnected_callback=handle_disconnect) as client:
# Set the UUID of the service you want to connect to and the callback
await client.start_notify(UART_TX_CHAR_UUID, handle_rx)
await asyncio.sleep(1.0)
# Set global client to be the current client
global global_client
global_client = client
# Send file size
print("Reading from: ", filename)
global file_bytes
file_bytes = open(filename, "rb").read()
file_parts = math.ceil(len(file_bytes) / PART)
file_length = len(file_bytes)
print(f'File size: {len(file_bytes)}')
# Send file length
await send_data(client, bytearray([0xFE,
file_length >> 24 & 0xFF,
file_length >> 16 & 0xFF,
file_length >> 8 & 0xFF,
file_length & 0xFF]))
# Send number of part and MTU value
global total
total = file_parts
await send_data(client, bytearray([0xFF,
file_parts//256,
file_parts % 256,
MTU // 256,
MTU % 256]))
# Remove previous update and receive transfer mode (start the update)
await send_data(client, bytearray([0xFD]))
# Wait til upload is complete
while not ble_ota_dfu_end:
await asyncio.sleep(1.0)
print("Waiting for disconnect... ", end="")
await disconnected_event.wait()
print("-----------Complete--------------")
def is_valid_address(value: str = None) -> bool:
# Regex to check valid MAC address
regex_0 = (r"^([0-9A-Fa-f]{2}[:-])"
r"{5}([0-9A-Fa-f]{2})|"
r"([0-9a-fA-F]{4}\\."
r"[0-9a-fA-F]{4}\\."
r"[0-9a-fA-F]{4}){17}$")
regex_1 = (r"^[{]?[0-9a-fA-F]{8}"
r"-([0-9a-fA-F]{4}-)"
r"{3}[0-9a-fA-F]{12}[}]?$")
# Compile the ReGex
regex_0 = re.compile(regex_0)
regex_1 = re.compile(regex_1)
# If the string is empty return false
if value is None:
return False
# Return if the string matched the ReGex
if re.search(regex_0, value) and len(value) == 17:
return True
return re.search(regex_1, value) and len(value) == 36
if __name__ == "__main__":
print(header)
# Check if the user has entered enough arguments
# sys.argv.append("C8:C9:A3:D2:60:8E")
# sys.argv.append("firmware.bin")
if len(sys.argv) < 3:
print("Specify the device address and firmware file")
import sys
import os
filename = os.path.join(os.path.dirname(
__file__), 'PIO', 'ESP32_BLE_OTA_DFU', '.pio', 'build', 'esp32dev', 'firmware.bin')
filename = filename if os.path.exists(filename) else "firmware.bin"
print(f"$ {sys.executable} {__file__} \"C8:C9:A3:D2:60:8E\" \"{filename}\"")
exit(1)
print("Trying to start OTA update")
ble_address = sys.argv[1]
filename = sys.argv[2]
# Check if the address is valid
if not is_valid_address(ble_address):
print(f"Invalid Address: {ble_address}")
exit(2)
# Check if the file exists
if not os.path.exists(filename):
print(f"File not found: {filename}")
exit(3)
asyncio.run(start_ota(ble_address, filename))
Best,
Vincent