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:
- 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.
- 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.
- 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).
- 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