/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2016-2018 Simon Stürz * * * * This file is part of nymea. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation; either * * version 2.1 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with this library; If not, see * * . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "nuimo.h" #include "extern-plugininfo.h" #include #include static QBluetoothUuid ledMatrinxServiceUuid = QBluetoothUuid(QUuid("f29b1523-cb19-40f3-be5c-7241ecb82fd1")); static QBluetoothUuid ledMatrixCharacteristicUuid = QBluetoothUuid(QUuid("f29b1524-cb19-40f3-be5c-7241ecb82fd1")); static QBluetoothUuid inputServiceUuid = QBluetoothUuid(QUuid("f29b1525-cb19-40f3-be5c-7241ecb82fd2")); static QBluetoothUuid inputSwipeCharacteristicUuid = QBluetoothUuid(QUuid("f29b1527-cb19-40f3-be5c-7241ecb82fd2")); static QBluetoothUuid inputRotationCharacteristicUuid = QBluetoothUuid(QUuid("f29b1528-cb19-40f3-be5c-7241ecb82fd2")); static QBluetoothUuid inputButtonCharacteristicUuid = QBluetoothUuid(QUuid("f29b1529-cb19-40f3-be5c-7241ecb82fd2")); Nuimo::Nuimo(BluetoothLowEnergyDevice *bluetoothDevice, QObject *parent) : QObject(parent), m_bluetoothDevice(bluetoothDevice) { connect(m_bluetoothDevice, &BluetoothLowEnergyDevice::connectedChanged, this, &Nuimo::onConnectedChanged); connect(m_bluetoothDevice, &BluetoothLowEnergyDevice::servicesDiscoveryFinished, this, &Nuimo::onServiceDiscoveryFinished); if (!m_longPressTimer) { m_longPressTimer = new QTimer(this); m_longPressTimer->setSingleShot(true); } connect(m_longPressTimer, &QTimer::timeout, this, &Nuimo::onLongPressTimer); } BluetoothLowEnergyDevice *Nuimo::bluetoothDevice() { return m_bluetoothDevice; } void Nuimo::showImage(const Nuimo::MatrixType &matrixType) { QByteArray matrix; int time = 3; switch (matrixType) { case MatrixTypeUp: matrix = QByteArray( " * " " *** " " * * * " " * * * " "* * *" " * " " * " " * " " * "); time = 3; break; case MatrixTypeDown: matrix = QByteArray( " * " " * " " * " " * " "* * *" " * * * " " * * * " " *** " " * "); time = 3; break; case MatrixTypeLeft: matrix = QByteArray( " * " " * " " * " " * " "*********" " * " " * " " * " " * "); time = 3; break; case MatrixTypeRight: matrix = QByteArray( " * " " * " " * " " * " "*********" " * " " * " " * " " * "); time = 3; break; case MatrixTypePlay: matrix = QByteArray( " " " * " " ** " " *** " " **** " " *** " " ** " " * " " "); time = 3; break; case MatrixTypePause: matrix = QByteArray( " " " " " ** ** " " ** ** " " ** ** " " ** ** " " ** ** " " " " "); time = 3; break; case MatrixTypeStop: matrix = QByteArray( " " " " " ***** " " ***** " " ***** " " ***** " " ***** " " " " "); time = 3; break; case MatrixTypeMusic: matrix = QByteArray( " *******" " *******" " * *" " * *" " * *" " * *" " ** **" "*** ***" " * * "); time = 5; break; case MatrixTypeHeart: matrix = QByteArray( " " " ** ** " " ******* " "*********" "*********" " ******* " " ***** " " *** " " * "); time = 5; break; case MatrixTypeNext: matrix = QByteArray( " " " * ** " " ** ** " " *** ** " " ****** " " *** ** " " ** ** " " * ** " " "); time = 5; break; case MatrixTypePrevious: matrix = QByteArray( " " " ** * " " ** ** " " ** *** " " ****** " " ** *** " " ** ** " " ** * " " "); time = 5; break; case MatrixTypeCircle: matrix = QByteArray( " " " " " *** " " * * " " * * " " * * " " *** " " " " "); time = 5; break; case MatrixTypeFilledCircle: matrix = QByteArray( " " " " " *** " " ***** " " ***** " " ***** " " *** " " " " "); time = 5; break; case MatrixTypeLight: matrix = QByteArray( " " " *** " " * * " " * * " " * * " " *** " " *** " " *** " " * "); time = 5; break; } showMatrix(matrix, time); } void Nuimo::setLongPressTime(int milliSeconds) { m_longPressTime = milliSeconds; } void Nuimo::showMatrix(const QByteArray &matrix, const int &seconds) { if (!m_ledMatrixService) return; QBitArray bits; bits.resize(81); for (int i = 0; i < matrix.size(); i++) { if (matrix.at(i) != ' ') { bits[i] = true; } } QByteArray bytes; bytes.resize(bits.count() / 8 + 1); bytes.fill(0); // Convert from QBitArray to QByteArray for(int b = 0; b < bits.count(); ++b) bytes[b / 8] = (bytes.at(b / 8) | ((bits[b] ? 1 : 0) << (b % 8))); bytes.append(QByteArray::fromHex("FF")); quint8 time = quint8(seconds * 10); bytes.append((char)time); m_ledMatrixService->writeCharacteristic(m_ledMatrixCharacteristic, bytes); } void Nuimo::printService(QLowEnergyService *service) { foreach (const QLowEnergyCharacteristic &characteristic, service->characteristics()) { qCDebug(dcSenic()) << " -->" << characteristic.name() << characteristic.uuid().toString() << characteristic.value(); foreach (const QLowEnergyDescriptor &descriptor, characteristic.descriptors()) { qCDebug(dcSenic()) << " -->" << descriptor.name() << descriptor.uuid().toString() << descriptor.value(); } } } void Nuimo::onLongPressTimer() { emit buttonLongPressed(); } void Nuimo::onConnectedChanged(bool connected) { qCDebug(dcSenic()) << m_bluetoothDevice->name() << m_bluetoothDevice->address().toString() << (connected ? "connected" : "disconnected"); m_longPressTimer->stop(); emit connectedChanged(connected); if (!connected) { // Clean up services m_deviceInfoService->deleteLater(); // FIXME: As of BlueZ 5.48, the Battery service isn't expose in the same way any more. // Until that's fixed m_batteryService might never be initialized if (m_batteryService) { m_batteryService->deleteLater(); } m_ledMatrixService->deleteLater(); m_inputService->deleteLater(); m_deviceInfoService = nullptr; m_batteryService = nullptr; m_ledMatrixService = nullptr; m_inputService = nullptr; } } void Nuimo::onServiceDiscoveryFinished() { qCDebug(dcSenic()) << "Service scan finised"; if (!m_bluetoothDevice->serviceUuids().contains(QBluetoothUuid::DeviceInformation)) { qCWarning(dcSenic()) << "Device Information service not found for device" << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); emit deviceInitializationFinished(false); return; } // FIXME: As of BlueZ 5.48, the Battery service isn't expose in the same way any more. // For now we're deactivating this check to make the Nuimo still work despite the broken battery info // if (!m_bluetoothDevice->serviceUuids().contains(QBluetoothUuid::BatteryService)) { // qCWarning(dcSenic()) << "Battery service not found for device" << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); // emit deviceInitializationFinished(false); // return; // } if (!m_bluetoothDevice->serviceUuids().contains(ledMatrinxServiceUuid)) { qCWarning(dcSenic()) << "Led matrix service not found for device" << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); emit deviceInitializationFinished(false); return; } if (!m_bluetoothDevice->serviceUuids().contains(inputServiceUuid)) { qCWarning(dcSenic()) << "Input service not found for device" << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); emit deviceInitializationFinished(false); return; } // Device info service if (!m_deviceInfoService) { m_deviceInfoService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid::DeviceInformation, this); if (!m_deviceInfoService) { qCWarning(dcSenic()) << "Could not create device info service."; emit deviceInitializationFinished(false); return; } connect(m_deviceInfoService, &QLowEnergyService::stateChanged, this, &Nuimo::onDeviceInfoServiceStateChanged); if (m_deviceInfoService->state() == QLowEnergyService::DiscoveryRequired) { m_deviceInfoService->discoverDetails(); } } // Battery service if (!m_batteryService) { m_batteryService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid::BatteryService, this); if (!m_batteryService) { qCWarning(dcSenic()) << "Could not create battery service."; // FIXME: As of BlueZ 5.48, the Battery service isn't expose in the same way any more. // For now we're deactivating this check to make the Nuimo still work despite the broken battery info // emit deviceInitializationFinished(false); // return; } else { connect(m_batteryService, &QLowEnergyService::stateChanged, this, &Nuimo::onBatteryServiceStateChanged); connect(m_batteryService, &QLowEnergyService::characteristicChanged, this, &Nuimo::onBatteryCharacteristicChanged); if (m_batteryService->state() == QLowEnergyService::DiscoveryRequired) { m_batteryService->discoverDetails(); } } } // Input service if (!m_inputService) { m_inputService = m_bluetoothDevice->controller()->createServiceObject(inputServiceUuid, this); if (!m_inputService) { qCWarning(dcSenic()) << "Could not create input service."; emit deviceInitializationFinished(false); return; } connect(m_inputService, &QLowEnergyService::stateChanged, this, &Nuimo::onInputServiceStateChanged); connect(m_inputService, &QLowEnergyService::characteristicChanged, this, &Nuimo::onInputCharacteristicChanged); if (m_inputService->state() == QLowEnergyService::DiscoveryRequired) { m_inputService->discoverDetails(); } } // Input service if (!m_ledMatrixService) { m_ledMatrixService = m_bluetoothDevice->controller()->createServiceObject(ledMatrinxServiceUuid, this); if (!m_ledMatrixService) { qCWarning(dcSenic()) << "Could not create led matrix service."; emit deviceInitializationFinished(false); return; } connect(m_ledMatrixService, &QLowEnergyService::stateChanged, this, &Nuimo::onLedMatrixServiceStateChanged); if (m_ledMatrixService->state() == QLowEnergyService::DiscoveryRequired) { m_ledMatrixService->discoverDetails(); } } emit deviceInitializationFinished(true); } void Nuimo::onDeviceInfoServiceStateChanged(const QLowEnergyService::ServiceState &state) { // Only continue if discovered if (state != QLowEnergyService::ServiceDiscovered) return; qCDebug(dcSenic()) << "Device info service discovered."; printService(m_deviceInfoService); QString firmware = QString::fromUtf8(m_deviceInfoService->characteristic(QBluetoothUuid::FirmwareRevisionString).value()); QString hardware = QString::fromUtf8(m_deviceInfoService->characteristic(QBluetoothUuid::HardwareRevisionString).value()); QString software = QString::fromUtf8(m_deviceInfoService->characteristic(QBluetoothUuid::SoftwareRevisionString).value()); emit deviceInformationChanged(firmware, hardware, software); } void Nuimo::onBatteryServiceStateChanged(const QLowEnergyService::ServiceState &state) { // Only continue if discovered if (state != QLowEnergyService::ServiceDiscovered) return; qCDebug(dcSenic()) << "Battery service discovered."; printService(m_batteryService); m_batteryCharacteristic = m_batteryService->characteristic(QBluetoothUuid::BatteryLevel); if (!m_batteryCharacteristic.isValid()) { qCWarning(dcSenic()) << "Battery characteristc not found for device " << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); return; } // Enable notifications QLowEnergyDescriptor notificationDescriptor = m_batteryCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); m_batteryService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100")); uint batteryPercentage = m_batteryCharacteristic.value().toHex().toUInt(nullptr, 16); emit batteryValueChanged(batteryPercentage); } void Nuimo::onBatteryCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) { if (characteristic.uuid() == m_batteryCharacteristic.uuid()) { uint batteryPercentage = value.toHex().toUInt(nullptr, 16); emit batteryValueChanged(batteryPercentage); } } void Nuimo::onInputServiceStateChanged(const QLowEnergyService::ServiceState &state) { // Only continue if discovered if (state != QLowEnergyService::ServiceDiscovered) return; qCDebug(dcSenic()) << "Input service discovered."; printService(m_inputService); // Button m_inputButtonCharacteristic = m_inputService->characteristic(inputButtonCharacteristicUuid); if (!m_inputButtonCharacteristic.isValid()) { qCWarning(dcSenic()) << "Input button characteristc not found for device " << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); return; } // Enable notifications QLowEnergyDescriptor notificationDescriptor = m_inputButtonCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); m_inputService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100")); // Swipe m_inputSwipeCharacteristic = m_inputService->characteristic(inputSwipeCharacteristicUuid); if (!m_inputSwipeCharacteristic.isValid()) { qCWarning(dcSenic()) << "Input swipe characteristc not found for device " << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); return; } // Enable notifications notificationDescriptor = m_inputSwipeCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); m_inputService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100")); // Rotation m_inputRotationCharacteristic = m_inputService->characteristic(inputRotationCharacteristicUuid); if (!m_inputRotationCharacteristic.isValid()) { qCWarning(dcSenic()) << "Input rotation characteristc not found for device " << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); return; } // Enable notifications notificationDescriptor = m_inputRotationCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); m_inputService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100")); } void Nuimo::onInputCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value) { if (characteristic.uuid() == m_inputButtonCharacteristic.uuid()) { bool pressed = (bool)value.toHex().toUInt(nullptr, 16); qCDebug(dcSenic()) << "Button:" << (pressed ? "pressed": "released"); if (pressed) { m_longPressTimer->start(m_longPressTime); } else { if (m_longPressTimer->isActive()) { m_longPressTimer->stop(); emit buttonPressed(); } // else the time run out and has the long pressed event emittted } return; } if (characteristic.uuid() == m_inputSwipeCharacteristic.uuid()) { quint8 swipe = (quint8)value.toHex().toUInt(nullptr, 16); switch (swipe) { case 0: qCDebug(dcSenic()) << "Swipe: Left"; emit swipeDetected(SwipeDirectionLeft); break; case 1: qCDebug(dcSenic()) << "Swipe: Right"; emit swipeDetected(SwipeDirectionRight); break; case 2: qCDebug(dcSenic()) << "Swipe: Up"; emit swipeDetected(SwipeDirectionUp); break; case 3: qCDebug(dcSenic()) << "Swipe: Down"; emit swipeDetected(SwipeDirectionDown); break; default: break; } return; } if (characteristic.uuid() == m_inputRotationCharacteristic.uuid()) { qint16 intValue = qFromLittleEndian((uchar *)value.constData()); qCDebug(dcSenic()) << "Rotation" << value.toHex() << intValue; int finalValue = m_rotationValue + qRound(intValue / 10.0); if (finalValue <= 0) { m_rotationValue = 0; } else if (finalValue >= 100) { m_rotationValue = 100; } else { m_rotationValue = finalValue; } emit rotationValueChanged(m_rotationValue); return; } qCDebug(dcSenic()) << "Service characteristic changed" << characteristic.name() << value.toHex(); } void Nuimo::onLedMatrixServiceStateChanged(const QLowEnergyService::ServiceState &state) { // Only continue if discovered if (state != QLowEnergyService::ServiceDiscovered) return; qCDebug(dcSenic()) << "Led matrix service discovered."; printService(m_ledMatrixService); // Led matrix m_ledMatrixCharacteristic = m_ledMatrixService->characteristic(ledMatrixCharacteristicUuid); if (!m_ledMatrixCharacteristic.isValid()) { qCWarning(dcSenic()) << "Led matrix characteristc not found for device " << bluetoothDevice()->name() << bluetoothDevice()->address().toString(); return; } }