├── LICENSE ├── README.md ├── esp32_ble_ota └── esp32_ble_ota.ino └── iOS_OTA_ESP32 ├── iOS_OTA_ESP32.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── claeshallberg.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── claeshallberg.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── iOS_OTA_ESP32 ├── 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 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Claes Hallberg 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino-ESP32-BLE-OTA-iOS-SwiftUI 2 | 3 | ----------------------------------------------------------------- 4 | *** Please use the NEW (https://github.com/ClaesClaes/Arduino-ESP32-NimBLE-OTA-iOS-SwiftUI) and improved repository instead. The new version use NimBLE BLE stack for superior memory footprint. Stability issues has been resolved. The iOS app also contains more features *** 5 | 6 | ----------------------------------------------------------------- 7 | Arduino example for BLE OTA on a ESP32 using an iOS app 8 | 9 | This is a demo on how to upload firmware (.bin file) from an iOS app to an ESP32. 10 | 11 | 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. 12 | 13 | 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. Does not work on a simulator as they lack physical Bluetooth). 14 | 15 | 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. 16 | 17 | * Ported to Arduino code and based on chegewara example for ESP-IDF: https://github.com/chegewara/esp32-OTA-over-BLE 18 | * Bluetooth class (BLEConnection) in BluetootheLE.swift inspired by: 19 | purpln https://github.com/purpln/bluetooth and 20 | Chris Hulbert http://www.splinter.com.au/2019/05/18/ios-swift-bluetooth-le/ 21 | -------------------------------------------------------------------------------- /esp32_ble_ota/esp32_ble_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 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | //#include 11 | #include "esp_ota_ops.h" 12 | #include "nvs_flash.h" 13 | #include "nvs.h" 14 | 15 | BLEServer* pServer = NULL; 16 | BLECharacteristic * pOtaCharacteristic; 17 | 18 | bool deviceConnected = false; 19 | bool oldDeviceConnected = false; 20 | 21 | #define SERVICE_UUID "4FAFC201-1FB5-459E-8FCC-C5C9C331914B" 22 | #define CHARACTERISTIC_OTA_UUID "62ec0272-3ec5-11eb-b378-0242ac130005" 23 | 24 | static esp_ota_handle_t otaHandler = 0; 25 | static const esp_partition_t *update_partition = NULL; 26 | 27 | bool downloadFlag = false; 28 | 29 | class MyServerCallbacks: public BLEServerCallbacks { 30 | void onConnect(BLEServer* pServer) { 31 | Serial.println("*** App connected"); 32 | deviceConnected = true; 33 | } 34 | 35 | void onDisconnect(BLEServer* pServer) { 36 | deviceConnected = false; 37 | Serial.println("*** App disconnected"); 38 | } 39 | }; 40 | 41 | class otaCallback: public BLECharacteristicCallbacks 42 | { 43 | void onWrite(BLECharacteristic *pCharacteristic) 44 | { 45 | std::string rxData = pCharacteristic->getValue(); 46 | if (!downloadFlag) { //If it's the first packet of OTA since bootup, begin OTA 47 | Serial.println("BeginOTA"); 48 | const esp_partition_t *configured = esp_ota_get_boot_partition(); 49 | const esp_partition_t *running = esp_ota_get_running_partition(); 50 | 51 | if (configured != running) 52 | { 53 | Serial.printf("Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x", configured->address, running->address); 54 | Serial.println("(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)"); 55 | //TODO: Tell client to abort OTA and why? 56 | //TODO: Recover... 57 | esp_ota_end(otaHandler); 58 | } else { 59 | Serial.printf("Running partition type %d subtype %d (offset 0x%08x) ", running->type, running->subtype, running->address); 60 | } 61 | 62 | update_partition = esp_ota_get_next_update_partition(NULL); 63 | assert(update_partition != NULL); 64 | 65 | Serial.printf(" Writing to partition subtype %d at offset 0x%x", update_partition->subtype, update_partition->address); 66 | 67 | esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &otaHandler); 68 | //OTA Add error handling if ota begin fails 69 | downloadFlag = true; 70 | } 71 | if (rxData.length() > 0) // 72 | { 73 | if(esp_ota_write(otaHandler, rxData.c_str(), rxData.length()) != ESP_OK) { 74 | Serial.println("Error: write to flash failed"); 75 | } 76 | 77 | if (rxData.length() < 512) // TODO Asumes at least 512 data bytes (@BLE 4.2). Change to: set via message from client 78 | { 79 | //Final chunk arrived 80 | if (esp_ota_end(otaHandler) != ESP_OK) 81 | { 82 | Serial.println("OTA end failed "); 83 | //TODO Deal with error. Tell client about it 84 | return; 85 | } 86 | Serial.println("Set Boot partion"); 87 | if (ESP_OK == esp_ota_set_boot_partition(update_partition)) 88 | { 89 | esp_ota_end(otaHandler); 90 | Serial.println("Wait 1 sec..."); 91 | delay(1000); 92 | downloadFlag = false; 93 | Serial.println("Restarting..."); 94 | esp_restart(); 95 | return; 96 | } 97 | else 98 | { 99 | Serial.println("Upload Error"); 100 | downloadFlag = false; 101 | //TODO Deal with error. Tell client about it 102 | esp_ota_end(otaHandler); 103 | return; 104 | } 105 | } 106 | } else { 107 | downloadFlag = false; 108 | } 109 | } 110 | }; 111 | 112 | 113 | 114 | void setup() { 115 | Serial.begin(115200); 116 | Serial.println("Starting BLE OTA work!"); 117 | 118 | // 1. Create the BLE Device 119 | BLEDevice::init("ESP32 iOS OTA"); 120 | BLEDevice::setMTU(517); 121 | nvs_flash_erase(); //TODO evaulate the need for this 122 | BLEDevice::setMTU(517); 123 | 124 | // 2. Create the BLE server 125 | pServer = BLEDevice::createServer(); 126 | pServer->setCallbacks(new MyServerCallbacks()); 127 | 128 | // 3. Create BLE Service 129 | BLEService *pService = pServer->createService(SERVICE_UUID); 130 | 131 | // 4. Create BLE Characteristics inside the service(s) 132 | pOtaCharacteristic = pService->createCharacteristic(CHARACTERISTIC_OTA_UUID, 133 | BLECharacteristic::PROPERTY_WRITE_NR); 134 | pOtaCharacteristic->setCallbacks(new otaCallback()); 135 | 136 | // 5. Start the service(s) 137 | pService->start(); 138 | 139 | // 6. Start advertising 140 | pServer->getAdvertising()->addServiceUUID(pService->getUUID()); 141 | pServer->getAdvertising()->start(); 142 | BLEDevice::startAdvertising(); 143 | Serial.println("Waiting a client connection to notify..."); 144 | } 145 | 146 | 147 | void loop() { 148 | if (!deviceConnected && oldDeviceConnected) { 149 | delay(100); 150 | pServer->startAdvertising(); 151 | Serial.println("start advertising"); 152 | oldDeviceConnected = deviceConnected; 153 | } 154 | if (deviceConnected && !oldDeviceConnected) { 155 | Serial.println("main loop started"); 156 | oldDeviceConnected = deviceConnected; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32.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.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS_OTA_ESP32.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 */, 46 | C564792625BE75DC00126AA8 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | C564792625BE75DC00126AA8 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | C564792525BE75DC00126AA8 /* iOS_OTA_ESP32.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | C564792725BE75DC00126AA8 /* iOS_OTA_ESP32 */ = { 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; 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 */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = C564793425BE75DD00126AA8 /* Build configuration list for PBXNativeTarget "iOS_OTA_ESP32" */; 95 | buildPhases = ( 96 | C564792125BE75DC00126AA8 /* Sources */, 97 | C564792225BE75DC00126AA8 /* Frameworks */, 98 | C564792325BE75DC00126AA8 /* Resources */, 99 | ); 100 | buildRules = ( 101 | ); 102 | dependencies = ( 103 | ); 104 | name = iOS_OTA_ESP32; 105 | productName = iOS_OTA_ESP32; 106 | productReference = C564792525BE75DC00126AA8 /* iOS_OTA_ESP32.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" */; 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 */, 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/Preview Content\""; 292 | DEVELOPMENT_TEAM = AL69NHXA3Y; 293 | ENABLE_PREVIEWS = YES; 294 | INFOPLIST_FILE = iOS_OTA_ESP32/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/Preview Content\""; 314 | DEVELOPMENT_TEAM = AL69NHXA3Y; 315 | ENABLE_PREVIEWS = YES; 316 | INFOPLIST_FILE = iOS_OTA_ESP32/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" */ = { 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" */ = { 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.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaesClaes/Arduino-ESP32-BLE-OTA-iOS-SwiftUI/dd05abb8d550912412eec17624f48352dbc806f8/iOS_OTA_ESP32/iOS_OTA_ESP32.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32.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 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32/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/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/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32/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/15/21. 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-0242AC130005") 13 | 14 | private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005")//ESP32 pOtaCharacteristic ESP receive 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 otaCharacteristic: CBCharacteristic? 23 | var state = State.poweredOff 24 | 25 | enum State { 26 | case poweredOff 27 | case restoringConnectingPeripheral(CBPeripheral) 28 | case restoringConnectedPeripheral(CBPeripheral) 29 | case disconnected 30 | case scanning(Countdown) 31 | case connecting(CBPeripheral, Countdown) 32 | case discoveringServices(CBPeripheral, Countdown) 33 | case discoveringCharacteristics(CBPeripheral, Countdown) 34 | case connected(CBPeripheral) 35 | case outOfRange(CBPeripheral) 36 | 37 | var peripheral: CBPeripheral? { 38 | switch self { 39 | case .poweredOff: return nil 40 | case .restoringConnectingPeripheral(let p): return p 41 | case .restoringConnectedPeripheral(let p): return p 42 | case .disconnected: return nil 43 | case .scanning: return nil 44 | case .connecting(let p, _): return p 45 | case .discoveringServices(let p, _): return p 46 | case .discoveringCharacteristics(let p, _): return p 47 | case .connected(let p): return p 48 | case .outOfRange(let p): return p 49 | } 50 | } 51 | } 52 | 53 | 54 | //Used by contentView.swift 55 | @Published var name = "" 56 | @Published var connected = false 57 | 58 | //transfer varibles 59 | var dataToSend = Data() 60 | var dataBuffer = Data() 61 | var chunkSize = 0 62 | var dataLength = 0 63 | var fileEmpty = false 64 | var transferOngoing = true 65 | var writeIterationsComplete = 0 66 | var connectionIterationsComplete = 0 67 | @Published var transferProgress : Double = 0.0 68 | 69 | //Initiate CentralManager 70 | override init() { 71 | super.init() 72 | manager = CBCentralManager(delegate: self, queue: .none) 73 | manager.delegate = self 74 | } 75 | // Callback from CentralManager when State updates (on, off, etc) 76 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 77 | print("\(Date()) CM DidUpdateState") 78 | switch manager.state { 79 | case .unknown: 80 | print("\(Date()) Unknown") 81 | case .resetting: 82 | print("\(Date()) Resetting") 83 | case .unsupported: 84 | print("\(Date()) Unsupported") 85 | case .unauthorized: 86 | print("\(Date()) Bluetooth disabled for this app, pls enable it in settings") 87 | case .poweredOff: 88 | print("\(Date()) turn on bluetooth") 89 | case .poweredOn: 90 | print("\(Date()) everything is ok") 91 | if case .poweredOff = state { 92 | // Firstly, try to reconnect: 93 | // 1. Any peripheralsID stored in UserDefaults? 94 | if let peripheralIdStr = UserDefaults.standard.object(forKey: peripheralIdDefaultsKey) as? String, 95 | // 2. Yes, so convert the String to a UUID type 96 | let peripheralId = UUID(uuidString: peripheralIdStr), 97 | // 3. Compare with UUID's already in the manager 98 | let previouslyConnected = manager 99 | .retrievePeripherals(withIdentifiers: [peripheralId]) 100 | .first { 101 | // 4. If ok then connect 102 | print("\(Date()) CM DidUpdateState: connect from userDefaults") 103 | connect(peripheral: previouslyConnected) 104 | // Next, try for ones that are connected to the system: 105 | } 106 | } 107 | print("\(Date()) End of .poweredOn") 108 | @unknown default: 109 | print("\(Date()) fatal error") 110 | } 111 | } 112 | 113 | // Discovery (scanning) and handling of BLE devices in range 114 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 115 | guard case .scanning = state else { return } 116 | name = String(peripheral.name ?? "unknown") 117 | print("\(Date()) \(name) is found") 118 | print("\(Date()) CM DidDiscover") 119 | manager.stopScan() 120 | connect(peripheral: peripheral) 121 | } 122 | 123 | // Connection established handler 124 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 125 | print("\(Date()) CM DidConnect") 126 | 127 | // set iteration info 128 | connectionIterationsComplete += 1 129 | writeIterationsComplete = 0 130 | 131 | // Clear the data that we may already have 132 | dataToSend.removeAll(keepingCapacity: false) 133 | 134 | // Make sure we get the discovery callbacks 135 | peripheral.delegate = self 136 | 137 | if peripheral.myDesiredCharacteristic == nil { 138 | //peripheral.discoverServices(nil) 139 | discoverServices(peripheral: peripheral) 140 | } else { 141 | setConnected(peripheral: peripheral) 142 | } 143 | } 144 | 145 | // Connection failed 146 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { 147 | print("\(Date()) CM DidFailToConnect") 148 | state = .disconnected 149 | } 150 | // Disconnection (out of range, ...) 151 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { 152 | print("\(Date()) CM DidDisconnectPeripheral") 153 | print("\(Date()) \(peripheral.name ?? "unknown") disconnected") 154 | // Did our currently-connected peripheral just disconnect? 155 | if state.peripheral?.identifier == peripheral.identifier { 156 | name = "" 157 | connected = false 158 | // IME the error codes encountered are: 159 | // 0 = rebooting the peripheral. 160 | // 6 = out of range. 161 | if let error = error, (error as NSError).domain == CBErrorDomain, 162 | let code = CBError.Code(rawValue: (error as NSError).code), 163 | outOfRangeHeuristics.contains(code) { 164 | // Try reconnect without setting a timeout in the state machine. 165 | // With CB, it's like saying 'please reconnect me at any point 166 | // in the future if this peripheral comes back into range'. 167 | print("\(Date()) connect: try reconnect when back in range") 168 | manager.connect(peripheral, options: nil) 169 | state = .outOfRange(peripheral) 170 | } else { 171 | // Likely a deliberate unpairing. 172 | state = .disconnected 173 | } 174 | } 175 | } 176 | 177 | //----------------------------------------- 178 | // Peripheral callbacks 179 | //----------------------------------------- 180 | 181 | // Discover BLE device service(s) 182 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 183 | print("\(Date()) PH didDiscoverServices") 184 | // Ignore services discovered late. 185 | guard case .discoveringServices = state else { 186 | return 187 | } 188 | if let error = error { 189 | print("\(Date()) Failed to discover services: \(error)") 190 | disconnect() 191 | return 192 | } 193 | guard peripheral.myDesiredService != nil else { 194 | print("\(Date()) Desired service missing") 195 | disconnect() 196 | return 197 | } 198 | // All fine so far, go to next step 199 | guard let services = peripheral.services else { return } 200 | for service in services { 201 | peripheral.discoverCharacteristics(nil, for: service) 202 | } 203 | 204 | } 205 | // Discover BLE device Service charachteristics 206 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 207 | print("\(Date()) PH didDiscoverCharacteristicsFor") 208 | 209 | if let error = error { 210 | print("\(Date()) Failed to discover characteristics: \(error)") 211 | disconnect() 212 | return 213 | } 214 | 215 | guard peripheral.myDesiredCharacteristic != nil else { 216 | print("\(Date()) Desired characteristic missing") 217 | disconnect() 218 | return 219 | } 220 | 221 | guard let characteristics = service.characteristics else { 222 | return 223 | } 224 | 225 | 226 | for characteristic in characteristics{ 227 | switch characteristic.uuid { 228 | 229 | case otaCharacteristicId: 230 | otaCharacteristic = characteristic 231 | print("\(Date())send OTA firmware to: \(otaCharacteristic!.uuid as Any)") 232 | peripheral.setNotifyValue(false, for: characteristic) 233 | 234 | default: 235 | print("\(Date()) unknown") 236 | } 237 | } 238 | setConnected(peripheral: peripheral) 239 | } 240 | // The BLE peripheral device sent some notify data. Deal with it! 241 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?){ 242 | print("\(Date()) PH didUpdateValueFor") 243 | if let error = error { 244 | print(error) 245 | return 246 | } 247 | /* 248 | if let data = characteristic.value{ 249 | // deal with incoming data 250 | } 251 | */ 252 | } 253 | // Called when .withResponse is used. 254 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?){ 255 | print("\(Date()) PH didWriteValueFor") 256 | if let error = error { 257 | print("\(Date()) Error writing to characteristic: \(error)") 258 | return 259 | } 260 | } 261 | // Callback indicating peripheral notifying state 262 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){ 263 | print("\(Date()) PH didUpdateNotificationStateFor") 264 | print("\(Date()) PH \(characteristic)") 265 | if error == nil { 266 | print("\(Date()) Notification Set OK, isNotifying: \(characteristic.isNotifying)") 267 | if !characteristic.isNotifying { 268 | print("\(Date()) isNotifying is false, set to true again!") 269 | peripheral.setNotifyValue(true, for: characteristic) 270 | } 271 | } 272 | } 273 | 274 | /*------------------------------------------------------------------------- 275 | Functions 276 | -------------------------------------------------------------------------*/ 277 | // Scanning for device with a specific Service UUID (myDesiredServiceId) 278 | func startScanning(){ 279 | print("\(Date()) FUNC StartScanning") 280 | guard manager.state == .poweredOn else { 281 | print("\(Date()) Cannot scan, BT is not powered on") 282 | return 283 | } 284 | manager.scanForPeripherals(withServices: [myDesiredServiceId], options: nil) 285 | state = .scanning(Countdown(seconds: 10, closure: { 286 | self.manager.stopScan() 287 | self.state = .disconnected 288 | print("\(Date()) Scan timed out") 289 | })) 290 | } 291 | // Disconnect by user request 292 | func disconnect(forget: Bool = false) { 293 | print("\(Date()) FUNC Disconnect") 294 | if let peripheral = state.peripheral { 295 | manager.cancelPeripheralConnection(peripheral) 296 | } 297 | if forget { 298 | UserDefaults.standard.removeObject(forKey: peripheralIdDefaultsKey) 299 | UserDefaults.standard.synchronize() 300 | } 301 | state = .disconnected 302 | connected = false 303 | } 304 | // Connect to the device from the scanning 305 | func connect(peripheral: CBPeripheral){ 306 | print("\(Date()) FUNC Connect") 307 | if connected { 308 | manager.cancelPeripheralConnection(peripheral) 309 | }else{ 310 | // Connect! 311 | print("\(Date()) connect: connect inside func connect()") 312 | manager.connect(peripheral, options: nil) 313 | name = String(peripheral.name ?? "unknown") 314 | print("\(Date()) \(name) is found") 315 | state = .connecting(peripheral, Countdown(seconds: 10, closure: { 316 | self.manager.cancelPeripheralConnection(self.state.peripheral!) 317 | self.state = .disconnected 318 | self.connected = false 319 | print("\(Date()) Connect timed out") 320 | })) 321 | } 322 | } 323 | // Discover Services of a device 324 | func discoverServices(peripheral: CBPeripheral) { 325 | print("\(Date()) FUNC DiscoverServices") 326 | peripheral.delegate = self 327 | peripheral.discoverServices([myDesiredServiceId]) 328 | state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: { 329 | self.disconnect() 330 | print("\(Date()) Could not discover services") 331 | })) 332 | } 333 | 334 | // Discover Characteristics of a Services 335 | func discoverCharacteristics(peripheral: CBPeripheral) { 336 | print("\(Date()) FUNC DiscoverCharacteristics") 337 | guard let myDesiredService = peripheral.myDesiredService else { 338 | self.disconnect() 339 | return 340 | } 341 | peripheral.discoverCharacteristics([myDesiredCharacteristicId], for: myDesiredService) 342 | state = .discoveringCharacteristics(peripheral, 343 | Countdown(seconds: 10, 344 | closure: { 345 | self.disconnect() 346 | print("\(Date()) Could not discover characteristics") 347 | })) 348 | } 349 | 350 | func setConnected(peripheral: CBPeripheral) { 351 | print("\(Date()) FUNC SetConnected") 352 | print("\(Date()) Max write value with response: \(peripheral.maximumWriteValueLength(for: .withResponse))") 353 | print("\(Date()) Max write value without response: \(peripheral.maximumWriteValueLength(for: .withoutResponse))") 354 | guard let myDesiredCharacteristic = peripheral.myDesiredCharacteristic 355 | else { 356 | print("\(Date()) Missing characteristic") 357 | disconnect() 358 | return 359 | } 360 | // Remember the ID for startup reconnecting. 361 | UserDefaults.standard.set(peripheral.identifier.uuidString, forKey: peripheralIdDefaultsKey) 362 | UserDefaults.standard.synchronize() 363 | 364 | peripheral.setNotifyValue(true, for: myDesiredCharacteristic) 365 | state = .connected(peripheral) 366 | connected = true 367 | name = String(peripheral.name ?? "unknown") 368 | } 369 | 370 | 371 | // Peripheral callback when its ready to receive more data without response 372 | func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { 373 | if transferOngoing { 374 | writeDataToPeriheral(characteristic: otaCharacteristic!) 375 | } 376 | } 377 | 378 | func sendFile(filename: String, fileEnding: String) { 379 | print("\(Date()) FUNC SendFile") 380 | 381 | // 1. Get the data from the file(name) and copy data to dataBUffer 382 | guard let data: Data = try? getBinFileToData(fileName: filename, fileEnding: fileEnding) else { 383 | print("\(Date()) failed to open file") 384 | return 385 | } 386 | dataBuffer = data 387 | dataLength = dataBuffer.count 388 | transferOngoing = true 389 | fileEmpty = false 390 | writeDataToPeriheral(characteristic: otaCharacteristic!) 391 | } 392 | 393 | func writeDataToPeriheral(characteristic: CBCharacteristic) { 394 | 395 | // 1. Get the peripheral and it's transfer characteristic 396 | guard let discoveredPeripheral = state.peripheral else {return} 397 | chunkSize = discoveredPeripheral.maximumWriteValueLength (for: .withoutResponse) 398 | var range:Range 399 | // 2. Loop through and send each chunk to the BLE device 400 | // check to see if number of iterations completed and peripheral can accept more data 401 | while transferOngoing && discoveredPeripheral.canSendWriteWithoutResponse { 402 | // 3. Create a range based on the length of data to return 403 | range = (0.. ()) { 448 | timer = Timer.scheduledTimer(withTimeInterval: seconds, 449 | repeats: false, block: { _ in 450 | closure() 451 | }) 452 | } 453 | deinit { 454 | timer.invalidate() 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32/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/25/21. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | @EnvironmentObject var ble : BLEConnection 13 | var body: some View{ 14 | VStack{ 15 | VStack { 16 | Text("Device : \(ble.name)") 17 | Text("Upload progress: \(ble.transferProgress, specifier: "%.1f") %") 18 | } 19 | HStack{ 20 | Button(action: { 21 | ble.startScanning() 22 | }){ 23 | Text("connect").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 24 | } 25 | Button(action: { 26 | ble.disconnect(forget: false) 27 | }){ 28 | Text("disconnect").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 29 | } 30 | Button(action: { 31 | ble.disconnect(forget: true) 32 | }){ 33 | Text("forget").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 34 | } 35 | } 36 | HStack{ 37 | Button(action: { 38 | ble.sendFile(filename: "testfile", fileEnding: ".bin") 39 | }){ 40 | Text("send .bin to ESP32 over OTA").padding().overlay(RoundedRectangle(cornerRadius: 15).stroke(colorChange(ble.connected), lineWidth: 2)) 41 | } 42 | 43 | } 44 | HStack{ 45 | Spacer() 46 | } 47 | }.padding().accentColor(colorChange(ble.connected)) 48 | } 49 | } 50 | 51 | func colorChange(_ connected:Bool) -> Color{ 52 | if connected{ 53 | return Color.green 54 | }else{ 55 | return Color.blue 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32/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/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, fileEnding: .bin) 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/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/firmware/testfile.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaesClaes/Arduino-ESP32-BLE-OTA-iOS-SwiftUI/dd05abb8d550912412eec17624f48352dbc806f8/iOS_OTA_ESP32/iOS_OTA_ESP32/firmware/testfile.bin -------------------------------------------------------------------------------- /iOS_OTA_ESP32/iOS_OTA_ESP32/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 | --------------------------------------------------------------------------------