Libdivecomputer Android API discussion

Jef Driesen jef at libdivecomputer.org
Mon Jun 30 05:24:02 PDT 2014


On 2014-06-30 00:40, Venkatesh Shukla wrote:
> On 06/27/2014 04:34 PM, Jef Driesen wrote:
>> Originally, the solution I had in mind for the different transport 
>> types (serial, irda, usb and bluetooth), was to introduce a specific 
>> data structure for each transport type:
>> 
>> typedef dc_params_serial_t {
>>     const char *devname;
>> } dc_params_serial_t;
>> 
>> typedef dc_params_usb_t {
>>     unsigned int vid;
>>     unsigned int pid;
>> } dc_params_usb_t;
>> 
>> typedef dc_params_bluetooth_t {
>>     unsigned char address[6];
>> } dc_params_bluetooth_t;
>> 
>> dc_status_t
>> dc_device_open (..., dc_descriptor_t *descriptor, const void *params);
>> 
>> The application would then have to pass the appropriate data structure 
>> to the dc_device_open function, depending on the transport type 
>> indicated by the device descriptor:
>> 
>> dc_params_serial_t serial;
>> dc_params_usb_t usb;
>> dc_params_bluetooth_t bluetooth;
>> 
>> switch (dc_descriptor_get_transport(descriptor)) {
>> case DC_TRANSPORT_SERIAL:
>>     serial.devname = ...;
>>     dc_device_open(..., descriptor, &serial);
>>     break;
>> case DC_TRANSPORT_USB:
>>     usb.vid = ...;
>>     usb.pid = ...;
>>     dc_device_open(..., descriptor, &usb);
>>     break;
>> case DC_TRANSPORT_BLUETOOTH:
>>     bluetooth.address = ...;
>>     dc_device_open(..., descriptor, &bluetooth);
>>     break;
>> }
>> 
> I was thinking something like
> 
> typedef struct dc_addr_t {
>     dc_transport_t transport;
>     dc_param_t param;
> } dc_addr_t;
> 
> dc_status_t
> dc_device_open (dc_device_t **out, dc_context_t *context,
> dc_descriptor_t *descriptor, const dc_addr_t *addr);
> 
> dc_addr_t *dev_addr = (dc_addr_t *) malloc (sizeof (dc_addr_t));
> 
> switch (dc_descriptor_get_transport(descriptor)) {
> case DC_TRANSPORT_SERIAL:
>     dev_addr->transport = DC_TRANSPORT_SERIAL;
>     dev_addr->param.devname = "/path/to/tty";
>     break;
> case DC_TRANSPORT_USB:
>     dev_addr->transport = DC_TRANSPORT_USB;
>     dev_addr->param.fd = XX; // File descriptor obtained from android
>     break;
> case DC_TRANSPORT_BLUETOOTH:
>     dev_addr->transport = DC_TRANSPORT_USB
>     dev_addr->param.mac_addr = ...;
>     break;
> }
> dc_device_open(..., descriptor, dev_addr);

That's basically the same as I wrote, but with dc_param_t as an union, 
instead of separate dc_param_xxx_t structs and a void pointer. 
Technically, there is not much difference between the two.

Note that there is no reason to use include the transport type into the 
parameters, because it's already present in the descriptor, which also 
gets passed to the dc_device_open function.

> The case of USB above is for Android usage as explained below.
>> Although this will certainly work, there are some drawbacks:
>> 
>> Ideally libdivecomputer should provide some api to enumerate serial, 
>> usb and bluetooth devices. When the user chooses a device that uses 
>> serial communication, you want to be able to show a list with all 
>> serial ports. The same goes for usb and bluetooth communication. But 
>> for some transport types like irda sockets (and maybe also bluetooth 
>> sockets) you need to open a socket before you can enumerate the 
>> available devices. But as long as the backend code opens the socket 
>> internally, this functionality can't be exposed to the application. 
>> Currently the USB (Atomics Cobalt) and IrDA (Uwatec Smart) based 
>> backends do work around this problem by performing the discovery 
>> internally, and pick the device that looks like the dive computer (e.g 
>> based on its name, USB VID/PID, etc). But this heuristics isn't 
>> perfect and can certainly fail. For example when connecting two 
>> identical dive computers, or when some other usb-serial gadget with 
>> the same USB VID/PID (not unlikely with simple usb-serial cables using 
>> the stock ftdi/pl2303 VID/PID) is connected. The heuristics will have 
>> to decide which device to pick (e.g. the first one), but that might be 
>> the wrong choice. Yes, I'm aware this is a corner case, but not 
>> impossible.
>> 
>> It's also not clear to me how this solution should deal with multiple 
>> implementations of a single transport. For Android, there'll be 
>> multiple implementations for serial communication, one for each 
>> chipset (ftdi, pl2303, cdc-acm, etc). But how do you decide which 
>> chipset to pick? The device backend doesn't always have this 
>> information. For example for Suunto devices,there are several cables 
>> on the market using different usb-serial chipsets. So there is no one 
>> to one mapping between devices and usb-serial chipsets.
>> 
> For usage of usb divecomputers on Android, the question of enumeration
> wouldn't necessarily arise. It is provided by android and would be
> used in the pre-libdivecomputer-usage stage (see step 0 below) in
> order to get the file descriptor which would then be passed on to
> libdivecomputer. Also connecting multiple USB devices on Android would
> not be a common phenomenon.
> 
> If still enumeration is needed, one could use libusb to get a list of
> all usb devices ( libusb_get_device_list ). This is a compulsory
> initial steps in any libusb related operations and is shown below
> (step 4).

If the enumeration is done by libdivecomputer, we can provide a 
consistent interface on all supported operating systems. The benefit is 
that the application code will be identical, no matter on which system 
the application runs.

Of course that doesn't mean we can't use the Android api under the hood 
to do the actual enumeration!

> The steps for usage of libdivecomputer on android as I have thought of 
> are:
> 
>    0. Android application lists the devices. User chooses his
>    divecomputer. The application asks for permission to use it. On
>    getting permission, the application acquires the file descriptor of
>    the Usb Device attached. It is then passed to libdivecomputer for
>    opening the device.

There is one important difference regarding usability here. I think the 
user should first choose his dive computer, and only then the 
application should show a list with the corresponding devices. The 
reason behind this is simple. If you select a model which uses bluetooth 
communication, it makes no sense to list USB devices. You could even go 
one step further. If a user selects a model which uses USB communication 
with some well known VID/PID (like the Atomic Cobalt), then ideally you 
would filter out all other USB devices. Same with bluetooth 
communication, where you may be able to filter out devices based on 
their name.

>    1. The application makes an appropriate addr and adds data to it.
>    Then calls dc_device_open with this addr.
>    dc_addr_t *addr = (dc_addr_t *) malloc(sizeof(dc_addr_t));
>    addr->transport = DC_TRANSPORT_USB;
>    addr->param.fd = usb_fd;
>    dc_device_open(&device, context, descriptor, addr);

I may not be aware of this, but DC_TRANSPORT_USB already exists today. 
It's used for those devices using real USB communication, instead of 
emulating serial communication with a usb-serial chip. In that case we 
are already using libusb directly. Currently there is only one such 
model, the Atomic Cobalt.

For all models relying on a usb-serial chip, the transport type is 
DC_TRANSPORT_SERIAL, even if we would not be using the serial emulation 
of the OS and use libusb to talk to the usb-serial chipset directly.

>    2. The dc_device_open looks for the device specific opening code
>    (ex. hw_ostc3_open) and finally reaches serial_open of android.
>    3. serial_open checks if bluetooth is to be used or usb.
>    int
>    serial_open (serial_t **out, dc_context_t *context, const dc_addr_t
>    *addr)
>    {
>         if (addr->transport == DC_TRANSPORT_USB)
>             serial_open_usb (out, context, addr->param.fd;
>         else if (addr->transport == DC_TRANSPORT_BLUETOOTH)
>             serial_open_bt (out, context, addr->param.mac_addr;
>         else
>             return -1;
>    }

No, this is the wrong place to handle this. The serial communication 
backend should be completely independent from any bluetooth code. Any 
such decisions should be handled in some layer above. This doesn't even 
belong in the device backends, because those are compiled to use one 
specific communication backend. For example a Suunto Vyper (which uses a 
usb-serial chip) will always use some serial communication backend. It 
will never need a bluetooth or usb backend.

In theory there might be one exception here for bluetooth devices. 
Assume we implement a native bluetooth communication, then those devices 
will be able to use either serial emulation (e.g. what we are using 
right now), or the native bluetooth communication. But in practice, I 
think it's not unreasonable to compile libdivecomputer for one of them 
but never both. If native bluetooth communication is supported we use it 
(DC_TRANSPORT_BLUETOOTH), and only when compiled without native 
bluetooth support, we fall back to serial emulation 
(DC_TRANSPORT_SERIAL).

>    4. If transport type is usb, make a libusb_device with the file
>    descriptor.
>    int
>    serial_open_usb (serial_t **out, dc_context_t *context, int fd)
>    {
>         int i, n, r;
>         libusb_init(NULL);
>         libusb_device *dev, **devs;
>         struct libusb_device_descriptor desc;
>         n = libusb_get_device_list (NULL, &devs);
>         for (i = 0; i < n; i++) {
>             dev = devs[i];
>             libusb_get_device_descriptor(dev, &desc);
>             if (is_dev_recognized( desc.idVendor, desc.idProduct) {
>                 switch (get_dc_chipset(desc_idVendor, desc.idProduct) {
>                 case FTDI:
>                     return serial_open_ftdi(out, context, dev);
>                 case PL2303:
>                     return serial_open_pl2303 (out, context, dev);
>                 case CP210X:
>                     return serial_open_cp210x (out, context, dev);
>                 default:
>                     return -1; // Chipset not yet supported
>                 }
>             }
>         }
>         return -2; // No recognized usb device found
>    }
> 
>    Checks have been ignored for brevity.

There is a flaw in the above logic. If you pass a file descriptor to the 
serial_open_usb function, that means you already have the usb device 
opened. Thus you should already know the VID and PID. Why enumerate the 
usb devices again? And more important, how do you know the device you 
just enumerated corresponds to the file descriptor you already have? 
This is another reason why I believe this is something that should be 
handled at a higher layer.

And that's exactly what I did in my second idea (see below). There the 
dc_serial_open, dc_bluetooth_socket_open, etc calls are moved to the 
application. This gives the application full control over opening the 
transport (and thus enumeration too), and libdivecomputer will just use 
the transport.

> The multiple-interfaces question is resolved because we can extract
> the vid pid of the chip directly. And this is used for choosing the
> chipset specific function. There is no manufacturer information
> needed.
> I might be wrong or missing something. Does using different cables
> with different usb-serial chipset change the vid pid of the usb
> device?

Yes, a different usb-serial chipset means another VID/PID. But even 
devices with the same chipset can have a different VID/PID. Usually the 
chipset vendor (e.g. ftdi, prolific, etc) has registered it's own 
VID/PID. But a hardware manufacturer that uses such a chipset in their 
own products can re-use this VID/PID, or register their own numbers.

See this page for an incomplete list of VID/PID numbers used by dive 
computers:

http://www.libdivecomputer.org/drivers.html

>> The next thing that comes to my mind is a solution where the 
>> application opens the appropriate "transport "itself, and passes an 
>> instance of an abstract transport object to the dc_device_open 
>> function:
>> 
>> dc_serial_t *serial = NULL;
>> dc_usb_t *usb = NULL;
>> dc_bluetooth_socket_t *bluetooth = NULL;
>> 
>> switch (dc_descriptor_get_transport(descriptor)) {
>> case DC_TRANSPORT_SERIAL:
>> #ifdef ANDROID
>>     dc_serial_{ftdi,pl2303,...}_open(&serial, fd);
>> #else
>>     dc_serial_open(&serial, name);
>> #endif
>>     dc_device_open(..., descriptor, serial);
>>     break;
>> case DC_TRANSPORT_USB:
>>     dc_usb_open(&usb, vid, pid);
>>     dc_device_open(..., descriptor, usb);
>>     break;
>> case DC_TRANSPORT_BLUETOOTH:
>>     dc_bluetooth_socket_open(&bluetooth, ...);
>>     dc_bluetooth_socket_discovery(bluetooth, ...);
>>     dc_bluetooth_socket_connect(bluetooth, address);
>>     dc_device_open(..., descriptor, bluetooth);
>>     break;
>> }
>> 
>> This would allow multiple implementations of a transport, as long as 
>> each of them implements the same interface, but that's easy. It also 
>> solves the discovery problem, because the low-level transport 
>> interface is now available to the application.
>> 
>> But unfortunately it also introduces a new problem. Take a look at the 
>> USB transport. Now the application suddenly needs to know which USB 
>> VID/PID to use. Previously the device backend took care of that 
>> internally (with the caveat mentioned earlier). For example the Atomic 
>> Cobalt uses the VID/PID 0x0471/0x0888. Right now the application 
>> doesn't need to know this, because the backend opens the usb device 
>> that matches these number. The same thing happens for Uwatec Smart, 
>> where the backend discovers the IrDA devices in range, and connects to 
>> first one that matches the known device names. How do we deal with 
>> this here?

Jef


More information about the devel mailing list