2017-06-25 05:00:52 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
2017-06-13 02:47:50 +00:00
|
|
|
#include <errno.h>
|
|
|
|
|
|
|
|
#include <QtBluetooth/QBluetoothAddress>
|
|
|
|
#include <QLowEnergyController>
|
2017-07-03 17:24:39 +00:00
|
|
|
#include <QLowEnergyService>
|
2017-06-27 13:58:36 +00:00
|
|
|
#include <QCoreApplication>
|
|
|
|
#include <QElapsedTimer>
|
2017-06-13 02:47:50 +00:00
|
|
|
#include <QEventLoop>
|
2017-06-27 13:58:36 +00:00
|
|
|
#include <QThread>
|
2017-06-13 02:47:50 +00:00
|
|
|
#include <QTimer>
|
|
|
|
#include <QDebug>
|
|
|
|
|
|
|
|
#include <libdivecomputer/version.h>
|
|
|
|
|
|
|
|
#include "libdivecomputer.h"
|
|
|
|
#include "core/qt-ble.h"
|
|
|
|
|
|
|
|
#if defined(SSRF_CUSTOM_IO)
|
|
|
|
|
|
|
|
#include <libdivecomputer/custom_io.h>
|
|
|
|
|
2017-07-04 00:46:22 +00:00
|
|
|
#define BLE_TIMEOUT 12000 // 12 seconds seems like a very long time to wait
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
extern "C" {
|
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
static int device_is_hw(dc_user_device_t *device);
|
|
|
|
|
2017-06-27 13:58:36 +00:00
|
|
|
void waitFor(int ms) {
|
|
|
|
Q_ASSERT(QCoreApplication::instance());
|
|
|
|
Q_ASSERT(QThread::currentThread());
|
|
|
|
|
|
|
|
QElapsedTimer timer;
|
|
|
|
timer.start();
|
|
|
|
|
|
|
|
do {
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, ms);
|
|
|
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
|
|
QThread::msleep(10);
|
|
|
|
} while (timer.elapsed() < ms);
|
|
|
|
}
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
void BLEObject::serviceStateChanged(QLowEnergyService::ServiceState s)
|
|
|
|
{
|
2017-06-28 03:53:11 +00:00
|
|
|
Q_UNUSED(s)
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
QList<QLowEnergyCharacteristic> list;
|
|
|
|
|
2017-06-27 12:56:30 +00:00
|
|
|
auto service = qobject_cast<QLowEnergyService*>(sender());
|
|
|
|
if (service)
|
|
|
|
list = service->characteristics();
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
Q_FOREACH(QLowEnergyCharacteristic c, list) {
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " " << c.uuid().toString();
|
2017-06-13 02:47:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void BLEObject::characteristcStateChanged(const QLowEnergyCharacteristic &c, const QByteArray &value)
|
|
|
|
{
|
2017-06-28 03:53:11 +00:00
|
|
|
Q_UNUSED(c)
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
receivedPackets.append(value);
|
|
|
|
waitForPacket.exit();
|
|
|
|
}
|
|
|
|
|
|
|
|
void BLEObject::writeCompleted(const QLowEnergyDescriptor &d, const QByteArray &value)
|
|
|
|
{
|
2017-06-28 03:53:11 +00:00
|
|
|
Q_UNUSED(d)
|
|
|
|
Q_UNUSED(value)
|
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << "BLE write completed";
|
2017-06-13 02:47:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void BLEObject::addService(const QBluetoothUuid &newService)
|
|
|
|
{
|
2017-06-27 11:50:19 +00:00
|
|
|
qDebug() << "Found service" << newService;
|
|
|
|
bool isStandardUuid = false;
|
|
|
|
newService.toUInt16(&isStandardUuid);
|
|
|
|
if (isStandardUuid) {
|
|
|
|
qDebug () << " .. ignoring standard service";
|
2017-06-13 02:47:50 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-06-27 11:50:19 +00:00
|
|
|
|
2017-06-27 12:56:30 +00:00
|
|
|
auto service = controller->createServiceObject(newService, this);
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " .. created service object" << service;
|
2017-06-13 02:47:50 +00:00
|
|
|
if (service) {
|
2017-06-27 12:56:30 +00:00
|
|
|
services.append(service);
|
2017-06-13 02:47:50 +00:00
|
|
|
connect(service, &QLowEnergyService::stateChanged, this, &BLEObject::serviceStateChanged);
|
|
|
|
connect(service, &QLowEnergyService::characteristicChanged, this, &BLEObject::characteristcStateChanged);
|
|
|
|
connect(service, &QLowEnergyService::descriptorWritten, this, &BLEObject::writeCompleted);
|
|
|
|
service->discoverDetails();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
BLEObject::BLEObject(QLowEnergyController *c, dc_user_device_t *d)
|
2017-06-13 02:47:50 +00:00
|
|
|
{
|
|
|
|
controller = c;
|
2017-06-27 22:14:27 +00:00
|
|
|
device = d;
|
2017-06-13 02:47:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
BLEObject::~BLEObject()
|
|
|
|
{
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << "Deleting BLE object";
|
2017-06-13 02:47:50 +00:00
|
|
|
}
|
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
/* Yeah, I could do the C++ inline member thing */
|
|
|
|
static int device_is_shearwater(dc_user_device_t *device)
|
|
|
|
{
|
|
|
|
return !strcmp(device->vendor, "Shearwater");
|
|
|
|
}
|
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
static int device_is_hw(dc_user_device_t *device)
|
|
|
|
{
|
|
|
|
return !strcmp(device->vendor, "Heinrichs Weikamp");
|
|
|
|
}
|
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
dc_status_t BLEObject::write(const void *data, size_t size, size_t *actual)
|
2017-06-13 02:47:50 +00:00
|
|
|
{
|
2017-06-28 03:53:11 +00:00
|
|
|
Q_UNUSED(actual) // that seems like it might cause problems
|
|
|
|
|
2017-06-27 12:56:30 +00:00
|
|
|
QList<QLowEnergyCharacteristic> list = preferredService()->characteristics();
|
2017-06-13 02:47:50 +00:00
|
|
|
QByteArray bytes((const char *)data, (int) size);
|
|
|
|
|
|
|
|
if (!list.isEmpty()) {
|
|
|
|
const QLowEnergyCharacteristic &c = list.constFirst();
|
|
|
|
QLowEnergyService::WriteMode mode;
|
|
|
|
|
|
|
|
mode = (c.properties() & QLowEnergyCharacteristic::WriteNoResponse) ?
|
|
|
|
QLowEnergyService::WriteWithoutResponse :
|
|
|
|
QLowEnergyService::WriteWithResponse;
|
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
if (device_is_shearwater(device))
|
|
|
|
bytes.prepend("\1\0", 2);
|
|
|
|
|
2017-06-27 12:56:30 +00:00
|
|
|
preferredService()->writeCharacteristic(c, bytes, mode);
|
2017-06-13 02:47:50 +00:00
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
}
|
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
dc_status_t BLEObject::read(void *data, size_t size, size_t *actual)
|
2017-06-13 02:47:50 +00:00
|
|
|
{
|
|
|
|
if (receivedPackets.isEmpty()) {
|
2017-06-27 12:56:30 +00:00
|
|
|
QList<QLowEnergyCharacteristic> list = preferredService()->characteristics();
|
2017-06-13 02:47:50 +00:00
|
|
|
if (list.isEmpty())
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
|
|
|
|
QTimer timer;
|
2017-07-04 00:46:22 +00:00
|
|
|
int msec = BLE_TIMEOUT;
|
2017-06-13 02:47:50 +00:00
|
|
|
timer.setSingleShot(true);
|
|
|
|
|
|
|
|
waitForPacket.connect(&timer, SIGNAL(timeout()), SLOT(quit()));
|
|
|
|
timer.start(msec);
|
|
|
|
waitForPacket.exec();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Still no packet?
|
|
|
|
if (receivedPackets.isEmpty())
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
|
|
|
|
QByteArray packet = receivedPackets.takeFirst();
|
2017-06-27 22:14:27 +00:00
|
|
|
|
|
|
|
if (device_is_shearwater(device))
|
|
|
|
packet.remove(0,2);
|
|
|
|
|
2017-06-28 03:53:11 +00:00
|
|
|
if (size > (size_t)packet.size())
|
2017-06-13 02:47:50 +00:00
|
|
|
size = packet.size();
|
|
|
|
memcpy(data, packet.data(), size);
|
|
|
|
*actual = size;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
int BLEObject::setupHwTerminalIo(QList<QLowEnergyCharacteristic> allC)
|
|
|
|
{ /* This initalizes the Terminal I/O client as described in
|
|
|
|
* http://www.telit.com/fileadmin/user_upload/products/Downloads/sr-rf/BlueMod/TIO_Implementation_Guide_r04.pdf
|
|
|
|
* Referenced section numbers below are from that document.
|
|
|
|
*
|
|
|
|
* This is for all HW computers, that use referenced BT/BLE hardware module from Telit
|
|
|
|
* (formerly Stollmann). The 16 bit UUID 0xFEFB (or a derived 128 bit UUID starting with
|
|
|
|
* 0x0000FEFB is a clear indication that the OSTC is equipped with this BT/BLE hardware.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (allC.length() != 4) {
|
|
|
|
qDebug() << "This should not happen. HW/OSTC BT/BLE device without 4 Characteristics";
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* The Terminal I/O client subscribes to indications of the UART credits TX
|
|
|
|
* characteristic (see 6.4).
|
|
|
|
*
|
|
|
|
* Notice that indications are subscribed to by writing 0x0200 to its descriptor. This
|
|
|
|
* can be understood by looking for Client Characteristic Configuration, Assigned
|
|
|
|
* Number: 0x2902. Enabling/Disabeling is setting the proper bit, and they
|
|
|
|
* differ for indications and notifications.
|
|
|
|
*/
|
|
|
|
QLowEnergyDescriptor d = allC[HW_OSTC_BLE_CREDITS_TX].descriptors().first();
|
|
|
|
preferredService()->writeDescriptor(d, QByteArray::fromHex("0200"));
|
|
|
|
|
|
|
|
/* The Terminal I/O client subscribes to notifications of the UART data TX
|
|
|
|
* characteristic (see 6.2).
|
|
|
|
*/
|
|
|
|
d = allC[HW_OSTC_BLE_DATA_TX].descriptors().first();
|
|
|
|
preferredService()->writeDescriptor(d, QByteArray::fromHex("0100"));
|
|
|
|
|
|
|
|
/* The Terminal I/O client transmits initial UART credits to the server (see 6.5).
|
|
|
|
*
|
|
|
|
* Notice that we have to write to the characteristic here, and not to its
|
|
|
|
* descriptor as for the enabeling of notifications or indications.
|
|
|
|
*/
|
|
|
|
isCharacteristicWritten = false;
|
|
|
|
preferredService()->writeCharacteristic(allC[HW_OSTC_BLE_CREDITS_RX],
|
|
|
|
QByteArray(1, 255),
|
|
|
|
QLowEnergyService::WriteWithResponse);
|
|
|
|
|
|
|
|
/* And give to OSTC some time to get initialized */
|
|
|
|
int msec = 5000;
|
|
|
|
while (msec > 0 && !isCharacteristicWritten) {
|
|
|
|
waitFor(100);
|
|
|
|
msec -= 100;
|
|
|
|
};
|
|
|
|
if (!isCharacteristicWritten)
|
|
|
|
return DC_STATUS_TIMEOUT;
|
|
|
|
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
dc_status_t qt_ble_open(dc_custom_io_t *io, dc_context_t *context, const char *devaddr)
|
|
|
|
{
|
2017-06-28 03:53:11 +00:00
|
|
|
Q_UNUSED(context)
|
2017-06-27 01:17:06 +00:00
|
|
|
/*
|
|
|
|
* LE-only devices get the "LE:" prepended by the scanning
|
|
|
|
* code, so that the rfcomm code can see they only do LE.
|
|
|
|
*
|
|
|
|
* We just skip that prefix (and it doesn't always exist,
|
|
|
|
* since the device may support both legacy BT and LE).
|
|
|
|
*/
|
|
|
|
if (!strncmp(devaddr, "LE:", 3))
|
|
|
|
devaddr += 3;
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
QBluetoothAddress remoteDeviceAddress(devaddr);
|
|
|
|
|
|
|
|
// HACK ALERT! Qt 5.9 needs this for proper Bluez operation
|
|
|
|
qputenv("QT_DEFAULT_CENTRAL_SERVICES", "1");
|
|
|
|
|
|
|
|
QLowEnergyController *controller = new QLowEnergyController(remoteDeviceAddress);
|
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << "qt_ble_open(" << devaddr << ")";
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-06-27 22:14:27 +00:00
|
|
|
if (device_is_shearwater(io->user_device))
|
|
|
|
controller->setRemoteAddressType(QLowEnergyController::RandomAddress);
|
|
|
|
|
2017-06-27 13:58:36 +00:00
|
|
|
// Try to connect to the device
|
|
|
|
controller->connectToDevice();
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
// Create a timer. If the connection doesn't succeed after five seconds or no error occurs then stop the opening step
|
2017-07-04 00:46:22 +00:00
|
|
|
int msec = BLE_TIMEOUT;
|
2017-06-27 13:58:36 +00:00
|
|
|
while (msec > 0 && controller->state() == QLowEnergyController::ConnectingState) {
|
|
|
|
waitFor(100);
|
|
|
|
msec -= 100;
|
|
|
|
};
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
switch (controller->state()) {
|
|
|
|
case QLowEnergyController::ConnectedState:
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << "connected to the controller for device" << devaddr;
|
2017-06-13 02:47:50 +00:00
|
|
|
break;
|
|
|
|
default:
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << "failed to connect to the controller " << devaddr << "with error" << controller->errorString();
|
2017-06-13 02:47:50 +00:00
|
|
|
report_error("Failed to connect to %s: '%s'", devaddr, controller->errorString().toUtf8().data());
|
|
|
|
controller->disconnectFromDevice();
|
|
|
|
delete controller;
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* We need to discover services etc here! */
|
2017-06-27 22:14:27 +00:00
|
|
|
BLEObject *ble = new BLEObject(controller, io->user_device);
|
2017-06-13 02:47:50 +00:00
|
|
|
ble->connect(controller, SIGNAL(serviceDiscovered(QBluetoothUuid)), SLOT(addService(QBluetoothUuid)));
|
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " .. discovering services";
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
controller->discoverServices();
|
2017-06-27 13:58:36 +00:00
|
|
|
|
2017-07-04 00:46:22 +00:00
|
|
|
msec = BLE_TIMEOUT;
|
2017-06-27 13:58:36 +00:00
|
|
|
while (msec > 0 && controller->state() == QLowEnergyController::DiscoveringState) {
|
|
|
|
waitFor(100);
|
|
|
|
msec -= 100;
|
|
|
|
};
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " .. done discovering services";
|
2017-06-27 13:58:36 +00:00
|
|
|
if (ble->preferredService() == nullptr) {
|
|
|
|
qDebug() << "failed to find suitable service on" << devaddr;
|
|
|
|
report_error("Failed to find suitable service on '%s'", devaddr);
|
|
|
|
controller->disconnectFromDevice();
|
|
|
|
delete controller;
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
}
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " .. discovering details";
|
2017-07-04 00:46:22 +00:00
|
|
|
msec = BLE_TIMEOUT;
|
2017-06-27 13:58:36 +00:00
|
|
|
while (msec > 0 && ble->preferredService()->state() == QLowEnergyService::DiscoveringServices) {
|
|
|
|
waitFor(100);
|
|
|
|
msec -= 100;
|
|
|
|
};
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-06-27 13:58:36 +00:00
|
|
|
if (ble->preferredService()->state() != QLowEnergyService::ServiceDiscovered) {
|
2017-06-27 12:56:30 +00:00
|
|
|
qDebug() << "failed to find suitable service on" << devaddr;
|
|
|
|
report_error("Failed to find suitable service on '%s'", devaddr);
|
|
|
|
controller->disconnectFromDevice();
|
|
|
|
delete controller;
|
|
|
|
return DC_STATUS_IO;
|
|
|
|
}
|
|
|
|
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-06-25 05:32:47 +00:00
|
|
|
qDebug() << " .. enabling notifications";
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
/* Enable notifications */
|
2017-06-27 12:56:30 +00:00
|
|
|
QList<QLowEnergyCharacteristic> list = ble->preferredService()->characteristics();
|
2017-06-13 02:47:50 +00:00
|
|
|
|
|
|
|
if (!list.isEmpty()) {
|
|
|
|
const QLowEnergyCharacteristic &c = list.constLast();
|
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
if (device_is_hw(io->user_device)) {
|
|
|
|
ble->setupHwTerminalIo(list);
|
|
|
|
} else {
|
|
|
|
QList<QLowEnergyDescriptor> l = c.descriptors();
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
qDebug() << "Descriptor list with" << l.length() << "elements";
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
QLowEnergyDescriptor d;
|
|
|
|
foreach(d, l)
|
|
|
|
qDebug() << "Descriptor:" << d.name() << "uuid:" << d.uuid().toString();
|
2017-06-25 05:32:47 +00:00
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
if (!l.isEmpty()) {
|
|
|
|
d = l.first();
|
|
|
|
qDebug() << "now writing \"0x0100\" to the first descriptor";
|
2017-06-13 02:47:50 +00:00
|
|
|
|
2017-07-03 17:24:39 +00:00
|
|
|
ble->preferredService()->writeDescriptor(d, QByteArray::fromHex("0100"));
|
|
|
|
}
|
2017-06-13 02:47:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fill in info
|
|
|
|
io->userdata = (void *)ble;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
dc_status_t qt_ble_close(dc_custom_io_t *io)
|
|
|
|
{
|
|
|
|
BLEObject *ble = (BLEObject *) io->userdata;
|
|
|
|
|
|
|
|
io->userdata = NULL;
|
|
|
|
delete ble;
|
|
|
|
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
dc_status_t qt_ble_read(dc_custom_io_t *io, void* data, size_t size, size_t *actual)
|
|
|
|
{
|
|
|
|
BLEObject *ble = (BLEObject *) io->userdata;
|
|
|
|
return ble->read(data, size, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
dc_status_t qt_ble_write(dc_custom_io_t *io, const void* data, size_t size, size_t *actual)
|
|
|
|
{
|
|
|
|
BLEObject *ble = (BLEObject *) io->userdata;
|
|
|
|
return ble->write(data, size, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
} /* extern "C" */
|
|
|
|
#endif
|