I/O layer refactoring finished
Jef Driesen
jef at libdivecomputer.org
Tue Apr 3 23:40:44 CEST 2018
Hi,
I just pushed the latest changes for the I/O layer refactoring to the
master branch. Unfortunately these changes break backwards compatibility
for anyone using libdivecomputer.
In a nutshell, the main changes are:
* The dc_device_open() function has been modified. Instead of a string
with the name of the serial port, it now takes a pointer to a
dc_iostream_t. That means that the application becomes responsible for
opening the underlying I/O stream.
* The dc_descriptor_get_transport() function has been replaced with a
new dc_descriptor_get_transports() function (notice the extra "s" in the
name). This new function returns a bitmask with all the transports
supported by the device.
* The dc_context_get_transports() function has been added to query the
available built-in transports.
These changes enables the use of Bluetooth Classic (rfcomm) with the
built-in implementation (Windows and Linux), and Bluetooth Low Energy
(ble) with a custom implementation. More details about these new
features are in the remainder of this email.
With the above changes, the typical sequence to download dives will now
look similar to this:
1. Enumerate all supported dive computers and choose one
2. Choose the transport type
3. Ignore unsupported transport types
4. Enumerate the I/O devices
5. Open the I/O stream
6. Connect to the dive computer
7. Download dives
Let's have a closer look at each step:
1. Enumerate all supported dive computers and choose one
This is exactly the same as before.
2. Choose the transport type
This is where the first important change will be. Previously, each
device descriptor supported exactly one transport type (serial, usb,
usbhid or irda). But with bluetooth we have devices that support more
than just one transport. Therefore, the transport type has been changed
into a bitmask:
typedef enum dc_transport_t {
DC_TRANSPORT_NONE = 0,
DC_TRANSPORT_SERIAL = (1 << 0),
DC_TRANSPORT_USB = (1 << 1),
DC_TRANSPORT_USBHID = (1 << 2),
DC_TRANSPORT_IRDA = (1 << 3),
DC_TRANSPORT_BLUETOOTH = (1 << 4),
DC_TRANSPORT_BLE = (1 << 5)
} dc_transport_t;
Now, each descriptor can support more than one transport, and you can
query them using the new function:
unsigned int
dc_descriptor_get_transports (dc_descriptor_t *descriptor);
For example, the Heinrichs Weikamp OSTC 2 will report
DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH | DC_TRANSPORT_BLE.
3. Ignore unsupported transport types
With support for custom I/O, libdivecomputer no longer knows which
devices are actually supported. Hence the #ifdef's will be removed from
the descriptor table, and thus libdivecomputer will always report all
the devices it knows about. That means it's up to the application to
filter out entries for which there is no suitable transport available
(either built-in or custom).
Therefore libdivecomputer provides a new function to query the built-in
transports:
unsigned int
dc_context_get_transports (dc_context_t *context);
This will again return a bitmask with the available built-in transports.
With a simple bitwise AND operation, you can easily filter out
transports for which no built-in implementation is available:
unsigned int supported = dc_descriptor_get_transports(descriptor);
unsigned int available = dc_context_get_transports(context);
unsigned int usable = supported & available;
When using custom I/O, the application should of course also keep the
transports for which it has a custom I/O implementation.
4. Enumerate the I/O devices
Once you have selected the underlying transport type, you want to scan
the system for possible matches. Previously, libdivecomputer did this
internally for irda and usbhid, and automatically picked the first
available device. For serial communication, applications were supposed
to enumerate the serial ports themselves and pass the name of the device
node. Since every transport types has different parameters (e.g a
vid/pid for usbhid, and address and port for irda and bluetooth classic,
etc), this can't easily be extended to arbitrary transport types.
Therefore, each of the built-in iostream implementations will provide
its own enumeration api, based on the existing iterator api:
dc_iterator_t *iterator = NULL;
dc_xxx_iterator_new (&iterator, context, descriptor);
dc_xxx_device_t *device = NULL;
while (dc_iterator_next (iterator, &device) == DC_STATUS_SUCCESS) {
dc_xxx_device_get_yyy(device);
dc_xxx_device_free (device);
}
dc_iterator_free (iterator);
Where xxx can be replaced with serial, irda, usbhid, etc.
For serial, you'll be able to get the device node, for irda/bluetooth
the address and device name, for usbhid the vid/pid, and so on.
Of course we also don't want to push too much knowledge to the
application. Ideally the application shouldn't need to know which
VID/PID's match certain dive computers, or things like that. That's why
the dc_xxx_iterator_new() function accepts an (optional) descriptor as
argument. It will acts as a filter and return only those devices that
match the selected dive computer. For example if you scan for usb hid
devices with the Eon Steel descriptor, you'll only get devices matching
the Eon Steel VID/PID. Same for bluetooth and irda, where we can filter
on the device name.
Note that for custom I/O, this whole discovery step is obviously always
the responsibility of the application.
5. Open the I/O stream
The next step is opening the I/O stream.
To use one of the built-in implementations, you simply call one of these
functions:
dc_serial_open (&iostream, context, name);
dc_irda_open (&iostream, context, address, 1);
dc_usbhid_open (&iostream, context, device);
dc_bluetooth_open (&iostream, context, address, 0);
The values for the extra parameters (name, address, device, etc) are
available from the device discovery in the previous step.
To use a custom implementation, you need to implement a set of callback
functions, and create an iostream for them as follows:
dc_custom_cbs_t callbacks = {
app_custom_set_timeout, /* set_timeout */
app_custom_set_latency, /* set_latency */
app_custom_set_break, /* set_break */
app_custom_set_dtr, /* set_dtr */
app_custom_set_rts, /* set_rts */
app_custom_get_available, /* get_received */
app_custom_get_lines, /* get_lines */
app_custom_configure, /* configure */
app_custom_read, /* read */
app_custom_write, /* write */
app_custom_flush, /* flush */
app_custom_purge, /* purge */
app_custom_sleep, /* sleep */
app_custom_close, /* close */
};
dc_custom_open (&iostream, context, transport, &callbacks, userdata);
6. Connect to the dive computer
Instead of accepting a string with the device node (which only makes
sense for serial communication), you now pass the iostream handle to the
dc_device_open() function:
dc_status_t
dc_device_open (dc_device_t **device, dc_context_t *context,
dc_descriptor_t *descriptor, dc_iostream_t *iostream);
7. Download dives
This remains exactly the same as before.
Jef
More information about the devel
mailing list