├── README.md
├── app.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ └── purpln.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
└── xcuserdata
│ └── purpln.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── app
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ └── secondary.colorset
│ │ └── Contents.json
├── Bluetooth.swift
├── ContentView.swift
├── Info.plist
├── ScanView.swift
├── appApp.swift
└── extensions.swift
└── bluetooth-main.ino
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI and CoreBluetooth with search list, connection/disconnection and no repeats in scan list send/read bytes/strings
2 | ## Connection to esp32 and communication with byte operations
3 |
4 | - scan list
5 | - send/read bytes
6 | - send/read strings
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E4067564254759950063556F /* appApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4067563254759950063556F /* appApp.swift */; };
11 | E4067566254759950063556F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4067565254759950063556F /* ContentView.swift */; };
12 | E4067568254759960063556F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E4067567254759960063556F /* Assets.xcassets */; };
13 | E4067574254759B80063556F /* Bluetooth.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4067573254759B80063556F /* Bluetooth.swift */; };
14 | E490BA6C2634EDD6000276EA /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E490BA6B2634EDD6000276EA /* ScanView.swift */; };
15 | E490BA6F2634EE39000276EA /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E490BA6E2634EE39000276EA /* extensions.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | E4067560254759950063556F /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | E4067563254759950063556F /* appApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = appApp.swift; sourceTree = ""; };
21 | E4067565254759950063556F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | E4067567254759960063556F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | E406756C254759960063556F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
24 | E4067573254759B80063556F /* Bluetooth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bluetooth.swift; sourceTree = ""; };
25 | E490BA6B2634EDD6000276EA /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = ""; };
26 | E490BA6E2634EE39000276EA /* extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = extensions.swift; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | E406755D254759950063556F /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | );
35 | runOnlyForDeploymentPostprocessing = 0;
36 | };
37 | /* End PBXFrameworksBuildPhase section */
38 |
39 | /* Begin PBXGroup section */
40 | E4067557254759950063556F = {
41 | isa = PBXGroup;
42 | children = (
43 | E4067562254759950063556F /* app */,
44 | E4067561254759950063556F /* Products */,
45 | );
46 | sourceTree = "";
47 | };
48 | E4067561254759950063556F /* Products */ = {
49 | isa = PBXGroup;
50 | children = (
51 | E4067560254759950063556F /* app.app */,
52 | );
53 | name = Products;
54 | sourceTree = "";
55 | };
56 | E4067562254759950063556F /* app */ = {
57 | isa = PBXGroup;
58 | children = (
59 | E4067563254759950063556F /* appApp.swift */,
60 | E4067565254759950063556F /* ContentView.swift */,
61 | E490BA6B2634EDD6000276EA /* ScanView.swift */,
62 | E4067573254759B80063556F /* Bluetooth.swift */,
63 | E490BA6E2634EE39000276EA /* extensions.swift */,
64 | E4067567254759960063556F /* Assets.xcassets */,
65 | E406756C254759960063556F /* Info.plist */,
66 | );
67 | path = app;
68 | sourceTree = "";
69 | };
70 | /* End PBXGroup section */
71 |
72 | /* Begin PBXNativeTarget section */
73 | E406755F254759950063556F /* app */ = {
74 | isa = PBXNativeTarget;
75 | buildConfigurationList = E406756F254759960063556F /* Build configuration list for PBXNativeTarget "app" */;
76 | buildPhases = (
77 | E406755C254759950063556F /* Sources */,
78 | E406755D254759950063556F /* Frameworks */,
79 | E406755E254759950063556F /* Resources */,
80 | );
81 | buildRules = (
82 | );
83 | dependencies = (
84 | );
85 | name = app;
86 | productName = app;
87 | productReference = E4067560254759950063556F /* app.app */;
88 | productType = "com.apple.product-type.application";
89 | };
90 | /* End PBXNativeTarget section */
91 |
92 | /* Begin PBXProject section */
93 | E4067558254759950063556F /* Project object */ = {
94 | isa = PBXProject;
95 | attributes = {
96 | LastSwiftUpdateCheck = 1210;
97 | LastUpgradeCheck = 1210;
98 | TargetAttributes = {
99 | E406755F254759950063556F = {
100 | CreatedOnToolsVersion = 12.1;
101 | };
102 | };
103 | };
104 | buildConfigurationList = E406755B254759950063556F /* Build configuration list for PBXProject "app" */;
105 | compatibilityVersion = "Xcode 9.3";
106 | developmentRegion = en;
107 | hasScannedForEncodings = 0;
108 | knownRegions = (
109 | en,
110 | Base,
111 | );
112 | mainGroup = E4067557254759950063556F;
113 | productRefGroup = E4067561254759950063556F /* Products */;
114 | projectDirPath = "";
115 | projectRoot = "";
116 | targets = (
117 | E406755F254759950063556F /* app */,
118 | );
119 | };
120 | /* End PBXProject section */
121 |
122 | /* Begin PBXResourcesBuildPhase section */
123 | E406755E254759950063556F /* Resources */ = {
124 | isa = PBXResourcesBuildPhase;
125 | buildActionMask = 2147483647;
126 | files = (
127 | E4067568254759960063556F /* Assets.xcassets in Resources */,
128 | );
129 | runOnlyForDeploymentPostprocessing = 0;
130 | };
131 | /* End PBXResourcesBuildPhase section */
132 |
133 | /* Begin PBXSourcesBuildPhase section */
134 | E406755C254759950063556F /* Sources */ = {
135 | isa = PBXSourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | E490BA6F2634EE39000276EA /* extensions.swift in Sources */,
139 | E4067566254759950063556F /* ContentView.swift in Sources */,
140 | E490BA6C2634EDD6000276EA /* ScanView.swift in Sources */,
141 | E4067574254759B80063556F /* Bluetooth.swift in Sources */,
142 | E4067564254759950063556F /* appApp.swift in Sources */,
143 | );
144 | runOnlyForDeploymentPostprocessing = 0;
145 | };
146 | /* End PBXSourcesBuildPhase section */
147 |
148 | /* Begin XCBuildConfiguration section */
149 | E406756D254759960063556F /* Debug */ = {
150 | isa = XCBuildConfiguration;
151 | buildSettings = {
152 | ALWAYS_SEARCH_USER_PATHS = NO;
153 | CLANG_ANALYZER_NONNULL = YES;
154 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
155 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
156 | CLANG_CXX_LIBRARY = "libc++";
157 | CLANG_ENABLE_MODULES = YES;
158 | CLANG_ENABLE_OBJC_ARC = YES;
159 | CLANG_ENABLE_OBJC_WEAK = YES;
160 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
161 | CLANG_WARN_BOOL_CONVERSION = YES;
162 | CLANG_WARN_COMMA = YES;
163 | CLANG_WARN_CONSTANT_CONVERSION = YES;
164 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
165 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
166 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
167 | CLANG_WARN_EMPTY_BODY = YES;
168 | CLANG_WARN_ENUM_CONVERSION = YES;
169 | CLANG_WARN_INFINITE_RECURSION = YES;
170 | CLANG_WARN_INT_CONVERSION = YES;
171 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
172 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
173 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
174 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
175 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
176 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
177 | CLANG_WARN_STRICT_PROTOTYPES = YES;
178 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
179 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
180 | CLANG_WARN_UNREACHABLE_CODE = YES;
181 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
182 | COPY_PHASE_STRIP = NO;
183 | DEBUG_INFORMATION_FORMAT = dwarf;
184 | ENABLE_STRICT_OBJC_MSGSEND = YES;
185 | ENABLE_TESTABILITY = YES;
186 | GCC_C_LANGUAGE_STANDARD = gnu11;
187 | GCC_DYNAMIC_NO_PIC = NO;
188 | GCC_NO_COMMON_BLOCKS = YES;
189 | GCC_OPTIMIZATION_LEVEL = 0;
190 | GCC_PREPROCESSOR_DEFINITIONS = (
191 | "DEBUG=1",
192 | "$(inherited)",
193 | );
194 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
195 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
196 | GCC_WARN_UNDECLARED_SELECTOR = YES;
197 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
198 | GCC_WARN_UNUSED_FUNCTION = YES;
199 | GCC_WARN_UNUSED_VARIABLE = YES;
200 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
201 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
202 | MTL_FAST_MATH = YES;
203 | ONLY_ACTIVE_ARCH = YES;
204 | SDKROOT = iphoneos;
205 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
206 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
207 | };
208 | name = Debug;
209 | };
210 | E406756E254759960063556F /* Release */ = {
211 | isa = XCBuildConfiguration;
212 | buildSettings = {
213 | ALWAYS_SEARCH_USER_PATHS = NO;
214 | CLANG_ANALYZER_NONNULL = YES;
215 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
217 | CLANG_CXX_LIBRARY = "libc++";
218 | CLANG_ENABLE_MODULES = YES;
219 | CLANG_ENABLE_OBJC_ARC = YES;
220 | CLANG_ENABLE_OBJC_WEAK = YES;
221 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
222 | CLANG_WARN_BOOL_CONVERSION = YES;
223 | CLANG_WARN_COMMA = YES;
224 | CLANG_WARN_CONSTANT_CONVERSION = YES;
225 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
227 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
228 | CLANG_WARN_EMPTY_BODY = YES;
229 | CLANG_WARN_ENUM_CONVERSION = YES;
230 | CLANG_WARN_INFINITE_RECURSION = YES;
231 | CLANG_WARN_INT_CONVERSION = YES;
232 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
233 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
234 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
235 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
236 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
237 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
238 | CLANG_WARN_STRICT_PROTOTYPES = YES;
239 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
240 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
241 | CLANG_WARN_UNREACHABLE_CODE = YES;
242 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
243 | COPY_PHASE_STRIP = NO;
244 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
245 | ENABLE_NS_ASSERTIONS = NO;
246 | ENABLE_STRICT_OBJC_MSGSEND = YES;
247 | GCC_C_LANGUAGE_STANDARD = gnu11;
248 | GCC_NO_COMMON_BLOCKS = YES;
249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
251 | GCC_WARN_UNDECLARED_SELECTOR = YES;
252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
253 | GCC_WARN_UNUSED_FUNCTION = YES;
254 | GCC_WARN_UNUSED_VARIABLE = YES;
255 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
256 | MTL_ENABLE_DEBUG_INFO = NO;
257 | MTL_FAST_MATH = YES;
258 | SDKROOT = iphoneos;
259 | SWIFT_COMPILATION_MODE = wholemodule;
260 | SWIFT_OPTIMIZATION_LEVEL = "-O";
261 | VALIDATE_PRODUCT = YES;
262 | };
263 | name = Release;
264 | };
265 | E4067570254759960063556F /* Debug */ = {
266 | isa = XCBuildConfiguration;
267 | buildSettings = {
268 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
269 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
270 | CODE_SIGN_STYLE = Automatic;
271 | DEVELOPMENT_TEAM = U3BVQ3ZA36;
272 | ENABLE_PREVIEWS = YES;
273 | INFOPLIST_FILE = app/Info.plist;
274 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
275 | LD_RUNPATH_SEARCH_PATHS = (
276 | "$(inherited)",
277 | "@executable_path/Frameworks",
278 | );
279 | PRODUCT_BUNDLE_IDENTIFIER = gq.purpln.identifier;
280 | PRODUCT_NAME = "$(TARGET_NAME)";
281 | SWIFT_VERSION = 5.0;
282 | TARGETED_DEVICE_FAMILY = "1,2";
283 | };
284 | name = Debug;
285 | };
286 | E4067571254759960063556F /* Release */ = {
287 | isa = XCBuildConfiguration;
288 | buildSettings = {
289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
291 | CODE_SIGN_STYLE = Automatic;
292 | DEVELOPMENT_TEAM = U3BVQ3ZA36;
293 | ENABLE_PREVIEWS = YES;
294 | INFOPLIST_FILE = app/Info.plist;
295 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
296 | LD_RUNPATH_SEARCH_PATHS = (
297 | "$(inherited)",
298 | "@executable_path/Frameworks",
299 | );
300 | PRODUCT_BUNDLE_IDENTIFIER = gq.purpln.identifier;
301 | PRODUCT_NAME = "$(TARGET_NAME)";
302 | SWIFT_VERSION = 5.0;
303 | TARGETED_DEVICE_FAMILY = "1,2";
304 | };
305 | name = Release;
306 | };
307 | /* End XCBuildConfiguration section */
308 |
309 | /* Begin XCConfigurationList section */
310 | E406755B254759950063556F /* Build configuration list for PBXProject "app" */ = {
311 | isa = XCConfigurationList;
312 | buildConfigurations = (
313 | E406756D254759960063556F /* Debug */,
314 | E406756E254759960063556F /* Release */,
315 | );
316 | defaultConfigurationIsVisible = 0;
317 | defaultConfigurationName = Release;
318 | };
319 | E406756F254759960063556F /* Build configuration list for PBXNativeTarget "app" */ = {
320 | isa = XCConfigurationList;
321 | buildConfigurations = (
322 | E4067570254759960063556F /* Debug */,
323 | E4067571254759960063556F /* Release */,
324 | );
325 | defaultConfigurationIsVisible = 0;
326 | defaultConfigurationName = Release;
327 | };
328 | /* End XCConfigurationList section */
329 | };
330 | rootObject = E4067558254759950063556F /* Project object */;
331 | }
332 |
--------------------------------------------------------------------------------
/app.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app.xcodeproj/project.xcworkspace/xcuserdata/purpln.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/purpln/swiftui-bluetooth/b95b4e5b7c9417d82ba33dd8b1a417d7dda0d011/app.xcodeproj/project.xcworkspace/xcuserdata/purpln.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/app.xcodeproj/xcuserdata/purpln.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/app.xcodeproj/xcuserdata/purpln.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | app.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/app/Assets.xcassets/secondary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "1.000",
27 | "green" : "1.000",
28 | "red" : "1.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Bluetooth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bluetooth.swift
3 | // app
4 | //
5 | // Created by Sergey Romanenko on 26.10.2020.
6 | //
7 |
8 | import CoreBluetooth
9 |
10 | protocol BluetoothProtocol {
11 | func state(state: Bluetooth.State)
12 | func list(list: [Bluetooth.Device])
13 | func value(data: Data)
14 | func rssi(value: Int)
15 | }
16 |
17 | final class Bluetooth: NSObject {
18 | static let shared = Bluetooth()
19 | var delegate: BluetoothProtocol?
20 |
21 | var peripherals = [Device]()
22 | var current: CBPeripheral?
23 | var state: State = .unknown { didSet { delegate?.state(state: state) } }
24 |
25 | private var manager: CBCentralManager?
26 | private var readCharacteristic: CBCharacteristic?
27 | private var writeCharacteristic: CBCharacteristic?
28 | private var notifyCharacteristic: CBCharacteristic?
29 | private var timer: Timer?
30 |
31 | private override init() {
32 | super.init()
33 | manager = CBCentralManager(delegate: self, queue: .none)
34 | manager?.delegate = self
35 | }
36 |
37 | func connect(_ peripheral: CBPeripheral) {
38 | if current != nil {
39 | guard let current = current else { return }
40 | manager?.cancelPeripheralConnection(current)
41 | manager?.connect(peripheral, options: nil)
42 | } else { manager?.connect(peripheral, options: nil) }
43 | }
44 |
45 | func disconnect() {
46 | guard let current = current else { return }
47 | manager?.cancelPeripheralConnection(current)
48 | }
49 |
50 | func startScanning() {
51 | peripherals.removeAll()
52 | manager?.scanForPeripherals(withServices: nil, options: nil)
53 | }
54 | func stopScanning() {
55 | peripherals.removeAll()
56 | manager?.stopScan()
57 | }
58 |
59 | func send(_ value: [UInt8]) {
60 | guard let characteristic = writeCharacteristic else { return }
61 | current?.writeValue(Data(value), for: characteristic, type: .withResponse)
62 | }
63 |
64 | enum State { case unknown, resetting, unsupported, unauthorized, poweredOff, poweredOn, error, connected, disconnected }
65 |
66 | struct Device: Identifiable {
67 | let id: Int
68 | let rssi: Int
69 | let uuid: String
70 | let peripheral: CBPeripheral
71 | }
72 | }
73 |
74 | extension Bluetooth: CBCentralManagerDelegate {
75 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
76 | switch manager?.state {
77 | case .unknown: state = .unknown
78 | case .resetting: state = .resetting
79 | case .unsupported: state = .unsupported
80 | case .unauthorized: state = .unauthorized
81 | case .poweredOff: state = .poweredOff
82 | case .poweredOn: state = .poweredOn
83 | default: state = .error
84 | }
85 | }
86 |
87 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
88 | let uuid = String(describing: peripheral.identifier)
89 | let filtered = peripherals.filter{$0.uuid == uuid}
90 | if filtered.count == 0{
91 | guard let _ = peripheral.name else { return }
92 | let new = Device(id: peripherals.count, rssi: RSSI.intValue, uuid: uuid, peripheral: peripheral)
93 | peripherals.append(new)
94 | delegate?.list(list: peripherals)
95 | }
96 | }
97 |
98 | func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { print(error!) }
99 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
100 | current = nil
101 | state = .disconnected
102 | timer?.invalidate()
103 | }
104 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
105 | current = peripheral
106 | state = .connected
107 | peripheral.delegate = self
108 | peripheral.discoverServices(nil)
109 | timer = .scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
110 | peripheral.readRSSI()
111 | }
112 | }
113 | }
114 |
115 | extension Bluetooth: CBPeripheralDelegate {
116 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
117 | guard let services = peripheral.services else { return }
118 | for service in services {
119 | peripheral.discoverCharacteristics(nil, for: service)
120 | }
121 | }
122 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
123 | guard let characteristics = service.characteristics else { return }
124 | for characteristic in characteristics {
125 | switch characteristic.properties {
126 | case .read:
127 | readCharacteristic = characteristic
128 | case .write:
129 | writeCharacteristic = characteristic
130 | case .notify:
131 | notifyCharacteristic = characteristic
132 | peripheral.setNotifyValue(true, for: characteristic)
133 | case .indicate: break //print("indicate")
134 | case .broadcast: break //print("broadcast")
135 | default: break
136 | }
137 | }
138 | }
139 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { }
140 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { }
141 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
142 | guard let value = characteristic.value else { return }
143 | delegate?.value(data: value)
144 | }
145 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
146 | delegate?.rssi(value: Int(truncating: RSSI))
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/app/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // app
4 | //
5 | // Created by Sergey Romanenko on 26.10.2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 | var bluetooth = Bluetooth.shared
12 | @State var presented: Bool = false
13 | @State var list = [Bluetooth.Device]()
14 | @State var isConnected: Bool = Bluetooth.shared.current != nil { didSet { if isConnected { presented.toggle() } } }
15 |
16 | @State var response = Data()
17 | @State var rssi: Int = 0
18 | @State var string: String = ""
19 | @State var value: Float = 0
20 | @State var state: Bool = false { didSet { bluetooth.send([UInt8(state.int)]) } }
21 |
22 | @State var editing = false
23 |
24 | var body: some View {
25 | VStack{
26 | HStack{
27 | Button("scan"){ presented.toggle() }.buttonStyle(appButton()).padding()
28 | Spacer()
29 | if isConnected {
30 | Button("disconnect"){ bluetooth.disconnect() }.buttonStyle(appButton()).padding()
31 | }
32 | }
33 | if isConnected {
34 | Slider(value: Binding( get: { value }, set: {(newValue) in sendValue(newValue) } ), in: 0...100).padding(.horizontal)
35 | Button("toggle"){ state.toggle() }.buttonStyle(appButton())
36 | TextField("string", text: $string, onEditingChanged: { editing = $0 })
37 | .onChange(of: string){ bluetooth.send(Array($0.utf8)) }
38 | .textFieldStyle(appTextField(focused: $editing))
39 | Text("returned byte value from \(bluetooth.current?.name ?? ""): \(response.hex)")
40 | Text("returned string: \(String(data: response, encoding: .utf8) ?? "")")
41 | Text("rssi: \(rssi)")
42 | }
43 | Spacer()
44 | }.sheet(isPresented: $presented){ ScanView(bluetooth: bluetooth, presented: $presented, list: $list, isConnected: $isConnected) }
45 | .onAppear{ bluetooth.delegate = self }
46 | }
47 |
48 | func sendValue(_ value: Float) {
49 | if Int(value) != Int(self.value) {
50 | guard let sendValue = map(Int(value), of: 0...100, to: 0...255) else { return }
51 | bluetooth.send([UInt8(state.int), UInt8(sendValue)])
52 | }
53 | self.value = value
54 | }
55 |
56 | func map(_ value: Int, of: ClosedRange, to: ClosedRange) -> Int? {
57 | guard let ofmin = of.min(), let ofmax = of.max(), let tomin = to.min(), let tomax = to.max() else { return nil }
58 | return Int(tomin + (tomax - tomin) * (value - ofmin) / (ofmax - ofmin))
59 | }
60 | }
61 |
62 | extension ContentView: BluetoothProtocol {
63 | func state(state: Bluetooth.State) {
64 | switch state {
65 | case .unknown: print("◦ .unknown")
66 | case .resetting: print("◦ .resetting")
67 | case .unsupported: print("◦ .unsupported")
68 | case .unauthorized: print("◦ bluetooth disabled, enable it in settings")
69 | case .poweredOff: print("◦ turn on bluetooth")
70 | case .poweredOn: print("◦ everything is ok")
71 | case .error: print("• error")
72 | case .connected:
73 | print("◦ connected to \(bluetooth.current?.name ?? "")")
74 | isConnected = true
75 | case .disconnected:
76 | print("◦ disconnected")
77 | isConnected = false
78 | }
79 | }
80 |
81 | func list(list: [Bluetooth.Device]) { self.list = list }
82 |
83 | func value(data: Data) { response = data }
84 |
85 | func rssi(value: Int) { rssi = value; print(value) }
86 | }
87 |
--------------------------------------------------------------------------------
/app/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 |
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 |
--------------------------------------------------------------------------------
/app/ScanView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScanView.swift
3 | // app
4 | //
5 | // Created by Sergey Romanenko on 25.04.2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ScanView: View {
11 | var bluetooth: Bluetooth
12 | @Binding var presented: Bool
13 | @Binding var list: [Bluetooth.Device]
14 | @Binding var isConnected: Bool
15 |
16 | var body: some View {
17 | HStack {
18 | Spacer()
19 | if isConnected {
20 | Text("connected to \(bluetooth.current?.name ?? "")")
21 | }
22 | Spacer()
23 | Button(action: { presented.toggle() }){
24 | Color(UIColor.secondarySystemBackground).overlay(
25 | Image(systemName: "multiply").foregroundColor(Color(UIColor.systemGray))
26 | ).frame(width: 30, height: 30).cornerRadius(15)
27 | }.padding([.horizontal, .top]).padding(.bottom, 8)
28 | }
29 | if isConnected {
30 | HStack {
31 | Button("disconnect"){ bluetooth.disconnect() }.buttonStyle(appButton()).padding([.horizontal])
32 | Spacer()
33 | }
34 | }
35 | List(list){ peripheral in
36 | Button(action: { bluetooth.connect(peripheral.peripheral) }){
37 | HStack{
38 | Text(peripheral.peripheral.name ?? "")
39 | Spacer()
40 | }
41 | HStack{
42 | Text(peripheral.uuid).font(.system(size: 10)).foregroundColor(.gray)
43 | Spacer()
44 | }
45 | }
46 | }.listStyle(InsetGroupedListStyle()).onAppear{
47 | bluetooth.startScanning()
48 | }.onDisappear{ bluetooth.stopScanning() }.padding(.vertical, 0)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/appApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // appApp.swift
3 | // app
4 | //
5 | // Created by Sergey Romanenko on 26.10.2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct appApp: App {
12 | var body: some Scene {
13 | WindowGroup{
14 | ContentView()
15 | }
16 | }
17 | }
18 |
19 | struct appButton: ButtonStyle {
20 | let color: Color
21 |
22 | public init(color: Color = .accentColor) {
23 | self.color = color
24 | }
25 |
26 | func makeBody(configuration: Self.Configuration) -> some View {
27 | configuration.label
28 | .padding(.horizontal, 8)
29 | .padding(.vertical, 3)
30 | .foregroundColor(.accentColor)
31 | .background(Color.accentColor.opacity(0.2))
32 | .cornerRadius(8)
33 | }
34 | }
35 |
36 | struct appTextField: TextFieldStyle {
37 | @Binding var focused: Bool
38 | func _body(configuration: TextField) -> some View {
39 | configuration
40 | .padding(10)
41 | .background(
42 | RoundedRectangle(cornerRadius: 10, style: .continuous)
43 | .stroke(focused ? Color.accentColor : Color.accentColor.opacity(0.2), lineWidth: 2)
44 | ).padding()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // extensions.swift
3 | // app
4 | //
5 | // Created by Sergey Romanenko on 25.04.2021.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Bool {
11 | var int: Int { self ? 1 : 0 }
12 | }
13 |
14 | extension Data {
15 | var hex: String { map{ String(format: "%02x", $0) }.joined() }
16 | }
17 |
--------------------------------------------------------------------------------
/bluetooth-main.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | BLEServer *pServer = NULL;
7 | BLECharacteristic * pTxCharacteristic;
8 | bool deviceConnected = false;
9 | bool oldDeviceConnected = false;
10 | uint8_t value[2];
11 | #define pin LED_BUILTIN
12 | #define service_uuid "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
13 | #define rx_uuid "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
14 | #define tx_uuid "6E400003-B5A3-F393-E0A9-E50E24DCCA9A"
15 |
16 | class MyServerCallbacks: public BLEServerCallbacks{
17 | void onConnect(BLEServer* pServer){
18 | deviceConnected = true;
19 | };
20 | void onDisconnect(BLEServer* pServer){
21 | deviceConnected = false;
22 | }
23 | };
24 | String values(uint8_t value) {
25 | char hex[2];
26 | sprintf(hex, "%02X", value);
27 | return hex;
28 | };
29 |
30 | class MyCallbacks: public BLECharacteristicCallbacks {
31 | void onWrite(BLECharacteristic *pCharacteristic) {
32 | std::string rxValue = pCharacteristic->getValue();
33 | if (rxValue.length() >0) {
34 | String allValues = "";
35 | String stringValues = "";
36 | for(int i=0; isetCallbacks(new MyServerCallbacks());
53 | BLEService *pService = pServer->createService(service_uuid);
54 | pTxCharacteristic = pService->createCharacteristic(tx_uuid, BLECharacteristic::PROPERTY_NOTIFY);
55 | pTxCharacteristic->addDescriptor(new BLE2902());
56 | BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(rx_uuid, BLECharacteristic::PROPERTY_WRITE);
57 | pRxCharacteristic->setCallbacks(new MyCallbacks());
58 | pService->start();
59 | pServer->getAdvertising()->start();
60 | Serial.println("waiting a client connection to notify...");
61 | }
62 |
63 | void loop(){
64 | if(deviceConnected) {
65 | pTxCharacteristic->setValue((uint8_t*)&value, sizeof(value));
66 | pTxCharacteristic->notify();
67 | delay(10);
68 | }
69 | if (value[0] == 0x01) {
70 | digitalWrite(pin, 1);
71 | }else{
72 | digitalWrite(pin, 0);
73 | }
74 |
75 | if (!deviceConnected && oldDeviceConnected) {
76 | delay(500);
77 | pServer->startAdvertising();
78 | Serial.println("start advertising");
79 | oldDeviceConnected = deviceConnected;
80 | }
81 | if (deviceConnected && !oldDeviceConnected) {
82 | oldDeviceConnected = deviceConnected;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------