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;
}