Compare commits

...

3 Commits

Author SHA1 Message Date
Dirk Hohndel
c9537fb0fa Garmin: add MTP support
Create parallel helper functions that use libmtp to walk the file tree on the
device and to then read a specific file from the device into our dc_buffer.
MTP is not a file system, it's an object storage, that just happens to allow
object names and parent/child relationships between objects. As a result we
need to remember those file ids for MTP downloads (we could actually just not
remember the file names in that case).

The mtp_get_file_list function is rather complex as it includes both the
initial communication with the device and the code to walk the object tree and
then create the list of file.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-10-27 16:33:02 -07:00
Dirk Hohndel
c7f67509d4 Garmin: refactor the code to prepare for MTP support
Breaking things into helper function makes it much easier to then support both
regular file operations and MTP.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-10-27 15:37:41 -07:00
Dirk Hohndel
9de9efcae0 Garmin: add the Descent Mk2/Mk2i
For the Descent Mk2/Mk2i I picked the USB product ID as model which makes
the later code slightly more logical.

Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2020-10-27 15:26:20 -07:00
5 changed files with 311 additions and 64 deletions

View File

@ -100,6 +100,19 @@ AS_IF([test "x$with_libusb" != "xno"], [
])
])
# Checks for MTP support.
AC_ARG_WITH([libmtp],
[AS_HELP_STRING([--without-libmtp],
[Build without the libmtp library])],
[], [with_libmtp=auto])
AS_IF([test "x$with_libmtp" != "xno"], [
PKG_CHECK_MODULES([LIBMTP], [libmtp], [have_libmtp=yes], [have_libmtp=no])
AS_IF([test "x$have_libmtp" = "xyes"], [
AC_DEFINE([HAVE_LIBMTP], [1], [libmtp library])
DEPENDENCIES="$DEPENDENCIES libmtp"
])
])
# Checks for HIDAPI support.
AC_ARG_WITH([hidapi],
[AS_HELP_STRING([--without-hidapi],

View File

@ -1,9 +1,9 @@
AM_CPPFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include
AM_CFLAGS = $(LIBUSB_CFLAGS) $(HIDAPI_CFLAGS) $(BLUEZ_CFLAGS)
AM_CFLAGS = $(LIBUSB_CFLAGS) $(LIBMTP_CFLAGS) $(HIDAPI_CFLAGS) $(BLUEZ_CFLAGS)
lib_LTLIBRARIES = libdivecomputer.la
libdivecomputer_la_LIBADD = $(LIBUSB_LIBS) $(HIDAPI_LIBS) $(BLUEZ_LIBS) -lm
libdivecomputer_la_LIBADD = $(LIBUSB_LIBS) $(LIBMTP_LIBS) $(HIDAPI_LIBS) $(BLUEZ_LIBS) -lm
libdivecomputer_la_LDFLAGS = \
-version-info $(DC_VERSION_LIBTOOL) \
-no-undefined \

View File

@ -408,6 +408,7 @@ static const dc_descriptor_t g_descriptors[] = {
// Not merged upstream yet
/* Garmin */
{"Garmin", "Descent Mk1", DC_FAMILY_GARMIN, 2859, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin},
{"Garmin", "Descent Mk2", DC_FAMILY_GARMIN, 0x4CBA, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin},
/* Deepblu */
{"Deepblu", "Cosmiq+", DC_FAMILY_DEEPBLU, 0, DC_TRANSPORT_BLE, dc_filter_deepblu},
/* Oceans S1 */

View File

@ -35,10 +35,26 @@
#include "device-private.h"
#include "array.h"
#ifdef HAVE_LIBMTP
#include "libmtp.h"
#define GARMIN_VENDOR 0x091E
#define DESCENT_MK2 0x4CBA
// deal with ancient libmpt found on older Linux distros
#ifndef LIBMTP_FILES_AND_FOLDERS_ROOT
#define LIBMTP_FILES_AND_FOLDERS_ROOT 0xffffffff
#endif
#endif
typedef struct garmin_device_t {
dc_device_t base;
dc_iostream_t *iostream;
unsigned char fingerprint[FIT_NAME_SIZE];
#ifdef HAVE_LIBMTP
unsigned char use_mtp;
LIBMTP_mtpdevice_t *mtp_device;
#endif
} garmin_device_t;
static dc_status_t garmin_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size);
@ -77,6 +93,12 @@ garmin_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *ios
device->iostream = iostream;
memset(device->fingerprint, 0, sizeof(device->fingerprint));
#ifdef HAVE_LIBMTP
// if this is a Descent Mk2/Mk2i, we have to use MTP to access its storage
device->use_mtp = (model == DESCENT_MK2);
DEBUG(context, "Found Garmin with model 0x%x which is a %s\n", model, (model == DESCENT_MK2 ? "Mk2i" : "Mk1"));
#endif
*out = (dc_device_t *) device;
return DC_STATUS_SUCCESS;
@ -104,13 +126,19 @@ garmin_device_close (dc_device_t *abstract)
{
dc_status_t status = DC_STATUS_SUCCESS;
garmin_device_t *device = (garmin_device_t *) abstract;
#ifdef HAVE_LIBMTP
if (device->use_mtp && device->mtp_device)
LIBMTP_Release_Device(device->mtp_device);
#endif
return DC_STATUS_SUCCESS;
}
struct file_list {
int nr, allocated;
struct fit_name *array;
#ifdef HAVE_LIBMTP
unsigned int *mtp_ids; // only used in the MTP case
#endif
};
static int name_cmp(const void *a, const void *b)
@ -124,29 +152,29 @@ static int name_cmp(const void *a, const void *b)
*
* Return number of files found.
*/
static int get_file_list(dc_device_t *abstract, DIR *dir, struct file_list *files)
{
struct dirent *de;
DEBUG (abstract->context, "Iterating over Garmin files");
while ((de = readdir(dir)) != NULL) {
int len = strlen(de->d_name);
struct fit_name *entry;
static int
check_filename(dc_device_t *abstract, const char *name)
{
int len = strlen(name);
const char *explain = NULL;
DEBUG (abstract->context, " %s", de->d_name);
DEBUG (abstract->context, " %s", name);
if (len < 5)
explain = "name too short";
if (len >= FIT_NAME_SIZE)
explain = "name too long";
if (strncasecmp(de->d_name + len - 4, ".FIT", 4))
if (strncasecmp(name + len - 4, ".FIT", 4))
explain = "name lacks FIT suffix";
DEBUG (abstract->context, " %s - %s", de->d_name, explain ? explain : "adding to list");
if (explain)
continue;
DEBUG (abstract->context, " %s - %s", name, explain ? explain : "adding to list");
return explain == NULL;
}
static dc_status_t
make_space(struct file_list *files, unsigned char is_mtp)
{
if (files->nr == files->allocated) {
struct fit_name *array;
int n = 3*(files->allocated + 8)/2;
@ -156,20 +184,53 @@ static int get_file_list(dc_device_t *abstract, DIR *dir, struct file_list *file
array = realloc(files->array, new_size);
if (!array)
return DC_STATUS_NOMEMORY;
#ifdef HAVE_LIBMTP
if (is_mtp) {
unsigned int *mtp_ids;
new_size = n * sizeof(unsigned int);
mtp_ids = realloc(files->mtp_ids, new_size);
if (!mtp_ids)
return DC_STATUS_NOMEMORY;
files->mtp_ids = mtp_ids;
}
#else
(void)is_mtp;
#endif
files->array = array;
files->allocated = n;
}
return DC_STATUS_SUCCESS;
}
static void
add_name(struct file_list *files, const char *name)
{
/*
* NOTE! This depends on the zero-padding that strncpy does.
*
* strncpy() doesn't just limit the size of the copy, it
* will zero-pad the end of the result buffer.
*/
entry = files->array + files->nr++;
strncpy(entry->name, de->d_name, FIT_NAME_SIZE);
struct fit_name *entry = files->array + files->nr++;
strncpy(entry->name, name, FIT_NAME_SIZE);
entry->name[FIT_NAME_SIZE] = 0; // ensure it's null-terminated
}
static dc_status_t
get_file_list(dc_device_t *abstract, DIR *dir, struct file_list *files)
{
struct dirent *de;
DEBUG (abstract->context, "Iterating over Garmin files");
while ((de = readdir(dir)) != NULL) {
if (!check_filename(abstract, de->d_name))
continue;
dc_status_t rc = make_space(files, 0);
if (rc != DC_STATUS_SUCCESS)
return rc;
add_name(files, de->d_name);
}
DEBUG (abstract->context, "Found %d files", files->nr);
@ -177,6 +238,117 @@ static int get_file_list(dc_device_t *abstract, DIR *dir, struct file_list *file
return DC_STATUS_SUCCESS;
}
#ifdef HAVE_LIBMTP
static unsigned int
mtp_get_folder_id(dc_device_t *abstract, LIBMTP_mtpdevice_t *device, LIBMTP_devicestorage_t *storage, const char *folder, unsigned int parent_id)
{
DEBUG(abstract->context, "looking for folder %s under parent id %d", folder, parent_id);
// memory management is interesting here - we have to always walk the list returned and destroy them one by one
unsigned int folder_id = LIBMTP_FILES_AND_FOLDERS_ROOT;
LIBMTP_file_t* files = LIBMTP_Get_Files_And_Folders (device, storage->id, parent_id);
while (files != NULL) {
LIBMTP_file_t* mtp_file = files;
if (mtp_file->filename)
DEBUG(abstract->context, "looking at %s", mtp_file->filename);
if (mtp_file->filetype == LIBMTP_FILETYPE_FOLDER && mtp_file->filename && !strncasecmp(mtp_file->filename, folder, strlen(folder))) {
folder_id = mtp_file->item_id;
}
files = files->next;
LIBMTP_destroy_file_t(mtp_file);
}
return folder_id;
}
static dc_status_t
mtp_get_file_list(dc_device_t *abstract, struct file_list *files)
{
garmin_device_t *device = (garmin_device_t *)abstract;
LIBMTP_raw_device_t *rawdevices;
int numrawdevices;
int i;
LIBMTP_Init();
DEBUG(abstract->context, "Attempting to connect to mtp device");
switch (LIBMTP_Detect_Raw_Devices(&rawdevices, &numrawdevices)) {
case LIBMTP_ERROR_NO_DEVICE_ATTACHED:
DEBUG(abstract->context, "Garmin/mtp: no device found");
return DC_STATUS_NODEVICE;
case LIBMTP_ERROR_CONNECTING:
DEBUG(abstract->context, "Garmin/mtp: error connecting");
return DC_STATUS_NOACCESS;
case LIBMTP_ERROR_MEMORY_ALLOCATION:
DEBUG(abstract->context, "Garmin/mtp: memory allocation error");
return DC_STATUS_NOMEMORY;
case LIBMTP_ERROR_GENERAL: // Unknown general errors - that's bad
default:
DEBUG(abstract->context, "Garmin/mtp: unknown error");
return DC_STATUS_UNSUPPORTED;
case LIBMTP_ERROR_NONE:
DEBUG(abstract->context, "Garmin/mtp: successfully connected with %d raw devices", numrawdevices);
}
/* iterate through connected MTP devices */
for (i = 0; i < numrawdevices; i++) {
LIBMTP_devicestorage_t *storage;
char *devicename;
int ret;
// we only want to read from a Garmin Descent Mk2 device at this point
if (rawdevices[i].device_entry.vendor_id != GARMIN_VENDOR || rawdevices[i].device_entry.product_id != DESCENT_MK2) {
DEBUG(abstract->context, "Garmin/mtp: skipping raw device %04x/%04x",
rawdevices[i].device_entry.vendor_id, rawdevices[i].device_entry.product_id);
continue;
}
device->mtp_device = LIBMTP_Open_Raw_Device_Uncached(&rawdevices[i]);
if (device->mtp_device == NULL) {
DEBUG(abstract->context, "Garmin/mtp: unable to open raw device %d", i);
continue;
}
DEBUG(abstract->context, "Garmin/mtp: succcessfully opened device");
LIBMTP_Dump_Errorstack(device->mtp_device);
LIBMTP_Clear_Errorstack(device->mtp_device);
for (storage = device->mtp_device->storage; storage != 0; storage = storage->next) {
DEBUG(abstract->context, "Garmin/mtp: looking at storage %d", storage->id);
unsigned int garmin_id = mtp_get_folder_id(abstract, device->mtp_device, storage, "Garmin", LIBMTP_FILES_AND_FOLDERS_ROOT);
DEBUG(abstract->context, "Garmin/mtp: garmin folder at file_id %d", garmin_id);
if (garmin_id == LIBMTP_FILES_AND_FOLDERS_ROOT)
continue; // this storage partition didn't have a Garmin folder
unsigned int activity_id = mtp_get_folder_id(abstract, device->mtp_device, storage, "Activity", garmin_id);
DEBUG(abstract->context, "Garmin/mtp: activity folder at file_id %d", activity_id);
if (activity_id == LIBMTP_FILES_AND_FOLDERS_ROOT)
continue; // no Activity folder
// now walk that folder to create our file_list
LIBMTP_file_t* activity_files = LIBMTP_Get_Files_And_Folders (device->mtp_device, storage->id, activity_id);
while (activity_files != NULL) {
LIBMTP_file_t* mtp_file = activity_files;
if (mtp_file->filetype != LIBMTP_FILETYPE_FOLDER && mtp_file->filename) {
if (check_filename(abstract, mtp_file->filename)) {
dc_status_t rc = make_space(files, 1);
if (rc != DC_STATUS_SUCCESS)
return rc;
add_name(files, mtp_file->filename);
files->mtp_ids[files->nr - 1] = mtp_file->item_id;
}
}
activity_files = activity_files->next;
LIBMTP_destroy_file_t(mtp_file);
}
}
}
free(rawdevices);
DEBUG (abstract->context, "Found %d files", files->nr);
qsort(files->array, files->nr, sizeof(struct fit_name), name_cmp);
return DC_STATUS_SUCCESS;
}
#endif /* HAVE_LIBMTP */
#ifndef O_BINARY
#define O_BINARY 0
#endif
@ -213,6 +385,37 @@ read_file(char *pathname, int pathlen, const char *name, dc_buffer_t *file)
return rc;
}
#ifdef HAVE_LIBMTP
// MTP hands us the file data in chunks which we then just add to our data buffer
static uint16_t
mtp_put_func(void* params, void* priv, uint32_t sendlen, unsigned char *data, uint32_t *putlen)
{
dc_buffer_t *file = (dc_buffer_t *)priv;
dc_buffer_append(file, data, sendlen);
if (putlen)
*putlen = sendlen;
return 0;
}
// read the file from the MTP device and store the content in the data buffer
static dc_status_t
mtp_read_file(garmin_device_t *device, unsigned int file_id, dc_buffer_t *file)
{
dc_status_t rc = DC_STATUS_SUCCESS;
if (!device->mtp_device) {
DEBUG(device->base.context, "Garmin/mtp: cannot read file without MTP device");
return DC_STATUS_NODEVICE;
}
DEBUG(device->base.context, "Garmin/mtp: call Get_File_To_Handler");
if (LIBMTP_Get_File_To_Handler(device->mtp_device, file_id, &mtp_put_func, (void *) file, NULL, NULL) != 0) {
// show errors?
LIBMTP_Dump_Errorstack(device->mtp_device);
return DC_STATUS_IO;
}
return rc;
}
#endif /* HAVE_LIBMTP */
static dc_status_t
garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata)
{
@ -221,10 +424,17 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void
dc_parser_t *parser;
char pathname[PATH_MAX];
size_t pathlen;
struct file_list files = { 0, 0, NULL };
struct file_list files = {
0, // nr
0, // allocated
NULL, // array of names
#ifdef HAVE_LIBMTP
NULL // array of file ids
#endif
};
dc_buffer_t *file;
DIR *dir;
int rc;
dc_status_t rc;
// Read the directory name from the iostream
rc = dc_iostream_read(device->iostream, &pathname, sizeof(pathname)-1, &pathlen);
@ -239,18 +449,30 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void
ERROR (abstract->context, "Invalid Garmin base directory '%s'", pathname);
return DC_STATUS_IO;
}
DEBUG (abstract->context, "Garmin base directory '%s'", pathname);
if (pathlen && pathname[pathlen-1] != '/')
pathname[pathlen++] = '/';
strcpy(pathname + pathlen, "Garmin/Activity");
pathlen += strlen("Garmin/Activity");
#ifdef HAVE_LIBMTP
if (device->use_mtp) {
rc = mtp_get_file_list(abstract, &files);
DEBUG(abstract->context, "mtp_get_file_list rc %d with %d files", (int)rc, files.nr);
if (rc != DC_STATUS_SUCCESS || !files.nr) {
free(files.array);
free(files.mtp_ids);
return rc;
}
} else
#endif
{ // slight coding style violation to deal with the non-MTP case
dir = opendir(pathname);
if (!dir) {
ERROR (abstract->context, "Failed to open directory '%s'.", pathname);
return DC_STATUS_IO;
}
// Get the list of FIT files
rc = get_file_list(abstract, dir, &files);
closedir(dir);
@ -258,7 +480,8 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void
free(files.array);
return rc;
}
}
// We found at least one file
// Can we find the fingerprint entry?
for (int i = 0; i < files.nr; i++) {
const char *name = files.array[i].name;
@ -271,7 +494,6 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void
DEBUG (abstract->context, "Ignoring '%s' and older", name);
break;
}
// Enable progress notifications.
dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
progress.maximum = files.nr;
@ -306,8 +528,15 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void
// Reset the membuffer, read the data
dc_buffer_clear(file);
dc_buffer_append(file, name, FIT_NAME_SIZE);
#ifdef HAVE_LIBMTP
if (device->use_mtp) {
DEBUG(abstract->context, "start reading file id %d", files.mtp_ids[i]);
status = mtp_read_file(device, files.mtp_ids[i], file);
} else
#endif
{
status = read_file(pathname, pathlen, name, file);
}
if (status != DC_STATUS_SUCCESS)
break;

View File

@ -76,10 +76,14 @@ dc_usb_storage_open (dc_iostream_t **out, dc_context_t *context, const char *nam
if (out == NULL || name == NULL)
return DC_STATUS_INVALIDARGS;
if (*name == '\0') {
// that indicates an MTP device
INFO (context, "Open MTP device");
} else {
INFO (context, "Open: name=%s", name);
if (stat(name, &st) < 0 || !S_ISDIR(st.st_mode))
return DC_STATUS_NODEVICE;
}
// Allocate memory.
device = (dc_usbstorage_t *) dc_iostream_allocate (context, &dc_usbstorage_vtable, DC_TRANSPORT_USBSTORAGE);
if (device == NULL) {