Commit graph

76 commits

Author SHA1 Message Date
Dirk Hohndel
300248f446 Qt6: use the newer createCentral() API
This will cause us problems when using Qt6 on Android - but I don't see
this happening any time soon.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2022-03-12 08:28:32 -08:00
Dirk Hohndel
6f46238fc4 Qt6: Bluetooth API changes
Use the explicit QBluetoothUuid instead of just QUuid and deal with new
constants and signal names.
At least with Qt6 we no longer need the ugly QOverload hack.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2022-03-12 08:28:32 -08:00
Jef Driesen
3a77d15eef Add support for the new OSTC hardware
In the latest OSTC hardware, the Telit/Stollman bluetooth module has
been replaced with a u-Blox Nina B2 bluetooth module. The BLE
communication protocol remains roughly the same, except for a few minor
differences:

 - New UUIDs for services and characteristics
 - Only one common characteristic for Rx and Tx
 - Credit based flow control is optional
 - Credit value of 255 corresponds to a disconnect

[Dirk Hohndel: small edit to a comment]

Signed-off-by: Jef Driesen <jef@libdivecomputer.org>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2022-01-31 12:28:32 -08:00
Andreas Buhr
41fc822d56 Use QtBluetooth enums from their namespace
For increased type safety, some enums have been changed to
scoped enums in Qt 6.2, see
https://codereview.qt-project.org/c/qt/qtconnectivity/+/337069
https://codereview.qt-project.org/c/qt/qtconnectivity/+/336678
This patch adapts subsurface to this change.
Since C++11, enums inject their symbols in both their own
and their parent namespace, so this patch can be merged right
now.

Signed-off-by: Andreas Buhr <andreas.buhr@qt.io>
2021-03-12 08:41:31 -08:00
Berthold Stoeger
5a19437311 cleanup: remove dc_user_device_t
The same structure was defined as "struct dc_user_device_t"
and typedefed as "device_data_t". Unify this. Since there
are much more of the latter, remove the former.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2020-10-24 09:51:37 -07:00
Linus Torvalds
aa95a9a744 Extend BLE uuid matching to the characteristics, add ignore-list
This extends the uuid matching to the low-level characteristics too, so
that we can ignore the McLean Extreme characteristics that aren't
interesting.

It also renames the uuid matching to be about a "uuid_list" rather than
being about the service we're matching, since we're now using it for
other uuid's than just services.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2020-09-17 07:54:35 -07:00
Linus Torvalds
981c1cb1d3 Fix 'uud' typo in BLE uuid matching code
Silly typo with a missing 'i' in 'uuid' that happened when I wrote this
code originally, and that compiled fine thanks to the error being
duplicated with cut-and-paste to all relevant places.

Fix it now, since I'll extend the uuid matching to the actual
characteristics for the McLean Extreme.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2020-09-17 07:54:35 -07:00
Dirk Hohndel
91557f79cd core/BLE: delay characteristics discovery until service discovery complete
While this code was added as I was trying to work through issues with a BLE
stack that turned out to be broken, the failure behavior of that device showed
that Qt doesn't like it when we start discovering the details of
characteristics while it is still busy discovering services.

So instead of handling the services as we find them, let's instead wait until
we are done discovering services and then discover the details for all those
services.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-08-22 19:35:58 -07:00
Dirk Hohndel
f74328df0b core/BLE: provide state and error updates during BLE discovery
This simply helps us see some possible errors while trying to talk to a device.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-08-22 19:35:58 -07:00
Dirk Hohndel
adeade5581 core/BLE: correct the reference for the Shearwater characteristic
They use that same UUID on the Peregrine as well.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-08-22 19:35:58 -07:00
Linus Torvalds
dee52409b1 BLE: trivial updates to the GATT services lists
Add a couple of known services (Scubapro G2 and Shearwater), and update
the names of others that turn out to be used for multiple dive
computers.  Also add another Broadcom upgrade service UUID.

While at it, sort the services numerically to make it easier to see that
a UUID already exists, since these service numbers do get used across
multiple different devices.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2020-05-17 13:52:24 -07:00
Linus Torvalds
e91c252093 BLE: add list of known good/bad BLE GATT services
We've tried to do this "automagic" service discovery, and it mostly
works, but then occasionally it doesn't.

Making things worse, I think different platforms end up enumerating
services differently, so our "pick the first service that looks like it
might be a serial service" ends up working on some platforms, but not
necessarily on others.  Because "first" might be different.

So start a list of known good/bad services - and fall back to the old
logic when you can't decide reliably.

This fills in juat a few cases that I can easily check myself, and the
"details" field for them may be incomplete.  For example, I know Nordic
Semiconductor has their vendor-specific UUIDs, and they can be found in
different devices, so calling them "Nordic UART" and "Nordic Flash"
services makes sense.

But the "Scubapro i770R" service? It might indeed be specific to the
Scubapro i770R.  Or it might be a general service UUID that Pelagic
uses.  Or it might be the service UUID of a particular chip, and found
in dive computers from other designs too (and not necessarily in all
i770R's either).

So this is a preliminary first stab at this, and I'm sure we'll extend
the list and possibly improve on the explanations.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2020-05-17 09:29:07 -07:00
Linus Torvalds
9543f53150 Update to new libdivecomputer version
Jef has changed the libdivecomputer iostream layer and extended it in
two different ways:

 - iostram's now have a 'poll()' method, which does what the name
   implies: waits for data to be available with a timeout.

 - iostreams now have a 'ioctl()' method, which can be used to implement
   miscellaneous operations. Right now the two ones that you can do are
   "set latency" (this replaces the old 'set_latency()' method) and "get
   BLE name" (this replaces our 'get_name()' method that was never part
   of the upstream libdivecomputer interfaces)

Neither of these is all that complicated, and the transition is fairly
obvious.

HOWEVER.

I have absolutely no idea how to do 'poll()' on Windows sockets, and I
have no intention of figuring it out.  We use a direct socket interface
to implement the (non-BLE) RFCOMM bluetooth serial protocol, and I'm not
sure why Windows is so special here.  I suspect - but cannot test - that
we should just switch the Windows RFCOMM implementation over to the use
the same QtBluetooth code that we use on other platforms.

I assume that the Windows Bluetooth support was originally not
sufficiently good for that, but these days we depend on Qt doing BLE for
us even on Windows, so presumably FRCOMM works too.

That would be a nice cleanup, and would make 'poll()' work on RFCOMM
under Windows too.  However, since I can't test it, I've not done that,
but instead just made the Windows RFCOMM 'poll()' method always return
success.  That may or may not get the thing limping along.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2020-01-27 09:51:31 -08:00
Berthold Stoeger
5da09a21bb Cleanup: move error reporting function declarations to errorhelper.h
Move the declarations of the "report_error()" and "set_error_cb()"
functions and the "verbose" variable to errorhelper.h.
Thus, error-reporting translation units don't have to import the
big dive.h header file.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2019-08-08 16:26:30 -07:00
Linus Torvalds
866ca76a58 qt-ble: return DC_STATUS_TIMEOUT rather than DC_STATUS_IO on timeout
This didn't use to matter, because none of the BLE-using backends did
retry on timeout until recently.

But Jef started doing packet sending retry for the Mares Icon backend,
and now we should make sure to distinguish the "IO failed" from "IO
timed out" cases.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2019-04-18 05:42:54 +12:00
Rolf Eike Beer
cf86ece73e use qDeleteAll()
Signed-off-by: Rolf Eike Beer <eike@sf-mail.de>
2019-04-12 12:59:17 +03:00
Rolf Eike Beer
c4c8094e32 get rid of some foreach and Q_FOREACH constructs
See https://www.kdab.com/goodbye-q_foreach/

This is reduced to the places where the container is const or can be made const
without the need to always introduce an extra variable. Sadly qAsConst (Qt 5.7)
and std::as_const (C++17) are not available in all supported setups.

Also do some minor cleanups along the way.

Signed-off-by: Rolf Eike Beer <eike@sf-mail.de>
2019-04-12 12:59:17 +03:00
Dirk Hohndel
01649503fb BLE debug: make things less verbose
Unless run with '-v -v -v'.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2019-01-22 11:14:03 +13:00
Linus Torvalds
9e3a22c522 qt-ble: add 'get_name()' function to expose the BLE name to libdivecomputer
Some divecomputer backends (ok, right now really only the Aqualung i770R
and i300C) want to know the bluetooth name of the dive computer they
connect to, because the name contains identifying information like the
serial number.

This just adds the support for that to our Qt BLE code.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-10-08 00:10:29 +03:00
Linus Torvalds
cd55344410 qt-ble: add support for libdivecomputer 'set_timeout()' function
Because some BLE operations can be very slow (device and service
discovery etc), we have some rather excessive default timeout for BLE
(currently set to 12 seconds).

But once we actually have started doing IO, that long timeout can be a
big performance problem, when the libdivecomputer backend has support
for retry and packet loss.

For that reason, libdivecomputer has a 'set_timeout()' function that
allows the divecomputer backend to say how quickly it expects the dive
computer to answer before the backend will start resending packets.

Let's just implement that for the actual IO side of BLE too.  The
default timeout value remains the general BLE timeout, and this only
affects the actual IO phase, but it improves things enormously for the
case where there is packet loss at that point.

For example, on the Aqualung i770R, the timeout for packet loss ends up
now being just one second rather than the full 12 seconds of default BLE
timeout.  Which gets the retry going much faster.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-10-06 19:38:28 -07:00
Linus Torvalds
88f4c06b99 qt-ble: add support to wait for descriptor write completion
When we enable notifications, we actually want to make sure to wait for
that write to have completed before we start communicating with the
device, because otherwise we might lose notification events.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-10-06 19:38:28 -07:00
Linus Torvalds
ec532b8f59 qt-ble: move basic uuid filtering back to service discovery
In commit 30fb7bf35c ("qt-ble: set up infrastructure for better
preferred service choice") I moved the service filtering from the
addService() callback into the "select_preferred_service()" function
that picks the right service for the device.

That was nice for debugging, since it meant that we showed the details
of _all_ services, but it also meant that we ended up starting service
discovery on _all_ services, whether they looked at all interesting or
not.

And that can make the BLE device discovery process quite a bit slower.

The debugging advantage is real, but honestly, service discovery can
generally be better done with specialized tools like the Nordic nRF app,
so the debugging advantage of just listing all the details of all the
services is not really worth the discovery slowdown in general.

So move the basic "filter by uuid" back to the service discovery phase,
and don't bother starting service detail discovery for the services that
we can dismiss immediately just based on the service UUID.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-10-06 19:38:28 -07:00
Dirk Hohndel
79d0185c62 Bluetooth: use_random_address helper isn't needed on Windows
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2018-10-01 15:55:19 -07:00
Dirk Hohndel
0422cd3662 Bluetooth: don't use random addresses on Windows
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2018-09-30 08:09:37 -07:00
Linus Torvalds
890d4c3d64 qt-ble: allow reading of partial packet data
The existing BLE dive computers treat BLE as the packetized protocol it
is, and read whole packets at a time.

However, the Mares BlueLink backend treats it as just a basic "serial
over BLE" transport, and for historical reasons reads the reply packets
in smaller chunks.

This allows that kind of IO behavior, where if the divecomputer backend
reads just a part of a packet, we'll split the packet, return the part
the user asked for, and push back the leftover packet onto the received
packet queue.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-26 12:13:20 -07:00
Linus Torvalds
05fe19a23f qt-ble: add BLE packet debugging code
This is perhaps overly verbose, but the timing details helped figure out
some EON Core download issues, and it's nice to see when things actually
happen.

It's also good to see when the data actually enters our queues, and when
we read and write the packets.  That might help debug the issues Fabio
is seeing with the Mares Bluelink.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-25 16:41:13 -07:00
Linus Torvalds
0ba105d2a7 qt-ble: only connect the signals to the preferred service
We used to just find all services and connect the characteristics change
signal etc to them all, but we really only care about the actual
preferred service that we'll be using.

So move the qt ble signal connection to after we've selected the
preferred service that we will actually be enabling notifications on and
do the writes to.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-25 16:41:13 -07:00
Linus Torvalds
1b16d570a1 qt-ble: re-organize how we pick the GATT characteristics to read and write
We used to just blindly pick "first" and "last" characteristic from the
preferred service, and that was stupid but happened to work for the dive
computers we supported.  Note that for some of them, "first" and "last"
was actually the *same* characteristic, since it could be a single one
that supported both.

However, this first/last hack definitely doesn't work for the Mares
BlueLink BLE dongle, and it's really all pretty wrong anyway.

So re-organize the code to actually look at the properties of the
characteristics.  I don't have a BlueLink to test with, but my EON Core
and Shearwater Perdix AI are still happy with this, and the code
conceptually makes a lot more sense.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-24 17:12:58 -07:00
Linus Torvalds
30fb7bf35c qt-ble: set up infrastructure for better preferred service choice
We used to just pick the first non-standard service we found (with a
special case for the Heinrichs Weikamp dive computers that have an
actual registered standard service).

We then waited for that service to finish discovery, and started using
it.

This changes the logic to wait for _all_ services to finish discovery,
and then after that we pick the one we like best.  Right now the rule
for picking a preferred service is the same one we had before, but the
difference is that we now have the full discovery data, so we *could* do
something better.

Plus this makes our debug messages a lot more legible, when we don't
have the mix of overlapping service discovery with the actual IO we do
to the preferred service.

NOTE! This doesn't much matter for most of the dive computers that we
currently support BLE for.  They don't tend to have a lot of odd
services.

But at least both the Mares BlueLink and the Garmin Descent both have
multiple services and it's not obvious which one to use, and this will
make it not only easier to debug those, it will make it easier to pick
the right preferred service descriptor to use.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-23 17:29:05 -07:00
Linus Torvalds
594f13eeaf qt-ble: use the WAITFOR() macro rather than open-coding wait loops
This is not only much clearer (and smaller code), but it also lowers the
latency for the waiting, since we don't always wait for the full 100ms.

Get rid of the now unused "waitfor()" function that just unconditionally
waited for 100ms.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-09-23 17:29:05 -07:00
Linus Torvalds
56a77f0fa3 Garmin devices - like Shearwater - want random BLE addressing
We had a special-case for the Shearwater case, let's just make it
slightly more generic and add Garmin to the list of vendors that want a
random BLE address rather than a static one.

The Bluez model of having to state this explicitly - but not giving the
information to the user - is completely broken and this is all very
annoying, credit goes to Wojciech Więckowski for pointing this out.

Of course, right now we don't actually know how to parse the BLE stream
from the Garmin Descent, but with this (and some libdivecomputer
hackery) I actually get connected and start receiving data.  That we
then can't parse, but that's hopefully just a libdivecomputer update
away.

Pointed-out-by: Wojciech Więckowski <xplwowi@gmail.com>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2018-09-06 18:35:18 -07:00
Linus Torvalds
98bd303d93 qt-ble: purge pending read data when writing
This should never happen, since our interface is bassically synchronous,
but it could happen with delayed replies that came in just after we
decided to re-transmit a command.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-06-20 16:38:04 +09:00
Linus Torvalds
21d6531e45 qt-ble: improve responsiveness of waiting for bluetooth data
Our model of waiting for 100ms before re-checking if we got a packet
over BLE resulted in potentially horrendously bad latency for received
packets.

That isn't just a possible performance issue, it actually seems to cause
IO errors with my Suunto EON Core.  I'm not entirely sure why, but it
might simply be some timing interaction, particularly since the IO
errors seemed to primarily happen when the dive computer itself was also
busy updating the screen (ie if you pressed buttons on the dive computer
to switch to compass mode, for example).

So replace the silly hardcoded 100ms "waitFor()" function with a
WAITFOR() macro that checks the provided expression every time through
the loop, which gets us a much lower latency (we basically check every
ten milliseconds).

The macro is not beautiful, but it WorksForMe(tm).

This makes a huge difference to the reliability of the download for me,
and might matter for some other dive computers too.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-06-20 16:38:04 +09:00
jan Iversen
2d1e5d7a99 core: qt-ble.cpp remove postEvent for all platforms
the postEvent is only called when downloading from a dc
with bluetooth, so in most it does not have an effect
on the deleteLater() in the code.

there are no reason to do special cleanup while waiting
for bluetooth

QEvent::DeferredDelete is not supported on iOS.

Signed-off-by: Jan Iversen <jani@apache.org>
2018-05-26 09:26:43 -07:00
jan Iversen
a312e53f0c core: Change Q_UNUSED to no parameter name
C++ permits use of parameters without name, which signals unused

Signed-off-by: Jan Iversen <jani@apache.org>
2018-05-21 12:48:04 -07:00
jan Iversen
67838de5bc iOS: qt-ble.cpp remove unsupported event.
QEvent::DeferredDelete is not supported in iOS

Signed-off-by: Jan Iversen <jani@apache.org>
2018-05-21 07:46:41 -07:00
Dirk Hohndel
d577467f97 Core: introduce new subsurface-string header
First small step to shrinking dive.h.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2018-05-14 10:13:39 -07:00
Linus Torvalds
13f5c75ac4 Convert our custom IO model to new libdivecomputer IO model
This converts our old custom IO model to the new model that
libdivecomputer introduced.  This is partly based on Jef's rough patch
to make things build, with further work by me.

The FTDI code is temporarily disabled here, because it will need to be
integrated with the new way of opening devices.

The ble_serial code goes away entirely, since now libdivecomputer knows
about BLE transport natively, and doesn't need to have any serial
wrapper around it.

Signed-off-by: Jef Driesen <jef@libdivecomputer.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2018-04-24 17:54:08 -07:00
Berthold Stoeger
529d407933 Cleanup: Make local function waitFor() of static linkage
Moreover, remove it from the `extern "C"` block, since extern/static
is oxymoronic.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2018-04-19 15:08:12 +03:00
Berthold Stoeger
8f4604ead8 Coding style: remove superfluous semicolons
Remove a semicolon after Q_OBJECT and a few others after the closing
braces of while loops.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2018-04-19 15:08:12 +03:00
Dirk Hohndel
d931652f69 BLE debug: show more packages
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2018-04-13 17:07:42 -07:00
Berthold Stoeger
b72cc1f317 Cleanup: consistently use qPrintable()
Replace constructs of the kind
  s.toUtf8().data(),
  s.toUtf8().constData(),
  s.toLocal8Bit().data(),
  s.toLocal8Bit.constData() or
  qUtf8Printable(s)
by
  qPrintable(s).

This is concise, consistent and - in principle - more performant than
the .data() versions.

Sadly, owing to a suboptimal implementation, qPrintable(s) currently
is a pessimization compared to s.toUtf8().data(). A fix is scheduled for
new Qt versions: https://codereview.qt-project.org/#/c/221331/

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2018-03-14 13:55:36 +02:00
Berthold Stoeger
7bd860f2b7 Cleanup: Use QString as parameter for [get|save]BtDeviceInfo()
Each callsite of saveBtDeviceInfo() has a QString, which is converted
to a C-string, passed and immediately converted back. Remove these
conversions by taking a reference to QString directly.

getBtDeviceInfo() is not as clear. Here, the callsite has a C-string
handed down from libdivecomputer. Nevertheless, pass a reference of
QString here as well. Firstly, for reasons of symmetry. Secondly,
to avoid multiple conversions in the getBtDeviceInfo() functions.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2018-02-26 08:52:34 +01:00
Jan Mulder
34c3818bd7 Cleanup: Uninitialized scalar field
CID 208315

Signed-off-by: Jan Mulder <jlmulder@xs4all.nl>
2017-12-28 17:30:25 +01:00
Berthold Stoeger
38b1b7d951 Better error message on BLE connection timeout
On BLE connection timeout a weird error-message was shown, because
the controller was still in connecting state and no error string was
set. Therefore, handle the timeout case with a special case label.

Moreover, remove three unnecessary calls to disconnectFromDevice(),
which is called in the destructor of the controller anyway (verified
by looking at Qt source).

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2017-11-13 20:44:26 -08:00
Berthold Stoeger
28299b0ebc Fix resource leaks in qt-ble.cpp
1) Destroy QLowEnergyService objects in destructor of BLEObject.

2) Let BLE object take ownership of the controller so that the
latter can be destroyed in the destructor of the former. This
introduces a certain ownership subtlety, which could be solved by
allocating the controller object in the BLE object. But let's
first do the less intrusive thing.

3) Destroy the BLE object for two error conditions.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2017-11-02 15:13:58 +01:00
Dirk Hohndel
bb1df1218d BLE support: simplify write function
It seems clearer to bail when list is empty...

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-09-20 19:19:25 -04:00
Dirk Hohndel
753c00a493 BLE on non-Apple OSs: switch back to using BT address
While this interface is deprecated, too much in our existing code depends
on being able to create the QLowEnergyController with just the address.

Additionally, createCentral() is new in Qt 5.7 and therefor this broke
builds on Linux distros that are still on 5.6.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-09-17 16:31:07 -07:00
Dirk Hohndel
90d73924c2 BLE: try to pick the correct descriptor to write to
The ordering on Mac appears to be random, but after looking through the
various successful logs of BLE downloads, it seems we always wrote to the
ClientCharacteristicConfiguration descriptor. So try to find that one first,
and only grab the first descriptor in the list if we didn't find a
ClientCharacteristicConfiguration descriptor.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-09-17 09:58:11 -07:00
Dirk Hohndel
26e610c3f4 BLE: create controller from QBtDeviceInfo
Creating it from an address is a) deprecated and b) impossible on Mac or iOS.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2017-09-17 09:58:11 -07:00