#include "include/quick_blue/quick_blue_plugin.h" // This must be included before many other Windows headers. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GUID_FORMAT "%08x-%04hx-%04hx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" #define GUID_ARG(guid) guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7] namespace { using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::Storage::Streams; using namespace winrt::Windows::Devices::Radios; using namespace winrt::Windows::Devices::Bluetooth; using namespace winrt::Windows::Devices::Bluetooth::Advertisement; using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; using flutter::EncodableValue; using flutter::EncodableMap; using flutter::EncodableList; union uint16_t_union { uint16_t uint16; byte bytes[sizeof(uint16_t)]; }; std::vector to_bytevc(IBuffer buffer) { auto reader = DataReader::FromBuffer(buffer); auto result = std::vector(reader.UnconsumedBufferLength()); reader.ReadBytes(result); return result; } IBuffer from_bytevc(std::vector bytes) { auto writer = DataWriter(); writer.WriteBytes(bytes); return writer.DetachBuffer(); } std::string to_hexstring(std::vector bytes) { auto ss = std::stringstream(); for (auto b : bytes) ss << std::setw(2) << std::setfill('0') << std::hex << static_cast(b); return ss.str(); } std::string to_uuidstr(winrt::guid guid) { char chars[36 + 1]; sprintf_s(chars, GUID_FORMAT, GUID_ARG(guid)); return std::string{ chars }; } struct BluetoothDeviceAgent { BluetoothLEDevice device; winrt::event_token connnectionStatusChangedToken; std::map gattServices; std::map gattCharacteristics; std::map valueChangedTokens; BluetoothDeviceAgent(BluetoothLEDevice device, winrt::event_token connnectionStatusChangedToken) : device(device), connnectionStatusChangedToken(connnectionStatusChangedToken) {} ~BluetoothDeviceAgent() { device = nullptr; } IAsyncOperation GetServiceAsync(std::string service) { if (gattServices.count(service) == 0) { auto serviceResult = co_await device.GetGattServicesAsync(); if (serviceResult.Status() != GattCommunicationStatus::Success) co_return nullptr; for (auto s : serviceResult.Services()) if (to_uuidstr(s.Uuid()) == service) gattServices.insert(std::make_pair(service, s)); } co_return gattServices.at(service); } IAsyncOperation GetCharacteristicAsync(std::string service, std::string characteristic) { if (gattCharacteristics.count(characteristic) == 0) { auto gattService = co_await GetServiceAsync(service); auto characteristicResult = co_await gattService.GetCharacteristicsAsync(); if (characteristicResult.Status() != GattCommunicationStatus::Success) co_return nullptr; for (auto c : characteristicResult.Characteristics()) if (to_uuidstr(c.Uuid()) == characteristic) gattCharacteristics.insert(std::make_pair(characteristic, c)); } co_return gattCharacteristics.at(characteristic); } }; class QuickBluePlugin : public flutter::Plugin, public flutter::StreamHandler { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); QuickBluePlugin(); virtual ~QuickBluePlugin(); private: winrt::fire_and_forget InitializeAsync(); // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); std::unique_ptr> OnListenInternal( const EncodableValue* arguments, std::unique_ptr>&& events) override; std::unique_ptr> OnCancelInternal( const EncodableValue* arguments) override; std::unique_ptr> message_connector_; std::unique_ptr> availability_change_sink_; std::unique_ptr> scan_result_sink_; Radio bluetoothRadio{ nullptr }; void Radio_StateChanged(Radio sender, IInspectable args); RadioState oldRadioState = RadioState::Unknown; BluetoothLEAdvertisementWatcher bluetoothLEWatcher{ nullptr }; winrt::event_token bluetoothLEWatcherReceivedToken; winrt::fire_and_forget BluetoothLEWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args); std::map> connectedDevices{}; winrt::event_revoker radioStateChangedRevoker; winrt::fire_and_forget ConnectAsync(uint64_t bluetoothAddress); void BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args); void CleanConnection(uint64_t bluetoothAddress); winrt::fire_and_forget DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent); winrt::fire_and_forget SetNotifiableAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, GattCharacteristic& gattCharacteristic, std::string bleInputProperty); winrt::fire_and_forget RequestMtuAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, uint64_t expectedMtu); winrt::fire_and_forget ReadValueAsync(GattCharacteristic& gattCharacteristic); winrt::fire_and_forget WriteValueAsync(GattCharacteristic& gattCharacteristic, std::vector value, std::string bleOutputProperty); void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args); }; // static void QuickBluePlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) { auto method = std::make_unique>( registrar->messenger(), "quick_blue/method", &flutter::StandardMethodCodec::GetInstance()); auto event_availability_change = std::make_unique>( registrar->messenger(), "quick_blue/event.availabilityChange", &flutter::StandardMethodCodec::GetInstance()); auto event_scan_result = std::make_unique>( registrar->messenger(), "quick_blue/event.scanResult", &flutter::StandardMethodCodec::GetInstance()); auto message_connector_ = std::make_unique>( registrar->messenger(), "quick_blue/message.connector", &flutter::StandardMessageCodec::GetInstance()); auto plugin = std::make_unique(); method->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); auto availability_handler = std::make_unique< flutter::StreamHandlerFunctions<>>( [plugin_pointer = plugin.get()]( const EncodableValue* arguments, std::unique_ptr>&& events) -> std::unique_ptr> { return plugin_pointer->OnListen(arguments, std::move(events)); }, [plugin_pointer = plugin.get()](const EncodableValue* arguments) -> std::unique_ptr> { return plugin_pointer->OnCancel(arguments); }); auto scan_result_handler = std::make_unique< flutter::StreamHandlerFunctions<>>( [plugin_pointer = plugin.get()]( const EncodableValue* arguments, std::unique_ptr>&& events) -> std::unique_ptr> { return plugin_pointer->OnListen(arguments, std::move(events)); }, [plugin_pointer = plugin.get()](const EncodableValue* arguments) -> std::unique_ptr> { return plugin_pointer->OnCancel(arguments); }); event_availability_change->SetStreamHandler(std::move(availability_handler)); event_scan_result->SetStreamHandler(std::move(scan_result_handler)); plugin->message_connector_ = std::move(message_connector_); registrar->AddPlugin(std::move(plugin)); } QuickBluePlugin::QuickBluePlugin() { InitializeAsync(); } QuickBluePlugin::~QuickBluePlugin() {} winrt::fire_and_forget QuickBluePlugin::InitializeAsync() { auto bluetoothAdapter = co_await BluetoothAdapter::GetDefaultAsync(); bluetoothRadio = co_await bluetoothAdapter.GetRadioAsync(); if (bluetoothRadio) { radioStateChangedRevoker = bluetoothRadio.StateChanged(winrt::auto_revoke, { this, &QuickBluePlugin::Radio_StateChanged }); } } void QuickBluePlugin::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { auto method_name = method_call.method_name(); OutputDebugString((L"HandleMethodCall " + winrt::to_hstring(method_name) + L"\n").c_str()); if (method_name.compare("isBluetoothAvailable") == 0) { result->Success(EncodableValue(bluetoothRadio && bluetoothRadio.State() == RadioState::On)); } else if (method_name.compare("startScan") == 0) { if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) { if (!bluetoothLEWatcher) { bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({ this, &QuickBluePlugin::BluetoothLEWatcher_Received }); } bluetoothLEWatcher.Start(); result->Success(nullptr); } else { result->Error("IllegalState", "Bluetooth unavailable"); } } else if (method_name.compare("stopScan") == 0) { if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) { if (bluetoothLEWatcher) { bluetoothLEWatcher.Stop(); bluetoothLEWatcher.Received(bluetoothLEWatcherReceivedToken); } bluetoothLEWatcher = nullptr; result->Success(nullptr); } else { result->Error("IllegalState", "Bluetooth unavailable"); } } else if (method_name.compare("connect") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); ConnectAsync(std::stoull(deviceId)); result->Success(nullptr); } else if (method_name.compare("disconnect") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); CleanConnection(std::stoull(deviceId)); // TODO send `disconnected` message result->Success(nullptr); } else if (method_name.compare("discoverServices") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); auto it = connectedDevices.find(std::stoull(deviceId)); if (it == connectedDevices.end()) { result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); return; } DiscoverServicesAsync(*it->second); result->Success(nullptr); } else if (method_name.compare("setNotifiable") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); auto service = std::get(args[EncodableValue("service")]); auto characteristic = std::get(args[EncodableValue("characteristic")]); auto bleInputProperty = std::get(args[EncodableValue("bleInputProperty")]); auto it = connectedDevices.find(std::stoull(deviceId)); if (it == connectedDevices.end()) { result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); return; } auto bluetoothAgent = *it->second; auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); async_c.Completed([&, result_pointer = result.get()] (IAsyncOperation const& sender, AsyncStatus const args) { // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 auto c = sender.GetResults(); if (c == nullptr) { result_pointer->Error("IllegalArgument", "Unknown characteristic:" + characteristic); return; } SetNotifiableAsync(bluetoothAgent, c, bleInputProperty); result_pointer->Success(nullptr); }); } else if (method_name.compare("readValue") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); auto service = std::get(args[EncodableValue("service")]); auto characteristic = std::get(args[EncodableValue("characteristic")]); auto it = connectedDevices.find(std::stoull(deviceId)); if (it == connectedDevices.end()) { result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); return; } auto bluetoothAgent = *it->second; auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); async_c.Completed([&, result_pointer = result.get()] (IAsyncOperation const& sender, AsyncStatus const args) { // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 auto c = sender.GetResults(); if (c == nullptr) { result_pointer->Error("IllegalArgument", "Unknown characteristic:" + characteristic); return; } ReadValueAsync(c); result_pointer->Success(nullptr); }); } else if (method_name.compare("writeValue") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); auto service = std::get(args[EncodableValue("service")]); auto characteristic = std::get(args[EncodableValue("characteristic")]); auto value = std::get>(args[EncodableValue("value")]); auto bleOutputProperty = std::get(args[EncodableValue("bleOutputProperty")]); auto it = connectedDevices.find(std::stoull(deviceId)); if (it == connectedDevices.end()) { result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); return; } auto bluetoothAgent = *it->second; auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); async_c.Completed([&, result_pointer = result.get()] (IAsyncOperation const& sender, AsyncStatus const args) { // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 auto c = sender.GetResults(); if (c == nullptr) { result_pointer->Error("IllegalArgument", "Unknown characteristic:" + characteristic); return; } WriteValueAsync(c, value, bleOutputProperty); result_pointer->Success(nullptr); }); } else if (method_name.compare("requestMtu") == 0) { auto args = std::get(*method_call.arguments()); auto deviceId = std::get(args[EncodableValue("deviceId")]); auto expectedMtu = std::get(args[EncodableValue("expectedMtu")]); auto it = connectedDevices.find(std::stoull(deviceId)); if (it == connectedDevices.end()) { result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); return; } RequestMtuAsync(*it->second, expectedMtu); result->Success(nullptr); } else { result->NotImplemented(); } } std::vector parseManufacturerDataHead(BluetoothLEAdvertisement advertisement) { if (advertisement.ManufacturerData().Size() == 0) return std::vector(); auto manufacturerData = advertisement.ManufacturerData().GetAt(0); // FIXME Compat with REG_DWORD_BIG_ENDIAN uint8_t* prefix = uint16_t_union{ manufacturerData.CompanyId() }.bytes; auto result = std::vector{ prefix, prefix + sizeof(uint16_t_union) }; auto data = to_bytevc(manufacturerData.Data()); result.insert(result.end(), data.begin(), data.end()); return result; } enum class AvailabilityState : int { unknown = 0, resetting = 1, unsupported = 2, unauthorized = 3, poweredOff = 4, poweredOn = 5, }; void QuickBluePlugin::Radio_StateChanged(Radio radio, IInspectable args) { auto radioState = !radio ? RadioState::Disabled : radio.State(); // FIXME https://stackoverflow.com/questions/66099947/bluetooth-radio-statechanged-event-fires-twice/67723902#67723902 if (oldRadioState == radioState) { return; } oldRadioState = radioState; auto state = [=]() -> AvailabilityState { if (radioState == RadioState::Unknown) { return AvailabilityState::unknown; } else if (radioState == RadioState::Off) { return AvailabilityState::poweredOff; } else if (radioState == RadioState::On) { return AvailabilityState::poweredOn; } else if (radioState == RadioState::Disabled) { return AvailabilityState::unsupported; } else { return AvailabilityState::unknown; } }(); if (availability_change_sink_) { availability_change_sink_->Success(static_cast(state)); } } winrt::fire_and_forget QuickBluePlugin::BluetoothLEWatcher_Received( BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) { auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(args.BluetoothAddress()); auto name = device ? device.Name() : args.Advertisement().LocalName(); OutputDebugString((L"Received BluetoothAddress:" + winrt::to_hstring(args.BluetoothAddress()) + L", Name:" + name + L", LocalName:" + args.Advertisement().LocalName() + L"\n").c_str()); if (scan_result_sink_) { scan_result_sink_->Success(EncodableMap{ {"name", winrt::to_string(name)}, {"deviceId", std::to_string(args.BluetoothAddress())}, {"manufacturerDataHead", parseManufacturerDataHead(args.Advertisement())}, {"rssi", args.RawSignalStrengthInDBm()}, }); } } std::unique_ptr> QuickBluePlugin::OnListenInternal( const EncodableValue* arguments, std::unique_ptr>&& events) { if (arguments == nullptr) { return nullptr; } auto args = std::get(*arguments); auto name = std::get(args[EncodableValue("name")]); if (name.compare("availabilityChange") == 0) { availability_change_sink_ = std::move(events); Radio_StateChanged(bluetoothRadio, nullptr); } else if (name.compare("scanResult") == 0) { scan_result_sink_ = std::move(events); } return nullptr; } std::unique_ptr> QuickBluePlugin::OnCancelInternal( const EncodableValue* arguments) { if (arguments == nullptr) { return nullptr; } auto args = std::get(*arguments); auto name = std::get(args[EncodableValue("name")]); if (name.compare("availabilityChange") == 0) { availability_change_sink_ = nullptr; } else if (name.compare("scanResult") == 0) { scan_result_sink_ = nullptr; } return nullptr; } winrt::fire_and_forget QuickBluePlugin::ConnectAsync(uint64_t bluetoothAddress) { auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetoothAddress); auto servicesResult = co_await device.GetGattServicesAsync(); if (servicesResult.Status() != GattCommunicationStatus::Success) { OutputDebugString((L"GetGattServicesAsync error: " + winrt::to_hstring((int32_t)servicesResult.Status()) + L"\n").c_str()); message_connector_->Send(EncodableMap{ {"deviceId", std::to_string(bluetoothAddress)}, {"ConnectionState", "disconnected"}, }); co_return; } auto connnectionStatusChangedToken = device.ConnectionStatusChanged({ this, &QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged }); auto deviceAgent = std::make_unique(device, connnectionStatusChangedToken); auto pair = std::make_pair(bluetoothAddress, std::move(deviceAgent)); connectedDevices.insert(std::move(pair)); message_connector_->Send(EncodableMap{ {"deviceId", std::to_string(bluetoothAddress)}, {"ConnectionState", "connected"}, }); } void QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args) { OutputDebugString((L"ConnectionStatusChanged " + winrt::to_hstring((int32_t)sender.ConnectionStatus()) + L"\n").c_str()); if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) { CleanConnection(sender.BluetoothAddress()); message_connector_->Send(EncodableMap{ {"deviceId", std::to_string(sender.BluetoothAddress())}, {"ConnectionState", "disconnected"}, }); } } void QuickBluePlugin::CleanConnection(uint64_t bluetoothAddress) { auto node = connectedDevices.extract(bluetoothAddress); if (!node.empty()) { auto deviceAgent = std::move(node.mapped()); deviceAgent->device.ConnectionStatusChanged(deviceAgent->connnectionStatusChangedToken); for (auto& tokenPair : deviceAgent->valueChangedTokens) { deviceAgent->gattCharacteristics.at(tokenPair.first).ValueChanged(tokenPair.second); } } } winrt::fire_and_forget QuickBluePlugin::DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent) { auto serviceResult = co_await bluetoothDeviceAgent.device.GetGattServicesAsync(); if (serviceResult.Status() != GattCommunicationStatus::Success) { message_connector_->Send( EncodableMap{ {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, {"ServiceState", "discovered"} } ); co_return; } for (auto s : serviceResult.Services()) { auto characteristicResult = co_await s.GetCharacteristicsAsync(); auto msg = EncodableMap{ {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, {"ServiceState", "discovered"}, {"service", to_uuidstr(s.Uuid())} }; if (characteristicResult.Status() == GattCommunicationStatus::Success) { EncodableList characteristics; for (auto c : characteristicResult.Characteristics()) { characteristics.push_back(to_uuidstr(c.Uuid())); } msg.insert({"characteristics", characteristics}); } message_connector_->Send(msg); } } winrt::fire_and_forget QuickBluePlugin::RequestMtuAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, uint64_t expectedMtu) { OutputDebugString(L"RequestMtuAsync expectedMtu"); auto gattSession = co_await GattSession::FromDeviceIdAsync(bluetoothDeviceAgent.device.BluetoothDeviceId()); message_connector_->Send(EncodableMap{ {"mtuConfig", (int64_t)gattSession.MaxPduSize()}, }); } winrt::fire_and_forget QuickBluePlugin::SetNotifiableAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, GattCharacteristic& gattCharacteristic, std::string bleInputProperty) { auto descriptorValue = bleInputProperty == "notification" ? GattClientCharacteristicConfigurationDescriptorValue::Notify : bleInputProperty == "indication" ? GattClientCharacteristicConfigurationDescriptorValue::Indicate : GattClientCharacteristicConfigurationDescriptorValue::None; auto writeDescriptorStatus = co_await gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(descriptorValue); if (writeDescriptorStatus != GattCommunicationStatus::Success) OutputDebugString((L"WriteClientCharacteristicConfigurationDescriptorAsync " + winrt::to_hstring((int32_t)writeDescriptorStatus) + L"\n").c_str()); auto uuid = to_uuidstr(gattCharacteristic.Uuid()); if (bleInputProperty != "disabled") { bluetoothDeviceAgent.valueChangedTokens[uuid] = gattCharacteristic.ValueChanged({ this, &QuickBluePlugin::GattCharacteristic_ValueChanged }); } else { gattCharacteristic.ValueChanged(std::exchange(bluetoothDeviceAgent.valueChangedTokens[uuid], {})); } } winrt::fire_and_forget QuickBluePlugin::ReadValueAsync(GattCharacteristic& gattCharacteristic) { auto readValueResult = co_await gattCharacteristic.ReadValueAsync(); auto uuid = to_uuidstr(gattCharacteristic.Uuid()); auto bytes = to_bytevc(readValueResult.Value()); OutputDebugString((L"ReadValueAsync " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); message_connector_->Send(EncodableMap{ {"deviceId", std::to_string(gattCharacteristic.Service().Device().BluetoothAddress())}, {"characteristicValue", EncodableMap{ {"characteristic", uuid}, {"value", bytes}, }}, }); } winrt::fire_and_forget QuickBluePlugin::WriteValueAsync(GattCharacteristic& gattCharacteristic, std::vector value, std::string bleOutputProperty) { auto writeOption = bleOutputProperty.compare("withoutResponse") == 0 ? GattWriteOption::WriteWithoutResponse : GattWriteOption::WriteWithResponse; auto writeValueStatus = co_await gattCharacteristic.WriteValueAsync(from_bytevc(value), writeOption); auto uuid = to_uuidstr(gattCharacteristic.Uuid()); OutputDebugString((L"WriteValueAsync " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(value)) + L", " + winrt::to_hstring((int32_t)writeValueStatus) + L"\n").c_str()); } void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) { auto uuid = to_uuidstr(sender.Uuid()); auto bytes = to_bytevc(args.CharacteristicValue()); OutputDebugString((L"GattCharacteristic_ValueChanged " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); message_connector_->Send(EncodableMap{ {"deviceId", std::to_string(sender.Service().Device().BluetoothAddress())}, {"characteristicValue", EncodableMap{ {"characteristic", uuid}, {"value", bytes}, }}, }); } } // namespace void QuickBluePluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { QuickBluePlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); }