#include "BLEScanner.h" // Qt #include #include #include // Project #include "main.h" #include "Logger.h" BLEScanner::BLEScanner(QObject *parent) : QObject(parent) { discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); timer = new QTimer(this); timer->setInterval(1000); // SRSUI 6 } void BLEScanner::onInitConnections() { // discovery agent connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), this, SLOT(onDeviceDiscovered(const QBluetoothDeviceInfo&))); connect(discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(onDiscoveryAgentError(QBluetoothDeviceDiscoveryAgent::Error))); connect(discoveryAgent, SIGNAL(finished()), this, SLOT(onScanFinished())); } /*! * \brief BLEScanner::init * \details Initializes the class by setting the connections * \return true on first initialization, false if it has already been initialized */ bool BLEScanner::doInit() { if (_init) return false; _init = true; onInitConnections(); LOG_EVENT("UI," + tr("%1 Initialized").arg(metaObject()->className())); return true; } /*! * \brief BLEScanner::quit * Called when the application is exiting. */ void BLEScanner::onQuit() { onQuitThread(); // verified } /*! * \brief BLEScanner::quitThread * \details Moves this object to main thread to be handled by QApplicaiton * It will also be destroyed there. */ void BLEScanner::onQuitThread() { if (!_thread ) return; moveToThread(qApp->thread()); } /*! * \brief BLEScanner::setMacAddress * Sets the BLE mac address to pair with * \param mac - (QString) The mac address to pair to (e.g. "EC:21:E5:F4:BC:C9") */ void BLEScanner::onSetMacAddress(const QString &mac) { macAddress = mac; } /*! * \brief BLEScanner::doSelectDevice * \param addr - The mac address of the device to connect to */ void BLEScanner::doSelectDevice(const QString &addr) { if (addr != macAddress) onSetMacAddress(addr); discoveryAgent->stop(); emit didFinishScan(); for (const QBluetoothDeviceInfo &deviceInfo : devices) { if (deviceInfo.address().toString() == addr) { selectedDeviceInfo = deviceInfo; onConnectToDevice(deviceInfo); return; } } } /*! * \brief BLEScanner::doReselectDevice * Called when reselecting a device (e.g. on reboot) * \param deviceInfo - the device info to connect to */ void BLEScanner::doReselectDevice(const QBluetoothDeviceInfo &deviceInfo) { LOG_DEBUG(QString("Reselecting device %1").arg(deviceInfo.address().toString())); selectedDeviceInfo = deviceInfo; onConnectToDevice(deviceInfo); updateBLECuffCheckinType(true); } /*! * \brief BLEScanner::onDeviceDiscovered * When a new mac address was discovered * \param deviceInfo - The discovered device's information */ void BLEScanner::onDeviceDiscovered(const QBluetoothDeviceInfo& deviceInfo) { for (const QString &prefix : omronDeviceNamesPrefixes) { if (deviceInfo.name().toLower().contains(prefix.toLower())) { devices.insert(0, deviceInfo); emit didDiscoverDevice(deviceInfo); } } } /*! * \brief BLEScanner::onDiscoveryAgentError * Called when the discovery agent encounters an error * \param error - the error enum */ void BLEScanner::onDiscoveryAgentError(QBluetoothDeviceDiscoveryAgent::Error error) { qDebug() << __FUNCTION__ << error; emit didReceiveScanForDevicesError(error); } /*! * \brief BLEScanner::onScanFinished * Called when the scan has completed */ void BLEScanner::onScanFinished() { emit didFinishScan(); } /*! * \brief BLEScanner::scanForDevices * Tells the discovery agent to start scanning for devices */ void BLEScanner::doScanForDevices() { timer->stop(); discoveryAgent->start(); } /*! * \brief BLEScanner::setupService * Creates a new service object and discovers details about the service * \param uuid - the uuid for the service * \return (QLowEnergyService*) - the created service */ QLowEnergyService* BLEScanner::setupService(const QBluetoothUuid &uuid) { QLowEnergyService *service = lowEnergyController->createServiceObject(uuid, this); service->discoverDetails(); qDebug() << "Service " << service->serviceName() << "UUID: " << uuid << " state: " << service->state(); foreach (const QLowEnergyCharacteristic &c, service->characteristics()) { qDebug() << "----> Characteristic: " << c.name() << " uuid: " << c.uuid(); } return service; } /*! * \brief BLEScanner::onServiceDiscovered * Called when a new service is discovered on the paired BLE device. * \param uuid - the QBluetoothUuid of the discovered service */ void BLEScanner::onServiceDiscovered(const QBluetoothUuid &uuid) { if (uuid.toString() == omronUnknownServiceName) { omronUnknownService = setupService(uuid); } else if (uuid.toString() == omronBloodPressureServiceName) { omronBloodPressureService = setupService(uuid); } else if (uuid.toString() == omronCurrentTimeServiceName) { omronCurrentTimeService = setupService(uuid); } else if (uuid.toString() == omronBatteryLevelServiceName) { omronBatteryLevelService = setupService(uuid); } else if (uuid.toString() == omronDeviceInformationServiceName) { omronDeviceInformationService = setupService(uuid); } // Keep in case it is needed for the BP/HR cuffs we haven't tested with yet /*else { QLowEnergyService *service = setupService(uuid); if (service != NULL) services.append(service); }*/ } /*! * \brief BLEScanner::onRequestDeviceSerialNumber * Sends a request for the device's serial number */ void BLEScanner::onRequestDeviceSerialNumber() { if (omronDeviceInformationService == nullptr) return; // read device serial number const QLowEnergyCharacteristic c = omronDeviceInformationService->characteristic( QBluetoothUuid(QBluetoothUuid::SerialNumberString) ); if (!c.isValid()) { qDebug() << "Cannot read device information."; updateBLECuffCheckinType(true); return; } omronDeviceInformationService->readCharacteristic(c); } /*! * \brief BLEScanner::onRequestDeviceInformation * Requests BP monitor device information */ void BLEScanner::onRequestDeviceInformation() { LOG_DEBUG("Requesting BLE device information"); onRequestDeviceSerialNumber(); } /*! * \brief BLEScanner::requestBPMeasurement * Requests BP Measurement data. * Must already be connected to the BLE device * The BLE device must be in the correct mode and it * must support indicate / notify for the blood pressure measurement characteristic (0x2A35). */ void BLEScanner::doRequestBPMeasurement() { if (omronBloodPressureService == nullptr) { qDebug() << "Blood pressure service is null. Cannot request BP Measurement"; return; } // blood pressure measurements const QLowEnergyCharacteristic bpCharacteristic = omronBloodPressureService->characteristic(QBluetoothUuid(QBluetoothUuid::BloodPressureMeasurement)); if (!bpCharacteristic.isValid()) { qDebug() << "Blood pressure service not found."; return; } notificationDesc = bpCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); if (notificationDesc.isValid()) omronBloodPressureService->writeDescriptor(notificationDesc, QByteArray::fromHex("0100")); // blood pressure feature const QLowEnergyCharacteristic c = omronBloodPressureService->characteristic(QBluetoothUuid(QBluetoothUuid::BloodPressureFeature)); if (!c.isValid()) { qDebug() << "Blood pressure feature not found."; return; } omronBloodPressureService->readCharacteristic(c); } /*! * \brief BLEScanner::serviceStateChanged * Called when the BLE service state has changed * Requests blood pressure measurement data * \param serviceState - the new state of the service */ void BLEScanner::onServiceStateChanged(const QLowEnergyService::ServiceState &serviceState) { switch (serviceState) { case QLowEnergyService::ServiceDiscovered: { doRequestBPMeasurement(); break; } case QLowEnergyService::InvalidService: { qDebug() << "Invalid Service"; break; } case QLowEnergyService::DiscoveryRequired: { qDebug() << "Discovery Required"; break; } case QLowEnergyService::DiscoveringServices: { qDebug() << "Discovering Services."; break; } case QLowEnergyService::LocalService: { qDebug() << "Invalid service state."; break; } default: { qDebug() << "Invalid service state"; break; } } } /*! * \brief BLEScanner::onCharacteristicChanged * Called when we received data for a particular characteristic * BP and HR data is received here * \param c - the characteristic we received data for * \param byteArray - the data received */ void BLEScanner::onCharacteristicChanged(const QLowEnergyCharacteristic &c, const QByteArray &byteArray) { qDebug() << "@@@@@@@@ Data Read @@@@@@@@@@: " << c.name() << byteArray; if (c.uuid() != QBluetoothUuid(QBluetoothUuid::BloodPressureMeasurement)) { qDebug() << "Ignoring data read for " << c.uuid(); return; } doParseMeasurement(byteArray); } /*! * \brief BLEScanner::parseMeasurement * Parses the BP and Pulse Rate measurement data * \param byteArray - the data to be parsed */ void BLEScanner::doParseMeasurement(const QByteArray &byteArray) { BLEMeasurementData measurement; // for debugging /* const char *data = "\x16t\x00M\x00Z\x00\xE4\x07\t\x04\n\x05""8@\x00\x00\x00"; QByteArray byteArray = QByteArray::fromRawData(data, sizeof(BLEMeasurementData)); */ const uint8_t *d = reinterpret_cast(byteArray.constData()); measurement.flags = *d; measurement.systolic = d[1]; measurement.diastolic = d[3]; measurement.mean_arterial_pressure_value = d[5]; measurement.year = d[7]; measurement.month = d[9]; measurement.day = d[10]; measurement.hour = d[11]; measurement.minute = d[12]; measurement.second = d[13]; measurement.pulse_rate = d[14]; measurement.user_id = d[16]; measurement.measurement_status = d[17]; qDebug() << "flags: " << measurement.flags; qDebug() << "systolic: " << measurement.systolic; qDebug() << "diastolic: " << measurement.diastolic; qDebug() << "mean arterial pressure: " << measurement.mean_arterial_pressure_value; qDebug() << "year: " << measurement.year; qDebug() << "month: " << measurement.month; qDebug() << "day: " << measurement.day; qDebug() << "hour: " << measurement.hour; qDebug() << "minute: " << measurement.minute; qDebug() << "second: " << measurement.second; qDebug() << "pulse_rate: " << measurement.pulse_rate; qDebug() << "user_id: " << measurement.user_id; qDebug() << "measurement_status: " << measurement.measurement_status; emit didReceiveBPMeasurement(measurement); } /*! * \brief BLEScanner::confirmedDescriptorWrite * When we received confirmation that the descriptor was written to * \param desc - the descriptor that was written to * \param byteArray - data confirming a descriptor disconnect or connection */ void BLEScanner::onConfirmedDescriptorWrite(const QLowEnergyDescriptor &desc, const QByteArray &byteArray) { qDebug() << "Confirmed descriptor write: " << byteArray; if (desc.isValid() && desc == desc && byteArray == QByteArray::fromHex("0000")) { //disabled notifications -> assume disconnect qDebug() << "deleting omron blood pressure service"; lowEnergyController->disconnectFromDevice(); delete omronBloodPressureService; omronBloodPressureService = nullptr; } emit didConnectToDevice(selectedDeviceInfo); } /*! * \brief BLEScanner::serviceCharacteristicsRead * Called when the provided characteristic has been read * \param c - the BLE characteristic that was read * \param byteArray - the data read from the BLE characteristic */ void BLEScanner::onServiceCharacteristicsRead(const QLowEnergyCharacteristic &c,const QByteArray &byteArray) { qDebug() << __FUNCTION__ << c.name() << " data: " << byteArray; } /*! * \brief BLEScanner::makeServiceConnections * Makes the necessary connections to interact with the given service * \param service - the service with connections we need */ void BLEScanner::makeServiceConnections(QLowEnergyService *service) { connect(service, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(onServiceStateChanged(QLowEnergyService::ServiceState))); connect(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic, QByteArray)), this, SLOT(onCharacteristicChanged(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray)), this, SLOT(onServiceCharacteristicsRead(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(descriptorWritten(QLowEnergyDescriptor, QByteArray)), this, SLOT(onConfirmedDescriptorWrite(QLowEnergyDescriptor, QByteArray))); } /*! * \brief BLEScanner::onServiceScanDone * Called when we have finished scanning the services provided by BLE device */ void BLEScanner::onServiceScanDone() { qDebug() << "##############################"; qDebug() << "##############################"; qDebug() << "######## Scan Finished #######"; qDebug() << "##############################"; qDebug() << "##############################"; // Keep in case it is needed for the BP/HR cuffs we haven't tested with yet // foreach (QLowEnergyService *service, services) // makeServiceConnections(service); if (omronUnknownService != NULL) makeServiceConnections(omronUnknownService); else LOG_DEBUG("Unknown service is null"); if (omronCurrentTimeService != NULL) makeServiceConnections(omronCurrentTimeService); else LOG_DEBUG("Current time service is null"); if (omronBatteryLevelService != NULL) makeServiceConnections(omronBatteryLevelService); else LOG_DEBUG("Battery level service is null"); if (omronBloodPressureService != NULL) makeServiceConnections(omronBloodPressureService); else LOG_DEBUG("Blood Pressure service is null"); if (omronDeviceInformationService != NULL) makeServiceConnections(omronDeviceInformationService); else LOG_DEBUG("Device Information service is null"); } /*! * \brief BLEScanner::onControllerError * Called when the BLE Controller encounters an error * \param error - the error enum */ void BLEScanner::onControllerError(const QLowEnergyController::Error &error) { emit didReceiveControllerError(error); } /*! * \brief BLEScanner::updateBLECuffCheckinType * Toggles the type of check we do with the BLE cuff. * If already connected, just query it repeatedly every 1Hz. * Otherwise, start trying to reconnect to it. * \param retryConnection - if true, retry connection, normal checkin otherwise. */ void BLEScanner::updateBLECuffCheckinType(bool retryConnection) { timer->stop(); if (retryConnection) { disconnect(timer, SIGNAL(timeout()), this, SLOT(onRequestDeviceInformation())); connect(timer, SIGNAL(timeout()), this, SLOT(onRetryConnectToDevice())); } else{ disconnect(timer, SIGNAL(timeout()), this, SLOT(onRetryConnectToDevice())); connect(timer, SIGNAL(timeout()), this, SLOT(onRequestDeviceInformation())); } timer->start(); } /*! * \brief BLEScanner::onDeviceConnected * Called when we successfully connect to a device. * Starts the scan for services on the device */ void BLEScanner::onDeviceConnected() { LOG_DEBUG("Device Connected"); lowEnergyController->discoverServices(); updateBLECuffCheckinType(false); if (!timer->isActive()) timer->start(); } /*! * \brief BLEScanner::onDeviceDisconnected * Called when we become disconnected from a device. */ void BLEScanner::onDeviceDisconnected() { LOG_DEBUG("Device Disconnected."); emit didDisconnectFromDevice(selectedDeviceInfo); } /*! * \brief BLEScanner::connectToDevice * Creates the low energy controller object * Configures the low energy controller signals * Connects to the desired device * \param deviceInfo - The QBluetoothDeviceInfo object to connect to */ void BLEScanner::onConnectToDevice(const QBluetoothDeviceInfo& deviceInfo) { LOG_DEBUG(QString("Connecting to (%1, %2)").arg(deviceInfo.name()).arg(deviceInfo.address().toString())); lowEnergyController = QLowEnergyController::createCentral(deviceInfo); // low energy controller connect(lowEnergyController, SIGNAL(serviceDiscovered(QBluetoothUuid)), this, SLOT(onServiceDiscovered(QBluetoothUuid))); connect(lowEnergyController, SIGNAL(discoveryFinished()), this, SLOT(onServiceScanDone())); connect(lowEnergyController, SIGNAL(error(QLowEnergyController::Error)), this, SLOT(onControllerError(QLowEnergyController::Error))); connect(lowEnergyController, SIGNAL(connected()), this, SLOT(onDeviceConnected())); connect(lowEnergyController, SIGNAL(disconnected()), this, SLOT(onDeviceDisconnected())); emit didStartConnectingToDevice(); lowEnergyController->connectToDevice(); } /*! * \brief BLEScanner::onRetryConnectToDevice * Called when we retry to connect to the specific device */ void BLEScanner::onRetryConnectToDevice() { // LOG_DEBUG(QString("Retrying to connect to BLE device with address %1").arg(selectedDeviceInfo.address().toString())); lowEnergyController->connectToDevice(); }