diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 36c67f77..e0526feb 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -103,6 +103,7 @@ set(SOURCES ./src/services/dk08wechatservice.cpp ./src/services/hrmservice.cpp ./src/services/immediatealertservice.cpp +./src/services/immediatealertserverservice.cpp ./src/services/infinitimemotionservice.cpp ./src/services/infinitimenavservice.cpp #./src/services/infinitimeresourceservice.cpp @@ -224,6 +225,7 @@ set(HEADERS ./src/services/dk08wechatservice.h ./src/services/hrmservice.h ./src/services/immediatealertservice.h +./src/services/immediatealertserverservice.h ./src/services/infinitimemotionservice.h ./src/services/infinitimenavservice.h #./src/services/infinitimeresourceservice.h diff --git a/daemon/src/devices/pinetimejfdevice.cpp b/daemon/src/devices/pinetimejfdevice.cpp index 01b82fa6..f34377e1 100644 --- a/daemon/src/devices/pinetimejfdevice.cpp +++ b/daemon/src/devices/pinetimejfdevice.cpp @@ -8,6 +8,7 @@ #include "dfuoperation.h" #include "infinitimenavservice.h" #include "immediatealertservice.h" +#include "immediatealertserverservice.h" #include "hrmservice.h" #include "infinitimemotionservice.h" #include "infinitimeweatherservice.h" @@ -16,6 +17,7 @@ #include "batteryservice.h" #include "amazfishconfig.h" #include "realtimeactivitysample.h" +#include "qble/qblelocalapplication.h" #include namespace { @@ -192,6 +194,9 @@ void PinetimeJFDevice::initialise() setConnectionState("connected"); parseServices(); + // Start the GATT server (IAS) so InfiniTime can find this phone + setupGattServer(); + AlertNotificationService *alert = qobject_cast(service(AlertNotificationService::UUID_SERVICE_ALERT_NOTIFICATION)); if (alert) { alert->enableNotification(AlertNotificationService::UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT); @@ -573,3 +578,48 @@ void PinetimeJFDevice::immediateAlert(int level) ias->sendAlert((ImmediateAlertService::Levels)level); } } + +// --------------------------------------------------------------------------- +// GATT server — serve IAS so that InfiniTime FindMyPhone can alert this device +// --------------------------------------------------------------------------- + +void PinetimeJFDevice::setupGattServer() +{ + if (m_iasApp) { + qDebug() << Q_FUNC_INFO << "GATT server already set up"; + return; + } + + qDebug() << Q_FUNC_INFO << "Setting up IAS GATT server for PineTime"; + + // Application root — implements org.freedesktop.DBus.ObjectManager on system bus + m_iasApp = new QBLELocalApplication(QStringLiteral("/uk/co/piggz/amazfish/gatt"), this); + + // IAS service (index 0), UUID 0x1802 + m_iasService = new ImmediateAlertServerService(QDBusConnection::systemBus(), 0, m_iasApp); + m_iasApp->addService(m_iasService); + + connect(m_iasService, &ImmediateAlertServerService::alertLevelChanged, this, &PinetimeJFDevice::onImmediateAlertFromWatch); + + // Register with the BlueZ adapter from config + QString adapterPath = AmazfishConfig::instance()->localAdapter(); + m_iasApp->registerWithAdapter(adapterPath); +} + +void PinetimeJFDevice::onImmediateAlertFromWatch(int level) +{ + qDebug() << Q_FUNC_INFO << "FindMyPhone alert level from watch:" << level; + + switch (static_cast(level)) { + case ImmediateAlertServerService::Levels::HighAlert: + case ImmediateAlertServerService::Levels::MildAlert: + emit deviceEvent(AbstractDevice::EVENT_FIND_PHONE); + break; + case ImmediateAlertServerService::Levels::NoAlert: + emit deviceEvent(AbstractDevice::EVENT_CANCEL_FIND_PHONE); + break; + default: + qWarning() << Q_FUNC_INFO << "Unknown alert level:" << level; + break; + } +} diff --git a/daemon/src/devices/pinetimejfdevice.h b/daemon/src/devices/pinetimejfdevice.h index 7d7b732f..0d443e4f 100644 --- a/daemon/src/devices/pinetimejfdevice.h +++ b/daemon/src/devices/pinetimejfdevice.h @@ -5,6 +5,9 @@ #include "abstractdevice.h" #include "realtimeactivitysample.h" +class QBLELocalApplication; +class ImmediateAlertServerService; + class PinetimeJFDevice : public AbstractDevice { public: @@ -53,6 +56,12 @@ class PinetimeJFDevice : public AbstractDevice RealtimeActivitySample realtimeActivitySample; Q_SLOT void sampledActivity(QDateTime dt, int kind, int intensity, int steps, int heartrate); + + // GATT server — serves IAS so InfiniTime FindMyPhone can alert this device + QBLELocalApplication *m_iasApp = nullptr; + ImmediateAlertServerService *m_iasService = nullptr; + void setupGattServer(); + Q_SLOT void onImmediateAlertFromWatch(int level); }; #endif // PINETIMEJFDEVICE_H diff --git a/daemon/src/services/immediatealertserverservice.cpp b/daemon/src/services/immediatealertserverservice.cpp new file mode 100644 index 00000000..298d007b --- /dev/null +++ b/daemon/src/services/immediatealertserverservice.cpp @@ -0,0 +1,48 @@ +#include "immediatealertserverservice.h" +#include "qble/qblelocalcharacteristic.h" + +#include + +const char *ImmediateAlertServerService::UUID_SERVICE_IMMEDIATE_ALERT = "00001802-0000-1000-8000-00805f9b34fb"; +const char *ImmediateAlertServerService::UUID_CHARACTERISTIC_IMMEDIATE_ALERT_LEVEL = "00002a06-0000-1000-8000-00805f9b34fb"; + +// --------------------------------------------------------------------------- +// Alert Level characteristic — write-without-response, no read +// InfiniTime uses ble_gattc_write_no_rsp() so the flag must be +// "write-without-response" (not "write"). +// --------------------------------------------------------------------------- +class AlertLevelCharacteristic : public QBLELocalCharacteristic +{ +public: + AlertLevelCharacteristic(QDBusConnection bus, unsigned int index, QBLELocalService *service) + : QBLELocalCharacteristic(bus, index, QString::fromLatin1(ImmediateAlertServerService::UUID_CHARACTERISTIC_IMMEDIATE_ALERT_LEVEL), + { QStringLiteral("write-without-response") }, + service, service) + { + } + // WriteValue is inherited from QBLELocalCharacteristic which already + // emits valueWritten(value) — that is all we need. +}; + +// --------------------------------------------------------------------------- + +ImmediateAlertServerService::ImmediateAlertServerService(QDBusConnection bus, unsigned int serviceIndex, QObject *parent) + : QBLELocalService(bus, serviceIndex, QString::fromLatin1(UUID_SERVICE_IMMEDIATE_ALERT), QStringLiteral("/uk/co/piggz/amazfish/gatt"), parent) +{ + auto *alertLevel = new AlertLevelCharacteristic(bus, 0, this); + addCharacteristic(alertLevel); + + connect(alertLevel, &QBLELocalCharacteristic::valueWritten, this, &ImmediateAlertServerService::onValueWritten); +} + +void ImmediateAlertServerService::onValueWritten(const QByteArray &value) +{ + if (value.isEmpty()) { + qWarning() << Q_FUNC_INFO << "Received empty write on Alert Level"; + return; + } + + int level = static_cast(value.at(0)); + qDebug() << Q_FUNC_INFO << "Alert level received from watch:" << level; + emit alertLevelChanged(level); +} diff --git a/daemon/src/services/immediatealertserverservice.h b/daemon/src/services/immediatealertserverservice.h new file mode 100644 index 00000000..7bfa5454 --- /dev/null +++ b/daemon/src/services/immediatealertserverservice.h @@ -0,0 +1,34 @@ +#ifndef IMMEDIATEALERTSERVERSERVICE_H +#define IMMEDIATEALERTSERVERSERVICE_H + +#include "qble/qblelocalservice.h" + +// https://www.bluetooth.com/specifications/specs/immediate-alert-service-1-0/ +// +// This class serves the IAS on the desktop/phone side so that an InfiniTime watch +// acting as GATT client (FindMyPhone screen) can write to the Alert Level +// characteristic and trigger an alert on the companion device. +// +// Contrast with ImmediateAlertService (client side) which writes *to* the watch. + +class ImmediateAlertServerService : public QBLELocalService +{ + Q_OBJECT +public: + explicit ImmediateAlertServerService(QDBusConnection bus, unsigned int serviceIndex, QObject *parent = nullptr); + + static const char *UUID_SERVICE_IMMEDIATE_ALERT; + static const char *UUID_CHARACTERISTIC_IMMEDIATE_ALERT_LEVEL; + + enum class Levels : uint8_t { NoAlert = 0, MildAlert = 1, HighAlert = 2 }; + Q_ENUM(Levels) + +Q_SIGNALS: + // Emitted when the watch writes a new alert level + void alertLevelChanged(int level); + +private Q_SLOTS: + void onValueWritten(const QByteArray &value); +}; + +#endif // IMMEDIATEALERTSERVERSERVICE_H diff --git a/daemon/src/services/immediatealertservice.h b/daemon/src/services/immediatealertservice.h index d752754d..e62b6953 100644 --- a/daemon/src/services/immediatealertservice.h +++ b/daemon/src/services/immediatealertservice.h @@ -15,6 +15,7 @@ class ImmediateAlertService : public QBLEService static const char* UUID_SERVICE_IMMEDIATE_ALERT; static const char* UUID_CHARACTERISTIC_IMMEDIATE_ALERT_LEVEL; enum class Levels : uint8_t { NoAlert = 0, MildAlert = 1, HighAlert = 2 }; + Q_ENUM(Levels); Levels alertLevel(); void sendAlert(ImmediateAlertService::Levels level); diff --git a/qble b/qble index 87db9811..5c391033 160000 --- a/qble +++ b/qble @@ -1 +1 @@ -Subproject commit 87db9811fea2e3ff10d854fe2205e03765a558df +Subproject commit 5c3910331272dbff0205e0995af85c5c56680342