diff --git a/BLE/BLEProtocolFactory.cpp b/BLE/BLEProtocolFactory.cpp index 2a61538..48df592 100644 --- a/BLE/BLEProtocolFactory.cpp +++ b/BLE/BLEProtocolFactory.cpp @@ -7,51 +7,37 @@ #include +#include +#include +#include +#include #include #include -#include - namespace Protocols { -struct BLEAdapters : std::enable_shared_from_this -{ - std::vector list = SimpleBLE::Adapter::get_adapters(); - ~BLEAdapters() - { - try { - while(!list.empty()) { - auto back = list.back(); - list.pop_back(); - } - } - catch(...){} - } -}; - class BLEEnumerator final : public Device::DeviceEnumerator { public: - std::shared_ptr adapters; - SimpleBLE::Adapter& adapter; - std::vector peripherals; - BLEEnumerator(std::shared_ptr ref, SimpleBLE::Adapter& adapter) - : adapters{std::move(ref)} - , adapter{adapter} + QBluetoothAddress adapter_address; + QBluetoothDeviceDiscoveryAgent* discovery_agent{}; + QList peripherals; + + BLEEnumerator(const QBluetoothAddress& addr) + : adapter_address{addr} { - adapter.set_callback_on_scan_start([]() {}); - adapter.set_callback_on_scan_stop([]() {}); - adapter.set_callback_on_scan_found([this](SimpleBLE::Peripheral p) { - if(p.initialized()) - { - peripherals.push_back(p); - addNewDevice(p); - } + discovery_agent = new QBluetoothDeviceDiscoveryAgent(adapter_address); + discovery_agent->setLowEnergyDiscoveryTimeout(5000); + + QObject::connect(discovery_agent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + [this](const QBluetoothDeviceInfo& info) { + peripherals.push_back(info); + addNewDevice(info); }); - adapter.set_callback_on_scan_updated([](SimpleBLE::Peripheral) {}); + try { - adapter.scan_start(); + discovery_agent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } catch(const std::exception& e) { @@ -61,8 +47,12 @@ class BLEEnumerator final : public Device::DeviceEnumerator ~BLEEnumerator() { - if(adapter.initialized() && adapter.scan_is_active()) - adapter.scan_stop(); + if(discovery_agent) + { + if(discovery_agent->isActive()) + discovery_agent->stop(); + discovery_agent->deleteLater(); + } } private: @@ -73,7 +63,7 @@ class BLEEnumerator final : public Device::DeviceEnumerator Device::DeviceSettings set; BLESpecificSettings sub; - sub.adapter = QString::fromStdString(adapter.address()); + sub.adapter = adapter_address.toString(); set.name = "Advertisements"; @@ -83,26 +73,29 @@ class BLEEnumerator final : public Device::DeviceEnumerator f("Advertisements", set); } - void addNewDevice(SimpleBLE::Peripheral& p) noexcept + void addNewDevice(const QBluetoothDeviceInfo& p) noexcept { using namespace std::literals; Device::DeviceSettings set; BLESpecificSettings sub; - sub.adapter = QString::fromStdString(adapter.address()); - sub.serial = QString::fromStdString(p.address()); + sub.adapter = adapter_address.toString(); + if(!p.address().isNull()) + sub.serial = p.address().toString(); + else + sub.serial = p.deviceUuid().toString(QBluetoothUuid::WithoutBraces); QString pretty_name; QString device_name; - if(p.identifier().empty()) + if(p.name().isEmpty()) { pretty_name = sub.serial; device_name = sub.serial; } else { - pretty_name = QString::fromStdString(p.identifier()) + " (" + sub.serial + ")"; - device_name = QString::fromStdString(p.identifier()); + pretty_name = p.name() + " (" + sub.serial + ")"; + device_name = p.name(); } set.name = device_name; @@ -130,18 +123,15 @@ QUrl BLEProtocolFactory::manual() const noexcept Device::DeviceEnumerators BLEProtocolFactory::getEnumerators(const score::DocumentContext& ctx) const { - if(!SimpleBLE::Adapter::bluetooth_enabled()) - return {}; - - auto adapter_list = std::make_shared(); - if(adapter_list->list.empty()) + auto adapter_list = QBluetoothLocalDevice::allDevices(); + if(adapter_list.empty()) return {}; Device::DeviceEnumerators enums; - for(auto& adapter : adapter_list->list) + for(const auto& adapter : adapter_list) enums.emplace_back( - QString::fromStdString(adapter.identifier()), - new BLEEnumerator{adapter_list, adapter}); + adapter.name(), + new BLEEnumerator{adapter.address()}); return enums; } diff --git a/BLE/BLEProtocolSettingsWidget.cpp b/BLE/BLEProtocolSettingsWidget.cpp index bc626c9..7fb3470 100644 --- a/BLE/BLEProtocolSettingsWidget.cpp +++ b/BLE/BLEProtocolSettingsWidget.cpp @@ -11,8 +11,7 @@ #include #include #include - -#include +#include #include @@ -42,9 +41,10 @@ BLEProtocolSettingsWidget::BLEProtocolSettingsWidget(QWidget* parent) layout->addRow(tr("Name"), m_deviceNameEdit); layout->addRow(tr("Include filters"), m_include); layout->addRow(tr("Exclude filters"), m_exclude); + bool bluetooth_enabled = !QBluetoothLocalDevice::allDevices().isEmpty(); layout->addRow(new QLabel( tr("Bluetooth status: %1") - .arg(SimpleBLE::Adapter::bluetooth_enabled() ? "enabled" : "disabled"))); + .arg(bluetooth_enabled ? "enabled" : "disabled"))); setLayout(layout); } diff --git a/BLE/Protocol.cpp b/BLE/Protocol.cpp index 3beff49..dd43041 100644 --- a/BLE/Protocol.cpp +++ b/BLE/Protocol.cpp @@ -12,7 +12,9 @@ #include #include -#include +#include +#include +#include #include namespace ossia @@ -24,10 +26,13 @@ const ble_map_type& ble_descriptor_map(); void expose_manufacturer_data_as_ossia_nodes( ossia::net::node_base& device_node, - const std::map& manufacturer_data) + const QMultiHash& manufacturer_data) { - for(const auto& [id, data] : manufacturer_data) + for(auto it = manufacturer_data.begin(); it != manufacturer_data.end(); ++it) { + const quint16 id = it.key(); + const QByteArray& data = it.value(); + bool got_good_cbor = false; // If the id is the special BLE CBOR id, we have a special advertisement containing CBOR data if(id == ossia::special_ble_cbor_id) @@ -42,15 +47,15 @@ void expose_manufacturer_data_as_ossia_nodes( ossia::net::node_base& data_node = ossia::net::find_or_create_node(device_node, std::to_string(id)); auto param = data_node.create_parameter(ossia::val_type::STRING); - param->set_value(data); + param->set_value(std::string(data.data(), data.size())); } } } -bool expose_cbor_as_ossia_nodes(ossia::net::node_base& device_node, const SimpleBLE::ByteArray& cbor_data) +bool expose_cbor_as_ossia_nodes(ossia::net::node_base& device_node, const QByteArray& cbor_data) { QCborStreamReader reader; - reader.addData(cbor_data.data(), cbor_data.size()); + reader.addData(cbor_data); if(!reader.isMap()) { // Right now, we only support non nested CBOR maps. Anything else would be kind of a waste of the precious 27 bytes of data @@ -147,126 +152,237 @@ ble_protocol::ble_protocol( ossia::net::network_context_ptr ptr, std::string_view adapter_uuid, std::string_view serial) : protocol_base{flags{SupportsMultiplex}} + , m_targetSerial{QString::fromUtf8(serial.data(), serial.size())} , m_context{ptr} , m_strand{boost::asio::make_strand(m_context->context)} { // First look for the correct adapter, or take the first one if // the exact one cannot be found - auto adapters = SimpleBLE::Adapter::get_adapters(); - if(adapters.empty()) - return; - for(auto& adapter : adapters) + auto adapters = QBluetoothLocalDevice::allDevices(); + if(!adapters.empty()) { - if(adapter_uuid == adapter.address()) + bool found = false; + QString adapter_str = QString::fromUtf8(adapter_uuid.data(), adapter_uuid.size()); + for(const auto& adapter : adapters) { - m_adapter = adapter; - break; + if(adapter.address().toString() == adapter_str) + { + m_adapterAddress = adapter.address(); + found = true; + break; + } } + if(!found) + m_adapterAddress = adapters[0].address(); } - if(!m_adapter.initialized()) - m_adapter = adapters[0]; - if(m_adapter.initialized()) + // Create discovery agent + if(!m_adapterAddress.isNull()) { - // Setup our callbacks - m_adapter.set_callback_on_scan_start([] {}); - m_adapter.set_callback_on_scan_stop([] {}); - m_adapter.set_callback_on_scan_found( - [this, serial = std::string(serial)](SimpleBLE::Peripheral p) { - if(p.address() == serial) - { - m_peripheral = p; - m_peripheral.connect(); + m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(m_adapterAddress, this); + m_discoveryAgent->setLowEnergyDiscoveryTimeout(5000); - boost::asio::post(m_strand, [this] { scan_services(); }); - } - }); - m_adapter.set_callback_on_scan_updated( - [this, serial = std::string(serial)](SimpleBLE::Peripheral p) { - if(p.address() == serial) - { - if(!p.is_connected()) - p.connect(); + QObject::connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &ble_protocol::onDeviceDiscovered); + QObject::connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, + this, &ble_protocol::onDeviceUpdated); + } + else + { + m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + m_discoveryAgent->setLowEnergyDiscoveryTimeout(5000); - boost::asio::post(m_strand, [this] { scan_services(); }); + QObject::connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &ble_protocol::onDeviceDiscovered); + QObject::connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, + this, &ble_protocol::onDeviceUpdated); + } +} + +void ble_protocol::onDeviceDiscovered(const QBluetoothDeviceInfo& info) +{ + // Check if this is the device we're looking for + if(info.address().toString() == m_targetSerial || info.name() == m_targetSerial + || info.deviceUuid().toString(QBluetoothUuid::WithoutBraces) == m_targetSerial) + { + // Stop discovery + if(m_discoveryAgent) + m_discoveryAgent->stop(); + + // Create controller + if(!m_adapterAddress.isNull()) + m_controller = QLowEnergyController::createCentral(info, m_adapterAddress, this); + else + m_controller = QLowEnergyController::createCentral(info, this); + + QObject::connect(m_controller, &QLowEnergyController::connected, + this, &ble_protocol::onConnected); + QObject::connect(m_controller, &QLowEnergyController::disconnected, + this, &ble_protocol::onDisconnected); + QObject::connect(m_controller, &QLowEnergyController::serviceDiscovered, + this, &ble_protocol::onServiceDiscovered); + QObject::connect(m_controller, &QLowEnergyController::discoveryFinished, + this, &ble_protocol::onDiscoveryFinished); + + m_controller->connectToDevice(); + } +} + +void ble_protocol::onDeviceUpdated(const QBluetoothDeviceInfo& info, QBluetoothDeviceInfo::Fields updatedFields) +{ + // Could handle manufacturer data updates here if needed + if(m_controller && m_controller->state() != QLowEnergyController::ConnectedState) + { + if(info.address().toString() == m_targetSerial || info.name() == m_targetSerial) + { + // Expose manufacturer data + if(m_device && updatedFields.testFlag(QBluetoothDeviceInfo::Field::ManufacturerData)) + { + expose_manufacturer_data_as_ossia_nodes(m_device->get_root_node(), info.manufacturerData()); } - }); + } + } +} + +void ble_protocol::onConnected() +{ + if(m_controller) + { + m_controller->discoverServices(); } } +void ble_protocol::onDisconnected() +{ + // Could attempt reconnection here +} + +void ble_protocol::onServiceDiscovered(const QBluetoothUuid& service) +{ + // Services will be processed in onDiscoveryFinished +} + +void ble_protocol::onDiscoveryFinished() +{ + boost::asio::post(m_strand, [this] { scan_services(); }); +} + void ble_protocol::scan_services() { - if(!m_device) + if(!m_device || !m_controller) return; + auto& service_names = ble_service_map(); auto& char_names = ble_characteristic_map(); - auto& desc_names = ble_descriptor_map(); auto name_or_uuid - = [&](const ble_map_type& map, const std::string& uuid) -> const std::string& { - if(auto it = map.find(uuid); it != map.end()) + = [&](const ble_map_type& map, const QString& uuid) -> std::string { + std::string uuid_str = uuid.toStdString(); + if(auto it = map.find(uuid_str); it != map.end()) return it->second; else - return uuid; + return uuid_str; }; - for(auto& service : m_peripheral.services()) - { - auto& svc_node = ossia::net::find_or_create_node( - m_device->get_root_node(), name_or_uuid(service_names, service.uuid())); - auto param = svc_node.create_parameter(ossia::val_type::STRING); - param->set_value(service.data()); - for(auto& characteristic : service.characteristics()) - { - auto& chara_node = ossia::net::find_or_create_node( - svc_node, name_or_uuid(char_names, characteristic.uuid())); - auto param = chara_node.create_parameter(ossia::val_type::STRING); - if(characteristic.can_read()) - { - try { - auto val = m_peripheral.read(service.uuid(), characteristic.uuid()); - param->set_value(val); - } catch(...) { - - } - } + for(const auto& serviceUuid : m_controller->services()) + { + QLowEnergyService* service = m_controller->createServiceObject(serviceUuid, this); + if(!service) + continue; - if(characteristic.can_write_request() || characteristic.can_write_command() - || !characteristic.descriptors().empty()) - { - m_params.emplace(param, ble_param_id{service.uuid(), characteristic}); - } + m_services[serviceUuid] = service; - if(characteristic.can_notify()) + auto& svc_node = ossia::net::find_or_create_node( + m_device->get_root_node(), + name_or_uuid( + service_names, serviceUuid.toString(QBluetoothUuid::WithoutBraces))); + auto svc_param = svc_node.create_parameter(ossia::val_type::STRING); + + // Discover service details + QObject::connect(service, &QLowEnergyService::stateChanged, + this, [this, service, svc_param, &char_names](QLowEnergyService::ServiceState newState) { + if(newState == QLowEnergyService::RemoteServiceDiscovered) { - try { - m_peripheral.notify( - service.uuid(), characteristic.uuid(), - [=](const SimpleBLE::ByteArray& arr) mutable { param->set_value(arr); }); - } catch(...) { + auto& svc_node = svc_param->get_node(); + for(const auto& characteristic : service->characteristics()) + { + QString char_uuid = characteristic.uuid().toString(); + auto& chara_node = ossia::net::find_or_create_node( + svc_node, [&] { + std::string uuid_str = char_uuid.toStdString(); + if(auto it = char_names.find(uuid_str); it != char_names.end()) + return it->second; + return uuid_str; + }()); + + auto param = chara_node.create_parameter(ossia::val_type::STRING); + + // Read characteristic if readable + if(characteristic.properties() & QLowEnergyCharacteristic::Read) + { + QByteArray val = characteristic.value(); + if(!val.isEmpty()) + param->set_value(std::string(val.data(), val.size())); + } + + // Store for writing + if((characteristic.properties() & QLowEnergyCharacteristic::Write) + || (characteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse) + || (characteristic.properties() & QLowEnergyCharacteristic::WriteSigned) + || !characteristic.descriptors().empty()) + { + m_params.emplace(param, ble_param_id{service->serviceUuid(), characteristic}); + } + + // Setup notifications + if(characteristic.properties() & QLowEnergyCharacteristic::Notify) + { + QObject::connect(service, &QLowEnergyService::characteristicChanged, + this, [param](const QLowEnergyCharacteristic& info, const QByteArray& value) { + param->set_value(std::string(value.data(), value.size())); + }); + + // Enable notifications + QLowEnergyDescriptor cccd = characteristic.clientCharacteristicConfiguration(); + if(cccd.isValid()) + { + service->writeDescriptor(cccd, QLowEnergyCharacteristic::CCCDEnableNotification); + } + } } } - } + }); + + service->discoverDetails(); } - ossia::expose_manufacturer_data_as_ossia_nodes(m_device->get_root_node(), m_peripheral.manufacturer_data()); } void ble_protocol::set_device(net::device_base& dev) { m_device = &dev; - m_adapter.scan_start(); + if(m_discoveryAgent) + m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } ble_protocol::~ble_protocol() { - if(m_adapter.initialized()) - if(m_adapter.scan_is_active()) - m_adapter.scan_stop(); + if(m_discoveryAgent && m_discoveryAgent->isActive()) + m_discoveryAgent->stop(); - if(m_peripheral.initialized()) - if(m_peripheral.is_connected()) - m_peripheral.disconnect(); + if(m_controller) + { + if(m_controller->state() != QLowEnergyController::UnconnectedState) + m_controller->disconnectFromDevice(); + } + + // Clean up services + for(auto& [uuid, service] : m_services) + { + if(service) + service->deleteLater(); + } + m_services.clear(); } bool ble_protocol::pull(ossia::net::parameter_base&) @@ -276,20 +392,33 @@ bool ble_protocol::pull(ossia::net::parameter_base&) bool ble_protocol::push(const ossia::net::parameter_base& p, const ossia::value& v) { - if(m_peripheral.is_connected()) + if(m_controller && m_controller->state() == QLowEnergyController::ConnectedState) { if(auto it = m_params.find(&p); it != m_params.end()) { - auto& [svc, chara] = it->second; + auto& [svc_uuid, chara] = it->second; + + auto service_it = m_services.find(svc_uuid); + if(service_it == m_services.end()) + return false; + + QLowEnergyService* service = service_it->second; + if(!service) + return false; + + std::string val_str = ossia::convert(v); + QByteArray data(val_str.data(), val_str.size()); - if(chara.can_write_request()) - m_peripheral.write_request(svc, chara.uuid(), ossia::convert(v)); - else if(chara.can_write_command()) - m_peripheral.write_request(svc, chara.uuid(), ossia::convert(v)); + if(chara.properties() & QLowEnergyCharacteristic::Write) + service->writeCharacteristic(chara, data, QLowEnergyService::WriteWithResponse); + else if(chara.properties() & QLowEnergyCharacteristic::WriteNoResponse) + service->writeCharacteristic(chara, data, QLowEnergyService::WriteWithoutResponse); + else if(chara.properties() & QLowEnergyCharacteristic::WriteSigned) + service->writeCharacteristic(chara, data, QLowEnergyService::WriteSigned); else if(!chara.descriptors().empty()) - m_peripheral.write( - svc, chara.uuid(), chara.descriptors().front().uuid(), - ossia::convert(v)); + service->writeDescriptor(chara.descriptors().first(), data); + + return true; } } return false; @@ -321,140 +450,161 @@ ble_scan_protocol::ble_scan_protocol( { // First look for the correct adapter, or take the first one if // the exact one cannot be found - auto adapters = SimpleBLE::Adapter::get_adapters(); - if(adapters.empty()) - return; - for(auto& adapter : adapters) + auto adapters = QBluetoothLocalDevice::allDevices(); + if(!adapters.empty()) { - if(conf.adapter == adapter.address()) + bool found = false; + QString adapter_str = QString::fromStdString(m_conf.adapter); + for(const auto& adapter : adapters) { - m_adapter = adapter; - break; + if(adapter.address().toString() == adapter_str) + { + m_adapterAddress = adapter.address(); + found = true; + break; + } } + if(!found) + m_adapterAddress = adapters[0].address(); } - if(!m_adapter.initialized()) - m_adapter = adapters[0]; - if(m_adapter.initialized()) + // Create discovery agent + if(!m_adapterAddress.isNull()) + m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(m_adapterAddress, this); + else + m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + + if(m_discoveryAgent) { - // Setup our callbacks - m_adapter.set_callback_on_scan_start([] {}); - m_adapter.set_callback_on_scan_stop([] {}); - m_adapter.set_callback_on_scan_found([this](SimpleBLE::Peripheral p) { - boost::asio::post(m_strand, [this] { scan_services(); }); - }); - m_adapter.set_callback_on_scan_updated([this](SimpleBLE::Peripheral p) { - boost::asio::post(m_strand, [this] { scan_services(); }); - }); + m_discoveryAgent->setLowEnergyDiscoveryTimeout(15000); + + QObject::connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &ble_scan_protocol::onDeviceDiscovered); } } -static auto name_or_uuid(const ble_map_type& map, const std::string& uuid) - -> const std::string& +void ble_scan_protocol::onDeviceDiscovered(const QBluetoothDeviceInfo& info) { - if(auto it = map.find(uuid); it != map.end()) - return it->second; - else - return uuid; + if(!should_include_device(info)) + return; + + boost::asio::post(m_strand, [this, info] { scan_services(); }); } -void ble_scan_protocol::scan_services() +bool ble_scan_protocol::should_include_device(const QBluetoothDeviceInfo& info) const noexcept { - if(!m_device) - return; auto& service_names = ble_service_map(); - auto& char_names = ble_characteristic_map(); - auto& desc_names = ble_descriptor_map(); - for(auto peripheral : m_adapter.scan_get_results()) - { - auto services = peripheral.services(); - apply_filters(services); - if(services.empty()) - continue; + // Get service UUIDs from the device + QList serviceUuids = info.serviceUuids(); - std::string periph_name = peripheral.identifier().empty() ? peripheral.address() - : peripheral.identifier(); - ossia::net::sanitize_name(periph_name); - auto& prp_node - = ossia::net::find_or_create_node(m_device->get_root_node(), periph_name); - for(auto& service : peripheral.services()) + // If no filters, include all BLE devices + if(m_conf.filter_exclude.empty() && m_conf.filter_include.empty()) + return true; + + // Check exclude filters + if(!m_conf.filter_exclude.empty()) + { + for(const auto& service_uuid : serviceUuids) { - auto& svc_node = ossia::net::find_or_create_node( - prp_node, name_or_uuid(service_names, service.uuid())); - auto param = svc_node.create_parameter(ossia::val_type::STRING); + std::string uuid_str + = service_uuid.toString(QBluetoothUuid::WithoutBraces).toStdString(); + if(ossia::contains(m_conf.filter_exclude, uuid_str)) + return false; - param->set_value(service.data()); + // Check pretty name too + if(auto it = service_names.find(uuid_str); it != service_names.end()) + { + if(ossia::contains(m_conf.filter_exclude, it->second)) + return false; + } } - ossia::expose_manufacturer_data_as_ossia_nodes( - prp_node, peripheral.manufacturer_data()); } -} -void ble_scan_protocol::apply_filters( - std::vector& services) const noexcept -{ - auto& service_names = ble_service_map(); - - // 1. Filter out the excluded ones - if(!this->m_conf.filter_exclude.empty()) + // Check include filters + if(!m_conf.filter_include.empty()) { - for(auto it = services.begin(); it != services.end();) + bool found = false; + for(const auto& service_uuid : serviceUuids) { - auto& service = *it; - if(ossia::contains(this->m_conf.filter_exclude, service.uuid())) + std::string uuid_str + = service_uuid.toString(QBluetoothUuid::WithoutBraces).toStdString(); + if(ossia::contains(m_conf.filter_include, uuid_str)) { - it = services.erase(it); - continue; + found = true; + break; } - else if(auto pretty_name_it = service_names.find(service.uuid()); - pretty_name_it != service_names.end()) + + // Check pretty name too + if(auto it = service_names.find(uuid_str); it != service_names.end()) { - if(ossia::contains(this->m_conf.filter_exclude, pretty_name_it->second)) + if(ossia::contains(m_conf.filter_include, it->second)) { - it = services.erase(it); - continue; + found = true; + break; } } - ++it; } + return found; } - // 2. Filter out the ones not in the include list - if(!this->m_conf.filter_include.empty()) + return true; +} + +void ble_scan_protocol::scan_services() +{ + if(!m_device || !m_discoveryAgent) + return; + + auto& service_names = ble_service_map(); + + for(const auto& deviceInfo : m_discoveryAgent->discoveredDevices()) { - for(auto it = services.begin(); it != services.end();) + if(!should_include_device(deviceInfo)) + continue; + + QString periph_name + = deviceInfo.name().isEmpty() + ? deviceInfo.address().isNull() + ? deviceInfo.deviceUuid().toString(QBluetoothUuid::WithoutBraces) + : deviceInfo.address().toString() + : deviceInfo.name(); + std::string periph_name_std = periph_name.toStdString(); + ossia::net::sanitize_name(periph_name_std); + + auto& prp_node + = ossia::net::find_or_create_node(m_device->get_root_node(), periph_name_std); + + for(const auto& service_uuid : deviceInfo.serviceUuids()) { - auto& service = *it; - if(!ossia::contains(this->m_conf.filter_include, service.uuid())) - { - it = services.erase(it); - continue; - } - else if(auto pretty_name_it = service_names.find(service.uuid()); - pretty_name_it != service_names.end()) - { - if(!ossia::contains(this->m_conf.filter_exclude, pretty_name_it->second)) - { - it = services.erase(it); - continue; - } - } - ++it; + std::string uuid_str + = service_uuid.toString(QBluetoothUuid::WithoutBraces).toStdString(); + std::string node_name; + if(auto it = service_names.find(uuid_str); it != service_names.end()) + node_name = it->second; + else + node_name = uuid_str; + + auto& svc_node = ossia::net::find_or_create_node(prp_node, node_name); + auto param = svc_node.create_parameter(ossia::val_type::STRING); + param->set_value(uuid_str); } + + ossia::expose_manufacturer_data_as_ossia_nodes(prp_node, deviceInfo.manufacturerData()); } } void ble_scan_protocol::set_device(net::device_base& dev) { m_device = &dev; - m_adapter.scan_start(); + if(m_discoveryAgent) + m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } ble_scan_protocol::~ble_scan_protocol() { - if(m_adapter.initialized()) - m_adapter.scan_stop(); + if(m_discoveryAgent && m_discoveryAgent->isActive()) + m_discoveryAgent->stop(); // FIXME we need to finish the strands before deleting this } diff --git a/BLE/Protocol.hpp b/BLE/Protocol.hpp index b515cd7..a5d7b80 100644 --- a/BLE/Protocol.hpp +++ b/BLE/Protocol.hpp @@ -1,12 +1,16 @@ #pragma once -#include "simpleble/Types.h" #include #include #include #include -#include +#include +#include +#include +#include +#include +#include #include #include @@ -26,7 +30,7 @@ constexpr int special_ble_cbor_id = 0xffff; * Expose the manufacturer data found in BLE advertisements as child nodes of device_node. * This will call expose_cbor_as_ossia_nodes if it detects the special_ble_cbor_id. */ -void expose_manufacturer_data_as_ossia_nodes(ossia::net::node_base& device_node, const std::map& manufacturer_data); +void expose_manufacturer_data_as_ossia_nodes(ossia::net::node_base& device_node, const QMultiHash& manufacturer_data); /** * basically qt's CBOR string reading example. could be replaced by a readAllString() call in qt 6.7 but I'm developing with qt 6.2. @@ -41,9 +45,9 @@ QString read_next_cbor_string(QCborStreamReader& reader); * * In case of error, this may still expose some of the data if there was valid data to expose. */ -bool expose_cbor_as_ossia_nodes(ossia::net::node_base& device_node, const SimpleBLE::ByteArray& cbor_data); +bool expose_cbor_as_ossia_nodes(ossia::net::node_base& device_node, const QByteArray& cbor_data); -class OSSIA_EXPORT ble_protocol final : public ossia::net::protocol_base +class OSSIA_EXPORT ble_protocol final : public QObject, public ossia::net::protocol_base { public: explicit ble_protocol( @@ -60,16 +64,25 @@ class OSSIA_EXPORT ble_protocol final : public ossia::net::protocol_base bool update(net::node_base& node_base) override; void scan_services(); - - SimpleBLE::Adapter m_adapter; - SimpleBLE::Peripheral m_peripheral; + void onDeviceDiscovered(const QBluetoothDeviceInfo& info); + void onDeviceUpdated(const QBluetoothDeviceInfo& info, QBluetoothDeviceInfo::Fields updatedFields); + void onConnected(); + void onDisconnected(); + void onServiceDiscovered(const QBluetoothUuid& service); + void onDiscoveryFinished(); + + QBluetoothDeviceDiscoveryAgent* m_discoveryAgent{}; + QLowEnergyController* m_controller{}; + QBluetoothAddress m_adapterAddress; + QString m_targetSerial; ossia::net::device_base* m_device{}; ossia::net::network_context_ptr m_context; ossia::net::strand_type m_strand; - using ble_param_id = std::pair; + using ble_param_id = std::pair; boost::container::flat_map m_params; + boost::container::flat_map m_services; }; struct ble_scan_configuration @@ -79,7 +92,7 @@ struct ble_scan_configuration std::vector filter_exclude; }; -class OSSIA_EXPORT ble_scan_protocol final : public ossia::net::protocol_base +class OSSIA_EXPORT ble_scan_protocol final : public QObject, public ossia::net::protocol_base { public: explicit ble_scan_protocol( @@ -95,9 +108,11 @@ class OSSIA_EXPORT ble_scan_protocol final : public ossia::net::protocol_base bool update(net::node_base& node_base) override; void scan_services(); - void apply_filters(std::vector& services) const noexcept; + void onDeviceDiscovered(const QBluetoothDeviceInfo& info); + bool should_include_device(const QBluetoothDeviceInfo& info) const noexcept; - SimpleBLE::Adapter m_adapter; + QBluetoothDeviceDiscoveryAgent* m_discoveryAgent{}; + QBluetoothAddress m_adapterAddress; ossia::net::device_base* m_device{}; ossia::net::network_context_ptr m_context; diff --git a/CMakeLists.txt b/CMakeLists.txt index cab047c..3b5aec1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,28 +7,14 @@ endif() project(score_addon_ble LANGUAGES CXX) score_common_setup() -if(WIN32 AND NOT MSVC) - if(OSSIA_SDK) - # For cppwinrt headers that simpleble requires on windows. - if(EXISTS "${OSSIA_SDK}/sysroot/include/winrt") - include_directories("${OSSIA_SDK}/sysroot/include") - else() - find_path(WINRT_FOUNDATION_H_PATH winrt/Windows.Foundation.h) - if(NOT WINRT_FOUNDATION_H_PATH) - return() - endif() - include_directories("${WINRT_FOUNDATION_H_PATH}") - endif() - endif() -endif() - -include(ble.cmake) -if(NOT TARGET simpleble) +# Find Qt Bluetooth +find_package(${QT_VERSION} COMPONENTS Bluetooth) +if(NOT TARGET ${QT_PREFIX}::Bluetooth) + message(STATUS "Qt Bluetooth not found, skipping BLE addon") return() endif() -# Necessary as SimpleBLE overwrites: -include(GenerateStaticExport) +include(ble.cmake) add_library(score_addon_ble "${CMAKE_CURRENT_SOURCE_DIR}/score_addon_ble.hpp" @@ -53,7 +39,9 @@ add_library(score_addon_ble target_link_libraries(score_addon_ble PUBLIC score_plugin_engine - simpleble::simpleble + ${QT_PREFIX}::Bluetooth + $<$:${QT_PREFIX}::QDarwinBluetoothPermissionPlugin> + $<$:RuntimeObject> ) setup_score_plugin(score_addon_ble) diff --git a/ble.cmake b/ble.cmake index c2b0e38..4770834 100644 --- a/ble.cmake +++ b/ble.cmake @@ -1,14 +1,14 @@ -set(LIBFMT_VENDORIZE OFF) -set(SIMPLEBLE_INSTALL OFF) +# set(LIBFMT_VENDORIZE OFF) +# set(SIMPLEBLE_INSTALL OFF) -block() - if(MSVC) - add_compile_options("/w") - else() - add_compile_options("-w") - endif() - add_subdirectory(3rdparty/SimpleBLE/simpleble "${CMAKE_BINARY_DIR}/simpleble-build" SYSTEM) -endblock() +#block() +# if(MSVC) +# add_compile_options("/w") +# else() +# add_compile_options("-w") +# endif() +# add_subdirectory(3rdparty/SimpleBLE/simpleble "${CMAKE_BINARY_DIR}/simpleble-build" SYSTEM) +#endblock() set(ble-database_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/bluetooth-numbers-database/") set(BLE_SERVICE_UUIDS_JSON "${ble-database_SOURCE_DIR}/v1/service_uuids.json") diff --git a/score_addon_ble.cpp b/score_addon_ble.cpp index c082df0..a83bc57 100644 --- a/score_addon_ble.cpp +++ b/score_addon_ble.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include @@ -10,9 +13,23 @@ #include #include +static bool g_bluetooth_allowed = true; score_addon_ble::score_addon_ble() { qRegisterMetaType(); + +#if defined(__APPLE__) + static std::atomic_bool ok{}; + QCoreApplication::instance()->requestPermission( + QBluetoothPermission{}, [](const QPermission& permission) { + g_bluetooth_allowed = permission.status() == Qt::PermissionStatus::Granted; + ok = true; + }); + while(!ok) + { + QCoreApplication::processEvents(); + } +#endif } score_addon_ble::~score_addon_ble() { } @@ -20,6 +37,9 @@ score_addon_ble::~score_addon_ble() { } std::vector score_addon_ble::factories( const score::ApplicationContext& ctx, const score::InterfaceKey& key) const { + if(key != Device::ProtocolFactory::static_interfaceKey()) + return {}; + #if !defined(_WIN32) && !defined(__APPLE__) && !defined(__EMSCRIPTEN__) if(!QFileInfo::exists("/sys/class") || !QFileInfo::exists("/sys/class/bluetooth")) return {}; @@ -27,7 +47,7 @@ std::vector score_addon_ble::factories( try { - if(SimpleBLE::Adapter::bluetooth_enabled()) + if(!QBluetoothLocalDevice::allDevices().isEmpty()) { return instantiate_factories< score::ApplicationContext,