// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "devicehandler.h" #include "deviceinfo.h" #include "heartrate-global.h" #include #include DeviceHandler::DeviceHandler(QObject *parent) : BluetoothBaseClass(parent) { if (simulator) { m_demoTimer.setSingleShot(false); m_demoTimer.setInterval(2000); connect(&m_demoTimer, &QTimer::timeout, this, &DeviceHandler::updateDemoHR); m_demoTimer.start(); updateDemoHR(); } } void DeviceHandler::setAddressType(AddressType type) { switch (type) { case DeviceHandler::AddressType::PublicAddress: m_addressType = QLowEnergyController::PublicAddress; break; case DeviceHandler::AddressType::RandomAddress: m_addressType = QLowEnergyController::RandomAddress; break; } } DeviceHandler::AddressType DeviceHandler::addressType() const { if (m_addressType == QLowEnergyController::RandomAddress) return DeviceHandler::AddressType::RandomAddress; return DeviceHandler::AddressType::PublicAddress; } void DeviceHandler::setDevice(DeviceInfo *device) { clearMessages(); m_currentDevice = device; if (simulator) { setInfo(tr("Demo device connected.")); setIcon(IconBluetooth); return; } // Disconnect and delete old connection if (m_control) { m_control->disconnectFromDevice(); delete m_control; m_control = nullptr; } // Create new controller and connect it if device available if (m_currentDevice) { // Make connections //! [Connect-Signals-1] m_control = QLowEnergyController::createCentral(m_currentDevice->getDevice(), this); //! [Connect-Signals-1] m_control->setRemoteAddressType(m_addressType); //! [Connect-Signals-2] connect(m_control, &QLowEnergyController::serviceDiscovered, this, &DeviceHandler::serviceDiscovered); connect(m_control, &QLowEnergyController::discoveryFinished, this, &DeviceHandler::serviceScanDone); connect(m_control, &QLowEnergyController::errorOccurred, this, [this](QLowEnergyController::Error error) { Q_UNUSED(error); setError("Cannot connect to remote device."); setIcon(IconError); }); connect(m_control, &QLowEnergyController::connected, this, [this]() { setInfo("Controller connected. Search services..."); setIcon(IconProgress); m_control->discoverServices(); }); connect(m_control, &QLowEnergyController::disconnected, this, [this]() { setError("LowEnergy controller disconnected"); setIcon(IconError); }); // Connect m_control->connectToDevice(); //! [Connect-Signals-2] } } void DeviceHandler::startMeasurement() { if (alive()) { m_start = QDateTime::currentDateTime(); m_min = 0; m_max = 0; m_avg = 0; m_sum = 0; m_calories = 0; m_measuring = true; m_measurements.clear(); emit measuringChanged(); } } void DeviceHandler::stopMeasurement() { m_measuring = false; emit measuringChanged(); } //! [Filter HeartRate service 1] void DeviceHandler::serviceDiscovered(const QBluetoothUuid &gatt) { if (gatt == QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate)) { setInfo("Heart Rate service discovered. Waiting for service scan to be done..."); setIcon(IconProgress); m_foundHeartRateService = true; } } //! [Filter HeartRate service 1] void DeviceHandler::serviceScanDone() { setInfo("Service scan done."); setIcon(IconBluetooth); // Delete old service if available if (m_service) { delete m_service; m_service = nullptr; } //! [Filter HeartRate service 2] // If heartRateService found, create new service if (m_foundHeartRateService) m_service = m_control->createServiceObject(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::HeartRate), this); if (m_service) { connect(m_service, &QLowEnergyService::stateChanged, this, &DeviceHandler::serviceStateChanged); connect(m_service, &QLowEnergyService::characteristicChanged, this, &DeviceHandler::updateHeartRateValue); connect(m_service, &QLowEnergyService::descriptorWritten, this, &DeviceHandler::confirmedDescriptorWrite); m_service->discoverDetails(); } else { setError("Heart Rate Service not found."); setIcon(IconError); } //! [Filter HeartRate service 2] } // Service functions //! [Find HRM characteristic] void DeviceHandler::serviceStateChanged(QLowEnergyService::ServiceState s) { switch (s) { case QLowEnergyService::RemoteServiceDiscovering: setInfo(tr("Discovering services...")); setIcon(IconProgress); break; case QLowEnergyService::RemoteServiceDiscovered: { setInfo(tr("Service discovered.")); setIcon(IconBluetooth); const QLowEnergyCharacteristic hrChar = m_service->characteristic(QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement)); if (!hrChar.isValid()) { setError("HR Data not found."); setIcon(IconError); break; } m_notificationDesc = hrChar.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); if (m_notificationDesc.isValid()) m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100")); break; } default: //nothing for now break; } emit aliveChanged(); } //! [Find HRM characteristic] //! [Reading value] void DeviceHandler::updateHeartRateValue(const QLowEnergyCharacteristic &c, const QByteArray &value) { // ignore any other characteristic change -> shouldn't really happen though if (c.uuid() != QBluetoothUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement)) return; auto data = reinterpret_cast(value.constData()); quint8 flags = *data; //Heart Rate int hrvalue = 0; if (flags & 0x1) // HR 16 bit? otherwise 8 bit hrvalue = static_cast(qFromLittleEndian(data[1])); else hrvalue = static_cast(data[1]); addMeasurement(hrvalue); } //! [Reading value] void DeviceHandler::updateDemoHR() { int randomValue = 0; if (m_currentValue < 30) // Initial value randomValue = 55 + QRandomGenerator::global()->bounded(30); else if (!m_measuring) // Value when relax randomValue = qBound(55, m_currentValue - 2 + QRandomGenerator::global()->bounded(5), 75); else // Measuring randomValue = m_currentValue + QRandomGenerator::global()->bounded(10) - 2; addMeasurement(randomValue); } void DeviceHandler::confirmedDescriptorWrite(const QLowEnergyDescriptor &d, const QByteArray &value) { if (d.isValid() && d == m_notificationDesc && value == QByteArray::fromHex("0000")) { //disabled notifications -> assume disconnect intent m_control->disconnectFromDevice(); delete m_service; m_service = nullptr; } } void DeviceHandler::disconnectService() { m_foundHeartRateService = false; //disable notifications if (m_notificationDesc.isValid() && m_service && m_notificationDesc.value() == QByteArray::fromHex("0100")) { m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0000")); } else { if (m_control) m_control->disconnectFromDevice(); delete m_service; m_service = nullptr; } } bool DeviceHandler::measuring() const { return m_measuring; } bool DeviceHandler::alive() const { if (simulator) return true; if (m_service) return m_service->state() == QLowEnergyService::RemoteServiceDiscovered; return false; } int DeviceHandler::hr() const { return m_currentValue; } int DeviceHandler::time() const { return m_start.secsTo(m_stop); } int DeviceHandler::maxHR() const { return m_max; } int DeviceHandler::minHR() const { return m_min; } float DeviceHandler::average() const { return m_avg; } float DeviceHandler::calories() const { return m_calories; } void DeviceHandler::addMeasurement(int value) { m_currentValue = value; // If measuring and value is appropriate if (m_measuring && value > 30 && value < 250) { m_stop = QDateTime::currentDateTime(); m_measurements << value; m_min = m_min == 0 ? value : qMin(value, m_min); m_max = qMax(value, m_max); m_sum += value; m_avg = (double)m_sum / m_measurements.size(); m_calories = ((-55.0969 + (0.6309 * m_avg) + (0.1988 * 94) + (0.2017 * 24)) / 4.184) * 60 * time() / 3600; } emit statsChanged(); }