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