From 8735156e892fe52d047554a2e35c05db198908cc Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Mon, 27 Aug 2018 09:16:42 -0700 Subject: [PATCH 01/16] Add 'USB storage' transport enumeration We now have at least two dive computers that enumerate as USB storage devices: the Uemis Zurich and the Garmin Descent Mk1. The Uemis is handled purely inside of subsurface, with no libdivecomputer support. That was likely a mistake, but it was not practical to do a libdivecomputer backend for it at the time. The Garmin Descent Mk1 support would be much nicer to have natively in libdivecomputer, and looks much more relevant and practical than the Uemis situation was. So start off with defining a new transport type. Signed-off-by: Linus Torvalds --- examples/common.c | 3 + include/libdivecomputer/common.h | 6 +- include/libdivecomputer/iostream.h | 3 + src/Makefile.am | 1 + src/context.c | 1 + src/usb_storage.c | 109 +++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/usb_storage.c diff --git a/examples/common.c b/examples/common.c index b97343f..830eac2 100644 --- a/examples/common.c +++ b/examples/common.c @@ -98,6 +98,7 @@ static const transport_table_t g_transports[] = { {"irda", DC_TRANSPORT_IRDA}, {"bluetooth", DC_TRANSPORT_BLUETOOTH}, {"ble", DC_TRANSPORT_BLE}, + {"usbstorage",DC_TRANSPORT_USBSTORAGE}, }; const char * @@ -536,6 +537,8 @@ dctool_iostream_open (dc_iostream_t **iostream, dc_context_t *context, dc_descri return dctool_irda_open (iostream, context, descriptor, devname); case DC_TRANSPORT_BLUETOOTH: return dctool_bluetooth_open (iostream, context, descriptor, devname); + case DC_TRANSPORT_USBSTORAGE: + return dc_usb_storage_open (iostream, context, devname); default: return DC_STATUS_UNSUPPORTED; } diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index 1058b01..7d89706 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -48,9 +48,13 @@ typedef enum dc_transport_t { DC_TRANSPORT_USBHID = (1 << 2), DC_TRANSPORT_IRDA = (1 << 3), DC_TRANSPORT_BLUETOOTH = (1 << 4), - DC_TRANSPORT_BLE = (1 << 5) + DC_TRANSPORT_BLE = (1 << 5), + DC_TRANSPORT_USBSTORAGE= (1 << 6), } dc_transport_t; +// Idiotic enums can't be queried +#define DC_TRANSPORT_USBSTORAGE DC_TRANSPORT_USBSTORAGE + typedef enum dc_family_t { DC_FAMILY_NULL = 0, /* Suunto */ diff --git a/include/libdivecomputer/iostream.h b/include/libdivecomputer/iostream.h index e2cb439..b1b36a6 100644 --- a/include/libdivecomputer/iostream.h +++ b/include/libdivecomputer/iostream.h @@ -282,6 +282,9 @@ dc_iostream_sleep (dc_iostream_t *iostream, unsigned int milliseconds); dc_status_t dc_iostream_close (dc_iostream_t *iostream); +dc_status_t +dc_usb_storage_open (dc_iostream_t **out, dc_context_t *context, const char *name); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/Makefile.am b/src/Makefile.am index 6afa24e..12f6b61 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -75,6 +75,7 @@ libdivecomputer_la_SOURCES = \ irda.c \ usbhid.c \ bluetooth.c \ + usb_storage.c \ custom.c if OS_WIN32 diff --git a/src/context.c b/src/context.c index 58009e1..7b6b969 100644 --- a/src/context.c +++ b/src/context.c @@ -334,6 +334,7 @@ dc_context_get_transports (dc_context_t *context) #elif defined(HAVE_LIBUSB) && !defined(__APPLE__) | DC_TRANSPORT_USBHID #endif + | DC_TRANSPORT_USBSTORAGE #ifdef _WIN32 #ifdef HAVE_AF_IRDA_H | DC_TRANSPORT_IRDA diff --git a/src/usb_storage.c b/src/usb_storage.c new file mode 100644 index 0000000..c667673 --- /dev/null +++ b/src/usb_storage.c @@ -0,0 +1,109 @@ +/* + * Dummy "stream" operations for USB storage + * + * Copyright (C) 2018 Linus Torvalds + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "common-private.h" +#include "context-private.h" +#include "iostream-private.h" +#include "iterator-private.h" +#include "descriptor-private.h" +#include "timer.h" + +// Fake "device" that just contains the directory name that +// you can read out of the iostream. All the actual IO is +// up to you. +typedef struct dc_usbstorage_t { + dc_iostream_t base; + char pathname[PATH_MAX]; +} dc_usbstorage_t; + +static dc_status_t +dc_usb_storage_read (dc_iostream_t *iostream, void *data, size_t size, size_t *actual); + +static const dc_iostream_vtable_t dc_usbstorage_vtable = { + sizeof(dc_usbstorage_t), + NULL, /* set_timeout */ + NULL, /* set_latency */ + NULL, /* set_break */ + NULL, /* set_dtr */ + NULL, /* set_rts */ + NULL, /* get_lines */ + NULL, /* get_available */ + NULL, /* configure */ + dc_usb_storage_read, /* read */ + NULL, /* write */ + NULL, /* flush */ + NULL, /* purge */ + NULL, /* sleep */ + NULL, /* close */ +}; + +dc_status_t +dc_usb_storage_open (dc_iostream_t **out, dc_context_t *context, const char *name) +{ + dc_usbstorage_t *device = NULL; + struct stat st; + + if (out == NULL || name == NULL) + return DC_STATUS_INVALIDARGS; + + 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) { + SYSERROR (context, ENOMEM); + return DC_STATUS_NOMEMORY; + } + + strncpy(device->pathname, name, PATH_MAX); + device->pathname[PATH_MAX-1] = 0; + + *out = (dc_iostream_t *) device; + return DC_STATUS_SUCCESS; +} + +static dc_status_t +dc_usb_storage_read (dc_iostream_t *iostream, void *data, size_t size, size_t *actual) +{ + dc_usbstorage_t *device = (dc_usbstorage_t *) iostream; + size_t len = strlen(device->pathname); + + if (size <= len) + return DC_STATUS_IO; + memcpy(data, device->pathname, len+1); + if (actual) + *actual = len; + return DC_STATUS_SUCCESS; +} From 8f790b52e4a219dfa2baf8b6502a38d3352745be Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Mon, 27 Aug 2018 11:00:39 -0700 Subject: [PATCH 02/16] Add Garmin Descent Mk1 skeleton This does absolutely nothing, but it adds the basic skeleton for a new dive computer support. Not only don't I have any real code for any of this yet, but I actually think it might be useful to have a "this is how to add a new dive computer" example commit. Signed-off-by: Linus Torvalds --- examples/common.c | 1 + include/libdivecomputer/common.h | 2 + msvc/libdivecomputer.vcproj | 12 ++++ src/Makefile.am | 1 + src/descriptor.c | 16 +++++ src/device.c | 4 ++ src/garmin.c | 107 ++++++++++++++++++++++++++++ src/garmin.h | 43 +++++++++++ src/garmin_parser.c | 118 +++++++++++++++++++++++++++++++ src/parser.c | 4 ++ 10 files changed, 308 insertions(+) create mode 100644 src/garmin.c create mode 100644 src/garmin.h create mode 100644 src/garmin_parser.c diff --git a/examples/common.c b/examples/common.c index 830eac2..ad7086f 100644 --- a/examples/common.c +++ b/examples/common.c @@ -89,6 +89,7 @@ static const backend_table_t g_backends[] = { {"idive", DC_FAMILY_DIVESYSTEM_IDIVE, 0x03}, {"cochran", DC_FAMILY_COCHRAN_COMMANDER, 0}, {"divecomputereu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0}, + {"descentmk1", DC_FAMILY_GARMIN, 0}, }; static const transport_table_t g_transports[] = { diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index 7d89706..8d77b9c 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -107,6 +107,8 @@ typedef enum dc_family_t { DC_FAMILY_COCHRAN_COMMANDER = (14 << 16), /* Tecdiving */ DC_FAMILY_TECDIVING_DIVECOMPUTEREU = (15 << 16), + /* Garmin */ + DC_FAMILY_GARMIN = (16 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index 1a88ff8..5ab415c 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -490,6 +490,14 @@ RelativePath="..\src\tecdiving_divecomputereu_parser.c" > + + + + @@ -828,6 +836,10 @@ RelativePath="..\src\tecdiving_divecomputereu.h" > + + diff --git a/src/Makefile.am b/src/Makefile.am index 12f6b61..b9ddbef 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -71,6 +71,7 @@ libdivecomputer_la_SOURCES = \ buffer.c \ cochran_commander.h cochran_commander.c cochran_commander_parser.c \ tecdiving_divecomputereu.h tecdiving_divecomputereu.c tecdiving_divecomputereu_parser.c \ + garmin.h garmin.c garmin_parser.c \ socket.h socket.c \ irda.c \ usbhid.c \ diff --git a/src/descriptor.c b/src/descriptor.c index 6871e1b..d664beb 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -34,6 +34,7 @@ static int dc_filter_suunto (dc_transport_t transport, const void *userdata); static int dc_filter_shearwater (dc_transport_t transport, const void *userdata); static int dc_filter_hw (dc_transport_t transport, const void *userdata); static int dc_filter_tecdiving (dc_transport_t transport, const void *userdata); +static int dc_filter_garmin (dc_transport_t transport, const void *userdata); static dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -326,6 +327,8 @@ static const dc_descriptor_t g_descriptors[] = { {"Cochran", "EMC-20H", DC_FAMILY_COCHRAN_COMMANDER, 5, DC_TRANSPORT_SERIAL, NULL}, /* Tecdiving DiveComputer.eu */ {"Tecdiving", "DiveComputer.eu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_tecdiving}, + /* Garmin */ + {"Garmin", "Descent Mk1", DC_FAMILY_GARMIN, 0, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin}, }; static int @@ -465,6 +468,19 @@ static int dc_filter_tecdiving (dc_transport_t transport, const void *userdata) return 1; } +static int dc_filter_garmin (dc_transport_t transport, const void *userdata) +{ + static const dc_usb_desc_t usbhid[] = { + {0x091e, 0x2b2b}, // Garmin Descent Mk1 + }; + + if (transport == DC_TRANSPORT_USBSTORAGE) { + return dc_filter_internal_usb ((const dc_usb_desc_t *) userdata, usbhid, C_ARRAY_SIZE(usbhid)); + } + + return 1; +} + dc_status_t dc_descriptor_iterator (dc_iterator_t **out) { diff --git a/src/device.c b/src/device.c index fc59464..c00aa3b 100644 --- a/src/device.c +++ b/src/device.c @@ -56,6 +56,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "garmin.h" #include "device-private.h" #include "context-private.h" @@ -207,6 +208,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_TECDIVING_DIVECOMPUTEREU: rc = tecdiving_divecomputereu_device_open (&device, context, iostream); break; + case DC_FAMILY_GARMIN: + rc = garmin_device_open (&device, context, iostream); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/garmin.c b/src/garmin.c new file mode 100644 index 0000000..df375e3 --- /dev/null +++ b/src/garmin.c @@ -0,0 +1,107 @@ +/* + * Garmin Descent Mk1 USB storage downloading + * + * Copyright (C) 2018 Linus Torvalds + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include +#include + +#include "garmin.h" +#include "context-private.h" +#include "device-private.h" +#include "array.h" + +typedef struct garmin_device_t { + dc_device_t base; + dc_iostream_t *iostream; +} garmin_device_t; + +static dc_status_t garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); +static dc_status_t garmin_device_close (dc_device_t *abstract); + +static const dc_device_vtable_t garmin_device_vtable = { + sizeof(garmin_device_t), + DC_FAMILY_GARMIN, + NULL, /* set_fingerprint */ + NULL, /* read */ + NULL, /* write */ + NULL, /* dump */ + garmin_device_foreach, /* foreach */ + NULL, /* timesync */ + garmin_device_close, /* close */ +}; + +dc_status_t +garmin_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream) +{ + dc_status_t status = DC_STATUS_SUCCESS; + garmin_device_t *device = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + device = (garmin_device_t *) dc_device_allocate (context, &garmin_device_vtable); + if (device == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + device->iostream = iostream; + + *out = (dc_device_t *) device; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +garmin_device_close (dc_device_t *abstract) +{ + dc_status_t status = DC_STATUS_SUCCESS; + garmin_device_t *device = (garmin_device_t *) abstract; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + garmin_device_t *device = (garmin_device_t *) abstract; + + // Enable progress notifications. + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Emit a device info event. + dc_event_devinfo_t devinfo; + devinfo.model = 0; + devinfo.firmware = 0; + devinfo.serial = 0; + device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); + + // Emit a vendor event. + dc_event_vendor_t vendor; + vendor.data = "Garmin"; + vendor.size = 6; + device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); + + return status; +} diff --git a/src/garmin.h b/src/garmin.h new file mode 100644 index 0000000..fd1caa4 --- /dev/null +++ b/src/garmin.h @@ -0,0 +1,43 @@ +/* + * Garmin Descent Mk1 + * + * Copyright (C) 2018 Linus Torvalds + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#ifndef GARMIN_H +#define GARMIN_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t +garmin_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream); + +dc_status_t +garmin_parser_create (dc_parser_t **parser, dc_context_t *context); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* GARMIN_H */ diff --git a/src/garmin_parser.c b/src/garmin_parser.c new file mode 100644 index 0000000..077e26d --- /dev/null +++ b/src/garmin_parser.c @@ -0,0 +1,118 @@ +/* + * Garmin Descent Mk1 parsing + * + * Copyright (C) 2018 Linus Torvalds + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include + +#include "garmin.h" +#include "context-private.h" +#include "parser-private.h" +#include "array.h" + +typedef struct garmin_parser_t { + dc_parser_t base; +} garmin_parser_t; + +static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size); +static dc_status_t garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime); +static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value); +static dc_status_t garmin_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata); + +static const dc_parser_vtable_t garmin_parser_vtable = { + sizeof(garmin_parser_t), + DC_FAMILY_GARMIN, + garmin_parser_set_data, /* set_data */ + garmin_parser_get_datetime, /* datetime */ + garmin_parser_get_field, /* fields */ + garmin_parser_samples_foreach, /* samples_foreach */ + NULL /* destroy */ +}; + + +dc_status_t +garmin_parser_create (dc_parser_t **out, dc_context_t *context) +{ + garmin_parser_t *parser = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + parser = (garmin_parser_t *) dc_parser_allocate (context, &garmin_parser_vtable); + if (parser == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + *out = (dc_parser_t *) parser; + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size) +{ + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) +{ + const unsigned char *data = abstract->data; + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) +{ + const unsigned char *data = abstract->data; + + if (!value) + return DC_STATUS_INVALIDARGS; + + switch (type) { + case DC_FIELD_DIVETIME: + *((unsigned int *) value) = 0; + break; + case DC_FIELD_AVGDEPTH: + *((double *) value) = 0; + break; + case DC_FIELD_MAXDEPTH: + *((double *) value) = 0; + break; + default: + return DC_STATUS_UNSUPPORTED; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +garmin_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) +{ + const unsigned char *data = abstract->data; + unsigned int size = abstract->size; + + return DC_STATUS_SUCCESS; +} diff --git a/src/parser.c b/src/parser.c index e9e97a2..1795f62 100644 --- a/src/parser.c +++ b/src/parser.c @@ -56,6 +56,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "garmin.h" #include "context-private.h" #include "parser-private.h" @@ -168,6 +169,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_TECDIVING_DIVECOMPUTEREU: rc = tecdiving_divecomputereu_parser_create (&parser, context); break; + case DC_FAMILY_GARMIN: + rc = garmin_parser_create (&parser, context); + break; default: return DC_STATUS_INVALIDARGS; } From a726a38cbbc1cec27e047b92eb9d96276a2fe383 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Mon, 27 Aug 2018 15:10:33 -0700 Subject: [PATCH 03/16] Garmin Descent Mk1: flesh out the actual downloading part It's really just reading files from storage, but with the proper sorting and fingerprint handling. The Garmin back-end does no actual parsing yet, so the end result is garbage, but now the data has technically been downloaded. Without the parser, I haven't actually verified that any of it is remotely correct, but it all looks good. Signed-off-by: Linus Torvalds --- src/garmin.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/src/garmin.c b/src/garmin.c index df375e3..01fe464 100644 --- a/src/garmin.c +++ b/src/garmin.c @@ -20,25 +20,44 @@ */ #include +#include +#include #include +#include +#include +#include +#include +#include +#include #include "garmin.h" #include "context-private.h" #include "device-private.h" #include "array.h" +// The dive names are of the form "2018-08-20-10-23-30.fit" +// With the terminating zero, that's 24 bytes. +// We use this as the fingerprint too +#define FIT_NAME_SIZE 24 + +struct fit_name { + char name[FIT_NAME_SIZE]; +}; + typedef struct garmin_device_t { dc_device_t base; dc_iostream_t *iostream; + unsigned char fingerprint[FIT_NAME_SIZE]; } garmin_device_t; +static dc_status_t garmin_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size); static dc_status_t garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); static dc_status_t garmin_device_close (dc_device_t *abstract); static const dc_device_vtable_t garmin_device_vtable = { sizeof(garmin_device_t), DC_FAMILY_GARMIN, - NULL, /* set_fingerprint */ + garmin_device_set_fingerprint, /* set_fingerprint */ NULL, /* read */ NULL, /* write */ NULL, /* dump */ @@ -65,12 +84,30 @@ garmin_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *ios // Set the default values. device->iostream = iostream; + memset(device->fingerprint, 0, sizeof(device->fingerprint)); *out = (dc_device_t *) device; return DC_STATUS_SUCCESS; } +static dc_status_t +garmin_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size) +{ + garmin_device_t *device = (garmin_device_t *)abstract; + + if (size && size != sizeof (device->fingerprint)) + return DC_STATUS_INVALIDARGS; + + if (size) + memcpy (device->fingerprint, data, sizeof (device->fingerprint)); + else + memset (device->fingerprint, 0, sizeof (device->fingerprint)); + + return DC_STATUS_SUCCESS; +} + + static dc_status_t garmin_device_close (dc_device_t *abstract) { @@ -80,14 +117,143 @@ garmin_device_close (dc_device_t *abstract) return DC_STATUS_SUCCESS; } +struct file_list { + int nr, allocated; + struct fit_name *array; +}; + +static int name_cmp(const void *a, const void *b) +{ + // Sort reverse string ordering (newest first), so use 'b,a' + return strcmp(b,a); +} + +/* + * Get the FIT file list and sort it. + * + * Return number of files found. +*/ +static int get_file_list(DIR *dir, struct file_list *files) +{ + struct dirent *de; + + while ((de = readdir(dir)) != NULL) { + int len = strlen(de->d_name); + + if (len != FIT_NAME_SIZE-1) + continue; + if (strncasecmp(de->d_name + len - 4, ".FIT", 4)) + continue; + + if (files->nr == files->allocated) { + struct fit_name *array; + int n = 3*(files->allocated + 8)/2; + size_t new_size; + + new_size = n * sizeof(array[0]); + array = realloc(files->array, new_size); + if (!array) + return DC_STATUS_NOMEMORY; + + files->array = array; + files->allocated = n; + } + + memcpy(files->array + files->nr++, de->d_name, FIT_NAME_SIZE); + } + + qsort(files->array, files->nr, sizeof(struct fit_name), name_cmp); + return DC_STATUS_SUCCESS; +} + +static dc_status_t +read_file(char *pathname, int pathlen, const char *name, dc_buffer_t *file) +{ + int fd, rc; + + pathname[pathlen] = '/'; + memcpy(pathname+pathlen+1, name, FIT_NAME_SIZE); + fd = open(pathname, O_RDONLY); + + if (fd < 0) + return DC_STATUS_IO; + + rc = DC_STATUS_SUCCESS; + for (;;) { + char buffer[4096]; + int n; + + n = read(fd, buffer, sizeof(buffer)); + if (!n) + break; + if (n > 0) { + dc_buffer_append(file, buffer, n); + continue; + } + rc = DC_STATUS_IO; + break; + } + + close(fd); + return rc; +} + static dc_status_t garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) { dc_status_t status = DC_STATUS_SUCCESS; garmin_device_t *device = (garmin_device_t *) abstract; + char pathname[PATH_MAX]; + size_t pathlen; + struct file_list files = { 0, 0, NULL }; + dc_buffer_t *file; + DIR *dir; + int rc; + + // Read the directory name from the iostream + rc = dc_iostream_read(device->iostream, &pathname, sizeof(pathname), &pathlen); + if (rc != DC_STATUS_SUCCESS) + return rc; + + // The actual dives are under the "Garmin/Activity/" directory + // as FIT files, with names like "2018-08-20-10-23-30.fit". + // Make sure our buffer is big enough. + if (pathlen + strlen("/Garmin/Activity/") + FIT_NAME_SIZE + 2 > PATH_MAX) + return DC_STATUS_IO; + + if (pathlen && pathname[pathlen-1] != '/') + pathname[pathlen++] = '/'; + strcpy(pathname + pathlen, "Garmin/Activity"); + pathlen += strlen("Garmin/Activity"); + + dir = opendir(pathname); + if (!dir) + return DC_STATUS_IO; + + // Get the list of FIT files + rc = get_file_list(dir, &files); + closedir(dir); + if (rc != DC_STATUS_SUCCESS || !files.nr) { + free(files.array); + return rc; + } + + // Can we find the fingerprint entry? + for (int i = 0; i < files.nr; i++) { + const char *name = files.array[i].name; + + if (memcmp(name, device->fingerprint, sizeof (device->fingerprint))) + continue; + + // Found fingerprint, just cut the array short here + files.nr = i; + break; + } // Enable progress notifications. dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + progress.maximum = files.nr; + progress.current = 0; device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); // Emit a device info event. @@ -103,5 +269,41 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void vendor.size = 6; device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); + file = dc_buffer_new (16384); + if (file == NULL) { + ERROR (abstract->context, "Insufficient buffer space available."); + free(files.array); + return DC_STATUS_NOMEMORY; + } + + for (int i = 0; i < files.nr; i++) { + const char *name = files.array[i].name; + const unsigned char *data; + unsigned int size; + + if (device_is_cancelled(abstract)) { + status = DC_STATUS_CANCELLED; + break; + } + + // Reset the membuffer, read the data + dc_buffer_clear(file); + dc_buffer_append(file, name, FIT_NAME_SIZE); + + status = read_file(pathname, pathlen, name, file); + if (status != DC_STATUS_SUCCESS) + break; + + data = dc_buffer_get_data(file); + size = dc_buffer_get_size(file); + + if (callback && !callback(data, size, name, FIT_NAME_SIZE, userdata)) + break; + + progress.current++; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + } + + free(files.array); return status; } From 2c7479ad1c5b0f6a03ce489410fffd0e6b9dc2a0 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Mon, 27 Aug 2018 18:15:02 -0700 Subject: [PATCH 04/16] Garmin: start parsing definition records This is _very_ incomplete. The FIT file is really fairly generic, but this has the basics for parsing, with tables to look up the low-level parsers by the FIT "message ID" and "field nr". It doesn't actually parse anything yet, so consider this a FIT decoder skeleton. Right now it basically prints out the different record values, and names then for the (few) cases where I've found or guessed the numbers. Signed-off-by: Linus Torvalds --- src/garmin.c | 9 - src/garmin.h | 11 ++ src/garmin_parser.c | 471 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 481 insertions(+), 10 deletions(-) diff --git a/src/garmin.c b/src/garmin.c index 01fe464..4c8c92f 100644 --- a/src/garmin.c +++ b/src/garmin.c @@ -35,15 +35,6 @@ #include "device-private.h" #include "array.h" -// The dive names are of the form "2018-08-20-10-23-30.fit" -// With the terminating zero, that's 24 bytes. -// We use this as the fingerprint too -#define FIT_NAME_SIZE 24 - -struct fit_name { - char name[FIT_NAME_SIZE]; -}; - typedef struct garmin_device_t { dc_device_t base; dc_iostream_t *iostream; diff --git a/src/garmin.h b/src/garmin.h index fd1caa4..50f9f6e 100644 --- a/src/garmin.h +++ b/src/garmin.h @@ -37,6 +37,17 @@ garmin_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t * dc_status_t garmin_parser_create (dc_parser_t **parser, dc_context_t *context); +// The dive names are of the form "2018-08-20-10-23-30.fit" +// With the terminating zero, that's 24 bytes. +// +// We use this as the fingerprint, but it ends up being a +// special fixed header in the parser data too. +#define FIT_NAME_SIZE 24 + +struct fit_name { + char name[FIT_NAME_SIZE]; +}; + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 077e26d..e11e4d9 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -20,16 +20,60 @@ */ #include +#include +#include #include "garmin.h" #include "context-private.h" #include "parser-private.h" #include "array.h" +#define C_ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a))) + +#define MAXFIELDS 128 + +struct msg_desc; + +// Local types +struct type_desc { + const char *msg_name; + const struct msg_desc *msg_desc; + unsigned char nrfields; + unsigned char fields[MAXFIELDS][3]; +}; + +#define MAXTYPE 16 +#define MAXGASES 16 +#define MAXSTRINGS 32 + typedef struct garmin_parser_t { dc_parser_t base; + struct type_desc type_desc[MAXTYPE]; + // Field cache + struct { + unsigned int initialized; + unsigned int protocol; + unsigned int profile; + unsigned int divetime; + double maxdepth; + double avgdepth; + unsigned int ngases; + dc_gasmix_t gasmix[MAXGASES]; + dc_salinity_t salinity; + double surface_pressure; + dc_divemode_t divemode; + double lowsetpoint; + double highsetpoint; + double customsetpoint; + dc_field_string_t strings[MAXSTRINGS]; + dc_tankinfo_t tankinfo[MAXGASES]; + double tanksize[MAXGASES]; + double tankworkingpressure[MAXGASES]; + } cache; } garmin_parser_t; +typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user); + static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size); static dc_status_t garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime); static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value); @@ -45,7 +89,6 @@ static const dc_parser_vtable_t garmin_parser_vtable = { NULL /* destroy */ }; - dc_status_t garmin_parser_create (dc_parser_t **out, dc_context_t *context) { @@ -66,10 +109,420 @@ garmin_parser_create (dc_parser_t **out, dc_context_t *context) return DC_STATUS_SUCCESS; } +typedef unsigned char ENUM; +typedef unsigned short UINT16; +typedef unsigned int UINT32; + +/* + * Garmin FIT events are described by tuples of "global mesg ID" and + * a "field number". There's lots of them, because you have events + * for pretty much anything ("cycling gear change") etc. + * + * There's a SDK that generates tables for you, but it looks nasty. + * + * So instead, we try to make sense of it manually. + */ +struct field_desc { + const char *name; + int (*parse)(struct garmin_parser_t *, const unsigned char *data); +}; + +#define DECLARE_FIELD(msg, name, type) __DECLARE_FIELD(msg##_##name, type) +#define __DECLARE_FIELD(name, type) \ + static int parse_##name(struct garmin_parser_t *, const type); \ + static int parse_##name##_##type(struct garmin_parser_t *g, const unsigned char *p) \ + { \ + type val = *(type *)p; \ + fprintf(stderr, "%s: %llx\n", #name, (long long)val); \ + return parse_##name(g, *(type *)p); \ + } \ + static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ + static int parse_##name(struct garmin_parser_t *garmin, type data) + +// FILE msg +DECLARE_FIELD(FILE, file_type, ENUM) { return 0; } +DECLARE_FIELD(FILE, manufacturer, UINT16) { return 0; } +DECLARE_FIELD(FILE, product, UINT16) { return 0; } +DECLARE_FIELD(FILE, serial, UINT32) { return 0; } +DECLARE_FIELD(FILE, creation_time, UINT32) { return 0; } + +// SESSION msg +DECLARE_FIELD(SESSION, start_time, UINT32) { return 0; } +DECLARE_FIELD(SESSION, timestamp, UINT32) { return 0; } + +// RECORD msg +DECLARE_FIELD(RECORD, start_time, UINT32) { return 0; } +DECLARE_FIELD(RECORD, timestamp, UINT32) { return 0; } + + +struct msg_desc { + unsigned char maxfield; + const struct field_desc *field[]; +}; + +#define SET_FIELD(msg, nr, name, type) \ + [nr] = &msg##_##name##_field_##type + +#define DECLARE_MESG(name) \ + static const struct msg_desc name##_msg_desc + +DECLARE_MESG(FILE) = { + .maxfield = 5, + .field = { + SET_FIELD(FILE, 0, file_type, ENUM), + SET_FIELD(FILE, 1, manufacturer, UINT16), + SET_FIELD(FILE, 2, product, UINT16), + SET_FIELD(FILE, 3, serial, UINT32), + SET_FIELD(FILE, 4, creation_time, UINT32), + } +}; + +DECLARE_MESG(DEVICE_SETTINGS) = { }; +DECLARE_MESG(USER_PROFILE) = { }; +DECLARE_MESG(ZONES_TARGET) = { }; +DECLARE_MESG(SPORT) = { }; + +DECLARE_MESG(SESSION) = { + .maxfield = 254, + .field = { + SET_FIELD(SESSION, 2, start_time, UINT32), + SET_FIELD(SESSION, 253, timestamp, UINT32), + } +}; + +DECLARE_MESG(LAP) = { }; + +DECLARE_MESG(RECORD) = { + .maxfield = 254, + .field = { + SET_FIELD(RECORD, 2, start_time, UINT32), + SET_FIELD(RECORD, 253, timestamp, UINT32), + } +}; + +DECLARE_MESG(EVENT) = { }; +DECLARE_MESG(DEVICE_INFO) = { }; +DECLARE_MESG(ACTIVITY) = { }; +DECLARE_MESG(FILE_CREATOR) = { }; +DECLARE_MESG(DIVE_SETTINGS) = { }; +DECLARE_MESG(DIVE_GAS) = { }; +DECLARE_MESG(DIVE_ALARM) = { }; +DECLARE_MESG(DIVE_SUMMARY) = { }; + +// Unknown global message ID's.. +DECLARE_MESG(WTF_13) = { }; +DECLARE_MESG(WTF_22) = { }; +DECLARE_MESG(WTF_79) = { }; +DECLARE_MESG(WTF_104) = { }; +DECLARE_MESG(WTF_125) = { }; +DECLARE_MESG(WTF_140) = { }; +DECLARE_MESG(WTF_141) = { }; +DECLARE_MESG(WTF_233) = { }; + +#define SET_MESG(nr, name) [nr] = { #name, &name##_msg_desc } + +static const struct { + const char *name; + const struct msg_desc *desc; +} message_array[] = { + SET_MESG( 0, FILE), + SET_MESG( 2, DEVICE_SETTINGS), + SET_MESG( 3, USER_PROFILE), + SET_MESG( 7, ZONES_TARGET), + SET_MESG( 12, SPORT), + SET_MESG( 13, WTF_13), + SET_MESG( 18, SESSION), + SET_MESG( 19, LAP), + SET_MESG( 20, RECORD), + SET_MESG( 21, EVENT), + SET_MESG( 22, WTF_22), + SET_MESG( 23, DEVICE_INFO), + SET_MESG( 34, ACTIVITY), + SET_MESG( 49, FILE_CREATOR), + SET_MESG( 79, WTF_79), + + SET_MESG(104, WTF_104), + SET_MESG(125, WTF_125), + SET_MESG(140, WTF_140), + SET_MESG(141, WTF_141), + + SET_MESG(233, WTF_233), + SET_MESG(258, DIVE_SETTINGS), + SET_MESG(259, DIVE_GAS), + SET_MESG(262, DIVE_ALARM), + SET_MESG(268, DIVE_SUMMARY), +}; + +#define MSG_NAME_LEN 16 +static const struct msg_desc *lookup_msg_desc(unsigned short msg, int local, const char **namep) +{ + static struct msg_desc local_array[16]; + static char local_name[16][MSG_NAME_LEN]; + struct msg_desc *desc; + char *name; + + /* Do we have a real one? */ + if (msg < C_ARRAY_SIZE(message_array) && message_array[msg].name) { + *namep = message_array[msg].name; + return message_array[msg].desc; + } + + /* If not, fake it */ + desc = &local_array[local]; + memset(desc, 0, sizeof(*desc)); + + name = local_name[local]; + snprintf(name, MSG_NAME_LEN, "msg-%d", msg); + *namep = name; + return desc; +} + +static int traverse_compressed(struct garmin_parser_t *garmin, + const unsigned char *data, unsigned int size, + unsigned char type, unsigned int time) +{ + fprintf(stderr, "Compressed record for local type %d:\n", type); + return -1; +} + +static int traverse_regular(struct garmin_parser_t *garmin, + const unsigned char *data, unsigned int size, + unsigned char type, unsigned int *timep) +{ + unsigned int total_len = 0; + struct type_desc *desc = garmin->type_desc + type; + const struct msg_desc *msg_desc = desc->msg_desc; + const char *msg_name = desc->msg_name; + + if (!msg_desc) { + ERROR(garmin->base.context, "Uninitialized type descriptor %d\n", type); + return -1; + } + + for (int i = 0; i < desc->nrfields; i++) { + const unsigned char *field = desc->fields[i]; + unsigned int field_nr = field[0]; + unsigned int len = field[1]; + unsigned int base_type = field[2] & 0x7f; + static const int base_size_array[] = { 1, 1, 1, 2, 2, 4, 4, 1, 4, 8, 1, 2, 4, 1, 8, 8, 8 }; + const struct field_desc *field_desc; + unsigned int base_size; + + if (!len) { + ERROR(garmin->base.context, "field with zero length\n"); + return -1; + } + + if (size < len) { + ERROR(garmin->base.context, "Data traversal size bigger than remaining data (%d vs %d)\n", len, size); + return -1; + } + + if (base_type > 16) { + ERROR(garmin->base.context, "Unknown base type %d\n", base_type); + data += size; + len -= size; + continue; + } + base_size = base_size_array[base_type]; + if (len % base_size) { + ERROR(garmin->base.context, "Data traversal size not a multiple of base size (%d vs %d)\n", len, base_size); + return -1; + } + // String + if (base_type == 7) { + int string_len = strnlen(data, size); + if (string_len >= size) { + ERROR(garmin->base.context, "Data traversal string bigger than remaining data\n"); + return -1; + } + if (len <= string_len) { + ERROR(garmin->base.context, "field length %d, string length %d\n", len, string_len + 1); + return -1; + } + } + + + field_desc = NULL; + if (field_nr < msg_desc->maxfield) + field_desc = msg_desc->field[field_nr]; + + if (field_desc) { + field_desc->parse(garmin, data); + } else { +#if 1 + fprintf(stderr, "%s/%d:", msg_name, field_nr); + if (base_type == 7) + fprintf(stderr, " %s\n", data); + else { + for (int i = 0; i < len; i += base_size) { + unsigned long long value; + const char *fmt; + const unsigned char *ptr = data + i; + switch (base_size) { + default: value = *ptr; fmt = " %02llx"; break; + case 2: value = *(unsigned short *)ptr; fmt = " %04llx"; break; + case 4: value = *(unsigned int *)ptr; fmt = " %08llx"; break; + case 8: value = *(unsigned long long *)ptr; fmt = " %016llx"; break; + } + fprintf(stderr, fmt, value); + } + fprintf(stderr, "\n"); + } +#else + DEBUG(garmin->base.context, "Unknown field %s:%02x %02x %d/%d\n", msg_name, field_nr, field[2], len, base_size); + HEXDUMP(garmin->base.context, DC_LOGLEVEL_DEBUG, "next", data, len); +#endif + } + + data += len; + total_len += len; + size -= len; + } + + return total_len; +} + +/* + * A definition record: + * + * 5 bytes of fixed header: + * - 1x reserved byte + * - 1x architecture byte (0 = LE) + * - 2x msg number bytes + * - 1x field number byte + * + * Followed by the specified number of field definitions: + * + * 3 bytes for each field definition: + * - 1x "field definition number" (look up in the FIT profile) + * - 1x field size in bytes (so you can know the size even if you don't know the definition) + * - 1x base type bit field + * + * Followed *optionally* by developer definitions (if record header & 0x20): + * + * - 1x number of developer definitions + * - 3 bytes each + */ +static int traverse_definition(struct garmin_parser_t *garmin, + const unsigned char *data, unsigned int size, + unsigned char record) +{ + unsigned short msg; + unsigned char type = record & 0xf; + struct type_desc *desc = garmin->type_desc + type; + int fields, devfields, len; + + msg = array_uint16_le(data+2); + desc->msg_desc = lookup_msg_desc(msg, type, &desc->msg_name); + fields = data[4]; + + DEBUG(garmin->base.context, "Define local type %d: %02x %02x %04x %02x %s", + type, data[0], data[1], msg, fields, desc->msg_name); + + if (data[1]) { + ERROR(garmin->base.context, "Only handling little-endian definitions\n"); + return -1; + } + + if (fields > MAXFIELDS) { + ERROR(garmin->base.context, "Too many fields in description: %d (max %d)\n", fields, MAXFIELDS); + return -1; + } + desc->nrfields = fields; + len = 5 + fields*3; + devfields = 0; + if (record & 0x20) { + devfields = data[len]; + len += 1 + devfields*3; + ERROR(garmin->base.context, "NO support for developer fields yet\n"); + return -1; + } + + for (int i = 0; i < fields; i++) { + unsigned char *field = desc->fields[i]; + memcpy(field, data + (5+i*3), 3); + DEBUG(garmin->base.context, " %d: %02x %02x %02x", i, field[0], field[1], field[2]); + } + + return len; +} + + +static int traverse_data(struct garmin_parser_t *garmin) +{ + const unsigned char *data = garmin->base.data; + int len = garmin->base.size; + unsigned int hdrsize, protocol, profile, datasize; + unsigned int time; + + // The data starts with our filename fingerprint. Skip it. + data += FIT_NAME_SIZE; + len -= FIT_NAME_SIZE; + + // The FIT header + if (len < 12) + return -1; + + hdrsize = data[0]; + protocol = data[1]; + profile = array_uint16_le(data+2); + datasize = array_uint16_le(data+4); + if (memcmp(data+8, ".FIT", 4)) + return -1; + if (hdrsize < 12 || datasize > len || datasize + hdrsize > len) + return -1; + + garmin->cache.protocol = protocol; + garmin->cache.profile = profile; + + data += hdrsize; + time = 0; + + while (datasize > 0) { + unsigned char record = data[0]; + int len; + + data++; + datasize--; + + if (record & 0x80) { // Compressed record? + unsigned int newtime; + unsigned char type; + + type = (record >> 5) & 3; + newtime = (record & 0x1f) | (time & ~0x1f); + if (newtime < time) + newtime += 0x20; + time = newtime; + + len = traverse_compressed(garmin, data, datasize, type, time); + } else if (record & 0x40) { // Definition record? + len = traverse_definition(garmin, data, datasize, record); + } else { // Normal data record + len = traverse_regular(garmin, data, datasize, record, &time); + } + if (len <= 0 || len > datasize) + return -1; + data += len; + datasize -= len; + } + return 0; +} + +static void initialize_field_caches(garmin_parser_t *garmin) +{ + memset(&garmin->cache, 0, sizeof(garmin->cache)); + traverse_data(garmin); +} static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size) { + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + + memset(garmin->type_desc, 0, sizeof(garmin->type_desc)); + initialize_field_caches(garmin); return DC_STATUS_SUCCESS; } @@ -78,6 +531,22 @@ static dc_status_t garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) { const unsigned char *data = abstract->data; + unsigned int yyyy, mm, dd, h, m, s; + + if (abstract->size < FIT_NAME_SIZE) + return DC_STATUS_UNSUPPORTED; + + if (sscanf(data, "%04u-%02u-%02u-%02u-%02u-%02u", + &yyyy, &mm, &dd, &h, &m, &s) != 6) + return DC_STATUS_UNSUPPORTED; + + datetime->year = yyyy; + datetime->month = mm; + datetime->day = dd; + datetime->hour = h; + datetime->minute = m; + datetime->second = s; + datetime->timezone = DC_TIMEZONE_NONE; return DC_STATUS_SUCCESS; } From b65b2318f1283f8faf842a75a57ca11339cd4fba Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Tue, 28 Aug 2018 15:00:54 -0700 Subject: [PATCH 05/16] garmin: some fields are defined in all message types It turns out that the timestamp field can exist across all message types, as can a few other special fields. Split those out as "ANY" type fields, so that we get the field descriptor without having to fill in every message descriptor. This also makes the message descriptors smaller, since we no longer need to worry about the high-numbered (253) timestamp field in the arrays. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index e11e4d9..22def7c 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -139,6 +139,11 @@ struct field_desc { static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ static int parse_##name(struct garmin_parser_t *garmin, type data) +// All msg formats can have a timestamp +DECLARE_FIELD(ANY, timestamp, UINT32) { return 0; } +DECLARE_FIELD(ANY, message_index, UINT16) { return 0; } +DECLARE_FIELD(ANY, part_index, UINT32) { return 0; } + // FILE msg DECLARE_FIELD(FILE, file_type, ENUM) { return 0; } DECLARE_FIELD(FILE, manufacturer, UINT16) { return 0; } @@ -148,11 +153,9 @@ DECLARE_FIELD(FILE, creation_time, UINT32) { return 0; } // SESSION msg DECLARE_FIELD(SESSION, start_time, UINT32) { return 0; } -DECLARE_FIELD(SESSION, timestamp, UINT32) { return 0; } // RECORD msg DECLARE_FIELD(RECORD, start_time, UINT32) { return 0; } -DECLARE_FIELD(RECORD, timestamp, UINT32) { return 0; } struct msg_desc { @@ -183,20 +186,18 @@ DECLARE_MESG(ZONES_TARGET) = { }; DECLARE_MESG(SPORT) = { }; DECLARE_MESG(SESSION) = { - .maxfield = 254, + .maxfield = 3, .field = { SET_FIELD(SESSION, 2, start_time, UINT32), - SET_FIELD(SESSION, 253, timestamp, UINT32), } }; DECLARE_MESG(LAP) = { }; DECLARE_MESG(RECORD) = { - .maxfield = 254, + .maxfield = 3, .field = { SET_FIELD(RECORD, 2, start_time, UINT32), - SET_FIELD(RECORD, 253, timestamp, UINT32), } }; @@ -343,9 +344,23 @@ static int traverse_regular(struct garmin_parser_t *garmin, } - field_desc = NULL; - if (field_nr < msg_desc->maxfield) - field_desc = msg_desc->field[field_nr]; + + // Certain field numbers have fixed meaning across all messages + switch (field_nr) { + case 250: + field_desc = &ANY_part_index_field_UINT32; + break; + case 253: + field_desc = &ANY_timestamp_field_UINT32; + break; + case 254: + field_desc = &ANY_message_index_field_UINT16; + break; + default: + field_desc = NULL; + if (field_nr < msg_desc->maxfield) + field_desc = msg_desc->field[field_nr]; + } if (field_desc) { field_desc->parse(garmin, data); From bc2ba57302ad2ffcbc36b511180c3bb11236c0e5 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Tue, 28 Aug 2018 16:18:06 -0700 Subject: [PATCH 06/16] garmin: teach the parser about invalid values and more dates The invalid values skip the parser callback function entirely. Of course, since it's not really doing anything right now, that's mostly costmetic. Extend the FIT type declarations to also have the invalid values. Also, add a few timestamp entries, and print them out to show the timestamps in a human-legible format. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 58 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 22def7c..22421af 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -109,9 +109,24 @@ garmin_parser_create (dc_parser_t **out, dc_context_t *context) return DC_STATUS_SUCCESS; } -typedef unsigned char ENUM; -typedef unsigned short UINT16; -typedef unsigned int UINT32; +#define DECLARE_FIT_TYPE(name, ctype, inval) \ + typedef ctype name; \ + static const name name##_INVAL = inval + +DECLARE_FIT_TYPE(ENUM, unsigned char, 0xff); +DECLARE_FIT_TYPE(UINT8, unsigned char, 0xff); +DECLARE_FIT_TYPE(UINT16, unsigned short, 0xffff); +DECLARE_FIT_TYPE(UINT32, unsigned int, 0xffffffff); +DECLARE_FIT_TYPE(UINT64, unsigned long long, 0xffffffffffffffffull); + +DECLARE_FIT_TYPE(UINT8Z, unsigned char, 0); +DECLARE_FIT_TYPE(UINT16Z, unsigned short, 0); +DECLARE_FIT_TYPE(UINT32Z, unsigned int, 0); + +DECLARE_FIT_TYPE(SINT8, signed char, 0x7f); +DECLARE_FIT_TYPE(SINT16, signed short, 0x7fff); +DECLARE_FIT_TYPE(SINT32, signed int, 0x7fffffff); +DECLARE_FIT_TYPE(SINT64, signed long long, 0x7fffffffffffffffll); /* * Garmin FIT events are described by tuples of "global mesg ID" and @@ -133,6 +148,7 @@ struct field_desc { static int parse_##name##_##type(struct garmin_parser_t *g, const unsigned char *p) \ { \ type val = *(type *)p; \ + if (val == type##_INVAL) return 0; \ fprintf(stderr, "%s: %llx\n", #name, (long long)val); \ return parse_##name(g, *(type *)p); \ } \ @@ -140,7 +156,19 @@ struct field_desc { static int parse_##name(struct garmin_parser_t *garmin, type data) // All msg formats can have a timestamp -DECLARE_FIELD(ANY, timestamp, UINT32) { return 0; } +// Garmin timestamps are in seconds since 00:00 Dec 31 1989 UTC +// Convert to "standard epoch time" by adding 631065600. +DECLARE_FIELD(ANY, timestamp, UINT32) +{ + dc_ticks_t time = 631065600 + (dc_ticks_t) data; + dc_datetime_t date; + + dc_datetime_gmtime(&date, time); + fprintf(stderr, "%04d-%02d-%02d %2d:%02d:%02d\n", + date.year, date.month, date.day, + date.hour, date.minute, date.second); + return 0; +} DECLARE_FIELD(ANY, message_index, UINT16) { return 0; } DECLARE_FIELD(ANY, part_index, UINT32) { return 0; } @@ -149,13 +177,18 @@ DECLARE_FIELD(FILE, file_type, ENUM) { return 0; } DECLARE_FIELD(FILE, manufacturer, UINT16) { return 0; } DECLARE_FIELD(FILE, product, UINT16) { return 0; } DECLARE_FIELD(FILE, serial, UINT32) { return 0; } -DECLARE_FIELD(FILE, creation_time, UINT32) { return 0; } +DECLARE_FIELD(FILE, creation_time, UINT32) { return parse_ANY_timestamp(garmin, data); } +DECLARE_FIELD(FILE, number, UINT16) { return 0; } +DECLARE_FIELD(FILE, other_time, UINT32) { return parse_ANY_timestamp(garmin, data); } // SESSION msg -DECLARE_FIELD(SESSION, start_time, UINT32) { return 0; } +DECLARE_FIELD(SESSION, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } + +// LAP msg +DECLARE_FIELD(LAP, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } // RECORD msg -DECLARE_FIELD(RECORD, start_time, UINT32) { return 0; } +DECLARE_FIELD(RECORD, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } struct msg_desc { @@ -170,13 +203,15 @@ struct msg_desc { static const struct msg_desc name##_msg_desc DECLARE_MESG(FILE) = { - .maxfield = 5, + .maxfield = 8, .field = { SET_FIELD(FILE, 0, file_type, ENUM), SET_FIELD(FILE, 1, manufacturer, UINT16), SET_FIELD(FILE, 2, product, UINT16), SET_FIELD(FILE, 3, serial, UINT32), SET_FIELD(FILE, 4, creation_time, UINT32), + SET_FIELD(FILE, 5, number, UINT16), + SET_FIELD(FILE, 7, other_time, UINT32), } }; @@ -192,7 +227,12 @@ DECLARE_MESG(SESSION) = { } }; -DECLARE_MESG(LAP) = { }; +DECLARE_MESG(LAP) = { + .maxfield = 3, + .field = { + SET_FIELD(LAP, 2, start_time, UINT32), + } +}; DECLARE_MESG(RECORD) = { .maxfield = 3, From 6d53e31cba4ea5494aadb8c6c3f7bc56e17318cd Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Tue, 28 Aug 2018 17:26:48 -0700 Subject: [PATCH 07/16] garmin: fix file length header parsing Oops. I used array_uint16_le() to get the data size. Too much copy-and-paste from the profile version (which is indeed 16 bits). The data size is a 32-bit entity, and this would truncate the data we read. Also, verify that there is space for the final CRC in the file, even if we don't actually check it. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 22421af..c6cacf7 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -363,6 +363,7 @@ static int traverse_regular(struct garmin_parser_t *garmin, ERROR(garmin->base.context, "Unknown base type %d\n", base_type); data += size; len -= size; + total_len += size; continue; } base_size = base_size_array[base_type]; @@ -522,10 +523,10 @@ static int traverse_data(struct garmin_parser_t *garmin) hdrsize = data[0]; protocol = data[1]; profile = array_uint16_le(data+2); - datasize = array_uint16_le(data+4); + datasize = array_uint32_le(data+4); if (memcmp(data+8, ".FIT", 4)) return -1; - if (hdrsize < 12 || datasize > len || datasize + hdrsize > len) + if (hdrsize < 12 || datasize > len || datasize + hdrsize + 2 > len) return -1; garmin->cache.protocol = protocol; From 3dbe5353f5f03263956a6182aed19cd15a24cafe Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Tue, 28 Aug 2018 17:29:38 -0700 Subject: [PATCH 08/16] garmin: teach the parser to show undefined values for unknown fields too Make it easier to see which of the unknown fields don't contain anything interesting. Soon this will be at the stage where the parser skeleton itself doesn't need much work, and I should look at the actual data and turn it into samples instead. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index c6cacf7..ec6bac5 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -407,21 +407,31 @@ static int traverse_regular(struct garmin_parser_t *garmin, field_desc->parse(garmin, data); } else { #if 1 - fprintf(stderr, "%s/%d:", msg_name, field_nr); - if (base_type == 7) - fprintf(stderr, " %s\n", data); - else { + static const unsigned long long inval_value_array[] = { + 0xff, 0x7f, 0xff, 0x7fff, 0xffff, 0x7fffffff, 0xffffffff, 0, + 0xffffffff, 0xfffffffffffffffful, 0x00, 0x0000, 0x00000000, 0xff, + 0x7fffffffffffffff, 0xffffffffffffffff, 0x0000000000000000 }; + const unsigned long long inval = inval_value_array[base_type]; + + switch (base_type) { + case 7: + if (!*data) + break; + fprintf(stderr, "%s/%d: %s\n", msg_name, field_nr, data); + break; + default: + fprintf(stderr, "%s/%d:", msg_name, field_nr); for (int i = 0; i < len; i += base_size) { - unsigned long long value; + unsigned long long val; const char *fmt; const unsigned char *ptr = data + i; switch (base_size) { - default: value = *ptr; fmt = " %02llx"; break; - case 2: value = *(unsigned short *)ptr; fmt = " %04llx"; break; - case 4: value = *(unsigned int *)ptr; fmt = " %08llx"; break; - case 8: value = *(unsigned long long *)ptr; fmt = " %016llx"; break; + default: val = *ptr; fmt = val == inval ? " --" : " %02llx"; break; + case 2: val = *(unsigned short *)ptr; fmt = val == inval ? " ----" : " %04llx"; break; + case 4: val = *(unsigned int *)ptr; fmt = val == inval ? " --------" : " %08llx"; break; + case 8: val = *(unsigned long long *)ptr; fmt = val == inval ? " ----------------" : " %016llx"; break; } - fprintf(stderr, fmt, value); + fprintf(stderr, fmt, val); } fprintf(stderr, "\n"); } From 6d470d8430245f0df2d1e41875183dfd6b25ce85 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Wed, 29 Aug 2018 12:22:42 -0700 Subject: [PATCH 09/16] garmin: add a lot of new field definitions This actually seems to cover almost all of the relevant dive fields. There's a lot of different GPS coordinates, I have no idea what they all are, but they are at least marked in the definitions. NOTE! None of this actually fills in any information yet. It's all about just parsing things and getting the types etc right. On that note, this also adds a bit of dynamic type checking, which caught a mistake or two. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 181 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 22 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index ec6bac5..044645e 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -54,6 +54,7 @@ typedef struct garmin_parser_t { unsigned int initialized; unsigned int protocol; unsigned int profile; + unsigned int utc_offset, time_offset; unsigned int divetime; double maxdepth; double avgdepth; @@ -128,6 +129,30 @@ DECLARE_FIT_TYPE(SINT16, signed short, 0x7fff); DECLARE_FIT_TYPE(SINT32, signed int, 0x7fffffff); DECLARE_FIT_TYPE(SINT64, signed long long, 0x7fffffffffffffffll); +static const struct { + const char *type_name; + int type_size; + unsigned long long type_inval; +} base_type_info[17] = { + { "ENUM", 1, 0xff }, + { "SINT8", 1, 0x7f }, + { "UINT8", 1, 0xff }, + { "SINT16", 2, 0x7fff }, + { "UINT16", 2, 0xffff }, + { "SINT32", 4, 0x7fffffff }, + { "UINT32", 4, 0xffffffff }, + { "STRING", 1, 0 }, + { "FLOAT", 4, 0xffffffff }, + { "DOUBLE", 8, 0xfffffffffffffffful }, + { "UINT8Z", 1, 0x00 }, + { "UINT16Z", 2, 0x0000 }, + { "UINT32Z", 4, 0x00000000 }, + { "BYTE", 1, 0xff }, + { "SINT64", 8, 0x7fffffffffffffff }, + { "UINT64", 8, 0xffffffffffffffff }, + { "UINT64Z", 8, 0x0000000000000000 }, +}; + /* * Garmin FIT events are described by tuples of "global mesg ID" and * a "field number". There's lots of them, because you have events @@ -139,17 +164,19 @@ DECLARE_FIT_TYPE(SINT64, signed long long, 0x7fffffffffffffffll); */ struct field_desc { const char *name; - int (*parse)(struct garmin_parser_t *, const unsigned char *data); + int (*parse)(struct garmin_parser_t *, unsigned char base_type, const unsigned char *data); }; #define DECLARE_FIELD(msg, name, type) __DECLARE_FIELD(msg##_##name, type) #define __DECLARE_FIELD(name, type) \ static int parse_##name(struct garmin_parser_t *, const type); \ - static int parse_##name##_##type(struct garmin_parser_t *g, const unsigned char *p) \ + static int parse_##name##_##type(struct garmin_parser_t *g, unsigned char base_type, const unsigned char *p) \ { \ + if (strcmp(#type, base_type_info[base_type].type_name)) \ + fprintf(stderr, "%s: %s should be %s\n", #name, #type, base_type_info[base_type].type_name); \ type val = *(type *)p; \ if (val == type##_INVAL) return 0; \ - fprintf(stderr, "%s: %llx\n", #name, (long long)val); \ + DEBUG(g->base.context, "%s (%s): %lld\n", #name, #type, (long long)val); \ return parse_##name(g, *(type *)p); \ } \ static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ @@ -163,8 +190,10 @@ DECLARE_FIELD(ANY, timestamp, UINT32) dc_ticks_t time = 631065600 + (dc_ticks_t) data; dc_datetime_t date; - dc_datetime_gmtime(&date, time); - fprintf(stderr, "%04d-%02d-%02d %2d:%02d:%02d\n", + // Show local time (time_offset) + dc_datetime_gmtime(&date, time + garmin->cache.time_offset); + DEBUG(garmin->base.context, + "%04d-%02d-%02d %02d:%02d:%02d", date.year, date.month, date.day, date.hour, date.minute, date.second); return 0; @@ -176,19 +205,69 @@ DECLARE_FIELD(ANY, part_index, UINT32) { return 0; } DECLARE_FIELD(FILE, file_type, ENUM) { return 0; } DECLARE_FIELD(FILE, manufacturer, UINT16) { return 0; } DECLARE_FIELD(FILE, product, UINT16) { return 0; } -DECLARE_FIELD(FILE, serial, UINT32) { return 0; } +DECLARE_FIELD(FILE, serial, UINT32Z) { return 0; } DECLARE_FIELD(FILE, creation_time, UINT32) { return parse_ANY_timestamp(garmin, data); } DECLARE_FIELD(FILE, number, UINT16) { return 0; } DECLARE_FIELD(FILE, other_time, UINT32) { return parse_ANY_timestamp(garmin, data); } // SESSION msg DECLARE_FIELD(SESSION, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } +DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, start_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { return 0; } // 180 deg / 2**31 // LAP msg DECLARE_FIELD(LAP, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } +DECLARE_FIELD(LAP, start_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, start_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, end_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, end_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, some_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, some_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, other_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, other_pos_long, SINT32) { return 0; } // 180 deg / 2**31 // RECORD msg -DECLARE_FIELD(RECORD, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } +DECLARE_FIELD(RECORD, position_lat, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(RECORD, position_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(RECORD, altitude, UINT16) { return 0; } // 5 *m + 500 ? +DECLARE_FIELD(RECORD, heart_rate, UINT8) { return 0; } // bpm +DECLARE_FIELD(RECORD, distance, UINT32) { return 0; } // Distance in 100 * m? WTF? +DECLARE_FIELD(RECORD, temperature, SINT8) { return 0; } // degrees C +DECLARE_FIELD(RECORD, abs_pressure, UINT32) {return 0; } // Pascal +DECLARE_FIELD(RECORD, depth, UINT32) { return 0; } // mm +DECLARE_FIELD(RECORD, next_stop_depth, UINT32) { return 0; } // mm +DECLARE_FIELD(RECORD, next_stop_time, UINT32) { return 0; } // seconds +DECLARE_FIELD(RECORD, tts, UINT32) { return 0; } // seconds +DECLARE_FIELD(RECORD, ndl, UINT32) { return 0; } // s +DECLARE_FIELD(RECORD, cns_load, UINT8) { return 0; } // percent +DECLARE_FIELD(RECORD, n2_load, UINT16) { return 0; } // percent + +// DEVICE_SETTINGS +DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->cache.utc_offset = data; return 0; } +DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->cache.time_offset = data; return 0; } + +// DIVE_GAS - uses msg index +DECLARE_FIELD(DIVE_GAS, helium, UINT8) { return 0; } // percent +DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) { return 0; } // percent +DECLARE_FIELD(DIVE_GAS, status, ENUM) { return 0; } // 0 - disabled, 1 - enabled, 2 - backup + +// DIVE_SUMMARY +DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { return 0; } // mm +DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { return 0; } // mm +DECLARE_FIELD(DIVE_SUMMARY, surface_interval, UINT32) { return 0; } // sec +DECLARE_FIELD(DIVE_SUMMARY, start_cns, UINT8) { return 0; } // percent +DECLARE_FIELD(DIVE_SUMMARY, end_cns, UINT8) { return 0; } // percent +DECLARE_FIELD(DIVE_SUMMARY, start_n2, UINT16) { return 0; } // percent +DECLARE_FIELD(DIVE_SUMMARY, end_n2, UINT16) { return 0; } // percent +DECLARE_FIELD(DIVE_SUMMARY, o2_toxicity, UINT16) { return 0; } // OTUs +DECLARE_FIELD(DIVE_SUMMARY, dive_number, UINT32) { return 0; } +DECLARE_FIELD(DIVE_SUMMARY, bottom_time, UINT32) { return 0; } // ms struct msg_desc { @@ -208,47 +287,107 @@ DECLARE_MESG(FILE) = { SET_FIELD(FILE, 0, file_type, ENUM), SET_FIELD(FILE, 1, manufacturer, UINT16), SET_FIELD(FILE, 2, product, UINT16), - SET_FIELD(FILE, 3, serial, UINT32), + SET_FIELD(FILE, 3, serial, UINT32Z), SET_FIELD(FILE, 4, creation_time, UINT32), SET_FIELD(FILE, 5, number, UINT16), SET_FIELD(FILE, 7, other_time, UINT32), } }; -DECLARE_MESG(DEVICE_SETTINGS) = { }; +DECLARE_MESG(DEVICE_SETTINGS) = { + .maxfield = 3, + .field = { + SET_FIELD(DEVICE_SETTINGS, 1, utc_offset, UINT32), // Convert to UTC + SET_FIELD(DEVICE_SETTINGS, 2, time_offset, UINT32), // Convert to local + } +}; DECLARE_MESG(USER_PROFILE) = { }; DECLARE_MESG(ZONES_TARGET) = { }; DECLARE_MESG(SPORT) = { }; DECLARE_MESG(SESSION) = { - .maxfield = 3, + .maxfield = 40, .field = { SET_FIELD(SESSION, 2, start_time, UINT32), + SET_FIELD(SESSION, 3, start_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 4, start_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 29, nec_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 30, nec_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 31, swc_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 32, swc_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 38, exit_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(SESSION, 39, exit_pos_long, SINT32), // 180 deg / 2**31 } }; DECLARE_MESG(LAP) = { - .maxfield = 3, + .maxfield = 31, .field = { SET_FIELD(LAP, 2, start_time, UINT32), + SET_FIELD(LAP, 3, start_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 4, start_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 5, end_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 6, end_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 27, some_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 28, some_pos_long, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 29, other_pos_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(LAP, 30, other_pos_long, SINT32), // 180 deg / 2**31 } }; DECLARE_MESG(RECORD) = { - .maxfield = 3, + .maxfield = 99, .field = { - SET_FIELD(RECORD, 2, start_time, UINT32), + SET_FIELD(RECORD, 0, position_lat, SINT32), // 180 deg / 2**31 + SET_FIELD(RECORD, 1, position_long, SINT32), // 180 deg / 2**31 + SET_FIELD(RECORD, 2, altitude, UINT16), // 5 *m + 500 ? + SET_FIELD(RECORD, 3, heart_rate, UINT8), // bpm + SET_FIELD(RECORD, 5, distance, UINT32), // Distance in 100 * m? WTF? + SET_FIELD(RECORD, 13, temperature, SINT8), // degrees C + SET_FIELD(RECORD, 91, abs_pressure, UINT32), // Pascal + SET_FIELD(RECORD, 92, depth, UINT32), // mm + SET_FIELD(RECORD, 93, next_stop_depth, UINT32), // mm + SET_FIELD(RECORD, 94, next_stop_time, UINT32), // seconds + SET_FIELD(RECORD, 95, tts, UINT32), // seconds + SET_FIELD(RECORD, 96, ndl, UINT32), // s + SET_FIELD(RECORD, 97, cns_load, UINT8), // percent + SET_FIELD(RECORD, 98, n2_load, UINT16), // percent } }; +DECLARE_MESG(DIVE_GAS) = { + .maxfield = 3, + .field = { + // This uses a "message index" field to set the gas index + SET_FIELD(DIVE_GAS, 0, helium, UINT8), + SET_FIELD(DIVE_GAS, 1, oxygen, UINT8), + SET_FIELD(DIVE_GAS, 2, status, ENUM), + } +}; + +DECLARE_MESG(DIVE_SUMMARY) = { + .maxfield = 12, + .field = { + SET_FIELD(DIVE_SUMMARY, 2, avg_depth, UINT32), // mm + SET_FIELD(DIVE_SUMMARY, 3, max_depth, UINT32), // mm + SET_FIELD(DIVE_SUMMARY, 4, surface_interval, UINT32), // sec + SET_FIELD(DIVE_SUMMARY, 5, start_cns, UINT8), // percent + SET_FIELD(DIVE_SUMMARY, 6, end_cns, UINT8), // percent + SET_FIELD(DIVE_SUMMARY, 7, start_n2, UINT16), // percent + SET_FIELD(DIVE_SUMMARY, 8, end_n2, UINT16), // percent + SET_FIELD(DIVE_SUMMARY, 9, o2_toxicity, UINT16), // OTUs + SET_FIELD(DIVE_SUMMARY, 10, dive_number, UINT32), + SET_FIELD(DIVE_SUMMARY, 11, bottom_time, UINT32), // ms + } +}; + + DECLARE_MESG(EVENT) = { }; DECLARE_MESG(DEVICE_INFO) = { }; DECLARE_MESG(ACTIVITY) = { }; DECLARE_MESG(FILE_CREATOR) = { }; DECLARE_MESG(DIVE_SETTINGS) = { }; -DECLARE_MESG(DIVE_GAS) = { }; DECLARE_MESG(DIVE_ALARM) = { }; -DECLARE_MESG(DIVE_SUMMARY) = { }; // Unknown global message ID's.. DECLARE_MESG(WTF_13) = { }; @@ -258,6 +397,7 @@ DECLARE_MESG(WTF_104) = { }; DECLARE_MESG(WTF_125) = { }; DECLARE_MESG(WTF_140) = { }; DECLARE_MESG(WTF_141) = { }; +DECLARE_MESG(WTF_216) = { }; DECLARE_MESG(WTF_233) = { }; #define SET_MESG(nr, name) [nr] = { #name, &name##_msg_desc } @@ -287,6 +427,7 @@ static const struct { SET_MESG(140, WTF_140), SET_MESG(141, WTF_141), + SET_MESG(216, WTF_216), SET_MESG(233, WTF_233), SET_MESG(258, DIVE_SETTINGS), SET_MESG(259, DIVE_GAS), @@ -404,14 +545,10 @@ static int traverse_regular(struct garmin_parser_t *garmin, } if (field_desc) { - field_desc->parse(garmin, data); + field_desc->parse(garmin, base_type, data); } else { #if 1 - static const unsigned long long inval_value_array[] = { - 0xff, 0x7f, 0xff, 0x7fff, 0xffff, 0x7fffffff, 0xffffffff, 0, - 0xffffffff, 0xfffffffffffffffful, 0x00, 0x0000, 0x00000000, 0xff, - 0x7fffffffffffffff, 0xffffffffffffffff, 0x0000000000000000 }; - const unsigned long long inval = inval_value_array[base_type]; + const unsigned long long inval = base_type_info[base_type].type_inval; switch (base_type) { case 7: @@ -433,7 +570,7 @@ static int traverse_regular(struct garmin_parser_t *garmin, } fprintf(stderr, fmt, val); } - fprintf(stderr, "\n"); + fprintf(stderr, " %s\n", base_type_info[base_type].type_name); } #else DEBUG(garmin->base.context, "Unknown field %s:%02x %02x %d/%d\n", msg_name, field_nr, field[2], len, base_size); From 00a90e2822ee44bb0965c9ae3d1c0a8ab77ec158 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Wed, 29 Aug 2018 12:49:56 -0700 Subject: [PATCH 10/16] garmin: turn all the remaining unrecognized fields into DEBUG messages There aren't that many relevant ones left, and I have reached the point where I think the remaining missing fields just aren't that important any more. You can always get them by saving the libdivecomputer log-file and see the debug messages that way. Now I'll need to turn the parsing skeleton into actually generating the actual libdivecomputer data. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 044645e..f9e123a 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -176,7 +176,7 @@ struct field_desc { fprintf(stderr, "%s: %s should be %s\n", #name, #type, base_type_info[base_type].type_name); \ type val = *(type *)p; \ if (val == type##_INVAL) return 0; \ - DEBUG(g->base.context, "%s (%s): %lld\n", #name, #type, (long long)val); \ + DEBUG(g->base.context, "%s (%s): %lld", #name, #type, (long long)val); \ return parse_##name(g, *(type *)p); \ } \ static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ @@ -547,35 +547,8 @@ static int traverse_regular(struct garmin_parser_t *garmin, if (field_desc) { field_desc->parse(garmin, base_type, data); } else { -#if 1 - const unsigned long long inval = base_type_info[base_type].type_inval; - - switch (base_type) { - case 7: - if (!*data) - break; - fprintf(stderr, "%s/%d: %s\n", msg_name, field_nr, data); - break; - default: - fprintf(stderr, "%s/%d:", msg_name, field_nr); - for (int i = 0; i < len; i += base_size) { - unsigned long long val; - const char *fmt; - const unsigned char *ptr = data + i; - switch (base_size) { - default: val = *ptr; fmt = val == inval ? " --" : " %02llx"; break; - case 2: val = *(unsigned short *)ptr; fmt = val == inval ? " ----" : " %04llx"; break; - case 4: val = *(unsigned int *)ptr; fmt = val == inval ? " --------" : " %08llx"; break; - case 8: val = *(unsigned long long *)ptr; fmt = val == inval ? " ----------------" : " %016llx"; break; - } - fprintf(stderr, fmt, val); - } - fprintf(stderr, " %s\n", base_type_info[base_type].type_name); - } -#else - DEBUG(garmin->base.context, "Unknown field %s:%02x %02x %d/%d\n", msg_name, field_nr, field[2], len, base_size); - HEXDUMP(garmin->base.context, DC_LOGLEVEL_DEBUG, "next", data, len); -#endif + DEBUG(garmin->base.context, "%s/%d %s", msg_name, field_nr, base_type_info[base_type].type_name); + HEXDUMP(garmin->base.context, DC_LOGLEVEL_DEBUG, "next", data, len); } data += len; From c8e52081cdeb51ec21026ece7c68395fcdcd49b2 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Wed, 29 Aug 2018 15:55:48 -0700 Subject: [PATCH 11/16] garmin: actually start using the parsed data This gets me real profiles, with depth and temperature information. Sadly, the temperature data seems to be in whole degrees C, which is not good for diving. But certainly not unheard of. Also, while this does actually transfer a lot of other information too, there are certainly things missing. No gas information is gathered (although we do parse it, we just don't save it), and none of the events are parsed at all. And the GPS information that we have isn't passed on yet, because there are no libdivecomputer interfaces to do that. I'll have to come up with something. But it's actually almost useful. All the basics seem to be there. How *buggy* it is, I do not know, but the profiles don't look obviously broken. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 281 +++++++++++++++++++++++++++----------------- 1 file changed, 172 insertions(+), 109 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index f9e123a..07c8c82 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -48,13 +48,26 @@ struct type_desc { typedef struct garmin_parser_t { dc_parser_t base; + + dc_sample_callback_t callback; + void *userdata; + + // Some sample data needs to be bunched up + // and sent together. + struct { + unsigned int time; + int stop_time; + double ceiling; + } sample_data; struct type_desc type_desc[MAXTYPE]; + // Field cache struct { unsigned int initialized; unsigned int protocol; unsigned int profile; - unsigned int utc_offset, time_offset; + unsigned int time; + int utc_offset, time_offset; unsigned int divetime; double maxdepth; double avgdepth; @@ -75,6 +88,23 @@ typedef struct garmin_parser_t { typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user); +static void flush_pending_sample(struct garmin_parser_t *garmin) +{ + if (!garmin->callback) + return; + + if (garmin->sample_data.stop_time && garmin->sample_data.ceiling) { + dc_sample_value_t sample = {0}; + sample.deco.type = DC_DECO_DECOSTOP; + sample.deco.time = garmin->sample_data.stop_time; + sample.deco.depth = garmin->sample_data.ceiling; + garmin->callback(DC_SAMPLE_DECO, sample, garmin->userdata); + } + garmin->sample_data.stop_time = 0; + garmin->sample_data.ceiling = 0; +} + + static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size); static dc_status_t garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime); static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value); @@ -164,110 +194,148 @@ static const struct { */ struct field_desc { const char *name; - int (*parse)(struct garmin_parser_t *, unsigned char base_type, const unsigned char *data); + void (*parse)(struct garmin_parser_t *, unsigned char base_type, const unsigned char *data); }; #define DECLARE_FIELD(msg, name, type) __DECLARE_FIELD(msg##_##name, type) #define __DECLARE_FIELD(name, type) \ - static int parse_##name(struct garmin_parser_t *, const type); \ - static int parse_##name##_##type(struct garmin_parser_t *g, unsigned char base_type, const unsigned char *p) \ + static void parse_##name(struct garmin_parser_t *, const type); \ + static void parse_##name##_##type(struct garmin_parser_t *g, unsigned char base_type, const unsigned char *p) \ { \ if (strcmp(#type, base_type_info[base_type].type_name)) \ fprintf(stderr, "%s: %s should be %s\n", #name, #type, base_type_info[base_type].type_name); \ type val = *(type *)p; \ - if (val == type##_INVAL) return 0; \ + if (val == type##_INVAL) return; \ DEBUG(g->base.context, "%s (%s): %lld", #name, #type, (long long)val); \ - return parse_##name(g, *(type *)p); \ + parse_##name(g, *(type *)p); \ } \ static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ - static int parse_##name(struct garmin_parser_t *garmin, type data) + static void parse_##name(struct garmin_parser_t *garmin, type data) // All msg formats can have a timestamp // Garmin timestamps are in seconds since 00:00 Dec 31 1989 UTC // Convert to "standard epoch time" by adding 631065600. DECLARE_FIELD(ANY, timestamp, UINT32) { - dc_ticks_t time = 631065600 + (dc_ticks_t) data; - dc_datetime_t date; + if (garmin->callback) { + dc_sample_value_t sample = {0}; - // Show local time (time_offset) - dc_datetime_gmtime(&date, time + garmin->cache.time_offset); - DEBUG(garmin->base.context, - "%04d-%02d-%02d %02d:%02d:%02d", - date.year, date.month, date.day, - date.hour, date.minute, date.second); - return 0; + // Turn the timestamp relative to the beginning of the dive + if (data < garmin->cache.time) + return; + data -= garmin->cache.time; + + // Did we already do this? + if (data <= garmin->sample_data.time) + return; + + // Flush any pending sample data before sending the next time event + flush_pending_sample(garmin); + + // *Now* we're ready to actually update the sample times + garmin->sample_data.time = data; + sample.time = data; + garmin->callback(DC_SAMPLE_TIME, sample, garmin->userdata); + } } -DECLARE_FIELD(ANY, message_index, UINT16) { return 0; } -DECLARE_FIELD(ANY, part_index, UINT32) { return 0; } +DECLARE_FIELD(ANY, message_index, UINT16) { } +DECLARE_FIELD(ANY, part_index, UINT32) { } // FILE msg -DECLARE_FIELD(FILE, file_type, ENUM) { return 0; } -DECLARE_FIELD(FILE, manufacturer, UINT16) { return 0; } -DECLARE_FIELD(FILE, product, UINT16) { return 0; } -DECLARE_FIELD(FILE, serial, UINT32Z) { return 0; } -DECLARE_FIELD(FILE, creation_time, UINT32) { return parse_ANY_timestamp(garmin, data); } -DECLARE_FIELD(FILE, number, UINT16) { return 0; } -DECLARE_FIELD(FILE, other_time, UINT32) { return parse_ANY_timestamp(garmin, data); } +DECLARE_FIELD(FILE, file_type, ENUM) { } +DECLARE_FIELD(FILE, manufacturer, UINT16) { } +DECLARE_FIELD(FILE, product, UINT16) { } +DECLARE_FIELD(FILE, serial, UINT32Z) { } +DECLARE_FIELD(FILE, creation_time, UINT32) { } +DECLARE_FIELD(FILE, number, UINT16) { } +DECLARE_FIELD(FILE, other_time, UINT32) { } // SESSION msg -DECLARE_FIELD(SESSION, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } -DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, start_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, start_time, UINT32) { garmin->cache.time = data; } +DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, start_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { } // 180 deg / 2**31 NE corner +DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { } // 180 deg / 2**31 pos +DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { } // 180 deg / 2**31 SW corner +DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { } // 180 deg / 2**31 pos +DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { } // 180 deg / 2**31 // LAP msg -DECLARE_FIELD(LAP, start_time, UINT32) { return parse_ANY_timestamp(garmin, data); } -DECLARE_FIELD(LAP, start_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, start_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, end_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, end_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, some_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, some_pos_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, other_pos_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(LAP, other_pos_long, SINT32) { return 0; } // 180 deg / 2**31 +DECLARE_FIELD(LAP, start_time, UINT32) { } +DECLARE_FIELD(LAP, start_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, start_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, end_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, end_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, some_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, some_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, other_pos_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, other_pos_long, SINT32) { } // 180 deg / 2**31 // RECORD msg -DECLARE_FIELD(RECORD, position_lat, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(RECORD, position_long, SINT32) { return 0; } // 180 deg / 2**31 -DECLARE_FIELD(RECORD, altitude, UINT16) { return 0; } // 5 *m + 500 ? -DECLARE_FIELD(RECORD, heart_rate, UINT8) { return 0; } // bpm -DECLARE_FIELD(RECORD, distance, UINT32) { return 0; } // Distance in 100 * m? WTF? -DECLARE_FIELD(RECORD, temperature, SINT8) { return 0; } // degrees C -DECLARE_FIELD(RECORD, abs_pressure, UINT32) {return 0; } // Pascal -DECLARE_FIELD(RECORD, depth, UINT32) { return 0; } // mm -DECLARE_FIELD(RECORD, next_stop_depth, UINT32) { return 0; } // mm -DECLARE_FIELD(RECORD, next_stop_time, UINT32) { return 0; } // seconds -DECLARE_FIELD(RECORD, tts, UINT32) { return 0; } // seconds -DECLARE_FIELD(RECORD, ndl, UINT32) { return 0; } // s -DECLARE_FIELD(RECORD, cns_load, UINT8) { return 0; } // percent -DECLARE_FIELD(RECORD, n2_load, UINT16) { return 0; } // percent +DECLARE_FIELD(RECORD, position_lat, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(RECORD, position_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(RECORD, altitude, UINT16) { } // 5 *m + 500 ? +DECLARE_FIELD(RECORD, heart_rate, UINT8) { } // bpm +DECLARE_FIELD(RECORD, distance, UINT32) { } // Distance in 100 * m? WTF? +DECLARE_FIELD(RECORD, temperature, SINT8) // degrees C +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.temperature = data; + garmin->callback(DC_SAMPLE_TEMPERATURE, sample, garmin->userdata); + } +} +DECLARE_FIELD(RECORD, abs_pressure, UINT32) {} // Pascal +DECLARE_FIELD(RECORD, depth, UINT32) // mm +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.depth = data / 1000.0; + garmin->callback(DC_SAMPLE_DEPTH, sample, garmin->userdata); + } +} +DECLARE_FIELD(RECORD, next_stop_depth, UINT32) // mm +{ + garmin->sample_data.ceiling = data / 1000.0; +} +DECLARE_FIELD(RECORD, next_stop_time, UINT32) // seconds +{ + garmin->sample_data.stop_time = data; +} +DECLARE_FIELD(RECORD, tts, UINT32) { } // seconds +DECLARE_FIELD(RECORD, ndl, UINT32) // s +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.deco.type = DC_DECO_NDL; + sample.deco.time = data; + garmin->callback(DC_SAMPLE_DECO, sample, garmin->userdata); + } +} +DECLARE_FIELD(RECORD, cns_load, UINT8) { } // percent +DECLARE_FIELD(RECORD, n2_load, UINT16) { } // percent // DEVICE_SETTINGS -DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->cache.utc_offset = data; return 0; } -DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->cache.time_offset = data; return 0; } +DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->cache.utc_offset = (SINT32) data; } // wrong type in FIT +DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->cache.time_offset = (SINT32) data; } // wrong type in FIT // DIVE_GAS - uses msg index -DECLARE_FIELD(DIVE_GAS, helium, UINT8) { return 0; } // percent -DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) { return 0; } // percent -DECLARE_FIELD(DIVE_GAS, status, ENUM) { return 0; } // 0 - disabled, 1 - enabled, 2 - backup +DECLARE_FIELD(DIVE_GAS, helium, UINT8) { } // percent +DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) { } // percent +DECLARE_FIELD(DIVE_GAS, status, ENUM) { } // 0 - disabled, 1 - enabled, 2 - backup // DIVE_SUMMARY -DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { return 0; } // mm -DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { return 0; } // mm -DECLARE_FIELD(DIVE_SUMMARY, surface_interval, UINT32) { return 0; } // sec -DECLARE_FIELD(DIVE_SUMMARY, start_cns, UINT8) { return 0; } // percent -DECLARE_FIELD(DIVE_SUMMARY, end_cns, UINT8) { return 0; } // percent -DECLARE_FIELD(DIVE_SUMMARY, start_n2, UINT16) { return 0; } // percent -DECLARE_FIELD(DIVE_SUMMARY, end_n2, UINT16) { return 0; } // percent -DECLARE_FIELD(DIVE_SUMMARY, o2_toxicity, UINT16) { return 0; } // OTUs -DECLARE_FIELD(DIVE_SUMMARY, dive_number, UINT32) { return 0; } -DECLARE_FIELD(DIVE_SUMMARY, bottom_time, UINT32) { return 0; } // ms +DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { garmin->cache.avgdepth = data / 1000.0; } // mm +DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { garmin->cache.maxdepth = data / 1000.0; } // mm +DECLARE_FIELD(DIVE_SUMMARY, surface_interval, UINT32) { } // sec +DECLARE_FIELD(DIVE_SUMMARY, start_cns, UINT8) { } // percent +DECLARE_FIELD(DIVE_SUMMARY, end_cns, UINT8) { } // percent +DECLARE_FIELD(DIVE_SUMMARY, start_n2, UINT16) { } // percent +DECLARE_FIELD(DIVE_SUMMARY, end_n2, UINT16) { } // percent +DECLARE_FIELD(DIVE_SUMMARY, o2_toxicity, UINT16) { } // OTUs +DECLARE_FIELD(DIVE_SUMMARY, dive_number, UINT32) { } +DECLARE_FIELD(DIVE_SUMMARY, bottom_time, UINT32) { garmin->cache.divetime = data / 1000; } // ms struct msg_desc { @@ -625,29 +693,36 @@ static int traverse_definition(struct garmin_parser_t *garmin, } -static int traverse_data(struct garmin_parser_t *garmin) +static dc_status_t +traverse_data(struct garmin_parser_t *garmin) { const unsigned char *data = garmin->base.data; int len = garmin->base.size; unsigned int hdrsize, protocol, profile, datasize; unsigned int time; + // Reset the time and type descriptors before walking + memset(&garmin->sample_data, 0, sizeof(garmin->sample_data)); + memset(garmin->type_desc, 0, sizeof(garmin->type_desc)); + // The data starts with our filename fingerprint. Skip it. + if (len < FIT_NAME_SIZE) + return DC_STATUS_IO; data += FIT_NAME_SIZE; len -= FIT_NAME_SIZE; // The FIT header if (len < 12) - return -1; + return DC_STATUS_IO; hdrsize = data[0]; protocol = data[1]; profile = array_uint16_le(data+2); datasize = array_uint32_le(data+4); if (memcmp(data+8, ".FIT", 4)) - return -1; + return DC_STATUS_IO; if (hdrsize < 12 || datasize > len || datasize + hdrsize + 2 > len) - return -1; + return DC_STATUS_IO; garmin->cache.protocol = protocol; garmin->cache.profile = profile; @@ -679,17 +754,11 @@ static int traverse_data(struct garmin_parser_t *garmin) len = traverse_regular(garmin, data, datasize, record, &time); } if (len <= 0 || len > datasize) - return -1; + return DC_STATUS_IO; data += len; datasize -= len; } - return 0; -} - -static void initialize_field_caches(garmin_parser_t *garmin) -{ - memset(&garmin->cache, 0, sizeof(garmin->cache)); - traverse_data(garmin); + return DC_STATUS_SUCCESS; } static dc_status_t @@ -697,8 +766,13 @@ garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsign { garmin_parser_t *garmin = (garmin_parser_t *) abstract; - memset(garmin->type_desc, 0, sizeof(garmin->type_desc)); - initialize_field_caches(garmin); + /* Walk the data once without a callback to set up the core fields */ + garmin->callback = NULL; + garmin->userdata = NULL; + memset(&garmin->cache, 0, sizeof(garmin->cache)); + + traverse_data(garmin); + flush_pending_sample(garmin); return DC_STATUS_SUCCESS; } @@ -706,22 +780,11 @@ garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsign static dc_status_t garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) { - const unsigned char *data = abstract->data; - unsigned int yyyy, mm, dd, h, m, s; + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + dc_ticks_t time = 631065600 + (dc_ticks_t) garmin->cache.time; - if (abstract->size < FIT_NAME_SIZE) - return DC_STATUS_UNSUPPORTED; - - if (sscanf(data, "%04u-%02u-%02u-%02u-%02u-%02u", - &yyyy, &mm, &dd, &h, &m, &s) != 6) - return DC_STATUS_UNSUPPORTED; - - datetime->year = yyyy; - datetime->month = mm; - datetime->day = dd; - datetime->hour = h; - datetime->minute = m; - datetime->second = s; + // Show local time (time_offset) + dc_datetime_gmtime(datetime, time + garmin->cache.time_offset); datetime->timezone = DC_TIMEZONE_NONE; return DC_STATUS_SUCCESS; @@ -731,33 +794,33 @@ garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) { - const unsigned char *data = abstract->data; + garmin_parser_t *garmin = (garmin_parser_t *) abstract; if (!value) return DC_STATUS_INVALIDARGS; switch (type) { case DC_FIELD_DIVETIME: - *((unsigned int *) value) = 0; + *((unsigned int *) value) = garmin->cache.divetime; break; case DC_FIELD_AVGDEPTH: - *((double *) value) = 0; + *((double *) value) = garmin->cache.avgdepth; break; case DC_FIELD_MAXDEPTH: - *((double *) value) = 0; + *((double *) value) = garmin->cache.maxdepth; break; default: return DC_STATUS_UNSUPPORTED; } - return DC_STATUS_SUCCESS; } static dc_status_t garmin_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) { - const unsigned char *data = abstract->data; - unsigned int size = abstract->size; + garmin_parser_t *garmin = (garmin_parser_t *) abstract; - return DC_STATUS_SUCCESS; + garmin->callback = callback; + garmin->userdata = userdata; + return traverse_data(garmin); } From 6b7c269c9c4c972374a96d611438867752ff7c62 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Wed, 29 Aug 2018 17:52:07 -0700 Subject: [PATCH 12/16] garmin: add GPS coordinate data and improve parser_get_field() reports This adds all the GPS information I found, although for dives the primary ones do seem to be the "session" entry and exit ones. But I'm exporting all of them as strings, so that we can try to figure out what they mean. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 214 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 178 insertions(+), 36 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 07c8c82..5a52278 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -21,6 +21,7 @@ #include #include +#include #include #include "garmin.h" @@ -42,6 +43,12 @@ struct type_desc { unsigned char fields[MAXFIELDS][3]; }; +// Positions are signed 32-bit values, turning +// into 180 * val // 2**31 degrees. +struct pos { + int lat, lon; +}; + #define MAXTYPE 16 #define MAXGASES 16 #define MAXSTRINGS 32 @@ -68,9 +75,26 @@ typedef struct garmin_parser_t { unsigned int profile; unsigned int time; int utc_offset, time_offset; - unsigned int divetime; - double maxdepth; - double avgdepth; + + unsigned int DIVETIME; + double MAXDEPTH; + double AVGDEPTH; + + // I count nine (!) different GPS fields Hmm. + // Reporting all of them just to try to figure + // out what is what. + struct { + struct { + struct pos entry, exit; + struct pos NE, SW; // NE, SW corner + } SESSION; + struct { + struct pos entry, exit; + struct pos some, other; + } LAP; + struct pos RECORD; + } gps; + unsigned int ngases; dc_gasmix_t gasmix[MAXGASES]; dc_salinity_t salinity; @@ -86,6 +110,14 @@ typedef struct garmin_parser_t { } cache; } garmin_parser_t; +/* + * Macro to make it easy to set DC_FIELD_xyz values + */ +#define ASSIGN_FIELD(name, value) do { \ + garmin->cache.initialized |= 1u << DC_FIELD_##name; \ + garmin->cache.name = (value); \ +} while (0) + typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user); static void flush_pending_sample(struct garmin_parser_t *garmin) @@ -140,6 +172,34 @@ garmin_parser_create (dc_parser_t **out, dc_context_t *context) return DC_STATUS_SUCCESS; } +static void add_string(garmin_parser_t *garmin, const char *desc, const char *value) +{ + int i; + + garmin->cache.initialized |= 1 << DC_FIELD_STRING; + for (i = 0; i < MAXSTRINGS; i++) { + dc_field_string_t *str = garmin->cache.strings+i; + if (str->desc) + continue; + str->desc = desc; + str->value = strdup(value); + break; + } +} + +static void add_string_fmt(garmin_parser_t *garmin, const char *desc, const char *fmt, ...) +{ + char buffer[256]; + va_list ap; + + va_start(ap, fmt); + buffer[sizeof(buffer)-1] = 0; + (void) vsnprintf(buffer, sizeof(buffer)-1, fmt, ap); + va_end(ap); + + add_string(garmin, desc, buffer); +} + #define DECLARE_FIT_TYPE(name, ctype, inval) \ typedef ctype name; \ static const name name##_INVAL = inval @@ -229,10 +289,7 @@ DECLARE_FIELD(ANY, timestamp, UINT32) if (data <= garmin->sample_data.time) return; - // Flush any pending sample data before sending the next time event - flush_pending_sample(garmin); - - // *Now* we're ready to actually update the sample times + // Now we're ready to actually update the sample times garmin->sample_data.time = data; sample.time = data; garmin->callback(DC_SAMPLE_TIME, sample, garmin->userdata); @@ -252,29 +309,29 @@ DECLARE_FIELD(FILE, other_time, UINT32) { } // SESSION msg DECLARE_FIELD(SESSION, start_time, UINT32) { garmin->cache.time = data; } -DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, start_pos_long, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { } // 180 deg / 2**31 NE corner -DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { } // 180 deg / 2**31 pos -DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { } // 180 deg / 2**31 SW corner -DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { } // 180 deg / 2**31 pos -DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { garmin->cache.gps.SESSION.entry.lat = data; } +DECLARE_FIELD(SESSION, start_pos_long, SINT32) { garmin->cache.gps.SESSION.entry.lon = data; } +DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { garmin->cache.gps.SESSION.NE.lat = data; } +DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { garmin->cache.gps.SESSION.NE.lon = data; } +DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { garmin->cache.gps.SESSION.SW.lat = data; } +DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { garmin->cache.gps.SESSION.SW.lon = data; } +DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { garmin->cache.gps.SESSION.exit.lat = data; } +DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { garmin->cache.gps.SESSION.exit.lon = data; } // LAP msg DECLARE_FIELD(LAP, start_time, UINT32) { } -DECLARE_FIELD(LAP, start_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, start_pos_long, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, end_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, end_pos_long, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, some_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, some_pos_long, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, other_pos_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(LAP, other_pos_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(LAP, start_pos_lat, SINT32) { garmin->cache.gps.LAP.entry.lat = data; } +DECLARE_FIELD(LAP, start_pos_long, SINT32) { garmin->cache.gps.LAP.entry.lon = data; } +DECLARE_FIELD(LAP, end_pos_lat, SINT32) { garmin->cache.gps.LAP.exit.lat = data; } +DECLARE_FIELD(LAP, end_pos_long, SINT32) { garmin->cache.gps.LAP.exit.lon = data; } +DECLARE_FIELD(LAP, some_pos_lat, SINT32) { garmin->cache.gps.LAP.some.lat = data; } +DECLARE_FIELD(LAP, some_pos_long, SINT32) { garmin->cache.gps.LAP.some.lon = data; } +DECLARE_FIELD(LAP, other_pos_lat, SINT32) { garmin->cache.gps.LAP.other.lat = data; } +DECLARE_FIELD(LAP, other_pos_long, SINT32) { garmin->cache.gps.LAP.other.lon = data; } // RECORD msg -DECLARE_FIELD(RECORD, position_lat, SINT32) { } // 180 deg / 2**31 -DECLARE_FIELD(RECORD, position_long, SINT32) { } // 180 deg / 2**31 +DECLARE_FIELD(RECORD, position_lat, SINT32) { garmin->cache.gps.RECORD.lat = data; } +DECLARE_FIELD(RECORD, position_long, SINT32) { garmin->cache.gps.RECORD.lon = data; } DECLARE_FIELD(RECORD, altitude, UINT16) { } // 5 *m + 500 ? DECLARE_FIELD(RECORD, heart_rate, UINT8) { } // bpm DECLARE_FIELD(RECORD, distance, UINT32) { } // Distance in 100 * m? WTF? @@ -326,8 +383,8 @@ DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) { } // percent DECLARE_FIELD(DIVE_GAS, status, ENUM) { } // 0 - disabled, 1 - enabled, 2 - backup // DIVE_SUMMARY -DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { garmin->cache.avgdepth = data / 1000.0; } // mm -DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { garmin->cache.maxdepth = data / 1000.0; } // mm +DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { ASSIGN_FIELD(AVGDEPTH, data / 1000.0); } +DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { ASSIGN_FIELD(MAXDEPTH, data / 1000.0); } DECLARE_FIELD(DIVE_SUMMARY, surface_interval, UINT32) { } // sec DECLARE_FIELD(DIVE_SUMMARY, start_cns, UINT8) { } // percent DECLARE_FIELD(DIVE_SUMMARY, end_cns, UINT8) { } // percent @@ -335,7 +392,7 @@ DECLARE_FIELD(DIVE_SUMMARY, start_n2, UINT16) { } // percent DECLARE_FIELD(DIVE_SUMMARY, end_n2, UINT16) { } // percent DECLARE_FIELD(DIVE_SUMMARY, o2_toxicity, UINT16) { } // OTUs DECLARE_FIELD(DIVE_SUMMARY, dive_number, UINT32) { } -DECLARE_FIELD(DIVE_SUMMARY, bottom_time, UINT32) { garmin->cache.divetime = data / 1000; } // ms +DECLARE_FIELD(DIVE_SUMMARY, bottom_time, UINT32) { ASSIGN_FIELD(DIVETIME, data / 1000); } struct msg_desc { @@ -757,10 +814,50 @@ traverse_data(struct garmin_parser_t *garmin) return DC_STATUS_IO; data += len; datasize -= len; + + // Flush pending data on record boundaries + flush_pending_sample(garmin); } return DC_STATUS_SUCCESS; } +/* Don't use floating point printing, because of "," vs "." confusion */ +static void add_gps_string(garmin_parser_t *garmin, const char *desc, struct pos *pos) +{ + int lat = pos->lat, lon = pos->lon; + + if (lat && lon) { + int latsign = 0, lonsign = 0; + int latfrac, lonfrac; + long long tmp; + + if (lat < 0) { + lat = -lat; + latsign = 1; + } + if (lon < 0) { + lon = -lon; + lonsign = 1; + } + + tmp = 360 * (long long) lat; + lat = tmp >> 32; + tmp &= 0xffffffff; + tmp *= 1000000; + latfrac = tmp >> 32; + + tmp = 360 * (long long) lon; + lon = tmp >> 32; + tmp &= 0xffffffff; + tmp *= 1000000; + lonfrac = tmp >> 32; + + add_string_fmt(garmin, desc, "%s%d.%06d, %s%d.%06d", + latsign ? "-" : "", lat, latfrac, + lonsign ? "-" : "", lon, lonfrac); + } +} + static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size) { @@ -772,7 +869,20 @@ garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsign memset(&garmin->cache, 0, sizeof(garmin->cache)); traverse_data(garmin); - flush_pending_sample(garmin); + // These seem to be the "real" GPS dive coordinates + add_gps_string(garmin, "GPS1", &garmin->cache.gps.SESSION.entry); + add_gps_string(garmin, "GPS2", &garmin->cache.gps.SESSION.exit); + + add_gps_string(garmin, "Session NE corner GPS", &garmin->cache.gps.SESSION.NE); + add_gps_string(garmin, "Session SW corner GPS", &garmin->cache.gps.SESSION.SW); + + add_gps_string(garmin, "Lap entry GPS", &garmin->cache.gps.LAP.entry); + add_gps_string(garmin, "Lap exit GPS", &garmin->cache.gps.LAP.exit); + add_gps_string(garmin, "Lap some GPS", &garmin->cache.gps.LAP.some); + add_gps_string(garmin, "Lap other GPS", &garmin->cache.gps.LAP.other); + + add_gps_string(garmin, "Record GPS", &garmin->cache.gps.RECORD); + return DC_STATUS_SUCCESS; } @@ -790,6 +900,22 @@ garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) return DC_STATUS_SUCCESS; } +static dc_status_t get_string_field(dc_field_string_t *strings, unsigned idx, dc_field_string_t *value) +{ + if (idx < MAXSTRINGS) { + dc_field_string_t *res = strings+idx; + if (res->desc && res->value) { + *value = *res; + return DC_STATUS_SUCCESS; + } + } + return DC_STATUS_UNSUPPORTED; +} + +// Ugly define thing makes the code much easier to read +// I'd love to use __typeof__, but that's a gcc'ism +#define field_value(p, NAME) \ + (memcpy((p), &garmin->cache.NAME, sizeof(garmin->cache.NAME)), DC_STATUS_SUCCESS) static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) @@ -799,16 +925,32 @@ garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned i if (!value) return DC_STATUS_INVALIDARGS; + /* This whole sequence should be standardized */ + if (!(garmin->cache.initialized & (1 << type))) + return DC_STATUS_UNSUPPORTED; + switch (type) { case DC_FIELD_DIVETIME: - *((unsigned int *) value) = garmin->cache.divetime; - break; - case DC_FIELD_AVGDEPTH: - *((double *) value) = garmin->cache.avgdepth; - break; + return field_value(value, DIVETIME); case DC_FIELD_MAXDEPTH: - *((double *) value) = garmin->cache.maxdepth; - break; + return field_value(value, MAXDEPTH); + case DC_FIELD_AVGDEPTH: + return field_value(value, AVGDEPTH); + case DC_FIELD_GASMIX_COUNT: + case DC_FIELD_TANK_COUNT: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_GASMIX: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_SALINITY: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_ATMOSPHERIC: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_DIVEMODE: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_TANK: + return DC_STATUS_UNSUPPORTED; + case DC_FIELD_STRING: + return get_string_field(garmin->cache.strings, flags, (dc_field_string_t *)value); default: return DC_STATUS_UNSUPPORTED; } From 994efff75a90285b82c0d9585246f369b4a9d5b9 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Fri, 31 Aug 2018 09:44:37 -0700 Subject: [PATCH 13/16] garmin: add support for downloading gas mixes This clarifies and generalizes the "pending sample data" a bit to also work for gas mixes, since it's one of those things where you get multiple fields in random order, and it needs to be batched up into one "this gas for this cylinder" thing. Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 114 ++++++++++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 5a52278..97313d1 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -53,19 +53,33 @@ struct pos { #define MAXGASES 16 #define MAXSTRINGS 32 +// Some record data needs to be bunched up +// and sent together. +struct record_data { + unsigned int pending; + unsigned int time; + + // RECORD_DECO + int stop_time; + double ceiling; + + // RECORD_GASMIX + int index, gas_status; + dc_gasmix_t gasmix; +}; + +#define RECORD_GASMIX 1 +#define RECORD_DECO 2 + typedef struct garmin_parser_t { dc_parser_t base; dc_sample_callback_t callback; void *userdata; - // Some sample data needs to be bunched up - // and sent together. - struct { - unsigned int time; - int stop_time; - double ceiling; - } sample_data; + // Multi-value record data + struct record_data record_data; + struct type_desc type_desc[MAXTYPE]; // Field cache @@ -76,9 +90,12 @@ typedef struct garmin_parser_t { unsigned int time; int utc_offset, time_offset; + // dc_get_field() data unsigned int DIVETIME; double MAXDEPTH; double AVGDEPTH; + unsigned int GASMIX_COUNT; + dc_gasmix_t gasmix[MAXGASES]; // I count nine (!) different GPS fields Hmm. // Reporting all of them just to try to figure @@ -95,8 +112,6 @@ typedef struct garmin_parser_t { struct pos RECORD; } gps; - unsigned int ngases; - dc_gasmix_t gasmix[MAXGASES]; dc_salinity_t salinity; double surface_pressure; dc_divemode_t divemode; @@ -120,20 +135,40 @@ typedef struct garmin_parser_t { typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user); -static void flush_pending_sample(struct garmin_parser_t *garmin) +/* + * Some data isn't just something we can save off directly: it's a record with + * multiple fields where one field describes another. + * + * The solution is to just batch it up in the "garmin->record_data", and then + * this function gets called at the end of a record. + */ +static void flush_pending_record(struct garmin_parser_t *garmin) { - if (!garmin->callback) - return; + struct record_data *record = &garmin->record_data; + unsigned int pending = record->pending; - if (garmin->sample_data.stop_time && garmin->sample_data.ceiling) { + record->pending = 0; + if (!garmin->callback) { + if (pending & RECORD_GASMIX) { + int index = record->index; + if (index < MAXGASES) { + garmin->cache.gasmix[index] = record->gasmix; + garmin->cache.GASMIX_COUNT = index+1; + } + garmin->cache.initialized |= 1 << DC_FIELD_GASMIX; + garmin->cache.initialized |= 1 << DC_FIELD_GASMIX_COUNT; + garmin->cache.initialized |= 1 << DC_FIELD_TANK_COUNT; + } + return; + } + + if (pending & RECORD_DECO) { dc_sample_value_t sample = {0}; sample.deco.type = DC_DECO_DECOSTOP; - sample.deco.time = garmin->sample_data.stop_time; - sample.deco.depth = garmin->sample_data.ceiling; + sample.deco.time = record->stop_time; + sample.deco.depth = record->ceiling; garmin->callback(DC_SAMPLE_DECO, sample, garmin->userdata); } - garmin->sample_data.stop_time = 0; - garmin->sample_data.ceiling = 0; } @@ -286,17 +321,17 @@ DECLARE_FIELD(ANY, timestamp, UINT32) data -= garmin->cache.time; // Did we already do this? - if (data <= garmin->sample_data.time) + if (data <= garmin->record_data.time) return; // Now we're ready to actually update the sample times - garmin->sample_data.time = data; + garmin->record_data.time = data; sample.time = data; garmin->callback(DC_SAMPLE_TIME, sample, garmin->userdata); } } -DECLARE_FIELD(ANY, message_index, UINT16) { } -DECLARE_FIELD(ANY, part_index, UINT32) { } +DECLARE_FIELD(ANY, message_index, UINT16) { garmin->record_data.index = data; } +DECLARE_FIELD(ANY, part_index, UINT32) { garmin->record_data.index = data; } // FILE msg DECLARE_FIELD(FILE, file_type, ENUM) { } @@ -354,11 +389,13 @@ DECLARE_FIELD(RECORD, depth, UINT32) // mm } DECLARE_FIELD(RECORD, next_stop_depth, UINT32) // mm { - garmin->sample_data.ceiling = data / 1000.0; + garmin->record_data.pending |= RECORD_DECO; + garmin->record_data.ceiling = data / 1000.0; } DECLARE_FIELD(RECORD, next_stop_time, UINT32) // seconds { - garmin->sample_data.stop_time = data; + garmin->record_data.pending |= RECORD_DECO; + garmin->record_data.stop_time = data; } DECLARE_FIELD(RECORD, tts, UINT32) { } // seconds DECLARE_FIELD(RECORD, ndl, UINT32) // s @@ -378,9 +415,21 @@ DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->cache.utc_offset = DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->cache.time_offset = (SINT32) data; } // wrong type in FIT // DIVE_GAS - uses msg index -DECLARE_FIELD(DIVE_GAS, helium, UINT8) { } // percent -DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) { } // percent -DECLARE_FIELD(DIVE_GAS, status, ENUM) { } // 0 - disabled, 1 - enabled, 2 - backup +DECLARE_FIELD(DIVE_GAS, helium, UINT8) +{ + garmin->record_data.gasmix.helium = data / 100.0; + garmin->record_data.pending |= RECORD_GASMIX; +} +DECLARE_FIELD(DIVE_GAS, oxygen, UINT8) +{ + garmin->record_data.gasmix.oxygen = data / 100.0; + garmin->record_data.pending |= RECORD_GASMIX; +} +DECLARE_FIELD(DIVE_GAS, status, ENUM) +{ + // 0 - disabled, 1 - enabled, 2 - backup + garmin->record_data.gas_status = data; +} // DIVE_SUMMARY DECLARE_FIELD(DIVE_SUMMARY, avg_depth, UINT32) { ASSIGN_FIELD(AVGDEPTH, data / 1000.0); } @@ -759,7 +808,7 @@ traverse_data(struct garmin_parser_t *garmin) unsigned int time; // Reset the time and type descriptors before walking - memset(&garmin->sample_data, 0, sizeof(garmin->sample_data)); + memset(&garmin->record_data, 0, sizeof(garmin->record_data)); memset(garmin->type_desc, 0, sizeof(garmin->type_desc)); // The data starts with our filename fingerprint. Skip it. @@ -816,7 +865,8 @@ traverse_data(struct garmin_parser_t *garmin) datasize -= len; // Flush pending data on record boundaries - flush_pending_sample(garmin); + if (garmin->record_data.pending) + flush_pending_record(garmin); } return DC_STATUS_SUCCESS; } @@ -916,6 +966,8 @@ static dc_status_t get_string_field(dc_field_string_t *strings, unsigned idx, dc // I'd love to use __typeof__, but that's a gcc'ism #define field_value(p, NAME) \ (memcpy((p), &garmin->cache.NAME, sizeof(garmin->cache.NAME)), DC_STATUS_SUCCESS) +// Hacky hack hack +#define GASMIX gasmix[flags] static dc_status_t garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) @@ -938,9 +990,11 @@ garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned i return field_value(value, AVGDEPTH); case DC_FIELD_GASMIX_COUNT: case DC_FIELD_TANK_COUNT: - return DC_STATUS_UNSUPPORTED; + return field_value(value, GASMIX_COUNT); case DC_FIELD_GASMIX: - return DC_STATUS_UNSUPPORTED; + if (flags >= MAXGASES) + return DC_STATUS_UNSUPPORTED; + return field_value(value, GASMIX); case DC_FIELD_SALINITY: return DC_STATUS_UNSUPPORTED; case DC_FIELD_ATMOSPHERIC: From 6a6e60c9bb608555bd80bb7fc4de1b986ae68462 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Fri, 31 Aug 2018 09:54:08 -0700 Subject: [PATCH 14/16] garmin: don't emit fake device info and vendor event The libdivecomputer model is just broken - we don't know this information before parsing the dive. But let's not emit a fake event that generates bogus serial number data. I thought I'd be able to fill it in, but this really isn't reasonable, so disable it entirely for now. Signed-off-by: Linus Torvalds --- src/garmin.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/garmin.c b/src/garmin.c index 4c8c92f..4a731e1 100644 --- a/src/garmin.c +++ b/src/garmin.c @@ -247,6 +247,7 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void progress.current = 0; device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); +#if 0 // Emit a device info event. dc_event_devinfo_t devinfo; devinfo.model = 0; @@ -259,6 +260,7 @@ garmin_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void vendor.data = "Garmin"; vendor.size = 6; device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); +#endif file = dc_buffer_new (16384); if (file == NULL) { From 22a96bf395fad9597174f11eb6a275ca0ae54fae Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Fri, 31 Aug 2018 12:25:43 -0700 Subject: [PATCH 15/16] garmin: only record gasmixes for cylinders that aren't enabled This actually takes the gas status information into account, and doesn't show gas mixes that are disabled. All the Garmin Descent data now looks reasonable, but we're not generating any events (so no warnings, but also no gas change events etc). Signed-off-by: Linus Torvalds --- src/garmin_parser.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 97313d1..6a33d87 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -150,8 +150,10 @@ static void flush_pending_record(struct garmin_parser_t *garmin) record->pending = 0; if (!garmin->callback) { if (pending & RECORD_GASMIX) { + // 0 - disabled, 1 - enabled, 2 - backup + int enabled = record->gas_status > 0; int index = record->index; - if (index < MAXGASES) { + if (enabled && index < MAXGASES) { garmin->cache.gasmix[index] = record->gasmix; garmin->cache.GASMIX_COUNT = index+1; } From fe2a43e7989b67f8719138bd6abae3844d04107e Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Fri, 31 Aug 2018 13:21:56 -0700 Subject: [PATCH 16/16] Add dc_usb_storage_open to the symbols list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparetly the Windows build needs this for the library building. Reported-by: Wojciech Więckowski Signed-off-by: Linus Torvalds --- src/libdivecomputer.symbols | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols index 2d7e7de..eeb7240 100644 --- a/src/libdivecomputer.symbols +++ b/src/libdivecomputer.symbols @@ -75,6 +75,8 @@ dc_usbhid_device_free dc_usbhid_iterator_new dc_usbhid_open +dc_usb_storage_open + dc_custom_open dc_parser_new