├── README.md ├── esp32_NimBLE_ota └── esp32_NIMBLE_ota.ino └── iOS_OTA_ESP32 ├── iOS_OTA_ESP32_NimBLE.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── claeshallberg.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcuserdata │ └── claeshallberg.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── iOS_OTA_ESP32_NimBLE ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── BluetoothLE.swift ├── ContentView.swift ├── Info.plist ├── MyHelper.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── firmware └── testfile.bin └── iOS_OTA_ESP32App.swift /README.md: -------------------------------------------------------------------------------- 1 | # Arduino-ESP32-NimBLE-OTA-iOS-SwiftUI 2 | 3 | _NOTE! if you are experience problems with uncomplete uploads and error messages please see solution in issue #3_ 4 | 5 | ESP32 OTA with SwiftUI over BLE using NimBLE 6 | 7 | Arduino example (ESP32 core 1.06) for BLE OTA on a ESP32 using an iOS app 8 | 9 | This is an demo on how to upload firmware (.bin file) from an iOS app to an ESP32. 10 | 11 | Using NimBLE stack (using ver 1.3.1) for substantially lower memory footprint. 12 | ![Simulator Screen Shot - iPhone 12 - 2022-03-12 at 09 12 20](https://user-images.githubusercontent.com/10321738/158010035-edb0e682-6b7b-4eec-89e5-b711f492a2dc.png) 13 | 14 | iOS app shows upload transfer speed and elapsed time. Possible to set number of data chunks per write cycle to test optimal number of chunks before handshake signal needed from ESP32. 15 | 16 | The app will auto connect to the ESP32 when it discovers the BLE service UUID of the ESP32 BLE device. It will also re-connect in situation when the ESP32 BLE device comes out of range and later returns in range. 17 | 18 | Flash the ESP32 device with the .ino file via Arduino IDE and run the App in Xcode (tested on 12.3 for minimum iOS 14.0) on a real device (iPhone, iPad. Simulator does not work). 19 | 20 | After starting the app, press "send .bin to ESP32 over OTA" to start the OTA file transfer. Watch the "Upload progress percentage" going from 0 to 100%. Once the upload is done the ESP32 waits 1 second and thereafter restarts. 21 | 22 | Ported to Arduino code and based on chegewara example for ESP-IDF: https://github.com/chegewara/esp32-OTA-over-BLE 23 | Bluetooth class (BLEConnection) in BluetootheLE.swift inspired by: purpln https://github.com/purpln/bluetooth and Chris Hulbert http://www.splinter.com.au/2019/05/18/ios-swift-bluetooth-le/ 24 | -------------------------------------------------------------------------------- /esp32_NimBLE_ota/esp32_NIMBLE_ota.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Based on chegewara example for IDF: https://github.com/chegewara/esp32-OTA-over-BLE 3 | Ported to Arduino ESP32 by Claes Hallberg 4 | Licence: MIT 5 | OTA Bluetooth example between ESP32 (using NimBLE Bluetooth stack) and iOS swift (CoreBluetooth framework) 6 | Tested withh NimBLE v 1.3.1, iOS 14, ESP32 core 1.06 7 | N.B standard "nimconfig.h" needs to be customised (see below). In this example we only use the ESP32 8 | as perhipheral, hence no need to activate scan or central mode. Stack usage performs better for file transfer 9 | if stack is increased to 8192 Bytes 10 | */ 11 | #include "NimBLEDevice.h" // via Arduino library manager // https://github.com/h2zero/NimBLE-Arduino 12 | // The following file needs to be changed: "nimconfig.h" 13 | // Line 14: uncomment and increase MTU size to: #define CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU 512 14 | // Line 45: uncomment : #define CONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED 15 | // Line 50: uncomment : #define CONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED 16 | // Line 86: uncomments and increase stack size to : #define CONFIG_BT_NIMBLE_TASK_STACK_SIZE 8192 17 | 18 | #include "esp_ota_ops.h" 19 | #include "nvs_flash.h" 20 | #include "nvs.h" 21 | #include 22 | 23 | /*------------------------------------------------------------------------------ 24 | BLE instances & variables 25 | ----------------------------------------------------------------------------*/ 26 | BLEServer* pServer = NULL; 27 | BLECharacteristic * pTxCharacteristic; 28 | BLECharacteristic * pOtaCharacteristic; 29 | 30 | bool deviceConnected = false; 31 | bool oldDeviceConnected = false; 32 | 33 | String fileExtension = ""; 34 | 35 | #define SERVICE_UUID "4FAFC201-1FB5-459E-8FCC-C5C9C331914B" 36 | #define CHARACTERISTIC_TX_UUID "62ec0272-3ec5-11eb-b378-0242ac130003" 37 | #define CHARACTERISTIC_OTA_UUID "62ec0272-3ec5-11eb-b378-0242ac130005" 38 | 39 | /*------------------------------------------------------------------------------ 40 | OTA instances & variables 41 | ----------------------------------------------------------------------------*/ 42 | static esp_ota_handle_t otaHandler = 0; 43 | static const esp_partition_t *update_partition = NULL; 44 | 45 | uint8_t txValue = 0; 46 | int bufferCount = 0; 47 | bool downloadFlag = false; 48 | 49 | /*------------------------------------------------------------------------------ 50 | BLE Server callback 51 | ----------------------------------------------------------------------------*/ 52 | class MyServerCallbacks: public BLEServerCallbacks { 53 | 54 | void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) { 55 | Serial.println("*** App connected"); 56 | /*---------------------------------------- 57 | * BLE Power settings. P9 = max power +9db 58 | ---------------------------------------*/ 59 | esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_CONN_HDL0, ESP_PWR_LVL_P9); 60 | esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_CONN_HDL1, ESP_PWR_LVL_P9); 61 | esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); 62 | esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); 63 | 64 | Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str()); 65 | /* We can use the connection handle here to ask for different connection parameters. 66 | Args: connection handle, min connection interval, max connection interval 67 | latency, supervision timeout. 68 | Units; Min/Max Intervals: 1.25 millisecond increments. 69 | Latency: number of intervals allowed to skip. 70 | Timeout: 10 millisecond increments, try for 5x interval time for best results. 71 | */ 72 | pServer->updateConnParams(desc->conn_handle, 12, 12, 2, 100); 73 | deviceConnected = true; 74 | } 75 | 76 | void onDisconnect(BLEServer* pServer) { 77 | deviceConnected = false; 78 | downloadFlag = false; 79 | Serial.println("*** App disconnected"); 80 | } 81 | }; 82 | 83 | /*------------------------------------------------------------------------------ 84 | BLE Peripheral callback(s) 85 | ----------------------------------------------------------------------------*/ 86 | 87 | class otaCallback: public BLECharacteristicCallbacks { 88 | 89 | void onWrite(BLECharacteristic *pCharacteristic) 90 | { 91 | std::string rxData = pCharacteristic->getValue(); 92 | bufferCount++; 93 | 94 | if (!downloadFlag) 95 | { 96 | //----------------------------------------------- 97 | // First BLE bytes have arrived 98 | //----------------------------------------------- 99 | 100 | Serial.println("1. BeginOTA"); 101 | const esp_partition_t *configured = esp_ota_get_boot_partition(); 102 | const esp_partition_t *running = esp_ota_get_running_partition(); 103 | 104 | if (configured != running) 105 | { 106 | Serial.printf("ERROR: Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x", configured->address, running->address); 107 | Serial.println("(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)"); 108 | downloadFlag = false; 109 | esp_ota_end(otaHandler); 110 | } else { 111 | Serial.printf("2. Running partition type %d subtype %d (offset 0x%08x) \n", running->type, running->subtype, running->address); 112 | } 113 | 114 | update_partition = esp_ota_get_next_update_partition(NULL); 115 | assert(update_partition != NULL); 116 | 117 | Serial.printf("3. Writing to partition subtype %d at offset 0x%x \n", update_partition->subtype, update_partition->address); 118 | 119 | //------------------------------------------------------------------------------------------ 120 | // esp_ota_begin can take a while to complete as it erase the flash partition (3-5 seconds) 121 | // so make sure there's no timeout on the client side (iOS) that triggers before that. 122 | //------------------------------------------------------------------------------------------ 123 | esp_task_wdt_init(10, false); 124 | vTaskDelay(5); 125 | 126 | if (esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &otaHandler) != ESP_OK) { 127 | downloadFlag = false; 128 | return; 129 | } 130 | downloadFlag = true; 131 | } 132 | 133 | if (bufferCount >= 1 || rxData.length() > 0) 134 | { 135 | if(esp_ota_write(otaHandler, (uint8_t *) rxData.c_str(), rxData.length()) != ESP_OK) { 136 | Serial.println("Error: write to flash failed"); 137 | downloadFlag = false; 138 | return; 139 | } else { 140 | bufferCount = 1; 141 | Serial.println("--Data received---"); 142 | //Notify the iOS app so next batch can be sent 143 | pTxCharacteristic->setValue(&txValue, 1); 144 | pTxCharacteristic->notify(); 145 | } 146 | 147 | //------------------------------------------------------------------- 148 | // check if this was the last data chunk? (normaly the last chunk is 149 | // smaller than the maximum MTU size). For improvement: let iOS app send byte 150 | // length instead of hardcoding "510" 151 | //------------------------------------------------------------------- 152 | if (rxData.length() < 510) // TODO Asumes at least 511 data bytes (@BLE 4.2). 153 | { 154 | Serial.println("4. Final byte arrived"); 155 | //----------------------------------------------------------------- 156 | // Final chunk arrived. Now check that 157 | // the length of total file is correct 158 | //----------------------------------------------------------------- 159 | if (esp_ota_end(otaHandler) != ESP_OK) 160 | { 161 | Serial.println("OTA end failed "); 162 | downloadFlag = false; 163 | return; 164 | } 165 | 166 | //----------------------------------------------------------------- 167 | // Clear download flag and restart the ESP32 if the firmware 168 | // update was successful 169 | //----------------------------------------------------------------- 170 | Serial.println("Set Boot partion"); 171 | if (ESP_OK == esp_ota_set_boot_partition(update_partition)) 172 | { 173 | esp_ota_end(otaHandler); 174 | downloadFlag = false; 175 | Serial.println("Restarting..."); 176 | esp_restart(); 177 | return; 178 | } else { 179 | //------------------------------------------------------------ 180 | // Something whent wrong, the upload was not successful 181 | //------------------------------------------------------------ 182 | Serial.println("Upload Error"); 183 | downloadFlag = false; 184 | esp_ota_end(otaHandler); 185 | return; 186 | } 187 | } 188 | } else { 189 | downloadFlag = false; 190 | } 191 | } 192 | }; 193 | 194 | 195 | 196 | void setup() { 197 | Serial.begin(115200); 198 | Serial.println("Starting BLE OTA work!"); 199 | Serial.printf("ESP32 Chip model = %d\n", ESP.getChipRevision()); 200 | Serial.printf("This chip has %d MHz\n", ESP.getCpuFreqMHz()); 201 | 202 | // 1. Create the BLE Device 203 | NimBLEDevice::init("ESP32 iOS OTA NimBLE"); 204 | NimBLEDevice::setMTU(517); 205 | 206 | // 2. Create the BLE server 207 | pServer = NimBLEDevice::createServer(); 208 | pServer->setCallbacks(new MyServerCallbacks()); 209 | 210 | // 3. Create BLE Service 211 | NimBLEService *pService = pServer->createService(SERVICE_UUID); 212 | 213 | // 4. Create BLE Characteristics inside the service(s) 214 | pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_TX_UUID, 215 | NIMBLE_PROPERTY:: NOTIFY); 216 | 217 | pOtaCharacteristic = pService->createCharacteristic(CHARACTERISTIC_OTA_UUID, 218 | NIMBLE_PROPERTY:: WRITE_NR); 219 | pOtaCharacteristic->setCallbacks(new otaCallback()); 220 | 221 | // 5. Start the service(s) 222 | pService->start(); 223 | 224 | // 6. Start advertising 225 | pServer->getAdvertising()->addServiceUUID(pService->getUUID()); 226 | pServer->getAdvertising()->start(); 227 | 228 | NimBLEDevice::startAdvertising(); 229 | Serial.println("Waiting a client connection to notify..."); 230 | downloadFlag = false; 231 | } 232 | 233 | 234 | void loop() { 235 | if (!deviceConnected && oldDeviceConnected) { 236 | delay(100); 237 | pServer->startAdvertising(); 238 | Serial.println("start advertising"); 239 | oldDeviceConnected = deviceConnected; 240 | } 241 | if (deviceConnected && !oldDeviceConnected) { 242 | Serial.println("main loop started"); 243 | oldDeviceConnected = deviceConnected; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C564792925BE75DC00126AA8 /* iOS_OTA_ESP32App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C564792825BE75DC00126AA8 /* iOS_OTA_ESP32App.swift */; }; 11 | C564792B25BE75DC00126AA8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C564792A25BE75DC00126AA8 /* ContentView.swift */; }; 12 | C564792D25BE75DD00126AA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C564792C25BE75DD00126AA8 /* Assets.xcassets */; }; 13 | C564793025BE75DD00126AA8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C564792F25BE75DD00126AA8 /* Preview Assets.xcassets */; }; 14 | C564793925BE763500126AA8 /* MyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C564793825BE763500126AA8 /* MyHelper.swift */; }; 15 | C564794025BE776C00126AA8 /* BluetoothLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = C564793F25BE776C00126AA8 /* BluetoothLE.swift */; }; 16 | C564794725BE829A00126AA8 /* testfile.bin in Resources */ = {isa = PBXBuildFile; fileRef = C564794625BE829A00126AA8 /* testfile.bin */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | C564792525BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS_OTA_ESP32_NimBLE.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | C564792825BE75DC00126AA8 /* iOS_OTA_ESP32App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOS_OTA_ESP32App.swift; sourceTree = ""; }; 22 | C564792A25BE75DC00126AA8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | C564792C25BE75DD00126AA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | C564792F25BE75DD00126AA8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | C564793125BE75DD00126AA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | C564793825BE763500126AA8 /* MyHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyHelper.swift; sourceTree = ""; }; 27 | C564793F25BE776C00126AA8 /* BluetoothLE.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothLE.swift; sourceTree = ""; }; 28 | C564794625BE829A00126AA8 /* testfile.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = testfile.bin; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | C564792225BE75DC00126AA8 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | C564791C25BE75DC00126AA8 = { 43 | isa = PBXGroup; 44 | children = ( 45 | C564792725BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE */, 46 | C564792625BE75DC00126AA8 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | C564792625BE75DC00126AA8 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | C564792525BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | C564792725BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | C564792825BE75DC00126AA8 /* iOS_OTA_ESP32App.swift */, 62 | C564792A25BE75DC00126AA8 /* ContentView.swift */, 63 | C564793825BE763500126AA8 /* MyHelper.swift */, 64 | C564792C25BE75DD00126AA8 /* Assets.xcassets */, 65 | C564793125BE75DD00126AA8 /* Info.plist */, 66 | C564793F25BE776C00126AA8 /* BluetoothLE.swift */, 67 | C564793B25BE775A00126AA8 /* firmware */, 68 | C564792E25BE75DD00126AA8 /* Preview Content */, 69 | ); 70 | path = iOS_OTA_ESP32_NimBLE; 71 | sourceTree = ""; 72 | }; 73 | C564792E25BE75DD00126AA8 /* Preview Content */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | C564792F25BE75DD00126AA8 /* Preview Assets.xcassets */, 77 | ); 78 | path = "Preview Content"; 79 | sourceTree = ""; 80 | }; 81 | C564793B25BE775A00126AA8 /* firmware */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | C564794625BE829A00126AA8 /* testfile.bin */, 85 | ); 86 | path = firmware; 87 | sourceTree = ""; 88 | }; 89 | /* End PBXGroup section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | C564792425BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = C564793425BE75DD00126AA8 /* Build configuration list for PBXNativeTarget "iOS_OTA_ESP32_NimBLE" */; 95 | buildPhases = ( 96 | C564792125BE75DC00126AA8 /* Sources */, 97 | C564792225BE75DC00126AA8 /* Frameworks */, 98 | C564792325BE75DC00126AA8 /* Resources */, 99 | ); 100 | buildRules = ( 101 | ); 102 | dependencies = ( 103 | ); 104 | name = iOS_OTA_ESP32_NimBLE; 105 | productName = iOS_OTA_ESP32; 106 | productReference = C564792525BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | C564791D25BE75DC00126AA8 /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | LastSwiftUpdateCheck = 1230; 116 | LastUpgradeCheck = 1230; 117 | TargetAttributes = { 118 | C564792425BE75DC00126AA8 = { 119 | CreatedOnToolsVersion = 12.3; 120 | }; 121 | }; 122 | }; 123 | buildConfigurationList = C564792025BE75DC00126AA8 /* Build configuration list for PBXProject "iOS_OTA_ESP32_NimBLE" */; 124 | compatibilityVersion = "Xcode 9.3"; 125 | developmentRegion = en; 126 | hasScannedForEncodings = 0; 127 | knownRegions = ( 128 | en, 129 | Base, 130 | ); 131 | mainGroup = C564791C25BE75DC00126AA8; 132 | productRefGroup = C564792625BE75DC00126AA8 /* Products */; 133 | projectDirPath = ""; 134 | projectRoot = ""; 135 | targets = ( 136 | C564792425BE75DC00126AA8 /* iOS_OTA_ESP32_NimBLE */, 137 | ); 138 | }; 139 | /* End PBXProject section */ 140 | 141 | /* Begin PBXResourcesBuildPhase section */ 142 | C564792325BE75DC00126AA8 /* Resources */ = { 143 | isa = PBXResourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | C564794725BE829A00126AA8 /* testfile.bin in Resources */, 147 | C564793025BE75DD00126AA8 /* Preview Assets.xcassets in Resources */, 148 | C564792D25BE75DD00126AA8 /* Assets.xcassets in Resources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXResourcesBuildPhase section */ 153 | 154 | /* Begin PBXSourcesBuildPhase section */ 155 | C564792125BE75DC00126AA8 /* Sources */ = { 156 | isa = PBXSourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | C564792B25BE75DC00126AA8 /* ContentView.swift in Sources */, 160 | C564792925BE75DC00126AA8 /* iOS_OTA_ESP32App.swift in Sources */, 161 | C564793925BE763500126AA8 /* MyHelper.swift in Sources */, 162 | C564794025BE776C00126AA8 /* BluetoothLE.swift in Sources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXSourcesBuildPhase section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | C564793225BE75DD00126AA8 /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | CLANG_ANALYZER_NONNULL = YES; 174 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 175 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 176 | CLANG_CXX_LIBRARY = "libc++"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | COPY_PHASE_STRIP = NO; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | GCC_C_LANGUAGE_STANDARD = gnu11; 207 | GCC_DYNAMIC_NO_PIC = NO; 208 | GCC_NO_COMMON_BLOCKS = YES; 209 | GCC_OPTIMIZATION_LEVEL = 0; 210 | GCC_PREPROCESSOR_DEFINITIONS = ( 211 | "DEBUG=1", 212 | "$(inherited)", 213 | ); 214 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 215 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 216 | GCC_WARN_UNDECLARED_SELECTOR = YES; 217 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 218 | GCC_WARN_UNUSED_FUNCTION = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 222 | MTL_FAST_MATH = YES; 223 | ONLY_ACTIVE_ARCH = YES; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | }; 228 | name = Debug; 229 | }; 230 | C564793325BE75DD00126AA8 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu11; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | MTL_FAST_MATH = YES; 278 | SDKROOT = iphoneos; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 281 | VALIDATE_PRODUCT = YES; 282 | }; 283 | name = Release; 284 | }; 285 | C564793525BE75DD00126AA8 /* Debug */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 290 | CODE_SIGN_STYLE = Automatic; 291 | DEVELOPMENT_ASSET_PATHS = "\"iOS_OTA_ESP32_NimBLE/Preview Content\""; 292 | DEVELOPMENT_TEAM = AL69NHXA3Y; 293 | ENABLE_PREVIEWS = YES; 294 | INFOPLIST_FILE = iOS_OTA_ESP32_NimBLE/Info.plist; 295 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 296 | LD_RUNPATH_SEARCH_PATHS = ( 297 | "$(inherited)", 298 | "@executable_path/Frameworks", 299 | ); 300 | PRODUCT_BUNDLE_IDENTIFIER = "be.hallberg.iOS-OTA-ESP32"; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_VERSION = 5.0; 303 | TARGETED_DEVICE_FAMILY = "1,2"; 304 | }; 305 | name = Debug; 306 | }; 307 | C564793625BE75DD00126AA8 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 312 | CODE_SIGN_STYLE = Automatic; 313 | DEVELOPMENT_ASSET_PATHS = "\"iOS_OTA_ESP32_NimBLE/Preview Content\""; 314 | DEVELOPMENT_TEAM = AL69NHXA3Y; 315 | ENABLE_PREVIEWS = YES; 316 | INFOPLIST_FILE = iOS_OTA_ESP32_NimBLE/Info.plist; 317 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/Frameworks", 321 | ); 322 | PRODUCT_BUNDLE_IDENTIFIER = "be.hallberg.iOS-OTA-ESP32"; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SWIFT_VERSION = 5.0; 325 | TARGETED_DEVICE_FAMILY = "1,2"; 326 | }; 327 | name = Release; 328 | }; 329 | /* End XCBuildConfiguration section */ 330 | 331 | /* Begin XCConfigurationList section */ 332 | C564792025BE75DC00126AA8 /* Build configuration list for PBXProject "iOS_OTA_ESP32_NimBLE" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | C564793225BE75DD00126AA8 /* Debug */, 336 | C564793325BE75DD00126AA8 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | C564793425BE75DD00126AA8 /* Build configuration list for PBXNativeTarget "iOS_OTA_ESP32_NimBLE" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | C564793525BE75DD00126AA8 /* Debug */, 345 | C564793625BE75DD00126AA8 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | /* End XCConfigurationList section */ 351 | }; 352 | rootObject = C564791D25BE75DC00126AA8 /* Project object */; 353 | } 354 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaesClaes/Arduino-ESP32-NimBLE-OTA-iOS-SwiftUI/eccee41b66219e0cc6730a047c774ca54f0e8918/iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/xcuserdata/claeshallberg.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE.xcodeproj/xcuserdata/claeshallberg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iOS_OTA_ESP32.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | iOS_OTA_ESP32_NimBLE.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/BluetoothLE.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothLE.swift 3 | // 4 | // Inspired by: purpln https://github.com/purpln/bluetooth and Chris Hulbert http://www.splinter.com.au/2019/05/18/ios-swift-bluetooth-le/ 5 | // Created by Claes Hallberg on 1/12/22. 6 | // Licence: MIT 7 | 8 | import CoreBluetooth 9 | 10 | private let peripheralIdDefaultsKey = "MyBluetoothManagerPeripheralId" 11 | private let myDesiredServiceId = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B") //Used for auto connect and re connect to this Service UIID only 12 | private let myDesiredCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") 13 | private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") //ESP32 pTxCharacteristic ESP send (notifying) 14 | private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005")//ESP32 pOtaCharacteristic ESP write 15 | 16 | private let outOfRangeHeuristics: Set = [.unknown, .connectionTimeout, .peripheralDisconnected, .connectionFailed] 17 | 18 | // Class definition 19 | class BLEConnection:NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate{ 20 | 21 | var manager: CBCentralManager! 22 | var statusCharacteristic: CBCharacteristic? 23 | var otaCharacteristic: CBCharacteristic? 24 | 25 | var state = StateBLE.poweredOff 26 | 27 | enum StateBLE { 28 | case poweredOff 29 | case restoringConnectingPeripheral(CBPeripheral) 30 | case restoringConnectedPeripheral(CBPeripheral) 31 | case disconnected 32 | case scanning(Countdown) 33 | case connecting(CBPeripheral, Countdown) 34 | case discoveringServices(CBPeripheral, Countdown) 35 | case discoveringCharacteristics(CBPeripheral, Countdown) 36 | case connected(CBPeripheral) 37 | case outOfRange(CBPeripheral) 38 | 39 | var peripheral: CBPeripheral? { 40 | switch self { 41 | case .poweredOff: return nil 42 | case .restoringConnectingPeripheral(let p): return p 43 | case .restoringConnectedPeripheral(let p): return p 44 | case .disconnected: return nil 45 | case .scanning: return nil 46 | case .connecting(let p, _): return p 47 | case .discoveringServices(let p, _): return p 48 | case .discoveringCharacteristics(let p, _): return p 49 | case .connected(let p): return p 50 | case .outOfRange(let p): return p 51 | } 52 | } 53 | } 54 | 55 | //Used by contentView.swift 56 | @Published var name = "" 57 | @Published var connected = false 58 | @Published var transferProgress : Double = 0.0 59 | @Published var chunkCount = 2 // number of chunks to be sent before peripheral needs to accknowledge. 60 | @Published var elapsedTime = 0.0 61 | @Published var kBPerSecond = 0.0 62 | 63 | 64 | //transfer varibles 65 | var dataToSend = Data() 66 | var dataBuffer = Data() 67 | var chunkSize = 0 68 | var dataLength = 0 69 | var transferOngoing = true 70 | var sentBytes = 0 71 | var packageCounter = 0 72 | var startTime = 0.0 73 | var stopTime = 0.0 74 | var firstAcknowledgeFromESP32 = false 75 | 76 | //Initiate CentralManager 77 | override init() { 78 | super.init() 79 | manager = CBCentralManager(delegate: self, queue: .none) 80 | manager.delegate = self 81 | } 82 | 83 | // Callback from CentralManager when State updates (on, off, etc) 84 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 85 | print("\(Date()) CM DidUpdateState") 86 | switch manager.state { 87 | case .unknown: 88 | print("\(Date()) Unknown") 89 | case .resetting: 90 | print("\(Date()) Resetting") 91 | case .unsupported: 92 | print("\(Date()) Unsupported") 93 | case .unauthorized: 94 | print("\(Date()) Bluetooth disabled for this app, pls enable it in settings") 95 | case .poweredOff: 96 | print("\(Date()) Turn on bluetooth") 97 | case .poweredOn: 98 | print("\(Date()) Everything is ok :-) ") 99 | if case .poweredOff = state { 100 | // Firstly, try to reconnect: 101 | // 1. Any peripheralsID stored in UserDefaults? 102 | if let peripheralIdStr = UserDefaults.standard.object(forKey: peripheralIdDefaultsKey) as? String, 103 | // 2. Yes, so convert the String to a UUID type 104 | let peripheralId = UUID(uuidString: peripheralIdStr), 105 | // 3. Compare with UUID's already in the manager 106 | let previouslyConnected = manager 107 | .retrievePeripherals(withIdentifiers: [peripheralId]) 108 | .first { 109 | // 4. If ok then connect 110 | print("\(Date()) CM DidUpdateState: connect from userDefaults") 111 | connect(peripheral: previouslyConnected) 112 | // Next, try for ones that are connected to the system: 113 | } 114 | } 115 | print("\(Date()) End of .poweredOn") 116 | @unknown default: 117 | print("\(Date()) fatal error") 118 | } 119 | } 120 | 121 | // Discovery (scanning) and handling of BLE devices in range 122 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 123 | guard case .scanning = state else { return } 124 | name = String(peripheral.name ?? "unknown") 125 | print("\(Date()) \(name) is found") 126 | print("\(Date()) CM DidDiscover") 127 | manager.stopScan() 128 | connect(peripheral: peripheral) 129 | } 130 | 131 | // Connection established handler 132 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 133 | print("\(Date()) CM DidConnect") 134 | 135 | transferOngoing = false 136 | 137 | // Clear the data that we may already have 138 | dataToSend.removeAll(keepingCapacity: false) 139 | 140 | // Make sure we get the discovery callbacks 141 | peripheral.delegate = self 142 | 143 | if peripheral.myDesiredCharacteristic == nil { 144 | discoverServices(peripheral: peripheral) 145 | } else { 146 | setConnected(peripheral: peripheral) 147 | } 148 | } 149 | 150 | // Connection failed 151 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 152 | print("\(Date()) CM DidFailToConnect") 153 | state = .disconnected 154 | } 155 | 156 | // Disconnection (out of range, ...) 157 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 158 | transferOngoing = false 159 | print("\(Date()) CM DidDisconnectPeripheral") 160 | print("\(Date()) \(peripheral.name ?? "unknown") disconnected") 161 | // Did our currently-connected peripheral just disconnect? 162 | if state.peripheral?.identifier == peripheral.identifier { 163 | name = "" 164 | connected = false 165 | // IME the error codes encountered are: 166 | // 0 = rebooting the peripheral. 167 | // 6 = out of range. 168 | if let error = error, (error as NSError).domain == CBErrorDomain, 169 | let code = CBError.Code(rawValue: (error as NSError).code), 170 | outOfRangeHeuristics.contains(code) { 171 | // Try reconnect without setting a timeout in the state machine. 172 | // With CB, it's like saying 'please reconnect me at any point 173 | // in the future if this peripheral comes back into range'. 174 | print("\(Date()) connect: try reconnect when back in range") 175 | manager.connect(peripheral, options: nil) 176 | state = .outOfRange(peripheral) 177 | } else { 178 | // Likely a deliberate unpairing. 179 | state = .disconnected 180 | } 181 | } 182 | } 183 | 184 | //----------------------------------------- 185 | // Peripheral callbacks 186 | //----------------------------------------- 187 | 188 | // Discover BLE device service(s) 189 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 190 | print("\(Date()) PH didDiscoverServices") 191 | // Ignore services discovered late. 192 | guard case .discoveringServices = state else { 193 | return 194 | } 195 | if let error = error { 196 | print("\(Date()) Failed to discover services: \(error)") 197 | disconnect() 198 | return 199 | } 200 | guard peripheral.myDesiredService != nil else { 201 | print("\(Date()) Desired service missing") 202 | disconnect() 203 | return 204 | } 205 | // All fine so far, go to next step 206 | guard let services = peripheral.services else { return } 207 | for service in services { 208 | peripheral.discoverCharacteristics(nil, for: service) 209 | } 210 | 211 | } 212 | // Discover BLE device Service charachteristics 213 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 214 | print("\(Date()) PH didDiscoverCharacteristicsFor") 215 | 216 | if let error = error { 217 | print("\(Date()) Failed to discover characteristics: \(error)") 218 | disconnect() 219 | return 220 | } 221 | 222 | guard peripheral.myDesiredCharacteristic != nil else { 223 | print("\(Date()) Desired characteristic missing") 224 | disconnect() 225 | return 226 | } 227 | 228 | guard let characteristics = service.characteristics else { 229 | return 230 | } 231 | 232 | 233 | for characteristic in characteristics{ 234 | switch characteristic.uuid { 235 | 236 | case statusCharacteristicId: 237 | statusCharacteristic = characteristic 238 | print("\(Date()) receive status from: \(statusCharacteristic!.uuid as Any)") 239 | peripheral.setNotifyValue(true, for: characteristic) 240 | 241 | case otaCharacteristicId: 242 | otaCharacteristic = characteristic 243 | print("\(Date()) send OTA firmware to: \(otaCharacteristic!.uuid as Any)") 244 | peripheral.setNotifyValue(false, for: characteristic) 245 | 246 | default: 247 | print("\(Date()) unknown") 248 | } 249 | } 250 | setConnected(peripheral: peripheral) 251 | } 252 | // The BLE peripheral device sent some notify data. Deal with it! 253 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 254 | //print("\(Date()) PH didUpdateValueFor") 255 | if let error = error { 256 | print(error) 257 | return 258 | } 259 | if let data = characteristic.value{ 260 | // deal with incoming data 261 | // First check if the incoming data is one byte length? 262 | // if so it's the peripheral acknowledging and telling 263 | // us to send another batch of data 264 | if data.count == 1 { 265 | if !firstAcknowledgeFromESP32 { 266 | firstAcknowledgeFromESP32 = true 267 | startTime = CFAbsoluteTimeGetCurrent() 268 | } 269 | //print("\(Date()) -X-") 270 | if transferOngoing { 271 | packageCounter = 0 272 | writeDataToPeriheral(characteristic: otaCharacteristic!) 273 | } 274 | } 275 | } 276 | } 277 | 278 | // Called when .withResponse is used. 279 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?){ 280 | print("\(Date()) PH didWriteValueFor") 281 | if let error = error { 282 | print("\(Date()) Error writing to characteristic: \(error)") 283 | return 284 | } 285 | } 286 | 287 | // Callback indicating peripheral notifying state 288 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){ 289 | print("\(Date()) PH didUpdateNotificationStateFor") 290 | print("\(Date()) PH \(characteristic)") 291 | if error == nil { 292 | print("\(Date()) Notification Set OK, isNotifying: \(characteristic.isNotifying)") 293 | if !characteristic.isNotifying { 294 | print("\(Date()) isNotifying is false, set to true again!") 295 | peripheral.setNotifyValue(true, for: characteristic) 296 | } 297 | } 298 | } 299 | 300 | /*------------------------------------------------------------------------- 301 | Functions 302 | -------------------------------------------------------------------------*/ 303 | // Scanning for device with a specific Service UUID (myDesiredServiceId) 304 | func startScanning(){ 305 | print("\(Date()) FUNC StartScanning") 306 | guard manager.state == .poweredOn else { 307 | print("\(Date()) Cannot scan, BT is not powered on") 308 | return 309 | } 310 | manager.scanForPeripherals(withServices: [myDesiredServiceId], options: nil) 311 | state = .scanning(Countdown(seconds: 10, closure: { 312 | self.manager.stopScan() 313 | self.state = .disconnected 314 | print("\(Date()) Scan timed out") 315 | })) 316 | } 317 | 318 | // Disconnect by user request 319 | func disconnect(forget: Bool = false) { 320 | print("\(Date()) FUNC Disconnect") 321 | if let peripheral = state.peripheral { 322 | manager.cancelPeripheralConnection(peripheral) 323 | } 324 | if forget { 325 | UserDefaults.standard.removeObject(forKey: peripheralIdDefaultsKey) 326 | UserDefaults.standard.synchronize() 327 | } 328 | state = .disconnected 329 | connected = false 330 | transferOngoing = false 331 | } 332 | 333 | // Connect to the device from the scanning 334 | func connect(peripheral: CBPeripheral){ 335 | print("\(Date()) FUNC Connect") 336 | if connected { 337 | manager.cancelPeripheralConnection(peripheral) 338 | }else{ 339 | // Connect! 340 | print("\(Date()) connect: connect inside func connect()") 341 | manager.connect(peripheral, options: nil) 342 | name = String(peripheral.name ?? "unknown") 343 | print("\(Date()) \(name) is found") 344 | state = .connecting(peripheral, Countdown(seconds: 10, closure: { 345 | self.manager.cancelPeripheralConnection(self.state.peripheral!) 346 | self.state = .disconnected 347 | self.connected = false 348 | print("\(Date()) Connect timed out") 349 | })) 350 | } 351 | } 352 | 353 | // Discover Services of a device 354 | func discoverServices(peripheral: CBPeripheral) { 355 | print("\(Date()) FUNC DiscoverServices") 356 | peripheral.delegate = self 357 | peripheral.discoverServices([myDesiredServiceId]) 358 | state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: { 359 | self.disconnect() 360 | print("\(Date()) Could not discover services") 361 | })) 362 | } 363 | 364 | // Discover Characteristics of a Services 365 | func discoverCharacteristics(peripheral: CBPeripheral) { 366 | print("\(Date()) FUNC DiscoverCharacteristics") 367 | guard let myDesiredService = peripheral.myDesiredService else { 368 | self.disconnect() 369 | return 370 | } 371 | peripheral.discoverCharacteristics([myDesiredCharacteristicId], for: myDesiredService) 372 | state = .discoveringCharacteristics(peripheral, 373 | Countdown(seconds: 10, 374 | closure: { 375 | self.disconnect() 376 | print("\(Date()) Could not discover characteristics") 377 | })) 378 | } 379 | 380 | func setConnected(peripheral: CBPeripheral) { 381 | print("\(Date()) FUNC SetConnected") 382 | print("\(Date()) Max write value with response: \(peripheral.maximumWriteValueLength(for: .withResponse))") 383 | print("\(Date()) Max write value without response: \(peripheral.maximumWriteValueLength(for: .withoutResponse))") 384 | guard let myDesiredCharacteristic = peripheral.myDesiredCharacteristic 385 | else { 386 | print("\(Date()) Missing characteristic") 387 | disconnect() 388 | return 389 | } 390 | 391 | // Remember the ID for startup reconnecting. 392 | UserDefaults.standard.set(peripheral.identifier.uuidString, forKey: peripheralIdDefaultsKey) 393 | UserDefaults.standard.synchronize() 394 | 395 | peripheral.setNotifyValue(true, for: myDesiredCharacteristic) 396 | state = .connected(peripheral) 397 | connected = true 398 | name = String(peripheral.name ?? "unknown") 399 | } 400 | 401 | 402 | // Peripheral callback when its ready to receive more data without response 403 | func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { 404 | if transferOngoing && packageCounter < chunkCount { 405 | writeDataToPeriheral(characteristic: otaCharacteristic!) 406 | } 407 | } 408 | 409 | func sendFile(filename: String, fileEnding: String) { 410 | print("\(Date()) FUNC SendFile") 411 | 412 | // 1. Get the data from the file(name) and copy data to dataBUffer 413 | guard let data: Data = try? getBinFileToData(fileName: filename, fileEnding: fileEnding) else { 414 | print("\(Date()) failed to open file") 415 | return 416 | } 417 | dataBuffer = data 418 | dataLength = dataBuffer.count 419 | transferOngoing = true 420 | packageCounter = 0 421 | // Send the first chunk 422 | elapsedTime = 0.0 423 | sentBytes = 0 424 | firstAcknowledgeFromESP32 = false 425 | startTime = CFAbsoluteTimeGetCurrent() 426 | writeDataToPeriheral(characteristic: otaCharacteristic!) 427 | } 428 | 429 | 430 | func writeDataToPeriheral(characteristic: CBCharacteristic) { 431 | 432 | // 1. Get the peripheral and it's transfer characteristic 433 | guard let discoveredPeripheral = state.peripheral else {return} 434 | // ATT MTU - 3 bytes 435 | chunkSize = discoveredPeripheral.maximumWriteValueLength (for: .withoutResponse) - 3 436 | // Get the data range 437 | var range:Range 438 | // 2. Loop through and send each chunk to the BLE device 439 | // check to see if number of iterations completed and peripheral can accept more data 440 | // package counter allow only "chunkCount" of data to be sent per time. 441 | while transferOngoing && discoveredPeripheral.canSendWriteWithoutResponse && packageCounter < chunkCount { 442 | 443 | // 3. Create a range based on the length of data to return 444 | range = (0.. ()) { 496 | timer = Timer.scheduledTimer(withTimeInterval: seconds, 497 | repeats: false, block: { _ in 498 | closure() 499 | }) 500 | } 501 | deinit { 502 | timer.invalidate() 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // iOS_OTA_ESP32 4 | // Inspired by: purpln https://github.com/purpln/bluetooth 5 | // Licence: MIT 6 | // Created by Claes Hallberg on 1/13/22. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | @EnvironmentObject var ble : BLEConnection 13 | var body: some View{ 14 | VStack{ 15 | Text("iOS OTA for ESP32 using NimBLE").bold() 16 | VStack { 17 | Text("Device : \(ble.name)") 18 | Text("Transfer speed : \(ble.kBPerSecond, specifier: "%.1f") kB/s") 19 | Text("Elapsed time : \(ble.elapsedTime, specifier: "%.1f") s") 20 | Text("Upload progress: \(ble.transferProgress, specifier: "%.1f") %") 21 | } 22 | HStack{ 23 | Button(action: { 24 | ble.startScanning() 25 | }){ 26 | Text("connect").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 27 | } 28 | Button(action: { 29 | ble.disconnect(forget: false) 30 | }){ 31 | Text("disconnect").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 32 | } 33 | Button(action: { 34 | ble.disconnect(forget: true) 35 | }){ 36 | Text("forget bond").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 37 | } 38 | } 39 | HStack{ 40 | Button(action: { 41 | ble.sendFile(filename: "testfile", fileEnding: ".bin") 42 | }){ 43 | Text("send .bin file to ESP32 over OTA").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 44 | }.disabled(ble.transferOngoing) 45 | 46 | 47 | 48 | } 49 | Divider() 50 | VStack{ 51 | Stepper("chunks (1-4) per write cycle: \(ble.chunkCount)", value: $ble.chunkCount, in: 1...4) 52 | .disabled(ble.transferOngoing) 53 | } 54 | HStack{ 55 | Spacer() 56 | } 57 | }.padding().accentColor(colorChange(ble.connected)) 58 | } 59 | } 60 | 61 | func colorChange(_ connected:Bool) -> Color{ 62 | if connected{ 63 | return Color.green 64 | }else{ 65 | return Color.blue 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSBluetoothAlwaysUsageDescription 24 | This App uses Bluetooth to perform OTA on a ESP32 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UIApplicationSupportsIndirectInputEvents 31 | 32 | UILaunchScreen 33 | 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/MyHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyHelper.swift 3 | // 4 | // Created by Claes Hallberg on 7/6/20. 5 | // Copyright © 2020 Claes Hallberg. All rights reserved. 6 | // Licence: MIT 7 | 8 | import Foundation 9 | 10 | /*---------------------------------------------------------------------------- 11 | Load file (fileName: name.extension) return it in Data type 12 | Stored in App main bundle 13 | ----------------------------------------------------------------------------*/ 14 | func getBinFileToData(fileName: String, fileEnding: String) throws -> Data? { 15 | guard let fileURL = Bundle.main.url(forResource: fileName, withExtension: fileEnding) else { return nil } 16 | do { 17 | let fileData = try Data(contentsOf: fileURL) 18 | return Data(fileData) 19 | } catch { 20 | print("Error loading file: \(error)") 21 | return nil 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/firmware/testfile.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaesClaes/Arduino-ESP32-NimBLE-OTA-iOS-SwiftUI/eccee41b66219e0cc6730a047c774ca54f0e8918/iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/firmware/testfile.bin -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32_NimBLE/iOS_OTA_ESP32App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOS_OTA_ESP32App.swift 3 | // iOS_OTA_ESP32 4 | // 5 | // Created by Claes Hallberg on 1/25/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct iOS_OTA_ESP32App: App { 12 | var ble = BLEConnection() 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | .environmentObject(ble) 17 | } 18 | } 19 | } 20 | 21 | --------------------------------------------------------------------------------