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