diff --git a/README.md b/README.md index cfb2e67..4872f80 100644 --- a/README.md +++ b/README.md @@ -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); diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 6598e44..875197c 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -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 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 +encodable_to_int64(const flutter::EncodableValue &value) { + if (const auto *v = std::get_if(&value)) { + return static_cast(*v); + } + if (const auto *v = std::get_if(&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(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 +merge_peripheral_write_value(const std::vector ¤t_value, + const std::vector &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(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(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 UniversalBlePlugin::IsPeripheralSupported() { return false; } +ErrorOr 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 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( + std::get(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(args.Status())) + + " error=" + + bluetooth_error_to_string(args.Error())); + }); + + for (const auto &characteristic_value : service_config.characteristics()) { + const auto &characteristic_config = + std::any_cast( + std::get(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{}; + + 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( + request.Offset()); + const auto response_bytes = std::vector( + 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(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 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 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( + std::get(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{}; + 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(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 reply)> result) { + StopPeripheralInternal(); result(std::nullopt); } @@ -464,18 +1090,80 @@ void UniversalBlePlugin::UpdatePeripheralCharacteristicValue( const std::string &service, const std::string &characteristic, const std::vector &value, std::function 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 &value, bool indicate, std::function 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 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 { diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 0806af2..2de2bb7 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -22,6 +22,8 @@ #include "ui_thread_handler.hpp" #include "universal_ble_thread_safe.h" #include +#include +#include namespace universal_ble { struct GattCharacteristicObject { @@ -65,6 +67,34 @@ struct BluetoothDeviceAgent { } }; +struct PeripheralDescriptorObject { + GattLocalDescriptor obj = nullptr; + std::vector 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 value; + std::unordered_map descriptors; + std::unordered_set subscribed_client_ids; + std::optional read_requested_token; + std::optional write_requested_token; + std::optional subscribed_clients_changed_token; +}; + +struct PeripheralServiceObject { + std::string uuid; + GattServiceProvider provider = nullptr; + GattLocalService service = nullptr; + std::unordered_map characteristics; + std::optional 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> connected_devices_{}; + std::unordered_map peripheral_services_{}; + std::unordered_set peripheral_connected_client_ids_{}; ThreadSafeMap device_watcher_devices_{}; ThreadSafeMap 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 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 &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);