summaryrefslogtreecommitdiff
path: root/src/bt.cc
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-10 10:01:01 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-19 00:13:47 +0200
commitdad0aaa9b33a5a217ac115334a94fe299dce9e08 (patch)
treebaeffb1e3ef6a6e100bc2f255581d77c4b9a45d3 /src/bt.cc
parent86ec0b5386fc2078891a829026844d2ec21ea7db (diff)
Add bluetooth module
Can't really do anything yet, but finds the bluetooth adapter and registers an agent to make it pairable.
Diffstat (limited to 'src/bt.cc')
-rw-r--r--src/bt.cc953
1 files changed, 953 insertions, 0 deletions
diff --git a/src/bt.cc b/src/bt.cc
new file mode 100644
index 0000000..6ab7d1d
--- /dev/null
+++ b/src/bt.cc
@@ -0,0 +1,953 @@
+#include "bt.hh"
+
+#include "cfg.hh"
+#include "logger.hh"
+#include "looper.hh"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <format>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <poll.h>
+#include <sdbus-c++/Error.h>
+#include <sdbus-c++/IConnection.h>
+#include <sdbus-c++/IObject.h>
+#include <sdbus-c++/IProxy.h>
+#include <sdbus-c++/MethodResult.h>
+#include <sdbus-c++/ProxyInterfaces.h>
+#include <sdbus-c++/StandardInterfaces.h>
+#include <sdbus-c++/Types.h>
+#include <sdbus-c++/VTableItems.h>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace bt {
+
+namespace {
+
+class IManager {
+ public:
+ virtual ~IManager() = default;
+
+ IManager(IManager const&) = delete;
+ IManager& operator=(IManager const&) = delete;
+
+ virtual void add_device(
+ sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const& properties) = 0;
+ virtual void add_adapter(
+ sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const& properties) = 0;
+ virtual void add_agent_manager(sdbus::ObjectPath const& path) = 0;
+
+ virtual void remove_device(sdbus::ObjectPath const& path) = 0;
+ virtual void remove_adapter(sdbus::ObjectPath const& path) = 0;
+ virtual void remove_agent_manager(sdbus::ObjectPath const& path) = 0;
+ virtual void remove_agent() = 0;
+
+ virtual void update_adapter(sdbus::ObjectPath const& path) = 0;
+ virtual void update_device(sdbus::ObjectPath const& path) = 0;
+
+ virtual std::vector<Device*> get_devices(
+ sdbus::ObjectPath const& adapter_path) = 0;
+
+ virtual void agent_request_pincode(
+ sdbus::ObjectPath const& device,
+ std::function<void(std::optional<std::string>)> callback) = 0;
+ virtual void agent_display_pincode(sdbus::ObjectPath const& device,
+ std::string const& pincode) = 0;
+ virtual void agent_request_passkey(
+ sdbus::ObjectPath const& device,
+ std::function<void(std::optional<uint32_t>)> callback) = 0;
+ virtual void agent_display_passkey(sdbus::ObjectPath const& device,
+ uint32_t passkey, uint16_t entered) = 0;
+ virtual void agent_request_confirmation(
+ sdbus::ObjectPath const& device, uint32_t passkey,
+ std::function<void(bool)> callback) = 0;
+ virtual void agent_request_authorization(
+ sdbus::ObjectPath const& device, std::function<void(bool)> callback) = 0;
+ virtual void agent_authorize_service(sdbus::ObjectPath const& device,
+ std::string const& uuid,
+ std::function<void(bool)> callback) = 0;
+ virtual void agent_cancel() = 0;
+
+ virtual logger::Logger& logger() = 0;
+
+ protected:
+ IManager() = default;
+};
+
+const sdbus::ServiceName kService{"org.bluez"};
+const sdbus::InterfaceName kDeviceInterface{"org.bluez.Device1"};
+const sdbus::InterfaceName kAdapterInterface{"org.bluez.Adapter1"};
+const sdbus::InterfaceName kAgentInterface{"org.bluez.Agent1"};
+const sdbus::InterfaceName kAgentManagerInterface{"org.bluez.AgentManager1"};
+const sdbus::InterfaceName kPropertiesInterface{
+ "org.freedesktop.DBus.Properties"};
+
+class ObjectManagerProxy {
+ private:
+ static inline sdbus::InterfaceName kInterface{
+ "org.freedesktop.DBus.ObjectManager"};
+
+ public:
+ ObjectManagerProxy(IManager& manager, sdbus::IConnection& connection,
+ sdbus::ServiceName destination, sdbus::ObjectPath path,
+ sdbus::ObjectPath filter)
+ : proxy_(sdbus::createProxy(connection, std::move(destination),
+ std::move(path))),
+ manager_(manager),
+ filter_(std::move(filter)) {
+ proxy_->uponSignal("InterfacesAdded")
+ .onInterface(kInterface)
+ .call([this](
+ sdbus::ObjectPath const& path,
+ std::map<sdbus::InterfaceName,
+ std::map<sdbus::PropertyName, sdbus::Variant>> const&
+ interfaces_and_properties) {
+ interfaces_added(path, interfaces_and_properties);
+ });
+
+ proxy_->uponSignal("InterfacesRemoved")
+ .onInterface(kInterface)
+ .call([this](sdbus::ObjectPath const& path,
+ std::vector<sdbus::InterfaceName> const& interfaces) {
+ this->interfaces_removed(path, interfaces);
+ });
+
+ proxy_->callMethodAsync("GetManagedObjects")
+ .onInterface(kInterface)
+ .uponReplyInvoke(
+ [this](
+ std::optional<sdbus::Error> err,
+ std::map<sdbus::ObjectPath,
+ std::map<sdbus::InterfaceName,
+ std::map<sdbus::PropertyName,
+ sdbus::Variant>>> const& objects) {
+ if (err.has_value()) {
+ if (err->getName() ==
+ "org.freedesktop.DBus.Error.ServiceUnknown") {
+ manager_.logger().info("Waiting for server to start ...");
+ } else {
+ manager_.logger().err(std::format("GetManagedObjects {}: {}",
+ std::string(err->getName()),
+ err->getMessage()));
+ }
+ return;
+ }
+
+ for (auto const& pair : objects) {
+ interfaces_added(pair.first, pair.second);
+ }
+ });
+ }
+
+ ObjectManagerProxy(ObjectManagerProxy const&) = delete;
+ ObjectManagerProxy& operator=(ObjectManagerProxy const&) = delete;
+
+ virtual ~ObjectManagerProxy() { proxy_->unregister(); }
+
+ private:
+ void interfaces_added(
+ sdbus::ObjectPath const& object_path,
+ std::map<sdbus::InterfaceName,
+ std::map<sdbus::PropertyName, sdbus::Variant>> const&
+ interfaces_and_properties) {
+ if (!object_path.starts_with(filter_)) {
+ manager_.logger().dbg(
+ std::format("ignoring: {}", std::string(object_path)));
+ return;
+ }
+
+ auto it = interfaces_and_properties.find(kDeviceInterface);
+ if (it != interfaces_and_properties.end()) {
+ manager_.add_device(object_path, it->second);
+ }
+ it = interfaces_and_properties.find(kAdapterInterface);
+ if (it != interfaces_and_properties.end()) {
+ manager_.add_adapter(object_path, it->second);
+ }
+ it = interfaces_and_properties.find(kAgentManagerInterface);
+ if (it != interfaces_and_properties.end()) {
+ manager_.add_agent_manager(object_path);
+ }
+ }
+
+ void interfaces_removed(sdbus::ObjectPath const& object_path,
+ std::vector<sdbus::InterfaceName> const& interfaces) {
+ if (!object_path.starts_with(filter_))
+ return;
+
+ for (auto const& interface : interfaces) {
+ if (interface == kDeviceInterface) {
+ manager_.remove_device(object_path);
+ } else if (interface == kAdapterInterface) {
+ manager_.remove_adapter(object_path);
+ } else if (interface == kAgentManagerInterface) {
+ manager_.remove_agent_manager(object_path);
+ }
+ }
+ }
+
+ std::unique_ptr<sdbus::IProxy> proxy_;
+ IManager& manager_;
+ sdbus::ObjectPath filter_;
+};
+
+class BaseProxy : public sdbus::ProxyInterfaces<sdbus::Properties_proxy> {
+ public:
+ virtual ~BaseProxy() { unregisterProxy(); }
+
+ protected:
+ BaseProxy(sdbus::IConnection& connection, sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, size_t> const& names)
+ : sdbus::ProxyInterfaces<sdbus::Properties_proxy>(connection, kService,
+ path),
+ names_(names) {}
+
+ virtual void update(size_t field, sdbus::Variant const& value) = 0;
+ virtual void notify_updated() = 0;
+
+ void init(std::map<sdbus::PropertyName, sdbus::Variant> const& properties) {
+ for (auto const& pair : properties) {
+ auto it = names_.find(pair.first);
+ if (it != names_.end()) {
+ update(it->second, pair.second);
+ }
+ }
+
+ registerProxy();
+ }
+
+ private:
+ void onPropertiesChanged(
+ const sdbus::InterfaceName& interface_name,
+ const std::map<sdbus::PropertyName, sdbus::Variant>& changed,
+ const std::vector<sdbus::PropertyName>& invalidated) override {
+ if (interface_name != kAdapterInterface)
+ return;
+ ++waiting_;
+ for (auto const& pair : changed) {
+ auto it = names_.find(pair.first);
+ if (it != names_.end()) {
+ updated_ = true;
+ update(it->second, pair.second);
+ }
+ }
+ for (auto const& property_name : invalidated) {
+ auto it = names_.find(property_name);
+ if (it != names_.end()) {
+ ++waiting_;
+ GetAsync(interface_name, property_name,
+ // NOLINTNEXTLINE(performance-unnecessary-value-param)
+ [this, field = it->second](std::optional<sdbus::Error> err,
+ sdbus::Variant const& value) {
+ if (!err.has_value()) {
+ updated_ = true;
+ update(field, value);
+ }
+ if (--waiting_ == 0 && updated_) {
+ notify_updated();
+ }
+ });
+ }
+ }
+ if (--waiting_ == 0 && updated_) {
+ notify_updated();
+ }
+ }
+
+ std::map<sdbus::PropertyName, size_t> const& names_;
+ uint8_t waiting_{0};
+ bool updated_{false};
+};
+
+class DeviceProxy : public BaseProxy, public Device {
+ private:
+ static inline const std::map<sdbus::PropertyName, size_t> kNames{
+ {sdbus::PropertyName{"Address"}, 0}, {sdbus::PropertyName{"Name"}, 1},
+ {sdbus::PropertyName{"Alias"}, 2}, {sdbus::PropertyName{"Paired"}, 3},
+ {sdbus::PropertyName{"Connected"}, 4}, {sdbus::PropertyName{"Adapter"}, 5},
+ };
+
+ public:
+ DeviceProxy(IManager& manager, sdbus::IConnection& connection,
+ sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const& properties)
+ : BaseProxy(connection, path, kNames), manager_(manager) {
+ init(properties);
+ }
+
+ [[nodiscard]]
+ std::string const& address() const override {
+ return address_;
+ }
+
+ [[nodiscard]]
+ std::string const& name() const override {
+ if (alias_.has_value() && !alias_->empty())
+ return alias_.value();
+ if (name_.has_value() && !name_->empty())
+ return name_.value();
+ return address_;
+ }
+
+ [[nodiscard]]
+ bool paired() const override {
+ return paired_;
+ }
+
+ [[nodiscard]]
+ bool connected() const override {
+ return connected_;
+ }
+
+ [[nodiscard]]
+ sdbus::ObjectPath const& adapter() const {
+ return adapter_;
+ }
+
+ private:
+ void update(size_t field, sdbus::Variant const& value) override {
+ switch (field) {
+ case 0:
+ address_ = value.get<std::string>();
+ break;
+ case 1:
+ name_ = value.isEmpty()
+ ? std::nullopt
+ : std::optional<std::string>(value.get<std::string>());
+ break;
+ case 2:
+ alias_ = value.isEmpty()
+ ? std::nullopt
+ : std::optional<std::string>(value.get<std::string>());
+ break;
+ case 3:
+ paired_ = value.get<bool>();
+ break;
+ case 4:
+ connected_ = value.get<bool>();
+ break;
+ case 5:
+ adapter_ = value.get<std::string>();
+ break;
+ default:
+ std::unreachable();
+ }
+ }
+
+ void notify_updated() override {
+ manager_.update_device(getProxy().getObjectPath());
+ }
+
+ IManager& manager_;
+ std::string address_;
+ std::optional<std::string> name_;
+ std::optional<std::string> alias_;
+ bool connected_{false};
+ bool paired_{false};
+ sdbus::ObjectPath adapter_;
+};
+
+class AdapterProxy : public BaseProxy, public Adapter {
+ private:
+ static inline const std::map<sdbus::PropertyName, size_t> kNames{
+ {sdbus::PropertyName{"Address"}, 0},
+ {sdbus::PropertyName{"Name"}, 1},
+ {sdbus::PropertyName{"Alias"}, 2},
+ {sdbus::PropertyName{"Discoverable"}, 3},
+ {sdbus::PropertyName{"Pairable"}, 4},
+ {sdbus::PropertyName{"Pairing"}, 5},
+ };
+
+ public:
+ AdapterProxy(IManager& manager, sdbus::IConnection& connection,
+ sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const& properties)
+ : BaseProxy(connection, path, kNames), manager_(manager) {
+ init(properties);
+ }
+
+ [[nodiscard]]
+ std::string const& address() const override {
+ return address_;
+ }
+
+ [[nodiscard]]
+ std::string const& name() const override {
+ if (!alias_.empty())
+ return alias_;
+ if (!name_.empty())
+ return name_;
+ return address_;
+ }
+
+ [[nodiscard]]
+ bool discoverable() const override {
+ return discoverable_;
+ }
+
+ [[nodiscard]]
+ bool pairable() const override {
+ return pairable_;
+ }
+
+ [[nodiscard]]
+ bool pairing() const override {
+ return pairing_;
+ }
+
+ [[nodiscard]]
+ std::vector<Device*> devices() const override {
+ return manager_.get_devices(getProxy().getObjectPath());
+ }
+
+ private:
+ void update(size_t field, sdbus::Variant const& value) override {
+ switch (field) {
+ case 0:
+ address_ = value.get<std::string>();
+ break;
+ case 1:
+ name_ = value.get<std::string>();
+ break;
+ case 2:
+ alias_ = value.get<std::string>();
+ break;
+ case 3:
+ discoverable_ = value.get<bool>();
+ break;
+ case 4:
+ pairable_ = value.get<bool>();
+ manager_.logger().dbg(std::format("pairable: {}", pairable_));
+ break;
+ case 5:
+ pairing_ = value.get<bool>();
+ break;
+ default:
+ std::unreachable();
+ }
+ }
+
+ void notify_updated() override {
+ manager_.update_adapter(getProxy().getObjectPath());
+ }
+
+ IManager& manager_;
+ std::string address_;
+ std::string name_;
+ std::string alias_;
+ bool discoverable_{false};
+ bool pairable_{false};
+ bool pairing_{false};
+};
+
+class AgentManagerProxy {
+ public:
+ AgentManagerProxy(sdbus::IConnection& connection,
+ sdbus::ObjectPath const& path)
+ : proxy_(sdbus::createProxy(connection, kService, path)) {}
+
+ void register_agent(
+ sdbus::ObjectPath const& path, std::string const& capability,
+ std::function<void(std::optional<sdbus::Error> err)> callback) {
+ proxy_->callMethodAsync("RegisterAgent")
+ .onInterface(kAgentManagerInterface)
+ .withArguments(path, capability)
+ .uponReplyInvoke(std::move(callback));
+ }
+
+ void unregister_agent_sync(sdbus::ObjectPath const& path) {
+ proxy_->callMethod("UnregisterAgent")
+ .onInterface(kAgentManagerInterface)
+ .withArguments(path);
+ }
+
+ [[nodiscard]]
+ sdbus::ObjectPath const& object_path() const {
+ return proxy_->getObjectPath();
+ }
+
+ private:
+ std::unique_ptr<sdbus::IProxy> proxy_;
+};
+
+class AgentObject {
+ private:
+ static inline const sdbus::ObjectPath kObjectPath{"/org/bluez/agent"};
+ static inline const sdbus::Error::Name kErrorCanceled{
+ "org.bluez.Error.Canceled"};
+ static inline const sdbus::Error::Name kErrorRejected{
+ "org.bluez.Error.Rejected"};
+
+ public:
+ AgentObject(IManager& manager, sdbus::IConnection& connection)
+ : manager_(manager),
+ object_(sdbus::createObject(connection, kObjectPath)) {
+ object_
+ ->addVTable(
+ sdbus::registerMethod("Release").withNoReply().implementedAs(
+ [this]() { manager_.remove_agent(); }),
+ sdbus::registerMethod("RequestPinCode")
+ .withInputParamNames("device")
+ .withOutputParamNames("pincode")
+ .implementedAs([this](sdbus::Result<std::string>&& result,
+ sdbus::ObjectPath const& device) {
+ cancel_pending();
+ pending_pincode_ = std::move(result);
+ manager_.agent_request_pincode(device, [this](auto pincode) {
+ if (!pending_pincode_.has_value())
+ return;
+ if (pincode.has_value()) {
+ pending_pincode_->returnResults(pincode.value());
+ } else {
+ pending_pincode_->returnError(
+ sdbus::Error{kErrorRejected});
+ }
+ pending_pincode_ = std::nullopt;
+ });
+ }),
+ sdbus::registerMethod("DisplayPinCode")
+ .withInputParamNames("device", "pincode")
+ .implementedAs([this](sdbus::ObjectPath const& device,
+ std::string const& pincode) {
+ manager_.agent_display_pincode(device, pincode);
+ }),
+ sdbus::registerMethod("RequestPasskey")
+ .withInputParamNames("device")
+ .withOutputParamNames("passkey")
+ .implementedAs([this](sdbus::Result<uint32_t>&& result,
+ sdbus::ObjectPath const& device) {
+ cancel_pending();
+ pending_passkey_ = std::move(result);
+ manager_.agent_request_passkey(device, [this](auto passkey) {
+ if (!pending_passkey_.has_value())
+ return;
+ if (passkey.has_value()) {
+ pending_passkey_->returnResults(passkey.value());
+ } else {
+ pending_passkey_->returnError(
+ sdbus::Error{kErrorRejected});
+ }
+ pending_passkey_ = std::nullopt;
+ });
+ }),
+ sdbus::registerMethod("DisplayPasskey")
+ .withInputParamNames("device", "passkey", "entered")
+ .implementedAs([this](sdbus::ObjectPath const& device,
+ uint32_t passkey, uint16_t entered) {
+ manager_.agent_display_passkey(device, passkey, entered);
+ }),
+ sdbus::registerMethod("RequestConfirmation")
+ .withInputParamNames("device", "passkey")
+ .withOutputParamNames("result")
+ .implementedAs([this](sdbus::Result<sdbus::Variant>&& result,
+ sdbus::ObjectPath const& device,
+ uint32_t passkey) {
+ cancel_pending();
+ pending_confirm_ = std::move(result);
+ manager_.agent_request_confirmation(
+ device, passkey, [this](auto ok) {
+ if (!pending_confirm_.has_value())
+ return;
+ if (ok) {
+ pending_confirm_->returnResults({});
+ } else {
+ pending_confirm_->returnError(
+ sdbus::Error{kErrorRejected});
+ }
+ pending_confirm_ = std::nullopt;
+ });
+ }),
+ sdbus::registerMethod("RequestAuthorization")
+ .withInputParamNames("device")
+ .withOutputParamNames("result")
+ .implementedAs([this](sdbus::Result<sdbus::Variant>&& result,
+ sdbus::ObjectPath const& device) {
+ cancel_pending();
+ pending_confirm_ = std::move(result);
+ manager_.agent_request_authorization(device, [this](auto ok) {
+ if (!pending_confirm_.has_value())
+ return;
+ if (ok) {
+ pending_confirm_->returnResults({});
+ } else {
+ pending_confirm_->returnError(
+ sdbus::Error{kErrorRejected});
+ }
+ pending_confirm_ = std::nullopt;
+ });
+ }),
+ sdbus::registerMethod("AuthorizeService")
+ .withInputParamNames("device", "uuid")
+ .withOutputParamNames("result")
+ .implementedAs([this](sdbus::Result<sdbus::Variant>&& result,
+ sdbus::ObjectPath const& device,
+ std::string const& uuid) {
+ cancel_pending();
+ pending_confirm_ = std::move(result);
+ manager_.agent_authorize_service(
+ device, uuid, [this](auto ok) {
+ if (!pending_confirm_.has_value())
+ return;
+ if (ok) {
+ pending_confirm_->returnResults({});
+ } else {
+ pending_confirm_->returnError(
+ sdbus::Error{kErrorRejected});
+ }
+ pending_confirm_ = std::nullopt;
+ });
+ }),
+ sdbus::registerMethod("Cancel").withNoReply().implementedAs(
+ [this]() {
+ cancel_pending();
+ manager_.agent_cancel();
+ }))
+ .forInterface(kAgentInterface);
+ }
+
+ [[nodiscard]]
+ sdbus::ObjectPath const& object_path() const {
+ return object_->getObjectPath();
+ }
+
+ private:
+ void cancel_pending() {
+ if (pending_pincode_.has_value()) {
+ pending_pincode_->returnError(sdbus::Error{kErrorCanceled});
+ pending_pincode_ = std::nullopt;
+ }
+ if (pending_passkey_.has_value()) {
+ pending_passkey_->returnError(sdbus::Error{kErrorCanceled});
+ pending_passkey_ = std::nullopt;
+ }
+ if (pending_confirm_.has_value()) {
+ pending_confirm_->returnError(sdbus::Error{kErrorCanceled});
+ pending_confirm_ = std::nullopt;
+ }
+ }
+
+ IManager& manager_;
+ std::unique_ptr<sdbus::IObject> object_;
+
+ std::optional<sdbus::Result<std::string>> pending_pincode_;
+ std::optional<sdbus::Result<uint32_t>> pending_passkey_;
+ std::optional<sdbus::Result<sdbus::Variant>> pending_confirm_;
+};
+
+class ManagerImpl : public Manager, public IManager, looper::Hook {
+ public:
+ ManagerImpl(logger::Logger& logger, cfg::Config const& cfg,
+ looper::Looper& looper, Manager::Delegate& delegate)
+ : logger_(logger),
+ cfg_(cfg),
+ looper_(looper),
+ delegate_(delegate),
+ conn_(sdbus::createSystemBusConnection()) {
+ looper_.add_hook(this);
+
+ root_proxy_ = std::make_unique<ObjectManagerProxy>(
+ *this, *conn_, kService, sdbus::ObjectPath{"/"},
+ sdbus::ObjectPath{"/org/bluez"});
+ }
+
+ ~ManagerImpl() override {
+ if (agent_ && agent_manager_proxy_) {
+ agent_manager_proxy_->unregister_agent_sync(agent_->object_path());
+ }
+ looper_.remove_hook(this);
+ }
+
+ Adapter* adapter() override {
+ if (primary_adapter_.empty())
+ return nullptr;
+ return adapters_.at(primary_adapter_).get();
+ }
+
+ private:
+ void add_device(sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const&
+ properties) override {
+ logger_.dbg(std::format("Add device: {}", std::string(path)));
+ auto [it, inserted] = devices_.emplace(
+ path, std::make_unique<DeviceProxy>(*this, *conn_, path, properties));
+
+ auto& device = *it->second;
+ if (device.adapter() == primary_adapter_) {
+ delegate_.added_device(device);
+ }
+ }
+
+ void add_adapter(sdbus::ObjectPath const& path,
+ std::map<sdbus::PropertyName, sdbus::Variant> const&
+ properties) override {
+ logger_.dbg(std::format("Add adapter: {}", std::string(path)));
+ adapters_.emplace(
+ path, std::make_unique<AdapterProxy>(*this, *conn_, path, properties));
+
+ if (primary_adapter_.empty()) {
+ update_primary_adapter();
+ }
+ }
+
+ void add_agent_manager(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Add agent manager: {}", std::string(path)));
+ if (agent_manager_proxy_)
+ return;
+
+ agent_manager_proxy_ = std::make_unique<AgentManagerProxy>(*conn_, path);
+ agent_ = std::make_unique<AgentObject>(*this, *conn_);
+ agent_manager_proxy_->register_agent(
+ agent_->object_path(), "", [this](std::optional<sdbus::Error> err) {
+ if (err.has_value()) {
+ agent_.reset();
+ logger_.warn(std::format("Failed to register agent: {} {}",
+ std::string(err->getName()),
+ err->getMessage()));
+ } else {
+ logger_.info("Registered agent");
+ }
+ });
+ }
+
+ void remove_device(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Remove device: {}", std::string(path)));
+ auto it = devices_.find(path);
+ if (it == devices_.end())
+ return;
+
+ auto& device = *it->second;
+ if (device.adapter() == primary_adapter_) {
+ auto address = device.address();
+ devices_.erase(it);
+ delegate_.removed_device(address);
+ } else {
+ devices_.erase(it);
+ }
+ }
+
+ void remove_adapter(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Remove adapter: {}", std::string(path)));
+ auto it = adapters_.find(path);
+ if (it != adapters_.end())
+ adapters_.erase(it);
+
+ if (path == primary_adapter_) {
+ update_primary_adapter();
+ }
+ }
+
+ void remove_agent_manager(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Remove agemtn manager: {}", std::string(path)));
+
+ if (!agent_manager_proxy_)
+ return;
+ if (agent_manager_proxy_->object_path() != path)
+ return;
+
+ agent_manager_proxy_.reset();
+ agent_.reset();
+ }
+
+ void update_device(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Update device: {}", std::string(path)));
+
+ auto& device = *devices_.at(path);
+ if (device.adapter() == primary_adapter_) {
+ delegate_.updated_device(device);
+ }
+ }
+
+ void update_adapter(sdbus::ObjectPath const& path) override {
+ logger_.dbg(std::format("Update adapter: {}", std::string(path)));
+
+ if (path == primary_adapter_) {
+ delegate_.updated_adapter(*adapters_.at(path));
+ }
+ }
+
+ std::vector<Device*> get_devices(
+ sdbus::ObjectPath const& adapter_path) override {
+ std::vector<Device*> ret;
+ ret.reserve(devices_.size());
+ for (auto& pair : devices_) {
+ if (pair.second->adapter() == adapter_path) {
+ ret.push_back(pair.second.get());
+ }
+ }
+ return ret;
+ }
+
+ [[nodiscard]]
+ logger::Logger& logger() override {
+ return logger_;
+ }
+
+ void remove_agent() override {
+ logger_.info("Remove agent");
+ agent_ = nullptr;
+ }
+
+ void agent_request_pincode(
+ sdbus::ObjectPath const& device,
+ std::function<void(std::optional<std::string>)> callback) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_) {
+ callback(std::nullopt);
+ return;
+ }
+ delegate_.agent_request_pincode(*it->second, std::move(callback));
+ }
+
+ void agent_display_pincode(sdbus::ObjectPath const& device,
+ std::string const& pincode) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_)
+ return;
+ delegate_.agent_display_pincode(*it->second, pincode);
+ }
+
+ void agent_request_passkey(
+ sdbus::ObjectPath const& device,
+ std::function<void(std::optional<uint32_t>)> callback) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_) {
+ callback(std::nullopt);
+ return;
+ }
+ delegate_.agent_request_passkey(*it->second, std::move(callback));
+ }
+
+ void agent_display_passkey(sdbus::ObjectPath const& device, uint32_t passkey,
+ uint16_t entered) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_)
+ return;
+ delegate_.agent_display_passkey(*it->second, passkey, entered);
+ }
+
+ void agent_request_confirmation(sdbus::ObjectPath const& device,
+ uint32_t passkey,
+ std::function<void(bool)> callback) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_) {
+ callback(false);
+ return;
+ }
+ delegate_.agent_request_confirmation(*it->second, passkey,
+ std::move(callback));
+ }
+
+ void agent_request_authorization(
+ sdbus::ObjectPath const& device,
+ std::function<void(bool)> callback) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_) {
+ callback(false);
+ return;
+ }
+ delegate_.agent_request_authorization(*it->second, std::move(callback));
+ }
+
+ void agent_authorize_service(sdbus::ObjectPath const& device,
+ std::string const& uuid,
+ std::function<void(bool)> callback) override {
+ auto it = devices_.find(device);
+ if (it == devices_.end() || it->second->adapter() != primary_adapter_) {
+ callback(false);
+ return;
+ }
+ delegate_.agent_authorize_service(*it->second, uuid, std::move(callback));
+ }
+
+ void agent_cancel() override { delegate_.agent_cancel(); }
+
+ void before(looper::HookHelper& helper) override {
+ auto data = conn_->getEventLoopPollData();
+
+ uint8_t events = 0;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ if (data.events & (POLLIN | POLLPRI | POLLHUP))
+ events |= looper::EVENT_READ;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ if (data.events & POLLOUT)
+ events |= looper::EVENT_WRITE;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ if (data.events & (POLLERR | POLLNVAL))
+ events |= looper::EVENT_ERROR;
+
+ if (data.fd != -1 && events != 0)
+ helper.monitor(data.fd, events);
+
+ helper.monitor(data.eventFd, looper::EVENT_READ);
+
+ auto timeout = data.getRelativeTimeout();
+ if (timeout != std::chrono::microseconds::max()) {
+ helper.timeout(
+ std::chrono::duration_cast<std::chrono::milliseconds>(timeout));
+ }
+ }
+
+ void after() override {
+ while (conn_->processPendingEvent())
+ ;
+ }
+
+ void update_primary_adapter() {
+ bool const was_empty = primary_adapter_.empty();
+ primary_adapter_.clear();
+
+ auto cfg_adapter = cfg_.get("bluetooth.adapter");
+ if (cfg_adapter.has_value()) {
+ for (auto const& pair : adapters_) {
+ if (pair.second->address() == cfg_adapter.value()) {
+ primary_adapter_ = pair.first;
+ delegate_.new_adapter(pair.second.get());
+ return;
+ }
+ }
+ } else {
+ // Pick first (map is sorted) that is pairable.
+ for (auto const& pair : adapters_) {
+ if (pair.second->pairable()) {
+ primary_adapter_ = pair.first;
+ delegate_.new_adapter(pair.second.get());
+ return;
+ }
+ }
+ }
+
+ if (!was_empty)
+ delegate_.new_adapter(nullptr);
+ }
+
+ logger::Logger& logger_;
+ cfg::Config const& cfg_;
+ looper::Looper& looper_;
+ Manager::Delegate& delegate_;
+ std::unique_ptr<sdbus::IConnection> conn_;
+ std::unique_ptr<ObjectManagerProxy> root_proxy_;
+ std::unique_ptr<AgentManagerProxy> agent_manager_proxy_;
+ std::unique_ptr<AgentObject> agent_;
+ std::map<sdbus::ObjectPath, std::unique_ptr<DeviceProxy>> devices_;
+ std::map<sdbus::ObjectPath, std::unique_ptr<AdapterProxy>> adapters_;
+ sdbus::ObjectPath primary_adapter_;
+};
+
+} // namespace
+
+std::unique_ptr<Manager> create_manager(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper,
+ Manager::Delegate& delegate) {
+ return std::make_unique<ManagerImpl>(logger, cfg, looper, delegate);
+}
+
+} // namespace bt