diff --git a/CMakeLists.txt b/CMakeLists.txt index bdae9af..8c3923c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,9 @@ set(SOURCES qbledescriptor.cpp qbledevice.cpp qbleservice.cpp + qblelocalcharacteristic.cpp + qblelocalservice.cpp + qblelocalapplication.cpp ) set(HEADERS @@ -49,6 +52,9 @@ set(HEADERS qbledescriptor.h qbledevice.h qbleservice.h + qblelocalcharacteristic.h + qblelocalservice.h + qblelocalapplication.h ) target_sources(qble PUBLIC ${SOURCES} ${HEADERS}) diff --git a/qblelocalapplication.cpp b/qblelocalapplication.cpp new file mode 100644 index 0000000..65fcf54 --- /dev/null +++ b/qblelocalapplication.cpp @@ -0,0 +1,114 @@ +#include "qblelocalapplication.h" +#include "qblelocalservice.h" +#include "qblelocalcharacteristic.h" + +#include +#include +#include +#include +#include + +static const char *BLUEZ_SERVICE = "org.bluez"; +static const char *GATT_MANAGER_IFACE = "org.bluez.GattManager1"; +static const char *GATT_SERVICE_IFACE = "org.bluez.GattService1"; +static const char *GATT_CHRC_IFACE = "org.bluez.GattCharacteristic1"; + +QBLELocalApplication::QBLELocalApplication(const QString &path, QObject *parent) + : QObject(parent), m_path(path), m_bus(QDBusConnection::systemBus()) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + if (!m_bus.registerObject(m_path, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllProperties)) { + qCritical() << Q_FUNC_INFO << "Failed to register GATT application object at" << m_path << ":" << m_bus.lastError().message(); + } else { + qDebug() << Q_FUNC_INFO << "GATT application registered at" << m_path; + } +} + +QBLELocalApplication::~QBLELocalApplication() +{ + m_bus.unregisterObject(m_path); +} + +QDBusObjectPath QBLELocalApplication::path() const +{ + return QDBusObjectPath(m_path); +} + +void QBLELocalApplication::addService(QBLELocalService *service) +{ + m_services.append(service); +} + +bool QBLELocalApplication::registerWithAdapter(const QString &adapterPath) +{ + qDebug() << Q_FUNC_INFO << "Registering GATT application with adapter" << adapterPath; + + QDBusInterface gattManager(BLUEZ_SERVICE, adapterPath, GATT_MANAGER_IFACE, m_bus); + if (!gattManager.isValid()) { + qWarning() << Q_FUNC_INFO << "GattManager1 not available on" << adapterPath << ":" << gattManager.lastError().message(); + return false; + } + + QDBusPendingCall reply = gattManager.asyncCall( + QStringLiteral("RegisterApplication"), + QVariant::fromValue(path()), + QVariantMap()); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [adapterPath](QDBusPendingCallWatcher *w) { + QDBusPendingReply<> reply = *w; + if (reply.isError()) { + qWarning() << "GATT RegisterApplication failed on" << adapterPath << ":" << reply.error().message(); + } else { + qInfo() << "GATT application successfully registered with" << adapterPath; + } + w->deleteLater(); + }); + + return true; +} + +void QBLELocalApplication::unregisterFromAdapter(const QString &adapterPath) +{ + qDebug() << Q_FUNC_INFO << "Unregistering GATT application from" << adapterPath; + + QDBusInterface gattManager(BLUEZ_SERVICE, adapterPath, GATT_MANAGER_IFACE, m_bus); + if (gattManager.isValid()) { + gattManager.asyncCall(QStringLiteral("UnregisterApplication"), QVariant::fromValue(path())); + } +} + +ManagedObjectList QBLELocalApplication::GetManagedObjects() +{ + ManagedObjectList result; + + for (QBLELocalService *svc : m_services) { + // --- Service entry --- + InterfaceList svcInterfaces; + QVariantMap svcProps; + svcProps.insert(QStringLiteral("UUID"), svc->uuid()); + svcProps.insert(QStringLiteral("Primary"), svc->primary()); + svcProps.insert(QStringLiteral("Characteristics"), + QVariant::fromValue(svc->characteristicPaths())); + svcInterfaces.insert(QLatin1String(GATT_SERVICE_IFACE), svcProps); + result.insert(svc->path(), svcInterfaces); + + // --- Characteristic entries --- + for (QBLELocalCharacteristic *ch : svc->characteristics()) { + InterfaceList chInterfaces; + QVariantMap chProps; + chProps.insert(QStringLiteral("Service"), QVariant::fromValue(ch->servicePath())); + chProps.insert(QStringLiteral("UUID"), ch->uuid()); + chProps.insert(QStringLiteral("Flags"), ch->flags()); + chProps.insert(QStringLiteral("Descriptors"), + QVariant::fromValue(ch->descriptorPaths())); + chInterfaces.insert(QLatin1String(GATT_CHRC_IFACE), chProps); + result.insert(ch->path(), chInterfaces); + } + } + + return result; +} diff --git a/qblelocalapplication.h b/qblelocalapplication.h new file mode 100644 index 0000000..867f8e7 --- /dev/null +++ b/qblelocalapplication.h @@ -0,0 +1,69 @@ +#ifndef QBLELOCALAPPLICATION_H +#define QBLELOCALAPPLICATION_H + +#include +#include +#include +#include + +class QBLELocalService; +class QBLELocalCharacteristic; + +// D-Bus type aliases required by the BlueZ ObjectManager interface +typedef QMap InterfaceList; +typedef QMap ManagedObjectList; + +Q_DECLARE_METATYPE(InterfaceList) +Q_DECLARE_METATYPE(ManagedObjectList) + +/** + * @brief GATT application root that implements org.freedesktop.DBus.ObjectManager. + * + * BlueZ calls GetManagedObjects() on this object to enumerate all GATT services + * and characteristics offered by the application. + * + * Usage: + * 1. Create a QBLELocalApplication. + * 2. Add QBLELocalService instances with addService(). + * 3. Call registerWithAdapter() with the BlueZ adapter D-Bus path. + * 4. BlueZ will then advertise and handle the registered services. + * + * The application is registered on the system D-Bus at the path provided in the + * constructor (default: "/uk/co/piggz/amazfish/gatt"). + */ +class QBLELocalApplication : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.DBus.ObjectManager") + +public: + explicit QBLELocalApplication(const QString &path = QStringLiteral("/uk/co/piggz/amazfish/gatt"), QObject *parent = nullptr); + ~QBLELocalApplication() override; + + QDBusObjectPath path() const; + void addService(QBLELocalService *service); + + /** + * Register this application with the BlueZ GattManager1 on the given adapter. + * @param adapterPath e.g. "/org/bluez/hci0" from AmazfishConfig::localAdapter() + * @return true on success + */ + bool registerWithAdapter(const QString &adapterPath); + + /** + * Unregister this application from the BlueZ GattManager1. + * Call this on shutdown or when the adapter disappears. + */ + void unregisterFromAdapter(const QString &adapterPath); + +public Q_SLOTS: + // Called by BlueZ to enumerate all GATT objects managed by this application + ManagedObjectList GetManagedObjects(); + +private: + QString m_path; + QList m_services; + QDBusConnection m_bus; +}; + +#endif // QBLELOCALAPPLICATION_H diff --git a/qblelocalcharacteristic.cpp b/qblelocalcharacteristic.cpp new file mode 100644 index 0000000..4c248f8 --- /dev/null +++ b/qblelocalcharacteristic.cpp @@ -0,0 +1,60 @@ +#include "qblelocalcharacteristic.h" +#include "qblelocalservice.h" + +#include + +QBLELocalCharacteristic::QBLELocalCharacteristic(QDBusConnection bus, unsigned int index, const QString &uuid, const QStringList &flags, QBLELocalService *service, QObject *parent) + : QObject(parent), m_uuid(uuid), m_flags(flags), m_service(service) +{ + m_path = service->path().path() + QStringLiteral("/char") + QString::number(index); + bus.registerObject(m_path, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllProperties); +} + +QDBusObjectPath QBLELocalCharacteristic::path() const +{ + return QDBusObjectPath(m_path); +} + +QDBusObjectPath QBLELocalCharacteristic::servicePath() const +{ + return m_service->path(); +} + +QString QBLELocalCharacteristic::uuid() const +{ + return m_uuid; +} + +QStringList QBLELocalCharacteristic::flags() const +{ + return m_flags; +} + +QList QBLELocalCharacteristic::descriptorPaths() const +{ + return {}; +} + +// Default implementations — subclasses override what they need + +QByteArray QBLELocalCharacteristic::ReadValue(const QVariantMap &) +{ + qWarning() << Q_FUNC_INFO << "Default ReadValue called — override in subclass"; + return QByteArray(); +} + +void QBLELocalCharacteristic::WriteValue(const QByteArray &value, const QVariantMap &) +{ + qDebug() << Q_FUNC_INFO << "Default WriteValue — emitting valueWritten"; + emit valueWritten(value); +} + +void QBLELocalCharacteristic::StartNotify() +{ + qDebug() << Q_FUNC_INFO << "StartNotify called"; +} + +void QBLELocalCharacteristic::StopNotify() +{ + qDebug() << Q_FUNC_INFO << "StopNotify called"; +} diff --git a/qblelocalcharacteristic.h b/qblelocalcharacteristic.h new file mode 100644 index 0000000..fa8ddd2 --- /dev/null +++ b/qblelocalcharacteristic.h @@ -0,0 +1,52 @@ +#ifndef QBLELOCALCHARACTERISTIC_H +#define QBLELOCALCHARACTERISTIC_H + +#include +#include + +// Forward declaration +class QBLELocalService; + +/** + * @brief Server-side GATT characteristic exposed over D-Bus using the BlueZ GATT API. + * + * Implements org.bluez.GattCharacteristic1. Subclass this and override + * ReadValue() and/or WriteValue() to respond to requests from the remote + * BLE device (e.g. an InfiniTime watch acting as GATT client). + */ +class QBLELocalCharacteristic : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.bluez.GattCharacteristic1") + Q_PROPERTY(QDBusObjectPath Service READ servicePath) + Q_PROPERTY(QString UUID READ uuid) + Q_PROPERTY(QStringList Flags READ flags) + Q_PROPERTY(QList Descriptors READ descriptorPaths) + +public: + explicit QBLELocalCharacteristic(QDBusConnection bus, unsigned int index, const QString &uuid, const QStringList &flags, QBLELocalService *service, QObject *parent = nullptr); + + QDBusObjectPath path() const; + QDBusObjectPath servicePath() const; + QString uuid() const; + QStringList flags() const; + QList descriptorPaths() const; // empty for now + +Q_SIGNALS: + void valueWritten(const QByteArray &value); + +public Q_SLOTS: + // BlueZ GATT server callbacks — override in subclass as needed + virtual QByteArray ReadValue(const QVariantMap &options); + virtual void WriteValue(const QByteArray &value, const QVariantMap &options); + virtual void StartNotify(); + virtual void StopNotify(); + +private: + QString m_path; + QString m_uuid; + QStringList m_flags; + QBLELocalService *m_service; +}; + +#endif // QBLELOCALCHARACTERISTIC_H diff --git a/qblelocalservice.cpp b/qblelocalservice.cpp new file mode 100644 index 0000000..83d8402 --- /dev/null +++ b/qblelocalservice.cpp @@ -0,0 +1,45 @@ +#include "qblelocalservice.h" +#include "qblelocalcharacteristic.h" + +#include + +QBLELocalService::QBLELocalService(QDBusConnection bus, unsigned int index, const QString &uuid, const QString &appPath, QObject *parent) + : QObject(parent), m_uuid(uuid) +{ + m_path = appPath + QStringLiteral("/service") + QString::number(index); + bus.registerObject(m_path, this, QDBusConnection::ExportAdaptors | QDBusConnection::ExportAllProperties); +} + +QDBusObjectPath QBLELocalService::path() const +{ + return QDBusObjectPath(m_path); +} + +void QBLELocalService::addCharacteristic(QBLELocalCharacteristic *ch) +{ + m_characteristics.append(ch); +} + +QList QBLELocalService::characteristicPaths() const +{ + QList result; + for (QBLELocalCharacteristic *ch : m_characteristics) { + result.append(ch->path()); + } + return result; +} + +QList QBLELocalService::characteristics() const +{ + return m_characteristics; +} + +QString QBLELocalService::uuid() const +{ + return m_uuid; +} + +bool QBLELocalService::primary() const +{ + return true; +} diff --git a/qblelocalservice.h b/qblelocalservice.h new file mode 100644 index 0000000..cc4ca6a --- /dev/null +++ b/qblelocalservice.h @@ -0,0 +1,45 @@ +#ifndef QBLELOCALSERVICE_H +#define QBLELOCALSERVICE_H + +#include +#include +#include +#include + +class QBLELocalCharacteristic; + +/** + * @brief Server-side GATT service exposed over D-Bus using the BlueZ GATT API. + * + * Implements org.bluez.GattService1. Add characteristics with addCharacteristic(). + * The service is registered at a path like /uk/co/piggz/amazfish/gatt/service. + * + * BlueZ will discover this service when the application is registered via + * QBLELocalApplication::registerWithAdapter(). + */ +class QBLELocalService : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.bluez.GattService1") + Q_PROPERTY(QString UUID READ uuid) + Q_PROPERTY(bool Primary READ primary) + Q_PROPERTY(QList Characteristics READ characteristicPaths) + +public: + explicit QBLELocalService(QDBusConnection bus, unsigned int index, const QString &uuid, const QString &appPath, QObject *parent = nullptr); + + QDBusObjectPath path() const; + void addCharacteristic(QBLELocalCharacteristic *ch); + QList characteristicPaths() const; + QList characteristics() const; + + QString uuid() const; + bool primary() const; + +private: + QString m_path; + QString m_uuid; + QList m_characteristics; +}; + +#endif // QBLELOCALSERVICE_H