Compare commits
97 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05e43d9a74 | ||
|
|
5e5995eb5d | ||
|
|
78a3e93679 | ||
|
|
47ffad0c0d | ||
|
|
82a6a37a9b | ||
|
|
4c924c2605 | ||
|
|
d290b65d91 | ||
|
|
f17d2017ce | ||
|
|
ad8e7ffade | ||
|
|
251bd8ec31 | ||
|
|
4d268e4f4f | ||
|
|
e2bd12b539 | ||
|
|
969e1eb7c0 | ||
|
|
d22f07df7d | ||
|
|
60c0ae8c6b | ||
|
|
263c1163e9 | ||
|
|
c089e932da | ||
|
|
5125b08706 | ||
|
|
5f22e72211 | ||
|
|
542f5ca8c8 | ||
|
|
0d33821c88 | ||
|
|
76ac60c734 | ||
|
|
5f9bfb3ee9 | ||
|
|
ec13361e4f | ||
|
|
7c7ae8578f | ||
|
|
3fdf7a15fe | ||
|
|
cd834da0a1 | ||
|
|
d487170a96 | ||
|
|
d706ae85fd | ||
|
|
0d7c091fb6 | ||
|
|
5e9eeabe5e | ||
|
|
d6269f59be | ||
|
|
13a4c4ff3d | ||
|
|
49b2508da4 | ||
|
|
44e4654301 | ||
|
|
b5394dab0a | ||
|
|
36872363d8 | ||
|
|
cdf816f848 | ||
|
|
380891dceb | ||
|
|
2090698fc4 | ||
|
|
91d5322795 | ||
|
|
019befae61 | ||
|
|
c02a36c976 | ||
|
|
3feac2a2e5 | ||
|
|
a61def2b4c | ||
|
|
1c0cb8235e | ||
|
|
c1a88e264e | ||
|
|
a1cc6d6306 | ||
|
|
5244715073 | ||
|
|
19768e4c52 | ||
|
|
d1e72eb47e | ||
|
|
90da0e9896 | ||
|
|
519022fcf5 | ||
|
|
0b7259834e | ||
|
|
3ba4fa2d9d | ||
|
|
213910c54a | ||
|
|
b020a9ee43 | ||
|
|
4eec8bb5b7 | ||
|
|
a44b33e66f | ||
|
|
031bab1241 | ||
|
|
f48758ed77 | ||
|
|
b96425596a | ||
|
|
148a33a5d8 | ||
|
|
e326b0b9d5 | ||
|
|
a220f3dec4 | ||
|
|
e4f4c8bcd1 | ||
|
|
a105506ba6 | ||
|
|
170809c2f8 | ||
|
|
f75db839ff | ||
|
|
0bf2400b88 | ||
|
|
1c98c92bac | ||
|
|
379d61a3b1 | ||
|
|
f5af18568e | ||
|
|
f56cfee413 | ||
|
|
3933990111 | ||
|
|
eb4182c30e | ||
|
|
1dab5a618e | ||
|
|
eacad0dc42 | ||
|
|
462ea1c289 | ||
|
|
9c8d6f2e00 | ||
|
|
95ab2e834e | ||
|
|
ed84a02c7c | ||
|
|
597ba1920e | ||
|
|
e4ca5d4b2e | ||
|
|
52ab739680 | ||
|
|
d31f154280 | ||
|
|
abaf6c923b | ||
|
|
b9f5da2aee | ||
|
|
2d5d116447 | ||
|
|
64e94256ee | ||
|
|
c4325844d9 | ||
|
|
bbd179a207 | ||
|
|
c48f116cc3 | ||
|
|
529952e495 | ||
|
|
14f2d53a22 | ||
|
|
b9760b163f | ||
|
|
28157189a8 |
26
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Build App
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build-stable:
|
||||
name: Build Bluefruit in stable Xcode
|
||||
runs-on: macos-13
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['15.0.1']
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Select ${{ matrix.xcode }}
|
||||
run: |
|
||||
sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app
|
||||
xcode-select -p
|
||||
env:
|
||||
XCODE_VERSION: ${{ matrix.xcode }}
|
||||
|
||||
- name: Build
|
||||
run: xcodebuild -scheme BluefruitPlayground -workspace BluefruitPlayground.xcworkspace -destination "generic/platform=iOS" -configuration Release build CODE_SIGNING_ALLOWED=NO
|
||||
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
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
<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>
|
||||
|
|
@ -13,13 +15,21 @@
|
|||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<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>NSCameraUsageDescription</key>
|
||||
<string>Puppet module requires access to the camera</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
@ -41,5 +51,7 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
1802
BluefruitPlayground.xcodeproj/project.pbxproj
Normal file
|
|
@ -2,6 +2,6 @@
|
|||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:CPX+BLE.xcodeproj">
|
||||
location = "self:CPX.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
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 = "1500"
|
||||
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 = "1500"
|
||||
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>
|
||||
|
|
@ -2,13 +2,7 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>CPX+BLE.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitAccelerometer.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 kAdafruitAccelerometerServiceUUID = CBUUID(string: "ADAF0200-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitAccelerometerCharacteristicUUID = CBUUID(string: "ADAF0201-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitAccelerometerVersion = 1
|
||||
|
||||
// Structs
|
||||
/// Acceleration in m/s²
|
||||
struct AccelerometerValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitAccelerometerCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitAccelerometerCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitAccelerometerCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitAccelerometerCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitAccelerometerEnable(responseHandler: @escaping(Result<(AccelerometerValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitAccelerometerVersion, serviceUuid: BlePeripheral.kAdafruitAccelerometerServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitAccelerometerCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self.adafruitAccelerometerDataToAcceleromterValue(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
} else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitAccelerometerCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitAccelerometerCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitAccelerometerIsEnabled() -> Bool {
|
||||
return adafruitAccelerometerCharacteristic != nil && adafruitAccelerometerCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitAccelerometerDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitAccelerometerCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitAccelerometerCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitAccelerometerLastValue() -> AccelerometerValue? {
|
||||
guard let data = adafruitAccelerometerCharacteristic?.value else { return nil }
|
||||
return adafruitAccelerometerDataToAcceleromterValue(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitAccelerometerDataToAcceleromterValue(_ data: Data) -> AccelerometerValue? {
|
||||
|
||||
guard let bytes = adafruitDataToFloatArray(data) else { return nil }
|
||||
guard bytes.count >= 3 else { return nil }
|
||||
return AccelerometerValue(x: bytes[0], y: bytes[1], z: bytes[2])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitBarometricPressure.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitBarometricPressureServiceUUID = CBUUID(string: "ADAF0800-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitBarometricPressureCharacteristicUUID = CBUUID(string: "ADAF0801-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitBarometricPressureVersion = 1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitBarometricPressureCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitBarometricPressureCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitBarometricPressureCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitBarometricPressureCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitBarometricPressureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitBarometricPressureVersion, serviceUuid: BlePeripheral.kAdafruitBarometricPressureServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitBarometricPressureCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let value = self.adafruitBarometricPressureDataToFloat(data)
|
||||
responseHandler(.success((value, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitBarometricPressureCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitBarometricPressureCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureIsEnabled() -> Bool {
|
||||
return adafruitBarometricPressureCharacteristic != nil && adafruitBarometricPressureCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitBarometricPressureCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitBarometricPressureCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureLastValue() -> Float? {
|
||||
guard let data = adafruitBarometricPressureCharacteristic?.value else { return nil }
|
||||
return adafruitBarometricPressureDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitBarometricPressureDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitButtons.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 kAdafruitButtonsServiceUUID = CBUUID(string: "ADAF0600-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitButtonsCharacteristicUUID = CBUUID(string: "ADAF0601-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitButtonsVersion = 1
|
||||
|
||||
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 adafruitButtonsCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitButtonsCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitButtonsCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitButtonsCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitButtonsEnable(responseHandler: @escaping(Result<(ButtonsState, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
let timePeriod: TimeInterval = 0 // 0 means that the responseHandler will be called only when there is a change
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitButtonsVersion, serviceUuid: BlePeripheral.kAdafruitButtonsServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitButtonsCharacteristicUUID, timePeriod: timePeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let state = self.adafruitButtonsDataToStateMask(data)
|
||||
responseHandler(.success((state, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitButtonsCharacteristic = characteristic
|
||||
|
||||
if timePeriod == 0 { // Read initial state if the timePeriod is 0 (update only when changed)
|
||||
self.adafruitButtonsReadState { response in
|
||||
switch response {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
DLog("Error receiving initial button state data: \(error)")
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitButtonsCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitButtonsIsEnabled() -> Bool {
|
||||
return adafruitButtonsCharacteristic != nil && adafruitButtonsCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitButtonsDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitButtonsCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitButtonsCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitButtonsReadState(completion: @escaping(Result<(ButtonsState, UUID), Error>) -> Void) {
|
||||
guard let adafruitButtonsCharacteristic = adafruitButtonsCharacteristic else {
|
||||
completion(.failure(PeripheralAdafruitError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
self.readCharacteristic(adafruitButtonsCharacteristic) { [weak self] (data, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard error == nil, let data = data as? Data else {
|
||||
completion(.failure(error ?? PeripheralAdafruitError.invalidResponseData))
|
||||
return
|
||||
}
|
||||
|
||||
let state = self.adafruitButtonsDataToStateMask(data)
|
||||
completion(.success((state, self.identifier)))
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitButtonsLastValue() -> ButtonsState? {
|
||||
guard let data = adafruitButtonsCharacteristic?.value else { return nil }
|
||||
return adafruitButtonsDataToStateMask(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitButtonsDataToStateMask(_ 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,88 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitColorSensor.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitColorSensorServiceUUID = CBUUID(string: "ADAF0A00-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitColorSensorCharacteristicUUID = CBUUID(string: "ADAF0A01-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitColorSensorVersion = 1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitColorSensorCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitColorSensorCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitColorSensorCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitColorSensorCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitColorSensorEnable(responseHandler: @escaping(Result<(UIColor, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitColorSensorVersion, serviceUuid: BlePeripheral.kAdafruitColorSensorServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitColorSensorCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self.adafruitColorSensorDataToColor(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
}
|
||||
else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitColorSensorCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitColorSensorCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitColorSensorIsEnabled() -> Bool {
|
||||
return adafruitColorSensorCharacteristic != nil && adafruitColorSensorCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitColorSensorDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitColorSensorCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitColorSensorCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitColorSensorLastValue() -> UIColor? {
|
||||
guard let data = adafruitColorSensorCharacteristic?.value else { return nil }
|
||||
return adafruitColorSensorDataToColor(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitColorSensorDataToColor(_ data: Data) -> UIColor? {
|
||||
guard let components = adafruitDataToUInt16Array(data) else { return nil }
|
||||
guard components.count >= 3 else { return nil }
|
||||
return UIColor(red: CGFloat(components[0])/CGFloat(UInt16.max), green: CGFloat(components[1])/CGFloat(UInt16.max), blue: CGFloat(components[2])/CGFloat(UInt16.max), alpha: 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
//
|
||||
// BlePeripehral+AdafruitCommon.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 kAdafruitMeasurementPeriodCharacteristicUUID = CBUUID(string: "ADAF0001-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitServiceVersionCharacteristicUUID = CBUUID(string: "ADAF0002-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
private static let kAdafruitDefaultVersionValue = 1 // Used as default version value if version characteristic cannot be read
|
||||
|
||||
static let kAdafruitSensorDefaultPeriod: TimeInterval = 0.2
|
||||
|
||||
|
||||
// MARK: - Errors
|
||||
enum PeripheralAdafruitError: Error {
|
||||
case invalidCharacteristic
|
||||
case enableNotifyFailed
|
||||
case disableNotifyFailed
|
||||
case unknownVersion
|
||||
case invalidResponseData
|
||||
}
|
||||
|
||||
// MARK: - Service Actions
|
||||
func adafruitServiceEnable(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 ?? PeripheralAdafruitError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
// Check version
|
||||
self.adafruitVersion(serviceUuid: serviceUuid) { version in
|
||||
completion?(.success((version, characteristic)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitServiceEnableIfVersion(version expectedVersion: Int, serviceUuid: CBUUID, mainCharacteristicUuid: CBUUID, completion: ((Result<CBCharacteristic, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnable(serviceUuid: serviceUuid, mainCharacteristicUuid: mainCharacteristicUuid) { [weak self] result in
|
||||
self?.checkVersionResult(expectedVersion: expectedVersion, result: result, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
- parameters:
|
||||
- timePeriod: seconds between measurements. -1 to disable measurements
|
||||
|
||||
*/
|
||||
func adafruitServiceEnableIfVersion(version expectedVersion: Int, serviceUuid: CBUUID, mainCharacteristicUuid: CBUUID, timePeriod: TimeInterval?, responseHandler: @escaping(Result<(Data, UUID), Error>) -> Void, completion: ((Result<CBCharacteristic, Error>) -> Void)?) {
|
||||
|
||||
adafruitServiceEnableIfVersion(version: expectedVersion, serviceUuid: serviceUuid, mainCharacteristicUuid: mainCharacteristicUuid) { [weak self] result in
|
||||
|
||||
switch result {
|
||||
case let .success(characteristic): // Version supported
|
||||
self?.adafruitServiceSetRepeatingResponse(characteristic: characteristic, timePeriod: timePeriod, responseHandler: responseHandler, completion: { result in
|
||||
|
||||
completion?(.success(characteristic))
|
||||
})
|
||||
|
||||
case let .failure(error): // Unsupported version (or error)
|
||||
completion?(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func adafruitServiceSetRepeatingResponse(characteristic: CBCharacteristic, timePeriod: TimeInterval?, responseHandler: @escaping(Result<(Data, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
// 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(PeripheralAdafruitError.enableNotifyFailed))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
|
||||
})
|
||||
} else {
|
||||
self.updateNotifyHandler(for: characteristic, handler: notifyHandler)
|
||||
completion?(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
// Time period
|
||||
if let timePeriod = timePeriod, let serviceUuid = characteristic.service?.uuid { // Set timePeriod if not nil
|
||||
|
||||
self.adafruitSetPeriod(timePeriod, serviceUuid: serviceUuid) { _ in
|
||||
|
||||
if Config.isDebugEnabled {
|
||||
// Check period
|
||||
self.adafruitPeriod(serviceUuid: serviceUuid) { period in
|
||||
guard period != nil else { DLog("Error setting service period"); return }
|
||||
//DLog("service period: \(period!)")
|
||||
}
|
||||
}
|
||||
|
||||
enableNotificationsHandler()
|
||||
}
|
||||
} else { // Use default timePeriod
|
||||
enableNotificationsHandler()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkVersionResult(expectedVersion: Int, result: Result<(Int, CBCharacteristic), Error>, completion: ((Result<CBCharacteristic, Error>) -> Void)?) {
|
||||
switch result {
|
||||
case let .success((version, characteristic)):
|
||||
guard version == expectedVersion else {
|
||||
DLog("Warning: adafruitServiceEnableIfVersion unknown version: \(version). Expected: \(expectedVersion)")
|
||||
completion?(.failure(PeripheralAdafruitError.unknownVersion))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success(characteristic))
|
||||
case let .failure(error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitServiceDisable(serviceUuid: CBUUID, mainCharacteristicUuid: CBUUID, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
self.characteristic(uuid: mainCharacteristicUuid, serviceUuid: serviceUuid) { [weak self] (characteristic, error) in
|
||||
guard let characteristic = characteristic, error == nil else {
|
||||
completion?(.failure(error ?? PeripheralAdafruitError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
let kDisablePeriod: TimeInterval = -1 // -1 means taht the updates will be disabled
|
||||
self?.adafruitSetPeriod(kDisablePeriod, serviceUuid: serviceUuid) { [weak self] result in
|
||||
// Disable notifications
|
||||
if characteristic.isNotifying {
|
||||
self?.disableNotify(for: characteristic) { error in
|
||||
guard error == nil else {
|
||||
completion?(.failure(error!))
|
||||
return
|
||||
}
|
||||
guard !characteristic.isNotifying else {
|
||||
completion?(.failure(PeripheralAdafruitError.disableNotifyFailed))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
}
|
||||
else {
|
||||
completion?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitVersion(serviceUuid: CBUUID, completion: @escaping(Int) -> Void) {
|
||||
self.characteristic(uuid: BlePeripheral.kAdafruitServiceVersionCharacteristicUUID, serviceUuid: serviceUuid) { [weak self] (characteristic, error) in
|
||||
|
||||
// Check if version characteristic exists or return default value
|
||||
guard error == nil, let characteristic = characteristic else {
|
||||
completion(BlePeripheral.kAdafruitDefaultVersionValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the version
|
||||
self?.readCharacteristic(characteristic) { (result, error) in
|
||||
guard error == nil, let data = result as? Data, data.count >= 4 else {
|
||||
completion(BlePeripheral.kAdafruitDefaultVersionValue)
|
||||
return
|
||||
}
|
||||
|
||||
let version = data.toIntFrom32Bits()
|
||||
completion(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitPeriod(serviceUuid: CBUUID, completion: @escaping(TimeInterval?) -> Void) {
|
||||
self.characteristic(uuid: BlePeripheral.kAdafruitMeasurementPeriodCharacteristicUUID, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Set measurement period
|
||||
|
||||
- parameters:
|
||||
- period: seconds between measurements. -1 to disable measurements
|
||||
|
||||
*/
|
||||
func adafruitSetPeriod(_ period: TimeInterval, serviceUuid: CBUUID, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.characteristic(uuid: BlePeripheral.kAdafruitMeasurementPeriodCharacteristicUUID, serviceUuid: serviceUuid) { (characteristic, error) in
|
||||
|
||||
guard error == nil, let characteristic = characteristic else {
|
||||
DLog("Error: adafruitSetPeriod: \(String(describing: error))")
|
||||
return
|
||||
}
|
||||
|
||||
let periodMillis = period == -1 ? -1 : Int32(period * 1000) // -1 means disable measurements. It is a special value
|
||||
let data = periodMillis.littleEndian.data
|
||||
self.write(data: data, for: characteristic, type: .withResponse) { error in
|
||||
guard error == nil else {
|
||||
DLog("Error: adafruitSetPeriod \(error!)")
|
||||
completion?(.failure(error!))
|
||||
return
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
func adafruitDataToFloatArray(_ data: Data) -> [Float]? {
|
||||
let unitSize = MemoryLayout<Float32>.stride
|
||||
var bytes = [Float32](repeating: 0, count: data.count / unitSize)
|
||||
(data as NSData).getBytes(&bytes, length: data.count * unitSize)
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
func adafruitDataToUInt16Array(_ data: Data) -> [UInt16]? {
|
||||
let unitSize = MemoryLayout<UInt16>.stride
|
||||
var words = [UInt16](repeating: 0, count: data.count / unitSize)
|
||||
(data as NSData).getBytes(&words, length: data.count * unitSize)
|
||||
return words
|
||||
}
|
||||
|
||||
func adafruitDataToInt16Array(_ data: Data) -> [Int16]? {
|
||||
let unitSize = MemoryLayout<Int16>.stride
|
||||
var words = [Int16](repeating: 0, count: data.count / unitSize)
|
||||
(data as NSData).getBytes(&words, length: data.count * unitSize)
|
||||
return words
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitGyroscope.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitGyroscopeServiceUUID = CBUUID(string: "ADAF0400-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitGyroscopeCharacteristicUUID = CBUUID(string: "ADAF0401-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitGyroscopeVersion = 1
|
||||
|
||||
// Structs
|
||||
/// Values in rad/s
|
||||
struct GyroscopeValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitGyroscopeCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitGyroscopeCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitGyroscopeCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitGyroscopeCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitGyroscopeEnable(responseHandler: @escaping(Result<(GyroscopeValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitGyroscopeVersion, serviceUuid: BlePeripheral.kAdafruitGyroscopeServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitGyroscopeCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self.adafruitGyroscopeDataToGyroscopeValue(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
} else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitGyroscopeCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitGyroscopeCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitGyroscopeIsEnabled() -> Bool {
|
||||
return adafruitGyroscopeCharacteristic != nil && adafruitGyroscopeCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitGyroscopeDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitGyroscopeCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitGyroscopeCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitGyroscopeLastValue() -> GyroscopeValue? {
|
||||
guard let data = adafruitGyroscopeCharacteristic?.value else { return nil }
|
||||
return adafruitGyroscopeDataToGyroscopeValue(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitGyroscopeDataToGyroscopeValue(_ data: Data) -> GyroscopeValue? {
|
||||
guard let bytes = adafruitDataToFloatArray(data) else { return nil }
|
||||
guard bytes.count >= 3 else { return nil }
|
||||
return GyroscopeValue(x: bytes[0], y: bytes[1], z: bytes[2])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitHumidity.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitHumidityServiceUUID = CBUUID(string: "ADAF0700-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitHumidityCharacteristicUUID = CBUUID(string: "ADAF0701-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitHumidityVersion = 1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitHumidityCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitHumidityCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitHumidityCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitHumidityCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitHumidityEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitHumidityVersion, serviceUuid: BlePeripheral.kAdafruitHumidityServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitHumidityCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let value = self.adafruitHumidityDataToFloat(data)
|
||||
responseHandler(.success((value, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitHumidityCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitHumidityCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitHumidityIsEnabled() -> Bool {
|
||||
return adafruitHumidityCharacteristic != nil && adafruitHumidityCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitHumidityDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitHumidityCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitHumidityCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitHumidityLastValue() -> Float? {
|
||||
guard let data = adafruitHumidityCharacteristic?.value else { return nil }
|
||||
return adafruitHumidityDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitHumidityDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitLight.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 kAdafruitLightServiceUUID = CBUUID(string: "ADAF0300-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitLightCharacteristicUUID = CBUUID(string: "ADAF0301-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitLightVersion = 1
|
||||
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitLightCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitLightCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitLightCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitLightCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitLightEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitLightVersion, serviceUuid: BlePeripheral.kAdafruitLightServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitLightCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let value = self.adafruitLightDataToFloat(data)
|
||||
responseHandler(.success((value, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitLightCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitLightCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitLightIsEnabled() -> Bool {
|
||||
return adafruitLightCharacteristic != nil && adafruitLightCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitLightDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitLightCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitLightCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitLightLastValue() -> Float? {
|
||||
guard let data = adafruitLightCharacteristic?.value else { return nil }
|
||||
return adafruitLightDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitLightDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitMagnetometer.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitMagnetometerServiceUUID = CBUUID(string: "ADAF0500-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitMagnetometerCharacteristicUUID = CBUUID(string: "ADAF0501-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitMagnetometerVersion = 1
|
||||
|
||||
// Structs
|
||||
/// Values in microTesla (μT)
|
||||
struct MagnetometerValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitMagnetometerCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitMagnetometerCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitMagnetometerCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitMagnetometerCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitMagnetometerEnable(responseHandler: @escaping(Result<(MagnetometerValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitMagnetometerVersion, serviceUuid: BlePeripheral.kAdafruitMagnetometerServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitMagnetometerCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self.adafruitMagnetometerDataToMagnetometerValue(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
} else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitMagnetometerCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitMagnetometerCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitMagnetometerIsEnabled() -> Bool {
|
||||
return adafruitMagnetometerCharacteristic != nil && adafruitMagnetometerCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitMagnetometerDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitMagnetometerCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitMagnetometerCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitMagnetometerLastValue() -> MagnetometerValue? {
|
||||
guard let data = adafruitMagnetometerCharacteristic?.value else { return nil }
|
||||
return adafruitMagnetometerDataToMagnetometerValue(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitMagnetometerDataToMagnetometerValue(_ data: Data) -> MagnetometerValue? {
|
||||
guard let bytes = adafruitDataToFloatArray(data) else { return nil }
|
||||
guard bytes.count >= 3 else { return nil }
|
||||
return MagnetometerValue(x: bytes[0], y: bytes[1], z: bytes[2])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitNeoPixels.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 kAdafruitNeoPixelsServiceNumberOfBitsPerPixel = 3
|
||||
private static let kAdafruitNeoPixelsVersion = 1
|
||||
|
||||
// Constants
|
||||
static let kAdafruitNeoPixelsServiceUUID = CBUUID(string: "ADAF0900-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitNeoPixelsDataCharacteristicUUID = CBUUID(string: "ADAF0903-C332-42A8-93BD-25E905756CB8")
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitNeoPixelsDataCharacteristic: CBCharacteristic?
|
||||
static var adafruitNeoPixelsDataValue: Data?
|
||||
}
|
||||
|
||||
private var adafruitNeoPixelsDataCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitNeoPixelsDataCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitNeoPixelsDataCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var adafruitNeoPixelsDataValue: Data {
|
||||
get {
|
||||
if let data = objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitNeoPixelsDataValue) as? Data {
|
||||
return data
|
||||
} else { // Initial value
|
||||
return Data(repeating: 0, count: adafruitNeoPixelsCount * BlePeripheral.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitNeoPixelsDataValue, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitNeoPixelsEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitNeoPixelsVersion, serviceUuid: BlePeripheral.kAdafruitNeoPixelsServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitNeoPixelsDataCharacteristicUUID) { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitNeoPixelsDataCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitNeoPixelsDataCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var adafruitNeoPixelsCount: Int {
|
||||
return self.adafruitManufacturerData()?.boardModel?.neoPixelsCount ?? 0
|
||||
}
|
||||
|
||||
func adafruitNeoPixelsIsEnabled() -> Bool {
|
||||
return adafruitNeoPixelsDataCharacteristic != nil
|
||||
}
|
||||
|
||||
func adafruitNeoPixelsDisable() {
|
||||
// Clear all specific data
|
||||
adafruitNeoPixelsDataCharacteristic = nil
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetAllPixelsColor(_ color: UIColor) {
|
||||
let colors = [UIColor](repeating: color, count: adafruitNeoPixelsCount)
|
||||
adafruitNeoPixelsWriteData(offset: 0, colors: colors)
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetPixelColor(index: Int, color: UIColor) {
|
||||
let offset = UInt16(index * BlePeripheral.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel)
|
||||
adafruitNeoPixelsWriteData(offset: offset, colors: [color])
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetColor(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.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel))
|
||||
adafruitNeoPixelsWriteData(offset: offset, pixelData: pixelData)
|
||||
}
|
||||
|
||||
// MARK: - Low level actions
|
||||
func adafruitNeoPixelsWriteData(offset: UInt16, colors: [UIColor]) {
|
||||
let pixelData = BlePeripheral.pixelDataFromColors(colors)
|
||||
adafruitNeoPixelsWriteData(offset: offset, pixelData: pixelData)
|
||||
}
|
||||
|
||||
func adafruitNeoPixelsWriteData(offset: UInt16, pixelData: Data) {
|
||||
guard let adafruitNeoPixelsDataCharacteristic = adafruitNeoPixelsDataCharacteristic 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: adafruitNeoPixelsDataCharacteristic, type: .withResponse) { [unowned self] error in
|
||||
guard error == nil else { DLog("Error adafruitNeoPixelsWriteData: \(error!)"); return }
|
||||
|
||||
self.adafruitNeoPixelsDataValue = 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.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel
|
||||
DLog("adafruitNeoPixelsDataValue.count: \(adafruitNeoPixelsDataValue.count) ")
|
||||
if byteOffset < adafruitNeoPixelsDataValue.count {
|
||||
existingColorData = Data(adafruitNeoPixelsDataValue[byteOffset..<(byteOffset + BlePeripheral.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel)])
|
||||
} else {
|
||||
existingColorData = Data(repeating: 0, count: BlePeripheral.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
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]?
|
||||
|
||||
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.kAdafruitNeoPixelsServiceNumberOfBitsPerPixel)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitQuaternion.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 kAdafruitQuaternionServiceUUID = CBUUID(string: "ADAF0D00-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitQuaternionCharacteristicUUID = CBUUID(string: "ADAF0D01-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitQuaternionCalibrationInCharacteristicUUID = CBUUID(string: "ADAFD002-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitQuaternionCalibrationOutCharacteristicUUID = CBUUID(string: "ADAFD003-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitQuaternionVersion = 1
|
||||
|
||||
// Structs
|
||||
struct QuaternionValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
var w: Float
|
||||
}
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitQuaternionCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitQuaternionCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitQuaternionCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitQuaternionCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitQuaternionEnable(responseHandler: @escaping(Result<(QuaternionValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitQuaternionVersion, serviceUuid: BlePeripheral.kAdafruitQuaternionServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitQuaternionCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self.adafruitQuaternionDataToQuaternionValue(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
} else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitQuaternionCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitQuaternionCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitQuaternionIsEnabled() -> Bool {
|
||||
return adafruitQuaternionCharacteristic != nil && adafruitQuaternionCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitQuaternionDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitQuaternionCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitQuaternionCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitQuaternionLastValue() -> QuaternionValue? {
|
||||
guard let data = adafruitQuaternionCharacteristic?.value else { return nil }
|
||||
return adafruitQuaternionDataToQuaternionValue(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitQuaternionDataToQuaternionValue(_ data: Data) -> QuaternionValue? {
|
||||
|
||||
guard let bytes = adafruitDataToFloatArray(data) else { return nil }
|
||||
guard bytes.count >= 4 else { return nil }
|
||||
//return QuaternionValue(x: bytes[0], y: bytes[1], z: bytes[2], w: bytes[3])
|
||||
return QuaternionValue(x: bytes[1], y: bytes[2], z: bytes[3], w: bytes[0])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitSoundSensor.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 09/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
static let kAdafruitSoundSensorServiceUUID = CBUUID(string: "ADAF0B00-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitSoundSamplesCharacteristicUUID = CBUUID(string: "ADAF0B01-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitSoundNumberOfChannelsCharacteristicUUID = CBUUID(string: "ADAF0B02-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitSoundSensorVersion = 1
|
||||
|
||||
static let kAdafruitSoundSensorMaxAmplitude = 32768 // Int16 range is from -32768 to 32767
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitSoundCharacteristic: CBCharacteristic?
|
||||
static var adafruitSoundNumChannels: Int = 0
|
||||
}
|
||||
|
||||
private var adafruitSoundCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitSoundCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitSoundCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
private var adafruitSoundNumChannels: Int {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitSoundNumChannels) as? Int ?? 0
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitSoundNumChannels, newValue, .OBJC_ASSOCIATION_ASSIGN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitSoundEnable(responseHandler: @escaping(Result<([Double], UUID), Error>) -> Void, completion: ((Result<Int, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitSoundSensorVersion, serviceUuid: BlePeripheral.kAdafruitSoundSensorServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitSoundSamplesCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { [weak self] response in
|
||||
|
||||
guard self?.adafruitSoundNumChannels ?? 0 > 0 else { return } // Ignore received data until sound channels are defined
|
||||
// TODO: read sound channels BEFORE enabling notify
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
if let value = self?.adafruitSoundDataToAmplitudePerChannel(data) {
|
||||
responseHandler(.success((value, uuid)))
|
||||
}
|
||||
else {
|
||||
responseHandler(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
}
|
||||
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitSoundCharacteristic = characteristic
|
||||
|
||||
// Read number of channels
|
||||
self.characteristic(uuid: BlePeripheral.kAdafruitSoundNumberOfChannelsCharacteristicUUID, serviceUuid: BlePeripheral.kAdafruitSoundSensorServiceUUID) { [weak self] (characteristic, error) in
|
||||
|
||||
guard error == nil, let characteristic = characteristic else {
|
||||
self?.adafruitSoundDisable() // Error, disable sound // TODO: dont enable until checks have been performed
|
||||
completion?(.failure(PeripheralAdafruitError.invalidCharacteristic))
|
||||
return
|
||||
}
|
||||
|
||||
self?.readCharacteristic(characteristic) { (result, error) in
|
||||
guard error == nil, let data = result as? Data, data.count >= 1 else {
|
||||
DLog("Error reading numChannels: \(error?.localizedDescription ?? "")")
|
||||
completion?(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
return
|
||||
}
|
||||
|
||||
let numChannels = Int(data[0]) // from 0 to 100
|
||||
self?.adafruitSoundNumChannels = numChannels
|
||||
completion?(.success(numChannels))
|
||||
}
|
||||
}
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitSoundCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitSoundIsEnabled() -> Bool {
|
||||
return adafruitSoundCharacteristic != nil && adafruitSoundCharacteristic!.isNotifying && adafruitSoundNumChannels > 0
|
||||
}
|
||||
|
||||
func adafruitSoundDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitSoundCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitSoundCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitSoundLastAmplitudePerChannel() -> [Double]? { // Samples fo reach channel
|
||||
guard let data = adafruitSoundCharacteristic?.value else { return nil }
|
||||
return adafruitSoundDataToAmplitudePerChannel(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
/**
|
||||
Convert raw data into the amplitude for each channel
|
||||
- returns: array with amplitude for each channel measured in decibel relative to full scale (dBFS)
|
||||
*/
|
||||
private func adafruitSoundDataToAmplitudePerChannel(_ data: Data) -> [Double]? {
|
||||
guard let samples = adafruitDataToInt16Array(data) else { return nil }
|
||||
let numChannels = adafruitSoundNumChannels
|
||||
guard numChannels > 0, samples.count >= numChannels else { return nil }
|
||||
|
||||
var samplesSumPerChannel = [Double](repeating: 0, count: numChannels)
|
||||
for (index, sample) in samples.enumerated() {
|
||||
let channelIndex = index % numChannels
|
||||
samplesSumPerChannel[channelIndex] += abs(Double(sample))
|
||||
}
|
||||
|
||||
let samplesPerChannel = samples.count / numChannels
|
||||
var amplitudePerChannel = [Double](repeating: 0, count: numChannels)
|
||||
for (index, samplesSum) in samplesSumPerChannel.enumerated() {
|
||||
let samplesAvg = samplesSum / Double(samplesPerChannel)
|
||||
|
||||
// Calculate amplitude
|
||||
// based on: https://devzone.nordicsemi.com/f/nordic-q-a/28248/get-amplitude-db-from-pdm/111560#111560
|
||||
let amplitude = 20 * log10(abs(samplesAvg) / Double(BlePeripheral.kAdafruitSoundSensorMaxAmplitude))
|
||||
|
||||
// Note:
|
||||
// The base 10 log of -1 is NaN.
|
||||
// The base 10 log of 0 is -Infinity.
|
||||
|
||||
amplitudePerChannel[index] = amplitude
|
||||
}
|
||||
|
||||
return amplitudePerChannel
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// AdafruitTemperature.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 kAdafruitTemperatureServiceUUID = CBUUID(string: "ADAF0100-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitTemperatureCharacteristicUUID = CBUUID(string: "ADAF0101-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitTemperatureVersion = 1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitTemperatureCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitTemperatureCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitTemperatureCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitTemperatureCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitTemperatureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitTemperatureVersion, serviceUuid: BlePeripheral.kAdafruitTemperatureServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitTemperatureCharacteristicUUID, timePeriod: BlePeripheral.kAdafruitSensorDefaultPeriod, responseHandler: { response in
|
||||
|
||||
switch response {
|
||||
case let .success((data, uuid)):
|
||||
let temperature = self.adafruitTemperatureDataToFloat(data)
|
||||
responseHandler(.success((temperature, uuid)))
|
||||
case let .failure(error):
|
||||
responseHandler(.failure(error))
|
||||
}
|
||||
|
||||
}, completion: { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitTemperatureCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitTemperatureCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adafruitTemperatureIsEnabled() -> Bool {
|
||||
return adafruitTemperatureCharacteristic != nil && adafruitTemperatureCharacteristic!.isNotifying
|
||||
}
|
||||
|
||||
func adafruitTemperatureDisable() {
|
||||
// Clear all specific data
|
||||
defer {
|
||||
adafruitTemperatureCharacteristic = nil
|
||||
}
|
||||
|
||||
// Disable notify
|
||||
guard let characteristic = adafruitTemperatureCharacteristic, characteristic.isNotifying else { return }
|
||||
disableNotify(for: characteristic)
|
||||
}
|
||||
|
||||
func adafruitTemperatureLastValue() -> Float? {
|
||||
guard let data = adafruitTemperatureCharacteristic?.value else { return nil }
|
||||
return adafruitTemperatureDataToFloat(data)
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
private func adafruitTemperatureDataToFloat(_ data: Data) -> Float {
|
||||
return data.toFloatFrom32Bits()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitToneGenerator.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 kAdafruitToneGeneratorServiceUUID = CBUUID(string: "ADAF0C00-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitToneGeneratorCharacteristicUUID = CBUUID(string: "ADAF0C01-C332-42A8-93BD-25E905756CB8")
|
||||
private static let kAdafruitToneGeneratorVersion = 1
|
||||
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitToneGeneratorCharacteristic: CBCharacteristic?
|
||||
}
|
||||
|
||||
private var adafruitToneGeneratorCharacteristic: CBCharacteristic? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitToneGeneratorCharacteristic) as? CBCharacteristic
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitToneGeneratorCharacteristic, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitToneGeneratorEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
self.adafruitServiceEnableIfVersion(version: BlePeripheral.kAdafruitToneGeneratorVersion, serviceUuid: BlePeripheral.kAdafruitToneGeneratorServiceUUID, mainCharacteristicUuid: BlePeripheral.kAdafruitToneGeneratorCharacteristicUUID) { result in
|
||||
switch result {
|
||||
case let .success(characteristic):
|
||||
self.adafruitToneGeneratorCharacteristic = characteristic
|
||||
completion?(.success(()))
|
||||
|
||||
case let .failure(error):
|
||||
self.adafruitToneGeneratorCharacteristic = nil
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorIsEnabled() -> Bool {
|
||||
return adafruitToneGeneratorCharacteristic != nil
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorDisable() {
|
||||
// Clear all specific data
|
||||
adafruitToneGeneratorCharacteristic = nil
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorStartPlaying(frequency: UInt16, duration: UInt32 = 0) { // Duration 0 means non-stop
|
||||
guard let adafruitToneGeneratorCharacteristic = adafruitToneGeneratorCharacteristic else { return }
|
||||
|
||||
let data = frequency.littleEndian.data + duration.littleEndian.data
|
||||
self.write(data: data, for: adafruitToneGeneratorCharacteristic, type: .withResponse)
|
||||
//DLog("tone: \(frequency)")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// BlePeripheral+ManufacturerAdafruit.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 10/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Constants
|
||||
internal static let kManufacturerAdafruitIdentifier: [UInt8] = [0x22, 0x08]
|
||||
|
||||
// MARK: - Check Manufacturer
|
||||
func isManufacturerAdafruit() -> Bool {
|
||||
guard let manufacturerIdentifier = advertisement.manufacturerIdentifier else { return false }
|
||||
|
||||
let manufacturerIdentifierBytes = [UInt8](manufacturerIdentifier)
|
||||
//DLog("\(name) manufacturer: \(advertisement.manufacturerString)")
|
||||
return manufacturerIdentifierBytes == BlePeripheral.kManufacturerAdafruitIdentifier
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Adafruit Specific Data
|
||||
struct AdafruitManufacturerData {
|
||||
// Types
|
||||
enum BoardModel: CaseIterable {
|
||||
case circuitPlaygroundBluefruit
|
||||
case clue_nRF52840
|
||||
case feather_nRF52840_express
|
||||
case feather_nRF52832
|
||||
|
||||
var identifier: [[UInt8]] { // Board identifiers used on the advertisement packet (USB PID)
|
||||
switch self {
|
||||
case .circuitPlaygroundBluefruit: return [[0x45, 0x80], [0x46, 0x80]]
|
||||
case .clue_nRF52840: return [[0x71, 0x80], [0x72, 0x80]]
|
||||
case .feather_nRF52840_express: return [[0x29, 0x80], [0x2A, 0x80]]
|
||||
case .feather_nRF52832: return [[0x60, 0xEA]]
|
||||
}
|
||||
}
|
||||
|
||||
var neoPixelsCount: Int {
|
||||
switch self {
|
||||
case .circuitPlaygroundBluefruit: return 10
|
||||
case .clue_nRF52840: return 1
|
||||
case .feather_nRF52840_express: return 0
|
||||
case .feather_nRF52832: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
var color: UIColor?
|
||||
var boardModel: BoardModel?
|
||||
|
||||
// Utils
|
||||
static func board(withBoardTypeData data: Data) -> BoardModel? {
|
||||
let bytes = [UInt8](data)
|
||||
|
||||
let board = BoardModel.allCases.first(where: {
|
||||
$0.identifier.contains(bytes)
|
||||
})
|
||||
|
||||
return board
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func adafruitManufacturerData() -> AdafruitManufacturerData? {
|
||||
guard let manufacturerData = advertisement.manufacturerData else { return nil }
|
||||
guard manufacturerData.count > 2 else { return nil } // It should have fields beyond the manufacturer identifier
|
||||
|
||||
var manufacturerFieldsData = Data(manufacturerData.dropFirst(2)) // Remove manufacturer identifier
|
||||
|
||||
var adafruitManufacturerData = AdafruitManufacturerData()
|
||||
|
||||
// Parse fields
|
||||
let kHeaderLength = 1 + 2 // 1 byte len + 2 bytes key
|
||||
while manufacturerFieldsData.count >= kHeaderLength {
|
||||
// Parse current field
|
||||
guard let fieldKey = Int16(data: manufacturerFieldsData[1...2]) else { return nil }
|
||||
let fieldDataLenght = Int(manufacturerFieldsData[0]) - kHeaderLength // don't count header
|
||||
let fieldData: Data
|
||||
if manufacturerFieldsData.count >= kHeaderLength + fieldDataLenght {
|
||||
fieldData = Data(manufacturerFieldsData[kHeaderLength...])
|
||||
} else {
|
||||
fieldData = Data()
|
||||
}
|
||||
|
||||
// Decode field
|
||||
switch fieldKey {
|
||||
case 0 where fieldData.count >= 3: // Color
|
||||
let r = fieldData[0]
|
||||
let g = fieldData[1]
|
||||
let b = fieldData[2]
|
||||
adafruitManufacturerData.color = UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: 1)
|
||||
|
||||
case 1 where fieldData.count >= 2: // Board type
|
||||
let boardTypeData = fieldData[0..<2]
|
||||
|
||||
if let board = AdafruitManufacturerData.board(withBoardTypeData: boardTypeData) {
|
||||
adafruitManufacturerData.boardModel = board
|
||||
}
|
||||
else {
|
||||
DLog("Warning: unknown board type found: \([UInt8](boardTypeData))")
|
||||
}
|
||||
|
||||
default:
|
||||
DLog("Error processing manufacturer data with key: \(fieldKey) len: \(fieldData.count) expectedLen: \(fieldDataLenght)")
|
||||
break
|
||||
}
|
||||
|
||||
// Remove processed field
|
||||
manufacturerFieldsData = Data(manufacturerFieldsData.dropFirst(3 + fieldDataLenght))
|
||||
}
|
||||
|
||||
return adafruitManufacturerData
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
//
|
||||
// BleManager.swift
|
||||
// BleManager
|
||||
//
|
||||
// Created by Antonio García on 13/10/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
import QuartzCore
|
||||
|
||||
public class BleManager: NSObject {
|
||||
// Configuration
|
||||
private static let kStopScanningWhenConnectingToPeripheral = false
|
||||
private static let kAlwaysAllowDuplicateKeys = true
|
||||
|
||||
// Singleton
|
||||
public static let shared = BleManager()
|
||||
|
||||
// Ble
|
||||
var centralManager: CBCentralManager?
|
||||
|
||||
// Scanning
|
||||
public var isScanning: Bool {
|
||||
return scanningStartTime != nil
|
||||
}
|
||||
public var scanningElapsedTime: TimeInterval? {
|
||||
guard let scanningStartTime = scanningStartTime else { return nil }
|
||||
return CACurrentMediaTime() - scanningStartTime
|
||||
}
|
||||
private var isScanningWaitingToStart = false
|
||||
internal var scanningStartTime: TimeInterval? // Time when the scanning started. nil if stopped
|
||||
private var scanningServicesFilter: [CBUUID]?
|
||||
internal var peripheralsFound = [UUID: BlePeripheral]()
|
||||
private var peripheralsFoundFirstTime = [UUID: Date]() // Date that the perihperal was discovered for the first time. Useful for sorting
|
||||
internal var peripheralsFoundLock = NSLock()
|
||||
|
||||
// Connecting
|
||||
private var connectionTimeoutTimers = [UUID: Foundation.Timer]()
|
||||
private var autoreconnectOnDisconnection = Set<UUID>() // List of peripheral IDs to automatically reconnect if disconnected
|
||||
|
||||
// Notifications
|
||||
public enum NotificationUserInfoKey: String {
|
||||
case uuid = "uuid"
|
||||
case error = "error"
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.global(qos: .background), options: [:])
|
||||
// centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [:])
|
||||
}
|
||||
|
||||
deinit {
|
||||
scanningServicesFilter?.removeAll()
|
||||
peripheralsFound.removeAll()
|
||||
peripheralsFoundFirstTime.removeAll()
|
||||
}
|
||||
|
||||
public var state: CBManagerState {
|
||||
return centralManager?.state ?? .unknown
|
||||
}
|
||||
|
||||
func restoreCentralManager() {
|
||||
DLog("Restoring central manager")
|
||||
|
||||
// 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
|
||||
public func startScan(withServices services: [CBUUID]? = nil) {
|
||||
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")
|
||||
scanningStartTime = CACurrentMediaTime()
|
||||
NotificationCenter.default.post(name: .didStartScanning, object: nil)
|
||||
|
||||
let options = BleManager.kAlwaysAllowDuplicateKeys ? [CBCentralManagerScanOptionAllowDuplicatesKey: true] : nil
|
||||
centralManager.scanForPeripherals(withServices: services, options: options)
|
||||
isScanningWaitingToStart = false
|
||||
}
|
||||
|
||||
public func stopScan() {
|
||||
// DLog("stop scan")
|
||||
centralManager?.stopScan()
|
||||
scanningStartTime = nil
|
||||
isScanningWaitingToStart = false
|
||||
NotificationCenter.default.post(name: .didStopScanning, object: nil)
|
||||
}
|
||||
|
||||
public func numPeripherals() -> Int {
|
||||
return peripheralsFound.count
|
||||
}
|
||||
|
||||
public func peripherals() -> [BlePeripheral] {
|
||||
peripheralsFoundLock.lock(); defer { peripheralsFoundLock.unlock() }
|
||||
return Array(peripheralsFound.values)
|
||||
}
|
||||
|
||||
public func peripheralsSortedByFirstDiscovery() -> [BlePeripheral] {
|
||||
let now = Date()
|
||||
var peripheralsList = peripherals()
|
||||
peripheralsList.sort { (p0, p1) -> Bool in
|
||||
peripheralsFoundFirstTime[p0.identifier] ?? now < peripheralsFoundFirstTime[p1.identifier] ?? now
|
||||
}
|
||||
|
||||
return peripheralsList
|
||||
}
|
||||
|
||||
public func peripheralsSortedByRSSI() -> [BlePeripheral] {
|
||||
var peripheralsList = peripherals()
|
||||
peripheralsList.sort { (p0, p1) -> Bool in
|
||||
return (p0.rssi ?? -127) > (p1.rssi ?? -127)
|
||||
}
|
||||
|
||||
return peripheralsList
|
||||
}
|
||||
|
||||
public func connectedPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connected}
|
||||
}
|
||||
|
||||
public func connectingPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connecting}
|
||||
}
|
||||
|
||||
public func connectedOrConnectingPeripherals() -> [BlePeripheral] {
|
||||
return peripherals().filter {$0.state == .connected || $0.state == .connecting}
|
||||
}
|
||||
|
||||
public 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)
|
||||
peripheralsFoundFirstTime.removeValue(forKey: identifier)
|
||||
}
|
||||
}
|
||||
peripheralsFoundLock.unlock()
|
||||
|
||||
//
|
||||
NotificationCenter.default.post(name: .didUnDiscoverPeripheral, object: nil)
|
||||
startScan(withServices: scanningServicesFilter)
|
||||
}
|
||||
|
||||
// MARK: - Connection Management
|
||||
public func connect(to peripheral: BlePeripheral, timeout: TimeInterval? = nil, shouldNotifyOnConnection: Bool = false, shouldNotifyOnDisconnection: Bool = false, shouldNotifyOnNotification: Bool = false) {
|
||||
|
||||
guard centralManager?.state == .poweredOn else {
|
||||
DLog("connect failed because central manager is not ready")
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
self.connectionTimeoutTimers[peripheral.identifier] = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(self.connectionTimeoutFired), userInfo: peripheral.identifier, repeats: false)
|
||||
}
|
||||
centralManager?.connect(peripheral.peripheral, options: options)
|
||||
}
|
||||
|
||||
@objc private func connectionTimeoutFired(timer: Foundation.Timer) {
|
||||
let peripheralIdentifier = timer.userInfo as! UUID
|
||||
DLog("connection timeout for: \(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])
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect(from peripheral: BlePeripheral, waitForQueuedCommands: Bool = false) {
|
||||
guard let centralManager = centralManager else { return}
|
||||
|
||||
DLog("disconnect: \(peripheral.identifier)")
|
||||
autoreconnectOnDisconnection.remove(peripheral.identifier) // Disable autoreconnection because user initiated the disconection
|
||||
|
||||
NotificationCenter.default.post(name: .willDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
|
||||
if waitForQueuedCommands {
|
||||
// Send the disconnection to the command queue, so all the previous command are executed before disconnecting
|
||||
peripheral.disconnect(centralManager: centralManager)
|
||||
} else {
|
||||
centralManager.cancelPeripheralConnection(peripheral.peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverConnectedPeripherals(services: [CBUUID]) {
|
||||
guard let centralManager = centralManager else { return}
|
||||
|
||||
let peripheralsWithServices = centralManager.retrieveConnectedPeripherals(withServices: services)
|
||||
if !peripheralsWithServices.isEmpty {
|
||||
let alreadyConnectingOrConnectedPeripheralsIds = BleManager.shared.connectedOrConnectingPeripherals().map{$0.identifier}
|
||||
for peripheral in peripheralsWithServices {
|
||||
if !alreadyConnectingOrConnectedPeripheralsIds.contains(peripheral.identifier) {
|
||||
DLog("Discovered peripheral with known service: \(peripheral.identifier)")
|
||||
let advertisementData = [CBAdvertisementDataServiceUUIDsKey: services]
|
||||
discovered(peripheral: peripheral, advertisementData: advertisementData )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func reconnecToPeripherals(peripheralsData: [(identifier: UUID, advertisementData: [String: Any])], withServices services: [CBUUID], timeout: Double? = nil) -> Bool {
|
||||
|
||||
guard let centralManager = centralManager else { return false }
|
||||
var reconnecting = false
|
||||
|
||||
// Reconnect to a known identifier
|
||||
let identifiers = peripheralsData.map({$0.identifier})
|
||||
|
||||
if !identifiers.isEmpty {
|
||||
let peripheralsWithIdentifiers = centralManager.retrievePeripherals(withIdentifiers: identifiers)
|
||||
for peripheral in peripheralsWithIdentifiers {
|
||||
if let peripheralData = peripheralsData.first(where: {$0.identifier == peripheral.identifier}) {
|
||||
DLog("Try to connect to known peripheral: \(peripheral.identifier)")
|
||||
discovered(peripheral: peripheral, advertisementData: peripheralData.advertisementData)
|
||||
if let blePeripheral = peripheralsFound[peripheral.identifier] {
|
||||
connect(to: blePeripheral, timeout: timeout)
|
||||
reconnecting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect even if no identifier was saved if we are already connected to a device with the expected services
|
||||
let peripheralsWithServices = centralManager.retrieveConnectedPeripherals(withServices: services)
|
||||
if !peripheralsWithServices.isEmpty {
|
||||
let alreadyConnectingOrConnectedPeripheralsIds = BleManager.shared.connectedOrConnectingPeripherals().map{$0.identifier}
|
||||
for peripheral in peripheralsWithServices {
|
||||
if !alreadyConnectingOrConnectedPeripheralsIds.contains(peripheral.identifier) {
|
||||
if let peripheralData = peripheralsData.first(where: {$0.identifier == peripheral.identifier}) {
|
||||
DLog("Connect to peripheral with known service: \(peripheral.identifier)")
|
||||
discovered(peripheral: peripheral, advertisementData: peripheralData.advertisementData )
|
||||
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 != BlePeripheral.kUndefinedRssiValue { // 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
|
||||
peripheralsFoundFirstTime[peripheral.identifier] = Date()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
public func peripheralUUID(from notification: Notification) -> UUID? {
|
||||
return notification.userInfo?[NotificationUserInfoKey.uuid.rawValue] as? UUID
|
||||
}
|
||||
|
||||
public func peripheral(from notification: Notification) -> BlePeripheral? {
|
||||
guard let uuid = peripheralUUID(from: notification) else { return nil }
|
||||
|
||||
return peripheral(with: uuid)
|
||||
}
|
||||
|
||||
public func error(from notification: Notification) -> Error? {
|
||||
return notification.userInfo?[NotificationUserInfoKey.error.rawValue] as? Error
|
||||
}
|
||||
|
||||
public func peripheral(with uuid: UUID) -> BlePeripheral? {
|
||||
return peripheralsFound[uuid]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBCentralManagerDelegate
|
||||
extension BleManager: CBCentralManagerDelegate {
|
||||
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
DLog("centralManagerDidUpdateState: \(central.state.rawValue)")
|
||||
|
||||
// Scanning
|
||||
if central.state == .poweredOn {
|
||||
if isScanningWaitingToStart {
|
||||
startScan(withServices: scanningServicesFilter) // Continue scanning now that bluetooth is back
|
||||
}
|
||||
} else {
|
||||
if isScanning {
|
||||
isScanningWaitingToStart = true
|
||||
}
|
||||
scanningStartTime = nil
|
||||
|
||||
// Remove all peripherals found (Important because the BlePeripheral queues could contain old commands that were processing when the bluetooth state changed)
|
||||
peripheralsFound.removeAll()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .didUpdateBleState, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
|
||||
}*/
|
||||
|
||||
public 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])
|
||||
}
|
||||
}
|
||||
|
||||
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
DLog("didConnect: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
|
||||
// Remove connection timeout if exists
|
||||
if let timer = connectionTimeoutTimers[peripheral.identifier] {
|
||||
timer.invalidate()
|
||||
connectionTimeoutTimers[peripheral.identifier] = nil
|
||||
}
|
||||
|
||||
// Set reconnection flag
|
||||
autoreconnectOnDisconnection.insert(peripheral.identifier)
|
||||
|
||||
// Send notification
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .didConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
}
|
||||
|
||||
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
DLog("didFailToConnect: \(peripheral.name ?? peripheral.identifier.uuidString). \(String(describing: error))")
|
||||
|
||||
// Clean
|
||||
peripheralsFound[peripheral.identifier]?.reset()
|
||||
|
||||
// Notify
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [
|
||||
NotificationUserInfoKey.uuid.rawValue: peripheral.identifier,
|
||||
NotificationUserInfoKey.error.rawValue: error as Any
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
|
||||
let peripheralIdentifier = peripheral.identifier
|
||||
DLog("didDisconnectPeripheral: \(peripheral.name ?? peripheralIdentifier.uuidString)")
|
||||
|
||||
// Clean
|
||||
peripheralsFound[peripheralIdentifier]?.reset()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Try to reconnect automatically
|
||||
if self.autoreconnectOnDisconnection.contains(peripheralIdentifier),
|
||||
let blePeripheral = self.peripheralsFound[peripheralIdentifier] {
|
||||
self.autoreconnectOnDisconnection.remove(peripheral.identifier)
|
||||
|
||||
DLog("Trying to reconnect to peripheral: \(peripheral.name ?? peripheralIdentifier.uuidString)")
|
||||
NotificationCenter.default.post(name: .willReconnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
|
||||
self.connect(to: blePeripheral)
|
||||
}
|
||||
else {
|
||||
// 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)
|
||||
self.peripheralsFoundLock.lock()
|
||||
self.peripheralsFound.removeValue(forKey: peripheral.identifier)
|
||||
self.peripheralsFoundLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom Notifications
|
||||
extension Notification.Name {
|
||||
private static let kPrefix = Bundle.main.bundleIdentifier!
|
||||
public static let didUpdateBleState = Notification.Name(kPrefix+".didUpdateBleState")
|
||||
public static let didStartScanning = Notification.Name(kPrefix+".didStartScanning")
|
||||
public static let didStopScanning = Notification.Name(kPrefix+".didStopScanning")
|
||||
public static let didDiscoverPeripheral = Notification.Name(kPrefix+".didDiscoverPeripheral")
|
||||
public static let didUnDiscoverPeripheral = Notification.Name(kPrefix+".didUnDiscoverPeripheral")
|
||||
public static let willConnectToPeripheral = Notification.Name(kPrefix+".willConnectToPeripheral")
|
||||
public static let didConnectToPeripheral = Notification.Name(kPrefix+".didConnectToPeripheral")
|
||||
public static let willDisconnectFromPeripheral = Notification.Name(kPrefix+".willDisconnectFromPeripheral")
|
||||
public static let didDisconnectFromPeripheral = Notification.Name(kPrefix+".didDisconnectFromPeripheral")
|
||||
public static let willReconnectToPeripheral = Notification.Name(kPrefix+".willReconnectToPeripheral")
|
||||
}
|
||||
|
|
@ -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,336 @@
|
|||
//
|
||||
// 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(uartServiceUuid: CBUUID = BlePeripheral.kUartServiceUUID,
|
||||
txCharacteristicUuid: CBUUID = BlePeripheral.kUartTxCharacteristicUUID,
|
||||
rxCharacteristicUuid: CBUUID = BlePeripheral.kUartRxCharacteristicUUID,
|
||||
uartRxHandler: ((Data?, UUID, Error?) -> Void)?, completion: ((Error?) -> Void)?) {
|
||||
|
||||
// Get uart communications characteristic
|
||||
characteristic(uuid: txCharacteristicUuid, serviceUuid: uartServiceUuid) { [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: rxCharacteristicUuid, serviceUuid: uartServiceUuid) { [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() {
|
||||
// Disable notify
|
||||
guard let characteristic = uartRxCharacteristic, characteristic.isNotifying else { return }
|
||||
|
||||
disableNotify(for: characteristic) { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Clear all Uart specific data
|
||||
self.uartRxCharacteristic = nil
|
||||
self.uartTxCharacteristic = nil
|
||||
self.uartTxCharacteristicWriteType = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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,682 @@
|
|||
//
|
||||
// BlePeripheral.swift
|
||||
// NewtManager
|
||||
//
|
||||
// Created by Antonio García on 12/09/2016.
|
||||
// Copyright © 2016 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
// TODO: Modernize completion blocks to use Swift.Result
|
||||
|
||||
open class BlePeripheral: NSObject {
|
||||
// Config
|
||||
private static var kProfileCharacteristicUpdates = true
|
||||
|
||||
// Constants
|
||||
static var kUndefinedRssiValue = 127
|
||||
|
||||
// Notifications
|
||||
public enum NotificationUserInfoKey: String {
|
||||
case uuid = "uuid"
|
||||
case name = "name"
|
||||
case invalidatedServices = "invalidatedServices"
|
||||
}
|
||||
|
||||
enum PeripheralError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
||||
// Data
|
||||
var peripheral: CBPeripheral
|
||||
|
||||
public static var rssiRunningAverageFactor: Double = 1 /// Global Parameter that affects all rssi measurements. 1 means don't use a running average. The closer to 0 the more resistant the value it is to change
|
||||
private var runningRssi: Int?
|
||||
public 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. If runningRssiFactorFactor == 1, the newer value replaces the old value and not average is calculated
|
||||
get {
|
||||
return runningRssi
|
||||
}
|
||||
set {
|
||||
guard newValue != BlePeripheral.kUndefinedRssiValue else { return } // Don't accept undefined values
|
||||
|
||||
// based on https://en.wikipedia.org/wiki/Exponential_smoothing
|
||||
if newValue == nil || runningRssi == nil || runningRssi == BlePeripheral.kUndefinedRssiValue {
|
||||
runningRssi = newValue
|
||||
} else {
|
||||
runningRssi = Int(BlePeripheral.rssiRunningAverageFactor * Double(newValue!) + (1-BlePeripheral.rssiRunningAverageFactor) * Double(runningRssi!))
|
||||
}
|
||||
}
|
||||
}
|
||||
public var lastSeenTime: CFAbsoluteTime
|
||||
|
||||
open var identifier: UUID {
|
||||
return peripheral.identifier
|
||||
}
|
||||
|
||||
open var name: String? {
|
||||
return peripheral.name
|
||||
}
|
||||
|
||||
public var debugName: String {
|
||||
return peripheral.name ?? peripheral.identifier.uuidString
|
||||
}
|
||||
|
||||
open var state: CBPeripheralState {
|
||||
return peripheral.state
|
||||
}
|
||||
|
||||
func maximumWriteValueLength(for: CBCharacteristicWriteType) -> Int {
|
||||
return peripheral.maximumWriteValueLength(for: .withoutResponse)
|
||||
}
|
||||
|
||||
public struct Advertisement {
|
||||
var advertisementData: [String: Any]
|
||||
|
||||
init(advertisementData: [String: Any]?) {
|
||||
self.advertisementData = advertisementData ?? [String: Any]()
|
||||
}
|
||||
|
||||
// Advertisement data formatted
|
||||
public var localName: String? {
|
||||
return advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
||||
}
|
||||
|
||||
public var manufacturerData: Data? {
|
||||
return advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
||||
}
|
||||
|
||||
public var manufacturerHexDescription: String? {
|
||||
guard let manufacturerData = manufacturerData else { return nil }
|
||||
return HexUtils.hexDescription(data: manufacturerData)
|
||||
// return String(data: manufacturerData, encoding: .utf8)
|
||||
}
|
||||
|
||||
public var manufacturerIdentifier: Data? {
|
||||
guard let manufacturerData = manufacturerData, manufacturerData.count >= 2 else { return nil }
|
||||
let manufacturerIdentifierData = manufacturerData[0..<2]
|
||||
return manufacturerIdentifierData
|
||||
}
|
||||
|
||||
public var services: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
public var servicesOverflow: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
public var servicesSolicited: [CBUUID]? {
|
||||
return advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
|
||||
}
|
||||
|
||||
public var serviceData: [CBUUID: Data]? {
|
||||
return advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
|
||||
}
|
||||
|
||||
public var txPower: Int? {
|
||||
let number = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
|
||||
return number?.intValue
|
||||
}
|
||||
|
||||
public var isConnectable: Bool? {
|
||||
let connectableNumber = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber
|
||||
return connectableNumber?.boolValue
|
||||
}
|
||||
}
|
||||
public var advertisement: Advertisement
|
||||
|
||||
typealias CapturedReadCompletionHandler = ((_ value: Any?, _ error: Error?) -> Void)
|
||||
private class CaptureReadHandler {
|
||||
|
||||
var identifier: String
|
||||
var result: CapturedReadCompletionHandler
|
||||
var timeoutTimer: Foundation.Timer?
|
||||
var timeoutAction: ((String) -> Void)?
|
||||
var isNotifyOmitted: Bool
|
||||
|
||||
init(identifier: String, result: @escaping CapturedReadCompletionHandler, timeout: Double?, timeoutAction: ((String) -> Void)?, isNotifyOmitted: Bool = false) {
|
||||
self.identifier = identifier
|
||||
self.result = result
|
||||
self.isNotifyOmitted = isNotifyOmitted
|
||||
|
||||
if let timeout = timeout {
|
||||
self.timeoutAction = timeoutAction
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
self.timeoutTimer = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(self.timerFired), userInfo: nil, repeats: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
public init(peripheral: CBPeripheral, advertisementData: [String: Any]?, rssi: Int?) {
|
||||
self.peripheral = peripheral
|
||||
self.advertisement = Advertisement(advertisementData: advertisementData)
|
||||
self.lastSeenTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
super.init()
|
||||
self.rssi = rssi
|
||||
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: - Connection
|
||||
func disconnect(centralManager: CBCentralManager) {
|
||||
let command = BleCommand(type: .disconnect, parameters: [centralManager], completion: nil)
|
||||
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
|
||||
internal class BleCommand: Equatable {
|
||||
enum CommandType {
|
||||
case discoverService
|
||||
case discoverCharacteristic
|
||||
case discoverDescriptor
|
||||
case setNotify
|
||||
case readCharacteristic
|
||||
case writeCharacteristic
|
||||
case writeCharacteristicAndWaitNofity
|
||||
case readDescriptor
|
||||
case disconnect
|
||||
}
|
||||
|
||||
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)
|
||||
case .disconnect:
|
||||
disconnect(with: command)
|
||||
}
|
||||
}
|
||||
|
||||
private func handlerIdentifier(from characteristic: CBCharacteristic) -> String {
|
||||
guard let service = characteristic.service else { DLog("Error: handleIdentifier with nil characteritic service"); return "" }
|
||||
return "\(service.uuid.uuidString)-\(characteristic.uuid.uuidString)"
|
||||
}
|
||||
|
||||
private func handlerIdentifier(from descriptor: CBDescriptor) -> String {
|
||||
guard let characteristic = descriptor.characteristic, let service = characteristic.service else { DLog("Error: handleIdentifier with nil descriptor service"); return "" }
|
||||
return "\(service.uuid.uuidString)-\(characteristic.uuid.uuidString)-\(descriptor.uuid.uuidString)"
|
||||
}
|
||||
|
||||
internal 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 {
|
||||
// Everything 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
|
||||
|
||||
|
||||
if writeType == .withoutResponse {
|
||||
let mtu = maximumWriteValueLength(for: .withoutResponse)
|
||||
var offset = 0
|
||||
while offset < data.count {
|
||||
let chunkData = data.subdata(in: offset ..< min(offset + mtu, data.count))
|
||||
//DLog("blewrite offset: \(offset) / \(data.count), size: \(chunkData.count)")
|
||||
peripheral.writeValue(chunkData, for: characteristic, type: .withoutResponse)
|
||||
offset += chunkData.count
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
else {
|
||||
peripheral.writeValue(data, for: characteristic, type: writeType)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
internal func disconnect(with command: BleCommand) {
|
||||
let centralManager = command.parameters!.first as! CBCentralManager
|
||||
centralManager.cancelPeripheralConnection(self.peripheral)
|
||||
finishedExecutingCommand(error: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBPeripheralDelegate
|
||||
extension BlePeripheral: CBPeripheralDelegate {
|
||||
public 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])
|
||||
}
|
||||
|
||||
public 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])
|
||||
}
|
||||
|
||||
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
//DLog("didDiscoverServices for: \(peripheral.name ?? peripheral.identifier.uuidString)")
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
///DLog("didDiscoverCharacteristicsFor: \(service.uuid.uuidString)")
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
|
||||
finishedExecutingCommand(error: error)
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public 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 != BlePeripheral.kUndefinedRssiValue { // 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!
|
||||
public static let peripheralDidUpdateName = Notification.Name(kPrefix+".peripheralDidUpdateName")
|
||||
public static let peripheralDidModifyServices = Notification.Name(kPrefix+".peripheralDidModifyServices")
|
||||
public 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: AnyObject {
|
||||
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,110 @@
|
|||
//
|
||||
// UartPacketManager.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio on 01/02/2017.
|
||||
// Copyright © 2017 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class UartPacketManager: UartPacketManagerBase {
|
||||
|
||||
// Params
|
||||
var isResetPacketsOnReconnectionEnabled = true
|
||||
|
||||
// MARK: - Lifecycle
|
||||
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
|
||||
guard let self = self else { return }
|
||||
if self.isResetPacketsOnReconnectionEnabled {
|
||||
self.clearPacketsCache()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send data
|
||||
private func sendUart(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)
|
||||
}
|
||||
|
||||
/*
|
||||
Send data to MQTT (if enabled) and also to UART (if MQTT configuration allows it)
|
||||
*/
|
||||
func send(blePeripheral: BlePeripheral, data: Data, wasReceivedFromMqtt: Bool = false) {
|
||||
|
||||
#if MQTT_ENABLED
|
||||
if isMqttEnabled {
|
||||
// Mqtt publish to TX
|
||||
let mqttSettings = MqttSettings.shared
|
||||
if mqttSettings.isPublishEnabled, let text = String(data: data, encoding: .utf8) {
|
||||
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
|
||||
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 {
|
||||
sendUart(blePeripheral: blePeripheral, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Force reset
|
||||
func reset(blePeripheral: BlePeripheral) {
|
||||
blePeripheral.reset()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// BleManagerSimulated.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 14/12/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreBluetooth
|
||||
|
||||
class BleManagerSimulated: BleManager {
|
||||
|
||||
// Singleton
|
||||
static let simulated = BleManagerSimulated()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override init() {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Scanning
|
||||
override func startScan(withServices services: [CBUUID]? = nil) {
|
||||
scanningStartTime = CACurrentMediaTime()
|
||||
|
||||
// Add simulated peripherals
|
||||
let simulatedCPB = BlePeripheralSimulated(model: .circuitPlaygroundBluefruit)
|
||||
peripheralsFound[simulatedCPB.identifier] = simulatedCPB
|
||||
NotificationCenter.default.post(name: .didDiscoverPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: simulatedCPB.identifier])
|
||||
|
||||
let simulatedClue = BlePeripheralSimulated(model: .clue_nRF52840)
|
||||
peripheralsFound[simulatedClue.identifier] = simulatedClue
|
||||
NotificationCenter.default.post(name: .didDiscoverPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: simulatedClue.identifier])
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func stopScan() {
|
||||
}
|
||||
|
||||
// MARK: - Connect
|
||||
override func connect(to peripheral: BlePeripheral, timeout: TimeInterval? = nil, shouldNotifyOnConnection: Bool = false, shouldNotifyOnDisconnection: Bool = false, shouldNotifyOnNotification: Bool = false) {
|
||||
|
||||
guard let blePeripheral = peripheral as? BlePeripheralSimulated else { return }
|
||||
blePeripheral.simulateConnect()
|
||||
|
||||
// Send notification
|
||||
NotificationCenter.default.post(name: .didConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
}
|
||||
|
||||
override func reconnecToPeripherals(peripheralsData: [(identifier: UUID, advertisementData: [String: Any])], withServices services: [CBUUID], timeout: Double? = nil) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Disconnect
|
||||
override func disconnect(from peripheral: BlePeripheral, waitForQueuedCommands: Bool = false) {
|
||||
guard let blePeripheral = peripheral as? BlePeripheralSimulated else { return }
|
||||
|
||||
DLog("disconnect")
|
||||
NotificationCenter.default.post(name: .willDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
|
||||
|
||||
if waitForQueuedCommands {
|
||||
// Send the disconnection to the command queue, so all the previous command are executed before disconnecting
|
||||
if let centralManager = centralManager {
|
||||
blePeripheral.disconnect(centralManager: centralManager)
|
||||
}
|
||||
} else {
|
||||
didDisconnectPeripheral(blePeripheral: blePeripheral)
|
||||
}
|
||||
}
|
||||
|
||||
func didDisconnectPeripheral(blePeripheral: BlePeripheralSimulated) {
|
||||
DLog("didDisconnectPeripheral")
|
||||
|
||||
// Clean
|
||||
peripheralsFound[blePeripheral.identifier]?.reset()
|
||||
|
||||
// Notify
|
||||
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: blePeripheral.identifier])
|
||||
|
||||
// Don't remove the peripheral from the peripheral list (so we can select again the simulated peripheral)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitAccelerometer.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 25/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Structs
|
||||
struct AccelerometerValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitAccelerometerEnable(responseHandler: @escaping(Result<(AccelerometerValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitAccelerometerIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitAccelerometerDisable() {
|
||||
}
|
||||
|
||||
func adafruitAccelerometerLastValue() -> AccelerometerValue? {
|
||||
return AccelerometerValue(x: 0, y: 0, z: 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitBarometricPressure.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitBarometricPressureResponseDataTimer: Timer?
|
||||
}
|
||||
|
||||
private var adafruitBarometricPressureResponseDataTimer: Timer? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitBarometricPressureResponseDataTimer) as! Timer?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitBarometricPressureResponseDataTimer, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitBarometricPressureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
adafruitBarometricPressureResponseDataTimer = Timer.scheduledTimer(withTimeInterval: BlePeripheral.kAdafruitSensorDefaultPeriod, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let value = self.adafruitBarometricPressureLastValue() else { return }
|
||||
responseHandler(.success((value, self.identifier)))
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureIsEnabled() -> Bool {
|
||||
return self.adafruitManufacturerData()?.boardModel == .clue_nRF52840
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureDisable() {
|
||||
}
|
||||
|
||||
func adafruitBarometricPressureLastValue() -> Float? {
|
||||
return Float.random(in: 1190.0 ..< 1191.0)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitButtons.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 15/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
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 adafruitButtonsEnable(responseHandler: @escaping(Result<(ButtonsState, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitButtonsIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitButtonsDisable() {
|
||||
}
|
||||
|
||||
func adafruitButtonsReadState(completion: @escaping(Result<(ButtonsState, UUID), Error>) -> Void) {
|
||||
guard let state = adafruitButtonsLastValue() else {
|
||||
completion(.failure(PeripheralAdafruitError.invalidResponseData))
|
||||
return
|
||||
}
|
||||
completion(.success((state, self.identifier)))
|
||||
}
|
||||
|
||||
func adafruitButtonsLastValue() -> ButtonsState? {
|
||||
return ButtonsState(slideSwitch: .left, buttonA: .pressed, buttonB: .released)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitHumidity.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 06/03/2020.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// MARK: - Custom properties
|
||||
private struct CustomPropertiesKeys {
|
||||
static var adafruitHumidityResponseDataTimer: Timer?
|
||||
}
|
||||
|
||||
private var adafruitHumidityResponseDataTimer: Timer? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitHumidityResponseDataTimer) as! Timer?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitHumidityResponseDataTimer, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitHumidityEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
adafruitHumidityResponseDataTimer = Timer.scheduledTimer(withTimeInterval: BlePeripheral.kAdafruitSensorDefaultPeriod, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let value = self.adafruitHumidityLastValue() else { return }
|
||||
responseHandler(.success((value, self.identifier)))
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitHumidityIsEnabled() -> Bool {
|
||||
return self.adafruitManufacturerData()?.boardModel == .clue_nRF52840
|
||||
}
|
||||
|
||||
func adafruitHumidityDisable() {
|
||||
}
|
||||
|
||||
func adafruitHumidityLastValue() -> Float? {
|
||||
return Float.random(in: 28.5 ..< 29.0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitLight.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 adafruitLightResponseDataTimer: Timer?
|
||||
}
|
||||
|
||||
private var adafruitLightResponseDataTimer: Timer? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitLightResponseDataTimer) as! Timer?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitLightResponseDataTimer, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitLightEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
adafruitLightResponseDataTimer = Timer.scheduledTimer(withTimeInterval: BlePeripheral.kAdafruitSensorDefaultPeriod, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let value = self.adafruitLightLastValue() else { return }
|
||||
responseHandler(.success((value, self.identifier)))
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitLightIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitLightDisable() {
|
||||
adafruitLightResponseDataTimer?.invalidate()
|
||||
adafruitLightResponseDataTimer = nil
|
||||
}
|
||||
|
||||
func adafruitLightLastValue() -> Float? {
|
||||
let temperature = Float.random(in: 300 ..< 400)
|
||||
return temperature
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitNeoPixels.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 kAdafruitNeoPixelsServiceNumberOfBitsPerPixel = 3
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitNeoPixelsEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitNeoPixelsIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitNeoPixelsDisable() {
|
||||
}
|
||||
|
||||
var adafruitNeoPixelsCount: Int {
|
||||
return self.adafruitManufacturerData()?.boardModel?.neoPixelsCount ?? 0
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetAllPixelsColor(_ color: UIColor) {
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetPixelColor(index: Int, color: UIColor) {
|
||||
}
|
||||
|
||||
func adafruitNeoPixelSetColor(index: UInt, color: UIColor, pixelMask: [Bool]) {
|
||||
}
|
||||
|
||||
// MARK: - Low level actions
|
||||
func adafruitNeoPixelsWriteData(offset: UInt16, pixelData: Data) {
|
||||
}
|
||||
|
||||
static func pixelDataFromColor(_ color: UIColor) -> Data {
|
||||
let bytes = pixelUInt8FromColor(color)
|
||||
return bytes.data
|
||||
}
|
||||
|
||||
static func pixelUInt8FromColor(_ color: UIColor) -> [UInt8] {
|
||||
return [UInt8]([0, 0, 0])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// BlePeripheral+AdafruitQuaternion.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 25/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// Structs
|
||||
struct QuaternionValue {
|
||||
var x: Float
|
||||
var y: Float
|
||||
var z: Float
|
||||
var w: Float
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitQuaternionEnable(responseHandler: @escaping(Result<(QuaternionValue, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitQuaternionIsEnabled() -> Bool {
|
||||
return self.adafruitManufacturerData()?.boardModel == .clue_nRF52840
|
||||
}
|
||||
|
||||
func adafruitQuaternionDisable() {
|
||||
}
|
||||
|
||||
func adafruitQuaternionLastValue() -> QuaternionValue? {
|
||||
return QuaternionValue(x: 0, y: 0, z: 0, w: 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitTemperature.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 adafruitTemperatureResponseDataTimer: Timer?
|
||||
}
|
||||
|
||||
private var adafruitTemperatureResponseDataTimer: Timer? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &CustomPropertiesKeys.adafruitTemperatureResponseDataTimer) as! Timer?
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &CustomPropertiesKeys.adafruitTemperatureResponseDataTimer, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
func adafruitTemperatureEnable(responseHandler: @escaping(Result<(Float, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
|
||||
|
||||
adafruitTemperatureResponseDataTimer = Timer.scheduledTimer(withTimeInterval: BlePeripheral.kAdafruitSensorDefaultPeriod, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let value = self.adafruitTemperatureLastValue() else { return }
|
||||
responseHandler(.success((value, self.identifier)))
|
||||
}
|
||||
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitTemperatureIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitTemperatureDisable() {
|
||||
adafruitTemperatureResponseDataTimer?.invalidate()
|
||||
adafruitTemperatureResponseDataTimer = nil
|
||||
}
|
||||
|
||||
func adafruitTemperatureLastValue() -> Float? {
|
||||
let temperature = Float.random(in: 18.5 ..< 19.5)
|
||||
return temperature
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// BlePeripheralSimulated+AdafruitToneGenerator.swift
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Antonio García on 18/11/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
extension BlePeripheral {
|
||||
// MARK: - Actions
|
||||
func adafruitToneGeneratorEnable(completion: ((Result<Void, Error>) -> Void)?) {
|
||||
completion?(.success(()))
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorIsEnabled() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorDisable() {
|
||||
}
|
||||
|
||||
func adafruitToneGeneratorStartPlaying(frequency: UInt16, duration: UInt32 = 0) { // Duration 0 means non-stop
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// 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 {
|
||||
// Data
|
||||
private var simulatedIdentifier = UUID()
|
||||
override var identifier: UUID {
|
||||
return simulatedIdentifier
|
||||
}
|
||||
|
||||
override var name: String? {
|
||||
let result: String
|
||||
switch model {
|
||||
case .circuitPlaygroundBluefruit:
|
||||
result = "CPB"
|
||||
case .clue_nRF52840:
|
||||
result = "CLUE"
|
||||
case .feather_nRF52832:
|
||||
result = "Feather"
|
||||
case .feather_nRF52840_express:
|
||||
result = "Feather Express"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private var simulatedState: CBPeripheralState = .disconnected
|
||||
override var state: CBPeripheralState {
|
||||
return simulatedState
|
||||
}
|
||||
|
||||
private var model: AdafruitManufacturerData.BoardModel
|
||||
|
||||
// MARK: - Lifecycle
|
||||
init(model: AdafruitManufacturerData.BoardModel) {
|
||||
self.model = model
|
||||
|
||||
// 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 adafruitManufacturerIdentifier = BlePeripheral.kManufacturerAdafruitIdentifier
|
||||
let boardId = model.identifier.first!
|
||||
let boardField: [UInt8] = [0x04, 0x01, 0x00] + boardId
|
||||
let manufacturerDataBytes: [UInt8] = adafruitManufacturerIdentifier + boardField
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Connect
|
||||
func simulateConnect() {
|
||||
simulatedState = .connected
|
||||
}
|
||||
|
||||
// MARK: - Disconnect
|
||||
override internal func disconnect(with command: BleCommand) {
|
||||
// Simulate disconnection
|
||||
simulatedState = .disconnected
|
||||
BleManagerSimulated.simulated.didDisconnectPeripheral(blePeripheral: self)
|
||||
|
||||
// Finished
|
||||
finishedExecutingCommand(error: 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
|
||||
103
BluefruitPlayground/AdafruitKit/Ble/UartPacketManagerBase.swift
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// UartPacketManagerBase.swift
|
||||
// Bluefruit
|
||||
//
|
||||
// Created by Antonio García on 05/08/2017.
|
||||
// Copyright © 2017 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol UartPacketManagerDelegate: AnyObject {
|
||||
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,24 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
54
BluefruitPlayground/AdafruitKit/Utils/Types+Data.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// CPX+BLE
|
||||
// BluefruitPlayground
|
||||
//
|
||||
// Created by Trevor B on 8/14/19.
|
||||
// Copyright © 2019 Adafruit Industries LLC. All rights reserved.
|
||||
// Created by Antonio García on 09/10/2019.
|
||||
// Copyright © 2019 Adafruit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
|
@ -13,25 +13,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
var vc: UIViewController?
|
||||
|
||||
|
||||
if (UserDefaults.standard.value(forKey: "name") as? String) == nil {
|
||||
vc = storyboard.instantiateViewController(withIdentifier: "rootPage")
|
||||
} else {
|
||||
vc = storyboard.instantiateInitialViewController()!
|
||||
}
|
||||
|
||||
self.window?.rootViewController = vc
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
|
||||
|
||||
|
||||
startup()
|
||||
|
||||
ScreenFlowManager.enableBleStateManagement()
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +41,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
|
||||
ScreenFlowManager.disableBleStateManagement()
|
||||
}
|
||||
|
||||
// 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 |
|
|
@ -1,93 +1,111 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
{
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
"idiom": "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-20@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
{
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BluefruitPlayground-Icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BluefruitPlayground-Icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "BluefruitPlayground-Icon-1024.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
|
|
@ -95,4 +113,4 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
23
BluefruitPlayground/Assets.xcassets/autoscan/scanning_background.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "scanning_background.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "scanning_background@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "scanning_background@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/autoscan/scanning_background.imageset/scanning_background.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
BluefruitPlayground/Assets.xcassets/autoscan/scanning_background.imageset/scanning_background@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
BluefruitPlayground/Assets.xcassets/autoscan/scanning_background.imageset/scanning_background@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 87 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
BluefruitPlayground/Assets.xcassets/bluetooth/bluetooth_status.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bluetooth_status.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bluetooth_status@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "bluetooth_status@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/bluetooth/bluetooth_status.imageset/bluetooth_status.png
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
BluefruitPlayground/Assets.xcassets/bluetooth/bluetooth_status.imageset/bluetooth_status@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
BluefruitPlayground/Assets.xcassets/bluetooth/bluetooth_status.imageset/bluetooth_status@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
6
BluefruitPlayground/Assets.xcassets/boards/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
BluefruitPlayground/Assets.xcassets/boards/board_clue_back.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_back.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_back@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_back@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_back.imageset/board_clue_back.png
vendored
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_back.imageset/board_clue_back@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 687 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_back.imageset/board_clue_back@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
23
BluefruitPlayground/Assets.xcassets/boards/board_clue_front.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_front.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_front@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_clue_front@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_front.imageset/board_clue_front.png
vendored
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_front.imageset/board_clue_front@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 563 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_clue_front.imageset/board_clue_front@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -2,15 +2,17 @@
|
|||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "CPlaygroundLogo2.png",
|
||||
"filename" : "board_cpb.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_cpb@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "board_cpb@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
BIN
BluefruitPlayground/Assets.xcassets/boards/board_cpb.imageset/board_cpb.png
vendored
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_cpb.imageset/board_cpb@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 466 KiB |
BIN
BluefruitPlayground/Assets.xcassets/boards/board_cpb.imageset/board_cpb@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 745 KiB |