#include "BLEScanner.h" // Qt #include #include #include // Project #include "main.h" #include "logger.h" BLEScanner::BLEScanner(QObject *parent) : QObject(parent) { discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); macAddress = "EC:21:E5:F4:BC:C9"; // for testing only } void BLEScanner::initConnections() { // discovery agent connect(discoveryAgent, SIGNAL(deviceDiscovered(const 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::init() { if (_init) return false; _init = true; initConnections(); LOG_EVENT("UI," + tr("%1 Initialized").arg(metaObject()->className())); return true; } /*! * \brief BLEScanner::quit * Called when the application is exiting. */ void BLEScanner::quit() { quitThread(); // verified } /*! * \brief BLEScanner::quitThread * \details Moves this object to main thread to be handled by QApplicaiton * It will also be destroyed there. */ void BLEScanner::quitThread() { 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::setMacAddress(const QString &mac) { macAddress = mac; } /*! * \brief BLEScanner::onSelectedDevice * \param addr - The mac address of the device to connect to */ void BLEScanner::onSelectedDevice(const QString &addr) { setMacAddress(addr); for (const QBluetoothDeviceInfo &deviceInfo : devices) { if (deviceInfo.address().toString() == addr) { connectToDevice(deviceInfo); return; } } } /*! * \brief BLEScanner::onDeviceDiscovered * When a new mac address was discovered * \param deviceInfo - The discovered device's information */ void BLEScanner::onDeviceDiscovered(const QBluetoothDeviceInfo& deviceInfo) { qDebug() << __FUNCTION__ << deviceInfo.address(); devices.append(deviceInfo); emit deviceDiscovered(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 scanForDevicesError(error); } /*! * \brief BLEScanner::onScanFinished * Called when the scan has completed */ void BLEScanner::onScanFinished() { emit scanFinished(); } /*! * \brief BLEScanner::scanForDevices * Tells the discovery agent to start scanning for devices */ void BLEScanner::scanForDevices() { discoveryAgent->start(); } /*! * \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) { QLowEnergyService* service = lowEnergyController->createServiceObject(uuid, this); services.append(service); if (uuid.toString() == omronUnknownServiceName) { omronUnknownService = service; } else if (uuid.toString() == omronBloodPressureServiceName) { omronBloodPressureService = service; } else if (uuid.toString() == omronCurrentTimeServiceName) { omronCurrentTimeService = service; } else if (uuid.toString() == omronBatteryLevelServiceName) { omronBatteryLevelService = service; } service->discoverDetails(); qDebug() << "Service " << service->serviceName() << "UUID: " << uuid << " state: " << service->state(); foreach (const QLowEnergyCharacteristic &c, service->characteristics()) { qDebug() << "----> Characteristic: " << c.name() << " uuid: " << c.uuid(); } } /*! * \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::requestBPMeasurement() { 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::serviceStateChanged(const QLowEnergyService::ServiceState &serviceState) { switch (serviceState) { case QLowEnergyService::ServiceDiscovered: { requestBPMeasurement(); 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; } parseMeasurement(byteArray); } /*! * \brief BLEScanner::parseMeasurement * Parses the BP and Pulse Rate measurement data * \param byteArray - the data to be parsed */ void BLEScanner::parseMeasurement(const QByteArray &byteArray) { bp_measurement_t 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(bp_measurement)); */ 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 receivedBPMeasurement(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::confirmedDescriptorWrite(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; } } /*! * \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::serviceCharacteristicsRead(const QLowEnergyCharacteristic &c,const QByteArray &byteArray) { qDebug() << __FUNCTION__ << c.name() << " data: " << byteArray; } /*! * \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() << "##############################"; foreach (QLowEnergyService* service, services) { connect(service, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), this, SLOT(serviceStateChanged(QLowEnergyService::ServiceState))); connect(service, SIGNAL(characteristicChanged(QLowEnergyCharacteristic, QByteArray)), this, SLOT(onCharacteristicChanged(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(characteristicRead(QLowEnergyCharacteristic,QByteArray)), this, SLOT(serviceCharacteristicsRead(QLowEnergyCharacteristic,QByteArray))); connect(service, SIGNAL(descriptorWritten(QLowEnergyDescriptor, QByteArray)), this, SLOT(confirmedDescriptorWrite(QLowEnergyDescriptor, QByteArray))); } } /*! * \brief BLEScanner::onControllerError * Called when the BLE Controller encounters an error * \param error - the error enum */ void BLEScanner::onControllerError(const QLowEnergyController::Error &error) { qDebug() << __FUNCTION__ << "LE controller error: " << error; } /*! * \brief BLEScanner::onDeviceConnected * Called when we successfully connect to a device. * Starts the scan for services on the device */ void BLEScanner::onDeviceConnected() { qDebug() << __FUNCTION__; lowEnergyController->discoverServices(); } /*! * \brief BLEScanner::onDeviceDisconnected * Called when we become disconnected from a device. */ void BLEScanner::onDeviceDisconnected() { qDebug() << __FUNCTION__; } /*! * \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::connectToDevice(const QBluetoothDeviceInfo& deviceInfo) { 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())); lowEnergyController->connectToDevice(); }