├── .gitignore ├── README.md ├── ble_client └── main.py └── esp ├── CMakeLists.txt ├── main ├── CMakeLists.txt ├── gap.c ├── gap.h ├── gatt_svr.c ├── gatt_svr.h └── main.c └── sdkconfig.defaults /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,c 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,c 3 | 4 | ## VSCode 5 | .vscode 6 | 7 | ## Pycharm 8 | .idea 9 | 10 | ## ESP 11 | sdkconfig 12 | *.old 13 | *.bin 14 | 15 | ### C ### 16 | # Prerequisites 17 | *.d 18 | 19 | # Object files 20 | *.o 21 | *.ko 22 | *.obj 23 | *.elf 24 | 25 | # Linker output 26 | *.ilk 27 | *.map 28 | *.exp 29 | 30 | # Precompiled Headers 31 | *.gch 32 | *.pch 33 | 34 | # Libraries 35 | *.lib 36 | *.a 37 | *.la 38 | *.lo 39 | 40 | # Shared objects (inc. Windows DLLs) 41 | *.dll 42 | *.so 43 | *.so.* 44 | *.dylib 45 | 46 | # Executables 47 | *.exe 48 | *.out 49 | *.app 50 | *.i*86 51 | *.x86_64 52 | *.hex 53 | 54 | # Debug files 55 | *.dSYM/ 56 | *.su 57 | *.idb 58 | *.pdb 59 | 60 | # Kernel Module Compile Results 61 | *.mod* 62 | *.cmd 63 | .tmp_versions/ 64 | modules.order 65 | Module.symvers 66 | Mkfile.old 67 | dkms.conf 68 | 69 | ### Python ### 70 | # Byte-compiled / optimized / DLL files 71 | __pycache__/ 72 | *.py[cod] 73 | *$py.class 74 | 75 | # C extensions 76 | 77 | # Distribution / packaging 78 | .Python 79 | build/ 80 | develop-eggs/ 81 | dist/ 82 | downloads/ 83 | eggs/ 84 | .eggs/ 85 | parts/ 86 | sdist/ 87 | var/ 88 | wheels/ 89 | pip-wheel-metadata/ 90 | share/python-wheels/ 91 | *.egg-info/ 92 | .installed.cfg 93 | *.egg 94 | MANIFEST 95 | 96 | # PyInstaller 97 | # Usually these files are written by a python script from a template 98 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 99 | *.manifest 100 | *.spec 101 | 102 | # Installer logs 103 | pip-log.txt 104 | pip-delete-this-directory.txt 105 | 106 | # Unit test / coverage reports 107 | htmlcov/ 108 | .tox/ 109 | .nox/ 110 | .coverage 111 | .coverage.* 112 | .cache 113 | nosetests.xml 114 | coverage.xml 115 | *.cover 116 | *.py,cover 117 | .hypothesis/ 118 | .pytest_cache/ 119 | pytestdebug.log 120 | 121 | # Translations 122 | *.mo 123 | *.pot 124 | 125 | # Django stuff: 126 | *.log 127 | local_settings.py 128 | db.sqlite3 129 | db.sqlite3-journal 130 | 131 | # Flask stuff: 132 | instance/ 133 | .webassets-cache 134 | 135 | # Scrapy stuff: 136 | .scrapy 137 | 138 | # Sphinx documentation 139 | docs/_build/ 140 | doc/_build/ 141 | 142 | # PyBuilder 143 | target/ 144 | 145 | # Jupyter Notebook 146 | .ipynb_checkpoints 147 | 148 | # IPython 149 | profile_default/ 150 | ipython_config.py 151 | 152 | # pyenv 153 | .python-version 154 | 155 | # pipenv 156 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 157 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 158 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 159 | # install all needed dependencies. 160 | #Pipfile.lock 161 | 162 | # poetry 163 | #poetry.lock 164 | 165 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 166 | __pypackages__/ 167 | 168 | # Celery stuff 169 | celerybeat-schedule 170 | celerybeat.pid 171 | 172 | # SageMath parsed files 173 | *.sage.py 174 | 175 | # Environments 176 | # .env 177 | .env/ 178 | .venv/ 179 | env/ 180 | venv/ 181 | ENV/ 182 | env.bak/ 183 | venv.bak/ 184 | pythonenv* 185 | 186 | # Spyder project settings 187 | .spyderproject 188 | .spyproject 189 | 190 | # Rope project settings 191 | .ropeproject 192 | 193 | # mkdocs documentation 194 | /site 195 | 196 | # mypy 197 | .mypy_cache/ 198 | .dmypy.json 199 | dmypy.json 200 | 201 | # Pyre type checker 202 | .pyre/ 203 | 204 | # pytype static type analyzer 205 | .pytype/ 206 | 207 | # operating system-related files 208 | # file properties cache/storage on macOS 209 | *.DS_Store 210 | # thumbnail cache on Windows 211 | Thumbs.db 212 | 213 | # profiling data 214 | .prof 215 | 216 | 217 | ### VisualStudioCode ### 218 | .vscode/* 219 | !.vscode/settings.json 220 | !.vscode/tasks.json 221 | !.vscode/launch.json 222 | !.vscode/extensions.json 223 | *.code-workspace 224 | 225 | ### VisualStudioCode Patch ### 226 | # Ignore all local history of files 227 | .history 228 | .ionide 229 | 230 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,c -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLE OTA Ready ESP Project + Python Client Code 2 | 3 | This project contains OTA update capabilities via BLE implemented using the NimBLE stack and a Python-based client code using the Bleak library. 4 | 5 | # References 6 | 7 | The ESP code is based on the official example provided by Espressif: 8 | * https://github.com/espressif/esp-idf/tree/v4.2/examples/bluetooth/nimble/blehr 9 | 10 | The Python client code is implemented using Bleak: 11 | * https://github.com/hbldh/bleak 12 | 13 | # License 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | # Blog Post 17 | 18 | The concept behind this project is fully explained in the following blog post: 19 | * https://michaelangerer.dev/esp32/ble/ota/2021/06/01/esp32-ota-part-1.html 20 | -------------------------------------------------------------------------------- /ble_client/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from bleak import BleakClient, BleakScanner 4 | 5 | 6 | OTA_DATA_UUID = '23408888-1F40-4CD8-9B89-CA8D45F8A5B0' 7 | OTA_CONTROL_UUID = '7AD671AA-21C0-46A4-B722-270E3AE3D830' 8 | 9 | SVR_CHR_OTA_CONTROL_NOP = bytearray.fromhex("00") 10 | SVR_CHR_OTA_CONTROL_REQUEST = bytearray.fromhex("01") 11 | SVR_CHR_OTA_CONTROL_REQUEST_ACK = bytearray.fromhex("02") 12 | SVR_CHR_OTA_CONTROL_REQUEST_NAK = bytearray.fromhex("03") 13 | SVR_CHR_OTA_CONTROL_DONE = bytearray.fromhex("04") 14 | SVR_CHR_OTA_CONTROL_DONE_ACK = bytearray.fromhex("05") 15 | SVR_CHR_OTA_CONTROL_DONE_NAK = bytearray.fromhex("06") 16 | 17 | 18 | async def _search_for_esp32(): 19 | print("Searching for ESP32...") 20 | esp32 = None 21 | 22 | devices = await BleakScanner.discover() 23 | for device in devices: 24 | if device.name == "esp32": 25 | esp32 = device 26 | 27 | if esp32 is not None: 28 | print("ESP32 found!") 29 | else: 30 | print("ESP32 has not been found.") 31 | assert esp32 is not None 32 | 33 | return esp32 34 | 35 | 36 | async def send_ota(file_path): 37 | t0 = datetime.datetime.now() 38 | queue = asyncio.Queue() 39 | firmware = [] 40 | 41 | esp32 = await _search_for_esp32() 42 | async with BleakClient(esp32) as client: 43 | 44 | async def _ota_notification_handler(sender: int, data: bytearray): 45 | if data == SVR_CHR_OTA_CONTROL_REQUEST_ACK: 46 | print("ESP32: OTA request acknowledged.") 47 | await queue.put("ack") 48 | elif data == SVR_CHR_OTA_CONTROL_REQUEST_NAK: 49 | print("ESP32: OTA request NOT acknowledged.") 50 | await queue.put("nak") 51 | await client.stop_notify(OTA_CONTROL_UUID) 52 | elif data == SVR_CHR_OTA_CONTROL_DONE_ACK: 53 | print("ESP32: OTA done acknowledged.") 54 | await queue.put("ack") 55 | await client.stop_notify(OTA_CONTROL_UUID) 56 | elif data == SVR_CHR_OTA_CONTROL_DONE_NAK: 57 | print("ESP32: OTA done NOT acknowledged.") 58 | await queue.put("nak") 59 | await client.stop_notify(OTA_CONTROL_UUID) 60 | else: 61 | print(f"Notification received: sender: {sender}, data: {data}") 62 | 63 | # subscribe to OTA control 64 | await client.start_notify( 65 | OTA_CONTROL_UUID, 66 | _ota_notification_handler 67 | ) 68 | 69 | # compute the packet size 70 | packet_size = (client.mtu_size - 3) 71 | 72 | # write the packet size to OTA Data 73 | print(f"Sending packet size: {packet_size}.") 74 | await client.write_gatt_char( 75 | OTA_DATA_UUID, 76 | packet_size.to_bytes(2, 'little'), 77 | response=True 78 | ) 79 | 80 | # split the firmware into packets 81 | with open(file_path, "rb") as file: 82 | while chunk := file.read(packet_size): 83 | firmware.append(chunk) 84 | 85 | # write the request OP code to OTA Control 86 | print("Sending OTA request.") 87 | await client.write_gatt_char( 88 | OTA_CONTROL_UUID, 89 | SVR_CHR_OTA_CONTROL_REQUEST 90 | ) 91 | 92 | # wait for the response 93 | await asyncio.sleep(1) 94 | if await queue.get() == "ack": 95 | 96 | # sequentially write all packets to OTA data 97 | for i, pkg in enumerate(firmware): 98 | print(f"Sending packet {i+1}/{len(firmware)}.") 99 | await client.write_gatt_char( 100 | OTA_DATA_UUID, 101 | pkg, 102 | response=True 103 | ) 104 | 105 | # write done OP code to OTA Control 106 | print("Sending OTA done.") 107 | await client.write_gatt_char( 108 | OTA_CONTROL_UUID, 109 | SVR_CHR_OTA_CONTROL_DONE 110 | ) 111 | 112 | # wait for the response 113 | await asyncio.sleep(1) 114 | if await queue.get() == "ack": 115 | dt = datetime.datetime.now() - t0 116 | print(f"OTA successful! Total time: {dt}") 117 | else: 118 | print("OTA failed.") 119 | 120 | else: 121 | print("ESP32 did not acknowledge the OTA request.") 122 | 123 | 124 | if __name__ == '__main__': 125 | asyncio.run(send_ota("esp32_ble_ota.bin")) 126 | -------------------------------------------------------------------------------- /esp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The following lines of boilerplate have to be in your project's 2 | # CMakeLists in this exact order for cmake to work correctly 3 | cmake_minimum_required(VERSION 3.5) 4 | 5 | set(PROJECT_VER "1.0") 6 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 7 | project(esp32_ble_ota) -------------------------------------------------------------------------------- /esp/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "gap.c" "gatt_svr.c" "main.c" 2 | INCLUDE_DIRS ".") -------------------------------------------------------------------------------- /esp/main/gap.c: -------------------------------------------------------------------------------- 1 | #include "gap.h" 2 | 3 | uint8_t addr_type; 4 | 5 | int gap_event_handler(struct ble_gap_event *event, void *arg); 6 | 7 | void advertise() { 8 | struct ble_gap_adv_params adv_params; 9 | struct ble_hs_adv_fields fields; 10 | int rc; 11 | 12 | memset(&fields, 0, sizeof(fields)); 13 | 14 | // flags: discoverability + BLE only 15 | fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; 16 | 17 | // include power levels 18 | fields.tx_pwr_lvl_is_present = 1; 19 | fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; 20 | 21 | // include device name 22 | fields.name = (uint8_t *)device_name; 23 | fields.name_len = strlen(device_name); 24 | fields.name_is_complete = 1; 25 | 26 | rc = ble_gap_adv_set_fields(&fields); 27 | if (rc != 0) { 28 | ESP_LOGE(LOG_TAG_GAP, "Error setting advertisement data: rc=%d", rc); 29 | return; 30 | } 31 | 32 | // start advertising 33 | memset(&adv_params, 0, sizeof(adv_params)); 34 | adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; 35 | adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; 36 | rc = ble_gap_adv_start(addr_type, NULL, BLE_HS_FOREVER, &adv_params, 37 | gap_event_handler, NULL); 38 | if (rc != 0) { 39 | ESP_LOGE(LOG_TAG_GAP, "Error enabling advertisement data: rc=%d", rc); 40 | return; 41 | } 42 | } 43 | 44 | void reset_cb(int reason) { 45 | ESP_LOGE(LOG_TAG_GAP, "BLE reset: reason = %d", reason); 46 | } 47 | 48 | void sync_cb(void) { 49 | // determine best adress type 50 | ble_hs_id_infer_auto(0, &addr_type); 51 | 52 | // start avertising 53 | advertise(); 54 | } 55 | 56 | int gap_event_handler(struct ble_gap_event *event, void *arg) { 57 | switch (event->type) { 58 | case BLE_GAP_EVENT_CONNECT: 59 | // A new connection was established or a connection attempt failed 60 | ESP_LOGI(LOG_TAG_GAP, "GAP: Connection %s: status=%d", 61 | event->connect.status == 0 ? "established" : "failed", 62 | event->connect.status); 63 | break; 64 | 65 | case BLE_GAP_EVENT_DISCONNECT: 66 | ESP_LOGI(LOG_TAG_GAP, "GAP: Disconnect: reason=%d\n", 67 | event->disconnect.reason); 68 | 69 | // Connection terminated; resume advertising 70 | advertise(); 71 | break; 72 | 73 | case BLE_GAP_EVENT_ADV_COMPLETE: 74 | ESP_LOGI(LOG_TAG_GAP, "GAP: adv complete"); 75 | advertise(); 76 | break; 77 | 78 | case BLE_GAP_EVENT_SUBSCRIBE: 79 | ESP_LOGI(LOG_TAG_GAP, "GAP: Subscribe: conn_handle=%d", 80 | event->connect.conn_handle); 81 | break; 82 | 83 | case BLE_GAP_EVENT_MTU: 84 | ESP_LOGI(LOG_TAG_GAP, "GAP: MTU update: conn_handle=%d, mtu=%d", 85 | event->mtu.conn_handle, event->mtu.value); 86 | break; 87 | } 88 | 89 | return 0; 90 | } 91 | 92 | void host_task(void *param) { 93 | // returns only when nimble_port_stop() is executed 94 | nimble_port_run(); 95 | nimble_port_freertos_deinit(); 96 | } 97 | -------------------------------------------------------------------------------- /esp/main/gap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esp_log.h" 4 | #include "esp_nimble_hci.h" 5 | #include "host/ble_hs.h" 6 | #include "nimble/ble.h" 7 | #include "nimble/nimble_port.h" 8 | #include "nimble/nimble_port_freertos.h" 9 | 10 | #define LOG_TAG_GAP "gap" 11 | 12 | static const char device_name[] = "esp32"; 13 | 14 | void advertise(); 15 | void reset_cb(int reason); 16 | void sync_cb(void); 17 | void host_task(void *param); -------------------------------------------------------------------------------- /esp/main/gatt_svr.c: -------------------------------------------------------------------------------- 1 | #include "gatt_svr.h" 2 | 3 | uint8_t gatt_svr_chr_ota_control_val; 4 | uint8_t gatt_svr_chr_ota_data_val[512]; 5 | 6 | uint16_t ota_control_val_handle; 7 | uint16_t ota_data_val_handle; 8 | 9 | const esp_partition_t *update_partition; 10 | esp_ota_handle_t update_handle; 11 | bool updating = false; 12 | uint16_t num_pkgs_received = 0; 13 | uint16_t packet_size = 0; 14 | 15 | static const char *manuf_name = "Company 42"; 16 | static const char *model_num = "ESP32"; 17 | 18 | static int gatt_svr_chr_write(struct os_mbuf *om, uint16_t min_len, 19 | uint16_t max_len, void *dst, uint16_t *len); 20 | 21 | static int gatt_svr_chr_ota_control_cb(uint16_t conn_handle, 22 | uint16_t attr_handle, 23 | struct ble_gatt_access_ctxt *ctxt, 24 | void *arg); 25 | 26 | static int gatt_svr_chr_ota_data_cb(uint16_t conn_handle, uint16_t attr_handle, 27 | struct ble_gatt_access_ctxt *ctxt, 28 | void *arg); 29 | 30 | static int gatt_svr_chr_access_device_info(uint16_t conn_handle, 31 | uint16_t attr_handle, 32 | struct ble_gatt_access_ctxt *ctxt, 33 | void *arg); 34 | 35 | static const struct ble_gatt_svc_def gatt_svr_svcs[] = { 36 | {// Service: Device Information 37 | .type = BLE_GATT_SVC_TYPE_PRIMARY, 38 | .uuid = BLE_UUID16_DECLARE(GATT_DEVICE_INFO_UUID), 39 | .characteristics = 40 | (struct ble_gatt_chr_def[]){ 41 | { 42 | // Characteristic: Manufacturer Name 43 | .uuid = BLE_UUID16_DECLARE(GATT_MANUFACTURER_NAME_UUID), 44 | .access_cb = gatt_svr_chr_access_device_info, 45 | .flags = BLE_GATT_CHR_F_READ, 46 | }, 47 | { 48 | // Characteristic: Model Number 49 | .uuid = BLE_UUID16_DECLARE(GATT_MODEL_NUMBER_UUID), 50 | .access_cb = gatt_svr_chr_access_device_info, 51 | .flags = BLE_GATT_CHR_F_READ, 52 | }, 53 | { 54 | 0, 55 | }, 56 | }}, 57 | 58 | { 59 | // service: OTA Service 60 | .type = BLE_GATT_SVC_TYPE_PRIMARY, 61 | .uuid = &gatt_svr_svc_ota_uuid.u, 62 | .characteristics = 63 | (struct ble_gatt_chr_def[]){ 64 | { 65 | // characteristic: OTA control 66 | .uuid = &gatt_svr_chr_ota_control_uuid.u, 67 | .access_cb = gatt_svr_chr_ota_control_cb, 68 | .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | 69 | BLE_GATT_CHR_F_NOTIFY, 70 | .val_handle = &ota_control_val_handle, 71 | }, 72 | { 73 | // characteristic: OTA data 74 | .uuid = &gatt_svr_chr_ota_data_uuid.u, 75 | .access_cb = gatt_svr_chr_ota_data_cb, 76 | .flags = BLE_GATT_CHR_F_WRITE, 77 | .val_handle = &ota_data_val_handle, 78 | }, 79 | { 80 | 0, 81 | }}, 82 | }, 83 | 84 | { 85 | 0, 86 | }, 87 | }; 88 | 89 | static int gatt_svr_chr_access_device_info(uint16_t conn_handle, 90 | uint16_t attr_handle, 91 | struct ble_gatt_access_ctxt *ctxt, 92 | void *arg) { 93 | uint16_t uuid; 94 | int rc; 95 | 96 | uuid = ble_uuid_u16(ctxt->chr->uuid); 97 | 98 | if (uuid == GATT_MODEL_NUMBER_UUID) { 99 | rc = os_mbuf_append(ctxt->om, model_num, strlen(model_num)); 100 | return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; 101 | } 102 | 103 | if (uuid == GATT_MANUFACTURER_NAME_UUID) { 104 | rc = os_mbuf_append(ctxt->om, manuf_name, strlen(manuf_name)); 105 | return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; 106 | } 107 | 108 | assert(0); 109 | return BLE_ATT_ERR_UNLIKELY; 110 | } 111 | 112 | static int gatt_svr_chr_write(struct os_mbuf *om, uint16_t min_len, 113 | uint16_t max_len, void *dst, uint16_t *len) { 114 | uint16_t om_len; 115 | int rc; 116 | 117 | om_len = OS_MBUF_PKTLEN(om); 118 | if (om_len < min_len || om_len > max_len) { 119 | return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; 120 | } 121 | 122 | rc = ble_hs_mbuf_to_flat(om, dst, max_len, len); 123 | if (rc != 0) { 124 | return BLE_ATT_ERR_UNLIKELY; 125 | } 126 | 127 | return 0; 128 | } 129 | 130 | static void update_ota_control(uint16_t conn_handle) { 131 | struct os_mbuf *om; 132 | esp_err_t err; 133 | 134 | // check which value has been received 135 | switch (gatt_svr_chr_ota_control_val) { 136 | case SVR_CHR_OTA_CONTROL_REQUEST: 137 | // OTA request 138 | ESP_LOGI(LOG_TAG_GATT_SVR, "OTA has been requested via BLE."); 139 | // get the next free OTA partition 140 | update_partition = esp_ota_get_next_update_partition(NULL); 141 | // start the ota update 142 | err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, 143 | &update_handle); 144 | if (err != ESP_OK) { 145 | ESP_LOGE(LOG_TAG_GATT_SVR, "esp_ota_begin failed (%s)", 146 | esp_err_to_name(err)); 147 | esp_ota_abort(update_handle); 148 | gatt_svr_chr_ota_control_val = SVR_CHR_OTA_CONTROL_REQUEST_NAK; 149 | } else { 150 | gatt_svr_chr_ota_control_val = SVR_CHR_OTA_CONTROL_REQUEST_ACK; 151 | updating = true; 152 | 153 | // retrieve the packet size from OTA data 154 | packet_size = 155 | (gatt_svr_chr_ota_data_val[1] << 8) + gatt_svr_chr_ota_data_val[0]; 156 | ESP_LOGI(LOG_TAG_GATT_SVR, "Packet size is: %d", packet_size); 157 | 158 | num_pkgs_received = 0; 159 | } 160 | 161 | // notify the client via BLE that the OTA has been acknowledged (or not) 162 | om = ble_hs_mbuf_from_flat(&gatt_svr_chr_ota_control_val, 163 | sizeof(gatt_svr_chr_ota_control_val)); 164 | ble_gattc_notify_custom(conn_handle, ota_control_val_handle, om); 165 | ESP_LOGI(LOG_TAG_GATT_SVR, "OTA request acknowledgement has been sent."); 166 | 167 | break; 168 | 169 | case SVR_CHR_OTA_CONTROL_DONE: 170 | 171 | updating = false; 172 | 173 | // end the OTA and start validation 174 | err = esp_ota_end(update_handle); 175 | if (err != ESP_OK) { 176 | if (err == ESP_ERR_OTA_VALIDATE_FAILED) { 177 | ESP_LOGE(LOG_TAG_GATT_SVR, 178 | "Image validation failed, image is corrupted!"); 179 | } else { 180 | ESP_LOGE(LOG_TAG_GATT_SVR, "esp_ota_end failed (%s)!", 181 | esp_err_to_name(err)); 182 | } 183 | } else { 184 | // select the new partition for the next boot 185 | err = esp_ota_set_boot_partition(update_partition); 186 | if (err != ESP_OK) { 187 | ESP_LOGE(LOG_TAG_GATT_SVR, "esp_ota_set_boot_partition failed (%s)!", 188 | esp_err_to_name(err)); 189 | } 190 | } 191 | 192 | // set the control value 193 | if (err != ESP_OK) { 194 | gatt_svr_chr_ota_control_val = SVR_CHR_OTA_CONTROL_DONE_NAK; 195 | } else { 196 | gatt_svr_chr_ota_control_val = SVR_CHR_OTA_CONTROL_DONE_ACK; 197 | } 198 | 199 | // notify the client via BLE that DONE has been acknowledged 200 | om = ble_hs_mbuf_from_flat(&gatt_svr_chr_ota_control_val, 201 | sizeof(gatt_svr_chr_ota_control_val)); 202 | ble_gattc_notify_custom(conn_handle, ota_control_val_handle, om); 203 | ESP_LOGI(LOG_TAG_GATT_SVR, "OTA DONE acknowledgement has been sent."); 204 | 205 | // restart the ESP to finish the OTA 206 | if (err == ESP_OK) { 207 | ESP_LOGI(LOG_TAG_GATT_SVR, "Preparing to restart!"); 208 | vTaskDelay(pdMS_TO_TICKS(REBOOT_DEEP_SLEEP_TIMEOUT)); 209 | esp_restart(); 210 | } 211 | 212 | break; 213 | 214 | default: 215 | break; 216 | } 217 | } 218 | 219 | static int gatt_svr_chr_ota_control_cb(uint16_t conn_handle, 220 | uint16_t attr_handle, 221 | struct ble_gatt_access_ctxt *ctxt, 222 | void *arg) { 223 | int rc; 224 | uint8_t length = sizeof(gatt_svr_chr_ota_control_val); 225 | 226 | switch (ctxt->op) { 227 | case BLE_GATT_ACCESS_OP_READ_CHR: 228 | // a client is reading the current value of ota control 229 | rc = os_mbuf_append(ctxt->om, &gatt_svr_chr_ota_control_val, length); 230 | return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; 231 | break; 232 | 233 | case BLE_GATT_ACCESS_OP_WRITE_CHR: 234 | // a client is writing a value to ota control 235 | rc = gatt_svr_chr_write(ctxt->om, 1, length, 236 | &gatt_svr_chr_ota_control_val, NULL); 237 | // update the OTA state with the new value 238 | update_ota_control(conn_handle); 239 | return rc; 240 | break; 241 | 242 | default: 243 | break; 244 | } 245 | 246 | // this shouldn't happen 247 | assert(0); 248 | return BLE_ATT_ERR_UNLIKELY; 249 | } 250 | 251 | static int gatt_svr_chr_ota_data_cb(uint16_t conn_handle, uint16_t attr_handle, 252 | struct ble_gatt_access_ctxt *ctxt, 253 | void *arg) { 254 | int rc; 255 | esp_err_t err; 256 | 257 | // store the received data into gatt_svr_chr_ota_data_val 258 | rc = gatt_svr_chr_write(ctxt->om, 1, sizeof(gatt_svr_chr_ota_data_val), 259 | gatt_svr_chr_ota_data_val, NULL); 260 | 261 | // write the received packet to the partition 262 | if (updating) { 263 | err = esp_ota_write(update_handle, (const void *)gatt_svr_chr_ota_data_val, 264 | packet_size); 265 | if (err != ESP_OK) { 266 | ESP_LOGE(LOG_TAG_GATT_SVR, "esp_ota_write failed (%s)!", 267 | esp_err_to_name(err)); 268 | } 269 | 270 | num_pkgs_received++; 271 | ESP_LOGI(LOG_TAG_GATT_SVR, "Received packet %d", num_pkgs_received); 272 | } 273 | 274 | return rc; 275 | } 276 | 277 | void gatt_svr_init() { 278 | ble_svc_gap_init(); 279 | ble_svc_gatt_init(); 280 | ble_gatts_count_cfg(gatt_svr_svcs); 281 | ble_gatts_add_svcs(gatt_svr_svcs); 282 | } 283 | -------------------------------------------------------------------------------- /esp/main/gatt_svr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esp_ota_ops.h" 4 | #include "host/ble_hs.h" 5 | #include "host/ble_uuid.h" 6 | #include "services/gap/ble_svc_gap.h" 7 | #include "services/gatt/ble_svc_gatt.h" 8 | 9 | #define LOG_TAG_GATT_SVR "gatt_svr" 10 | #define REBOOT_DEEP_SLEEP_TIMEOUT 500 11 | #define GATT_DEVICE_INFO_UUID 0x180A 12 | #define GATT_MANUFACTURER_NAME_UUID 0x2A29 13 | #define GATT_MODEL_NUMBER_UUID 0x2A24 14 | 15 | typedef enum { 16 | SVR_CHR_OTA_CONTROL_NOP, 17 | SVR_CHR_OTA_CONTROL_REQUEST, 18 | SVR_CHR_OTA_CONTROL_REQUEST_ACK, 19 | SVR_CHR_OTA_CONTROL_REQUEST_NAK, 20 | SVR_CHR_OTA_CONTROL_DONE, 21 | SVR_CHR_OTA_CONTROL_DONE_ACK, 22 | SVR_CHR_OTA_CONTROL_DONE_NAK, 23 | } svr_chr_ota_control_val_t; 24 | 25 | // service: OTA Service 26 | // d6f1d96d-594c-4c53-b1c6-244a1dfde6d8 27 | static const ble_uuid128_t gatt_svr_svc_ota_uuid = 28 | BLE_UUID128_INIT(0xd8, 0xe6, 0xfd, 0x1d, 0x4a, 024, 0xc6, 0xb1, 0x53, 0x4c, 29 | 0x4c, 0x59, 0x6d, 0xd9, 0xf1, 0xd6); 30 | 31 | // characteristic: OTA Control 32 | // 7ad671aa-21c0-46a4-b722-270e3ae3d830 33 | static const ble_uuid128_t gatt_svr_chr_ota_control_uuid = 34 | BLE_UUID128_INIT(0x30, 0xd8, 0xe3, 0x3a, 0x0e, 0x27, 0x22, 0xb7, 0xa4, 0x46, 35 | 0xc0, 0x21, 0xaa, 0x71, 0xd6, 0x7a); 36 | 37 | // characteristic: OTA Data 38 | // 23408888-1f40-4cd8-9b89-ca8d45f8a5b0 39 | static const ble_uuid128_t gatt_svr_chr_ota_data_uuid = 40 | BLE_UUID128_INIT(0xb0, 0xa5, 0xf8, 0x45, 0x8d, 0xca, 0x89, 0x9b, 0xd8, 0x4c, 41 | 0x40, 0x1f, 0x88, 0x88, 0x40, 0x23); 42 | 43 | void gatt_svr_init(); -------------------------------------------------------------------------------- /esp/main/main.c: -------------------------------------------------------------------------------- 1 | #include "esp_log.h" 2 | #include "esp_ota_ops.h" 3 | #include "gap.h" 4 | #include "gatt_svr.h" 5 | #include "nvs_flash.h" 6 | 7 | #define LOG_TAG_MAIN "main" 8 | 9 | bool run_diagnostics() { 10 | // do some diagnostics 11 | return true; 12 | } 13 | 14 | void app_main(void) { 15 | // check which partition is running 16 | const esp_partition_t *partition = esp_ota_get_running_partition(); 17 | 18 | switch (partition->address) { 19 | case 0x00010000: 20 | ESP_LOGI(LOG_TAG_MAIN, "Running partition: factory"); 21 | break; 22 | case 0x00110000: 23 | ESP_LOGI(LOG_TAG_MAIN, "Running partition: ota_0"); 24 | break; 25 | case 0x00210000: 26 | ESP_LOGI(LOG_TAG_MAIN, "Running partition: ota_1"); 27 | break; 28 | 29 | default: 30 | ESP_LOGE(LOG_TAG_MAIN, "Running partition: unknown"); 31 | break; 32 | } 33 | 34 | // check if an OTA has been done, if so run diagnostics 35 | esp_ota_img_states_t ota_state; 36 | if (esp_ota_get_state_partition(partition, &ota_state) == ESP_OK) { 37 | if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { 38 | ESP_LOGI(LOG_TAG_MAIN, "An OTA update has been detected."); 39 | if (run_diagnostics()) { 40 | ESP_LOGI(LOG_TAG_MAIN, 41 | "Diagnostics completed successfully! Continuing execution."); 42 | esp_ota_mark_app_valid_cancel_rollback(); 43 | } else { 44 | ESP_LOGE(LOG_TAG_MAIN, 45 | "Diagnostics failed! Start rollback to the previous version."); 46 | esp_ota_mark_app_invalid_rollback_and_reboot(); 47 | } 48 | } 49 | } 50 | 51 | // Initialize NVS 52 | esp_err_t ret = nvs_flash_init(); 53 | if (ret == ESP_ERR_NVS_NO_FREE_PAGES || 54 | ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { 55 | ESP_ERROR_CHECK(nvs_flash_erase()); 56 | ret = nvs_flash_init(); 57 | } 58 | ESP_ERROR_CHECK(ret); 59 | 60 | // BLE Setup 61 | 62 | // initialize BLE controller and nimble stack 63 | esp_nimble_hci_and_controller_init(); 64 | nimble_port_init(); 65 | 66 | // register sync and reset callbacks 67 | ble_hs_cfg.sync_cb = sync_cb; 68 | ble_hs_cfg.reset_cb = reset_cb; 69 | 70 | // initialize service table 71 | gatt_svr_init(); 72 | 73 | // set device name and start host task 74 | ble_svc_gap_device_name_set(device_name); 75 | nimble_port_freertos_init(host_task); 76 | } -------------------------------------------------------------------------------- /esp/sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # OTA 2 | CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y 3 | CONFIG_PARTITION_TABLE_TWO_OTA=y 4 | CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y 5 | 6 | # BLE 7 | CONFIG_BT_ENABLED=y 8 | CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y 9 | CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n 10 | CONFIG_BTDM_CTRL_MODE_BTDM=n 11 | CONFIG_BT_BLUEDROID_ENABLED=n 12 | CONFIG_BT_NIMBLE_ENABLED=y 13 | --------------------------------------------------------------------------------