@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,459 +1,379 @@
|
|
|
1
|
-
#
|
|
1
|
+
# BLE Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Longer recipes for the `core-bluetooth` skill.
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
|
-
- [SwiftUI
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [Write
|
|
11
|
-
- [
|
|
12
|
-
- [L2CAP
|
|
13
|
-
- [
|
|
7
|
+
- [A SwiftUI heart-rate monitor](#a-swiftui-heart-rate-monitor)
|
|
8
|
+
- [Reconnecting](#reconnecting)
|
|
9
|
+
- [Parsing bytes safely](#parsing-bytes-safely)
|
|
10
|
+
- [Write flow control](#write-flow-control)
|
|
11
|
+
- [Several peripherals at once](#several-peripherals-at-once)
|
|
12
|
+
- [L2CAP channels](#l2cap-channels)
|
|
13
|
+
- [Answering requests as a peripheral](#answering-requests-as-a-peripheral)
|
|
14
14
|
|
|
15
|
-
## SwiftUI
|
|
15
|
+
## A SwiftUI heart-rate monitor
|
|
16
16
|
|
|
17
|
-
###
|
|
17
|
+
### Model
|
|
18
18
|
|
|
19
19
|
```swift
|
|
20
20
|
import CoreBluetooth
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
@Observable
|
|
24
|
-
@MainActor
|
|
25
|
-
final class BLEViewModel: NSObject {
|
|
26
|
-
private var centralManager: CBCentralManager!
|
|
27
|
-
private var connectedPeripheral: CBPeripheral?
|
|
28
|
-
|
|
29
|
-
var isBluetoothOn = false
|
|
30
|
-
var isScanning = false
|
|
31
|
-
var isConnected = false
|
|
32
|
-
var discoveredDevices: [DiscoveredDevice] = []
|
|
33
|
-
var heartRate: Int = 0
|
|
34
|
-
|
|
35
|
-
struct DiscoveredDevice: Identifiable {
|
|
36
|
-
let id: UUID
|
|
37
|
-
let name: String
|
|
38
|
-
let rssi: Int
|
|
39
|
-
let peripheral: CBPeripheral
|
|
40
|
-
}
|
|
21
|
+
import Observation
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
struct NearbySensor: Identifiable {
|
|
24
|
+
let id: UUID
|
|
25
|
+
let name: String
|
|
26
|
+
var signal: Int
|
|
27
|
+
let handle: CBPeripheral
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Observable @MainActor
|
|
31
|
+
final class PulseMonitor: NSObject {
|
|
32
|
+
private(set) var radioReady = false
|
|
33
|
+
private(set) var isScanning = false
|
|
34
|
+
private(set) var linked: NearbySensor?
|
|
35
|
+
private(set) var sensors: [NearbySensor] = []
|
|
36
|
+
private(set) var bpm: Int?
|
|
37
|
+
|
|
38
|
+
private static let heartRateService = CBUUID(string: "180D")
|
|
39
|
+
private static let measurement = CBUUID(string: "2A37")
|
|
40
|
+
@ObservationIgnored private var central: CBCentralManager?
|
|
46
41
|
|
|
47
|
-
func
|
|
48
|
-
|
|
49
|
-
discoveredDevices.removeAll()
|
|
50
|
-
isScanning = true
|
|
51
|
-
centralManager.scanForPeripherals(
|
|
52
|
-
withServices: [CBUUID(string: "180D")],
|
|
53
|
-
options: nil
|
|
54
|
-
)
|
|
42
|
+
func prepare() {
|
|
43
|
+
if central == nil { central = CBCentralManager(delegate: self, queue: nil) }
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
func
|
|
58
|
-
|
|
59
|
-
isScanning
|
|
46
|
+
func toggleScan() {
|
|
47
|
+
guard let central, radioReady else { return }
|
|
48
|
+
if isScanning {
|
|
49
|
+
central.stopScan()
|
|
50
|
+
} else {
|
|
51
|
+
sensors.removeAll()
|
|
52
|
+
central.scanForPeripherals(withServices: [Self.heartRateService])
|
|
53
|
+
}
|
|
54
|
+
isScanning.toggle()
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
func
|
|
63
|
-
stopScan()
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
func link(_ sensor: NearbySensor) {
|
|
58
|
+
central?.stopScan()
|
|
59
|
+
isScanning = false
|
|
60
|
+
linked = sensor
|
|
61
|
+
central?.connect(sensor.handle)
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
func
|
|
69
|
-
guard let
|
|
70
|
-
|
|
64
|
+
func unlink() {
|
|
65
|
+
guard let handle = linked?.handle else { return }
|
|
66
|
+
central?.cancelPeripheralConnection(handle)
|
|
71
67
|
}
|
|
72
68
|
}
|
|
69
|
+
```
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
`prepare()` creates the manager, which is also what triggers the permission
|
|
72
|
+
prompt, so call it when the user opens the feature.
|
|
73
|
+
|
|
74
|
+
### Delegate callbacks
|
|
75
|
+
|
|
76
|
+
`prepare()` passes `queue: nil`, and Core Bluetooth then delivers central and
|
|
77
|
+
peripheral events on the main queue. The conformance is `@preconcurrency`, so
|
|
78
|
+
the callbacks run as main-actor methods and update state directly. The
|
|
79
|
+
`nonisolated` plus `Task { @MainActor in }` shape does not compile under
|
|
80
|
+
Swift 6 here: the discovered `NearbySensor` holds a `CBPeripheral`, which is not
|
|
81
|
+
`Sendable`, and the region-isolation pass rejects sending it into the task.
|
|
82
|
+
With a background queue instead, keep the callbacks `nonisolated` and send only
|
|
83
|
+
`Sendable` values (identifiers, numbers) across.
|
|
84
|
+
|
|
85
|
+
```swift
|
|
86
|
+
extension PulseMonitor: @preconcurrency CBCentralManagerDelegate, @preconcurrency CBPeripheralDelegate {
|
|
87
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
88
|
+
radioReady = central.state == .poweredOn
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let name = peripheral.name ?? "Unknown"
|
|
89
|
-
let device = DiscoveredDevice(
|
|
90
|
-
id: peripheral.identifier,
|
|
91
|
-
name: name,
|
|
92
|
-
rssi: RSSI.intValue,
|
|
93
|
-
peripheral: peripheral
|
|
94
|
-
)
|
|
95
|
-
if !discoveredDevices.contains(where: { $0.id == device.id }) {
|
|
96
|
-
discoveredDevices.append(device)
|
|
97
|
-
}
|
|
91
|
+
func centralManager(_ central: CBCentralManager, didDiscover found: CBPeripheral,
|
|
92
|
+
advertisementData: [String: Any], rssi: NSNumber) {
|
|
93
|
+
if let index = sensors.firstIndex(where: { $0.id == found.identifier }) {
|
|
94
|
+
sensors[index].signal = rssi.intValue
|
|
95
|
+
} else {
|
|
96
|
+
sensors.append(NearbySensor(id: found.identifier, name: found.name ?? "Unknown",
|
|
97
|
+
signal: rssi.intValue, handle: found))
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
) {
|
|
105
|
-
Task { @MainActor in
|
|
106
|
-
isConnected = true
|
|
107
|
-
peripheral.delegate = self
|
|
108
|
-
peripheral.discoverServices([CBUUID(string: "180D")])
|
|
109
|
-
}
|
|
101
|
+
func centralManager(_ central: CBCentralManager, didConnect device: CBPeripheral) {
|
|
102
|
+
device.delegate = self
|
|
103
|
+
device.discoverServices([Self.heartRateService])
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
isReconnecting: Bool,
|
|
117
|
-
error: Error?
|
|
118
|
-
) {
|
|
119
|
-
Task { @MainActor in
|
|
120
|
-
isConnected = false
|
|
121
|
-
connectedPeripheral = nil
|
|
122
|
-
}
|
|
106
|
+
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral device: CBPeripheral,
|
|
107
|
+
timestamp: CFAbsoluteTime, isReconnecting: Bool, error: (any Error)?) {
|
|
108
|
+
linked = nil
|
|
109
|
+
bpm = nil
|
|
123
110
|
}
|
|
124
|
-
}
|
|
125
111
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
didDiscoverServices error: Error?
|
|
130
|
-
) {
|
|
131
|
-
guard let services = peripheral.services else { return }
|
|
132
|
-
for service in services {
|
|
133
|
-
peripheral.discoverCharacteristics(
|
|
134
|
-
[CBUUID(string: "2A37")],
|
|
135
|
-
for: service
|
|
136
|
-
)
|
|
112
|
+
func peripheral(_ device: CBPeripheral, didDiscoverServices error: (any Error)?) {
|
|
113
|
+
for service in device.services ?? [] {
|
|
114
|
+
device.discoverCharacteristics([Self.measurement], for: service)
|
|
137
115
|
}
|
|
138
116
|
}
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
) {
|
|
145
|
-
guard let characteristics = service.characteristics else { return }
|
|
146
|
-
for char in characteristics where char.properties.contains(.notify) {
|
|
147
|
-
peripheral.setNotifyValue(true, for: char)
|
|
118
|
+
func peripheral(_ device: CBPeripheral, didDiscoverCharacteristicsFor service: CBService,
|
|
119
|
+
error: (any Error)?) {
|
|
120
|
+
for trait in service.characteristics ?? [] where trait.properties.contains(.notify) {
|
|
121
|
+
device.setNotifyValue(true, for: trait)
|
|
148
122
|
}
|
|
149
123
|
}
|
|
150
124
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
let flags = data[0]
|
|
158
|
-
let bpm: Int
|
|
159
|
-
if (flags & 0x01) != 0 {
|
|
160
|
-
guard data.count >= 3 else { return }
|
|
161
|
-
bpm = Int(data[1]) | (Int(data[2]) << 8)
|
|
125
|
+
func peripheral(_ device: CBPeripheral, didUpdateValueFor trait: CBCharacteristic,
|
|
126
|
+
error: (any Error)?) {
|
|
127
|
+
guard let bytes = trait.value, bytes.count >= 2 else { return }
|
|
128
|
+
if bytes[bytes.startIndex] & 0x01 != 0 {
|
|
129
|
+
guard let wide = bytes.uint16LE(at: 1) else { return }
|
|
130
|
+
bpm = Int(wide)
|
|
162
131
|
} else {
|
|
163
|
-
bpm = Int(
|
|
164
|
-
}
|
|
165
|
-
Task { @MainActor in
|
|
166
|
-
heartRate = bpm
|
|
132
|
+
bpm = Int(bytes[bytes.startIndex + 1])
|
|
167
133
|
}
|
|
168
134
|
}
|
|
169
135
|
}
|
|
170
136
|
```
|
|
171
137
|
|
|
172
|
-
|
|
138
|
+
Discovery asks only for the `2A37` characteristic, and the byte-length checks
|
|
139
|
+
run before `bpm` changes. Discovered sensors are merged by identifier so the
|
|
140
|
+
list does not fill with duplicates.
|
|
141
|
+
|
|
142
|
+
### View
|
|
173
143
|
|
|
174
144
|
```swift
|
|
175
|
-
struct
|
|
176
|
-
@State private var
|
|
145
|
+
struct PulseScreen: View {
|
|
146
|
+
@State private var monitor = PulseMonitor()
|
|
177
147
|
|
|
178
148
|
var body: some View {
|
|
179
149
|
NavigationStack {
|
|
180
150
|
Group {
|
|
181
|
-
if
|
|
182
|
-
VStack {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.foregroundStyle(.red)
|
|
186
|
-
Text("\(viewModel.heartRate) BPM")
|
|
151
|
+
if let sensor = monitor.linked {
|
|
152
|
+
VStack(spacing: 12) {
|
|
153
|
+
Text(sensor.name).font(.headline)
|
|
154
|
+
Text(monitor.bpm.map { "\($0) BPM" } ?? "Waiting for data")
|
|
187
155
|
.font(.largeTitle.monospacedDigit())
|
|
188
|
-
Button("Disconnect") {
|
|
156
|
+
Button("Disconnect", role: .destructive) { monitor.unlink() }
|
|
189
157
|
}
|
|
190
158
|
} else {
|
|
191
|
-
List(
|
|
192
|
-
Button {
|
|
193
|
-
|
|
194
|
-
} label: {
|
|
195
|
-
HStack {
|
|
196
|
-
Text(device.name)
|
|
197
|
-
Spacer()
|
|
198
|
-
Text("\(device.rssi) dBm")
|
|
199
|
-
.foregroundStyle(.secondary)
|
|
200
|
-
}
|
|
159
|
+
List(monitor.sensors) { sensor in
|
|
160
|
+
Button { monitor.link(sensor) } label: {
|
|
161
|
+
LabeledContent(sensor.name, value: "\(sensor.signal) dBm")
|
|
201
162
|
}
|
|
202
163
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
.toolbar {
|
|
207
|
-
if !viewModel.isConnected {
|
|
208
|
-
Button(viewModel.isScanning ? "Stop" : "Scan") {
|
|
209
|
-
viewModel.isScanning ? viewModel.stopScan() : viewModel.startScan()
|
|
164
|
+
.toolbar {
|
|
165
|
+
Button(monitor.isScanning ? "Stop" : "Scan") { monitor.toggleScan() }
|
|
166
|
+
.disabled(!monitor.radioReady)
|
|
210
167
|
}
|
|
211
168
|
}
|
|
212
169
|
}
|
|
170
|
+
.navigationTitle("Heart Rate")
|
|
171
|
+
.onAppear { monitor.prepare() }
|
|
213
172
|
}
|
|
214
173
|
}
|
|
215
174
|
}
|
|
216
175
|
```
|
|
217
176
|
|
|
218
|
-
##
|
|
177
|
+
## Reconnecting
|
|
219
178
|
|
|
220
|
-
|
|
179
|
+
**Automatic.** In the disconnect callback, if `isReconnecting` is false and
|
|
180
|
+
the user did not ask to disconnect, call `connect(_:)` again. A pending
|
|
181
|
+
connect never times out, so it completes whenever the device comes back in
|
|
182
|
+
range.
|
|
221
183
|
|
|
222
|
-
|
|
223
|
-
func centralManager(
|
|
224
|
-
_ central: CBCentralManager,
|
|
225
|
-
didDisconnectPeripheral peripheral: CBPeripheral,
|
|
226
|
-
timestamp: CFAbsoluteTime,
|
|
227
|
-
isReconnecting: Bool,
|
|
228
|
-
error: Error?
|
|
229
|
-
) {
|
|
230
|
-
if !isReconnecting {
|
|
231
|
-
// Attempt to reconnect
|
|
232
|
-
central.connect(peripheral, options: nil)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Reconnecting to Known Peripherals
|
|
238
|
-
|
|
239
|
-
Store the peripheral's UUID and reconnect at next launch using
|
|
240
|
-
`retrievePeripherals(withIdentifiers:)`.
|
|
184
|
+
**Remembered devices.** Save `peripheral.identifier`. On the next launch:
|
|
241
185
|
|
|
242
186
|
```swift
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
if let peripheral = peripherals.first {
|
|
248
|
-
connectedPeripheral = peripheral
|
|
249
|
-
centralManager.connect(peripheral, options: nil)
|
|
250
|
-
}
|
|
187
|
+
let saved: [UUID] = SensorStore.knownIdentifiers()
|
|
188
|
+
for device in central.retrievePeripherals(withIdentifiers: saved) {
|
|
189
|
+
retained[device.identifier] = device
|
|
190
|
+
central.connect(device)
|
|
251
191
|
}
|
|
252
192
|
```
|
|
253
193
|
|
|
254
|
-
|
|
194
|
+
**Already connected elsewhere.** A device another app (or the system) is
|
|
195
|
+
already connected to shows up through
|
|
196
|
+
`central.retrieveConnectedPeripherals(withServices: [heartRateService])`.
|
|
255
197
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
```swift
|
|
259
|
-
func findConnectedHeartRateMonitors() -> [CBPeripheral] {
|
|
260
|
-
centralManager.retrieveConnectedPeripherals(
|
|
261
|
-
withServices: [CBUUID(string: "180D")]
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Data Parsing Helpers
|
|
267
|
-
|
|
268
|
-
### Generic Data Reader
|
|
198
|
+
## Parsing bytes safely
|
|
269
199
|
|
|
270
200
|
```swift
|
|
271
201
|
extension Data {
|
|
272
|
-
func
|
|
273
|
-
guard offset < count else { return nil }
|
|
274
|
-
return self[offset]
|
|
202
|
+
func uint8(at offset: Int) -> UInt8? {
|
|
203
|
+
guard offset >= 0, offset < count else { return nil }
|
|
204
|
+
return self[startIndex + offset]
|
|
275
205
|
}
|
|
276
206
|
|
|
277
|
-
func
|
|
278
|
-
guard offset + 1
|
|
279
|
-
return UInt16(
|
|
207
|
+
func uint16LE(at offset: Int) -> UInt16? {
|
|
208
|
+
guard let low = uint8(at: offset), let high = uint8(at: offset + 1) else { return nil }
|
|
209
|
+
return UInt16(low) | UInt16(high) << 8
|
|
280
210
|
}
|
|
281
211
|
|
|
282
|
-
func
|
|
283
|
-
guard offset
|
|
284
|
-
return UInt32(
|
|
285
|
-
| (UInt32(self[offset + 1]) << 8)
|
|
286
|
-
| (UInt32(self[offset + 2]) << 16)
|
|
287
|
-
| (UInt32(self[offset + 3]) << 24)
|
|
212
|
+
func uint32LE(at offset: Int) -> UInt32? {
|
|
213
|
+
guard let low = uint16LE(at: offset), let high = uint16LE(at: offset + 2) else { return nil }
|
|
214
|
+
return UInt32(low) | UInt32(high) << 16
|
|
288
215
|
}
|
|
289
216
|
}
|
|
290
|
-
```
|
|
291
217
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
```swift
|
|
295
|
-
func parseBatteryLevel(_ data: Data) -> Int? {
|
|
296
|
-
data.readUInt8(at: 0).map { Int($0) }
|
|
218
|
+
func batteryLevel(from bytes: Data) -> Int? {
|
|
219
|
+
bytes.uint8(at: 0).map(Int.init)
|
|
297
220
|
}
|
|
298
221
|
```
|
|
299
222
|
|
|
300
|
-
|
|
223
|
+
Every accessor returns `nil` instead of trapping when the value is shorter
|
|
224
|
+
than expected. Battery Level is the first byte.
|
|
301
225
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
226
|
+
## Write flow control
|
|
227
|
+
|
|
228
|
+
- `.withResponse` when the command must be confirmed (settings, firmware
|
|
229
|
+
control points).
|
|
230
|
+
- `.withoutResponse` for streams and fire-and-forget data. The peripheral can
|
|
231
|
+
hold only so many unacknowledged writes, so pause when it is full.
|
|
305
232
|
|
|
306
233
|
```swift
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
234
|
+
final class Uploader: NSObject, CBPeripheralDelegate {
|
|
235
|
+
private var pending: [Data] = []
|
|
236
|
+
private let device: CBPeripheral
|
|
237
|
+
private let target: CBCharacteristic
|
|
238
|
+
|
|
239
|
+
init(device: CBPeripheral, target: CBCharacteristic) {
|
|
240
|
+
self.device = device
|
|
241
|
+
self.target = target
|
|
314
242
|
}
|
|
315
|
-
}
|
|
316
243
|
|
|
317
|
-
func
|
|
318
|
-
|
|
244
|
+
func enqueue(_ blob: Data) {
|
|
245
|
+
let size = device.maximumWriteValueLength(for: .withoutResponse)
|
|
246
|
+
pending += stride(from: 0, to: blob.count, by: size).map {
|
|
247
|
+
blob.subdata(in: $0..<Swift.min($0 + size, blob.count))
|
|
248
|
+
}
|
|
249
|
+
drain()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func drain() {
|
|
253
|
+
while let next = pending.first, device.canSendWriteWithoutResponse {
|
|
254
|
+
guard next.count <= device.maximumWriteValueLength(for: .withoutResponse) else { return }
|
|
255
|
+
device.writeValue(next, for: target, type: .withoutResponse)
|
|
256
|
+
pending.removeFirst()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func peripheralIsReady(toSendWriteWithoutResponse device: CBPeripheral) {
|
|
261
|
+
drain()
|
|
262
|
+
}
|
|
319
263
|
}
|
|
320
264
|
```
|
|
321
265
|
|
|
322
|
-
|
|
266
|
+
Sending stops when the peripheral cannot take more, or a chunk is too large,
|
|
267
|
+
and resumes from `peripheralIsReady(toSendWriteWithoutResponse:)`.
|
|
323
268
|
|
|
324
|
-
|
|
269
|
+
## Several peripherals at once
|
|
325
270
|
|
|
326
271
|
```swift
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
private var
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
advertisementData: [String: Any],
|
|
336
|
-
rssi RSSI: NSNumber
|
|
337
|
-
) {
|
|
338
|
-
let id = peripheral.identifier
|
|
339
|
-
guard connectedPeripherals[id] == nil else { return }
|
|
340
|
-
connectedPeripherals[id] = peripheral
|
|
341
|
-
central.connect(peripheral)
|
|
272
|
+
final class SensorHub: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
273
|
+
private var central: CBCentralManager!
|
|
274
|
+
private var connected: [UUID: CBPeripheral] = [:]
|
|
275
|
+
|
|
276
|
+
func attach(_ device: CBPeripheral) {
|
|
277
|
+
guard connected[device.identifier] == nil else { return }
|
|
278
|
+
connected[device.identifier] = device
|
|
279
|
+
central.connect(device)
|
|
342
280
|
}
|
|
343
281
|
|
|
344
|
-
func centralManager(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
) {
|
|
348
|
-
peripheral.delegate = self
|
|
349
|
-
peripheral.discoverServices(nil)
|
|
282
|
+
func centralManager(_ central: CBCentralManager, didConnect device: CBPeripheral) {
|
|
283
|
+
device.delegate = self
|
|
284
|
+
device.discoverServices(nil)
|
|
350
285
|
}
|
|
351
286
|
|
|
352
|
-
func
|
|
353
|
-
for
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
connectedPeripherals.removeAll()
|
|
287
|
+
func detachAll() {
|
|
288
|
+
for device in connected.values { central.cancelPeripheralConnection(device) }
|
|
289
|
+
connected.removeAll()
|
|
357
290
|
}
|
|
291
|
+
|
|
292
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {}
|
|
358
293
|
}
|
|
359
294
|
```
|
|
360
295
|
|
|
361
|
-
|
|
296
|
+
Key the dictionary by `identifier`, skip devices already tracked, and start
|
|
297
|
+
discovery for each one as it connects. Passing `nil` to `discoverServices`
|
|
298
|
+
here keeps the sample short; list the services you need in real code.
|
|
299
|
+
|
|
300
|
+
## L2CAP channels
|
|
362
301
|
|
|
363
|
-
|
|
302
|
+
An L2CAP channel is a stream between the two devices, with more throughput
|
|
303
|
+
than characteristic writes for bulk data.
|
|
364
304
|
|
|
365
|
-
|
|
305
|
+
**Central side:**
|
|
366
306
|
|
|
367
307
|
```swift
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
error
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
inputStream.schedule(in: .main, forMode: .default)
|
|
380
|
-
outputStream.schedule(in: .main, forMode: .default)
|
|
381
|
-
inputStream.open()
|
|
382
|
-
outputStream.open()
|
|
308
|
+
device.openL2CAPChannel(CBL2CAPPSM(0x0081))
|
|
309
|
+
|
|
310
|
+
func peripheral(_ device: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: (any Error)?) {
|
|
311
|
+
guard let channel, error == nil,
|
|
312
|
+
let input = channel.inputStream, let output = channel.outputStream else { return }
|
|
313
|
+
for stream in [input, output] as [Stream] {
|
|
314
|
+
stream.delegate = self
|
|
315
|
+
stream.schedule(in: .main, forMode: .default)
|
|
316
|
+
stream.open()
|
|
317
|
+
}
|
|
318
|
+
activeChannel = channel
|
|
383
319
|
}
|
|
384
|
-
|
|
385
|
-
// Open a channel to a known PSM
|
|
386
|
-
peripheral.openL2CAPChannel(CBL2CAPPSM(0x0025))
|
|
387
320
|
```
|
|
388
321
|
|
|
389
|
-
|
|
322
|
+
Keep the channel object alive while you use its streams.
|
|
323
|
+
|
|
324
|
+
**Peripheral side:**
|
|
390
325
|
|
|
391
326
|
```swift
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
) {
|
|
400
|
-
// Share the PSM with centrals via a characteristic value
|
|
401
|
-
print("Published L2CAP channel on PSM: \(PSM)")
|
|
327
|
+
manager.publishL2CAPChannel(withEncryption: true)
|
|
328
|
+
|
|
329
|
+
func peripheralManager(_ manager: CBPeripheralManager, didPublishL2CAPChannel psm: CBL2CAPPSM,
|
|
330
|
+
error: (any Error)?) {
|
|
331
|
+
guard error == nil else { return }
|
|
332
|
+
var value = psm.littleEndian
|
|
333
|
+
psmCharacteristic.value = Data(bytes: &value, count: MemoryLayout<CBL2CAPPSM>.size)
|
|
402
334
|
}
|
|
403
335
|
```
|
|
404
336
|
|
|
405
|
-
|
|
337
|
+
The system assigns the PSM. Publish it through a characteristic so centrals
|
|
338
|
+
know which channel to open.
|
|
406
339
|
|
|
407
|
-
|
|
340
|
+
## Answering requests as a peripheral
|
|
408
341
|
|
|
409
342
|
```swift
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
) {
|
|
415
|
-
if request.characteristic.uuid == charUUID {
|
|
416
|
-
let value = currentSensorData()
|
|
417
|
-
request.value = value.subdata(
|
|
418
|
-
in: request.offset..<value.count
|
|
419
|
-
)
|
|
420
|
-
peripheral.respond(to: request, withResult: .success)
|
|
421
|
-
} else {
|
|
422
|
-
peripheral.respond(to: request, withResult: .attributeNotFound)
|
|
423
|
-
}
|
|
343
|
+
func peripheralManager(_ manager: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
|
344
|
+
guard request.characteristic.uuid == ThermometerBeacon.reading else {
|
|
345
|
+
manager.respond(to: request, withResult: .attributeNotFound)
|
|
346
|
+
return
|
|
424
347
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
) {
|
|
430
|
-
for request in requests {
|
|
431
|
-
if let value = request.value {
|
|
432
|
-
handleIncomingData(value)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
// Respond to the first request -- Core Bluetooth sends the
|
|
436
|
-
// response for all requests in the batch
|
|
437
|
-
if let first = requests.first {
|
|
438
|
-
peripheral.respond(to: first, withResult: .success)
|
|
439
|
-
}
|
|
348
|
+
let current = Thermometer.latestReading()
|
|
349
|
+
guard request.offset <= current.count else {
|
|
350
|
+
manager.respond(to: request, withResult: .invalidOffset)
|
|
351
|
+
return
|
|
440
352
|
}
|
|
353
|
+
request.value = current.subdata(in: request.offset..<current.count)
|
|
354
|
+
manager.respond(to: request, withResult: .success)
|
|
355
|
+
}
|
|
441
356
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
didSubscribeTo characteristic: CBCharacteristic
|
|
446
|
-
) {
|
|
447
|
-
// Central subscribed to notifications -- start sending updates
|
|
448
|
-
startSendingUpdates()
|
|
357
|
+
func peripheralManager(_ manager: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
358
|
+
for request in requests {
|
|
359
|
+
if let value = request.value { Thermostat.apply(value, to: request.characteristic.uuid) }
|
|
449
360
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
_ peripheral: CBPeripheralManager,
|
|
453
|
-
central: CBCentral,
|
|
454
|
-
didUnsubscribeFrom characteristic: CBCharacteristic
|
|
455
|
-
) {
|
|
456
|
-
stopSendingUpdates()
|
|
361
|
+
if let first = requests.first {
|
|
362
|
+
manager.respond(to: first, withResult: .success)
|
|
457
363
|
}
|
|
458
364
|
}
|
|
365
|
+
|
|
366
|
+
func peripheralManager(_ manager: CBPeripheralManager, central: CBCentral,
|
|
367
|
+
didSubscribeTo characteristic: CBCharacteristic) {
|
|
368
|
+
Thermometer.startStreaming(to: central)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func peripheralManager(_ manager: CBPeripheralManager, central: CBCentral,
|
|
372
|
+
didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
373
|
+
Thermometer.stopStreaming(to: central)
|
|
374
|
+
}
|
|
459
375
|
```
|
|
376
|
+
|
|
377
|
+
Reads honour `request.offset` so long values can be fetched in pieces. For a
|
|
378
|
+
batch of writes, respond once, to the first request; that single response
|
|
379
|
+
covers the whole batch. Subscriptions start and stop your update stream.
|