Compare commits

...

97 commits

Author SHA1 Message Date
TrevKnows
05e43d9a74
Merge pull request #6 from adafruit/dependabot/bundler/rexml-3.3.3
Bump rexml from 3.2.6 to 3.3.3
2025-03-04 09:57:54 -05:00
dependabot[bot]
5e5995eb5d
Bump rexml from 3.2.6 to 3.3.3
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.6 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.6...v3.3.3)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 01:50:28 +00:00
TrevKnows
78a3e93679
Merge pull request #4 from adafruit/xcode15_update
Xcode15 update
2024-02-29 11:11:28 -05:00
Antonio
47ffad0c0d Github Workflow: updated build action to use Xcode 15.0.1 because Xcode 15.1 is not available yet 2023-12-19 16:44:46 +01:00
Antonio
82a6a37a9b Bump version number 2023-12-19 16:22:46 +01:00
Antonio
4c924c2605 Add GitHub Action to build app 2023-12-19 16:05:48 +01:00
Antonio
d290b65d91 Fix warning about thread priority inversion 2023-12-19 15:54:22 +01:00
Antonio
f17d2017ce Update fastlane 2023-12-19 12:30:11 +01:00
Antonio
ad8e7ffade Update BluefruitPlayground-SimulateBluetooth target to make it work in Xcode15 2023-11-29 12:23:32 +01:00
Antonio
251bd8ec31 Updated pods and manually modified FlexColorPicker and Charts to make it work with Xcode15 2023-11-29 12:14:01 +01:00
Antonio
4d268e4f4f Updated Ble code to the current version of AdafruitKit (used in Bluefruit Connect) 2023-11-29 12:12:55 +01:00
Antonio
e2bd12b539 Update Bluetooth API usage (CBCharacteristic service is now optional) 2023-11-28 18:13:59 +01:00
Antonio
969e1eb7c0 Update to new Swift syntax
Minor cosmetic code changes
2023-11-28 18:13:05 +01:00
Antonio
d22f07df7d Force to use always the "Light" theme 2023-11-28 17:57:20 +01:00
Melissa LeBlanc-Williams
60c0ae8c6b
Merge pull request #1 from adafruit/dependabot/bundler/addressable-2.8.0
Bump addressable from 2.7.0 to 2.8.0
2022-06-07 10:51:41 -06:00
dependabot[bot]
263c1163e9
Bump addressable from 2.7.0 to 2.8.0
Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/sporkmonger/addressable/releases)
- [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.7.0...addressable-2.8.0)

---
updated-dependencies:
- dependency-name: addressable
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 06:23:25 +00:00
Antonio
c089e932da Neopixels: Fixed the number of bytes sent for animations. It was hardcoded and now depends on the board.
Updated for Swift 5.2 and fixed new warnings about tuples
2020-05-07 20:42:52 +02:00
Antonio
5125b08706 Sound: minor change to the amplitude value calculation 2020-05-07 16:26:01 +02:00
Antonio
5f22e72211 Sound: fixed a crash when receiving a sample with the -32768 value 2020-05-07 15:58:59 +02:00
Antonio
542f5ca8c8 Add CLUE screens to the automatic screenshot generation process 2020-03-20 11:09:49 +01:00
Antonio
0d33821c88 Fixed text links
Updated strings
2020-03-18 13:03:05 +01:00
Antonio
76ac60c734 Puppets: tweak rotation angles 2020-03-17 22:22:29 +01:00
Antonio
5f9bfb3ee9 Quaternion: fix z axis rotation 2020-03-17 21:56:13 +01:00
Antonio
ec13361e4f Puppets: make head to tilt with the jaw movement 2020-03-15 15:45:11 +01:00
Antonio
7c7ae8578f Minor Refactoring
Add CLUE simulated sensors
2020-03-15 14:46:44 +01:00
Antonio
3fdf7a15fe Scanner: sort peripherals by discovery time 2020-03-15 14:01:53 +01:00
Antonio
cd834da0a1 Puppet: change gesture to open mouth 2020-03-15 13:22:24 +01:00
Antonio
d487170a96 Puppet: make low pass filter depend on the accelerometer period 2020-03-14 19:38:49 +01:00
Antonio
d706ae85fd Scanner: refresh peripherals when the view appears 2020-03-14 19:38:19 +01:00
Antonio
0d7c091fb6 Orientation module: disable z rotation until gyro calibration is implemented 2020-03-14 18:53:09 +01:00
Antonio
5e9eeabe5e Sound: change unit to dBFS, cosmetic changes
Updated text strings
2020-03-14 18:47:23 +01:00
Antonio
d6269f59be Fix crash when the board is not sending data and the chart dataset is empty
Fix crash when the amplitude is -infinite
Improve charts performance
2020-03-14 18:46:49 +01:00
Antonio
13a4c4ff3d Fix dataSeries using wrong order when reloading charts
Fix showing old data for a moment when reloading the chart
Cosmetic code changes
2020-03-14 00:08:27 +01:00
Antonio
49b2508da4 Quaternion: adjust quaternion to take into account the sensor position
Refactor period interval for sensors
2020-03-13 19:06:46 +01:00
Antonio
44e4654301 Updated image for CLUE board
Accelerometer: flip X and Z values for CLUE
Show Accelerometer module only if Quaternion service is not available
Updated text strings
2020-03-13 17:10:24 +01:00
Antonio
b5394dab0a Remove debug parameters from charts DataSeries 2020-03-12 15:48:15 +01:00
Antonio
36872363d8 ToneGeneration: support for iPhone 4s 2020-03-12 15:28:46 +01:00
Antonio
cdf816f848 Autoconnect: cosmetic changes
Better compatibility for iPhone 4s
2020-03-12 15:26:57 +01:00
Antonio
380891dceb Autoconnect: update for CLUE 2020-03-12 14:50:57 +01:00
Antonio
2090698fc4 Refactor board assets 2020-03-12 14:37:22 +01:00
Antonio
91d5322795 Scanner: sort peripherals by RSSI value 2020-03-12 13:18:03 +01:00
Antonio
019befae61 added colors for new modules 2020-03-12 13:13:24 +01:00
Antonio
c02a36c976 Tips, About: Automatize ActiveLabel links generation 2020-03-12 13:13:02 +01:00
Antonio
3feac2a2e5 Added Sound module
Added Quaternion module
Pressure: minor UI changes
Accelerometer: refactor shared code with quaternion module
About: added more links (wip)
2020-03-12 10:51:35 +01:00
Antonio
a61def2b4c Adding sound module (work-in-progress) 2020-03-10 20:30:58 +01:00
Antonio
1c0cb8235e Fix version characteristic reading
Refactor adafruit service notify enable
2020-03-10 19:31:26 +01:00
Antonio
c1a88e264e Added Sound service 2020-03-09 15:14:36 +01:00
Antonio
a1cc6d6306 Tone Generator module updated for CLUE 2020-03-09 10:55:06 +01:00
Antonio
5244715073 Light module updated for CLUE
Button Status: updated help for CLUE
2020-03-09 10:50:42 +01:00
Antonio
19768e4c52 Neopixel module updated for CLUE 2020-03-09 10:39:31 +01:00
Antonio
d1e72eb47e Smoother animations for Light and Humidity changes 2020-03-08 20:43:17 +01:00
Antonio
90da0e9896 Barometric pressure module
Clue front board
2020-03-08 13:02:49 +01:00
Antonio
519022fcf5 Save advertisement data for reconnecting 2020-03-08 00:17:48 +01:00
Antonio
0b7259834e Refactor temperature panels 2020-03-07 22:48:30 +01:00
Antonio
3ba4fa2d9d Humidity module 2020-03-07 21:47:10 +01:00
Antonio
213910c54a Split main storyboard 2020-03-07 02:06:26 +01:00
Antonio
b020a9ee43 New assets for CLUE 2020-03-07 01:45:05 +01:00
Antonio
4eec8bb5b7 New Services: barometric pressure, color sensor, gyroscope, humidity, magnetometer 2020-03-06 22:03:31 +01:00
Antonio
a44b33e66f Refactored boards and added a boardManager to manage multiple connected boards in the future 2020-03-06 20:57:45 +01:00
Antonio
031bab1241 Modules: modules can change now depending on the board connected
Test: added new simulated sensors
2020-03-06 01:25:07 +01:00
Antonio
f48758ed77 Scanner: added ability to decode manufacturer data (board type, color...) 2020-03-05 20:39:46 +01:00
Antonio
b96425596a Refactor CPBle into AdafruitBoard (to support more boards) 2020-03-05 18:11:10 +01:00
Antonio
148a33a5d8 Update Autoconnect parameters 2020-03-05 02:21:01 +01:00
Antonio
e326b0b9d5 Fix comments for auto-connect helper functions 2020-02-19 11:27:06 +01:00
Antonio
a220f3dec4 Apply SwiftLint code format suggestions 2020-02-19 10:30:38 +01:00
Antonio
e4f4c8bcd1 Autoconnect: improved logic for selecting a peripheral 2020-02-19 02:10:31 +01:00
Antonio
a105506ba6 Updated automatic screenshot generation 2020-02-12 12:49:05 +01:00
Antonio
170809c2f8 Puppet: updated help
Manual Scanner: fix table layout warnings while in background
Simulated target: handle disconnections
2020-02-08 18:45:41 +01:00
Antonio
f75db839ff Puppet: update help
Update help view to allow images...
2020-02-08 11:41:12 +01:00
Antonio
0bf2400b88 Remove old unused code 2020-02-07 02:30:21 +01:00
Antonio
1c98c92bac Puppets: set is as a main module (currently it was an accelerometer submodule) 2020-02-07 02:17:16 +01:00
Antonio
379d61a3b1 Autoconnect: added running average to rssi calculation 2020-02-06 01:03:08 +01:00
Antonio
f5af18568e ToneGenerator: fix vertical views constraints 2020-02-05 03:07:52 +01:00
Antonio
f56cfee413 Puppet: add camera permissions to plist, fix camerabuttons position on iPad 2020-02-05 02:36:45 +01:00
Antonio
3933990111 Changed animation between modules and manual scan on disconnect 2020-02-05 01:48:39 +01:00
Antonio
eb4182c30e ToneGenerator: expand keyboard to take all the available width
Updated strings
2020-02-05 01:43:01 +01:00
Antonio
1dab5a618e Changed the color that represents each module 2020-02-05 01:32:06 +01:00
Antonio
eacad0dc42 Added Puppet mode
Refactor code
2020-02-04 22:41:48 +01:00
Antonio
462ea1c289 Remove unused code to check ble status 2020-02-03 19:11:57 +01:00
Antonio
9c8d6f2e00 Added config setting to disable auto-connect UI 2020-02-03 15:28:24 +01:00
Antonio
95ab2e834e Update tips texts
Better handling for disconnections on startup
Remove unused code
2020-01-31 13:51:41 +01:00
Antonio
ed84a02c7c ToneGenerator: reenable pop recognizer on exit 2020-01-31 13:31:50 +01:00
Antonio
597ba1920e Update Simulated peripheral for the latest BleManager changes
Autoconnect: fix the rssi sorting
2020-01-31 13:14:12 +01:00
Antonio
e4ca5d4b2e ToneGenerator: move keyboard bounds away from edges 2020-01-31 13:12:06 +01:00
Antonio
52ab739680 Neopixels: changed reset icon 2020-01-31 11:25:55 +01:00
Antonio
d31f154280 Light: add chart
Temperature: decrease time period
2020-01-31 03:37:17 +01:00
Antonio
abaf6c923b Auto-connect to closest CPB
Bluetooth: better bluetooth state management, better info when is unauthorized or powered off
Add underline to external links
Add "reconnecting" text to the splash screen
Added "flash color" neopixel animation when changing modules
Neopixels: show colors on circuit preview without brightness
2020-01-31 03:12:16 +01:00
Antonio
b9f5da2aee ButtonStatus: refactor initialisation when period == 0 2020-01-28 16:58:29 +01:00
Antonio
2d5d116447 Neopixels: update texts
Remove unused classes
Improve automatic screenshots generation
2020-01-28 16:40:36 +01:00
Antonio
64e94256ee ButtonStatus: updated images for A, B buttons 2019-12-19 12:59:09 +01:00
Antonio
c4325844d9 Accelerometer: fixed acceleration units 2019-12-19 12:48:44 +01:00
Antonio
bbd179a207 Neopixels: change color on slider release 2019-12-19 12:47:33 +01:00
Antonio
c48f116cc3 Accelerometer: Update help 2019-12-19 11:46:36 +01:00
Antonio
529952e495 ToneGenerator: fix bottom row white keys frequencies 2019-12-18 11:00:44 +01:00
Antonio
14f2d53a22 Fix BluefruitPlayground_SimulatedBluetooth target not compiling 2019-12-18 11:00:26 +01:00
Antonio
b9760b163f Initial import: app build 7 2019-12-17 11:45:21 +01:00
Antonio
28157189a8 Move prototype to specific branch and clear master 2019-12-17 11:36:56 +01:00
792 changed files with 60076 additions and 40218 deletions

26
.github/workflows/build.yml vendored Normal file
View 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
View 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

View file

@ -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>

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "self:CPX+BLE.xcodeproj">
location = "self:CPX.xcodeproj">
</FileRef>
</Workspace>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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])
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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])
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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])
}
}

View file

@ -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)
}
}

View file

@ -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])
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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)")
}
}

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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]
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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])
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View 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 */

View 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

View 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
}
}

View 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()
}
}

View file

@ -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) }))
}
}

View 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) }
}
}

View 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)}
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -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"
}
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -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"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Some files were not shown because too many files have changed in this diff Show more