├── iOS_OTA_ESP32
├── iOS_OTA_ESP32
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── firmware
│ │ └── testfile.bin
│ ├── iOS_OTA_ESP32App.swift
│ ├── MyHelper.swift
│ ├── Info.plist
│ ├── ContentView.swift
│ └── BluetoothLE.swift
└── iOS_OTA_ESP32.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── claeshallberg.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── xcuserdata
│ └── claeshallberg.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── project.pbxproj
├── LICENSE
├── README.md
└── esp32_ble_ota
└── esp32_ble_ota.ino
/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/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/HEAD/iOS_OTA_ESP32/iOS_OTA_ESP32/firmware/testfile.bin
--------------------------------------------------------------------------------
/iOS_OTA_ESP32/iOS_OTA_ESP32.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClaesClaes/Arduino-ESP32-BLE-OTA-iOS-SwiftUI/HEAD/iOS_OTA_ESP32/iOS_OTA_ESP32.xcodeproj/project.xcworkspace/xcuserdata/claeshallberg.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------