Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 511f1fff5b | |||
| 52712055f5 | |||
| 9083c54cd2 | |||
| 784c6f3929 | |||
| c8e0c60c24 | |||
| 4ca00e7258 | |||
| 46f9106a27 |
@@ -28,7 +28,14 @@ migrate_working_dir/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
# Swift Package Manager (SPM) artifacts (used when Flutter enables SPM for plugins)
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
# Generated plugin registrant files
|
||||
**/generated_plugin_registrant.cc
|
||||
**/generated_plugin_registrant.h
|
||||
**/generated_plugins.cmake
|
||||
**/GeneratedPluginRegistrant.swift
|
||||
@@ -58,7 +58,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
|
||||
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
|
||||
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| peripheral/GATT server | ✔️ | ✔️ | ✔️ | 🚧 | ✔️ | ❌ |
|
||||
| peripheral/GATT server | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -342,12 +342,14 @@ await characteristic.unsubscribe();
|
||||
|
||||
## Peripheral Mode
|
||||
|
||||
`UniversalBlePeripheral` exposes a local GATT-server API for apps that need to advertise services and accept central/client writes. Android, iOS, macOS, and Linux are implemented; Windows currently reports `notSupported` until its platform server implementation is added. Web browsers do not expose a standard GATT-server API.
|
||||
`UniversalBlePeripheral` exposes a local GATT-server API for apps that need to advertise services and accept central/client writes. Android, iOS, macOS, Linux, and Windows are implemented. Web browsers do not expose a standard GATT-server API.
|
||||
|
||||
On iOS and macOS, peripheral mode uses CoreBluetooth `CBPeripheralManager`. CoreBluetooth manages CCCD subscription state internally, and central connection events are reported when a central reads, writes, subscribes, or unsubscribes from a local characteristic.
|
||||
|
||||
On Linux, peripheral mode uses BlueZ `GattManager1` and `LEAdvertisingManager1`. The Bluetooth adapter must be powered, support the peripheral role, and the app process must be allowed to register GATT applications and LE advertisements on the system bus.
|
||||
|
||||
On Windows, peripheral mode uses WinRT `GattServiceProvider` and `GattLocalCharacteristic`. The Bluetooth adapter must support the LE peripheral role and Bluetooth must be turned on. Windows auto-generates CCCD for notify/indicate characteristics, uses the system Bluetooth device name instead of the requested `advertisedName`, and requires the `bluetooth` device capability when the app is packaged as MSIX/UWP.
|
||||
|
||||
```dart
|
||||
await UniversalBle.requestPermissions(withAndroidBluetoothAdvertise: true);
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '11'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -35,12 +35,11 @@ class UniversalBleAsyncServiceDiscovery: NSObject {
|
||||
}
|
||||
isDiscoveryInProgress = true
|
||||
|
||||
// Check if services are already cached
|
||||
if let cachedServices = peripheral.services, !cachedServices.isEmpty {
|
||||
handleServicesDiscovered(cachedServices)
|
||||
} else {
|
||||
peripheral.discoverServices(nil)
|
||||
}
|
||||
// A peripheral can keep CoreBluetooth's previous GATT view across a
|
||||
// firmware update. UniversalBle.discoverServices is the explicit fresh
|
||||
// discovery API; callers that want cached data use BleDevice's
|
||||
// preferCached path instead.
|
||||
peripheral.discoverServices(nil)
|
||||
}
|
||||
|
||||
/// Cleans up discovery state
|
||||
@@ -64,13 +63,7 @@ class UniversalBleAsyncServiceDiscovery: NSObject {
|
||||
|
||||
// Discover characteristics for each service
|
||||
for service in services {
|
||||
if let cachedChar = service.characteristics, !cachedChar.isEmpty {
|
||||
// Characteristics already cached, process them
|
||||
processCharacteristicsForService(service)
|
||||
} else {
|
||||
// Need to discover characteristics
|
||||
peripheral.discoverCharacteristics(nil, for: service)
|
||||
}
|
||||
peripheral.discoverCharacteristics(nil, for: service)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -33,6 +33,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
pubspec.lock
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
@@ -44,4 +45,11 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
.metadata
|
||||
.metadata
|
||||
|
||||
|
||||
# Generated plugin registrant files
|
||||
**/generated_plugin_registrant.cc
|
||||
**/generated_plugin_registrant.h
|
||||
**/generated_plugins.cmake
|
||||
**/GeneratedPluginRegistrant.swift
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -1,23 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import universal_ble
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
bluez:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bluez
|
||||
sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.3"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
expandable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expandable
|
||||
sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_bluetooth:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_web_bluetooth
|
||||
sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_ble:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.3.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
sdks:
|
||||
dart: ">=3.11.4 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <universal_ble/universal_ble_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
UniversalBlePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UniversalBlePluginCApi"));
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -1,24 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
universal_ble
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -35,6 +35,189 @@ const auto device_address_key = L"System.Devices.Aep.DeviceAddress";
|
||||
const auto signal_strength_key = L"System.Devices.Aep.SignalStrength";
|
||||
static std::unique_ptr<UniversalBleCallbackChannel> callback_channel;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPeripheralExtendedPropertiesDescriptorUuid =
|
||||
"00002900-0000-1000-8000-00805f9b34fb";
|
||||
constexpr auto kPeripheralUserDescriptionDescriptorUuid =
|
||||
"00002901-0000-1000-8000-00805f9b34fb";
|
||||
constexpr auto kPeripheralClientConfigurationDescriptorUuid =
|
||||
"00002902-0000-1000-8000-00805f9b34fb";
|
||||
constexpr auto kPeripheralPresentationFormatDescriptorUuid =
|
||||
"00002904-0000-1000-8000-00805f9b34fb";
|
||||
constexpr auto kPeripheralAggregateFormatDescriptorUuid =
|
||||
"00002905-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
std::string normalize_uuid_string(const std::string &uuid) {
|
||||
return guid_to_uuid(uuid_to_guid(uuid));
|
||||
}
|
||||
|
||||
std::optional<int64_t>
|
||||
encodable_to_int64(const flutter::EncodableValue &value) {
|
||||
if (const auto *v = std::get_if<int32_t>(&value)) {
|
||||
return static_cast<int64_t>(*v);
|
||||
}
|
||||
if (const auto *v = std::get_if<int64_t>(&value)) {
|
||||
return *v;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool encodable_list_contains_int(const flutter::EncodableList &list,
|
||||
const int64_t target) {
|
||||
return std::any_of(list.begin(), list.end(), [target](const auto &value) {
|
||||
const auto parsed_value = encodable_to_int64(value);
|
||||
return parsed_value.has_value() && parsed_value.value() == target;
|
||||
});
|
||||
}
|
||||
|
||||
GattCharacteristicProperties
|
||||
to_peripheral_properties(const flutter::EncodableList &properties) {
|
||||
auto flags = GattCharacteristicProperties::None;
|
||||
for (const auto &value : properties) {
|
||||
const auto parsed_value = encodable_to_int64(value);
|
||||
if (!parsed_value.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (static_cast<CharacteristicProperty>(parsed_value.value())) {
|
||||
case CharacteristicProperty::broadcast:
|
||||
throw create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"Broadcast characteristic property is not supported on Windows");
|
||||
case CharacteristicProperty::read:
|
||||
flags = flags | GattCharacteristicProperties::Read;
|
||||
break;
|
||||
case CharacteristicProperty::writeWithoutResponse:
|
||||
flags = flags | GattCharacteristicProperties::WriteWithoutResponse;
|
||||
break;
|
||||
case CharacteristicProperty::write:
|
||||
flags = flags | GattCharacteristicProperties::Write;
|
||||
break;
|
||||
case CharacteristicProperty::notify:
|
||||
flags = flags | GattCharacteristicProperties::Notify;
|
||||
break;
|
||||
case CharacteristicProperty::indicate:
|
||||
flags = flags | GattCharacteristicProperties::Indicate;
|
||||
break;
|
||||
case CharacteristicProperty::authenticatedSignedWrites:
|
||||
flags = flags | GattCharacteristicProperties::AuthenticatedSignedWrites;
|
||||
break;
|
||||
case CharacteristicProperty::extendedProperties:
|
||||
flags = flags | GattCharacteristicProperties::ExtendedProperties;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool is_generated_descriptor_uuid(const std::string &uuid) {
|
||||
const auto normalized_uuid = normalize_uuid_string(uuid);
|
||||
return normalized_uuid == kPeripheralExtendedPropertiesDescriptorUuid ||
|
||||
normalized_uuid == kPeripheralUserDescriptionDescriptorUuid ||
|
||||
normalized_uuid == kPeripheralClientConfigurationDescriptorUuid ||
|
||||
normalized_uuid == kPeripheralPresentationFormatDescriptorUuid ||
|
||||
normalized_uuid == kPeripheralAggregateFormatDescriptorUuid;
|
||||
}
|
||||
|
||||
bool advertisement_status_is_started(
|
||||
const GattServiceProviderAdvertisementStatus status) {
|
||||
return status == GattServiceProviderAdvertisementStatus::Started ||
|
||||
status ==
|
||||
GattServiceProviderAdvertisementStatus::StartedWithoutAllAdvertisementData;
|
||||
}
|
||||
|
||||
std::string bluetooth_error_to_string(const BluetoothError error) {
|
||||
switch (error) {
|
||||
case BluetoothError::Success:
|
||||
return "Success";
|
||||
case BluetoothError::RadioNotAvailable:
|
||||
return "RadioNotAvailable";
|
||||
case BluetoothError::ResourceInUse:
|
||||
return "ResourceInUse";
|
||||
case BluetoothError::DeviceNotConnected:
|
||||
return "DeviceNotConnected";
|
||||
case BluetoothError::OtherError:
|
||||
return "OtherError";
|
||||
case BluetoothError::DisabledByPolicy:
|
||||
return "DisabledByPolicy";
|
||||
case BluetoothError::NotSupported:
|
||||
return "NotSupported";
|
||||
case BluetoothError::DisabledByUser:
|
||||
return "DisabledByUser";
|
||||
case BluetoothError::ConsentRequired:
|
||||
return "ConsentRequired";
|
||||
case BluetoothError::TransportNotSupported:
|
||||
return "TransportNotSupported";
|
||||
}
|
||||
return "UnknownBluetoothError";
|
||||
}
|
||||
|
||||
FlutterError create_flutter_error_from_bluetooth_error(
|
||||
const BluetoothError error, const std::string &message) {
|
||||
UniversalBleErrorCode error_code = UniversalBleErrorCode::kFailed;
|
||||
switch (error) {
|
||||
case BluetoothError::RadioNotAvailable:
|
||||
case BluetoothError::DisabledByUser:
|
||||
error_code = UniversalBleErrorCode::kBluetoothNotEnabled;
|
||||
break;
|
||||
case BluetoothError::ResourceInUse:
|
||||
error_code = UniversalBleErrorCode::kOperationInProgress;
|
||||
break;
|
||||
case BluetoothError::DeviceNotConnected:
|
||||
error_code = UniversalBleErrorCode::kDeviceDisconnected;
|
||||
break;
|
||||
case BluetoothError::DisabledByPolicy:
|
||||
case BluetoothError::ConsentRequired:
|
||||
error_code = UniversalBleErrorCode::kAccessDenied;
|
||||
break;
|
||||
case BluetoothError::NotSupported:
|
||||
case BluetoothError::TransportNotSupported:
|
||||
error_code = UniversalBleErrorCode::kNotSupported;
|
||||
break;
|
||||
case BluetoothError::OtherError:
|
||||
case BluetoothError::Success:
|
||||
error_code = UniversalBleErrorCode::kFailed;
|
||||
break;
|
||||
}
|
||||
|
||||
auto final_message = message;
|
||||
if (final_message.empty()) {
|
||||
final_message = bluetooth_error_to_string(error);
|
||||
}
|
||||
return create_flutter_error(error_code, final_message,
|
||||
bluetooth_error_to_string(error));
|
||||
}
|
||||
|
||||
std::string peripheral_session_device_id(const GattSession &session) {
|
||||
return to_string(session.DeviceId().Id());
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
merge_peripheral_write_value(const std::vector<uint8_t> ¤t_value,
|
||||
const std::vector<uint8_t> &incoming_value,
|
||||
const uint32_t offset) {
|
||||
if (offset == 0) {
|
||||
return incoming_value;
|
||||
}
|
||||
|
||||
auto updated_value = current_value;
|
||||
if (updated_value.size() < offset) {
|
||||
updated_value.resize(offset);
|
||||
}
|
||||
|
||||
const auto new_size = static_cast<size_t>(offset) + incoming_value.size();
|
||||
if (updated_value.size() < new_size) {
|
||||
updated_value.resize(new_size);
|
||||
}
|
||||
|
||||
std::copy(incoming_value.begin(), incoming_value.end(),
|
||||
updated_value.begin() + offset);
|
||||
return updated_value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void UniversalBlePlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows *registrar) {
|
||||
auto plugin = std::make_unique<UniversalBlePlugin>(registrar);
|
||||
@@ -50,7 +233,7 @@ UniversalBlePlugin::UniversalBlePlugin(
|
||||
InitializeAsync();
|
||||
}
|
||||
|
||||
UniversalBlePlugin::~UniversalBlePlugin() = default;
|
||||
UniversalBlePlugin::~UniversalBlePlugin() { ResetState(); }
|
||||
|
||||
// UniversalBlePlatformChannel implementation.
|
||||
void UniversalBlePlugin::GetBluetoothAvailabilityState(
|
||||
@@ -445,18 +628,461 @@ void UniversalBlePlugin::RequestConnectionPriority(
|
||||
"requestConnectionPriority is not supported on Windows platform"));
|
||||
}
|
||||
|
||||
ErrorOr<bool> UniversalBlePlugin::IsPeripheralSupported() { return false; }
|
||||
ErrorOr<bool> UniversalBlePlugin::IsPeripheralSupported() {
|
||||
try {
|
||||
if (!bluetooth_adapter_) {
|
||||
bluetooth_adapter_ = async_get(BluetoothAdapter::GetDefaultAsync());
|
||||
}
|
||||
|
||||
if (!bluetooth_adapter_ || !bluetooth_adapter_.IsLowEnergySupported() ||
|
||||
!bluetooth_adapter_.IsPeripheralRoleSupported()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bluetooth_radio_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return bluetooth_radio_.State() == RadioState::On;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::StartPeripheral(
|
||||
const UniversalBlePeripheralConfig &config,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"BLE peripheral mode is not implemented on Windows platform yet"));
|
||||
if (!bluetooth_radio_ || bluetooth_radio_.State() != RadioState::On) {
|
||||
result(create_flutter_error(UniversalBleErrorCode::kBluetoothNotEnabled,
|
||||
"Bluetooth not enabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!bluetooth_adapter_) {
|
||||
bluetooth_adapter_ = async_get(BluetoothAdapter::GetDefaultAsync());
|
||||
}
|
||||
if (!bluetooth_adapter_ || !bluetooth_adapter_.IsLowEnergySupported() ||
|
||||
!bluetooth_adapter_.IsPeripheralRoleSupported()) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"BLE peripheral mode is not supported by this Windows adapter"));
|
||||
return;
|
||||
}
|
||||
} catch (const FlutterError &err) {
|
||||
result(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (peripheral_start_in_progress_) {
|
||||
result(create_flutter_error(UniversalBleErrorCode::kOperationInProgress,
|
||||
"Peripheral start already in progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
peripheral_start_in_progress_ = true;
|
||||
StopPeripheralInternal();
|
||||
|
||||
[this, config, result = std::move(result)]() mutable -> fire_and_forget {
|
||||
try {
|
||||
if (!config.advertised_name().empty()) {
|
||||
UniversalBleLogger::LogWarning(
|
||||
"Windows GATT server ignores advertisedName and uses the system Bluetooth name");
|
||||
}
|
||||
|
||||
for (const auto &service_value : config.services()) {
|
||||
const auto &service_config = std::any_cast<const UniversalBlePeripheralService &>(
|
||||
std::get<flutter::CustomEncodableValue>(service_value));
|
||||
const auto service_uuid = normalize_uuid_string(service_config.uuid());
|
||||
|
||||
auto service_provider_result =
|
||||
co_await GattServiceProvider::CreateAsync(uuid_to_guid(service_uuid));
|
||||
if (service_provider_result.Error() != BluetoothError::Success) {
|
||||
throw create_flutter_error_from_bluetooth_error(
|
||||
service_provider_result.Error(),
|
||||
"Failed to create peripheral service " + service_uuid);
|
||||
}
|
||||
|
||||
PeripheralServiceObject service_object;
|
||||
service_object.uuid = service_uuid;
|
||||
service_object.provider = service_provider_result.ServiceProvider();
|
||||
service_object.service = service_object.provider.Service();
|
||||
service_object.advertisement_status_changed_token =
|
||||
service_object.provider.AdvertisementStatusChanged(
|
||||
[service_uuid](const GattServiceProvider &,
|
||||
const GattServiceProviderAdvertisementStatusChangedEventArgs &args) {
|
||||
UniversalBleLogger::LogInfo(
|
||||
"PERIPHERAL_ADV_STATUS <- " + service_uuid +
|
||||
" status=" +
|
||||
std::to_string(static_cast<int>(args.Status())) +
|
||||
" error=" +
|
||||
bluetooth_error_to_string(args.Error()));
|
||||
});
|
||||
|
||||
for (const auto &characteristic_value : service_config.characteristics()) {
|
||||
const auto &characteristic_config =
|
||||
std::any_cast<const UniversalBlePeripheralCharacteristic &>(
|
||||
std::get<flutter::CustomEncodableValue>(characteristic_value));
|
||||
const auto characteristic_uuid =
|
||||
normalize_uuid_string(characteristic_config.uuid());
|
||||
const auto properties =
|
||||
to_peripheral_properties(characteristic_config.properties());
|
||||
const bool read_permitted =
|
||||
encodable_list_contains_int(characteristic_config.permissions(), 0);
|
||||
const bool write_permitted =
|
||||
encodable_list_contains_int(characteristic_config.permissions(), 1);
|
||||
|
||||
GattLocalCharacteristicParameters parameters;
|
||||
parameters.CharacteristicProperties(properties);
|
||||
if (characteristic_config.initial_value() != nullptr) {
|
||||
parameters.StaticValue(from_bytevc(*characteristic_config.initial_value()));
|
||||
}
|
||||
if (read_permitted) {
|
||||
parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
|
||||
}
|
||||
if (write_permitted) {
|
||||
parameters.WriteProtectionLevel(GattProtectionLevel::Plain);
|
||||
}
|
||||
|
||||
auto characteristic_result = co_await service_object.service
|
||||
.CreateCharacteristicAsync(
|
||||
uuid_to_guid(characteristic_uuid),
|
||||
parameters);
|
||||
if (characteristic_result.Error() != BluetoothError::Success) {
|
||||
throw create_flutter_error_from_bluetooth_error(
|
||||
characteristic_result.Error(),
|
||||
"Failed to create peripheral characteristic " +
|
||||
characteristic_uuid);
|
||||
}
|
||||
|
||||
PeripheralCharacteristicObject characteristic_object;
|
||||
characteristic_object.service_uuid = service_uuid;
|
||||
characteristic_object.uuid = characteristic_uuid;
|
||||
characteristic_object.obj = characteristic_result.Characteristic();
|
||||
characteristic_object.properties = properties;
|
||||
characteristic_object.read_permitted = read_permitted;
|
||||
characteristic_object.write_permitted = write_permitted;
|
||||
characteristic_object.value =
|
||||
characteristic_config.initial_value() != nullptr
|
||||
? *characteristic_config.initial_value()
|
||||
: std::vector<uint8_t>{};
|
||||
|
||||
if ((properties & GattCharacteristicProperties::Read) !=
|
||||
GattCharacteristicProperties::None) {
|
||||
characteristic_object.read_requested_token =
|
||||
characteristic_object.obj.ReadRequested(
|
||||
[this, service_uuid, characteristic_uuid](
|
||||
const GattLocalCharacteristic &sender,
|
||||
const GattReadRequestedEventArgs &args) {
|
||||
[this, service_uuid, characteristic_uuid, sender,
|
||||
args]() -> fire_and_forget {
|
||||
auto deferral = args.GetDeferral();
|
||||
try {
|
||||
const auto device_id =
|
||||
peripheral_session_device_id(args.Session());
|
||||
MarkPeripheralClientConnected(device_id);
|
||||
|
||||
auto request = co_await args.GetRequestAsync();
|
||||
if (!request ||
|
||||
request.State() == GattRequestState::Canceled) {
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto *characteristic = FindPeripheralCharacteristic(
|
||||
service_uuid, characteristic_uuid);
|
||||
if (characteristic == nullptr) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::AttributeNotFound());
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!characteristic->read_permitted) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::ReadNotPermitted());
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (request.Offset() > characteristic->value.size()) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::InvalidOffset());
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
const auto begin = characteristic->value.begin() +
|
||||
static_cast<std::ptrdiff_t>(
|
||||
request.Offset());
|
||||
const auto response_bytes = std::vector<uint8_t>(
|
||||
begin, characteristic->value.end());
|
||||
request.RespondWithValue(from_bytevc(response_bytes));
|
||||
} catch (const hresult_error &err) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_READ_FAILED <- " + service_uuid +
|
||||
" " + characteristic_uuid +
|
||||
" hr=" + std::to_string(err.code()) +
|
||||
" msg=" + to_string(err.message()));
|
||||
} catch (const std::exception &ex) {
|
||||
UniversalBleLogger::LogError(
|
||||
std::string("PERIPHERAL_READ_FAILED <- ") +
|
||||
service_uuid + " " + characteristic_uuid +
|
||||
" error=" + ex.what());
|
||||
} catch (...) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_READ_FAILED <- unknown error");
|
||||
}
|
||||
deferral.Complete();
|
||||
}();
|
||||
});
|
||||
}
|
||||
|
||||
if ((properties & (GattCharacteristicProperties::Write |
|
||||
GattCharacteristicProperties::WriteWithoutResponse)) !=
|
||||
GattCharacteristicProperties::None) {
|
||||
characteristic_object.write_requested_token =
|
||||
characteristic_object.obj.WriteRequested(
|
||||
[this, service_uuid, characteristic_uuid](
|
||||
const GattLocalCharacteristic &sender,
|
||||
const GattWriteRequestedEventArgs &args) {
|
||||
[this, service_uuid, characteristic_uuid, sender,
|
||||
args]() -> fire_and_forget {
|
||||
auto deferral = args.GetDeferral();
|
||||
try {
|
||||
const auto device_id =
|
||||
peripheral_session_device_id(args.Session());
|
||||
MarkPeripheralClientConnected(device_id);
|
||||
|
||||
auto request = co_await args.GetRequestAsync();
|
||||
if (!request ||
|
||||
request.State() == GattRequestState::Canceled) {
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto *characteristic = FindPeripheralCharacteristic(
|
||||
service_uuid, characteristic_uuid);
|
||||
if (characteristic == nullptr) {
|
||||
if (request.Option() ==
|
||||
GattWriteOption::WriteWithResponse) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::AttributeNotFound());
|
||||
}
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (!characteristic->write_permitted) {
|
||||
if (request.Option() ==
|
||||
GattWriteOption::WriteWithResponse) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::WriteNotPermitted());
|
||||
}
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
const auto request_value = to_bytevc(request.Value());
|
||||
if (request.Offset() > characteristic->value.size()) {
|
||||
if (request.Option() ==
|
||||
GattWriteOption::WriteWithResponse) {
|
||||
request.RespondWithProtocolError(
|
||||
GattProtocolError::InvalidOffset());
|
||||
}
|
||||
deferral.Complete();
|
||||
co_return;
|
||||
}
|
||||
|
||||
characteristic->value = merge_peripheral_write_value(
|
||||
characteristic->value, request_value,
|
||||
static_cast<uint32_t>(request.Offset()));
|
||||
|
||||
if (request.Option() ==
|
||||
GattWriteOption::WriteWithResponse) {
|
||||
request.Respond();
|
||||
}
|
||||
|
||||
NotifyPeripheralWriteEvent(
|
||||
UniversalBlePeripheralWriteEvent(
|
||||
device_id, characteristic->service_uuid,
|
||||
characteristic->uuid, request_value));
|
||||
} catch (const hresult_error &err) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_WRITE_FAILED <- " + service_uuid +
|
||||
" " + characteristic_uuid +
|
||||
" hr=" + std::to_string(err.code()) +
|
||||
" msg=" + to_string(err.message()));
|
||||
} catch (const std::exception &ex) {
|
||||
UniversalBleLogger::LogError(
|
||||
std::string("PERIPHERAL_WRITE_FAILED <- ") +
|
||||
service_uuid + " " + characteristic_uuid +
|
||||
" error=" + ex.what());
|
||||
} catch (...) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_WRITE_FAILED <- unknown error");
|
||||
}
|
||||
deferral.Complete();
|
||||
}();
|
||||
});
|
||||
}
|
||||
|
||||
if ((properties & (GattCharacteristicProperties::Notify |
|
||||
GattCharacteristicProperties::Indicate)) !=
|
||||
GattCharacteristicProperties::None) {
|
||||
characteristic_object.subscribed_clients_changed_token =
|
||||
characteristic_object.obj.SubscribedClientsChanged(
|
||||
[this, service_uuid, characteristic_uuid](
|
||||
const GattLocalCharacteristic &sender,
|
||||
const IInspectable &) {
|
||||
try {
|
||||
auto *characteristic = FindPeripheralCharacteristic(
|
||||
service_uuid, characteristic_uuid);
|
||||
if (characteristic == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> next_client_ids;
|
||||
for (const auto &client : sender.SubscribedClients()) {
|
||||
const auto device_id =
|
||||
peripheral_session_device_id(client.Session());
|
||||
next_client_ids.insert(device_id);
|
||||
if (characteristic->subscribed_client_ids.insert(device_id)
|
||||
.second) {
|
||||
MarkPeripheralClientConnected(device_id);
|
||||
NotifyPeripheralSubscriptionChanged(
|
||||
device_id, characteristic->service_uuid,
|
||||
characteristic->uuid, true);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> removed_client_ids;
|
||||
for (const auto &device_id :
|
||||
characteristic->subscribed_client_ids) {
|
||||
if (next_client_ids.count(device_id) == 0) {
|
||||
removed_client_ids.push_back(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &device_id : removed_client_ids) {
|
||||
characteristic->subscribed_client_ids.erase(device_id);
|
||||
NotifyPeripheralSubscriptionChanged(
|
||||
device_id, characteristic->service_uuid,
|
||||
characteristic->uuid, false);
|
||||
MaybeMarkPeripheralClientDisconnected(device_id);
|
||||
}
|
||||
} catch (const hresult_error &err) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_SUBSCRIBE_FAILED <- " + service_uuid +
|
||||
" " + characteristic_uuid +
|
||||
" hr=" + std::to_string(err.code()) +
|
||||
" msg=" + to_string(err.message()));
|
||||
} catch (const std::exception &ex) {
|
||||
UniversalBleLogger::LogError(
|
||||
std::string("PERIPHERAL_SUBSCRIBE_FAILED <- ") +
|
||||
service_uuid + " " + characteristic_uuid +
|
||||
" error=" + ex.what());
|
||||
} catch (...) {
|
||||
UniversalBleLogger::LogError(
|
||||
"PERIPHERAL_SUBSCRIBE_FAILED <- unknown error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const auto &descriptor_value :
|
||||
characteristic_config.descriptors()) {
|
||||
const auto &descriptor_config =
|
||||
std::any_cast<const UniversalBlePeripheralDescriptor &>(
|
||||
std::get<flutter::CustomEncodableValue>(descriptor_value));
|
||||
const auto descriptor_uuid =
|
||||
normalize_uuid_string(descriptor_config.uuid());
|
||||
if (is_generated_descriptor_uuid(descriptor_uuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GattLocalDescriptorParameters descriptor_parameters;
|
||||
if (descriptor_config.initial_value() != nullptr) {
|
||||
descriptor_parameters.StaticValue(
|
||||
from_bytevc(*descriptor_config.initial_value()));
|
||||
}
|
||||
if (encodable_list_contains_int(descriptor_config.permissions(), 0)) {
|
||||
descriptor_parameters.ReadProtectionLevel(
|
||||
GattProtectionLevel::Plain);
|
||||
}
|
||||
if (encodable_list_contains_int(descriptor_config.permissions(), 1)) {
|
||||
descriptor_parameters.WriteProtectionLevel(
|
||||
GattProtectionLevel::Plain);
|
||||
}
|
||||
|
||||
auto descriptor_result =
|
||||
co_await characteristic_object.obj.CreateDescriptorAsync(
|
||||
uuid_to_guid(descriptor_uuid), descriptor_parameters);
|
||||
if (descriptor_result.Error() != BluetoothError::Success) {
|
||||
throw create_flutter_error_from_bluetooth_error(
|
||||
descriptor_result.Error(),
|
||||
"Failed to create peripheral descriptor " + descriptor_uuid);
|
||||
}
|
||||
|
||||
PeripheralDescriptorObject descriptor_object;
|
||||
descriptor_object.obj = descriptor_result.Descriptor();
|
||||
descriptor_object.value =
|
||||
descriptor_config.initial_value() != nullptr
|
||||
? *descriptor_config.initial_value()
|
||||
: std::vector<uint8_t>{};
|
||||
characteristic_object.descriptors.insert_or_assign(descriptor_uuid,
|
||||
descriptor_object);
|
||||
}
|
||||
|
||||
service_object.characteristics.insert_or_assign(characteristic_uuid,
|
||||
std::move(characteristic_object));
|
||||
}
|
||||
|
||||
peripheral_services_.insert_or_assign(service_uuid, std::move(service_object));
|
||||
|
||||
auto &inserted_service = peripheral_services_.at(service_uuid);
|
||||
GattServiceProviderAdvertisingParameters advertising_parameters;
|
||||
advertising_parameters.IsConnectable(true);
|
||||
advertising_parameters.IsDiscoverable(true);
|
||||
inserted_service.provider.StartAdvertising(advertising_parameters);
|
||||
|
||||
const auto advertisement_status =
|
||||
inserted_service.provider.AdvertisementStatus();
|
||||
if (!advertisement_status_is_started(advertisement_status)) {
|
||||
throw create_flutter_error(
|
||||
UniversalBleErrorCode::kFailed,
|
||||
"Failed to start peripheral advertising for service " +
|
||||
service_uuid,
|
||||
std::to_string(static_cast<int>(advertisement_status)));
|
||||
}
|
||||
}
|
||||
|
||||
peripheral_start_in_progress_ = false;
|
||||
result(std::nullopt);
|
||||
} catch (const FlutterError &err) {
|
||||
StopPeripheralInternal();
|
||||
peripheral_start_in_progress_ = false;
|
||||
result(err);
|
||||
} catch (const hresult_error &err) {
|
||||
StopPeripheralInternal();
|
||||
peripheral_start_in_progress_ = false;
|
||||
result(create_flutter_error(UniversalBleErrorCode::kFailed,
|
||||
to_string(err.message()),
|
||||
std::to_string(err.code())));
|
||||
} catch (const std::exception &ex) {
|
||||
StopPeripheralInternal();
|
||||
peripheral_start_in_progress_ = false;
|
||||
result(create_flutter_error(UniversalBleErrorCode::kFailed, ex.what()));
|
||||
} catch (...) {
|
||||
StopPeripheralInternal();
|
||||
peripheral_start_in_progress_ = false;
|
||||
result(create_flutter_unknown_error());
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::StopPeripheral(
|
||||
std::function<void(std::optional<FlutterError> reply)> result) {
|
||||
StopPeripheralInternal();
|
||||
result(std::nullopt);
|
||||
}
|
||||
|
||||
@@ -464,18 +1090,80 @@ void UniversalBlePlugin::UpdatePeripheralCharacteristicValue(
|
||||
const std::string &service, const std::string &characteristic,
|
||||
const std::vector<uint8_t> &value,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"BLE peripheral mode is not implemented on Windows platform yet"));
|
||||
auto *peripheral_characteristic =
|
||||
FindPeripheralCharacteristic(service, characteristic);
|
||||
if (peripheral_characteristic == nullptr) {
|
||||
result(create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound,
|
||||
"Unknown peripheral characteristic " +
|
||||
characteristic));
|
||||
return;
|
||||
}
|
||||
|
||||
peripheral_characteristic->value = value;
|
||||
result(std::nullopt);
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::NotifyPeripheralCharacteristic(
|
||||
const std::string &service, const std::string &characteristic,
|
||||
const std::vector<uint8_t> &value, bool indicate,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"BLE peripheral mode is not implemented on Windows platform yet"));
|
||||
auto *peripheral_characteristic =
|
||||
FindPeripheralCharacteristic(service, characteristic);
|
||||
if (peripheral_characteristic == nullptr) {
|
||||
result(create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound,
|
||||
"Unknown peripheral characteristic " +
|
||||
characteristic));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto supports_notify =
|
||||
(peripheral_characteristic->properties &
|
||||
GattCharacteristicProperties::Notify) !=
|
||||
GattCharacteristicProperties::None;
|
||||
const auto supports_indicate =
|
||||
(peripheral_characteristic->properties &
|
||||
GattCharacteristicProperties::Indicate) !=
|
||||
GattCharacteristicProperties::None;
|
||||
|
||||
if (indicate && !supports_indicate) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kCharacteristicDoesNotSupportIndicate,
|
||||
"Characteristic does not support indicate"));
|
||||
return;
|
||||
}
|
||||
if (!indicate && !supports_notify) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kCharacteristicDoesNotSupportNotify,
|
||||
"Characteristic does not support notify"));
|
||||
return;
|
||||
}
|
||||
|
||||
peripheral_characteristic->value = value;
|
||||
const auto gatt_characteristic = peripheral_characteristic->obj;
|
||||
|
||||
[gatt_characteristic, value,
|
||||
result = std::move(result)]() mutable -> fire_and_forget {
|
||||
try {
|
||||
const auto notification_results =
|
||||
co_await gatt_characteristic.NotifyValueAsync(from_bytevc(value));
|
||||
for (const auto ¬ification_result : notification_results) {
|
||||
if (notification_result.Status() != GattCommunicationStatus::Success) {
|
||||
result(create_flutter_error_from_gatt_communication_status(
|
||||
notification_result.Status()));
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
result(std::nullopt);
|
||||
} catch (const FlutterError &err) {
|
||||
result(err);
|
||||
} catch (const hresult_error &err) {
|
||||
result(create_flutter_error(UniversalBleErrorCode::kFailed,
|
||||
to_string(err.message()),
|
||||
std::to_string(err.code())));
|
||||
} catch (...) {
|
||||
result(create_flutter_unknown_error());
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::ReadRssi(
|
||||
@@ -548,6 +1236,11 @@ void UniversalBlePlugin::GetSystemDevices(
|
||||
/// Helper Methods
|
||||
|
||||
fire_and_forget UniversalBlePlugin::InitializeAsync() {
|
||||
try {
|
||||
bluetooth_adapter_ = co_await BluetoothAdapter::GetDefaultAsync();
|
||||
} catch (...) {
|
||||
bluetooth_adapter_ = nullptr;
|
||||
}
|
||||
const auto radios = co_await Radio::GetRadiosAsync();
|
||||
for (auto &&radio : radios) {
|
||||
if (radio.Kind() == RadioKind::Bluetooth) {
|
||||
@@ -1309,12 +2002,155 @@ void UniversalBlePlugin::DisposeServices(
|
||||
device_agent->gatt_map.clear();
|
||||
}
|
||||
|
||||
PeripheralCharacteristicObject *UniversalBlePlugin::FindPeripheralCharacteristic(
|
||||
const std::string &service_uuid, const std::string &characteristic_uuid) {
|
||||
const auto normalized_service_uuid = normalize_uuid_string(service_uuid);
|
||||
const auto service_it = peripheral_services_.find(normalized_service_uuid);
|
||||
if (service_it == peripheral_services_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto normalized_characteristic_uuid =
|
||||
normalize_uuid_string(characteristic_uuid);
|
||||
const auto characteristic_it =
|
||||
service_it->second.characteristics.find(normalized_characteristic_uuid);
|
||||
if (characteristic_it == service_it->second.characteristics.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &characteristic_it->second;
|
||||
}
|
||||
|
||||
bool UniversalBlePlugin::IsPeripheralClientSubscribed(
|
||||
const std::string &device_id) const {
|
||||
for (const auto &[service_uuid, service] : peripheral_services_) {
|
||||
for (const auto &[characteristic_uuid, characteristic] :
|
||||
service.characteristics) {
|
||||
if (characteristic.subscribed_client_ids.count(device_id) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::MarkPeripheralClientConnected(
|
||||
const std::string &device_id) {
|
||||
if (peripheral_connected_client_ids_.insert(device_id).second) {
|
||||
NotifyPeripheralConnectionChanged(device_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::MaybeMarkPeripheralClientDisconnected(
|
||||
const std::string &device_id) {
|
||||
if (IsPeripheralClientSubscribed(device_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (peripheral_connected_client_ids_.erase(device_id) != 0) {
|
||||
NotifyPeripheralConnectionChanged(device_id, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::StopPeripheralInternal() {
|
||||
std::vector<std::string> disconnected_client_ids(
|
||||
peripheral_connected_client_ids_.begin(),
|
||||
peripheral_connected_client_ids_.end());
|
||||
|
||||
for (auto &[service_uuid, service] : peripheral_services_) {
|
||||
for (auto &[characteristic_uuid, characteristic] : service.characteristics) {
|
||||
if (characteristic.read_requested_token.has_value()) {
|
||||
try {
|
||||
characteristic.obj.ReadRequested(
|
||||
characteristic.read_requested_token.value());
|
||||
} catch (...) {
|
||||
}
|
||||
characteristic.read_requested_token = std::nullopt;
|
||||
}
|
||||
if (characteristic.write_requested_token.has_value()) {
|
||||
try {
|
||||
characteristic.obj.WriteRequested(
|
||||
characteristic.write_requested_token.value());
|
||||
} catch (...) {
|
||||
}
|
||||
characteristic.write_requested_token = std::nullopt;
|
||||
}
|
||||
if (characteristic.subscribed_clients_changed_token.has_value()) {
|
||||
try {
|
||||
characteristic.obj.SubscribedClientsChanged(
|
||||
characteristic.subscribed_clients_changed_token.value());
|
||||
} catch (...) {
|
||||
}
|
||||
characteristic.subscribed_clients_changed_token = std::nullopt;
|
||||
}
|
||||
characteristic.subscribed_client_ids.clear();
|
||||
characteristic.descriptors.clear();
|
||||
characteristic.obj = nullptr;
|
||||
}
|
||||
service.characteristics.clear();
|
||||
|
||||
if (service.advertisement_status_changed_token.has_value()) {
|
||||
try {
|
||||
service.provider.AdvertisementStatusChanged(
|
||||
service.advertisement_status_changed_token.value());
|
||||
} catch (...) {
|
||||
}
|
||||
service.advertisement_status_changed_token = std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
service.provider.StopAdvertising();
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
service.service = nullptr;
|
||||
service.provider = nullptr;
|
||||
}
|
||||
|
||||
peripheral_services_.clear();
|
||||
peripheral_connected_client_ids_.clear();
|
||||
peripheral_start_in_progress_ = false;
|
||||
|
||||
for (const auto &device_id : disconnected_client_ids) {
|
||||
NotifyPeripheralConnectionChanged(device_id, false);
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::NotifyPeripheralConnectionChanged(
|
||||
const std::string &device_id, const bool connected) {
|
||||
ui_thread_handler_.Post([device_id, connected] {
|
||||
callback_channel->OnPeripheralConnectionChanged(device_id, connected,
|
||||
SuccessCallback,
|
||||
ErrorCallback);
|
||||
});
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::NotifyPeripheralWriteEvent(
|
||||
const UniversalBlePeripheralWriteEvent &event) {
|
||||
ui_thread_handler_.Post([event] {
|
||||
callback_channel->OnPeripheralWrite(event, SuccessCallback, ErrorCallback);
|
||||
});
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::NotifyPeripheralSubscriptionChanged(
|
||||
const std::string &device_id, const std::string &service_uuid,
|
||||
const std::string &characteristic_uuid, const bool subscribed) {
|
||||
ui_thread_handler_.Post([device_id, service_uuid, characteristic_uuid,
|
||||
subscribed] {
|
||||
callback_channel->OnPeripheralSubscriptionChanged(
|
||||
device_id, service_uuid, characteristic_uuid, subscribed,
|
||||
SuccessCallback, ErrorCallback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief In some cases, it helps to reset the whole Bluetooth state to get
|
||||
* rid of any dangling connections, before scanning or connecting.
|
||||
*/
|
||||
void UniversalBlePlugin::ResetState() {
|
||||
try {
|
||||
StopPeripheralInternal();
|
||||
|
||||
// Stop and detach advertisement watcher
|
||||
if (bluetooth_le_watcher_ != nullptr) {
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "ui_thread_handler.hpp"
|
||||
#include "universal_ble_thread_safe.h"
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace universal_ble {
|
||||
struct GattCharacteristicObject {
|
||||
@@ -65,6 +67,34 @@ struct BluetoothDeviceAgent {
|
||||
}
|
||||
};
|
||||
|
||||
struct PeripheralDescriptorObject {
|
||||
GattLocalDescriptor obj = nullptr;
|
||||
std::vector<uint8_t> value;
|
||||
};
|
||||
|
||||
struct PeripheralCharacteristicObject {
|
||||
std::string service_uuid;
|
||||
std::string uuid;
|
||||
GattLocalCharacteristic obj = nullptr;
|
||||
GattCharacteristicProperties properties = GattCharacteristicProperties::None;
|
||||
bool read_permitted = false;
|
||||
bool write_permitted = false;
|
||||
std::vector<uint8_t> value;
|
||||
std::unordered_map<std::string, PeripheralDescriptorObject> descriptors;
|
||||
std::unordered_set<std::string> subscribed_client_ids;
|
||||
std::optional<event_token> read_requested_token;
|
||||
std::optional<event_token> write_requested_token;
|
||||
std::optional<event_token> subscribed_clients_changed_token;
|
||||
};
|
||||
|
||||
struct PeripheralServiceObject {
|
||||
std::string uuid;
|
||||
GattServiceProvider provider = nullptr;
|
||||
GattLocalService service = nullptr;
|
||||
std::unordered_map<std::string, PeripheralCharacteristicObject> characteristics;
|
||||
std::optional<event_token> advertisement_status_changed_token;
|
||||
};
|
||||
|
||||
class UniversalBlePlugin : public flutter::Plugin,
|
||||
public UniversalBlePlatformChannel {
|
||||
public:
|
||||
@@ -101,9 +131,12 @@ private:
|
||||
RadioState old_radio_state_ = RadioState::Unknown;
|
||||
BluetoothLEAdvertisementWatcher bluetooth_le_watcher_{nullptr};
|
||||
DeviceWatcher device_watcher_{nullptr};
|
||||
BluetoothAdapter bluetooth_adapter_{nullptr};
|
||||
|
||||
std::unordered_map<uint64_t, std::unique_ptr<BluetoothDeviceAgent>>
|
||||
connected_devices_{};
|
||||
std::unordered_map<std::string, PeripheralServiceObject> peripheral_services_{};
|
||||
std::unordered_set<std::string> peripheral_connected_client_ids_{};
|
||||
ThreadSafeMap<std::string, DeviceInformation> device_watcher_devices_{};
|
||||
ThreadSafeMap<std::string, UniversalBleScanResult> scan_results_{};
|
||||
// Maps DeviceInformation.Id() -> MAC address string used as key in
|
||||
@@ -117,6 +150,7 @@ private:
|
||||
event_token device_watcher_enumeration_completed_token_;
|
||||
event_token device_watcher_stopped_token_;
|
||||
event_revoker<IRadio> radio_state_changed_revoker_;
|
||||
bool peripheral_start_in_progress_ = false;
|
||||
|
||||
fire_and_forget InitializeAsync();
|
||||
fire_and_forget ConnectAsync(uint64_t bluetooth_address);
|
||||
@@ -164,6 +198,20 @@ private:
|
||||
void ResetState();
|
||||
void
|
||||
DisposeServices(const std::unique_ptr<BluetoothDeviceAgent> &device_agent);
|
||||
PeripheralCharacteristicObject *
|
||||
FindPeripheralCharacteristic(const std::string &service_uuid,
|
||||
const std::string &characteristic_uuid);
|
||||
bool IsPeripheralClientSubscribed(const std::string &device_id) const;
|
||||
void MarkPeripheralClientConnected(const std::string &device_id);
|
||||
void MaybeMarkPeripheralClientDisconnected(const std::string &device_id);
|
||||
void StopPeripheralInternal();
|
||||
void NotifyPeripheralConnectionChanged(const std::string &device_id,
|
||||
bool connected);
|
||||
void NotifyPeripheralWriteEvent(const UniversalBlePeripheralWriteEvent &event);
|
||||
void NotifyPeripheralSubscriptionChanged(const std::string &device_id,
|
||||
const std::string &service_uuid,
|
||||
const std::string &characteristic_uuid,
|
||||
bool subscribed);
|
||||
|
||||
void GattCharacteristicValueChanged(const GattCharacteristic &sender,
|
||||
const GattValueChangedEventArgs &args);
|
||||
|
||||
Reference in New Issue
Block a user