From d136d84876174d7d869e9a2b83c74cdb10526a41 Mon Sep 17 00:00:00 2001 From: Vincent STRAGIER Date: Sun, 22 May 2022 11:23:25 +0200 Subject: [PATCH] Add esp32_ble_ota_lib_compact --- esp32_ble_ota_lib_compact/.gitignore | 5 + .../.vscode/extensions.json | 10 + .../.vscode/settings.json | 9 + esp32_ble_ota_lib_compact/README.md | 7 + esp32_ble_ota_lib_compact/discover.py | 13 + esp32_ble_ota_lib_compact/include/README | 39 ++ esp32_ble_ota_lib_compact/lib/README | 46 ++ .../lib/ble_ota_dfu/keywords.txt | 5 + .../lib/ble_ota_dfu/library.json | 14 + .../lib/ble_ota_dfu/library.properties | 9 + .../lib/ble_ota_dfu/src/ble_ota_dfu.cpp | 435 ++++++++++++++++++ .../lib/ble_ota_dfu/src/ble_ota_dfu.hpp | 87 ++++ .../lib/ble_ota_dfu/src/freertos_utils.cpp | 16 + .../lib/ble_ota_dfu/src/freertos_utils.hpp | 40 ++ esp32_ble_ota_lib_compact/ota_updater.py | 247 ++++++++++ esp32_ble_ota_lib_compact/platformio.ini | 52 +++ esp32_ble_ota_lib_compact/src/main.cpp | 20 + esp32_ble_ota_lib_compact/src/main.hpp | 17 + esp32_ble_ota_lib_compact/test/README | 11 + 19 files changed, 1082 insertions(+) create mode 100644 esp32_ble_ota_lib_compact/.gitignore create mode 100644 esp32_ble_ota_lib_compact/.vscode/extensions.json create mode 100644 esp32_ble_ota_lib_compact/.vscode/settings.json create mode 100644 esp32_ble_ota_lib_compact/README.md create mode 100644 esp32_ble_ota_lib_compact/discover.py create mode 100644 esp32_ble_ota_lib_compact/include/README create mode 100644 esp32_ble_ota_lib_compact/lib/README create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/keywords.txt create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.json create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.properties create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.cpp create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.hpp create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.cpp create mode 100644 esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.hpp create mode 100644 esp32_ble_ota_lib_compact/ota_updater.py create mode 100644 esp32_ble_ota_lib_compact/platformio.ini create mode 100644 esp32_ble_ota_lib_compact/src/main.cpp create mode 100644 esp32_ble_ota_lib_compact/src/main.hpp create mode 100644 esp32_ble_ota_lib_compact/test/README diff --git a/esp32_ble_ota_lib_compact/.gitignore b/esp32_ble_ota_lib_compact/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/esp32_ble_ota_lib_compact/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/esp32_ble_ota_lib_compact/.vscode/extensions.json b/esp32_ble_ota_lib_compact/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/esp32_ble_ota_lib_compact/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/esp32_ble_ota_lib_compact/.vscode/settings.json b/esp32_ble_ota_lib_compact/.vscode/settings.json new file mode 100644 index 0000000..62f5207 --- /dev/null +++ b/esp32_ble_ota_lib_compact/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "LOGD", + "LOGI", + "LOGW", + "ledc", + "LEDC", + ] +} \ No newline at end of file diff --git a/esp32_ble_ota_lib_compact/README.md b/esp32_ble_ota_lib_compact/README.md new file mode 100644 index 0000000..28ea7ac --- /dev/null +++ b/esp32_ble_ota_lib_compact/README.md @@ -0,0 +1,7 @@ +# Compact version of the BLE OTA example + +This directory contains a PlatformIO project which implement a compact version of the OTA. It also provides a library (in the lib folder) that allows to easily port BLE OTA to your own code. + +**Note:** this version of the code is not compatible with the mobile app for the moment (the update mechanism is not exactly the same). + +Furthermore, to use the FFAT mode you need to use another partition table (which you must configure in `platformio.ini`). diff --git a/esp32_ble_ota_lib_compact/discover.py b/esp32_ble_ota_lib_compact/discover.py new file mode 100644 index 0000000..aebcde9 --- /dev/null +++ b/esp32_ble_ota_lib_compact/discover.py @@ -0,0 +1,13 @@ +import asyncio +from bleak import BleakScanner + + +async def run(): + for device in await BleakScanner.discover(): + print(f'{device.name} - {device.address}') + +if __name__ == '__main__': + try: + asyncio.run(run()) + except KeyboardInterrupt: + pass diff --git a/esp32_ble_ota_lib_compact/include/README b/esp32_ble_ota_lib_compact/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/esp32_ble_ota_lib_compact/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/esp32_ble_ota_lib_compact/lib/README b/esp32_ble_ota_lib_compact/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/keywords.txt b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/keywords.txt new file mode 100644 index 0000000..9ca837e --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/keywords.txt @@ -0,0 +1,5 @@ +# To be filled if you want to use it on Arduino +#FUNCTIONS COLOR #D35400 - ORANGE KEYWORD1 +#FUNCTIONS COLOR #D35400 - ORANGE KEYWORD2 +#STRUCTURE COLOR #728E00 - GREEN KEYWORD3 +#VARIABLES COLOR #00979C - BLUE LITERAL1 diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.json b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.json new file mode 100644 index 0000000..9f282cd --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.json @@ -0,0 +1,14 @@ +{ + "name": "ble_ota_dfuU", + "keywords": "BLE OTA DFU", + "description": "BLE OTA DFU", + "repository": + { + "type": "email", + "url": "vincent.stragier@outlook.com" + }, + "version": "1.0.0", + "exclude": "doc", + "frameworks": "arduino", + "platforms": ["esp32"] +} diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.properties b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.properties new file mode 100644 index 0000000..de3f79c --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/library.properties @@ -0,0 +1,9 @@ +name=ble_ota_dfu +version=1.0.0 +author=Vincent STRAGIER +maintainer=Vincent STRAGIER +sentence=BLE OTA DFU +paragraph=BLE OTA DFU +category=Other +url=mailto:vincent.stragier@outlook.com +architectures=esp32 \ No newline at end of file diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.cpp b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.cpp new file mode 100644 index 0000000..48f6d1d --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.cpp @@ -0,0 +1,435 @@ +/* Copyright 2022 Vincent Stragier */ +#include "ble_ota_dfu.hpp" + +QueueHandle_t start_update_queue; +QueueHandle_t update_uploading_queue; + +void task_install_update(void *parameters) { + FS file_system = FLASH; + const char path[] = "/update.bin"; + uint8_t data = 0; + BLE_OTA_DFU *OTA_DFU_BLE; + OTA_DFU_BLE = reinterpret_cast(parameters); + delay(100); + + // Wait for the upload to be completed + bool start_update = false; + xQueuePeek(start_update_queue, &start_update, portMAX_DELAY); + while (!start_update) { + delay(500); + xQueuePeek(start_update_queue, &start_update, portMAX_DELAY); + } + + ESP_LOGE(TAG, "Starting OTA update"); + + // Open update.bin file. + File update_binary = FLASH.open(path); + + // If the file cannot be loaded, return. + if (!update_binary) { + ESP_LOGE(TAG, "Could not load update.bin from spiffs root"); + vTaskDelete(NULL); + } + + // 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(); + vTaskDelete(NULL); + } + + // 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_LOGE(TAG, "Error, update file is empty"); + update_binary.close(); + vTaskDelete(NULL); + } + + ESP_LOGI(TAG, "Starting the update"); + // perform_update(update_binary, update_size); + String result = (String) static_cast(0x0F); + + // Init update + if (Update.begin(update_size)) { + // Perform the update + size_t written = Update.writeStream(update_binary); + + ESP_LOGI(TAG, "Written: %d/%d. %s", written, update_size, + written == update_size ? "Success!" : "Retry?"); + 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..."); + } else { + ESP_LOGE(TAG, "Update not finished? Something went wrong!"); + } + + result += Update.isFinished() ? "Success!\n" : "Failed!\n"; + } else { + ESP_LOGE(TAG, "Error Occurred. Error #: %d", 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"; + } + + update_binary.close(); + + // When finished remove the binary from spiffs + // to indicate the end of the process + ESP_LOGI(TAG, "Removing update file"); + FLASH.remove(path); + + if (OTA_DFU_BLE->connected()) { + // Return the result to the client (tells the client if the update was a + // successfull or not) + ESP_LOGI(TAG, "Sending result to client"); + OTA_DFU_BLE->send_OTA_DFU(result); + // OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->setValue(result); + // OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->notify(); + ESP_LOGE(TAG, "%s", result.c_str()); + ESP_LOGI(TAG, "Result sent to client"); + delay(5000); + } + + ESP_LOGE(TAG, "Rebooting ESP32: complete OTA update"); + delay(5000); + ESP.restart(); + + // ESP_LOGI(TAG, "Installation is complete"); + vTaskDelete(NULL); +} + +// 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); +// } + +uint16_t BLEOverTheAirDeviceFirmwareUpdate::write_binary(fs::FS *file_system, + const char *path, + uint8_t *data, + uint16_t length, + bool keep_open) { + static File file; + + // Append data to the file + + if (!file_open) { + ESP_LOGI(TAG, "Opening binary file %s\r\n", path); + file = file_system->open(path, FILE_WRITE); + file_open = true; + } + + if (!file) { + ESP_LOGE(TAG, "Failed to open the file to write"); + return 0; + } + + if (data != nullptr) { + ESP_LOGI(TAG, "Write binary file %s\r\n", path); + file.write(data, length); + } + + if (keep_open) { + return length; + } else { + file.close(); + file_open = false; + return 0; + } +} + +void BLEOverTheAirDeviceFirmwareUpdate::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(); + uint8_t *pData = (uint8_t *)value.data(); + + 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 BLEOverTheAirDeviceFirmwareUpdate::onWrite( + BLECharacteristic *pCharacteristic) { + // uint8_t *pData; + std::string value = pCharacteristic->getValue(); + uint16_t len = value.length(); + // pData = pCharacteristic->getData(); + uint8_t *pData = (uint8_t *)value.data(); + + // Check that data have been received + if (pData != NULL) { +// #define DEBUG_BLE_OTA_DFU_RX +#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 (uint16_t index = 0; index < len; index++) { + Serial.printf("%02X ", pData[i]); + } + Serial.println(); +#endif + switch (pData[0]) { + // Send total and used sizes + case 0xEF: { + FLASH.format(); + + // Send flash size + uint16_t total_size = FLASH.totalBytes(); + uint16_t used_size = FLASH.usedBytes(); + uint8_t flash_size[] = {0xEF, + static_cast(total_size >> 16), + static_cast(total_size >> 8), + static_cast(total_size), + static_cast(used_size >> 16), + static_cast(used_size >> 8), + static_cast(used_size)}; + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->setValue(flash_size, 7); + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->notify(); + delay(10); + } break; + + // Write parts to RAM + case 0xFB: { + // 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; + + // Write updater content to the flash + case 0xFC: { + 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)}; + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->setValue(progression, 3); + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->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)}; + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->setValue(progression, 3); + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->notify(); + delay(10); + + if (received_file_size != expected_file_size) { + // received_file_size += (pData[1] * 256) + pData[2]; + 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"); + + // Start the installation + write_binary(&FLASH, "/update.bin", nullptr, 0, false); + bool start_update = true; + xQueueOverwrite(start_update_queue, &start_update); + } + } + } break; + + // Remove previous file and send transfer mode + case 0xFD: { + // 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}; + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->setValue(mode, 2); + OTA_DFU_BLE->pCharacteristic_BLE_OTA_DFU_TX->notify(); + delay(10); + } break; + + // Keep track of the received file and of the expected file sizes + case 0xFE: + 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; + + // Switch to update mode + case 0xFF: + parts = (pData[1] * 256) + pData[2]; + MTU = (pData[3] * 256) + pData[4]; + break; + + default: + ESP_LOGW(TAG, "Unknown command: %02X", pData[0]); + break; + } + // ESP_LOGE(TAG, "Not stuck in loop"); + } + delay(1); +} + +bool BLE_OTA_DFU::configure_OTA(NimBLEServer *pServer) { + // Init FLASH +#ifdef USE_SPIFFS + if (!SPIFFS.begin(FORMAT_FLASH_IF_MOUNT_FAILED)) { + ESP_LOGE(TAG, "SPIFFS Mount Failed"); + return false; + } + ESP_LOGI(TAG, "SPIFFS Mounted"); +#else + if (!FFat.begin()) { + ESP_LOGE(TAG, "FFat Mount Failed"); + if (FORMAT_FLASH_IF_MOUNT_FAILED) + FFat.format(); + return false; + } + ESP_LOGI(TAG, "FFat Mounted"); +#endif + + // Get the pointer to the pServer + this->pServer = pServer; + // Create the BLE OTA DFU Service + pServiceOTA = pServer->createService(SERVICE_OTA_BLE_UUID); + + if (pServiceOTA == nullptr) { + return false; + } + + BLECharacteristic *pCharacteristic_BLE_OTA_DFU_RX = + pServiceOTA->createCharacteristic(CHARACTERISTIC_OTA_BL_UUID_RX, + NIMBLE_PROPERTY::WRITE | + NIMBLE_PROPERTY::WRITE_NR); + + if (pCharacteristic_BLE_OTA_DFU_RX == nullptr) { + return false; + } + + auto *bleOTACharacteristicCallbacksRX = + new BLEOverTheAirDeviceFirmwareUpdate(); + bleOTACharacteristicCallbacksRX->OTA_DFU_BLE = this; + pCharacteristic_BLE_OTA_DFU_RX->setCallbacks(bleOTACharacteristicCallbacksRX); + + pCharacteristic_BLE_OTA_DFU_TX = pServiceOTA->createCharacteristic( + CHARACTERISTIC_OTA_BL_UUID_TX, NIMBLE_PROPERTY::NOTIFY); + + if (pCharacteristic_BLE_OTA_DFU_TX == nullptr) { + return false; + } + + // Start the BLE UART service + pServiceOTA->start(); + return true; +} + +void BLE_OTA_DFU::start_OTA() { + bool state = false; + initialize_queue(&start_update_queue, bool, &state, 1); + initialize_queue(&update_uploading_queue, bool, &state, 1); + // initialize_empty_queue(&update_queue, uint8_t, UPDATE_QUEUE_SIZE); + + ESP_LOGI(TAG, "Available heap memory: %d", ESP.getFreeHeap()); + // ESP_LOGE(TAG, "Available stack memory: %d", + + ESP_LOGI(TAG, "Available free space: %d", + FLASH.totalBytes() - FLASH.usedBytes()); + // ESP_LOGE(TAG, "Available free blocks: %d", FLASH.blockCount()); + TaskHandle_t taskInstallUpdate = NULL; + + xTaskCreatePinnedToCoreAndAssert(task_install_update, "task_install_update", + 5120, static_cast(this), 5, + &taskInstallUpdate, 1); + + ESP_LOGI(TAG, "Available memory (after task started): %d", ESP.getFreeHeap()); +} + +bool BLE_OTA_DFU::begin(String local_name) { + // Create the BLE Device + BLEDevice::init(local_name.c_str()); + + ESP_LOGI(TAG, "Starting BLE UART services"); + + // Create the BLE Server + pServer = BLEDevice::createServer(); + + if (pServer == nullptr) { + return false; + } + + this->configure_OTA(pServer); + + // Start advertising + pServer->getAdvertising()->addServiceUUID(pServiceOTA->getUUID()); + pServer->getAdvertising()->start(); + + this->start_OTA(); + return true; +} + +bool BLE_OTA_DFU::connected() { + // True if connected + return pServer->getConnectedCount() > 0; +} + +void BLE_OTA_DFU::send_OTA_DFU(uint8_t value) { + uint8_t _value = value; + this->pCharacteristic_BLE_OTA_DFU_TX->setValue(&_value, 1); + this->pCharacteristic_BLE_OTA_DFU_TX->notify(); +} + +void BLE_OTA_DFU::send_OTA_DFU(uint8_t *value, size_t size) { + this->pCharacteristic_BLE_OTA_DFU_TX->setValue(value, size); + this->pCharacteristic_BLE_OTA_DFU_TX->notify(); +} + +void BLE_OTA_DFU::send_OTA_DFU(String value) { + this->pCharacteristic_BLE_OTA_DFU_TX->setValue(value.c_str()); + this->pCharacteristic_BLE_OTA_DFU_TX->notify(); +} diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.hpp b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.hpp new file mode 100644 index 0000000..c90d270 --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/ble_ota_dfu.hpp @@ -0,0 +1,87 @@ +/* Copyright 2022 Vincent Stragier */ +// Highly inspired by https://github.com/fbiego/ESP32_BLE_OTA_Arduino +#pragma once +#ifndef SRC_BLE_OTA_DFU_HPP_ +#define SRC_BLE_OTA_DFU_HPP_ + +#include "./freertos_utils.hpp" +#include +#include +#include +#include +#include + +// comment to use FFat +#define USE_SPIFFS + +#ifdef USE_SPIFFS +// SPIFFS write is slower +#include +#define FLASH SPIFFS +const bool FASTMODE = false; +#else +// FFat is faster +#include +#define FLASH FFat +const bool FASTMODE = true; +#endif +// #endif + +const char SERVICE_OTA_BLE_UUID[] = "fe590001-54ae-4a28-9f74-dfccb248601d"; +const char CHARACTERISTIC_OTA_BL_UUID_RX[] = + "fe590002-54ae-4a28-9f74-dfccb248601d"; +const char CHARACTERISTIC_OTA_BL_UUID_TX[] = + "fe590003-54ae-4a28-9f74-dfccb248601d"; + +const bool FORMAT_FLASH_IF_MOUNT_FAILED = true; +const uint32_t UPDATER_SIZE = 20000; + +/* Dummy class */ +class BLE_OTA_DFU; + +class BLEOverTheAirDeviceFirmwareUpdate : public BLECharacteristicCallbacks { +private: + bool selected_updater = true; + bool file_open = false; + uint8_t updater[2][UPDATER_SIZE]; + 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; + +public: + friend class BLE_OTA_DFU; + BLE_OTA_DFU *OTA_DFU_BLE; + + uint16_t write_binary(fs::FS *file_system, const char *path, uint8_t *data, + uint16_t length, bool keep_open = true); + void onNotify(BLECharacteristic *pCharacteristic); + void onWrite(BLECharacteristic *pCharacteristic); +}; + +class BLE_OTA_DFU { +private: + BLEServer *pServer = nullptr; + BLEService *pServiceOTA = nullptr; + BLECharacteristic *pCharacteristic_BLE_OTA_DFU_TX = nullptr; + friend class BLEOverTheAirDeviceFirmwareUpdate; + +public: + BLE_OTA_DFU() = default; + ~BLE_OTA_DFU() = default; + + bool configure_OTA(NimBLEServer *pServer); + void start_OTA(); + + bool begin(String local_name); + + bool connected(); + + void send_OTA_DFU(uint8_t value); + void send_OTA_DFU(uint8_t *value, size_t size); + void send_OTA_DFU(String value); + + friend void task_install_update(void *parameters); +}; + +#endif /* SRC_BLE_OTA_DFU_HPP_ */ diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.cpp b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.cpp new file mode 100644 index 0000000..2cee696 --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.cpp @@ -0,0 +1,16 @@ +/* Copyright 2022 Vincent Stragier */ +#include "freertos_utils.hpp" + +BaseType_t xTaskCreatePinnedToCoreAndAssert( + TaskFunction_t pvTaskCode, const char *pcName, uint32_t usStackDepth, + void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pvCreatedTask, + BaseType_t xCoreID) { + + BaseType_t xReturn = + xTaskCreatePinnedToCore(pvTaskCode, pcName, usStackDepth, pvParameters, + uxPriority, pvCreatedTask, xCoreID); + + configASSERT(pvCreatedTask); + + return xReturn; +} diff --git a/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.hpp b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.hpp new file mode 100644 index 0000000..ee47773 --- /dev/null +++ b/esp32_ble_ota_lib_compact/lib/ble_ota_dfu/src/freertos_utils.hpp @@ -0,0 +1,40 @@ +/* Copyright 2022 Vincent Stragier */ +#pragma once +#ifndef SRC_FREERTOS_UTILS_HPP_ +#define SRC_FREERTOS_UTILS_HPP_ + +#include + +// Initialize the queue +#define initialize_queue(queue, T, initial_value, size) \ + _initialize_queue((queue), (#queue), (size), (initial_value)) +#define initialize_empty_queue(queue, T, size) \ + _initialize_queue((queue), (#queue), (size)) + +template +void _initialize_queue(QueueHandle_t *queue, const char *queue_name, + size_t size, T *initial_value = nullptr) { + *queue = xQueueCreate(size, sizeof(T)); + + ESP_LOGI(TAG, "Creating the queue \"%s\"", queue_name); + // Init start update queue + if (*queue == NULL || *queue == nullptr) { + ESP_LOGE(TAG, "Error creating the queue \"%s\"", queue_name); + } + + if (initial_value == nullptr || initial_value == NULL) { + return; + } + + if (!xQueueSend(*queue, initial_value, portMAX_DELAY)) { + ESP_LOGE(TAG, "Error initializing the queue \"%s\"", queue_name); + } +} + +// Create a task and assert it's creation +BaseType_t xTaskCreatePinnedToCoreAndAssert( + TaskFunction_t pvTaskCode, const char *pcName, uint32_t usStackDepth, + void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pvCreatedTask, + BaseType_t xCoreID); + +#endif /* SRC_FREERTOS_UTILS_HPP_ */ diff --git a/esp32_ble_ota_lib_compact/ota_updater.py b/esp32_ble_ota_lib_compact/ota_updater.py new file mode 100644 index 0000000..043f2fa --- /dev/null +++ b/esp32_ble_ota_lib_compact/ota_updater.py @@ -0,0 +1,247 @@ +from __future__ import print_function +import os +import asyncio +import sys +import re +from time import sleep + +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 = 19000 +MTU = 250 + +ble_ota_dfu_end = False + + +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() + total = 0 + file_content = None + client = None + + def handle_disconnect(_: BleakClient): + print("Device disconnected !") + disconnected_event.set() + sleep(1) + # sys.exit(0) + + 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_content, client) + case 1: # Fast mode + for index in range(file_parts): + await send_part(index, file_content, 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_content, 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 + # print(locals()) + 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): + # print(f'{locals()["data"]}') + 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 local_client: + client = local_client + + # Load file + print("Reading from: ", filename) + file_content = open(filename, "rb").read() + file_parts = -(len(file_content) // -PART) + total = file_parts + file_length = len(file_content) + print(f'File size: {len(file_content)}') + + # 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) + + # 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 parts and MTU value + 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 the update 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', 'build', 'esp32doit-devkit-v1', '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) + + try: + # Start the OTA update + asyncio.run(start_ota(ble_address, filename)) + except KeyboardInterrupt: + print("\nExiting...") + exit(0) + except OSError: + print("\nExiting (OSError)...") + exit(1) + except Exception: + import traceback + traceback.print_exc() + exit(2) diff --git a/esp32_ble_ota_lib_compact/platformio.ini b/esp32_ble_ota_lib_compact/platformio.ini new file mode 100644 index 0000000..3a4bee4 --- /dev/null +++ b/esp32_ble_ota_lib_compact/platformio.ini @@ -0,0 +1,52 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32doit-devkit-v1] +platform = espressif32 +board = esp32dev +framework = arduino +; ESP_LOGE - error (lowest) = 1 +; ESP_LOGW - warning = 2 +; ESP_LOGI - info = 3 +; ESP_LOGD - debug = 4 +; ESP_LOGV - verbose (highest) = 5 +; build_flags = -DCORE_DEBUG_LEVEL=3 +; build_type = debug + +; board_build.partitions = default_ffat.csv +board_build.partitions = default.csv + +; Serial Monitor options +; https://docs.platformio.org/en/latest/core/userguide/device/cmd_monitor.html +monitor_speed = 115200 +monitor_rts = 0 +monitor_dtr = 0 + +monitor_filters = + ; time + ; send_on_enter + esp32_exception_decoder + ; hexlify + ; colorize + +; Libraries +lib_deps = + https://github.com/h2zero/NimBLE-Arduino.git + +; Run before compilation +; extra_scripts = +; pre:some_script.py + +; Configure checking tool +;, pvs-studio, +check_tool = cppcheck, clangtidy +check_flags = + cppcheck: --addon=cert.py +check_skip_packages = yes diff --git a/esp32_ble_ota_lib_compact/src/main.cpp b/esp32_ble_ota_lib_compact/src/main.cpp new file mode 100644 index 0000000..43b6d56 --- /dev/null +++ b/esp32_ble_ota_lib_compact/src/main.cpp @@ -0,0 +1,20 @@ +/* Copyright 2022 Vincent Stragier */ +#include "main.hpp" + +/////////////////// +/// Setup /// +/////////////////// +void setup() { ota_dfu_ble.begin("Test OTA DFU"); } + +////////////////// +/// Loop /// +////////////////// +void loop() { + // Kill the holly loop() + // Delete the task, comment if you want to keep the loop() + vTaskDelete(NULL); +} + +/////////////////////// +/// Functions /// +/////////////////////// diff --git a/esp32_ble_ota_lib_compact/src/main.hpp b/esp32_ble_ota_lib_compact/src/main.hpp new file mode 100644 index 0000000..0a06867 --- /dev/null +++ b/esp32_ble_ota_lib_compact/src/main.hpp @@ -0,0 +1,17 @@ +/* Copyright 2022 Vincent Stragier */ + +#ifndef SRC_MAIN_HPP_ +#define SRC_MAIN_HPP_ +/////////////////////// +/// Libraries /// +/////////////////////// + +#include +#include + +/////////////////////// +/// Variables /// +/////////////////////// +BLE_OTA_DFU ota_dfu_ble; + +#endif /* SRC_MAIN_HPP_ */ diff --git a/esp32_ble_ota_lib_compact/test/README b/esp32_ble_ota_lib_compact/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/esp32_ble_ota_lib_compact/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html