/*! * \copyright * Copyright (c) 2019-2020 Diality Inc. - All Rights Reserved. * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * \file BluetoothInterface.h * \author (last) Behrouz NematiPour * \author (original) Behrouz NematiPour * \date (last) 22-Aug-2021 * \date (original) 22-Aug-2021 */ #include "BluetoothInterface.h" // Qt #include #include #include #include // Project #include "Logger.h" // namespace using namespace Bluetooth; using namespace Model; /*! * \brief BluetoothInterface::BluetoothInterface * \details Constructor * \param parent - QObject parent owner object. * Qt handles the children destruction by their parent objects life-cycle. */ BluetoothInterface::BluetoothInterface(QObject *parent) : QObject(parent) { _local = new QBluetoothLocalDevice (this); _agent = new QBluetoothDeviceDiscoveryAgent (this); startTimer(_interval); } /*! * \brief BluetoothInterface::init * \details Initializes the class * \return False if it has been called before. */ bool BluetoothInterface::init() { if ( _init ) return false; _init = true; // runs in BluetoothInterface thread initConnections(); LOG_DEBUG("UI," + tr("%1 Initialized").arg(metaObject()->className())); return true; } /*! * \brief BluetoothInterface::init * \details Initialized the Class by calling the init() method first * And initializes the thread vThread by calling initThread * on success init(). * \param vThread - the thread * \return returns the return value of the init() method */ bool BluetoothInterface::init(QThread &vThread) { if ( ! init() ) return false; initThread(vThread); return true; } /*! * \brief BluetoothInterface::quit * \details quits the class * Calls quitThread */ void BluetoothInterface::quit() { // coco begin validated: Application termination is not correctly done in coco!!! // it has been tested and works perfectly fine in normal run. quitThread(); // validated if ( _local ) _local ->deleteLater(); if ( _agent ) _agent ->deleteLater(); if ( _device ) _device ->deleteLater(); } // coco end /*! * \brief BluetoothInterface::initConnections * \details Initializes the required signal/slot connection between this class and other objects * to be able to communicate. */ void BluetoothInterface::initConnections() { // Local connections connect(_local , &QBluetoothLocalDevice :: pairingFinished , this , &BluetoothInterface ::onLocalPairingFinish ); connect(_local , &QBluetoothLocalDevice :: pairingDisplayConfirmation , this , &BluetoothInterface ::onLocalPairingDisplayConfirmation ); connect(_local , &QBluetoothLocalDevice :: pairingDisplayPinCode , this , &BluetoothInterface ::onLocalPairingDisplayPinCode ); connect(_local , &QBluetoothLocalDevice :: deviceConnected , this , &BluetoothInterface ::onLocalDeviceConnect ); connect(_local , &QBluetoothLocalDevice :: deviceDisconnected , this , &BluetoothInterface ::onLocalDeviceDisconnect ); connect(_local , &QBluetoothLocalDevice :: error , this , &BluetoothInterface ::onLocalError ); // Agent connections connect(_agent , &QBluetoothDeviceDiscoveryAgent ::deviceDiscovered , this , &BluetoothInterface ::onAgentDiscoverDevice ); connect(_agent , &QBluetoothDeviceDiscoveryAgent :: finished , this , &BluetoothInterface ::onAgentDiscoverFinish ); connect(_agent , &QBluetoothDeviceDiscoveryAgent :: canceled , this , &BluetoothInterface ::onAgentDiscoverCancel ); connect(_agent , static_cast(&QBluetoothDeviceDiscoveryAgent::error), this , &BluetoothInterface ::onAgentDiscoverError ); } /*! * \brief BluetoothInterface::initThread * \details Moves this object into the thread vThread. * And checks that this method is called from main thread. * Also connects quitThread to application aboutToQuit. * \param vThread - the thread */ void BluetoothInterface::initThread(QThread &vThread) { // runs in main thread Q_ASSERT_X(QThread::currentThread() == qApp->thread() , __func__, "The Class initialization must be done in Main Thread"); _thread = &vThread; _thread->setObjectName(QString("%1_Thread").arg(metaObject()->className())); connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(quit())); _thread->start(); moveToThread(_thread); } /*! * \brief BluetoothInterface::quitThread * \details Moves this object to main thread to be handled by QApplicaiton * And to be destroyed there. */ void BluetoothInterface::quitThread() { // coco begin validated: Application termination is not correctly done in coco!!! // it has been tested and works perfectly fine in normal run. if ( ! _thread ) return; // runs in thread moveToThread(qApp->thread()); // validated } // coco end #define NOTIFY_IDLE notifyStateChange(MBluetooth(MBluetooth::eIS_Idle )); #define NOTIFY_CLOSE notifyStateChange(MBluetooth(MBluetooth::eIS_Close )); // ~~~~~~~~~~~ Local #define NOTIFY_LOCAL_INIT notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Init , \ _local->address().toString() , \ _local->name() )); #define NOTIFY_LOCAL_CONNECT notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Connect , vAddress.toString()) ); #define NOTIFY_LOCAL_ERROR notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Error_Unknown )); #define NOTIFY_LOCAL_ERROR_IO notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Error_IO )); #define NOTIFY_LOCAL_ERROR_OFF notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Error_Off )); #define NOTIFY_LOCAL_ERROR_INVALID notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Error_Invalid )); #define NOTIFY_LOCAL_DISCONNECT notifyStateChange(MBluetooth(MBluetooth::eIS_Local_Disconnect , vAddress.toString()) ); // ~~~~~~~~~~~ Pair #define NOTIFY_PAIR_START notifyStateChange(MBluetooth(MBluetooth::eIS_Pair_Start , _device->remoteAddress().toString())); #define NOTIFY_PAIR_DONE notifyStateChange(MBluetooth(MBluetooth::eIS_Pair_Done , vAddress.toString(), "", "", vPairing)); #define NOTIFY_PAIR_CONFIRM notifyStateChange(MBluetooth(MBluetooth::eIS_Pair_Confirm , vAddress.toString(), "", vPin ) ); #define NOTIFY_PAIR_PINCODE notifyStateChange(MBluetooth(MBluetooth::eIS_Pair_PinCode , vAddress.toString(), "", vPin ) ); #define NOTIFY_PAIR_ERROR notifyStateChange(MBluetooth(MBluetooth::eIS_Pair_Error )); // ~~~~~~~~~~~ Scan #define NOTIFY_SCAN_DISCOVER notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Discover , \ vInfo.address().toString() , \ vInfo.name(), "" , \ _local->pairingStatus(vInfo.address()) , \ 0 , \ vInfo.isValid() , \ vInfo.deviceUuid().toString() )); #define NOTIFY_SCAN_FOUND _model = MBluetooth(MBluetooth::eIS_Scan_Found , \ _info.address().toString() , \ _info.name(), "" , \ _local->pairingStatus(vInfo.address()) , \ 0 , \ _info.isValid() , \ _info.deviceUuid().toString() );\ notifyStateChange(_model ); #define NOTIFY_SCAN_START notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Start )); #define NOTIFY_SCAN_REJECT notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Reject )); #define NOTIFY_SCAN_NOTFOUND notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_NotFound ,"", "" )); #define NOTIFY_SCAN_STOP notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Stop )); #define NOTIFY_SCAN_DONE notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Done )); #define NOTIFY_SCAN_DETAIL notifyStateChange(MBluetooth(MBluetooth::eIS_Scan_Detail , \ _info.address().toString() , \ _info.name() )); // ~~~~~~~~~ Device #define NOTIFY_DEVICE_INIT_ERROR notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Error_Init )); #define NOTIFY_DEVICE_INIT notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Init , \ _info.address().toString() , \ _info.name() )); #define NOTIFY_DEVICE_START notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Start , \ _info.address().toString() , \ _info.name() )); #define NOTIFY_DEVICE_CONNECT notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Connect , \ _info.address().toString() , \ _info.name() )); #define NOTIFY_DEVICE_DONE notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Done , \ _info.address().toString() , \ _info.name() )); #define NOTIFY_DEVICE_ERROR notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Error , \ _info.address().toString() , \ _info.name() , \ "",0, vError, false )); #define NOTIFY_DEVICE_DISCONNECT notifyStateChange(MBluetooth(MBluetooth::eIS_Device_Disconnect , \ _info.address().toString() , \ _info.name() )); #define NOTIFY_SERVICE_START notifyStateChange(MBluetooth(MBluetooth::eIS_Service_Start )); /* #define NOTIFY_SERVICE_DISCOVER notifyStateChange(MBluetooth(MBluetooth::eIS_Service_Discover , \ _service->serviceUuid().toString() , \ _service->serviceName() )); */ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~ Public interface to initialize and start the scan // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /*! * \brief BluetoothInterface::onstart * \details the thread safe slot to be called when the public function start is called. * initializes the local Bluetooth device _local and setups up the interface. */ void BluetoothInterface::onstart() { _local->powerOn(); if (! _local->isValid() ) { NOTIFY_LOCAL_ERROR_INVALID return; } resetDevice(); _agent->setLowEnergyDiscoveryTimeout(5000); NOTIFY_LOCAL_INIT NOTIFY_IDLE } /*! * \brief BluetoothInterface::ondoScan * \details the thread safe slot to be called when the public function doScan is called. * initializes/validates the discovery agent _agent and setups it up to start the discovery. */ void BluetoothInterface::ondoScan() { if (_agent && _agent->isActive()) { NOTIFY_SCAN_REJECT return; } NOTIFY_SCAN_START _agent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~ Local Device Slots // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Set of notifications on each event /*! * \brief BluetoothInterface::onLocalDeviceConnect * \details Notifies the observers (view: VBluetooth) that the Local Device Connected * \param vAddress */ void BluetoothInterface::onLocalDeviceConnect (const QBluetoothAddress &vAddress ) { NOTIFY_LOCAL_CONNECT } /*! * \brief BluetoothInterface::onLocalDeviceDisconnect * \details Notifies the observers (view: VBluetooth) that the Local Device Disconnected * \param vAddress */ void BluetoothInterface::onLocalDeviceDisconnect (const QBluetoothAddress &vAddress ) { NOTIFY_LOCAL_DISCONNECT } /*! * \brief BluetoothInterface::onLocalPairingDisplayConfirmation * \details Notifies the observers (view: VBluetooth) that the user needs to confirm paring. * \param vAddress - remote device address * \param vPin - remote device requested pin code * \note Not used for now just logged for later use. */ void BluetoothInterface::onLocalPairingDisplayConfirmation (const QBluetoothAddress &vAddress, const QString &vPin ) { NOTIFY_PAIR_CONFIRM } /*! * \brief BluetoothInterface::onLocalPairingDisplayPinCode * \details Notifies the observers (view: VBluetooth) the pin code to make sure this is the device it intends to connect. * Signal by some platforms to display the pin to the user for address. * The pin is automatically generated, and does not need to be confirmed. * \param vAddress * \param vPin * \note Not used for now just logged for later use. */ void BluetoothInterface::onLocalPairingDisplayPinCode (const QBluetoothAddress &vAddress, const QString &vPin ) { NOTIFY_PAIR_PINCODE } /*! * \brief BluetoothInterface::onLocalPairingFinish * \details Notifies the observers (view: VBluetooth) that the paring is successfully done * \param vAddress - Paired remote address * \param vPairing - The Paring status/Type */ void BluetoothInterface::onLocalPairingFinish (const QBluetoothAddress &vAddress, QBluetoothLocalDevice::Pairing vPairing ) { NOTIFY_PAIR_DONE } /*! * \brief BluetoothInterface::onLocalError * \details Notifies the observers (view: VBluetooth) that the local adapter has error * \param vError - The error */ void BluetoothInterface::onLocalError ( QBluetoothLocalDevice::Error vError) { switch (vError) { case QBluetoothLocalDevice::PairingError: NOTIFY_PAIR_ERROR break; default : NOTIFY_LOCAL_ERROR break; } if ( _device ) _device->disconnectFromDevice(); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~ Discovery Agent Slots // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /*! * \brief BluetoothInterface::onAgentDiscoverDevice * \details The slot to handle each discovered Bluetooth Low Energy device. * if the discovered BLE device , which its information provided in vInfo, has any identified keyword, the scan/discovery stops if it is still active, and * notifies the observers (view: VBluetooth) that a device found. * \param vInfo * \note If multiple supported devices are near, the first one will be identified as found. */ void BluetoothInterface::onAgentDiscoverDevice(const QBluetoothDeviceInfo &vInfo) { NOTIFY_SCAN_DISCOVER if (vInfo.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) { if ( isDeviceSupported( vInfo.name() ) ) { _info = QBluetoothDeviceInfo(vInfo.address(), vInfo.name(), QBluetoothDeviceInfo::HealthBloodPressureMonitor); NOTIFY_SCAN_FOUND if (_agent->isActive()) _agent->stop(); } } } /*! * \brief BluetoothInterface::onAgentDiscoverError * \details Notifies the observers (view: VBluetooth) that the discovery agent has error, during discovery. * \param vError - The discovery agent error */ void BluetoothInterface::onAgentDiscoverError(QBluetoothDeviceDiscoveryAgent::Error vError) { switch (vError) { case QBluetoothDeviceDiscoveryAgent::PoweredOffError : NOTIFY_LOCAL_ERROR_OFF break; case QBluetoothDeviceDiscoveryAgent::InputOutputError : NOTIFY_LOCAL_ERROR_IO break; default : NOTIFY_LOCAL_ERROR break; } if ( _device ) _device->disconnectFromDevice(); } /*! * \brief BluetoothInterface::onAgentDiscoverFinish * \details Notifies the observers (view: VBluetooth) that the discovery agent has done discovery, and * starts gathering details and setup the discovered device. */ void BluetoothInterface::onAgentDiscoverFinish() { NOTIFY_SCAN_DONE connectToDevice(); } /*! * \brief BluetoothInterface::onAgentDiscoverCancel * \details Notifies the observers (view: VBluetooth) that the discovery agent has stopped discovery, and * starts gathering details and setup the discovered device. * \note this slot is called when the discovery stopped before it is finished (currently by first supported found device). */ void BluetoothInterface::onAgentDiscoverCancel() { NOTIFY_SCAN_STOP connectToDevice(); } /*! * \brief BluetoothInterface::connectToDevice * \return false if * the local Bluetooth device _local is not valid * the device info _info is not valid * */ bool BluetoothInterface::connectToDevice() { if ( ! isLocalValid() ) return false; if ( ! isInfoValid () ) return false; if ( ! initDevice () ) return false; NOTIFY_DEVICE_START _device->connectToDevice(); return true; } void BluetoothInterface::onDeviceConnect() { NOTIFY_DEVICE_CONNECT _device->discoverServices(); NOTIFY_SERVICE_START } void BluetoothInterface::onDeviceDisconnect() { NOTIFY_DEVICE_DISCONNECT } void BluetoothInterface::onDeviceError(QLowEnergyController::Error vError) { NOTIFY_DEVICE_ERROR } void BluetoothInterface::onDeviceStateChanged(QLowEnergyController::ControllerState vState) { // qDebug() << "Device State Changed" << vState; } void BluetoothInterface::onDeviceConnectionUpdated(const QLowEnergyConnectionParameters &/*vParameters*/) { // qDebug() << "Device Connection Changed"; } bool BluetoothInterface::initDevice() { if ( _device ) { _device->disconnectFromDevice(); delete _device; _device = nullptr; } _device = QLowEnergyController::createCentral(_info, this); if ( _device ) NOTIFY_DEVICE_INIT else { NOTIFY_DEVICE_INIT_ERROR return false; } // Device controller connect( _device, SIGNAL( connected ( )), this , SLOT(onDeviceConnect ( ))); connect( _device, SIGNAL( error (QLowEnergyController::Error )), this , SLOT(onDeviceError (QLowEnergyController::Error ))); connect( _device, SIGNAL( disconnected ( )), this , SLOT(onDeviceDisconnect ( ))); connect( _device, SIGNAL( stateChanged(QLowEnergyController::ControllerState )), this , SLOT(onDeviceStateChanged(QLowEnergyController::ControllerState ))); connect( _device, SIGNAL( connectionUpdated(QLowEnergyConnectionParameters )), this , SLOT(onDeviceConnectionUpdated(QLowEnergyConnectionParameters ))); // Device Services connect( _device, &QLowEnergyController::serviceDiscovered, [=](const QBluetoothUuid &vService){ if (vService == QBluetoothUuid (QBluetoothUuid::DeviceInformation )) { if ( _serviceDeviceInformation )S delete _serviceDeviceInformation ; _serviceDeviceInformation = _device->createServiceObject(vService, this); qDebug() << " ~~~~~ Inf Service Found" << vService; return; } if (vService == QBluetoothUuid (QBluetoothUuid::CurrentTimeService )) { if ( _serviceCurrentTime ) delete _serviceCurrentTime ; _serviceCurrentTime = _device->createServiceObject(vService, this); qDebug() << " ~~~~~ TDt Service Found" << vService; return; } if (vService == QBluetoothUuid (QBluetoothUuid::BloodPressure )) { if ( _serviceBloodPressure ) delete _serviceBloodPressure ; _serviceBloodPressure = _device->createServiceObject(vService, this); qDebug() << " ~~~~~ BPr Service Found" << vService; return; } if (vService == QBluetoothUuid (QBluetoothUuid::BatteryService )) { if ( _serviceBattery ) delete _serviceBattery ; _serviceBattery = _device->createServiceObject(vService, this); qDebug() << " ~~~~~ Bat Service Found" << vService; return; } }); connect(_device, &QLowEnergyController::discoveryFinished, [=](){ // _serviceInformation->discoverDetails(); NOTIFY_DEVICE_DONE _device->disconnectFromDevice(); }); return true; } /*! * \brief BluetoothInterface::notifyStateChange * \details sends a notification signal to the observers (view: VBluetooth) on any reported state changes. * \param vData - the Bluetooth data model containing information about the current state and device. */ void BluetoothInterface::notifyStateChange(const BluetoothData &vData) { _state = vData.state; emit didStateChange(vData); } /*! * \brief BluetoothInterface::timerEvent * \details The main QObject's timer which has been set with the interval of the _interval (1s) * The main purpose is to reconnect to the device on each second interval to get any new measurement data. */ void BluetoothInterface::timerEvent(QTimerEvent *) { if ( _bpRead ) { } } /*! * \brief BluetoothInterface::isDeviceSupported * \param vName - remote device name * \return true if the name provided starts with one of the supported keywords mentioned in the Omron documentation */ bool BluetoothInterface::isDeviceSupported( const QString &vName ) { for (const QString &name : _supportedDeviceKeywords ) if ( vName.startsWith(name) ) return true; return false; } /*! * \brief BluetoothInterface::isLocalValid * \details checks if the local Bluetooth device is valid, and * notifies the observers (view: VBluetooth) that local is not valid if fails. * \return false if the local Bluetooth device _local is not valid */ bool BluetoothInterface::isLocalValid() { if (! _local->isValid() ) { NOTIFY_LOCAL_ERROR_INVALID return false; } return true; } /*! * \brief BluetoothInterface::isInfoValid * \details checks if the device info _info is valid. * notifies the observers (view: VBluetooth) that the device info is not valid. * \return false if the device info _local is not valid * \note the device info _info is set to a default QBluetoothDeviceInfo which is invalid by default, * when the interface initialized (onStart), and * when a scan started (ondoScan) */ bool BluetoothInterface::isInfoValid() { if ( ! _info.isValid() ) { NOTIFY_SCAN_NOTFOUND // not a valid device found return false; } return true; } /*! * \brief BluetoothInterface::isDeviceValid * \details checks if the remote Bluetooth device has been setup. * \return returns false if no device is setup. */ bool BluetoothInterface::isDeviceValid() { if ( _device ) { NOTIFY_DEVICE_INIT } else { NOTIFY_DEVICE_INIT_ERROR return false; } return true; } /*! * \brief BluetoothInterface::resetDevice * \details resets the remote device _device and the device info _info. */ void BluetoothInterface::resetDevice() { _info = QBluetoothDeviceInfo(); // reset the found info if ( _device ) { _device->disconnectFromDevice(); delete _device; _device = nullptr; } }