├── extras └── logo-Theengs.jpg ├── examples ├── platformio.ini ├── basic.cpp └── secure.cpp ├── library.properties ├── NimBLEDis.h ├── LICENSE ├── NimBLEOta.h ├── README.md ├── NimBLEDis.cpp ├── scripts └── nimbleota.py └── NimBLEOta.cpp /extras/logo-Theengs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2zero/NimBLEOta/HEAD/extras/logo-Theengs.jpg -------------------------------------------------------------------------------- /examples/platformio.ini: -------------------------------------------------------------------------------- 1 | [env] 2 | monitor_speed = 115200 3 | monitor_filters = esp32_exception_decoder 4 | 5 | [env:esp32] 6 | platform = espressif32 7 | board = esp32dev 8 | framework = arduino 9 | upload_speed = 1500000 10 | monitor_speed = 115200 11 | board_build.partitions = min_spiffs.csv 12 | lib_deps = h2zero/NimBLE-Arduino@^1.4.0 13 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=NimBLEOta 2 | version=0.2.0 3 | author=h2zero 4 | maintainer=Ryan Powell 5 | sentence=Bluetooth low energy (BLE) OTA update library for ESP32 devices. 6 | paragraph=Creates BLE services for updating over the air using included python script or the espressif android app. 7 | url=https://github.com/h2zero/NimBLEOta 8 | category=Communication 9 | architectures=esp32 10 | includes=NimBLEOta.h 11 | -------------------------------------------------------------------------------- /NimBLEDis.h: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Ryan Powell and NimBLEOta contributors 2 | // Sponsored by Theengs https://www.theengs.io 3 | // MIT License 4 | 5 | #ifndef NIMBLE_DIS_H_ 6 | #define NIMBLE_DIS_H_ 7 | 8 | class NimBLEService; 9 | class NimBLEUUID; 10 | 11 | /** 12 | * @brief A model of the BLE Device Information Service 13 | * https://www.bluetooth.com/specifications/specs/device-information-service-1-1/ 14 | */ 15 | class NimBLEDis { 16 | public: 17 | bool init(); 18 | bool start(); 19 | bool setModelNumber(const char* value); 20 | bool setSerialNumber(const char* value); 21 | bool setFirmwareRevision(const char* value); 22 | bool setHardwareRevision(const char* value); 23 | bool setSoftwareRevision(const char* value); 24 | bool setManufacturerName(const char* value); 25 | bool setSystemId(const char* value); 26 | bool setPnp(uint8_t src, uint16_t vid, uint16_t pid, uint16_t ver); 27 | 28 | private: 29 | bool createDisChar(const NimBLEUUID& uuid, const uint8_t* value, uint16_t length); 30 | NimBLEService* m_pDisService{nullptr}; 31 | }; 32 | 33 | #endif // NIMBLE_DIS_H_ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ryan Powell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NimBLEOta.h: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Ryan Powell and NimBLEOta contributors 2 | // Sponsored by Theengs https://www.theengs.io 3 | // MIT License 4 | 5 | #ifndef NIMBLE_OTA_H_ 6 | #define NIMBLE_OTA_H_ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | class NimBLEOtaCallbacks; 13 | struct ble_npl_callout; 14 | 15 | /** 16 | * @brief A model of the BLE OTA Service 17 | */ 18 | class NimBLEOta { 19 | public: 20 | NimBLEOta() = default; 21 | NimBLEService* start(NimBLEOtaCallbacks* pCallbacks = nullptr, bool secure = false); 22 | void abortUpdate(); 23 | bool startAbortTimer(uint32_t seconds); 24 | void stopAbortTimer(); 25 | bool isInProgress() const { return m_inProgress; }; 26 | NimBLEUUID getServiceUUID() const; 27 | 28 | enum Reason { 29 | StartCmd, 30 | StopCmd, 31 | Disconnected, 32 | Reconnected, 33 | FlashError, 34 | LengthError, 35 | }; 36 | 37 | private: 38 | static void abortTimerCb(ble_npl_event* event); 39 | 40 | class NimBLEOtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { 41 | public: 42 | NimBLEOtaCharacteristicCallbacks(NimBLEOta* pNimBLEOta) : m_pOta(pNimBLEOta) {} 43 | void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override; 44 | void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override; 45 | void commandOnWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo); 46 | void firmwareOnWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo); 47 | 48 | private: 49 | uint16_t getCrc16(const uint8_t* buf, int len); 50 | NimBLEOta* m_pOta{nullptr}; 51 | } m_charCallbacks{this}; 52 | 53 | NimBLEOtaCallbacks* m_pCallbacks{nullptr}; 54 | ble_npl_callout m_otaCallout{}; 55 | NimBLEAddress m_clientAddr{}; 56 | esp_ota_handle_t m_writeHandle{}; 57 | esp_partition_t m_partition{}; 58 | uint32_t m_fileLen{}; 59 | uint32_t m_recvLen{}; 60 | uint8_t* m_pBuf{nullptr}; 61 | uint16_t m_sector{}; 62 | uint16_t m_offset{}; 63 | uint8_t m_packet{}; 64 | bool m_inProgress{false}; 65 | }; 66 | 67 | class NimBLEOtaCallbacks { 68 | public: 69 | virtual ~NimBLEOtaCallbacks() = default; 70 | virtual void onStart(NimBLEOta* ota, uint32_t firmwareSize, NimBLEOta::Reason reason); 71 | virtual void onProgress(NimBLEOta* ota, uint32_t current, uint32_t total); 72 | virtual void onStop(NimBLEOta* ota, NimBLEOta::Reason reason); 73 | virtual void onComplete(NimBLEOta* ota); 74 | virtual void onError(NimBLEOta* ota, esp_err_t err, NimBLEOta::Reason reason); 75 | }; 76 | 77 | #endif // NIMBLE_OTA_H_ -------------------------------------------------------------------------------- /examples/basic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | NimBLEOta bleOta; 7 | NimBLEDis bleDis; 8 | 9 | class NimBleOtaServerCallbacks : public NimBLEServerCallbacks { 10 | void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { Serial.println("Client connected"); } 11 | 12 | void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { 13 | Serial.println("Client disconnected"); 14 | } 15 | } bleOtaServerCallbacks; 16 | 17 | class OtaCallbacks : public NimBLEOtaCallbacks { 18 | void onStart(NimBLEOta* ota, uint32_t firmwareSize, NimBLEOta::Reason reason) override { 19 | if (reason == NimBLEOta::StartCmd) { 20 | Serial.printf("Start OTA with firmware size: %d\n", firmwareSize); 21 | return; 22 | } 23 | 24 | if (reason == NimBLEOta::Reconnected) { 25 | Serial.println("Reconnected, resuming OTA Update"); 26 | ota->stopAbortTimer(); 27 | return; 28 | } 29 | 30 | Serial.printf("OTA start, firmware size: %d, Reason: %u\n", firmwareSize, reason); 31 | } 32 | 33 | void onProgress(NimBLEOta* ota, uint32_t current, uint32_t total) override { 34 | Serial.printf("OTA progress: %.1f%%, cur: %u, tot: %u\n", static_cast(current) / total * 100, current, total); 35 | } 36 | 37 | void onStop(NimBLEOta* ota, NimBLEOta::Reason reason) override { 38 | if (reason == NimBLEOta::Disconnected) { 39 | Serial.println("OTA stopped, client disconnected"); 40 | ota->startAbortTimer(30); // abort if client does not restart ota in 30 seconds 41 | return; 42 | } 43 | 44 | if (reason == NimBLEOta::StopCmd) { 45 | Serial.println("OTA stopped by command - aborting"); 46 | ota->abortUpdate(); 47 | return; 48 | } 49 | 50 | Serial.printf("OTA stopped, Reason: %u\n", reason); 51 | } 52 | 53 | void onComplete(NimBLEOta* ota) override { 54 | Serial.println("OTA update complete - restarting in 2 seconds"); 55 | delay(2000); 56 | ESP.restart(); 57 | } 58 | 59 | void onError(NimBLEOta* ota, esp_err_t err, NimBLEOta::Reason reason) override { 60 | Serial.printf("OTA error: %d\n", err); 61 | if (reason == NimBLEOta::FlashError) { 62 | Serial.println("Flash error, aborting OTA update"); 63 | ota->abortUpdate(); 64 | } 65 | } 66 | } otaCallbacks; 67 | 68 | void setup() { 69 | Serial.begin(115200); 70 | NimBLEDevice::init("NIMBLE OTA"); 71 | NimBLEDevice::setMTU(517); 72 | NimBLEServer* pServer = NimBLEDevice::createServer(); 73 | pServer->setCallbacks(&bleOtaServerCallbacks); 74 | 75 | bleDis.init(); 76 | bleDis.setManufacturerName("NimBLE-DIS"); 77 | bleDis.setModelNumber("NimBLE-DIS"); 78 | bleDis.setFirmwareRevision("1.0.0"); 79 | bleDis.setHardwareRevision("1.0.0"); 80 | bleDis.setSoftwareRevision("1.0.0"); 81 | bleDis.setSystemId("1.0.0"); 82 | bleDis.setPnp(0x01, 0x02, 0x03, 0x04); 83 | 84 | bleDis.start(); 85 | bleOta.start(&otaCallbacks); 86 | 87 | NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); 88 | pAdvertising->addServiceUUID(bleOta.getServiceUUID()); 89 | pAdvertising->start(); 90 | Serial.println("OTA service started"); 91 | } 92 | 93 | void loop() {} 94 | -------------------------------------------------------------------------------- /examples/secure.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | NimBLEOta bleOta; 7 | NimBLEDis bleDis; 8 | 9 | class NimBleOtaServerCallbacks : public NimBLEServerCallbacks { 10 | void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { Serial.println("Client connected"); } 11 | 12 | void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { 13 | Serial.println("Client disconnected"); 14 | } 15 | 16 | uint32_t onPassKeyDisplay() override { return 123456; } 17 | 18 | } bleOtaServerCallbacks; 19 | 20 | class OtaCallbacks : public NimBLEOtaCallbacks { 21 | void onStart(NimBLEOta* ota, uint32_t firmwareSize, NimBLEOta::Reason reason) override { 22 | if (reason == NimBLEOta::StartCmd) { 23 | Serial.printf("Start OTA with firmware size: %d\n", firmwareSize); 24 | return; 25 | } 26 | 27 | if (reason == NimBLEOta::Reconnected) { 28 | Serial.println("Reconnected, resuming OTA Update"); 29 | ota->stopAbortTimer(); 30 | return; 31 | } 32 | 33 | Serial.printf("OTA start, firmware size: %d, Reason: %u\n", firmwareSize, reason); 34 | } 35 | 36 | void onProgress(NimBLEOta* ota, uint32_t current, uint32_t total) override { 37 | Serial.printf("OTA progress: %.1f%%, cur: %u, tot: %u\n", static_cast(current) / total * 100, current, total); 38 | } 39 | 40 | void onStop(NimBLEOta* ota, NimBLEOta::Reason reason) override { 41 | if (reason == NimBLEOta::Disconnected) { 42 | Serial.println("OTA stopped, client disconnected"); 43 | ota->startAbortTimer(30); // abort if client does not restart ota in 30 seconds 44 | return; 45 | } 46 | 47 | if (reason == NimBLEOta::StopCmd) { 48 | Serial.println("OTA stopped by command - aborting"); 49 | ota->abortUpdate(); 50 | return; 51 | } 52 | 53 | Serial.printf("OTA stopped, Reason: %u\n", reason); 54 | } 55 | 56 | void onComplete(NimBLEOta* ota) override { 57 | Serial.println("OTA update complete - restarting in 2 seconds"); 58 | delay(2000); 59 | ESP.restart(); 60 | } 61 | 62 | void onError(NimBLEOta* ota, esp_err_t err, NimBLEOta::Reason reason) override { 63 | Serial.printf("OTA error: %d\n", err); 64 | if (reason == NimBLEOta::FlashError) { 65 | Serial.println("Flash error, aborting OTA update"); 66 | ota->abortUpdate(); 67 | } 68 | } 69 | } otaCallbacks; 70 | 71 | void setup() { 72 | Serial.begin(115200); 73 | NimBLEDevice::init("NIMBLE OTA"); 74 | NimBLEDevice::setMTU(517); 75 | NimBLEServer* pServer = NimBLEDevice::createServer(); 76 | pServer->setCallbacks(&bleOtaServerCallbacks); 77 | 78 | bleDis.init(); 79 | bleDis.setManufacturerName("NimBLE-DIS"); 80 | bleDis.setModelNumber("NimBLE-DIS"); 81 | bleDis.setFirmwareRevision("1.0.0"); 82 | bleDis.setHardwareRevision("1.0.0"); 83 | bleDis.setSoftwareRevision("1.0.0"); 84 | bleDis.setSystemId("1.0.0"); 85 | bleDis.setPnp(0x01, 0x02, 0x03, 0x04); 86 | bleDis.start(); 87 | 88 | // Use passkey authentication 89 | NimBLEDevice::setSecurityAuth(false, true, true); 90 | NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); 91 | bleOta.start(&otaCallbacks, true); 92 | 93 | NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); 94 | pAdvertising->addServiceUUID(bleOta.getServiceUUID()); 95 | pAdvertising->start(); 96 | Serial.println("OTA service started"); 97 | } 98 | 99 | void loop() {} 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NimBLE OTA 2 | 3 | `NimBLE OTA` is based on the [Espressif ble ota component](https://components.espressif.com/components/espressif/ble_ota/versions/0.1.12) which will update the firmware of an esp32 device Over The Air via Bluetooth Low Energy. 4 | 5 | This creates an OTA Service with the UUID `0x8018` and an optional [Device Information Service.](https://www.bluetooth.com/specifications/specs/device-information-service-1-1/) 6 | 7 | ## Sponsored by 8 | 9 | 10 | 11 | ## Dependencies 12 | 13 | [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) Version 2.1 or higher. Or [esp-nimble-cpp](https://github.com/h2zero/esp-nimble-cpp) Version 2.0 or higher. 14 | 15 | ## Supported MCU's 16 | 17 | Espressif esp32 devices with Bluetooth capability. 18 | 19 | ## Using 20 | ``` 21 | #include // include the header in your source file 22 | 23 | static NimBLEOta bleOta; // Create an instance of NimBLEOta 24 | 25 | void setup() { 26 | NimBLEDevice::init("NimBLE OTA"); // Initialize NimBLE 27 | bleOta.start(); // start the service 28 | } 29 | ``` 30 | 31 | If you want to have callbacks to your application to receive information about BLE OTA operations you can pass an optional pointer to your callback class which must be derived from `NimBLEOtaCallbacks`. 32 | ``` 33 | class OtaCallbacks : public NimBLEOtaCallbacks { 34 | } otaCallbacks; 35 | 36 | void setup() { 37 | NimBLEDevice::init("NimBLE OTA"); // Initialize NimBLE 38 | bleOta.start(&otaCallbacks); // start the service with callbacks 39 | } 40 | ``` 41 | 42 | Check out the examples for more detail. 43 | 44 | ### Security 45 | 46 | If you want to enable security you should initialize the NimBLE security options before calling `bleOta.start()`, you must enable man in the middle protection and use a passkey like so: 47 | ``` 48 | // Use passkey authentication 49 | NimBLEDevice::setSecurityAuth(false, true, true); 50 | NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); 51 | ``` 52 | 53 | You can now start NimBLEOta with security enabled by pass `true` as the second parameter to `start()`, i.e. `bleOta.start(&otaCallbacks, true);` 54 | 55 | ## Uploading OTA 56 | 57 | For best results use the included [python script.](scripts\nimbleota.py) 58 | This also works with [BLEOTA_WEBAPP](https://gb88.github.io/BLEOTA/) by @gb88. 59 | 60 | ## 1. How it works 61 | 62 | - `OTA Service`: It is used for OTA upgrade and contains 4 characteristics, as shown in the following table: 63 | 64 | | Characteristics | UUID | Prop | description | 65 | | ---- | ---- | ---- | ---- | 66 | | RECV_FW_CHAR | 0x8020 | Write, notify | Firmware received, send ACK | 67 | | PROGRESS_BAR_CHAR | 0x8021 | Read, notify | Read the progress bar and report the progress bar | 68 | | COMMAND_CHAR | 0x8022 | Write, notify | Send the command and ACK | 69 | | CUSTOMER_CHAR | 0x8023 | Write, notify | User-defined data to send and receive | 70 | 71 | ## 2. Data transmission 72 | 73 | ### 2.1 Command package format 74 | 75 | | unit | Command_ID | PayLoad | CRC16 | 76 | | ---- | ---- | ---- | ---- | 77 | | Byte | Byte: 0 ~ 1 | Byte: 2 ~ 17 | Byte: 18 ~ 19 | 78 | 79 | Command_ID: 80 | 81 | - 0x0001: Start 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). 82 | - 0x0002: Stop OTA, and the remaining Payload will be set to 0. CRC16 calculates bytes(0 to 17). 83 | - 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. Other payloads are set to 0. CRC16 computes bytes(0 to 17). 84 | 85 | ### 2.2 Firmware package format 86 | 87 | The format of the firmware package sent by the client is as follows: 88 | 89 | | unit | Sector_Index | Packet_Seq | PayLoad | 90 | | ---- | ---- | ---- | ---- | 91 | | Byte | Byte: 0 ~ 1 | Byte: 2 | Byte: 3 ~ (MTU_size - 4) | 92 | 93 | - 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. 94 | - 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. 95 | 96 | The format of the reply packet is as follows: 97 | 98 | | unit | Sector_Index | ACK_Status | CRC6 | 99 | | ---- | ---- | ---- | ---- | 100 | | Byte | Byte: 0 ~ 1 | Byte: 2 ~ 3 | Byte: 18 ~ 19 | 101 | 102 | ACK_Status: 103 | 104 | - 0x0000: Success 105 | - 0x0001: CRC error 106 | - 0x0002: Sector_Index error, bytes(4 ~ 5) indicates the desired Sector_Index 107 | - 0x0003:Payload length error 108 | -------------------------------------------------------------------------------- /NimBLEDis.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Ryan Powell and NimBLEOta contributors 2 | // Sponsored by Theengs https://www.theengs.io 3 | // MIT License 4 | 5 | #include "NimBLEDevice.h" 6 | #include "NimBLEDis.h" 7 | #include "NimBLELog.h" 8 | 9 | #define BLE_DIS_SERVICE_UUID (uint16_t)0x180a 10 | #define BLE_DIS_SYSTEM_ID_CHR_UUID (uint16_t)0x2A23 11 | #define BLE_DIS_MODEL_NUMBER_CHR_UUID (uint16_t)0x2A24 12 | #define BLE_DIS_SERIAL_NUMBER_CHR_UUID (uint16_t)0x2A25 13 | #define BLE_DIS_FIRMWARE_REVISION_CHR_UUID (uint16_t)0x2A26 14 | #define BLE_DIS_HARDWARE_REVISION_CHR_UUID (uint16_t)0x2A27 15 | #define BLE_DIS_SOFTWARE_REVISION_CHR_UUID (uint16_t)0x2A28 16 | #define BLE_DIS_MANUFACTURER_NAME_CHR_UUID (uint16_t)0x2A29 17 | #define BLE_DIS_REG_CERT_CHR_UUID (uint16_t)0x2A2A 18 | #define BLE_DIS_PNP_ID_CHR_UUID (uint16_t)0x2A50 19 | 20 | static const char* LOG_TAG = "NimBLEDis"; 21 | 22 | bool NimBLEDis::createDisChar(const NimBLEUUID& uuid, const uint8_t* value, uint16_t length) { 23 | if (!m_pDisService) { 24 | NIMBLE_LOGE(LOG_TAG, "Device Information Service not initialized"); 25 | return false; 26 | } 27 | 28 | if (m_pDisService->getCharacteristic(uuid)) { 29 | NIMBLE_LOGE(LOG_TAG, 30 | "%s - Characteristic value already set", 31 | NimBLEUUID(BLE_DIS_MODEL_NUMBER_CHR_UUID).toString().c_str()); 32 | return false; 33 | } 34 | 35 | NimBLECharacteristic* pChar = m_pDisService->createCharacteristic(uuid, NIMBLE_PROPERTY::READ, length); 36 | if (!pChar) { 37 | NIMBLE_LOGE(LOG_TAG, "Failed to create characteristic"); 38 | return false; 39 | } 40 | 41 | pChar->setValue(value, length); 42 | if (memcmp(pChar->getValue(), value, length) != 0) { 43 | NIMBLE_LOGE(LOG_TAG, "Failed to set characteristic value"); 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | bool NimBLEDis::setModelNumber(const char* value) { 51 | if (createDisChar(BLE_DIS_MODEL_NUMBER_CHR_UUID, reinterpret_cast(value), strlen(value))) { 52 | NIMBLE_LOGI(LOG_TAG, "Model Number set to: %s", value); 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | bool NimBLEDis::setSerialNumber(const char* value) { 59 | if (createDisChar(BLE_DIS_SERIAL_NUMBER_CHR_UUID, reinterpret_cast(value), strlen(value))) { 60 | NIMBLE_LOGI(LOG_TAG, "Serial Number set to: %s", value); 61 | } 62 | return false; 63 | } 64 | 65 | bool NimBLEDis::setFirmwareRevision(const char* value) { 66 | if (createDisChar(BLE_DIS_FIRMWARE_REVISION_CHR_UUID, reinterpret_cast(value), strlen(value))) { 67 | NIMBLE_LOGI(LOG_TAG, "Firmware Revision set to: %s", value); 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | bool NimBLEDis::setHardwareRevision(const char* value) { 74 | if (createDisChar(BLE_DIS_HARDWARE_REVISION_CHR_UUID, reinterpret_cast(value), strlen(value))) { 75 | NIMBLE_LOGI(LOG_TAG, "Hardware Revision set to: %s", value); 76 | return true; 77 | } 78 | return false; 79 | } 80 | 81 | bool NimBLEDis::setSoftwareRevision(const char* value) { 82 | if (createDisChar(BLE_DIS_SOFTWARE_REVISION_CHR_UUID, reinterpret_cast(value), strlen(value))) { 83 | NIMBLE_LOGI(LOG_TAG, "Software Revision set to: %s", value); 84 | return true; 85 | } 86 | return false; 87 | } 88 | 89 | bool NimBLEDis::setManufacturerName(const char* value) { 90 | if (createDisChar(BLE_DIS_MANUFACTURER_NAME_CHR_UUID, reinterpret_cast(value), strlen(value))) { 91 | NIMBLE_LOGI(LOG_TAG, "Manufacturer Name set to: %s", value); 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | bool NimBLEDis::setSystemId(const char* value) { 98 | if (createDisChar(BLE_DIS_SYSTEM_ID_CHR_UUID, reinterpret_cast(value), strlen(value))) { 99 | NIMBLE_LOGI(LOG_TAG, "System ID set to: %s", value); 100 | return true; 101 | } 102 | return false; 103 | } 104 | 105 | bool NimBLEDis::setPnp(uint8_t src, uint16_t vid, uint16_t pid, uint16_t ver) { 106 | uint8_t pnp[] = {src, 107 | static_cast(vid & 0xFF), 108 | static_cast((vid >> 8) & 0xFF), 109 | static_cast(pid & 0xFF), 110 | static_cast((pid >> 8) & 0xFF), 111 | static_cast(ver & 0xFF), 112 | static_cast((ver >> 8) & 0xFF)}; 113 | 114 | if (createDisChar(BLE_DIS_PNP_ID_CHR_UUID, pnp, sizeof(pnp))) { 115 | NIMBLE_LOGI(LOG_TAG, "PNP ID set to: %02x:%04x:%04x:%04x", src, vid, pid, ver); 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | bool NimBLEDis::init() { 122 | NimBLEServer* pServer = NimBLEDevice::createServer(); 123 | if (!pServer) { 124 | NIMBLE_LOGE(LOG_TAG, "Failed to get server"); 125 | return false; 126 | } 127 | 128 | m_pDisService = pServer->createService(BLE_DIS_SERVICE_UUID); 129 | if (!m_pDisService) { 130 | NIMBLE_LOGE(LOG_TAG, "Failed to create service"); 131 | return false; 132 | } 133 | 134 | NIMBLE_LOGD(LOG_TAG, "Device Information Service created"); 135 | return true; 136 | } 137 | 138 | bool NimBLEDis::start() { 139 | if (m_pDisService != nullptr) { 140 | return m_pDisService->start(); 141 | } 142 | 143 | NIMBLE_LOGE(LOG_TAG, "Device Information Service not initialized"); 144 | return false; 145 | } 146 | -------------------------------------------------------------------------------- /scripts/nimbleota.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Ryan Powell and NimBLEOta contributors 2 | # Sponsored by Theengs https://www.theengs.io, https://github.com/theengs 3 | # MIT License 4 | 5 | import asyncio 6 | import argparse 7 | import os 8 | import sys 9 | from bleak import BleakScanner, uuids, BleakClient 10 | 11 | OTA_SERVICE_UUID = uuids.normalize_uuid_16(0x8018) 12 | OTA_COMMAND_UUID = uuids.normalize_uuid_16(0x8022) 13 | OTA_FIRMWARE_UUID = uuids.normalize_uuid_16(0x8020) 14 | START_COMMAND = 0x0001 15 | STOP_COMMAND = 0x0002 16 | ACK_COMMAND = 0x0003 17 | ACK_ACCEPTED = 0x0000 18 | ACK_REJECTED = 0x0001 19 | FW_ACK_SUCCESS = 0x0000 20 | FW_ACK_CRC_ERROR = 0x0001 21 | FW_ACK_SECTOR_ERROR = 0x0002 22 | FW_ACK_LEN_ERROR = 0x0003 23 | RSP_CRC_ERROR = 0xFFFF 24 | 25 | def parse_args(): 26 | parser = argparse.ArgumentParser(description="OTA Update Script") 27 | parser.add_argument("file_name", nargs='?', help="The file name for the OTA update") 28 | parser.add_argument("mac_address", nargs='?', help="The MAC address of the device to connect to") 29 | return parser.parse_args() 30 | 31 | def crc16_ccitt(buf): 32 | crc16 = 0 33 | for byte in buf: 34 | crc16 ^= byte << 8 35 | for _ in range(8): 36 | if crc16 & 0x8000: 37 | crc16 = (crc16 << 1) ^ 0x1021 38 | else: 39 | crc16 = crc16 << 1 40 | crc16 &= 0xFFFF # Ensure crc16 remains a 16-bit value 41 | return crc16 42 | 43 | async def fw_notification_handler(sender, data, queue): 44 | if len(data) == 20: 45 | sector_sent = int.from_bytes(data[0:2], byteorder='little') 46 | status = int.from_bytes(data[2:4], byteorder='little') 47 | cur_sector = int.from_bytes(data[4:6], byteorder='little') 48 | crc = int.from_bytes(data[18:20], byteorder='little') 49 | # print(f"SECTOR_SENT: {sector_sent}") 50 | # print(f"STATUS: {status}") 51 | # print(f"CUR_SECTOR: {cur_sector}") 52 | 53 | if crc16_ccitt(data[0:18]) != crc: 54 | status = RSP_CRC_ERROR 55 | 56 | await queue.put((status, cur_sector)) 57 | 58 | async def cmd_notification_handler(sender, data, queue): 59 | if len(data) == 20: 60 | ack = int.from_bytes(data[0:2], byteorder='little') 61 | cmd = int.from_bytes(data[2:4], byteorder='little') 62 | rsp = int.from_bytes(data[4:6], byteorder='little') 63 | crc = int.from_bytes(data[18:20], byteorder='little') 64 | 65 | if crc16_ccitt(data[0:18]) != crc: 66 | print("Command response CRC error") 67 | rsp = RSP_CRC_ERROR 68 | 69 | await queue.put(rsp) 70 | 71 | async def upload_sector(client, sector, sec_idx): 72 | max_bytes = min(512, client.mtu_size - 3) - 3 # 3 bytes for the packet header, 3 bytes for the BLE overhead 73 | chunks = [sector[i:i+max_bytes] for i in range(0, len(sector), max_bytes)] 74 | for sequence, chunk in enumerate(chunks): 75 | if sequence == len(chunks) - 1: 76 | sequence = 0xFF # Indicate to peer this is the last chunk of sector 77 | 78 | data = sec_idx.to_bytes(2, byteorder='little') 79 | data += sequence.to_bytes(1, byteorder='little') 80 | data += chunk 81 | await client.write_gatt_char(OTA_FIRMWARE_UUID, data, response=False) 82 | 83 | async def connect_to_device(address, file_size, sectors): 84 | try: 85 | async with BleakClient(address) as client: 86 | print(f"Connected to {address}") 87 | queue = asyncio.Queue() 88 | await client.start_notify(OTA_COMMAND_UUID, lambda sender, 89 | data: asyncio.create_task(cmd_notification_handler(sender, data, queue))) 90 | print("Sending start command") 91 | command = bytearray(20) 92 | command[0:2] = START_COMMAND.to_bytes(2, byteorder='little') 93 | command[2:6] = file_size.to_bytes(4, byteorder='little') 94 | crc16 = crc16_ccitt(command[0:18]) 95 | command[18:20] = crc16.to_bytes(2, byteorder='little') 96 | while True: 97 | await client.write_gatt_char(OTA_COMMAND_UUID, command) 98 | ack = await queue.get() 99 | if ack != RSP_CRC_ERROR: 100 | break 101 | 102 | if ack == ACK_ACCEPTED: 103 | await client.start_notify(OTA_FIRMWARE_UUID, lambda sender, 104 | data: asyncio.create_task(fw_notification_handler(sender, data, queue))) 105 | print("Sending firmware...") 106 | sec_idx = 0 107 | sec_count = len(sectors) 108 | while sec_idx < sec_count: 109 | sector = sectors[sec_idx] 110 | print(f"Sector {sec_idx}: {len(sector)} bytes") 111 | await upload_sector(client, sector, 112 | sec_idx if len(sector) == 4098 else 0xFFFF) # send last sector as 0xFFFF 113 | ack, rsp_sector = await queue.get() 114 | 115 | if ack == FW_ACK_SUCCESS: 116 | print(round(sec_idx / (sec_count - 1) * 100, 1), '% complete') 117 | if sec_idx == sec_count - 1: 118 | print("OTA update complete") 119 | await client.disconnect() 120 | sec_idx += 1 121 | continue 122 | 123 | if ack == FW_ACK_CRC_ERROR or ack == FW_ACK_LEN_ERROR or ack == RSP_CRC_ERROR: 124 | print("Length Error" if ack == FW_ACK_LEN_ERROR else "CRC Error", "- Retrying sector") 125 | 126 | elif ack == FW_ACK_SECTOR_ERROR: 127 | print(f"Sector Error, sending sector: {rsp_sector}") 128 | sec_idx = rsp_sector 129 | 130 | else: 131 | print("Unknown error") 132 | await client.disconnect() 133 | break 134 | else: 135 | print("Start command rejected") 136 | await client.disconnect() 137 | 138 | except Exception as e: 139 | print(f"{e}") 140 | 141 | async def main(): 142 | devices = [] 143 | 144 | def detection_callback(device, advertisement_data): 145 | if device.address not in [d.address for d in devices]: 146 | print(f"Detected device: {device.name} - {device.address}") 147 | devices.append(device) 148 | try: 149 | args = parse_args() 150 | file_name = args.file_name 151 | mac_address = args.mac_address 152 | 153 | if not file_name: 154 | file_name = input("Enter the file name for the OTA update: ") 155 | 156 | if not os.path.isfile(file_name): 157 | print('Invalid file %s' % (file_name)) 158 | sys.exit() 159 | 160 | file_size = os.path.getsize(file_name) & 0xFFFFFFFF 161 | if not file_size: 162 | print('Invalid file size %d' % (file_size)) 163 | sys.exit() 164 | 165 | sectors = [] 166 | with open(file_name, 'rb') as file: 167 | while True: 168 | sector = file.read(4096) 169 | if not sector: 170 | break 171 | sector += crc16_ccitt(sector).to_bytes(2, byteorder='little') 172 | sectors.append(sector) 173 | 174 | if not mac_address: 175 | async with BleakScanner(detection_callback, [OTA_SERVICE_UUID]): 176 | print("Scanning for devices...") 177 | await asyncio.sleep(5) 178 | for dev_num, device in enumerate(devices): 179 | print(f"Option {dev_num + 1}: {device.name} - {device.address}") 180 | 181 | if not devices: 182 | print("No devices found") 183 | return 184 | 185 | while True: 186 | dev_num = input("Enter the device number to connect to: ") 187 | try: 188 | dev_num = int(dev_num) 189 | if dev_num < 1 or dev_num > len(devices): 190 | print("Invalid device number") 191 | continue 192 | else: 193 | break 194 | except ValueError: 195 | print("Invalid input, please enter a number") 196 | continue 197 | 198 | device = devices[dev_num - 1] # Adjust for 0-based index 199 | print(f"Selected: {device.name} - {device.address}") 200 | mac_address = device.address 201 | 202 | await connect_to_device(mac_address, file_size, sectors) 203 | 204 | except: 205 | sys.exit(0) 206 | 207 | asyncio.run(main()) 208 | -------------------------------------------------------------------------------- /NimBLEOta.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Ryan Powell and NimBLEOta contributors 2 | // Sponsored by Theengs https://www.theengs.io 3 | // MIT License 4 | 5 | #include "NimBLEOta.h" 6 | #include "NimBLEDevice.h" 7 | #include "NimBLELog.h" 8 | 9 | #define CMD_ACK_LENGTH 20 10 | #define FW_ACK_LENGTH 20 11 | #define OTA_BLOCK_SIZE 4098 12 | 13 | static constexpr uint16_t otaServiceUuid = 0x8018; 14 | static constexpr uint16_t recvFwUuid = 0x8020; 15 | static constexpr uint16_t otaBarUuid = 0x8021; 16 | static constexpr uint16_t commandUuid = 0x8022; 17 | static constexpr uint16_t customerUuid = 0x8023; 18 | static constexpr uint16_t startOtaCmd = 0x0001; 19 | static constexpr uint16_t stopOtaCmd = 0x0002; 20 | static constexpr uint16_t ackOtaCmd = 0x0003; 21 | static constexpr uint16_t otaAccept = 0x0000; 22 | static constexpr uint16_t otaReject = 0x0001; 23 | static constexpr uint16_t signError = 0x0003; 24 | static constexpr uint16_t crcError = 0x0001; 25 | static constexpr uint16_t indexError = 0x0002; 26 | static constexpr uint16_t otaFwSuccess = 0x0000; 27 | static constexpr uint16_t lenError = 0x0003; 28 | static constexpr uint16_t startError = 0x0005; 29 | static const char* LOG_TAG = "NimBLEOta"; 30 | static NimBLEOtaCallbacks defaultCallbacks; 31 | 32 | extern "C" struct ble_npl_eventq* nimble_port_get_dflt_eventq(void); 33 | 34 | void NimBLEOta::NimBLEOtaCharacteristicCallbacks::firmwareOnWrite(NimBLECharacteristic* pCharacteristic, 35 | NimBLEConnInfo& connInfo) { 36 | if (!m_pOta->isInProgress()) { 37 | NIMBLE_LOGW(LOG_TAG, "ota not started"); 38 | return; 39 | } 40 | 41 | if (NimBLEAddress(connInfo.getIdAddress()) != m_pOta->m_clientAddr) { 42 | NIMBLE_LOGW(LOG_TAG, "Received write from unknown client - ignored"); 43 | return; 44 | } 45 | 46 | esp_err_t err = ESP_OK; 47 | NimBLEAttValue data = pCharacteristic->getValue(); 48 | auto dataLen = data.length(); 49 | uint16_t otaResp = otaFwSuccess; 50 | uint16_t crc = 0; 51 | uint32_t writeLen = 0; 52 | uint16_t recvSector = data[0] | (data[1] << 8); 53 | uint8_t fwAck[FW_ACK_LENGTH]{}; 54 | 55 | fwAck[0] = data[0]; 56 | fwAck[1] = data[1]; 57 | 58 | if (recvSector != m_pOta->m_sector) { 59 | if (recvSector == 0xffff) { 60 | NIMBLE_LOGD(LOG_TAG, "Last sector received"); 61 | } else { 62 | if (data[2] == 0xff) { // only send ack after last packet received due to write without response not waiting 63 | NIMBLE_LOGE(LOG_TAG, "Sector index error, expected: %u, received: %u", m_pOta->m_sector, recvSector); 64 | otaResp = indexError; 65 | goto SendAck; 66 | } 67 | 68 | return; 69 | } 70 | } 71 | 72 | if (data[2] != m_pOta->m_packet) { 73 | if (data[2] == 0xff) { 74 | NIMBLE_LOGD(LOG_TAG, "last packet"); 75 | dataLen -= 2; // in the last packet the last 2 bytes are crc 76 | } else { 77 | // There is no response for out of sequence packet error, will fail crc or length check 78 | NIMBLE_LOGE(LOG_TAG, "packet sequence error, cur: %" PRIu32 ", recv: %d", m_pOta->m_packet, data[2]); 79 | } 80 | } 81 | 82 | memcpy(m_pOta->m_pBuf + m_pOta->m_offset, data + 3, dataLen - 3); 83 | m_pOta->m_offset += dataLen - 3; 84 | 85 | NIMBLE_LOGD(LOG_TAG, 86 | "Sector:%" PRIu32 ", total length:%" PRIu32 ", length:%d", 87 | m_pOta->m_sector, 88 | m_pOta->m_offset, 89 | dataLen - 3); 90 | if (data[2] != 0xff) { // not last packet 91 | NIMBLE_LOGD(LOG_TAG, "waiting for next packet"); 92 | m_pOta->m_packet++; 93 | return; 94 | } 95 | 96 | writeLen = std::min(OTA_BLOCK_SIZE - 2, m_pOta->m_offset); 97 | if ((recvSector != 0xffff && (m_pOta->m_recvLen + writeLen) != m_pOta->m_fileLen) && 98 | m_pOta->m_offset != OTA_BLOCK_SIZE - 2) { 99 | NIMBLE_LOGE(LOG_TAG, "sector length error, received: %d bytes", m_pOta->m_offset); 100 | otaResp = lenError; 101 | goto SendAck; 102 | } 103 | 104 | crc = *reinterpret_cast(data + dataLen); 105 | if (crc != getCrc16(m_pOta->m_pBuf, m_pOta->m_offset)) { 106 | NIMBLE_LOGE(LOG_TAG, "crc error"); 107 | otaResp = crcError; 108 | goto SendAck; 109 | } 110 | 111 | err = esp_ota_write(m_pOta->m_writeHandle, static_cast(m_pOta->m_pBuf), writeLen); 112 | if (err != ESP_OK) { 113 | NIMBLE_LOGE(LOG_TAG, "esp_ota_write failed! err=0x%x", err); 114 | goto Done; 115 | } 116 | 117 | m_pOta->m_recvLen += writeLen; 118 | m_pOta->m_pCallbacks->onProgress(m_pOta, m_pOta->m_recvLen, m_pOta->m_fileLen); 119 | if (m_pOta->m_recvLen >= m_pOta->m_fileLen) { 120 | err = esp_ota_end(m_pOta->m_writeHandle); 121 | if (err != ESP_OK) { 122 | NIMBLE_LOGE(LOG_TAG, "esp_ota_end failed! err=0x%x", err); 123 | goto Done; 124 | } 125 | 126 | err = esp_ota_set_boot_partition(&m_pOta->m_partition); 127 | if (err != ESP_OK) { 128 | NIMBLE_LOGE(LOG_TAG, "esp_ota_set_boot_partition failed! err=0x%x", err); 129 | goto Done; 130 | } 131 | } 132 | 133 | SendAck: 134 | m_pOta->m_packet = 0; 135 | m_pOta->m_offset = 0; 136 | fwAck[2] = otaResp; 137 | fwAck[3] = (otaResp >> 8) & 0xff; 138 | fwAck[4] = m_pOta->m_sector; 139 | fwAck[5] = (m_pOta->m_sector >> 8) & 0xff; 140 | crc = getCrc16(fwAck, 18); 141 | fwAck[18] = crc & 0xff; 142 | fwAck[19] = (crc & 0xff00) >> 8; 143 | pCharacteristic->setValue(fwAck, FW_ACK_LENGTH); 144 | pCharacteristic->indicate(); 145 | 146 | if (otaResp == otaFwSuccess) { 147 | m_pOta->m_sector++; 148 | } 149 | 150 | if (m_pOta->m_recvLen < m_pOta->m_fileLen) { 151 | return; 152 | } 153 | 154 | Done: 155 | if (err == ESP_OK) { 156 | m_pOta->abortUpdate(); // Reset the OTA state 157 | m_pOta->m_pCallbacks->onComplete(m_pOta); 158 | } else { 159 | m_pOta->m_pCallbacks->onError(m_pOta, err, NimBLEOta::FlashError); 160 | } 161 | } 162 | 163 | void NimBLEOta::NimBLEOtaCharacteristicCallbacks::commandOnWrite(NimBLECharacteristic* pCharacteristic, 164 | NimBLEConnInfo& connInfo) { 165 | if (m_pOta->m_clientAddr.isNull()) { 166 | m_pOta->m_clientAddr = connInfo.getIdAddress(); 167 | } else if (connInfo.getIdAddress() != m_pOta->m_clientAddr) { 168 | NIMBLE_LOGW(LOG_TAG, "Received command from unknown client - ignored"); 169 | return; 170 | } 171 | 172 | uint16_t crc = 0; 173 | uint8_t cmdAck[CMD_ACK_LENGTH]{}; 174 | cmdAck[0] = ackOtaCmd; 175 | cmdAck[1] = (ackOtaCmd >> 8) & 0xff; 176 | cmdAck[4] = otaReject; 177 | cmdAck[5] = (otaReject >> 8) & 0xff; 178 | 179 | if (pCharacteristic->getValue().length() == 20) { 180 | NimBLEAttValue data = pCharacteristic->getValue(); 181 | uint16_t cmd = data[0] | (data[1] << 8); 182 | crc = data[18] | (data[19] << 8); 183 | cmdAck[2] = data[0]; 184 | cmdAck[3] = data[1]; 185 | 186 | for (int i = 0; i < 20; i++) { 187 | printf("%02x ", data[i]); 188 | } 189 | printf("\n"); 190 | 191 | if (getCrc16(data, 18) != crc || (cmd != startOtaCmd && cmd != stopOtaCmd)) { 192 | NIMBLE_LOGE(LOG_TAG, "command %s error", cmd == startOtaCmd || cmd == stopOtaCmd ? "CRC" : "invalid"); 193 | } else if (cmd == startOtaCmd) { 194 | if (m_pOta->isInProgress()) { 195 | if (*reinterpret_cast(data + 2) == m_pOta->m_fileLen) { 196 | NIMBLE_LOGW(LOG_TAG, "Ota resuming"); 197 | m_pOta->m_pCallbacks->onStart(m_pOta, m_pOta->m_fileLen, NimBLEOta::Reconnected); 198 | cmdAck[4] = otaAccept; 199 | cmdAck[5] = (otaAccept >> 8) & 0xff; 200 | } else { 201 | NIMBLE_LOGE(LOG_TAG, "Ota command error, file length mismatch - aborting"); 202 | m_pOta->abortUpdate(); 203 | m_pOta->m_pCallbacks->onError(m_pOta, ESP_FAIL, NimBLEOta::LengthError); 204 | } 205 | } else { 206 | m_pOta->m_fileLen = *reinterpret_cast(data + 2); 207 | m_pOta->m_pBuf = static_cast(malloc(OTA_BLOCK_SIZE * sizeof(uint8_t))); 208 | if (m_pOta->m_pBuf == nullptr) { 209 | NIMBLE_LOGE(LOG_TAG, "%s - malloc fail", __func__); 210 | goto SendAck; 211 | } else { 212 | memset(m_pOta->m_pBuf, 0x0, OTA_BLOCK_SIZE); 213 | } 214 | 215 | const esp_partition_t* partition_ptr = esp_ota_get_boot_partition(); 216 | if (partition_ptr == NULL) { 217 | NIMBLE_LOGE(LOG_TAG, "boot partition NULL!\r\n"); 218 | goto SendAck; 219 | } 220 | 221 | if (partition_ptr->type != ESP_PARTITION_TYPE_APP) { 222 | NIMBLE_LOGE(LOG_TAG, "esp_current_partition->type != ESP_PARTITION_TYPE_APP\r\n"); 223 | goto SendAck; 224 | } 225 | 226 | if (partition_ptr->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY) { 227 | m_pOta->m_partition.subtype = ESP_PARTITION_SUBTYPE_APP_OTA_0; 228 | } else { 229 | const esp_partition_t* next_partition = esp_ota_get_next_update_partition(partition_ptr); 230 | if (next_partition) { 231 | m_pOta->m_partition.subtype = next_partition->subtype; 232 | } else { 233 | m_pOta->m_partition.subtype = ESP_PARTITION_SUBTYPE_APP_OTA_0; 234 | } 235 | } 236 | m_pOta->m_partition.type = ESP_PARTITION_TYPE_APP; 237 | 238 | partition_ptr = esp_partition_find_first(m_pOta->m_partition.type, m_pOta->m_partition.subtype, NULL); 239 | if (partition_ptr == nullptr) { 240 | NIMBLE_LOGE(LOG_TAG, "partition NULL!\r\n"); 241 | goto SendAck; 242 | } 243 | 244 | memcpy(&m_pOta->m_partition, partition_ptr, sizeof(esp_partition_t)); 245 | if (esp_ota_begin(&m_pOta->m_partition, OTA_SIZE_UNKNOWN, &m_pOta->m_writeHandle) != ESP_OK) { 246 | NIMBLE_LOGE(LOG_TAG, "esp_ota_begin failed!\r\n"); 247 | goto SendAck; 248 | } 249 | 250 | cmdAck[4] = otaAccept; 251 | cmdAck[5] = (otaAccept >> 8) & 0xff; 252 | m_pOta->m_inProgress = true; 253 | m_pOta->m_pCallbacks->onStart(m_pOta, m_pOta->m_fileLen, NimBLEOta::StartCmd); 254 | } 255 | } else if (cmd == stopOtaCmd) { 256 | if (!m_pOta->isInProgress()) { 257 | NIMBLE_LOGW(LOG_TAG, "ota not started"); 258 | } else { 259 | cmdAck[4] = otaAccept; 260 | cmdAck[5] = (otaAccept >> 8) & 0xff; 261 | m_pOta->m_pCallbacks->onStop(m_pOta, NimBLEOta::StopCmd); 262 | } 263 | } else { 264 | NIMBLE_LOGE(LOG_TAG, "Unknown Command"); 265 | } 266 | } else { 267 | NIMBLE_LOGE(LOG_TAG, "command length error"); 268 | } 269 | 270 | SendAck: 271 | crc = getCrc16(cmdAck, 18); 272 | cmdAck[18] = crc; 273 | cmdAck[19] = (crc >> 8) & 0xff; 274 | pCharacteristic->setValue(cmdAck, CMD_ACK_LENGTH); 275 | pCharacteristic->indicate(); 276 | } 277 | 278 | void NimBLEOta::NimBLEOtaCharacteristicCallbacks::onSubscribe(NimBLECharacteristic* pChar, 279 | NimBLEConnInfo& connInfo, 280 | uint16_t subValue) { 281 | NIMBLE_LOGI(LOG_TAG, "Ota client conn_handle: %d, subscribed: %s", connInfo.getConnHandle(), subValue ? "true" : "false"); 282 | if (m_pOta->isInProgress() && m_pOta->m_clientAddr == connInfo.getIdAddress() && pChar->getUUID().equals(commandUuid)) { 283 | if (!subValue) { // client disconnected 284 | m_pOta->m_pCallbacks->onStop(m_pOta, NimBLEOta::Disconnected); 285 | } 286 | } 287 | } 288 | 289 | void NimBLEOta::NimBLEOtaCharacteristicCallbacks::onWrite(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) { 290 | if (pChar->getUUID().equals(commandUuid)) { 291 | commandOnWrite(pChar, connInfo); 292 | } else if (pChar->getUUID().equals(recvFwUuid)) { 293 | firmwareOnWrite(pChar, connInfo); 294 | } 295 | } 296 | 297 | NimBLEService* NimBLEOta::start(NimBLEOtaCallbacks* pCallbacks, bool secure) { 298 | m_pCallbacks = pCallbacks; 299 | if (m_pCallbacks == nullptr) { 300 | m_pCallbacks = &defaultCallbacks; 301 | } 302 | 303 | ble_npl_callout_init(&m_otaCallout, nimble_port_get_dflt_eventq(), NimBLEOta::abortTimerCb, this); 304 | 305 | NimBLEService* pService = NimBLEDevice::createServer()->createService(otaServiceUuid); 306 | uint32_t properties = NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::INDICATE; 307 | if (secure) { 308 | if (!ble_hs_cfg.sm_mitm || 309 | (ble_hs_cfg.sm_io_cap != BLE_HS_IO_DISPLAY_ONLY && ble_hs_cfg.sm_io_cap != BLE_HS_IO_KEYBOARD_DISPLAY)) { 310 | NIMBLE_LOGE(LOG_TAG, "Error: Security requested without passkey authentication enabled!"); 311 | } 312 | properties |= NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC; 313 | } 314 | 315 | NimBLECharacteristic* pRecvFwCharacteristic = pService->createCharacteristic(recvFwUuid, properties); 316 | pRecvFwCharacteristic->setCallbacks(&m_charCallbacks); 317 | 318 | NimBLECharacteristic* pCommandCharacteristic = pService->createCharacteristic(commandUuid, properties); 319 | pCommandCharacteristic->setCallbacks(&m_charCallbacks); 320 | 321 | /* TODO Customer Characteristic 322 | NimBLECharacteristic *pCustomerCharacteristic = pService->createCharacteristic(CUSTOMER_UUID, NIMBLE_PROPERTY::WRITE 323 | | NIMBLE_PROPERTY::INDICATE); pCustomerCharacteristic->setCallbacks(&m_charCallbacks); 324 | */ 325 | 326 | /* TODO OTA Bar Characteristic 327 | NimBLECharacteristic *pOtaBarCharacteristic = pService->createCharacteristic(OTA_BAR_UUID, NIMBLE_PROPERTY::WRITE | 328 | NIMBLE_PROPERTY::INDICATE); pOtaBarCharacteristic->setCallbacks(&m_charCallbacks); 329 | */ 330 | 331 | pService->start(); 332 | return pService; 333 | } 334 | 335 | NimBLEUUID NimBLEOta::getServiceUUID() const { 336 | return otaServiceUuid; 337 | } 338 | 339 | void NimBLEOta::abortUpdate() { 340 | if (m_pBuf != nullptr) { 341 | free(m_pBuf); 342 | m_pBuf = nullptr; 343 | } 344 | 345 | m_recvLen = 0; 346 | m_offset = 0; 347 | m_packet = 0; 348 | m_sector = 0; 349 | m_fileLen = 0; 350 | m_clientAddr = NimBLEAddress{}; 351 | m_inProgress = false; 352 | esp_ota_abort(m_writeHandle); 353 | } 354 | 355 | bool NimBLEOta::startAbortTimer(uint32_t seconds) { 356 | ble_npl_time_t ticks; 357 | ble_npl_time_ms_to_ticks(seconds * 1000, &ticks); 358 | return ble_npl_callout_reset(&m_otaCallout, ticks) == BLE_NPL_OK; 359 | } 360 | 361 | void NimBLEOta::stopAbortTimer() { 362 | ble_npl_callout_stop(&m_otaCallout); 363 | } 364 | 365 | void NimBLEOta::abortTimerCb(ble_npl_event* event) { 366 | NimBLEOta* pOta = static_cast(ble_npl_event_get_arg(event)); 367 | NIMBLE_LOGW(LOG_TAG, "Abort timer expired: aborting update!"); 368 | pOta->abortUpdate(); 369 | } 370 | 371 | uint16_t NimBLEOta::NimBLEOtaCharacteristicCallbacks::getCrc16(const uint8_t* buf, int len) { 372 | uint16_t crc = 0; 373 | int32_t i; 374 | 375 | while (len--) { 376 | crc ^= *buf++ << 8; 377 | 378 | for (i = 0; i < 8; i++) { 379 | if (crc & 0x8000) { 380 | crc = (crc << 1) ^ 0x1021; 381 | } else { 382 | crc = crc << 1; 383 | } 384 | } 385 | } 386 | 387 | return crc; 388 | } 389 | 390 | static const char* CB_LOG_TAG = "Default-NimBLEOtaCallbacks"; 391 | 392 | void NimBLEOtaCallbacks::onStart(NimBLEOta* ota, uint32_t firmwareSize, NimBLEOta::Reason reason) { 393 | NIMBLE_LOGI(CB_LOG_TAG, "OTA started, firmware size: %" PRIu32, "Reason: %u", firmwareSize, reason); 394 | } 395 | 396 | void NimBLEOtaCallbacks::onProgress(NimBLEOta* ota, uint32_t current, uint32_t total) { 397 | NIMBLE_LOGI(CB_LOG_TAG, "OTA progress: %.f%%", current / total * 100.f); 398 | } 399 | 400 | void NimBLEOtaCallbacks::onStop(NimBLEOta* ota, NimBLEOta::Reason reason) { 401 | NIMBLE_LOGI(CB_LOG_TAG, "OTA stopped, Reason: %u - aborting", reason); 402 | ota->abortUpdate(); 403 | } 404 | 405 | void NimBLEOtaCallbacks::onComplete(NimBLEOta* ota) { 406 | NIMBLE_LOGI(CB_LOG_TAG, "OTA complete - restarting in 2 seconds"); 407 | vTaskDelay(2000 / portTICK_PERIOD_MS); 408 | esp_restart(); 409 | } 410 | 411 | void NimBLEOtaCallbacks::onError(NimBLEOta* ota, esp_err_t err, NimBLEOta::Reason reason) { 412 | NIMBLE_LOGE(CB_LOG_TAG, "OTA error: 0x%x, Reason: %u - aborting", err, reason); 413 | ota->abortUpdate(); 414 | } --------------------------------------------------------------------------------