Initial import: app build 7
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
# fastlane specific
|
||||
**/fastlane/report.xml
|
||||
|
||||
# deliver temporary files
|
||||
**/fastlane/Preview.html
|
||||
|
||||
# snapshot generated screenshots
|
||||
**/fastlane/screenshots
|
||||
|
||||
# scan temporary files
|
||||
**/fastlane/test_output
|
||||
55
BluefruitPlayground-SimulatedBluetooth-Info.plist
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BP Simulated</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>This app needs access to Bluetooth to connect to Circuit Playground Bluefruit devices</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>This app needs access to Bluetooth to connect to Circuit Playground Bluefruit devices</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
1449
BluefruitPlayground.xcodeproj/project.pbxproj
Normal file
7
BluefruitPlayground.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:CPX.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B3623A523C900498069"
|
||||
BuildableName = "BluefruitPlayground-SimulateBluetooth.app"
|
||||
BlueprintName = "BluefruitPlayground-SimulateBluetooth"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B3623A523C900498069"
|
||||
BuildableName = "BluefruitPlayground-SimulateBluetooth.app"
|
||||
BlueprintName = "BluefruitPlayground-SimulateBluetooth"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B3623A523C900498069"
|
||||
BuildableName = "BluefruitPlayground-SimulateBluetooth.app"
|
||||
BlueprintName = "BluefruitPlayground-SimulateBluetooth"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1120"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A92B9F08234DE4A2002374F0"
|
||||
BuildableName = "Bluefruit Playground.app"
|
||||
BlueprintName = "BluefruitPlayground"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B2B23A5238000498069"
|
||||
BuildableName = "BluefruitPlaygroundUITests.xctest"
|
||||
BlueprintName = "BluefruitPlaygroundUITests"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A92B9F08234DE4A2002374F0"
|
||||
BuildableName = "Bluefruit Playground.app"
|
||||
BlueprintName = "BluefruitPlayground"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A92B9F08234DE4A2002374F0"
|
||||
BuildableName = "Bluefruit Playground.app"
|
||||
BlueprintName = "BluefruitPlayground"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B2B23A5238000498069"
|
||||
BuildableName = "BluefruitPlaygroundUITests.xctest"
|
||||
BlueprintName = "BluefruitPlaygroundUITests"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B2B23A5238000498069"
|
||||
BuildableName = "BluefruitPlaygroundUITests.xctest"
|
||||
BlueprintName = "BluefruitPlaygroundUITests"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A9A08B3623A523C900498069"
|
||||
BuildableName = "BluefruitPlayground-SimulateBluetooth.app"
|
||||
BlueprintName = "BluefruitPlayground-SimulateBluetooth"
|
||||
ReferencedContainer = "container:BluefruitPlayground.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
BluefruitPlayground.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:BluefruitPlayground.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
//
|
||||
// BleManager.swift
|
||||
// BleManager
|
||||
//
|
||||
// Created by Antonio García on 13/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
#if COMMANDLINE
|
||||
#else
|
||||
import MSWeakTimer
|
||||
#endif
|
||||
|
||||
class BleManager: NSObject {
|
||||
// Configuration
|
||||
private static let kStopScanningWhenConnectingToPeripheral = false
|
||||
private static let kAlwaysAllowDuplicateKeys = true
|
||||
|
||||
// Singleton
|
||||
static let shared = BleManager()
|
||||
|
||||
// Ble
|
||||
var centralManager: CBCentralManager?
|
||||
private var centralManagerPoweredOnSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
// Scanning
|
||||
var isScanning = false
|
||||
private var isScanningWaitingToStart = false
|
||||
private var scanningServicesFilter: [CBUUID]?
|
||||
internal var peripheralsFound = [UUID: BlePeripheral]()
|
||||
private var peripheralsFoundLock = NSLock()
|
||||
|
||||
// Connecting
|
||||
private var connectionTimeoutTimers = [UUID: MSWeakTimer]()
|
||||
|
||||
// Notifications
|
||||
enum NotificationUserInfoKey: String {
|
||||
case uuid = "uuid"
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
centralManagerPoweredOnSemaphore.wait()
|
||||
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.global(qos: .background), options: [:])
|
||||
// centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [:])
|
||||
}
|
||||
|
||||
deinit {
|
||||
scanningServicesFilter?.removeAll()
|
||||
peripheralsFound.removeAll()
|
||||
}
|
||||
|
||||
public var state: CBManagerState {
|
||||
return centralManager?.state ?? .unknown
|
||||
}
|
||||
|
||||
func restoreCentralManager() {
|
||||
DLog("Restoring central manager")
|
||||
/*
|
||||
guard centralManager?.delegate !== self else {
|
||||
DLog("No need to restore it. It it still ours")
|
||||
return
|
||||
}*/
|
||||
|
||||
// Restore peripherals status
|
||||
peripheralsFoundLock.lock()
|
||||
|
||||
for (_, blePeripheral) in peripheralsFound {
|
||||
blePeripheral.peripheral.delegate = nil
|
||||
}
|
||||
|
||||
let knownIdentifiers = Array(peripheralsFound.keys)
|
||||
let knownPeripherals = centralManager?.retrievePeripherals(withIdentifiers: knownIdentifiers)
|
||||
|
||||
peripheralsFound.removeAll()
|
||||
|
||||
if let knownPeripherals = knownPeripherals {
|
||||
for peripheral in knownPeripherals {
|
||||
DLog("Adding prediscovered peripheral: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
discovered(peripheral: peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
peripheralsFoundLock.unlock()
|
||||
|
||||
// Restore central manager delegate if was changed
|
||||
centralManager?.delegate = self
|
||||
|
||||
if isScanning {
|
||||
startScan()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scan
|
||||
func startScan(withServices services: [CBUUID]? = nil) {
|
||||
centralManagerPoweredOnSemaphore.wait()
|
||||
centralManagerPoweredOnSemaphore.signal()
|
||||
|
||||
isScanningWaitingToStart = true
|
||||
guard let centralManager = centralManager, centralManager.state != .poweredOff && centralManager.state != .unauthorized && centralManager.state != .unsupported else {
|
||||
DLog("startScan failed because central manager is not ready")
|
||||
return
|
||||
}
|
||||
|
||||
scanningServicesFilter = services
|
||||
|
||||
guard centralManager.state == .poweredOn else {
|
||||
DLog("startScan failed because central manager is not powered on")
|
||||
return
|
||||
}
|
||||
|
||||
// DLog("start scan")
|
||||
isScanning = true
|
||||
NotificationCenter.default.post(name: .didStartScanning, object: nil)
|
||||
|
||||
let options = BleManager.kAlwaysAllowDuplicateKeys ? [CBCentralManagerScanOptionAllowDuplicatesKey: true] : nil
|
||||
centralManager.scanForPeripherals(withServices: services, options: options)
|
||||
isScanningWaitingToStart = false
|
||||
}
|
||||
|
||||
func stopScan() {
|
||||
// DLog("stop scan")
|
||||
centralManager?.stopScan()
|
||||
isScanning = false
|
||||
isScanningWaitingToStart = false
|
||||
NotificationCenter.default.post(name: .didStopScanning, object: nil)
|
||||
}
|
||||
|
||||
func peripherals() -> [BlePeripheral] {
|
||||
peripheralsFoundLock.lock(); defer { peripheralsFoundLock.unlock() }
|
||||
return Array(peripheralsFound.values)
|
||||
}
|
||||
|
||||
func connectedPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connected}
|
||||
}
|
||||
|
||||
func connectingPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connecting}
|
||||
}
|
||||
|
||||
func connectedOrConnectingPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connected || $0.state == .connecting}
|
||||
}
|
||||
|
||||
func refreshPeripherals() {
|
||||
stopScan()
|
||||
|
||||
peripheralsFoundLock.lock()
|
||||
// Don't remove connnected or connecting peripherals
|
||||
for (identifier, peripheral) in peripheralsFound {
|
||||
if peripheral.state != .connected && peripheral.state != .connecting {
|
||||
peripheralsFound.removeValue(forKey: identifier)
|
||||
}
|
||||
}
|
||||
peripheralsFoundLock.unlock()
|
||||
|
||||
//
|
||||
NotificationCenter.default.post(name: .didUnDiscoverPeripheral, object: nil)
|
||||
startScan(withServices: scanningServicesFilter)
|
||||
}
|
||||
|
||||
// MARK: - Connection Management
|
||||
func connect(to peripheral: BlePeripheral, timeout: TimeInterval? = nil, shouldNotifyOnConnection: Bool = false, shouldNotifyOnDisconnection: Bool = false, shouldNotifyOnNotification: Bool = false) {
|
||||
|
||||
centralManagerPoweredOnSemaphore.wait()
|
||||
centralManagerPoweredOnSemaphore.signal()
|
||||
|
||||
// Stop scanning when connecting to a peripheral
|
||||
if BleManager.kStopScanningWhenConnectingToPeripheral {
|
||||
stopScan()
|
||||
}
|
||||
|
||||
// Connect
|
||||
NotificationCenter.default.post(name: .willConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
|
||||
//DLog("connect")
|
||||
var options: [String: Bool]?
|
||||
|
||||
#if os(OSX)
|
||||
#else
|
||||
if shouldNotifyOnConnection || shouldNotifyOnDisconnection || shouldNotifyOnNotification {
|
||||
options = [CBConnectPeripheralOptionNotifyOnConnectionKey: shouldNotifyOnConnection, CBConnectPeripheralOptionNotifyOnDisconnectionKey: shouldNotifyOnDisconnection, CBConnectPeripheralOptionNotifyOnNotificationKey: shouldNotifyOnNotification]
|
||||
}
|
||||
#endif
|
||||
|
||||
if let timeout = timeout {
|
||||
connectionTimeoutTimers[peripheral.identifier] = MSWeakTimer.scheduledTimer(withTimeInterval: timeout, target: self, selector: #selector(connectionTimeoutFired), userInfo: peripheral.identifier, repeats: false, dispatchQueue: .global(qos: .background))
|
||||
}
|
||||
centralManager?.connect(peripheral.peripheral, options: options)
|
||||
}
|
||||
|
||||
@objc private func connectionTimeoutFired(timer: MSWeakTimer) {
|
||||
let peripheralIdentifier = timer.userInfo() as! UUID
|
||||
DLog("connection timeout fired: \(peripheralIdentifier)")
|
||||
connectionTimeoutTimers[peripheralIdentifier] = nil
|
||||
|
||||
NotificationCenter.default.post(name: .willDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheralIdentifier])
|
||||
|
||||
if let blePeripheral = peripheralsFound[peripheralIdentifier] {
|
||||
centralManager?.cancelPeripheralConnection(blePeripheral.peripheral)
|
||||
} else {
|
||||
DLog("simulate disconnection")
|
||||
// The blePeripheral is available on peripheralsFound, so simulate the disconnection
|
||||
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheralIdentifier])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect(from peripheral: BlePeripheral) {
|
||||
|
||||
DLog("disconnect")
|
||||
NotificationCenter.default.post(name: .willDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
centralManager?.cancelPeripheralConnection(peripheral.peripheral)
|
||||
}
|
||||
|
||||
func reconnecToPeripherals(withIdentifiers identifiers: [UUID], withServices services: [CBUUID], timeout: Double? = nil) -> Bool {
|
||||
var reconnecting = false
|
||||
|
||||
let knownPeripherals = centralManager?.retrievePeripherals(withIdentifiers: identifiers)
|
||||
if let peripherals = knownPeripherals?.filter({identifiers.contains($0.identifier)}), !peripherals.isEmpty {
|
||||
for peripheral in peripherals {
|
||||
discovered(peripheral: peripheral)
|
||||
if let blePeripheral = peripheralsFound[peripheral.identifier] {
|
||||
connect(to: blePeripheral, timeout: timeout)
|
||||
reconnecting = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let connectedPeripherals = centralManager?.retrieveConnectedPeripherals(withServices: services)
|
||||
if let peripherals = connectedPeripherals?.filter({identifiers.contains($0.identifier)}), !peripherals.isEmpty {
|
||||
for peripheral in peripherals {
|
||||
discovered(peripheral: peripheral)
|
||||
if let blePeripheral = peripheralsFound[peripheral.identifier] {
|
||||
connect(to: blePeripheral, timeout: timeout)
|
||||
reconnecting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reconnecting
|
||||
}
|
||||
|
||||
private func discovered(peripheral: CBPeripheral, advertisementData: [String: Any]? = nil, rssi: Int? = nil) {
|
||||
peripheralsFoundLock.lock(); defer { peripheralsFoundLock.unlock() }
|
||||
|
||||
if let existingPeripheral = peripheralsFound[peripheral.identifier] {
|
||||
existingPeripheral.lastSeenTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
if let rssi = rssi, rssi != 127 { // only update rssi value if is defined ( 127 means undefined )
|
||||
existingPeripheral.rssi = rssi
|
||||
}
|
||||
|
||||
if let advertisementData = advertisementData {
|
||||
for (key, value) in advertisementData {
|
||||
existingPeripheral.advertisement.advertisementData.updateValue(value, forKey: key)
|
||||
}
|
||||
}
|
||||
peripheralsFound[peripheral.identifier] = existingPeripheral
|
||||
} else { // New peripheral found
|
||||
let blePeripheral = BlePeripheral(peripheral: peripheral, advertisementData: advertisementData, rssi: rssi)
|
||||
peripheralsFound[peripheral.identifier] = blePeripheral
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
func peripheral(from notification: Notification) -> BlePeripheral? {
|
||||
guard let uuid = notification.userInfo?[NotificationUserInfoKey.uuid.rawValue] as? UUID else { return nil }
|
||||
|
||||
return peripheral(with: uuid)
|
||||
}
|
||||
|
||||
func peripheral(with uuid: UUID) -> BlePeripheral? {
|
||||
return peripheralsFound[uuid]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBCentralManagerDelegate
|
||||
extension BleManager: CBCentralManagerDelegate {
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
|
||||
DLog("centralManagerDidUpdateState: \(central.state.rawValue)")
|
||||
// Unlock state lock if we have a known state
|
||||
if central.state == .poweredOn || central.state == .poweredOff || central.state == .unsupported || central.state == .unauthorized {
|
||||
centralManagerPoweredOnSemaphore.signal()
|
||||
}
|
||||
|
||||
// Scanning
|
||||
if central.state == .poweredOn {
|
||||
if isScanningWaitingToStart {
|
||||
startScan(withServices: scanningServicesFilter) // Continue scanning now that bluetooth is back
|
||||
}
|
||||
} else {
|
||||
if isScanning {
|
||||
isScanningWaitingToStart = true
|
||||
}
|
||||
isScanning = false
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .didUpdateBleState, object: nil)
|
||||
}
|
||||
|
||||
/*
|
||||
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
|
||||
}*/
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
||||
// DLog("didDiscover: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
let rssi = RSSI.intValue
|
||||
DispatchQueue.main.async { // This Fixes iOS12 race condition on cached filtered peripherals. TODO: investigate
|
||||
self.discovered(peripheral: peripheral, advertisementData: advertisementData, rssi: rssi)
|
||||
NotificationCenter.default.post(name: .didDiscoverPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
DLog("didConnect: \(peripheral.identifier)")
|
||||
|
||||
// Remove connection timeout if exists
|
||||
if let timer = connectionTimeoutTimers[peripheral.identifier] {
|
||||
timer.invalidate()
|
||||
connectionTimeoutTimers[peripheral.identifier] = nil
|
||||
}
|
||||
|
||||
// Send notification
|
||||
NotificationCenter.default.post(name: .didConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
DLog("didFailToConnect")
|
||||
|
||||
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
DLog("didDisconnectPeripheral")
|
||||
|
||||
// Clean
|
||||
peripheralsFound[peripheral.identifier]?.reset()
|
||||
|
||||
// Notify
|
||||
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
|
||||
// Remove from peripheral list (after sending notification so the receiving objects can query about the peripheral before being removed)
|
||||
peripheralsFoundLock.lock()
|
||||
peripheralsFound.removeValue(forKey: peripheral.identifier)
|
||||
peripheralsFoundLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Notifications
|
||||
extension Notification.Name {
|
||||
private static let kPrefix = Bundle.main.bundleIdentifier!
|
||||
static let didUpdateBleState = Notification.Name(kPrefix+".didUpdateBleState")
|
||||
static let didStartScanning = Notification.Name(kPrefix+".didStartScanning")
|
||||
static let didStopScanning = Notification.Name(kPrefix+".didStopScanning")
|
||||
static let didDiscoverPeripheral = Notification.Name(kPrefix+".didDiscoverPeripheral")
|
||||
static let didUnDiscoverPeripheral = Notification.Name(kPrefix+".didUnDiscoverPeripheral")
|
||||
static let willConnectToPeripheral = Notification.Name(kPrefix+".willConnectToPeripheral")
|
||||
static let didConnectToPeripheral = Notification.Name(kPrefix+".didConnectToPeripheral")
|
||||
static let willDisconnectFromPeripheral = Notification.Name(kPrefix+".willDisconnectFromPeripheral")
|
||||
static let didDisconnectFromPeripheral = Notification.Name(kPrefix+".didDisconnectFromPeripheral")
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// BlePeripheral+Battery.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 22/06/2017.
|
||||
// Copyright © 2017 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Costants
|
||||
static let kBatteryServiceUUID = CBUUID(string: "180F")
|
||||
static let kBatteryCharacteristicUUID = CBUUID(string: "2A19")
|
||||
|
||||
// MARK: - Actions
|
||||
func readBatteryLevel(handler: @escaping ((Int, Error?) -> Void)) {
|
||||
self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in
|
||||
guard error == nil, let characteristic = characteristic else { DLog("Error reading battery characteristic: \(error?.localizedDescription ?? "")"); return }
|
||||
|
||||
self.readCharacteristic(characteristic) { (result, error) in
|
||||
guard error == nil, let data = result as? Data, data.count >= 1 else {
|
||||
DLog("Error reading battery level: \(error?.localizedDescription ?? "")")
|
||||
handler(-1, error)
|
||||
return
|
||||
}
|
||||
|
||||
let level = Int(data[0]) // from 0 to 100
|
||||
handler(level, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startReadingBatteryLevel(handler: @escaping ((Int) -> Void)) {
|
||||
|
||||
self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in
|
||||
guard error == nil, let characteristic = characteristic else { DLog("Error starting read for battery characteristic: \(error?.localizedDescription ?? "")"); return }
|
||||
|
||||
// Read current value
|
||||
self.readCharacteristic(characteristic) { (result, error) in
|
||||
guard error == nil, let data = result as? Data, data.count >= 1 else { DLog("Error reading battery level: \(error?.localizedDescription ?? "")"); return }
|
||||
|
||||
let level = Int(data[0]) // from 0 to 100
|
||||
handler(level)
|
||||
}
|
||||
|
||||
// Enable notifications to receive value changes
|
||||
self.enableNotify(for: characteristic, handler: { error in
|
||||
guard error == nil else { DLog("Error receiving notify for battery level"); return }
|
||||
guard let data = characteristic.value, data.count >= 1 else { DLog("Invalid data receiving notify for battery level"); return }
|
||||
|
||||
let level = Int(data[0]) // from 0 to 100
|
||||
handler(level)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stopReadingBatteryLevel() {
|
||||
self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in
|
||||
guard error == nil, let characteristic = characteristic else { DLog("Error stopping read for battery characteristic: \(error?.localizedDescription ?? "")"); return }
|
||||
|
||||
self.disableNotify(for: characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
func isBatteryAdvertised() -> Bool {
|
||||
return advertisement.services?.contains(BlePeripheral.kBatteryServiceUUID) ?? false
|
||||
}
|
||||
|
||||
func hasBattery() -> Bool {
|
||||
return peripheral.services?.first(where: {$0.uuid == BlePeripheral.kBatteryServiceUUID}) != nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
//
|
||||
// BlePeripheral+Uart.swift
|
||||
// Calibration
|
||||
//
|
||||
// Created by Antonio García on 19/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Config
|
||||
private static let kDebugLog = false
|
||||
|
||||
// Costants
|
||||
static let kUartServiceUUID = CBUUID(string: "6e400001-b5a3-f393-e0a9-e50e24dcca9e")
|
||||
static let kUartTxCharacteristicUUID = CBUUID(string: "6e400002-b5a3-f393-e0a9-e50e24dcca9e")
|
||||
static let kUartRxCharacteristicUUID = CBUUID(string: "6e400003-b5a3-f393-e0a9-e50e24dcca9e")
|
||||
//private static let kUartTxMaxBytes = 20
|
||||
static let kUartReplyDefaultTimeout = 2.0 // seconds
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var uartRxCharacteristic: CBCharacteristic?
|
||||
static var uartTxCharacteristic: CBCharacteristic?
|
||||
static var uartTxCharacteristicWriteType: CBCharacteristicWriteType?
|
||||
static var sendSequentiallyCancelled: Bool = false
|
||||
}
|
||||
|
||||
private var uartRxCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.uartRxCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.uartRxCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var uartTxCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.uartTxCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.uartTxCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var uartTxCharacteristicWriteType: CBCharacteristicWriteType? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.uartTxCharacteristicWriteType) as! CBCharacteristicWriteType?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.uartTxCharacteristicWriteType, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var isSendSequentiallyCancelled: Bool {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.sendSequentiallyCancelled) as! Bool
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.sendSequentiallyCancelled, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Errors
|
||||
enum PeripheralUartError: Error {
|
||||
case invalidCharacteristic
|
||||
case enableNotifyFailed
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
func uartEnable(uartRxHandler: ((Data?, UUID, Error?) -> Void)?, completion: ((Error?) -> Void)?) {
|
||||
|
||||
// Get uart communications characteristic
|
||||
characteristic(uuid: BlePeripheral.kUartTxCharacteristicUUID, serviceUuid: BlePeripheral.kUartServiceUUID) { [unowned self] (characteristic, error) in
|
||||
guard let characteristic = characteristic, error == nil else {
|
||||
completion?(error != nil ? error : PeripheralUartError.invalidCharacteristic)
|
||||
return
|
||||
}
|
||||
|
||||
self.uartTxCharacteristic = characteristic
|
||||
self.uartTxCharacteristicWriteType = characteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse:.withResponse
|
||||
//self.uartTxCharacteristicWriteType = .withResponse // Debug: force withResponse
|
||||
|
||||
self.characteristic(uuid: BlePeripheral.kUartRxCharacteristicUUID, serviceUuid: BlePeripheral.kUartServiceUUID) { [unowned self] (characteristic, error) in
|
||||
guard let characteristic = characteristic, error == nil else {
|
||||
completion?(error != nil ? error : PeripheralUartError.invalidCharacteristic)
|
||||
return
|
||||
}
|
||||
|
||||
// Get characteristic info
|
||||
self.uartRxCharacteristic = characteristic
|
||||
|
||||
// Prepare notification handler
|
||||
let notifyHandler: ((Error?) -> Void)? = { [unowned self] error in
|
||||
let value = characteristic.value
|
||||
if let value = value, BlePeripheral.kDebugLog == true, error == nil {
|
||||
UartLogManager.log(data: value, type: .uartRx)
|
||||
}
|
||||
|
||||
uartRxHandler?(value, self.identifier, error)
|
||||
}
|
||||
|
||||
// Enable notifications
|
||||
if !characteristic.isNotifying {
|
||||
self.enableNotify(for: characteristic, handler: notifyHandler, completion: { error in
|
||||
completion?(error != nil ? error : (characteristic.isNotifying ? nil : PeripheralUartError.enableNotifyFailed))
|
||||
})
|
||||
} else {
|
||||
self.updateNotifyHandler(for: characteristic, handler: notifyHandler)
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isUartEnabled() -> Bool {
|
||||
return uartRxCharacteristic != nil && uartTxCharacteristic != nil && uartTxCharacteristicWriteType != nil && uartRxCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func uartDisable() {
|
||||
// Clear all Uart specific data
|
||||
defer {
|
||||
uartRxCharacteristic = nil
|
||||
uartTxCharacteristic = nil
|
||||
uartTxCharacteristicWriteType = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = uartRxCharacteristic, characteristic.isNotifying else { return }
|
||||
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
// MARK: - Send
|
||||
func uartSend(data: Data?, progress: ((Float)->Void)? = nil, completion: ((Error?) -> Void)? = nil) {
|
||||
guard let data = data else { completion?(nil); return }
|
||||
|
||||
guard let uartTxCharacteristic = uartTxCharacteristic, let uartTxCharacteristicWriteType = uartTxCharacteristicWriteType else {
|
||||
DLog("Command Error: characteristic no longer valid")
|
||||
completion?(PeripheralUartError.invalidCharacteristic)
|
||||
return
|
||||
}
|
||||
|
||||
// Split data in kUartTxMaxBytes bytes packets
|
||||
var offset = 0
|
||||
var writtenSize = 0
|
||||
|
||||
//let maxPacketSize = peripheral.maximumWriteValueLength(for: uartTxCharacteristicWriteType)
|
||||
let maxPacketSize = peripheral.maximumWriteValueLength(for: .withoutResponse) // Use .withoutResponse event if sending .withResponse or didWriteValueFor is not called when using a larger packet
|
||||
|
||||
repeat {
|
||||
|
||||
let packetSize = min(data.count-offset, maxPacketSize)
|
||||
let packet = data.subdata(in: offset..<offset+packetSize)
|
||||
let writeStartingOffset = offset
|
||||
self.write(data: packet, for: uartTxCharacteristic, type: uartTxCharacteristicWriteType) { error in
|
||||
if let error = error {
|
||||
DLog("write packet at offset: \(writeStartingOffset) error: \(error)")
|
||||
} else {
|
||||
//DLog("tx offset:\(offset): \(hexDescription(data: packet))")
|
||||
//DLog("uart tx write (hex): \(hexDescription(data: packet))")
|
||||
// DLog("uart tx write (dec): \(decimalDescription(data: packet))")
|
||||
// DLog("uart tx write (utf8): \(String(data: packet, encoding: .utf8) ?? "<invalid>")")
|
||||
|
||||
writtenSize += packetSize
|
||||
if BlePeripheral.kDebugLog {
|
||||
UartLogManager.log(data: packet, type: .uartTx)
|
||||
}
|
||||
}
|
||||
|
||||
if writtenSize >= data.count {
|
||||
progress?(1)
|
||||
completion?(error)
|
||||
}
|
||||
else {
|
||||
progress?(Float(writtenSize) / Float(data.count))
|
||||
}
|
||||
}
|
||||
offset += packetSize
|
||||
} while offset < data.count
|
||||
|
||||
}
|
||||
|
||||
func uartSendEachPacketSequentially(data: Data?, withResponseEveryPacketCount: Int, progress: ((Float)->Void)? = nil, completion: ((Error?) -> Void)? = nil) {
|
||||
guard let data = data else { completion?(nil); return }
|
||||
|
||||
guard let uartTxCharacteristic = uartTxCharacteristic else {//}, let uartTxCharacteristicWriteType = uartTxCharacteristicWriteType else {
|
||||
DLog("Command Error: characteristic no longer valid")
|
||||
completion?(PeripheralUartError.invalidCharacteristic)
|
||||
return
|
||||
}
|
||||
|
||||
isSendSequentiallyCancelled = false
|
||||
uartSentPacket(data: data, offset: 0, uartTxCharacteristic: uartTxCharacteristic, withResponseEveryPacketCount: withResponseEveryPacketCount, numPacketsRemainingForDelay: withResponseEveryPacketCount, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
func uartCancelOngoingSendPacketSequentiallyInMainThread() {
|
||||
isSendSequentiallyCancelled = true
|
||||
}
|
||||
|
||||
private func uartSentPacket(data: Data, offset: Int, uartTxCharacteristic: CBCharacteristic, withResponseEveryPacketCount: Int, numPacketsRemainingForDelay: Int, progress: ((Float)->Void)? = nil, completion: ((Error?) -> Void)? = nil) {
|
||||
|
||||
//let maxPacketSize = peripheral.maximumWriteValueLength(for: uartTxCharacteristicWriteType)
|
||||
let maxPacketSize = peripheral.maximumWriteValueLength(for: .withoutResponse) // Use .withoutResponse event if sending .withResponse or didWriteValueFor is not called when using a larger packet
|
||||
let packetSize = min(data.count-offset, maxPacketSize)
|
||||
let packet = data.subdata(in: offset..<offset+packetSize)
|
||||
let writeStartingOffset = offset
|
||||
let uartTxCharacteristicWriteType: CBCharacteristicWriteType = numPacketsRemainingForDelay <= 0 ? .withResponse : .withoutResponse // Send a packet .withResponse to force wait until receive response and avoid dropping packets if the peripheral is not processing them fast enough
|
||||
|
||||
self.write(data: packet, for: uartTxCharacteristic, type: uartTxCharacteristicWriteType) { error in
|
||||
|
||||
var writtenSize = writeStartingOffset
|
||||
if let error = error {
|
||||
DLog("write packet at offset: \(writeStartingOffset) error: \(error)")
|
||||
} else {
|
||||
DLog("uart tx \(uartTxCharacteristicWriteType == .withResponse ? "withResponse":"withoutResponse") offset: \(writeStartingOffset): \(HexUtils.hexDescription(data: packet))")
|
||||
|
||||
writtenSize += packet.count
|
||||
if BlePeripheral.kDebugLog {
|
||||
UartLogManager.log(data: packet, type: .uartTx)
|
||||
}
|
||||
|
||||
if !self.isSendSequentiallyCancelled && writtenSize < data.count {
|
||||
DispatchQueue.main.async { // Send in main thread to avoid commandqueue function nesting limit if there is a lot of data to send
|
||||
self.uartSentPacket(data: data, offset: writtenSize, uartTxCharacteristic: uartTxCharacteristic, withResponseEveryPacketCount: withResponseEveryPacketCount, numPacketsRemainingForDelay: numPacketsRemainingForDelay <= 0 ? withResponseEveryPacketCount : numPacketsRemainingForDelay-1, progress: progress, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call completion handlers in main thread
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if self.isSendSequentiallyCancelled {
|
||||
completion?(nil)
|
||||
}
|
||||
else if writtenSize >= data.count {
|
||||
progress?(1)
|
||||
completion?(error)
|
||||
}
|
||||
else {
|
||||
progress?(Float(writtenSize) / Float(data.count))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uartSendAndWaitReply(data: Data?, writeProgress: ((Float)->Void)? = nil, writeCompletion: ((Error?) -> Void)? = nil, readTimeout: Double? = BlePeripheral.kUartReplyDefaultTimeout, readCompletion: @escaping CapturedReadCompletionHandler) {
|
||||
|
||||
guard let data = data else {
|
||||
if let writeCompletion = writeCompletion {
|
||||
writeCompletion(nil)
|
||||
} else {
|
||||
// If no writeCompletion defined, move the error result to the readCompletion
|
||||
readCompletion(nil, nil)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
guard let uartTxCharacteristic = uartTxCharacteristic, /*let uartTxCharacteristicWriteType = uartTxCharacteristicWriteType, */let uartRxCharacteristic = uartRxCharacteristic else {
|
||||
DLog("Command Error: characteristic no longer valid")
|
||||
if let writeCompletion = writeCompletion {
|
||||
writeCompletion(PeripheralUartError.invalidCharacteristic)
|
||||
} else {
|
||||
// If no writeCompletion defined, move the error result to the readCompletion
|
||||
readCompletion(nil, PeripheralUartError.invalidCharacteristic)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Split data in kUartTxMaxBytes bytes packets
|
||||
var offset = 0
|
||||
var writtenSize = 0
|
||||
//let maxPacketSize = peripheral.maximumWriteValueLength(for: .withResponse)
|
||||
let maxPacketSize = peripheral.maximumWriteValueLength(for: .withoutResponse) // Use .withoutResponse event if sending .withResponse or didWriteValueFor is not called when using a larger packet
|
||||
repeat {
|
||||
let packetSize = min(data.count-offset, maxPacketSize)
|
||||
let packet = data.subdata(in: offset..<offset+packetSize)
|
||||
offset += packetSize
|
||||
|
||||
writeAndCaptureNotify(data: packet, for: uartTxCharacteristic, writeCompletion: { error in
|
||||
if let error = error {
|
||||
DLog("write packet at offset: \(offset) error: \(error)")
|
||||
} else {
|
||||
DLog("uart tx writeAndWait (hex): \(HexUtils.hexDescription(data: packet))")
|
||||
// DLog("uart tx writeAndWait (dec): \(decimalDescription(data: packet))")
|
||||
// DLog("uart tx writeAndWait (utf8): \(String(data: packet, encoding: .utf8) ?? "<invalid>")")
|
||||
|
||||
writtenSize += packetSize
|
||||
}
|
||||
|
||||
if writtenSize >= data.count {
|
||||
writeProgress?(1)
|
||||
writeCompletion?(error)
|
||||
}
|
||||
else {
|
||||
writeProgress?(Float(writtenSize) / Float(data.count))
|
||||
}
|
||||
}, readCharacteristic: uartRxCharacteristic, readTimeout: readTimeout, readCompletion: readCompletion)
|
||||
|
||||
} while offset < data.count
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
func isUartAdvertised() -> Bool {
|
||||
return advertisement.services?.contains(BlePeripheral.kUartServiceUUID) ?? false
|
||||
}
|
||||
|
||||
func hasUart() -> Bool {
|
||||
return peripheral.services?.first(where: {$0.uuid == BlePeripheral.kUartServiceUUID}) != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data + CRC
|
||||
extension Data {
|
||||
mutating func appendCrc() {
|
||||
var dataBytes = [UInt8](repeating: 0, count: count)
|
||||
copyBytes(to: &dataBytes, count: count)
|
||||
|
||||
var crc: UInt8 = 0
|
||||
for i in dataBytes { //add all bytes
|
||||
crc = crc &+ i
|
||||
}
|
||||
crc = ~crc //invert
|
||||
|
||||
append(&crc, count: 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,627 @@
|
|||
//
|
||||
// BlePeripheral.swift
|
||||
// NewtManager
|
||||
//
|
||||
// Created by Antonio García on 12/09/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
#if COMMANDLINE
|
||||
#else
|
||||
import MSWeakTimer
|
||||
#endif
|
||||
|
||||
// TODO: Modernize completion blocks to use Swift.Result
|
||||
|
||||
class BlePeripheral: NSObject {
|
||||
// Config
|
||||
private static var kProfileCharacteristicUpdates = true
|
||||
|
||||
// Notifications
|
||||
enum NotificationUserInfoKey: String {
|
||||
case uuid = "uuid"
|
||||
case name = "name"
|
||||
case invalidatedServices = "invalidatedServices"
|
||||
}
|
||||
|
||||
enum PeripheralError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
||||
// Data
|
||||
var peripheral: CBPeripheral
|
||||
var rssi: Int? // rssi only is updated when a non undefined value is received from CoreBluetooth. Note: this is slighty different to the CoreBluetooth implementation, because it will not be updated with undefined values
|
||||
var lastSeenTime: CFAbsoluteTime
|
||||
|
||||
var identifier: UUID {
|
||||
return peripheral.identifier
|
||||
}
|
||||
|
||||
var name: String? {
|
||||
return peripheral.name
|
||||
}
|
||||
|
||||
var state: CBPeripheralState {
|
||||
return peripheral.state
|
||||
}
|
||||
|
||||
struct Advertisement {
|
||||
var advertisementData: [String: Any]
|
||||
|
||||
init(advertisementData: [String: Any]?) {
|
||||
self.advertisementData = advertisementData ?? [String: Any]()
|
||||
}
|
||||
|
||||
// Advertisement data formatted
|
||||
var localName: String? {
|
||||
return advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
||||
}
|
||||
|
||||
var manufacturerData: Data? {
|
||||
return advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
||||
}
|
||||
|
||||
var manufacturerHexDescription: String? {
|
||||
guard let manufacturerData = manufacturerData else { return nil }
|
||||
return HexUtils.hexDescription(data: manufacturerData)
|
||||
// return String(data: manufacturerData, encoding: .utf8)
|
||||
}
|
||||
|
||||
var manufacturerIdentifier: Data? {
|
||||
guard let manufacturerData = manufacturerData, manufacturerData.count >= 2 else { return nil }
|
||||
let manufacturerIdentifierData = manufacturerData[0..<2]
|
||||
return manufacturerIdentifierData
|
||||
}
|
||||
|
||||
var services: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
var servicesOverflow: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
var servicesSolicited: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
var serviceData: [CBUUID: Data]? {
|
||||
return advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
|
||||
}
|
||||
|
||||
var txPower: Int? {
|
||||
let number = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
|
||||
return number?.intValue
|
||||
}
|
||||
|
||||
var isConnectable: Bool? {
|
||||
let connectableNumber = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber
|
||||
return connectableNumber?.boolValue
|
||||
}
|
||||
}
|
||||
var advertisement: Advertisement
|
||||
|
||||
typealias CapturedReadCompletionHandler = ((_ value: Any?, _ error: Error?) -> Void)
|
||||
private class CaptureReadHandler {
|
||||
|
||||
var identifier: String
|
||||
var result: CapturedReadCompletionHandler
|
||||
var timeoutTimer: MSWeakTimer?
|
||||
var timeoutAction: ((String)->())?
|
||||
var isNotifyOmitted: Bool
|
||||
|
||||
init(identifier: String, result: @escaping CapturedReadCompletionHandler, timeout: Double?, timeoutAction:((String)->())?, isNotifyOmitted: Bool = false) {
|
||||
self.identifier = identifier
|
||||
self.result = result
|
||||
self.isNotifyOmitted = isNotifyOmitted
|
||||
|
||||
if let timeout = timeout {
|
||||
self.timeoutAction = timeoutAction
|
||||
timeoutTimer = MSWeakTimer.scheduledTimer(withTimeInterval: timeout, target: self, selector: #selector(timerFired), userInfo: nil, repeats: false, dispatchQueue: .global(qos: .background))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func timerFired() {
|
||||
timeoutTimer?.invalidate()
|
||||
timeoutTimer = nil
|
||||
result(nil, PeripheralError.timeout)
|
||||
timeoutAction?(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func timeOutRemoveCaptureHandler(identifier: String) { // Default behaviour for a capture handler timeout
|
||||
guard captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) else { return }
|
||||
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
|
||||
|
||||
// Remove capture handler
|
||||
captureReadHandlers.remove(at: index)
|
||||
finishedExecutingCommand(error: PeripheralError.timeout)
|
||||
}
|
||||
|
||||
// Internal data
|
||||
private var notifyHandlers = [String: ((Error?) -> Void)]() // Nofify handlers for each service-characteristic
|
||||
private var captureReadHandlers = [CaptureReadHandler]()
|
||||
private var commandQueue = CommandQueue<BleCommand>()
|
||||
|
||||
// Profiling
|
||||
//private var profileStartTime: CFTimeInterval = 0
|
||||
|
||||
// MARK: - Init
|
||||
init(peripheral: CBPeripheral, advertisementData: [String: Any]?, rssi: Int?) {
|
||||
self.peripheral = peripheral
|
||||
self.advertisement = Advertisement(advertisementData: advertisementData)
|
||||
self.rssi = rssi
|
||||
self.lastSeenTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
super.init()
|
||||
self.peripheral.delegate = self
|
||||
// DLog("create peripheral: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
commandQueue.executeHandler = executeCommand
|
||||
}
|
||||
|
||||
deinit {
|
||||
//DLog("peripheral deinit")
|
||||
}
|
||||
|
||||
func reset() {
|
||||
rssi = nil
|
||||
notifyHandlers.removeAll()
|
||||
captureReadHandlers.removeAll()
|
||||
commandQueue.first()?.isCancelled = true // Stop current command if is processing
|
||||
commandQueue.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Discover
|
||||
func discover(serviceUuids: [CBUUID]?, completion: ((Error?) -> Void)?) {
|
||||
let command = BleCommand(type: .discoverService, parameters: serviceUuids, completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func discover(characteristicUuids: [CBUUID]?, service: CBService, completion: ((Error?) -> Void)?) {
|
||||
let command = BleCommand(type: .discoverCharacteristic, parameters: [characteristicUuids as Any, service], completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func discover(characteristicUuids: [CBUUID]?, serviceUuid: CBUUID, completion: ((Error?) -> Void)?) {
|
||||
// Discover service
|
||||
discover(serviceUuids: [serviceUuid]) { [unowned self] error in
|
||||
guard error == nil else {
|
||||
completion?(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let service = self.peripheral.services?.first(where: {$0.uuid == serviceUuid}) else {
|
||||
completion?(BleCommand.CommandError.invalidService)
|
||||
return
|
||||
}
|
||||
|
||||
// Discover characteristic
|
||||
self.discover(characteristicUuids: characteristicUuids, service: service, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverDescriptors(characteristic: CBCharacteristic, completion: ((Error?) -> Void)?) {
|
||||
let command = BleCommand(type: .discoverDescriptor, parameters: [characteristic], completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
// MARK: - Service
|
||||
func discoveredService(uuid: CBUUID) -> CBService? {
|
||||
let service = peripheral.services?.first(where: {$0.uuid == uuid})
|
||||
return service
|
||||
}
|
||||
|
||||
func service(uuid: CBUUID, completion: ((CBService?, Error?) -> Void)?) {
|
||||
|
||||
if let discoveredService = discoveredService(uuid: uuid) { // Service was already discovered
|
||||
completion?(discoveredService, nil)
|
||||
} else {
|
||||
discover(serviceUuids: [uuid], completion: { [unowned self] (error) in // Discover service
|
||||
var discoveredService: CBService?
|
||||
if error == nil {
|
||||
discoveredService = self.discoveredService(uuid: uuid)
|
||||
}
|
||||
completion?(discoveredService, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Characteristic
|
||||
func discoveredCharacteristic(uuid: CBUUID, service: CBService) -> CBCharacteristic? {
|
||||
let characteristic = service.characteristics?.first(where: {$0.uuid == uuid})
|
||||
return characteristic
|
||||
}
|
||||
|
||||
func characteristic(uuid: CBUUID, service: CBService, completion: ((CBCharacteristic?, Error?) -> Void)?) {
|
||||
|
||||
if let discoveredCharacteristic = discoveredCharacteristic(uuid: uuid, service: service) { // Characteristic was already discovered
|
||||
completion?(discoveredCharacteristic, nil)
|
||||
} else {
|
||||
discover(characteristicUuids: [uuid], service: service, completion: { [unowned self] (error) in // Discover characteristic
|
||||
var discoveredCharacteristic: CBCharacteristic?
|
||||
if error == nil {
|
||||
discoveredCharacteristic = self.discoveredCharacteristic(uuid: uuid, service: service)
|
||||
}
|
||||
completion?(discoveredCharacteristic, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func characteristic(uuid: CBUUID, serviceUuid: CBUUID, completion: ((CBCharacteristic?, Error?) -> Void)?) {
|
||||
if let discoveredService = discoveredService(uuid: serviceUuid) { // Service was already discovered
|
||||
characteristic(uuid: uuid, service: discoveredService, completion: completion)
|
||||
} else { // Discover service
|
||||
service(uuid: serviceUuid) { (service, error) in
|
||||
if let service = service, error == nil { // Discover characteristic
|
||||
self.characteristic(uuid: uuid, service: service, completion: completion)
|
||||
} else {
|
||||
completion?(nil, error != nil ? error: BleCommand.CommandError.invalidService)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableNotify(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)?, completion: ((Error?) -> Void)? = nil) {
|
||||
let command = BleCommand(type: .setNotify, parameters: [characteristic, true, handler as Any], completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func disableNotify(for characteristic: CBCharacteristic, completion: ((Error?) -> Void)? = nil) {
|
||||
let command = BleCommand(type: .setNotify, parameters: [characteristic, false], completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func updateNotifyHandler(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)? = nil) {
|
||||
let identifier = handlerIdentifier(from: characteristic)
|
||||
if notifyHandlers[identifier] == nil {
|
||||
DLog("Warning: trying to update inexistent notifyHandler")
|
||||
}
|
||||
notifyHandlers[identifier] = handler
|
||||
}
|
||||
|
||||
func readCharacteristic(_ characteristic: CBCharacteristic, completion readCompletion: @escaping CapturedReadCompletionHandler) {
|
||||
let command = BleCommand(type: .readCharacteristic, parameters: [characteristic, readCompletion as Any], completion: nil)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func write(data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, completion: ((Error?) -> Void)? = nil) {
|
||||
let command = BleCommand(type: .writeCharacteristic, parameters: [characteristic, type, data], completion: completion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
func writeAndCaptureNotify(data: Data, for characteristic: CBCharacteristic, writeCompletion: ((Error?) -> Void)? = nil, readCharacteristic: CBCharacteristic, readTimeout: Double? = nil, readCompletion: CapturedReadCompletionHandler? = nil) {
|
||||
let type: CBCharacteristicWriteType = .withResponse // Force write with response
|
||||
let command = BleCommand(type: .writeCharacteristicAndWaitNofity, parameters: [characteristic, type, data, readCharacteristic, readCompletion as Any, readTimeout as Any], timeout: readTimeout, completion: writeCompletion)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
// MARK: - Descriptors
|
||||
func readDescriptor(_ descriptor: CBDescriptor, completion readCompletion: @escaping CapturedReadCompletionHandler) {
|
||||
let command = BleCommand(type: .readDescriptor, parameters: [descriptor, readCompletion as Any], completion: nil)
|
||||
commandQueue.append(command)
|
||||
}
|
||||
|
||||
// MARK: - Rssi
|
||||
func readRssi() {
|
||||
peripheral.readRSSI()
|
||||
}
|
||||
|
||||
// MARK: - Command Queue
|
||||
private class BleCommand: Equatable {
|
||||
enum CommandType {
|
||||
case discoverService
|
||||
case discoverCharacteristic
|
||||
case discoverDescriptor
|
||||
case setNotify
|
||||
case readCharacteristic
|
||||
case writeCharacteristic
|
||||
case writeCharacteristicAndWaitNofity
|
||||
case readDescriptor
|
||||
}
|
||||
|
||||
enum CommandError: Error {
|
||||
case invalidService
|
||||
}
|
||||
|
||||
var type: CommandType
|
||||
var parameters: [Any]?
|
||||
var completion: ((Error?) -> Void)?
|
||||
var isCancelled = false
|
||||
|
||||
init(type: CommandType, parameters: [Any]?, timeout: Double? = nil, completion: ((Error?) -> Void)?) {
|
||||
self.type = type
|
||||
self.parameters = parameters
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func completion(withError error: Error?) {
|
||||
completion?(error)
|
||||
}
|
||||
|
||||
static func == (left: BleCommand, right: BleCommand) -> Bool {
|
||||
return left.type == right.type
|
||||
}
|
||||
}
|
||||
|
||||
private func executeCommand(command: BleCommand) {
|
||||
|
||||
switch command.type {
|
||||
case .discoverService:
|
||||
discoverService(with: command)
|
||||
case .discoverCharacteristic:
|
||||
discoverCharacteristic(with: command)
|
||||
case .discoverDescriptor:
|
||||
discoverDescriptor(with: command)
|
||||
case .setNotify:
|
||||
setNotify(with: command)
|
||||
case .readCharacteristic:
|
||||
readCharacteristic(with: command)
|
||||
case .writeCharacteristic, .writeCharacteristicAndWaitNofity:
|
||||
write(with: command)
|
||||
case .readDescriptor:
|
||||
readDescriptor(with: command)
|
||||
}
|
||||
}
|
||||
|
||||
private func handlerIdentifier(from characteristic: CBCharacteristic) -> String {
|
||||
return "\(characteristic.service.uuid.uuidString)-\(characteristic.uuid.uuidString)"
|
||||
}
|
||||
|
||||
private func handlerIdentifier(from descriptor: CBDescriptor) -> String {
|
||||
return "\(descriptor.characteristic.service.uuid.uuidString)-\(descriptor.characteristic.uuid.uuidString)-\(descriptor.uuid.uuidString)"
|
||||
}
|
||||
|
||||
private func finishedExecutingCommand(error: Error?) {
|
||||
//DLog("finishedExecutingCommand")
|
||||
|
||||
// Result Callback
|
||||
if let command = commandQueue.first(), !command.isCancelled {
|
||||
command.completion(withError: error)
|
||||
}
|
||||
commandQueue.executeNext()
|
||||
}
|
||||
|
||||
// MARK: - Commands
|
||||
private func discoverService(with command: BleCommand) {
|
||||
var serviceUuids = command.parameters as? [CBUUID]
|
||||
let discoverAll = serviceUuids == nil
|
||||
|
||||
// Remove services already discovered from the query
|
||||
if let services = peripheral.services, let serviceUuidsToDiscover = serviceUuids {
|
||||
for (i, serviceUuid) in serviceUuidsToDiscover.enumerated().reversed() {
|
||||
if services.contains(where: {$0.uuid == serviceUuid}) {
|
||||
serviceUuids!.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover remaining uuids
|
||||
if discoverAll || (serviceUuids != nil && serviceUuids!.count > 0) {
|
||||
peripheral.discoverServices(serviceUuids)
|
||||
} else {
|
||||
// Everthing was already discovered
|
||||
finishedExecutingCommand(error: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func discoverCharacteristic(with command: BleCommand) {
|
||||
var characteristicUuids = command.parameters![0] as? [CBUUID]
|
||||
let discoverAll = characteristicUuids == nil
|
||||
let service = command.parameters![1] as! CBService
|
||||
|
||||
// Remove services already discovered from the query
|
||||
if let characteristics = service.characteristics, let characteristicUuidsToDiscover = characteristicUuids {
|
||||
for (i, characteristicUuid) in characteristicUuidsToDiscover.enumerated().reversed() {
|
||||
if characteristics.contains(where: {$0.uuid == characteristicUuid}) {
|
||||
characteristicUuids!.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover remaining uuids
|
||||
if discoverAll || (characteristicUuids != nil && characteristicUuids!.count > 0) {
|
||||
//DLog("discover \(characteristicUuids == nil ? "all": String(characteristicUuids!.count)) characteristics for \(service.uuid.uuidString)")
|
||||
peripheral.discoverCharacteristics(characteristicUuids, for: service)
|
||||
} else {
|
||||
// Everthing was already discovered
|
||||
finishedExecutingCommand(error: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func discoverDescriptor(with command: BleCommand) {
|
||||
let characteristic = command.parameters![0] as! CBCharacteristic
|
||||
peripheral.discoverDescriptors(for: characteristic)
|
||||
}
|
||||
|
||||
private func setNotify(with command: BleCommand) {
|
||||
let characteristic = command.parameters![0] as! CBCharacteristic
|
||||
let enabled = command.parameters![1] as! Bool
|
||||
let identifier = handlerIdentifier(from: characteristic)
|
||||
if enabled {
|
||||
let handler = command.parameters![2] as? ((Error?) -> Void)
|
||||
notifyHandlers[identifier] = handler
|
||||
} else {
|
||||
notifyHandlers.removeValue(forKey: identifier)
|
||||
}
|
||||
peripheral.setNotifyValue(enabled, for: characteristic)
|
||||
}
|
||||
|
||||
private func readCharacteristic(with command: BleCommand) {
|
||||
let characteristic = command.parameters!.first as! CBCharacteristic
|
||||
let completion = command.parameters![1] as! CapturedReadCompletionHandler
|
||||
|
||||
let identifier = handlerIdentifier(from: characteristic)
|
||||
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
|
||||
captureReadHandlers.append(captureReadHandler)
|
||||
|
||||
peripheral.readValue(for: characteristic)
|
||||
}
|
||||
|
||||
private func write(with command: BleCommand) {
|
||||
let characteristic = command.parameters![0] as! CBCharacteristic
|
||||
let writeType = command.parameters![1] as! CBCharacteristicWriteType
|
||||
let data = command.parameters![2] as! Data
|
||||
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
|
||||
if writeType == .withoutResponse {
|
||||
if !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
|
||||
let readCharacteristic = command.parameters![3] as! CBCharacteristic
|
||||
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
|
||||
let timeout = command.parameters![5] as? Double
|
||||
|
||||
let identifier = handlerIdentifier(from: readCharacteristic)
|
||||
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
|
||||
captureReadHandlers.append(captureReadHandler)
|
||||
}
|
||||
|
||||
finishedExecutingCommand(error: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func readDescriptor(with command: BleCommand) {
|
||||
let descriptor = command.parameters!.first as! CBDescriptor
|
||||
let completion = command.parameters![1] as! CapturedReadCompletionHandler
|
||||
|
||||
let identifier = handlerIdentifier(from: descriptor)
|
||||
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
|
||||
captureReadHandlers.append(captureReadHandler)
|
||||
|
||||
peripheral.readValue(for: descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBPeripheralDelegate
|
||||
extension BlePeripheral: CBPeripheralDelegate {
|
||||
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
|
||||
DLog("peripheralDidUpdateName: \(name ?? "{ No Name }")")
|
||||
NotificationCenter.default.post(name: .peripheralDidUpdateName, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.name.rawValue: name as Any])
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
|
||||
DLog("didModifyServices")
|
||||
NotificationCenter.default.post(name: .peripheralDidModifyServices, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.invalidatedServices.rawValue: invalidatedServices])
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
DLog("didDiscoverServices for: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
DLog("didDiscoverCharacteristicsFor: \(service.uuid.uuidString)")
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
|
||||
let identifier = handlerIdentifier(from: characteristic)
|
||||
|
||||
/*
|
||||
if (BlePeripheral.kProfileCharacteristicUpdates) {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
let elapsedTime = currentTime - profileStartTime
|
||||
DLog("elapsed: \(String(format: "%.1f", elapsedTime * 1000))")
|
||||
profileStartTime = currentTime
|
||||
}
|
||||
*/
|
||||
//DLog("didUpdateValueFor \(characteristic.uuid.uuidString): \(String(data: characteristic.value ?? Data(), encoding: .utf8) ?? "<invalid>")")
|
||||
|
||||
// Check if waiting to capture this read
|
||||
var isNotifyOmitted = false
|
||||
var hasCaptureHandler = false
|
||||
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
|
||||
hasCaptureHandler = true
|
||||
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
|
||||
|
||||
// Remove capture handler
|
||||
let captureReadHandler = captureReadHandlers.remove(at: index)
|
||||
|
||||
// DLog("captureReadHandlers postRemove count: \(captureReadHandlers.count)")
|
||||
|
||||
// Cancel timeout timer
|
||||
captureReadHandler.timeoutTimer?.invalidate()
|
||||
captureReadHandler.timeoutTimer = nil
|
||||
|
||||
// Send result
|
||||
let value = characteristic.value
|
||||
// DLog("updated value: \(String(data: value!, encoding: .utf8)!)")
|
||||
captureReadHandler.result(value, error)
|
||||
|
||||
isNotifyOmitted = captureReadHandler.isNotifyOmitted
|
||||
}
|
||||
|
||||
// Notify
|
||||
if !isNotifyOmitted {
|
||||
if let notifyHandler = notifyHandlers[identifier] {
|
||||
|
||||
//let currentTime = CACurrentMediaTime()
|
||||
notifyHandler(error)
|
||||
//DLog("elapsed: \(String(format: "%.1f", (CACurrentMediaTime() - currentTime) * 1000))")
|
||||
}
|
||||
}
|
||||
|
||||
if hasCaptureHandler {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
if let command = commandQueue.first(), !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
|
||||
let characteristic = command.parameters![3] as! CBCharacteristic
|
||||
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
|
||||
let timeout = command.parameters![5] as? Double
|
||||
let identifier = handlerIdentifier(from: characteristic)
|
||||
|
||||
//DLog("read timeout started")
|
||||
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
|
||||
captureReadHandlers.append(captureReadHandler)
|
||||
} else {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
|
||||
let identifier = handlerIdentifier(from: descriptor)
|
||||
|
||||
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
|
||||
// Remove capture handler
|
||||
let captureReadHandler = captureReadHandlers.remove(at: index)
|
||||
|
||||
// Send result
|
||||
let value = descriptor.value
|
||||
captureReadHandler.result(value, error)
|
||||
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
||||
guard error == nil else { DLog("didReadRSSI error: \(error!.localizedDescription)"); return }
|
||||
|
||||
let rssi = RSSI.intValue
|
||||
if rssi != 127 { // only update rssi value if is defined ( 127 means undefined )
|
||||
self.rssi = rssi
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .peripheralDidUpdateRssi, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Notifications
|
||||
extension Notification.Name {
|
||||
private static let kPrefix = Bundle.main.bundleIdentifier!
|
||||
static let peripheralDidUpdateName = Notification.Name(kPrefix+".peripheralDidUpdateName")
|
||||
static let peripheralDidModifyServices = Notification.Name(kPrefix+".peripheralDidModifyServices")
|
||||
static let peripheralDidUpdateRssi = Notification.Name(kPrefix+".peripheralDidUpdateRssi")
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// BleUUIDNames.swift
|
||||
// Bluefruit Connect
|
||||
//
|
||||
// Created by Antonio García on 15/02/16.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BleUUIDNames {
|
||||
// Manager
|
||||
static let shared = BleUUIDNames()
|
||||
|
||||
// Data
|
||||
private var gattUUIds: [String:String]?
|
||||
|
||||
init() {
|
||||
// Read known UUIDs
|
||||
let path = Bundle.main.path(forResource: "GattUUIDs", ofType: "plist")!
|
||||
gattUUIds = NSDictionary(contentsOfFile: path) as? [String: String]
|
||||
}
|
||||
|
||||
func nameForUUID(_ uuidString: String) -> String? {
|
||||
return gattUUIds?[uuidString]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// UartDataManager.swift
|
||||
// Calibration
|
||||
//
|
||||
// Created by Antonio García on 20/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol UartDataManagerDelegate: class {
|
||||
func onUartRx(data: Data, peripheralIdentifier: UUID) // data contents depends on the isRxCacheEnabled flag
|
||||
}
|
||||
|
||||
// Basic Uart Management. Use it to cache all data received and help parsing it
|
||||
class UartDataManager {
|
||||
|
||||
// Params
|
||||
var isEnabled: Bool {
|
||||
didSet {
|
||||
if isEnabled != oldValue {
|
||||
registerNotifications(enabled: isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
var isRxCacheEnabled: Bool { // If cache is enabled, onUartRx sends the cachedData. Cache can be cleared using removeRxCacheFirst or clearRxCache. If not enabled, onUartRx sends only the latest data received
|
||||
didSet {
|
||||
if !isRxCacheEnabled {
|
||||
DLog("Clearing all rx caches")
|
||||
rxDatas.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
weak var delegate: UartDataManagerDelegate?
|
||||
|
||||
// Data
|
||||
private var rxDatas = [UUID: Data]()
|
||||
private var rxDataSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
init(delegate: UartDataManagerDelegate?, isRxCacheEnabled: Bool) {
|
||||
self.delegate = delegate
|
||||
self.isRxCacheEnabled = isRxCacheEnabled
|
||||
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
isEnabled = false
|
||||
}
|
||||
|
||||
// MARK: - BLE Notifications
|
||||
private weak var didConnectToPeripheralObserver: NSObjectProtocol?
|
||||
private weak var didDisconnectFromPeripheralObserver: NSObjectProtocol?
|
||||
|
||||
private func registerNotifications(enabled: Bool) {
|
||||
let notificationCenter = NotificationCenter.default
|
||||
if enabled {
|
||||
didConnectToPeripheralObserver = notificationCenter.addObserver(forName: .didConnectToPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didConnectToPeripheral(notification: notification)})
|
||||
didDisconnectFromPeripheralObserver = notificationCenter.addObserver(forName: .didDisconnectFromPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didDisconnectFromPeripheral(notification: notification)})
|
||||
|
||||
} else {
|
||||
if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)}
|
||||
if let didDisconnectFromPeripheralObserver = didDisconnectFromPeripheralObserver {notificationCenter.removeObserver(didDisconnectFromPeripheralObserver)}
|
||||
}
|
||||
}
|
||||
|
||||
private func didConnectToPeripheral(notification: Notification) {
|
||||
guard let identifier = notification.userInfo?[BleManager.NotificationUserInfoKey.uuid.rawValue] as? UUID else { return }
|
||||
|
||||
clearRxCache(peripheralIdentifier: identifier)
|
||||
}
|
||||
|
||||
private func didDisconnectFromPeripheral(notification: Notification) {
|
||||
guard let identifier = notification.userInfo?[BleManager.NotificationUserInfoKey.uuid.rawValue] as? UUID else { return }
|
||||
|
||||
// Clean data on disconnect
|
||||
rxDatas[identifier] = nil
|
||||
|
||||
rxDataSemaphore.signal() // Force signal if was waiting
|
||||
}
|
||||
|
||||
// MARK: - Send data
|
||||
func send(blePeripheral: BlePeripheral, data: Data?, completion: ((Error?) -> Void)? = nil) {
|
||||
blePeripheral.uartSend(data: data, completion: completion)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Received data
|
||||
func rxDataReceived(data: Data?, peripheralIdentifier identifier: UUID, error: Error?) {
|
||||
guard error == nil else { DLog("rxDataReceived error: \(error!)"); return }
|
||||
guard let data = data else { return }
|
||||
|
||||
// Pre-create rxData entry if needed
|
||||
if isRxCacheEnabled && rxDatas[identifier] == nil {
|
||||
rxDatas[identifier] = Data()
|
||||
}
|
||||
|
||||
if isRxCacheEnabled {
|
||||
rxDataSemaphore.wait() // don't append more data, till the delegate has finished processing it
|
||||
rxDatas[identifier]!.append(data)
|
||||
|
||||
// Send data to delegate
|
||||
delegate?.onUartRx(data: rxDatas[identifier]!, peripheralIdentifier: identifier)
|
||||
|
||||
//DLog("cachedRxData: \(cachedRxData.count)")
|
||||
rxDataSemaphore.signal()
|
||||
} else {
|
||||
delegate?.onUartRx(data: data, peripheralIdentifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
func clearRxCache(peripheralIdentifier identifier: UUID) {
|
||||
guard rxDatas[identifier] != nil else { return }
|
||||
|
||||
rxDatas[identifier]!.removeAll()
|
||||
}
|
||||
|
||||
func removeRxCacheFirst(n: Int, peripheralIdentifier identifier: UUID) {
|
||||
// Note: this is usually called from onUartRx delegates, so don't use rxDataSemaphore because it is already being used by the onUartRX caller
|
||||
guard let rxData = rxDatas[identifier] else { return }
|
||||
|
||||
//DLog("remove \(n) items")
|
||||
//DLog("pre remove: \(hexDescription(data: rxData))")
|
||||
|
||||
if n < rxData.count {
|
||||
rxDatas[identifier]!.removeFirst(n)
|
||||
} else {
|
||||
clearRxCache(peripheralIdentifier: identifier)
|
||||
}
|
||||
|
||||
//DLog("post remove: \(hexDescription(data: rxDatas[identifier]!))")
|
||||
}
|
||||
|
||||
func flushRxCache(peripheralIdentifier identifier: UUID) {
|
||||
guard let rxData = rxDatas[identifier] else { return }
|
||||
|
||||
if rxData.count > 0 {
|
||||
rxDataSemaphore.wait()
|
||||
delegate?.onUartRx(data: rxData, peripheralIdentifier: identifier)
|
||||
rxDataSemaphore.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// UartLogManager.swift
|
||||
// Calibration
|
||||
//
|
||||
// Created by Antonio García on 24/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UartLogManager {
|
||||
|
||||
private static var kIsEnabled = false
|
||||
|
||||
enum LogType {
|
||||
case info
|
||||
case uartTx
|
||||
case uartRx
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .info: return ""
|
||||
case .uartTx: return "sent"
|
||||
case .uartRx: return "received"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LogItem {
|
||||
var type = LogType.info
|
||||
var data: Data
|
||||
}
|
||||
|
||||
static var logItems = [LogItem]()
|
||||
|
||||
static func log(data: Data, type: LogType) {
|
||||
if UartLogManager.kIsEnabled {
|
||||
let item = LogItem(type: type, data: data)
|
||||
UartLogManager.logItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
static func log(message: String, type: LogType = .info) {
|
||||
if UartLogManager.kIsEnabled {
|
||||
if let data = message.data(using: .utf8) {
|
||||
let item = LogItem(type: type, data: data)
|
||||
UartLogManager.logItems.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func clearLog() {
|
||||
UartLogManager.logItems.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// UartPacketManager.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio on 01/02/2017.
|
||||
// Copyright © 2017 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UartPacketManager: UartPacketManagerBase {
|
||||
|
||||
override init(delegate: UartPacketManagerDelegate?, isPacketCacheEnabled: Bool, isMqttEnabled: Bool) {
|
||||
super.init(delegate: delegate, isPacketCacheEnabled: isPacketCacheEnabled, isMqttEnabled: isMqttEnabled)
|
||||
|
||||
registerNotifications(enabled: true)
|
||||
}
|
||||
|
||||
deinit {
|
||||
registerNotifications(enabled: false)
|
||||
}
|
||||
|
||||
// MARK: - BLE Notifications
|
||||
private weak var didConnectToPeripheralObserver: NSObjectProtocol?
|
||||
private func registerNotifications(enabled: Bool) {
|
||||
let notificationCenter = NotificationCenter.default
|
||||
if enabled {
|
||||
didConnectToPeripheralObserver = notificationCenter.addObserver(forName: .didConnectToPeripheral, object: nil, queue: .main) {[weak self] _ in self?.clearPacketsCache()}
|
||||
} else {
|
||||
if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send data
|
||||
func send(blePeripheral: BlePeripheral, data: Data?, progress: ((Float)->Void)? = nil, completion: ((Error?) -> Void)? = nil) {
|
||||
sentBytes += Int64(data?.count ?? 0)
|
||||
blePeripheral.uartSend(data: data, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
func sendEachPacketSequentially(blePeripheral: BlePeripheral, data: Data?, withResponseEveryPacketCount: Int, progress: ((Float)->Void)? = nil, completion: ((Error?) -> Void)? = nil) {
|
||||
sentBytes += Int64(data?.count ?? 0)
|
||||
blePeripheral.uartSendEachPacketSequentially(data: data, withResponseEveryPacketCount: withResponseEveryPacketCount, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
func cancelOngoingSendPacketSequentiallyInMainThread(blePeripheral: BlePeripheral) {
|
||||
blePeripheral.uartCancelOngoingSendPacketSequentiallyInMainThread()
|
||||
}
|
||||
|
||||
func sendAndWaitReply(blePeripheral: BlePeripheral, data: Data?, writeProgress: ((Float)->Void)? = nil, writeCompletion: ((Error?) -> Void)? = nil, readTimeout: Double? = BlePeripheral.kUartReplyDefaultTimeout, readCompletion: @escaping BlePeripheral.CapturedReadCompletionHandler) {
|
||||
sentBytes += Int64(data?.count ?? 0)
|
||||
blePeripheral.uartSendAndWaitReply(data: data, writeProgress: writeProgress, writeCompletion: writeCompletion, readTimeout: readTimeout, readCompletion: readCompletion)
|
||||
}
|
||||
|
||||
func send(blePeripheral: BlePeripheral, text: String, wasReceivedFromMqtt: Bool = false) {
|
||||
|
||||
#if MQTT_ENABLED
|
||||
if isMqttEnabled {
|
||||
// Mqtt publish to TX
|
||||
let mqttSettings = MqttSettings.shared
|
||||
if mqttSettings.isPublishEnabled {
|
||||
if let topic = mqttSettings.getPublishTopic(index: MqttSettings.PublishFeed.tx.rawValue) {
|
||||
let qos = mqttSettings.getPublishQos(index: MqttSettings.PublishFeed.tx.rawValue)
|
||||
MqttManager.shared.publish(message: text, topic: topic, qos: qos)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create data and send to Uart
|
||||
if let data = text.data(using: .utf8) {
|
||||
let uartPacket = UartPacket(peripheralId: blePeripheral.identifier, mode: .tx, data: data)
|
||||
|
||||
// Add Packet
|
||||
packetsSemaphore.wait()
|
||||
packets.append(uartPacket)
|
||||
packetsSemaphore.signal()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onUartPacket(uartPacket)
|
||||
}
|
||||
|
||||
#if MQTT_ENABLED
|
||||
let shouldBeSent = !wasReceivedFromMqtt || (isMqttEnabled && MqttSettings.shared.subscribeBehaviour == .transmit)
|
||||
#else
|
||||
let shouldBeSent = true
|
||||
#endif
|
||||
|
||||
if shouldBeSent {
|
||||
send(blePeripheral: blePeripheral, data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Force reset
|
||||
func reset(blePeripheral: BlePeripheral) {
|
||||
blePeripheral.reset()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// BlePeripheral+CPBAccelerometer.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 25/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kCPBAccelerometerServiceUUID = CBUUID(string: "ADAF0200-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBAccelerometerCharacteristicUUID = CBUUID(string: "ADAF0201-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
static let kCPBAcceleromterDefaultPeriod: TimeInterval = 0.1
|
||||
|
||||
struct AccelerometerValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbAccelerometerCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbAccelerometerCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbAccelerometerCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbAccelerometerCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbAccelerometerEnable(responseHandler: @escaping(Result<(AccelerometerValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBAccelerometerServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBAccelerometerCharacteristicUUID, timePeriod: BlePeripheral.kCPBAcceleromterDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let acceleration = self.cpbAccelerometerDataToFloatVector(data) {
|
||||
responseHandler(.success((acceleration, uuid)))
|
||||
}
|
||||
else {
|
||||
responseHandler(.failure(PeripheralCPBError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbAccelerometerEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbAccelerometerCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbAccelerometerCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbAccelerometerEnabled() -> Bool {
|
||||
return cpbAccelerometerCharacteristic != nil && cpbAccelerometerCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func cpbAccelerometerDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
cpbAccelerometerCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = cpbAccelerometerCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func cpbAccelerometerLastValue() -> AccelerometerValue? {
|
||||
guard let data = cpbAccelerometerCharacteristic?.value else { return nil }
|
||||
return cpbAccelerometerDataToFloatVector(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func cpbAccelerometerDataToFloatVector(_ data: Data) -> AccelerometerValue? {
|
||||
|
||||
let unitSize = MemoryLayout<Float32>.stride
|
||||
var bytes = [Float32](repeating: 0, count: data.count / unitSize)
|
||||
(data as NSData).getBytes(&bytes, length: data.count * unitSize)
|
||||
|
||||
guard bytes.count >= 3 else { return nil }
|
||||
|
||||
return AccelerometerValue(x: bytes[0], y: bytes[1], z: bytes[2])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
//
|
||||
// BlePeripheral+CPBButtons.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 15/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kCPBButtonsServiceUUID = CBUUID(string: "ADAF0600-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBButtonsCharacteristicUUID = CBUUID(string: "ADAF0601-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
enum SlideSwitchState: Int32 {
|
||||
case right = 0
|
||||
case left = 1
|
||||
}
|
||||
|
||||
enum ButtonState: Int32 {
|
||||
case released = 0
|
||||
case pressed = 1
|
||||
}
|
||||
|
||||
struct ButtonsState {
|
||||
var slideSwitch: SlideSwitchState
|
||||
var buttonA: ButtonState
|
||||
var buttonB: ButtonState
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbButtonsCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbButtonsCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbButtonsCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbButtonsCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbButtonsEnable(responseHandler: @escaping(Result<(ButtonsState, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBButtonsServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBButtonsCharacteristicUUID, timePeriod: 0, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let state = self.cpbButtonsDataToStateMask(data)
|
||||
responseHandler(.success((state, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbButtonsEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbButtonsCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbButtonsCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbButtonsEnabled() -> Bool {
|
||||
return cpbButtonsCharacteristic != nil && cpbButtonsCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func cpbButtonsDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
cpbButtonsCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = cpbButtonsCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func cpbButtonsReadState(completion: @escaping(Result<(ButtonsState, UUID), Error>) -> Void) {
|
||||
guard let cpbButtonsCharacteristic = cpbButtonsCharacteristic else {
|
||||
completion(.failure(PeripheralCPBError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
self.readCharacteristic(cpbButtonsCharacteristic) { [weak self] (data, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard error == nil, let data = data as? Data else {
|
||||
completion(.failure(error ?? PeripheralCPBError.invalidResponseData))
|
||||
return
|
||||
}
|
||||
|
||||
let state = self.cpbButtonsDataToStateMask(data)
|
||||
completion(.success((state, self.identifier)))
|
||||
}
|
||||
}
|
||||
|
||||
func cpbButtonsLastValue() -> ButtonsState? {
|
||||
guard let data = cpbButtonsCharacteristic?.value else { return nil }
|
||||
return cpbButtonsDataToStateMask(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func cpbButtonsDataToStateMask(_ data: Data) -> ButtonsState {
|
||||
let stateMask = data.toInt32From32Bits()
|
||||
|
||||
let slideSwitchBit = stateMask & 0b1
|
||||
let slideSwitchState = SlideSwitchState(rawValue: slideSwitchBit)!
|
||||
|
||||
let buttonABit = ( stateMask >> 1 ) & 0b1
|
||||
let buttonAState = ButtonState(rawValue: buttonABit)!
|
||||
|
||||
let buttonBBit = ( stateMask >> 2 ) & 0b1
|
||||
let buttonBState = ButtonState(rawValue: buttonBBit)!
|
||||
|
||||
return ButtonsState(slideSwitch: slideSwitchState , buttonA: buttonAState, buttonB: buttonBState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
//
|
||||
// BlePeripehral+CPBCommon.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Costants
|
||||
private static let kCPBMeasurementPeriodCharacteristicUUID = CBUUID(string: "ADAF0001-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBMeasurementVersionCharacteristicUUID = CBUUID(string: "ADAF0002-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
private static let kCPBDefaultVersionValue = 1 // Used as default version value if version characteristic cannot be read
|
||||
|
||||
// MARK: - Errors
|
||||
enum PeripheralCPBError: Error {
|
||||
case invalidCharacteristic
|
||||
case enableNotifyFailed
|
||||
case unknownVersion
|
||||
case invalidResponseData
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
/*
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbMeasurementPeriodCharacteristic: CBCharacteristic?
|
||||
//static var cpbVersionCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbMeasurementPeriodCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbMeasurementPeriodCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbMeasurementPeriodCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var cpbVersionCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbVersionCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbVersionCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}*/
|
||||
|
||||
// MARK: - Service Actions
|
||||
func cpbServiceEnable(serviceUuid: CBUUID, mainCharacteristicUuid: CBUUID, completion: ((Result<(Int, CBCharacteristic), Error>) -> Void)?) {
|
||||
|
||||
self.characteristic(uuid: mainCharacteristicUuid, serviceUuid: serviceUuid) { [unowned self] (characteristic, error) in
|
||||
guard let characteristic = characteristic, error == nil else {
|
||||
completion?(.failure(error ?? PeripheralCPBError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
// Check version
|
||||
self.cpbVersion(serviceUuid: serviceUuid) { version in
|
||||
completion?(.success((version, characteristic)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cpbServiceEnable(serviceUuid: CBUUID, mainCharacteristicUuid: CBUUID, timePeriod: TimeInterval?, responseHandler: @escaping(Result<(Data, UUID), Error>) -> Void, completion: ((Result<(Int, CBCharacteristic), Error>) -> Void)?) {
|
||||
|
||||
self.characteristic(uuid: mainCharacteristicUuid, serviceUuid: serviceUuid) { [unowned self] (characteristic, error) in
|
||||
guard let characteristic = characteristic, error == nil else {
|
||||
completion?(.failure(error ?? PeripheralCPBError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
// Check version
|
||||
self.cpbVersion(serviceUuid: serviceUuid) { version in
|
||||
// Prepare notification handler
|
||||
let notifyHandler: ((Error?) -> Void)? = { [unowned self] error in
|
||||
guard error == nil else {
|
||||
responseHandler(.failure(error!))
|
||||
return
|
||||
}
|
||||
|
||||
if let data = characteristic.value {
|
||||
responseHandler(.success((data, self.identifier)))
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh period handler
|
||||
let enableNotificationsHandler = {
|
||||
// Enable notifications
|
||||
if !characteristic.isNotifying {
|
||||
self.enableNotify(for: characteristic, handler: notifyHandler, completion: { error in
|
||||
guard error == nil else {
|
||||
completion?(.failure(error!))
|
||||
return
|
||||
}
|
||||
guard characteristic.isNotifying else {
|
||||
completion?(.failure(PeripheralCPBError.enableNotifyFailed))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success((version, characteristic)))
|
||||
|
||||
})
|
||||
} else {
|
||||
self.updateNotifyHandler(for: characteristic, handler: notifyHandler)
|
||||
completion?(.success((version, characteristic)))
|
||||
}
|
||||
}
|
||||
|
||||
// Set timePeriod if not nil
|
||||
if let timePeriod = timePeriod {
|
||||
self.cpbSetPeriod(timePeriod, serviceUuid: serviceUuid) { result in
|
||||
|
||||
if Config.isDebugEnabled {
|
||||
// Check period
|
||||
self.cpbPeriod(serviceUuid: serviceUuid) { period in
|
||||
DLog("service period: \(String(describing: period))")
|
||||
}
|
||||
}
|
||||
|
||||
enableNotificationsHandler()
|
||||
}
|
||||
}
|
||||
else {
|
||||
enableNotificationsHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cpbVersion(serviceUuid: CBUUID, completion: @escaping(Int) -> Void) {
|
||||
self.characteristic(uuid: BlePeripheral.kCPBMeasurementVersionCharacteristicUUID, serviceUuid: serviceUuid) { (characteristic, error) in
|
||||
|
||||
guard error == nil, let characteristic = characteristic, let data = characteristic.value else {
|
||||
completion(BlePeripheral.kCPBDefaultVersionValue)
|
||||
return
|
||||
}
|
||||
let version = data.toIntFrom32Bits()
|
||||
completion(version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cpbPeriod(serviceUuid: CBUUID, completion: @escaping(TimeInterval?) -> Void) {
|
||||
self.characteristic(uuid: BlePeripheral.kCPBMeasurementPeriodCharacteristicUUID, serviceUuid: serviceUuid) { (characteristic, error) in
|
||||
|
||||
guard error == nil, let characteristic = characteristic else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.readCharacteristic(characteristic) { (data, error) in
|
||||
guard error == nil, let data = data as? Data else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let period = TimeInterval(data.toIntFrom32Bits()) / 1000.0
|
||||
completion(period)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cpbSetPeriod(_ period: TimeInterval, serviceUuid: CBUUID, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.characteristic(uuid: BlePeripheral.kCPBMeasurementPeriodCharacteristicUUID, serviceUuid: serviceUuid) { (characteristic, error) in
|
||||
|
||||
guard error == nil, let characteristic = characteristic else {
|
||||
DLog("Error: cpbSetPeriod: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
|
||||
let periodMillis = Int32(period * 1000)
|
||||
let data = periodMillis.littleEndian.data
|
||||
self.write(data: data, for: characteristic, type: .withResponse) { error in
|
||||
guard error == nil else {
|
||||
DLog("Error: cpbSetPeriod \(error!)")
|
||||
completion?(.failure(error!))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// BlePeripheral+CPBLight.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kCPBLightServiceUUID = CBUUID(string: "ADAF0300-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBLightCharacteristicUUID = CBUUID(string: "ADAF0301-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
static let kCPBLightDefaultPeriod: TimeInterval = 0.1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbLightCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbLightCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbLightCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbLightCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbLightEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBLightServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBLightCharacteristicUUID, timePeriod: BlePeripheral.kCPBLightDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let light = self.cpbLightDataToFloat(data)
|
||||
responseHandler(.success((light, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbLightEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbLightCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbLightCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbLightEnabled() -> Bool {
|
||||
return cpbLightCharacteristic != nil && cpbLightCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func cpbLightDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
cpbLightCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = cpbLightCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func cpbLightLastValue() -> Float? {
|
||||
guard let data = cpbLightCharacteristic?.value else { return nil }
|
||||
return cpbLightDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func cpbLightDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
//
|
||||
// BlePeripheral+CPBPixels.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 25/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Config
|
||||
private static let kPixelsServiceNumberOfBitsPerPixel = 3
|
||||
private static let kPixelsServiceNumPixels = 10
|
||||
|
||||
// Constants
|
||||
static let kCPBPixelsServiceUUID = CBUUID(string: "ADAF0900-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBPixelsDataCharacteristicUUID = CBUUID(string: "ADAF0903-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbPixelsDataCharacteristic: CBCharacteristic?
|
||||
static var cpbPixelsDataValue: Data?
|
||||
}
|
||||
|
||||
private var cpbPixelsDataCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbPixelsDataCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbPixelsDataCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var cpbPixelsDataValue: Data {
|
||||
get {
|
||||
if let data = objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbPixelsDataValue) as! Data? {
|
||||
return data
|
||||
}
|
||||
else { // Initial value
|
||||
return Data(repeating: 0, count: BlePeripheral.kPixelsServiceNumPixels * BlePeripheral.kPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbPixelsDataValue, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbPixelsEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBPixelsServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBPixelsDataCharacteristicUUID) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbPixelsEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbPixelsDataCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbPixelsDataCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbPixelsEnabled() -> Bool {
|
||||
return cpbPixelsDataCharacteristic != nil
|
||||
}
|
||||
|
||||
func cpbPixelsDisable() {
|
||||
// Clear all specific data
|
||||
cpbPixelsDataCharacteristic = nil
|
||||
}
|
||||
|
||||
func cpbNumPixels() -> Int {
|
||||
return BlePeripheral.kPixelsServiceNumPixels
|
||||
}
|
||||
|
||||
func cpbPixelSetAllPixelsColor(_ color: UIColor) {
|
||||
let colors = [UIColor](repeating: color, count: BlePeripheral.kPixelsServiceNumPixels)
|
||||
cpbPixelsWriteData(offset: 0, colors: colors)
|
||||
}
|
||||
|
||||
func cpbPixelSetPixelColor(index: Int, color: UIColor) {
|
||||
let offset = UInt16(index * BlePeripheral.kPixelsServiceNumberOfBitsPerPixel)
|
||||
cpbPixelsWriteData(offset: offset, colors: [color])
|
||||
}
|
||||
|
||||
func cpbPixelSetColor(index: UInt, color: UIColor, pixelMask: [Bool]) {
|
||||
guard let pixelData = pixelDataFromColorMask(color: color, pixelMask: pixelMask) else {
|
||||
DLog("Error neopixelSetColor invalid color data")
|
||||
return
|
||||
}
|
||||
let offset = UInt16(index * UInt(BlePeripheral.kPixelsServiceNumberOfBitsPerPixel))
|
||||
cpbPixelsWriteData(offset: offset, pixelData: pixelData)
|
||||
}
|
||||
|
||||
// MARK: - Low level actions
|
||||
func cpbPixelsWriteData(offset: UInt16, colors: [UIColor]) {
|
||||
let pixelData = BlePeripheral.pixelDataFromColors(colors)
|
||||
cpbPixelsWriteData(offset: offset, pixelData: pixelData)
|
||||
}
|
||||
|
||||
func cpbPixelsWriteData(offset: UInt16, pixelData: Data) {
|
||||
guard let cpbPixelsDataCharacteristic = cpbPixelsDataCharacteristic else { return }
|
||||
|
||||
enum Flags: UInt8 {
|
||||
case save = 0
|
||||
case flush = 1
|
||||
}
|
||||
|
||||
let flags = Flags.flush
|
||||
|
||||
let data = offset.littleEndian.data + flags.rawValue.littleEndian.data + pixelData
|
||||
// self.write(data: data, for: cpbPixelsDataCharacteristic, type: .withResponse)
|
||||
self.write(data: data, for: cpbPixelsDataCharacteristic, type: .withResponse) { [unowned self] error in
|
||||
guard error == nil else { DLog("Error cpbPixelsWriteData: \(error!)"); return }
|
||||
|
||||
self.cpbPixelsDataValue = pixelData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Utils
|
||||
private func pixelDataFromColorMask(color: UIColor, pixelMask: [Bool]) -> Data? {
|
||||
let colorData = BlePeripheral.pixelDataFromColor(color)
|
||||
|
||||
var pixelData = Data()
|
||||
for (i, mask) in pixelMask.enumerated() {
|
||||
if mask { // overwrite color
|
||||
pixelData += colorData
|
||||
}
|
||||
else { // use current color
|
||||
let existingColorData: Data
|
||||
let byteOffset = i * BlePeripheral.kPixelsServiceNumberOfBitsPerPixel
|
||||
DLog("cpbPixelsDataValue.count: \(cpbPixelsDataValue.count) ")
|
||||
if byteOffset < cpbPixelsDataValue.count {
|
||||
existingColorData = Data(cpbPixelsDataValue[byteOffset..<(byteOffset + BlePeripheral.kPixelsServiceNumberOfBitsPerPixel)])
|
||||
}
|
||||
else {
|
||||
existingColorData = Data(repeating: 0, count: BlePeripheral.kPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
pixelData += existingColorData
|
||||
}
|
||||
}
|
||||
|
||||
return pixelData
|
||||
}
|
||||
|
||||
private static func pixelDataFromColors(_ colors: [UIColor]) -> Data {
|
||||
var pixelData = Data()
|
||||
|
||||
for color in colors {
|
||||
pixelData += pixelDataFromColor(color)
|
||||
}
|
||||
|
||||
return pixelData
|
||||
}
|
||||
|
||||
static func pixelDataFromColor(_ color: UIColor) -> Data {
|
||||
let bytes = pixelUInt8FromColor(color)
|
||||
return bytes.data
|
||||
}
|
||||
|
||||
static func pixelUInt8FromColor(_ color: UIColor) -> [UInt8] {
|
||||
var pixelBytes: [UInt8]? = nil
|
||||
|
||||
let cgColor = color.cgColor
|
||||
let numComponents = cgColor.numberOfComponents
|
||||
if let components = cgColor.components {
|
||||
if numComponents == 2 {
|
||||
let white = UInt8(components[0] * 255)
|
||||
//let alpha = UInt8(components[1] * 255)
|
||||
|
||||
pixelBytes = [white, white, white]
|
||||
}
|
||||
else if numComponents == 4 {
|
||||
|
||||
let r = UInt8(components[0] * 255)
|
||||
let g = UInt8(components[1] * 255)
|
||||
let b = UInt8(components[2] * 255)
|
||||
//let alpha = UInt8(components[3] * 255)
|
||||
|
||||
pixelBytes = [g, r, b]
|
||||
}
|
||||
else {
|
||||
DLog("Error converting color (number of components is: \(numComponents))")
|
||||
}
|
||||
}
|
||||
|
||||
return pixelBytes ?? [UInt8](repeating: 0, count: BlePeripheral.kPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// BlePeripheral+CPBTemperature.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kCPBTemperatureServiceUUID = CBUUID(string: "ADAF0100-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBTemperatureCharacteristicUUID = CBUUID(string: "ADAF0101-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbTemperatureCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbTemperatureCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbTemperatureCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbTemperatureCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbTemperatureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBTemperatureServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBTemperatureCharacteristicUUID, timePeriod: 0.5, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let temperature = self.cpbTemperatureDataToFloat(data)
|
||||
responseHandler(.success((temperature, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbTemperatureEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbTemperatureCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbTemperatureCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbTemperatureEnabled() -> Bool {
|
||||
return cpbTemperatureCharacteristic != nil && cpbTemperatureCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func cpbTemperatureDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
cpbTemperatureCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = cpbTemperatureCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func cpbTemperatureLastValue() -> Float? {
|
||||
guard let data = cpbTemperatureCharacteristic?.value else { return nil }
|
||||
return cpbTemperatureDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func cpbTemperatureDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// BlePeripheral+CPBToneGenerator.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 18/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kCPBToneGeneratorServiceUUID = CBUUID(string: "ADAF0C00-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kCPBToneGeneratorCharacteristicUUID = CBUUID(string: "ADAF0C01-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbToneGeneratorCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var cpbToneGeneratorCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbToneGeneratorCharacteristic) as! CBCharacteristic?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbToneGeneratorCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbToneGeneratorEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.cpbServiceEnable(serviceUuid: BlePeripheral.kCPBToneGeneratorServiceUUID, mainCharacteristicUuid: BlePeripheral.kCPBToneGeneratorCharacteristicUUID) { result in
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == 1 else {
|
||||
DLog("Warning: cpbToneGeneratorEnable unknown version: \(version)")
|
||||
completion?(.failure(PeripheralCPBError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
self.cpbToneGeneratorCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.cpbToneGeneratorCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isCpbToneGeneratorEnabled() -> Bool {
|
||||
return cpbToneGeneratorCharacteristic != nil
|
||||
}
|
||||
|
||||
func cpbToneGeneratorDisable() {
|
||||
// Clear all specific data
|
||||
cpbToneGeneratorCharacteristic = nil
|
||||
}
|
||||
|
||||
func cpbToneGeneratorStartPlaying(frequency: UInt16, duration: UInt32 = 0) { // Duration 0 means non-stop
|
||||
guard let cpbToneGeneratorCharacteristic = cpbToneGeneratorCharacteristic else { return }
|
||||
|
||||
let data = frequency.littleEndian.data + duration.littleEndian.data
|
||||
self.write(data: data, for: cpbToneGeneratorCharacteristic, type: .withResponse)
|
||||
//DLog("tone: \(frequency)")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// BlePeripheral+ManufacturerAdafruit.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 10/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
private static let kManufacturerAdafruitIdentifier: [UInt8] = [0x22, 0x08]
|
||||
|
||||
func isManufacturerAdafruit() -> Bool {
|
||||
guard let manufacturerIdentifier = advertisement.manufacturerIdentifier else { return false }
|
||||
|
||||
let manufacturerIdentifierBytes = [UInt8](manufacturerIdentifier)
|
||||
//DLog("\(name) manufacturer: \(advertisement.manufacturerString)")
|
||||
return manufacturerIdentifierBytes == BlePeripheral.kManufacturerAdafruitIdentifier
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// BleManagerSimulated.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 14/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
class BleManagerSimulated: BleManager {
|
||||
|
||||
// Singleton
|
||||
static let simulated = BleManagerSimulated()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override init() {
|
||||
|
||||
}
|
||||
|
||||
override func startScan(withServices services: [CBUUID]? = nil) {
|
||||
isScanning = true
|
||||
|
||||
// Add simulated peripheral
|
||||
let simulatedBlePeripheral = BlePeripheralSimulated()
|
||||
peripheralsFound[simulatedBlePeripheral.identifier] = simulatedBlePeripheral
|
||||
NotificationCenter.default.post(name: .didDiscoverPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: simulatedBlePeripheral.identifier])
|
||||
}
|
||||
|
||||
override func stopScan() {
|
||||
}
|
||||
|
||||
override func connect(to peripheral: BlePeripheral, timeout: TimeInterval? = nil, shouldNotifyOnConnection: Bool = false, shouldNotifyOnDisconnection: Bool = false, shouldNotifyOnNotification: Bool = false) {
|
||||
|
||||
// Send notification
|
||||
NotificationCenter.default.post(name: .didConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
|
||||
override func reconnecToPeripherals(withIdentifiers identifiers: [UUID], withServices services: [CBUUID], timeout: Double? = nil) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// BlePeripheral+CPBButtons.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 15/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
|
||||
enum SlideSwitchState: Int32 {
|
||||
case right = 0
|
||||
case left = 1
|
||||
}
|
||||
|
||||
enum ButtonState: Int32 {
|
||||
case released = 0
|
||||
case pressed = 1
|
||||
}
|
||||
|
||||
struct ButtonsState {
|
||||
var slideSwitch: SlideSwitchState
|
||||
var buttonA: ButtonState
|
||||
var buttonB: ButtonState
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbButtonsEnable(responseHandler: @escaping(Result<(ButtonsState, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func isCpbButtonsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func cpbButtonsDisable() {
|
||||
}
|
||||
|
||||
func cpbButtonsLastValue() -> ButtonsState? {
|
||||
return ButtonsState(slideSwitch: .left, buttonA: .pressed, buttonB: .released)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// BlePeripheral+CPBLight.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// MARK: - Actions
|
||||
func cpbLightEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func isCpbLightEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func cpbLightDisable() {
|
||||
}
|
||||
|
||||
func cpbLightLastValue() -> Float? {
|
||||
return 523
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BlePeripheral+CPBTemperature.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var cpbTemperatureResponseDataTimer: Timer?
|
||||
}
|
||||
|
||||
private var cpbTemperatureResponseDataTimer: Timer? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.cpbTemperatureResponseDataTimer) as! Timer?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.cpbTemperatureResponseDataTimer, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func cpbTemperatureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
cpbTemperatureResponseDataTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
guard let self = self else { return }
|
||||
guard let temperature = self.cpbTemperatureLastValue() else { return }
|
||||
responseHandler(.success((temperature, self.identifier)))
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func isCpbTemperatureEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func cpbTemperatureDisable() {
|
||||
cpbTemperatureResponseDataTimer?.invalidate()
|
||||
cpbTemperatureResponseDataTimer = nil
|
||||
}
|
||||
|
||||
func cpbTemperatureLastValue() -> Float? {
|
||||
let temperature = Float.random(in: 18.5 ..< 19.5)
|
||||
return temperature
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// BlePeripheralSimulated.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 14/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
class BlePeripheralSimulated: BlePeripheral {
|
||||
// Constants
|
||||
private static let kSimulatedUUID = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!
|
||||
|
||||
// Data
|
||||
override var identifier: UUID {
|
||||
return BlePeripheralSimulated.kSimulatedUUID
|
||||
}
|
||||
|
||||
override var name: String? {
|
||||
return "Simulated Peripheral"
|
||||
}
|
||||
|
||||
override var state: CBPeripheralState {
|
||||
return .connected
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
init() {
|
||||
// Mocking CBPeripheral: https://forums.developer.apple.com/thread/29851
|
||||
guard let peripheral = ObjectBuilder.createInstance(ofClass: "CBPeripheral") as? CBPeripheral else {
|
||||
assertionFailure("Unable to mock CBPeripheral")
|
||||
let nilPeripheral: CBPeripheral! = nil // Just to avoid a compiling error. This will never be executed
|
||||
super.init(peripheral: nilPeripheral, advertisementData: nil, rssi: nil)
|
||||
return
|
||||
}
|
||||
peripheral.addObserver(peripheral, forKeyPath: "delegate", options: .new, context: nil)
|
||||
|
||||
let manufacturerDataBytes: [UInt8] = [0x22, 0x08, 0x04, 0x01, 0x00, 0x45, 0x80] // Adafruit CPB
|
||||
let advertisementData = [CBAdvertisementDataManufacturerDataKey: Data(manufacturerDataBytes)]
|
||||
super.init(peripheral: peripheral, advertisementData: advertisementData, rssi: 20)
|
||||
}
|
||||
|
||||
// MARK: - Discover
|
||||
override func discover(serviceUuids: [CBUUID]?, completion: ((Error?) -> Void)?) {
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
20
BluefruitPlayground/AdafruitKit/Ble/Tests/ObjectBuilder.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// ObjectBuilder.h
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 16/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
// from: https://forums.developer.apple.com/thread/29851
|
||||
|
||||
#ifndef ObjectBuilder_h
|
||||
#define ObjectBuilder_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface ObjectBuilder: NSObject
|
||||
+ (id)createInstanceOfClass:(NSString *)name;
|
||||
@end
|
||||
|
||||
#endif /* ObjectBuilder_h */
|
||||
17
BluefruitPlayground/AdafruitKit/Ble/Tests/ObjectBuilder.m
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// ObjectBuilder.m
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 16/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "ObjectBuilder.h"
|
||||
|
||||
@implementation ObjectBuilder
|
||||
+ (id)createInstanceOfClass:(NSString *)name {
|
||||
|
||||
return [[NSClassFromString(name) alloc] init];
|
||||
}
|
||||
@end
|
||||
105
BluefruitPlayground/AdafruitKit/Ble/UartPacketManagerBase.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// UartPacketManagerBase.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 05/08/2017.
|
||||
// Copyright © 2017 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol UartPacketManagerDelegate: class {
|
||||
func onUartPacket(_ packet: UartPacket)
|
||||
}
|
||||
|
||||
struct UartPacket { // A packet of data received or sent
|
||||
var timestamp: CFAbsoluteTime
|
||||
enum TransferMode {
|
||||
case tx
|
||||
case rx
|
||||
}
|
||||
var mode: TransferMode
|
||||
var data: Data
|
||||
var peripheralId: UUID?
|
||||
|
||||
init(peripheralId: UUID?, timestamp: CFAbsoluteTime? = nil, mode: TransferMode, data: Data) {
|
||||
self.peripheralId = peripheralId
|
||||
self.timestamp = timestamp ?? CFAbsoluteTimeGetCurrent()
|
||||
self.mode = mode
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UartPacketManagerBase {
|
||||
|
||||
// Data
|
||||
internal weak var delegate: UartPacketManagerDelegate?
|
||||
internal var packets = [UartPacket]()
|
||||
internal var packetsSemaphore = DispatchSemaphore(value: 1)
|
||||
internal var isMqttEnabled: Bool
|
||||
internal var isPacketCacheEnabled: Bool
|
||||
|
||||
var receivedBytes: Int64 = 0
|
||||
var sentBytes: Int64 = 0
|
||||
|
||||
init(delegate: UartPacketManagerDelegate?, isPacketCacheEnabled: Bool, isMqttEnabled: Bool) {
|
||||
self.isPacketCacheEnabled = isPacketCacheEnabled
|
||||
self.isMqttEnabled = isMqttEnabled
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Received data
|
||||
func rxPacketReceived(data: Data?, peripheralIdentifier: UUID?, error: Error?) {
|
||||
|
||||
guard error == nil else { DLog("uartRxPacketReceived error: \(error!)"); return }
|
||||
guard let data = data else { return }
|
||||
|
||||
let uartPacket = UartPacket(peripheralId: peripheralIdentifier, mode: .rx, data: data)
|
||||
|
||||
// Mqtt publish to RX. TODO: Remove the dependency with MqttSettings and pass parameters
|
||||
#if MQTT_ENABLED
|
||||
if isMqttEnabled {
|
||||
let mqttSettings = MqttSettings.shared
|
||||
if mqttSettings.isPublishEnabled {
|
||||
if let message = String(data: uartPacket.data, encoding: .utf8) {
|
||||
if let topic = mqttSettings.getPublishTopic(index: MqttSettings.PublishFeed.rx.rawValue) {
|
||||
let qos = mqttSettings.getPublishQos(index: MqttSettings.PublishFeed.rx.rawValue)
|
||||
MqttManager.shared.publish(message: message, topic: topic, qos: qos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
packetsSemaphore.wait() // don't append more data, till the delegate has finished processing it
|
||||
receivedBytes += Int64(data.count)
|
||||
if isPacketCacheEnabled {
|
||||
packets.append(uartPacket)
|
||||
}
|
||||
|
||||
// Send data to delegate
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onUartPacket(uartPacket)
|
||||
}
|
||||
|
||||
//DLog("packetsData: \(packetsData.count)")
|
||||
|
||||
packetsSemaphore.signal()
|
||||
}
|
||||
|
||||
func clearPacketsCache() {
|
||||
packets.removeAll()
|
||||
}
|
||||
|
||||
func packetsCache() -> [UartPacket] {
|
||||
return packets
|
||||
}
|
||||
|
||||
// MARK: - Counters
|
||||
func resetCounters() {
|
||||
receivedBytes = 0
|
||||
sentBytes = 0
|
||||
}
|
||||
}
|
||||
102
BluefruitPlayground/AdafruitKit/Utils/CommandQueue.swift
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// ElementQueue.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 17/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Command array, executed sequencially
|
||||
class CommandQueue<Element> {
|
||||
var executeHandler: ((_ command: Element) -> Void)?
|
||||
|
||||
private var queueLock = NSLock()
|
||||
|
||||
/*
|
||||
private var queue = [Element]() {
|
||||
didSet {
|
||||
queueLock.lock()
|
||||
var shouldExecute = false
|
||||
// Start executing the first command (if it was not already executing)
|
||||
let nextElement = queue.first
|
||||
if oldValue.isEmpty, nextElement != nil {
|
||||
shouldExecute = true
|
||||
}
|
||||
DLog("queue size: \(queue.count)")
|
||||
queueLock.unlock()
|
||||
|
||||
if shouldExecute {
|
||||
self.executeHandler?(nextElement!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func first() -> Element? {
|
||||
queueLock.lock(); defer { queueLock.unlock() }
|
||||
return queue.first
|
||||
}
|
||||
|
||||
func append(_ element: Element) {
|
||||
queue.append(element)
|
||||
}
|
||||
|
||||
func next() {
|
||||
guard !queue.isEmpty else { return }
|
||||
|
||||
// Delete finished command and trigger next execution if needed
|
||||
queue.removeFirst()
|
||||
|
||||
if let nextElement = queue.first {
|
||||
executeHandler?(nextElement)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
DLog("queue removeAll")
|
||||
queue.removeAll()
|
||||
}
|
||||
*/
|
||||
|
||||
private var queue = [Element]()
|
||||
|
||||
func first() -> Element? {
|
||||
queueLock.lock(); defer { queueLock.unlock() }
|
||||
//DLog("queue: \(queue) first: \(queue.first)")
|
||||
return queue.first
|
||||
}
|
||||
|
||||
func executeNext() {
|
||||
queueLock.lock()
|
||||
guard !queue.isEmpty else { queueLock.unlock(); return }
|
||||
|
||||
//DLog("queue remove finished: \(queue.first)")
|
||||
// Delete finished command and trigger next execution if needed
|
||||
queue.removeFirst()
|
||||
let nextElement = queue.first
|
||||
queueLock.unlock()
|
||||
|
||||
if let nextElement = nextElement {
|
||||
//DLog("execute next")
|
||||
executeHandler?(nextElement)
|
||||
}
|
||||
}
|
||||
|
||||
func append(_ element: Element) {
|
||||
queueLock.lock()
|
||||
let shouldExecute = queue.isEmpty
|
||||
queue.append(element)
|
||||
queueLock.unlock()
|
||||
//DLog("queue: \(queue) append: \(element). total: \(queue.count)")
|
||||
|
||||
if shouldExecute {
|
||||
executeHandler?(element)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
// DLog("queue removeAll: \(queue.count)")
|
||||
queue.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Data+LittleEndianTypes.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
extension Data {
|
||||
func toFloatFrom32Bits() -> Float {
|
||||
return Float(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) }))
|
||||
}
|
||||
|
||||
func toIntFrom32Bits() -> Int {
|
||||
return Int(Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) })))
|
||||
}
|
||||
|
||||
func toInt32From32Bits() -> Int32 {
|
||||
return Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) }))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
17
BluefruitPlayground/AdafruitKit/Utils/Data+ScanValue.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Data+ScanValues.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 17/11/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Data Scan
|
||||
extension Data {
|
||||
func scanValue<T>(start: Int, length: Int) -> T {
|
||||
let subdata = self.subdata(in: start..<start+length)
|
||||
return subdata.withUnsafeBytes { $0.load(as: T.self) }
|
||||
}
|
||||
}
|
||||
23
BluefruitPlayground/AdafruitKit/Utils/HexUtils.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// HexUtils.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 15/10/16.
|
||||
// Copyright © 2015 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HexUtils {
|
||||
static func hexDescription(data: Data, prefix: String = "", postfix: String = " ") -> String {
|
||||
return data.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)}
|
||||
}
|
||||
|
||||
static func hexDescription(bytes: [UInt8], prefix: String = "", postfix: String = " ") -> String {
|
||||
return bytes.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)}
|
||||
}
|
||||
|
||||
static func decimalDescription(data: Data, prefix: String = "", postfix: String = " ") -> String {
|
||||
return data.reduce("") {$0 + String(format: "%@%ld%@", prefix, $1, postfix)}
|
||||
}
|
||||
}
|
||||
45
BluefruitPlayground/AdafruitKit/Utils/Int+Bytes.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// Int+ToByteArray.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 11/06/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// From https://stackoverflow.com/questions/29970204/split-uint32-into-uint8-in-swift
|
||||
|
||||
protocol UIntToBytesConvertable {
|
||||
var toBytes: [UInt8] { get }
|
||||
}
|
||||
|
||||
extension UIntToBytesConvertable {
|
||||
fileprivate func toByteArr<T: FixedWidthInteger>(endian: T, count: Int) -> [UInt8] {
|
||||
var _endian = endian
|
||||
let bytePtr = withUnsafePointer(to: &_endian) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: count) {
|
||||
UnsafeBufferPointer(start: $0, count: count)
|
||||
}
|
||||
}
|
||||
return [UInt8](bytePtr)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt16: UIntToBytesConvertable {
|
||||
var toBytes: [UInt8] {
|
||||
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt16>.size)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt32: UIntToBytesConvertable {
|
||||
var toBytes: [UInt8] {
|
||||
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt32>.size)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt64: UIntToBytesConvertable {
|
||||
var toBytes: [UInt8] {
|
||||
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt64>.size)
|
||||
}
|
||||
}
|
||||
17
BluefruitPlayground/AdafruitKit/Utils/LogHelper.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// LogHelper.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 10/10/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Note: check that Build Settings -> Project -> Active Compilation Conditions -> Debug, has DEBUG
|
||||
|
||||
func DLog(_ message: String, function: String = #function) {
|
||||
if _isDebugAssertConfiguration() {
|
||||
NSLog("%@, %@", function, message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// TouchReleaseRectangularPaletteControl.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 16/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import FlexColorPicker
|
||||
|
||||
class TouchReleaseRectangularPaletteControl: RectangularPaletteControl {
|
||||
|
||||
/*
|
||||
// Only override draw() if you perform custom drawing.
|
||||
// An empty implementation adversely affects performance during animation.
|
||||
override func draw(_ rect: CGRect) {
|
||||
// Drawing code
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
55
BluefruitPlayground/AdafruitKit/Utils/Types+Data.swift
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// Types+Data.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 13/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// from: https://stackoverflow.com/questions/38023838/round-trip-swift-number-types-to-from-data
|
||||
|
||||
protocol DataConvertible {
|
||||
init?(data: Data)
|
||||
var data: Data { get }
|
||||
}
|
||||
|
||||
extension DataConvertible where Self: ExpressibleByIntegerLiteral {
|
||||
|
||||
init?(data: Data) {
|
||||
var value: Self = 0
|
||||
guard data.count == MemoryLayout.size(ofValue: value) else { return nil }
|
||||
_ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
|
||||
self = value
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
return withUnsafeBytes(of: self) { Data($0) }
|
||||
}
|
||||
}
|
||||
|
||||
// Declare conformance to all types which can safely be converted to Data and back
|
||||
extension Int: DataConvertible { }
|
||||
extension UInt8: DataConvertible { }
|
||||
extension Int16: DataConvertible { }
|
||||
extension UInt16: DataConvertible { }
|
||||
extension Int32: DataConvertible { }
|
||||
extension UInt32: DataConvertible { }
|
||||
extension Float: DataConvertible { }
|
||||
extension Double: DataConvertible { }
|
||||
|
||||
|
||||
// Convert from [UInt8] to Data and from Data to [UInt8]
|
||||
// from: https://stackoverflow.com/questions/31821709/nsdata-to-uint8-in-swift/31821838
|
||||
extension Data {
|
||||
var bytes: [UInt8] {
|
||||
return [UInt8](self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
var data: Data {
|
||||
return Data(self)
|
||||
}
|
||||
}
|
||||
71
BluefruitPlayground/AppDelegate.swift
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 09/10/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
|
||||
startup()
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
// MARK: - Startup
|
||||
private func startup() {
|
||||
// Settings
|
||||
Settings.registerDefaults()
|
||||
|
||||
// UI
|
||||
UINavigationBar.appearance().prefersLargeTitles = ConfigUI.prefersLargeTitles
|
||||
|
||||
// Navigation bar: add background when large title is used
|
||||
if #available(iOS 13.0, *) {
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.backgroundColor = UIColor(named: "main")
|
||||
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||||
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
|
||||
|
||||
UINavigationBar.appearance().tintColor = .white
|
||||
UINavigationBar.appearance().standardAppearance = appearance
|
||||
UINavigationBar.appearance().compactAppearance = appearance
|
||||
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
||||
} else {
|
||||
UINavigationBar.appearance().tintColor = .white
|
||||
UINavigationBar.appearance().barTintColor = UIColor(named: "main")
|
||||
UINavigationBar.appearance().isTranslucent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9 KiB |
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom": "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-20@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom": "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-20@3x.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom": "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-20.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom": "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-20@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "BluefruitPlayground-Icon-1024.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
6
BluefruitPlayground/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
6
BluefruitPlayground/Assets.xcassets/about/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
25
BluefruitPlayground/Assets.xcassets/about/info_adafruit_logo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "info_adafruit_logo.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "info_adafruit_logo@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/about/info_adafruit_logo.imageset/info_adafruit_logo.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
BluefruitPlayground/Assets.xcassets/about/info_adafruit_logo.imageset/info_adafruit_logo@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
26
BluefruitPlayground/Assets.xcassets/button_status/status_a.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_a.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_a@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_a@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_a.imageset/status_a.png
vendored
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_a.imageset/status_a@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_a.imageset/status_a@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
26
BluefruitPlayground/Assets.xcassets/button_status/status_b.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_b.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_b@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_b@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_b.imageset/status_b.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_b.imageset/status_b@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_b.imageset/status_b@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
26
BluefruitPlayground/Assets.xcassets/button_status/status_left.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_left.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_left@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_left@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_left.imageset/status_left.png
vendored
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_left.imageset/status_left@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_left.imageset/status_left@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
26
BluefruitPlayground/Assets.xcassets/button_status/status_right.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_right.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_right@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "status_right@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_right.imageset/status_right.png
vendored
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_right.imageset/status_right@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
BluefruitPlayground/Assets.xcassets/button_status/status_right.imageset/status_right@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
26
BluefruitPlayground/Assets.xcassets/circuit/circuit_all.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_all.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_all@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_all@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_all.imageset/circuit_all.png
vendored
Normal file
|
After Width: | Height: | Size: 929 B |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_all.imageset/circuit_all@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_all.imageset/circuit_all@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3 KiB |
26
BluefruitPlayground/Assets.xcassets/circuit/circuit_clear.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_clear.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_clear@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_clear@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_clear.imageset/circuit_clear.png
vendored
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_clear.imageset/circuit_clear@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_clear.imageset/circuit_clear@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
26
BluefruitPlayground/Assets.xcassets/circuit/circuit_reset.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_reset.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_reset@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "circuit_reset@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_reset.imageset/circuit_reset.png
vendored
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_reset.imageset/circuit_reset@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
BluefruitPlayground/Assets.xcassets/circuit/circuit_reset.imageset/circuit_reset@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
6
BluefruitPlayground/Assets.xcassets/colors/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"red" : "0x00",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xB1",
|
||||
"green" : "0x7C"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"red" : "0x00",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE1",
|
||||
"green" : "0x9E"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"red" : "1.000",
|
||||
"alpha" : "0.150",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"red" : "0.138",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.726",
|
||||
"green" : "0.218"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"red" : "0.000",
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.882",
|
||||
"green" : "0.620"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||