So I currently have the BLE GATT downloading working on both my Suunto
EON Steel and the Scubapro G2. This is to see if anybody else wants to
play with the code, but before looking at the code, a few faily big
caveats:

 (a) BLE is not fast. Just be warned. You *will* be happier with a USB
cable if you have that option.

 (b) divecomputers are odd, and not a lot of people seem to play with
them over bluetooth. At least Suunto seems to consider the BLE to be
mobile-only.  On desktop systems, bluetooth seems to be pretty hit and
miss outside of legacy mouse/keyboard/audio things. Odd BLE devices?
Yeah, bugs will happen.

 (c) I only work with Linux, and I don't do mobile. End result: right
now the code only works on Linux laptops and desktops with bluetooth.
A regular BT-4.0 USB dongle works fine for me on my desktop, and my
laptop has Intel wireless with BT that also works. So as long as it's
LE-capable (and not all BT dongles are), it probably works.

 (d) ...but I found several "features" due to (b), and you'll not only
need a fairly up-to-date Qt installation, you may need to literally
build your own due to Qt bugs (the EON Steel in particular is very
picky, and Qt/Bluez didn't connect to it without patches).

 (e) my BLE code is not pretty. It's scary. In fact, it can probably
be used to make small timid pets pee on the floor in fright. You have
been warned.

With those encouraging words, here's the scoop:

If using Qt, use at least 5.9.1, and if you want to play with the EON
Steel, make it work on some non-Bluez platform (which has so far never
been tested - Bluez is what the Linux desktop environments use), and
use the patch from here:

    https://codereview.qt-project.org/#/c/198202/

You also need to make sure that you have "device privacy" enabled, at
least in order to pair with the EON Steel. It will not talk to you
unless you have a device identity key ("IRK"). With Bluez (ie Linux
desktop environment), this involves making sure that you have

    Privacy = device

in your bluetooth config file (normally /etc/bluetooth/main.conf, and
normally it it set to "off").

Also, make sure that you have the current libdivecomputer
Subsurface-branch, which has the code for the EON Steel and the
Scubapro G2 to know the differences between a USB HID download and a
BLE GATT download.

And if y ou do all that, and if you apply the attached patch to the
current subsurface source base, and if you walk around three times
widdershins around your computer while you build it all, you *may*
just get working BLE GATT downloads.

I'll hopefully look at the Shearwater Perdix AI next. Since BLE is the
*only* way to download from that dive computer, it would be really
nice if I can make this work for that too. We'll see.

                          Linus
From 43d17083dee67bcfac9e2d9f1a2a84011bc40113 Mon Sep 17 00:00:00 2001
From: Linus Torvalds <torva...@linux-foundation.org>
Date: Mon, 12 Jun 2017 19:47:50 -0700
Subject: [PATCH] Very early and likely quite broken BLE GATT code

This is some very early and hacky code to be able to access BLE-enabled
dive computers that use the GATT protocol to send packets back and forth
(which seems to be pretty much all of them: a vendor-specific GATT
service with a write characteristic and a notification characteristic
for reading).

For testing only.  But it does successfully let me download dives from
my EON Steel and my Scubapro G2.

NOTE! There are several very hacky pieces in here, including just
"knowing" that the write characteristic is the first one, and the
notification characteristic is second.  The code should actually check
the properties rather than have those kinds of hardcoded assumptions.

It also checks "vendor specific" by looking at the UUID string
representation, and knowing that the standard ones start with zero.
Crazily, there doesn't seem to be any normal way to test for this,
although I guess that maybe the uuid.minimumSize() function could be
used.

There are other nasty corners. Don't complain, send me patches.

Signed-off-by: Linus Torvalds <torva...@linux-foundation.org>
---
 CMakeLists.txt             |   9 ++
 core/CMakeLists.txt        |   5 +
 core/qt-ble.cpp            | 228 +++++++++++++++++++++++++++++++++++++++++++++
 core/qt-ble.h              |  38 ++++++++
 core/qtserialbluetooth.cpp |  14 ++-
 scripts/build.sh           |   1 +
 6 files changed, 294 insertions(+), 1 deletion(-)
 create mode 100644 core/qt-ble.cpp
 create mode 100644 core/qt-ble.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3112c4b7..12bf1c4e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -216,10 +216,19 @@ if (BTSUPPORT AND "${Qt5Core_VERSION}" VERSION_LESS 5.4.0)
 	list(REMOVE_ITEM QT_LIBRARIES Qt5::Bluetooth)
 endif()
 
+#I can't test MacOS, and Windows Qt doesn't support BLE at all afaik
+if (BTSUPPORT AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
+	set(BLESUPPORT ON)
+endif()
+
 if(BTSUPPORT)
 	add_definitions(-DBT_SUPPORT)
 endif()
 
+if(BLESUPPORT)
+	add_definitions(-DBLE_SUPPORT)
+endif()
+
 #set up the subsurface_link_libraries variable
 set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} ${LIBDIVECOMPUTER_LIBRARIES} ${LIBGIT2_LIBRARIES} ${LIBUSB_LIBRARIES})
 qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc)
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 77521d6a..5622f5e6 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -24,6 +24,11 @@ if(BTSUPPORT)
 	set(BT_CORE_SRC_FILES qtserialbluetooth.cpp)
 endif()
 
+if(BLESUPPORT)
+	add_definitions(-DBLE_SUPPORT)
+	set(BT_CORE_SRC_FILES qt-ble.cpp)
+endif()
+
 # compile the core library, in C.
 set(SUBSURFACE_CORE_LIB_SRCS
 	btdiscovery.cpp
diff --git a/core/qt-ble.cpp b/core/qt-ble.cpp
new file mode 100644
index 00000000..3fe7e3ee
--- /dev/null
+++ b/core/qt-ble.cpp
@@ -0,0 +1,228 @@
+#include <errno.h>
+
+#include <QtBluetooth/QBluetoothAddress>
+#include <QLowEnergyController>
+#include <QEventLoop>
+#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>
+
+extern "C" {
+
+void BLEObject::serviceStateChanged(QLowEnergyService::ServiceState s)
+{
+	QList<QLowEnergyCharacteristic> list;
+
+	list = service->characteristics();
+
+	Q_FOREACH(QLowEnergyCharacteristic c, list) {
+		fprintf(stderr, "   %s\n", c.uuid().toString().toUtf8().data());
+	}
+}
+
+void BLEObject::characteristcStateChanged(const QLowEnergyCharacteristic &c, const QByteArray &value)
+{
+	receivedPackets.append(value);
+	waitForPacket.exit();
+}
+
+void BLEObject::writeCompleted(const QLowEnergyDescriptor &d, const QByteArray &value)
+{
+	fprintf(stderr, "Write completed\n");
+}
+
+void BLEObject::addService(const QBluetoothUuid &newService)
+{
+	const char *uuid = newService.toString().toUtf8().data();
+
+	fprintf(stderr, "Found service %s\n", uuid);
+	if (uuid[1] == '0') {
+		fprintf(stderr, " .. ignoring\n");
+		return;
+	}
+	service = controller->createServiceObject(newService, this);
+	fprintf(stderr, " .. created service object %p\n", service);
+	if (service) {
+		connect(service, &QLowEnergyService::stateChanged, this, &BLEObject::serviceStateChanged);
+		connect(service, &QLowEnergyService::characteristicChanged, this, &BLEObject::characteristcStateChanged);
+		connect(service, &QLowEnergyService::descriptorWritten, this, &BLEObject::writeCompleted);
+		service->discoverDetails();
+	}
+}
+
+BLEObject::BLEObject(QLowEnergyController *c)
+{
+	controller = c;
+}
+
+BLEObject::~BLEObject()
+{
+fprintf(stderr, "Deleting BLE object\n");
+}
+
+dc_status_t BLEObject::write(const void* data, size_t size, size_t *actual)
+{
+	QList<QLowEnergyCharacteristic> list = service->characteristics();
+	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;
+
+		service->writeCharacteristic(c, bytes, mode);
+		return DC_STATUS_SUCCESS;
+	}
+
+	return DC_STATUS_IO;
+}
+
+dc_status_t BLEObject::read(void* data, size_t size, size_t *actual)
+{
+	if (receivedPackets.isEmpty()) {
+		QList<QLowEnergyCharacteristic> list = service->characteristics();
+		if (list.isEmpty())
+			return DC_STATUS_IO;
+
+		const QLowEnergyCharacteristic &c = list.constLast();
+
+		QTimer timer;
+		int msec = 5000;
+		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();
+	if (size > packet.size())
+		size = packet.size();
+	memcpy(data, packet.data(), size);
+	*actual = size;
+	return DC_STATUS_SUCCESS;
+}
+
+dc_status_t qt_ble_open(dc_custom_io_t *io, dc_context_t *context, const char *devaddr)
+{
+	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);
+
+fprintf(stderr, "qt_ble_open(%s)\n", devaddr);
+
+	// Wait until the connection succeeds or until an error occurs
+	QEventLoop loop;
+	loop.connect(controller, SIGNAL(connected()), SLOT(quit()));
+	loop.connect(controller, SIGNAL(error(QLowEnergyController::Error)), SLOT(quit()));
+
+	// Create a timer. If the connection doesn't succeed after five seconds or no error occurs then stop the opening step
+	QTimer timer;
+	int msec = 5000;
+	timer.setSingleShot(true);
+	loop.connect(&timer, SIGNAL(timeout()), SLOT(quit()));
+
+	// Try to connect to the device
+	controller->connectToDevice();
+	timer.start(msec);
+	loop.exec();
+
+	switch (controller->state()) {
+	case QLowEnergyController::ConnectedState:
+		break;
+	default:
+		report_error("Failed to connect to %s: '%s'", devaddr, controller->errorString().toUtf8().data());
+		controller->disconnectFromDevice();
+		delete controller;
+		return DC_STATUS_IO;
+	}
+
+fprintf(stderr, "Connected to device %s\n", devaddr);
+
+	/* We need to discover services etc here! */
+	BLEObject *ble = new BLEObject(controller);
+	loop.connect(controller, SIGNAL(discoveryFinished()), SLOT(quit()));
+	ble->connect(controller, SIGNAL(serviceDiscovered(QBluetoothUuid)), SLOT(addService(QBluetoothUuid)));
+
+fprintf(stderr, "  .. discovering services\n");
+
+	controller->discoverServices();
+	timer.start(msec);
+	loop.exec();
+
+fprintf(stderr, " .. done discovering services\n");
+
+fprintf(stderr, " .. discovering details\n");
+
+	timer.start(msec);
+	loop.exec();
+
+fprintf(stderr, " .. done waiting\n");
+
+fprintf(stderr, " .. enabling notifications\n");
+
+	/* Enable notifications */
+	QList<QLowEnergyCharacteristic> list = ble->service->characteristics();
+
+	if (!list.isEmpty()) {
+		const QLowEnergyCharacteristic &c = list.constLast();
+		QList<QLowEnergyDescriptor> l = c.descriptors();
+
+fprintf(stderr, "Descriptor list (%p, %d)\n", l, l.length());
+
+		if (!l.isEmpty()) {
+			QLowEnergyDescriptor d = l.first();
+
+fprintf(stderr, "Descriptor: %s uuid: %s\n", d.name().toUtf8().data(), d.uuid().toString().toUtf8().data());
+
+			ble->service->writeDescriptor(d, QByteArray::fromHex("0100"));
+		}
+	}
+
+	// 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
diff --git a/core/qt-ble.h b/core/qt-ble.h
new file mode 100644
index 00000000..b819b012
--- /dev/null
+++ b/core/qt-ble.h
@@ -0,0 +1,38 @@
+#ifndef QT_BLE_H
+#define QT_BLE_H
+
+#include <QLowEnergyController>
+#include <QEventLoop>
+
+class BLEObject : public QObject
+{
+	Q_OBJECT
+
+public:
+	BLEObject(QLowEnergyController *c);
+	~BLEObject();
+	dc_status_t write(const void* data, size_t size, size_t *actual);
+	dc_status_t read(void* data, size_t size, size_t *actual);
+	QLowEnergyService *service;
+
+public slots:
+	void addService(const QBluetoothUuid &newService);
+	void serviceStateChanged(QLowEnergyService::ServiceState s);
+	void characteristcStateChanged(const QLowEnergyCharacteristic &c, const QByteArray &value);
+	void writeCompleted(const QLowEnergyDescriptor &d, const QByteArray &value);
+
+private:
+	QLowEnergyController *controller;
+	QList<QByteArray> receivedPackets;
+	QEventLoop waitForPacket;
+};
+
+
+extern "C" {
+dc_status_t qt_ble_open(dc_custom_io_t *io, dc_context_t *context, const char *name);
+dc_status_t qt_ble_read(dc_custom_io_t *io, void* data, size_t size, size_t *actual);
+dc_status_t qt_ble_write(dc_custom_io_t *io, const void* data, size_t size, size_t *actual);
+dc_status_t qt_ble_close(dc_custom_io_t *io);
+}
+
+#endif
diff --git a/core/qtserialbluetooth.cpp b/core/qtserialbluetooth.cpp
index d23265d4..ab05a1c7 100644
--- a/core/qtserialbluetooth.cpp
+++ b/core/qtserialbluetooth.cpp
@@ -19,6 +19,10 @@
 
 #include <libdivecomputer/custom_io.h>
 
+#ifdef BLE_SUPPORT
+# include "qt-ble.h"
+#endif
+
 QList<QBluetoothUuid> registeredUuids;
 
 static QBluetoothUuid getBtUuid()
@@ -414,7 +418,15 @@ dc_custom_io_t qt_serial_ops = {
 	.serial_set_dtr = NULL,
 	.serial_set_rts = NULL,
 	.serial_set_halfduplex = NULL,
-	.serial_set_break = NULL
+	.serial_set_break = NULL,
+
+#ifdef BLE_SUPPORT
+	.packet_size  = 20,
+	.packet_open  = qt_ble_open,
+	.packet_close = qt_ble_close,
+	.packet_read  = qt_ble_read,
+	.packet_write = qt_ble_write,
+#endif
 };
 
 dc_custom_io_t* get_qt_serial_ops() {
diff --git a/scripts/build.sh b/scripts/build.sh
index 88437c76..5f4dc607 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -19,6 +19,7 @@
 # create a log file of the build
 exec 1> >(tee build.log) 2>&1
 
+export CMAKE_PREFIX_PATH=/home/torvalds/src/qt5/qtbase/lib/cmake
 SRC=$(pwd)
 PLATFORM=$(uname)
 
-- 
2.13.1.518.g0d864c4df

_______________________________________________
subsurface mailing list
subsurface@subsurface-divelog.org
http://lists.subsurface-divelog.org/cgi-bin/mailman/listinfo/subsurface

Reply via email to