7 Commits

Author SHA1 Message Date
Tony 511f1fff5b Refactor service discovery to always perform fresh discovery, removing cached services check
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-08-08 08:41:43 +08:00
Tony 52712055f5 Remove pubspec.lock from version control and update .gitignore
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-25 08:15:13 +08:00
Tony 9083c54cd2 Update build.gradle to set Java and Kotlin compatibility to version 11
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-11 10:57:29 +08:00
Tony 784c6f3929 Implement BLE peripheral support for Windows; add necessary structures and methods
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
2026-05-08 07:50:32 +08:00
Tony c8e0c60c24 Add generated plugin registrant files to .gitignore
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
2026-05-08 06:55:05 +08:00
Tony 4ca00e7258 Remove generated plugin registrant files for Linux, macOS, and Windows from the repository
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
2026-05-08 06:48:07 +08:00
Tony 46f9106a27 Add .flutter-plugins-dependencies to .gitignore
Pull Request / test (push) Has been cancelled
Pull Request / pana (push) Has been cancelled
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-08 06:23:34 +08:00
15 changed files with 924 additions and 523 deletions
+7
View File
@@ -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
+4 -2
View File
@@ -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);
+3 -3
View File
@@ -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
View File
@@ -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"))
}
-379
View File
@@ -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)
+847 -11
View File
@@ -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> &current_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 &notification_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 {
+48
View File
@@ -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);