| /*************************************************************************** |
| ** |
| ** Copyright (C) 2017 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the examples of the QtBluetooth module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:BSD$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** BSD License Usage |
| ** Alternatively, you may use this file under the terms of the BSD license |
| ** as follows: |
| ** |
| ** "Redistribution and use in source and binary forms, with or without |
| ** modification, are permitted provided that the following conditions are |
| ** met: |
| ** * Redistributions of source code must retain the above copyright |
| ** notice, this list of conditions and the following disclaimer. |
| ** * Redistributions in binary form must reproduce the above copyright |
| ** notice, this list of conditions and the following disclaimer in |
| ** the documentation and/or other materials provided with the |
| ** distribution. |
| ** * Neither the name of The Qt Company Ltd nor the names of its |
| ** contributors may be used to endorse or promote products derived |
| ** from this software without specific prior written permission. |
| ** |
| ** |
| ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "heartrate-global.h" |
| #include "devicehandler.h" |
| #include "deviceinfo.h" |
| #include <QtEndian> |
| #include <QRandomGenerator> |
| |
| DeviceHandler::DeviceHandler(QObject *parent) : |
| BluetoothBaseClass(parent), |
| m_foundHeartRateService(false), |
| m_measuring(false), |
| m_currentValue(0), |
| m_min(0), m_max(0), m_sum(0), m_avg(0), m_calories(0) |
| { |
| #ifdef SIMULATOR |
| m_demoTimer.setSingleShot(false); |
| m_demoTimer.setInterval(2000); |
| connect(&m_demoTimer, &QTimer::timeout, this, &DeviceHandler::updateDemoHR); |
| m_demoTimer.start(); |
| updateDemoHR(); |
| #endif |
| } |
| |
| 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; |
| |
| #ifdef SIMULATOR |
| setInfo(tr("Demo device connected.")); |
| return; |
| #endif |
| |
| // 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, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error), |
| this, [this](QLowEnergyController::Error error) { |
| Q_UNUSED(error); |
| setError("Cannot connect to remote device."); |
| }); |
| connect(m_control, &QLowEnergyController::connected, this, [this]() { |
| setInfo("Controller connected. Search services..."); |
| m_control->discoverServices(); |
| }); |
| connect(m_control, &QLowEnergyController::disconnected, this, [this]() { |
| setError("LowEnergy controller disconnected"); |
| }); |
| |
| // 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::HeartRate)) { |
| setInfo("Heart Rate service discovered. Waiting for service scan to be done..."); |
| m_foundHeartRateService = true; |
| } |
| } |
| //! [Filter HeartRate service 1] |
| |
| void DeviceHandler::serviceScanDone() |
| { |
| setInfo("Service scan done."); |
| |
| // 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::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."); |
| } |
| //! [Filter HeartRate service 2] |
| } |
| |
| // Service functions |
| //! [Find HRM characteristic] |
| void DeviceHandler::serviceStateChanged(QLowEnergyService::ServiceState s) |
| { |
| switch (s) { |
| case QLowEnergyService::DiscoveringServices: |
| setInfo(tr("Discovering services...")); |
| break; |
| case QLowEnergyService::ServiceDiscovered: |
| { |
| setInfo(tr("Service discovered.")); |
| |
| const QLowEnergyCharacteristic hrChar = m_service->characteristic(QBluetoothUuid(QBluetoothUuid::HeartRateMeasurement)); |
| if (!hrChar.isValid()) { |
| setError("HR Data not found."); |
| break; |
| } |
| |
| m_notificationDesc = hrChar.descriptor(QBluetoothUuid::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::HeartRateMeasurement)) |
| return; |
| |
| auto data = reinterpret_cast<const quint8 *>(value.constData()); |
| quint8 flags = *data; |
| |
| //Heart Rate |
| int hrvalue = 0; |
| if (flags & 0x1) // HR 16 bit? otherwise 8 bit |
| hrvalue = static_cast<int>(qFromLittleEndian<quint16>(data[1])); |
| else |
| hrvalue = static_cast<int>(data[1]); |
| |
| addMeasurement(hrvalue); |
| } |
| //! [Reading value] |
| |
| #ifdef SIMULATOR |
| 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); |
| } |
| #endif |
| |
| 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 |
| { |
| #ifdef SIMULATOR |
| return true; |
| #endif |
| |
| if (m_service) |
| return m_service->state() == QLowEnergyService::ServiceDiscovered; |
| |
| 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(); |
| } |