├── 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 | --------------------------------------------------------------------------------