diff --git a/examples/common.c b/examples/common.c index ba22eaf..d5740d8 100644 --- a/examples/common.c +++ b/examples/common.c @@ -90,6 +90,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 154bf12..4ba705b 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -108,6 +108,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 047e35b..b7dc2e8 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -498,6 +498,14 @@ RelativePath="..\src\tecdiving_divecomputereu_parser.c" > + + + + @@ -844,6 +852,10 @@ RelativePath="..\src\tecdiving_divecomputereu.h" > + + diff --git a/src/Makefile.am b/src/Makefile.am index cc71d1a..a6ca28d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -73,6 +73,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 74d0f8a..17c3283 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -45,6 +45,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 int dc_filter_mares (dc_transport_t transport, const void *userdata); static int dc_filter_divesystem (dc_transport_t transport, const void *userdata); static int dc_filter_oceanic (dc_transport_t transport, const void *userdata); @@ -376,6 +377,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, 2859, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin}, }; static int @@ -581,6 +584,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 (userdata, usbhid, 0, dc_match_usb); + } + + return 1; +} + static int dc_filter_mares (dc_transport_t transport, const void *userdata) { static const char * const bluetooth[] = { diff --git a/src/device.c b/src/device.c index 9da9cd9..ce9a29f 100644 --- a/src/device.c +++ b/src/device.c @@ -57,6 +57,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "garmin.h" #include "device-private.h" #include "context-private.h" @@ -211,6 +212,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..f074422 --- /dev/null +++ b/src/garmin.c @@ -0,0 +1,338 @@ +/* + * 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 +#include +#include +#include +#include +#include +#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; + 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, + garmin_device_set_fingerprint, /* 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; + 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) +{ + dc_status_t status = DC_STATUS_SUCCESS; + garmin_device_t *device = (garmin_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(dc_device_t *abstract, DIR *dir, struct file_list *files) +{ + struct dirent *de; + + DEBUG (abstract->context, "Iterating over Garmin files"); + while ((de = readdir(dir)) != NULL) { + int len = strlen(de->d_name); + struct fit_name *entry; + const char *explain = NULL; + + DEBUG (abstract->context, " %s", de->d_name); + + if (len < 5) + explain = "name too short"; + if (len >= FIT_NAME_SIZE) + explain = "name too long"; + if (strncasecmp(de->d_name + len - 4, ".FIT", 4)) + explain = "name lacks FIT suffix"; + + DEBUG (abstract->context, " %s - %s", de->d_name, explain ? explain : "adding to list"); + if (explain) + 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; + } + + /* + * NOTE! This depends on the zero-padding that strncpy does. + * + * strncpy() doesn't just limit the size of the copy, it + * will zero-pad the end of the result buffer. + */ + entry = files->array + files->nr++; + strncpy(entry->name, de->d_name, FIT_NAME_SIZE); + } + DEBUG (abstract->context, "Found %d files", files->nr); + + qsort(files->array, files->nr, sizeof(struct fit_name), name_cmp); + return DC_STATUS_SUCCESS; +} + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +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 | O_BINARY); + + 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; + dc_parser_t *parser; + 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)-1, &pathlen); + if (rc != DC_STATUS_SUCCESS) + return rc; + pathname[pathlen] = 0; + + // 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) { + ERROR (abstract->context, "Invalid Garmin base directory '%s'", pathname); + 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) { + ERROR (abstract->context, "Failed to open directory '%s'.", pathname); + return DC_STATUS_IO; + } + + // Get the list of FIT files + rc = get_file_list(abstract, dir, &files); + closedir(dir); + 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; + DEBUG (abstract->context, "Ignoring '%s' and older", name); + 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); + + file = dc_buffer_new (16384); + if (file == NULL) { + ERROR (abstract->context, "Insufficient buffer space available."); + free(files.array); + return DC_STATUS_NOMEMORY; + } + if ((rc = garmin_parser_create(&parser, abstract->context) != DC_STATUS_SUCCESS)) { + ERROR (abstract->context, "Failed to create parser for dive verification."); + free(files.array); + return rc; + } + + dc_event_devinfo_t devinfo; + dc_event_devinfo_t *devinfo_p = &devinfo; + for (int i = 0; i < files.nr; i++) { + const char *name = files.array[i].name; + const unsigned char *data; + unsigned int size; + short is_dive = 0; + + 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); + + is_dive = garmin_parser_is_dive(parser, data, size, devinfo_p); + if (devinfo_p) { + // first time we came through here, let's emit the + // devinfo and vendor events + device_event_emit (abstract, DC_EVENT_DEVINFO, devinfo_p); + devinfo_p = NULL; + } + if (!is_dive) { + DEBUG (abstract->context, "decided %s isn't a dive.", name); + continue; + } + + if (callback && !callback(data, size, name, FIT_NAME_SIZE, userdata)) + break; + + progress.current++; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + } + + free(files.array); + dc_parser_destroy(parser); + return status; +} diff --git a/src/garmin.h b/src/garmin.h new file mode 100644 index 0000000..bd09ed4 --- /dev/null +++ b/src/garmin.h @@ -0,0 +1,59 @@ +/* + * 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); + +// we need to be able to call into the parser to check if the +// files that we find are actual dives +int +garmin_parser_is_dive (dc_parser_t *abstract, const unsigned char *data, unsigned int size, dc_event_devinfo_t *devinfo_p); + +// 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 */ +#endif /* GARMIN_H */ diff --git a/src/garmin_parser.c b/src/garmin_parser.c new file mode 100644 index 0000000..5ebbe6a --- /dev/null +++ b/src/garmin_parser.c @@ -0,0 +1,1318 @@ +/* + * 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 +#include +#include + +#include "garmin.h" +#include "context-private.h" +#include "parser-private.h" +#include "array.h" +#include "field-cache.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]; +}; + +// 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 + +// 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; + + // RECORD_EVENT + unsigned char event_type, event_nr, event_group; + unsigned int event_data, event_unknown; + + // RECORD_DEVICE_INFO + unsigned int device_index, firmware, serial, product; + + // RECORD_DECO_MODEL + unsigned char model, gf_low, gf_high; +}; + +#define RECORD_GASMIX 1 +#define RECORD_DECO 2 +#define RECORD_EVENT 4 +#define RECORD_DEVICE_INFO 8 +#define RECORD_DECO_MODEL 16 + +typedef struct garmin_parser_t { + dc_parser_t base; + + dc_sample_callback_t callback; + void *userdata; + + // Multi-value record data + struct record_data record_data; + + struct type_desc type_desc[MAXTYPE]; + + // Field cache + unsigned int initialized; + unsigned int sub_sport; + unsigned int serial; + unsigned int product; + unsigned int firmware; + unsigned int protocol; + unsigned int profile; + unsigned int time; + int utc_offset, time_offset; + + // 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; + + struct dc_field_cache cache; +} garmin_parser_t; + +typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user); + +/* + * Decode the event. Numbers from Wojtek's fit2subs python script + */ +static void garmin_event(struct garmin_parser_t *garmin, + unsigned char event, unsigned char type, unsigned char group, + unsigned int data, unsigned int unknown) +{ + static const struct { + // 1 - state, 2 - notify, 3 - warning, 4 - alarm + int severity; + const char *name; + } event_desc[] = { + [0] = { 2, "Deco required" }, + [1] = { 2, "Gas Switch prompted" }, + [2] = { 1, "Surface" }, + [3] = { 2, "Approaching NDL" }, + [4] = { 3, "ppO2 warning" }, + [5] = { 4, "ppO2 critical high" }, + [6] = { 4, "ppO2 critical low" }, + [7] = { 2, "Time alert" }, + [8] = { 2, "Depth alert" }, + [9] = { 3, "Deco ceiling broken" }, + [10] = { 1, "Deco completed" }, + [11] = { 3, "Safety stop ceiling broken" }, + [12] = { 1, "Safety stop completed" }, + [13] = { 3, "CNS warning" }, + [14] = { 4, "CNS critical" }, + [15] = { 3, "OTU warning" }, + [16] = { 4, "OTU critical" }, + [17] = { 3, "Ascent speed critical" }, + [18] = { 1, "Alert dismissed" }, + [19] = { 1, "Alert timed out" }, + [20] = { 3, "Battry Low" }, + [21] = { 3, "Battry Critical" }, + [22] = { 1, "Safety stop begin" }, + [23] = { 1, "Approaching deco stop" }, + }; + dc_sample_value_t sample = {0}; + + switch (event) { + case 38: + break; + case 48: + break; + case 56: + if (data >= C_ARRAY_SIZE(event_desc)) + return; + + sample.event.type = SAMPLE_EVENT_STRING; + sample.event.name = event_desc[data].name; + sample.event.flags = event_desc[data].severity << SAMPLE_FLAGS_SEVERITY_SHIFT; + if (!sample.event.name) + return; + garmin->callback(DC_SAMPLE_EVENT, sample, garmin->userdata); + return; + + case 57: + sample.gasmix = data - 1; + garmin->callback(DC_SAMPLE_GASMIX, sample, garmin->userdata); + return; + } +} + +/* + * 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) +{ + struct record_data *record = &garmin->record_data; + unsigned int pending = record->pending; + + 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 (enabled && index < MAXGASES) { + DC_ASSIGN_IDX(garmin->cache, GASMIX, index, record->gasmix); + DC_ASSIGN_FIELD(garmin->cache, GASMIX_COUNT, index+1); + } + } + if (pending & RECORD_DEVICE_INFO && record->device_index == 0) { + garmin->firmware = record->firmware; + garmin->serial = record->serial; + garmin->product = record->product; + } + if (pending & RECORD_DECO_MODEL) + dc_field_add_string_fmt(&garmin->cache, "Deco model", "Buhlmann ZHL-16C %u/%u", record->gf_low, record->gf_high); + return; + } + + if (pending & RECORD_DECO) { + dc_sample_value_t sample = {0}; + sample.deco.type = DC_DECO_DECOSTOP; + sample.deco.time = record->stop_time; + sample.deco.depth = record->ceiling; + garmin->callback(DC_SAMPLE_DECO, sample, garmin->userdata); + } + + if (pending & RECORD_EVENT) { + garmin_event(garmin, record->event_nr, record->event_type, + record->event_group, record->event_data, record->event_unknown); + } +} + + +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; +} + +#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); + +DECLARE_FIT_TYPE(FLOAT, unsigned int, 0xffffffff); +DECLARE_FIT_TYPE(DOUBLE, unsigned long long, 0xffffffffffffffffll); +DECLARE_FIT_TYPE(STRING, char *, NULL); + +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 + * 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; + 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 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; \ + DEBUG(g->base.context, "%s (%s): %lld", #name, #type, (long long)val); \ + parse_##name(g, *(type *)p); \ + } \ + static const struct field_desc name##_field_##type = { #name, parse_##name##_##type }; \ + 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) +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + + // Turn the timestamp relative to the beginning of the dive + if (data < garmin->time) + return; + data -= garmin->time; + + // Did we already do this? + if (data < garmin->record_data.time) + return; + + // Now we're ready to actually update the sample times + garmin->record_data.time = data+1; + sample.time = data; + garmin->callback(DC_SAMPLE_TIME, sample, garmin->userdata); + } +} +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) { } +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) { garmin->time = data; } +DECLARE_FIELD(SESSION, start_pos_lat, SINT32) { garmin->gps.SESSION.entry.lat = data; } +DECLARE_FIELD(SESSION, start_pos_long, SINT32) { garmin->gps.SESSION.entry.lon = data; } +DECLARE_FIELD(SESSION, nec_pos_lat, SINT32) { garmin->gps.SESSION.NE.lat = data; } +DECLARE_FIELD(SESSION, nec_pos_long, SINT32) { garmin->gps.SESSION.NE.lon = data; } +DECLARE_FIELD(SESSION, swc_pos_lat, SINT32) { garmin->gps.SESSION.SW.lat = data; } +DECLARE_FIELD(SESSION, swc_pos_long, SINT32) { garmin->gps.SESSION.SW.lon = data; } +DECLARE_FIELD(SESSION, exit_pos_lat, SINT32) { garmin->gps.SESSION.exit.lat = data; } +DECLARE_FIELD(SESSION, exit_pos_long, SINT32) { garmin->gps.SESSION.exit.lon = data; } + +// LAP msg +DECLARE_FIELD(LAP, start_time, UINT32) { } +DECLARE_FIELD(LAP, start_pos_lat, SINT32) { garmin->gps.LAP.entry.lat = data; } +DECLARE_FIELD(LAP, start_pos_long, SINT32) { garmin->gps.LAP.entry.lon = data; } +DECLARE_FIELD(LAP, end_pos_lat, SINT32) { garmin->gps.LAP.exit.lat = data; } +DECLARE_FIELD(LAP, end_pos_long, SINT32) { garmin->gps.LAP.exit.lon = data; } +DECLARE_FIELD(LAP, some_pos_lat, SINT32) { garmin->gps.LAP.some.lat = data; } +DECLARE_FIELD(LAP, some_pos_long, SINT32) { garmin->gps.LAP.some.lon = data; } +DECLARE_FIELD(LAP, other_pos_lat, SINT32) { garmin->gps.LAP.other.lat = data; } +DECLARE_FIELD(LAP, other_pos_long, SINT32) { garmin->gps.LAP.other.lon = data; } + +// RECORD msg +DECLARE_FIELD(RECORD, position_lat, SINT32) { garmin->gps.RECORD.lat = data; } +DECLARE_FIELD(RECORD, position_long, SINT32) { garmin->gps.RECORD.lon = data; } +DECLARE_FIELD(RECORD, altitude, UINT16) { } // 5 *m + 500 ? +DECLARE_FIELD(RECORD, heart_rate, UINT8) // bpm +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.heartbeat = data; + garmin->callback(DC_SAMPLE_HEARTBEAT, sample, garmin->userdata); + } +} +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->record_data.pending |= RECORD_DECO; + garmin->record_data.ceiling = data / 1000.0; +} +DECLARE_FIELD(RECORD, next_stop_time, UINT32) // seconds +{ + garmin->record_data.pending |= RECORD_DECO; + garmin->record_data.stop_time = data; +} +DECLARE_FIELD(RECORD, tts, UINT32) +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.time = data; + garmin->callback(DC_SAMPLE_TTS, sample, garmin->userdata); + } +} +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) +{ + if (garmin->callback) { + dc_sample_value_t sample = {0}; + sample.cns = data / 100.0; + garmin->callback(DC_SAMPLE_CNS, sample, garmin->userdata); + } +} +DECLARE_FIELD(RECORD, n2_load, UINT16) { } // percent + +// DEVICE_SETTINGS +DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->utc_offset = (SINT32) data; } // wrong type in FIT +DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->time_offset = (SINT32) data; } // wrong type in FIT + +// DEVICE_INFO +// collect the data and then use the record if it is for device_index 0 +DECLARE_FIELD(DEVICE_INFO, device_index, UINT8) +{ + garmin->record_data.device_index = data; + garmin->record_data.pending |= RECORD_DEVICE_INFO; +} +DECLARE_FIELD(DEVICE_INFO, product, UINT16) +{ + garmin->record_data.product = data; + garmin->record_data.pending |= RECORD_DEVICE_INFO; +} +DECLARE_FIELD(DEVICE_INFO, serial_nr, UINT32Z) +{ + garmin->record_data.serial = data; + garmin->record_data.pending |= RECORD_DEVICE_INFO; +} +DECLARE_FIELD(DEVICE_INFO, firmware, UINT16) +{ + garmin->record_data.firmware = data; + garmin->record_data.pending |= RECORD_DEVICE_INFO; +} + +// SPORT +DECLARE_FIELD(SPORT, sub_sport, ENUM) { garmin->sub_sport = (ENUM) data; } + +// DIVE_GAS - uses msg index +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) { DC_ASSIGN_FIELD(garmin->cache, AVGDEPTH, data / 1000.0); } +DECLARE_FIELD(DIVE_SUMMARY, max_depth, UINT32) { DC_ASSIGN_FIELD(garmin->cache, 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 +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) { DC_ASSIGN_FIELD(garmin->cache, DIVETIME, data / 1000); } + +// DIVE_SETTINGS +DECLARE_FIELD(DIVE_SETTINGS, name, STRING) { } +DECLARE_FIELD(DIVE_SETTINGS, model, ENUM) +{ + garmin->record_data.model = data; + garmin->record_data.pending |= RECORD_DECO_MODEL; +} +DECLARE_FIELD(DIVE_SETTINGS, gf_low, UINT8) +{ + garmin->record_data.gf_low = data; + garmin->record_data.pending |= RECORD_DECO_MODEL; +} +DECLARE_FIELD(DIVE_SETTINGS, gf_high, UINT8) +{ + garmin->record_data.gf_high = data; + garmin->record_data.pending |= RECORD_DECO_MODEL; +} +DECLARE_FIELD(DIVE_SETTINGS, water_type, ENUM) +{ + garmin->cache.SALINITY.type = data ? DC_WATER_SALT : DC_WATER_FRESH; + garmin->cache.initialized |= 1 << DC_FIELD_SALINITY; +} +DECLARE_FIELD(DIVE_SETTINGS, water_density, FLOAT) +{ + union { unsigned int binary; float actual; } val; + val.binary = data; + garmin->cache.SALINITY.density = val.actual; + garmin->cache.initialized |= 1 << DC_FIELD_SALINITY; +} +DECLARE_FIELD(DIVE_SETTINGS, po2_warn, UINT8) { } +DECLARE_FIELD(DIVE_SETTINGS, po2_critical, UINT8) { } +DECLARE_FIELD(DIVE_SETTINGS, po2_deco, UINT8) { } +DECLARE_FIELD(DIVE_SETTINGS, safety_stop_enabled, ENUM) { } +DECLARE_FIELD(DIVE_SETTINGS, bottom_depth, FLOAT) { } +DECLARE_FIELD(DIVE_SETTINGS, bottom_time, UINT32) { } +DECLARE_FIELD(DIVE_SETTINGS, apnea_countdown_enabled, ENUM) { } +DECLARE_FIELD(DIVE_SETTINGS, apnea_countdown_time, UINT32) { } +DECLARE_FIELD(DIVE_SETTINGS, backlight_mode, ENUM) { } +DECLARE_FIELD(DIVE_SETTINGS, backlight_brightness, UINT8) { } +DECLARE_FIELD(DIVE_SETTINGS, backlight_timeout, UINT8) { } +DECLARE_FIELD(DIVE_SETTINGS, repeat_dive_interval, UINT16) { } +DECLARE_FIELD(DIVE_SETTINGS, safety_stop_time, UINT16) { } +DECLARE_FIELD(DIVE_SETTINGS, heart_rate_source_type, ENUM) { } +DECLARE_FIELD(DIVE_SETTINGS, hear_rate_device_type, UINT8) { } + +// EVENT +DECLARE_FIELD(EVENT, event, ENUM) +{ + garmin->record_data.event_nr = data; + garmin->record_data.pending |= RECORD_EVENT; +} +DECLARE_FIELD(EVENT, type, ENUM) +{ + garmin->record_data.event_type = data; + garmin->record_data.pending |= RECORD_EVENT; +} +DECLARE_FIELD(EVENT, data, UINT32) +{ + garmin->record_data.event_data = data; +} +DECLARE_FIELD(EVENT, event_group, UINT8) +{ + garmin->record_data.event_group = data; +} +DECLARE_FIELD(EVENT, unknown, UINT32) +{ + garmin->record_data.event_unknown = data; +} + +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 = 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, 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) = { + .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) = { + .maxfield = 2, + .field = { + SET_FIELD(SPORT, 1, sub_sport, ENUM), // 53 - 57 are dive activities + } +}; + +DECLARE_MESG(SESSION) = { + .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 = 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 = 99, + .field = { + 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) = { + .maxfield = 16, + .field = { + SET_FIELD(EVENT, 0, event, ENUM), + SET_FIELD(EVENT, 1, type, ENUM), + SET_FIELD(EVENT, 3, data, UINT32), + SET_FIELD(EVENT, 4, event_group, UINT8), + SET_FIELD(EVENT, 15, unknown, UINT32), + } +}; + +DECLARE_MESG(DEVICE_INFO) = { + .maxfield = 6, + .field = { + SET_FIELD(DEVICE_INFO, 0, device_index, UINT8), + SET_FIELD(DEVICE_INFO, 3, serial_nr, UINT32Z), + SET_FIELD(DEVICE_INFO, 4, product, UINT16), + SET_FIELD(DEVICE_INFO, 5, firmware, UINT16), + } +}; + +DECLARE_MESG(ACTIVITY) = { }; +DECLARE_MESG(FILE_CREATOR) = { }; + +DECLARE_MESG(DIVE_SETTINGS) = { + .maxfield = 21, + .field = { + SET_FIELD(DIVE_SETTINGS, 0, name, STRING), // Unused except in dive plans + SET_FIELD(DIVE_SETTINGS, 1, model, ENUM), // model - Always 0 for Buhlmann ZHL-16C + SET_FIELD(DIVE_SETTINGS, 2, gf_low, UINT8), // 0 to 100 + SET_FIELD(DIVE_SETTINGS, 3, gf_high, UINT8), // 0 to 100 + SET_FIELD(DIVE_SETTINGS, 4, water_type, ENUM), // One of fresh (0), salt (1), or custom (3). 2 is en13319 which is unused. + SET_FIELD(DIVE_SETTINGS, 5, water_density, FLOAT), // If water_type is custom, this will be the density. Fresh is usually 1000, salt is usually 1025 + SET_FIELD(DIVE_SETTINGS, 6, po2_warn, UINT8), // PO2 * 100, so typically 140 to 160. When the PO2 starts blinking yellow + SET_FIELD(DIVE_SETTINGS, 7, po2_critical, UINT8), // See above; value when PO2 blinks red and you get a popup + SET_FIELD(DIVE_SETTINGS, 8, po2_deco, UINT8), // See above; PO2 limited used for choosing which gas to suggest + SET_FIELD(DIVE_SETTINGS, 9, safety_stop_enabled, ENUM), // Used in conjunction with safety_stop_time below + SET_FIELD(DIVE_SETTINGS, 10, bottom_depth, FLOAT), // Unused except in dive plans + SET_FIELD(DIVE_SETTINGS, 11, bottom_time, UINT32), // Unused except in dive plans + SET_FIELD(DIVE_SETTINGS, 12, apnea_countdown_enabled, ENUM), // This and apnea_countdown_time are the "Apnea Surface Alert" setting + SET_FIELD(DIVE_SETTINGS, 13, apnea_countdown_time, UINT32), // + SET_FIELD(DIVE_SETTINGS, 14, backlight_mode, ENUM), // 0 is "At Depth" and 1 is "Always On" + SET_FIELD(DIVE_SETTINGS, 15, backlight_brightness, UINT8), // 0 to 100 + SET_FIELD(DIVE_SETTINGS, 16, backlight_timeout, UINT8), // seconds; 0 is no timeout + SET_FIELD(DIVE_SETTINGS, 17, repeat_dive_interval, UINT16), // seconds between surfacing and when the watch stops and saves your dive. Must be at least 20. + SET_FIELD(DIVE_SETTINGS, 18, safety_stop_time, UINT16), // seconds; 180 or 300 are acceptable values + SET_FIELD(DIVE_SETTINGS, 19, heart_rate_source_type, ENUM), // For now all you need to know is source_type_local means WHR and source_type_antplus means strap data or off. (We're reusing existing infrastructure here which is why this is complex.) + SET_FIELD(DIVE_SETTINGS, 20, hear_rate_device_type, UINT8), // device type depending on heart_rate_source_type (ignorable for now) + } +}; +DECLARE_MESG(DIVE_ALARM) = { }; + +// 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_216) = { }; +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(216, WTF_216), + 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 all_data_inval(const unsigned char *data, int base_type, int len) +{ + int base_size = base_type_info[base_type].type_size; + unsigned long long invalid = base_type_info[base_type].type_inval; + + while (len > 0) { + unsigned long long val = 0; + memcpy(&val, data, base_size); + if (val != invalid) + return 0; + data += base_size; + len -= base_size; + } + return 1; +} + +static void unknown_field(struct garmin_parser_t *garmin, const unsigned char *data, + const char *msg_name, unsigned int field_nr, + int base_type, int len) +{ + char buffer[80]; + const char *str = (const char *)data; + + /* Skip empty strings */ + if (base_type == 7 && !*str) + return; + + /* Turn non-string data into hex values */ + if (base_type != 7) { + int pos = 0; + int base_size = base_type_info[base_type].type_size; + const char *sep = ""; + + /* Skip empty data */ + if (all_data_inval(data, base_type, len)) + return; + + str = buffer; + while (len > 0) { + long long val; + /* Space + hex + NUL */ + int need = 2+base_size*2; + + /* The "-4" is because we reserve that " ..\0" at the end */ + if (pos + need >= sizeof(buffer)-4) { + strcpy(buffer+pos, " .."); + break; + } + + val = 0; + memcpy(&val, data, base_size); + + pos += sprintf(buffer+pos, "%s%0*llx", sep, base_size*2, val); + sep = " "; + + data += base_size; + len -= base_size; + } + } + + DEBUG(garmin->base.context, "%s/%d %s '%s'", msg_name, field_nr, base_type_info[base_type].type_name, str); +} + + +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; + 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; + total_len += size; + continue; + } + base_size = base_type_info[base_type].type_size; + 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; + } + } + + + + // 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, base_type, data); + } else { + unknown_field(garmin, data, msg_name, field_nr, base_type, len); + } + + 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 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->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. + if (len < FIT_NAME_SIZE) + return DC_STATUS_IO; + + DEBUG(garmin->base.context, "file %s", data); + + data += FIT_NAME_SIZE; + len -= FIT_NAME_SIZE; + + // The FIT header + if (len < 12) + 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 DC_STATUS_IO; + if (hdrsize < 12 || datasize > len || datasize + hdrsize + 2 > len) + return DC_STATUS_IO; + + garmin->protocol = protocol; + garmin->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 DC_STATUS_IO; + data += len; + datasize -= len; + + // Flush pending data on record boundaries + if (garmin->record_data.pending) + flush_pending_record(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; + + dc_field_add_string_fmt(&garmin->cache, desc, "%s%d.%06d, %s%d.%06d", + latsign ? "-" : "", lat, latfrac, + lonsign ? "-" : "", lon, lonfrac); + } +} + +int +garmin_parser_is_dive (dc_parser_t *abstract, const unsigned char *data, unsigned int size, dc_event_devinfo_t *devinfo_p) +{ + // set up the parser and extract data + dc_parser_set_data(abstract, data, size); + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + + if (devinfo_p) { + devinfo_p->firmware = garmin->firmware; + devinfo_p->serial = garmin->serial; + devinfo_p->model = garmin->product; + } + switch (garmin->sub_sport) { + case 53: // Single-gas + case 54: // Multi-gas + case 55: // Gauge + case 56: // Apnea + case 57: // Apnea Hunt + case 63: // CCR + return 1; + default: + // Even if we don't recognize the sub_sport, + // let's assume it's a dive if we've seen + // average depth in the DIVE_SUMMARY record. + if (garmin->cache.AVGDEPTH) + return 1; + return 0; + } +} + +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; + + /* 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); + // These seem to be the "real" GPS dive coordinates + add_gps_string(garmin, "GPS1", &garmin->gps.SESSION.entry); + add_gps_string(garmin, "GPS2", &garmin->gps.SESSION.exit); + + add_gps_string(garmin, "Session NE corner GPS", &garmin->gps.SESSION.NE); + add_gps_string(garmin, "Session SW corner GPS", &garmin->gps.SESSION.SW); + + add_gps_string(garmin, "Lap entry GPS", &garmin->gps.LAP.entry); + add_gps_string(garmin, "Lap exit GPS", &garmin->gps.LAP.exit); + add_gps_string(garmin, "Lap some GPS", &garmin->gps.LAP.some); + add_gps_string(garmin, "Lap other GPS", &garmin->gps.LAP.other); + + add_gps_string(garmin, "Record GPS", &garmin->gps.RECORD); + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +garmin_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) +{ + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + dc_ticks_t time = 631065600 + (dc_ticks_t) garmin->time; + + // Show local time (time_offset) + dc_datetime_gmtime(datetime, time + garmin->time_offset); + datetime->timezone = DC_TIMEZONE_NONE; + + 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; +} + +static dc_status_t +garmin_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) +{ + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + + if (!value) + return DC_STATUS_INVALIDARGS; + if (type == DC_FIELD_TANK_COUNT) + type = DC_FIELD_GASMIX_COUNT; + + /* This whole sequence should be standardized */ + if (!(garmin->cache.initialized & (1 << type))) + return DC_STATUS_UNSUPPORTED; + + switch (type) { + case DC_FIELD_DIVETIME: + return DC_FIELD_VALUE(garmin->cache, value, DIVETIME); + case DC_FIELD_MAXDEPTH: + return DC_FIELD_VALUE(garmin->cache, value, MAXDEPTH); + case DC_FIELD_AVGDEPTH: + return DC_FIELD_VALUE(garmin->cache, value, AVGDEPTH); + case DC_FIELD_GASMIX_COUNT: + return DC_FIELD_VALUE(garmin->cache, value, GASMIX_COUNT); + case DC_FIELD_GASMIX: + if (flags >= MAXGASES) + return DC_STATUS_UNSUPPORTED; + return DC_FIELD_INDEX(garmin->cache, value, GASMIX, flags); + case DC_FIELD_SALINITY: + return DC_FIELD_VALUE(garmin->cache, value, SALINITY); + 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 dc_field_get_string(&garmin->cache, flags, (dc_field_string_t *)value); + 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) +{ + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + + garmin->callback = callback; + garmin->userdata = userdata; + return traverse_data(garmin); +} diff --git a/src/parser.c b/src/parser.c index 9203490..ca05a5e 100644 --- a/src/parser.c +++ b/src/parser.c @@ -57,6 +57,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "garmin.h" #include "context-private.h" #include "parser-private.h" @@ -172,6 +173,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; }