diff --git a/examples/common.c b/examples/common.c index 0950ecb6..926c7461 100644 --- a/examples/common.c +++ b/examples/common.c @@ -103,7 +103,7 @@ static const backend_table_t g_backends[] = { {"symbios", DC_FAMILY_HALCYON_SYMBIOS, 1}, // Not merged upstream yet - {"descentmk1", DC_FAMILY_GARMIN, 0}, + {"descentmk1", DC_FAMILY_GARMIN, 2859}, }; static const transport_table_t g_transports[] = { diff --git a/include/libdivecomputer/parser.h b/include/libdivecomputer/parser.h index 81519277..5c36ab96 100644 --- a/include/libdivecomputer/parser.h +++ b/include/libdivecomputer/parser.h @@ -362,6 +362,10 @@ dc_parser_set_density (dc_parser_t *parser, double density); dc_status_t dc_parser_get_datetime (dc_parser_t *parser, dc_datetime_t *datetime); +/* Returns parser-provided device information when available. */ +dc_status_t +dc_parser_get_device_info (dc_parser_t *parser, dc_event_devinfo_t *devinfo); + /* DC_FIELD_STRING values are caller-owned and must be freed after use. */ dc_status_t dc_parser_get_field (dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value); diff --git a/src/Makefile.am b/src/Makefile.am index 27201495..90c3278b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -97,7 +97,7 @@ libdivecomputer_la_SOURCES = \ libdivecomputer_la_SOURCES += \ usb_storage.c \ field-cache.h field-cache.c \ - garmin.h garmin.c garmin_parser.c + garmin.h garmin-models.h garmin.c garmin_parser.c if OS_WIN32 libdivecomputer_la_SOURCES += serial_win32.c diff --git a/src/descriptor.c b/src/descriptor.c index 5ff31fa8..16c4c372 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -30,6 +30,7 @@ #include "iterator-private.h" #include "platform.h" #include "array.h" +#include "garmin-models.h" #define DC_FILTER_INTERNAL(key, values, isnullterminated, match) \ dc_filter_internal( \ @@ -546,13 +547,11 @@ static const dc_descriptor_t g_descriptors[] = { {"Halcyon", "Symbios HUD", DC_FAMILY_HALCYON_SYMBIOS, 1, DC_TRANSPORT_BLE, dc_filter_halcyon}, {"Halcyon", "Symbios Handset", DC_FAMILY_HALCYON_SYMBIOS, 7, DC_TRANSPORT_BLE, dc_filter_halcyon}, - // Not merged upstream yet - /* Garmin -- model numbers as defined in FIT format; USB product id is (0x4000 | model) */ - /* for the Mk1 we are using the model of the global model */ - /* for the Mk2/Mk3 we are using the model of the Mk2 global model */ - /* see garmin_parser.c for a more comprehensive list of models */ - {"Garmin", "Descent Mk1", DC_FAMILY_GARMIN, 2859, DC_TRANSPORT_USBSTORAGE, NULL}, - {"Garmin", "Descent Mk2(i)/Mk3(i)(S)/G1/G2/X50i", DC_FAMILY_GARMIN, 3258, DC_TRANSPORT_USBSTORAGE, NULL}, + /* Garmin */ +#define GARMIN_DESCRIPTOR(name, id, mtp_capable) {"Garmin", name, DC_FAMILY_GARMIN, id, DC_TRANSPORT_USBSTORAGE, NULL}, + GARMIN_MODEL_LIST(GARMIN_DESCRIPTOR) +#undef GARMIN_DESCRIPTOR + /* Generic FIT file import, not a hardware model. */ {"FIT", "File import", DC_FAMILY_GARMIN, 0, DC_TRANSPORT_USBSTORAGE, NULL }, }; diff --git a/src/garmin-models.h b/src/garmin-models.h new file mode 100644 index 00000000..0f9ac19e --- /dev/null +++ b/src/garmin-models.h @@ -0,0 +1,57 @@ +// AI-generated (Claude) +/* + * libdivecomputer + * + * Copyright (C) 2026 Michael Keller + * + * 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_MODELS_H +#define GARMIN_MODELS_H + +#include + +typedef struct { + const char *name; + int id; + bool mtp_capable; +} garmin_model_t; + +/* FIT product IDs. See the Garmin Connect IQ device reference. */ +#define GARMIN_MODEL_LIST(MODEL) \ + MODEL("Descent™ G1 / G1 Solar", 4005, true) \ + MODEL("Descent™ G2", 4588, true) \ + MODEL("Descent™ Mk1", 2859, false) \ + MODEL("Descent™ Mk1 APAC", 2991, false) \ + MODEL("Descent™ Mk2(i)", 3258, true) \ + MODEL("Descent™ Mk2(i) APAC", 3702, true) \ + MODEL("Descent™ Mk2 S", 3542, true) \ + MODEL("Descent™ Mk2 S APAC", 3930, true) \ + MODEL("Descent™ Mk3(i) 43mm", 4222, true) \ + MODEL("Descent™ Mk3(i) 51mm", 4223, true) \ + MODEL("Descent™ X50i", 4518, true) \ + MODEL("fēnix® 8 43mm", 4534, true) \ + MODEL("fēnix® 8 47mm / 51mm / tactix® 8 47mm / 51mm / quatix® 8 47mm / 51mm APAC", 4536, true) \ + MODEL("fēnix® 8 47mm / 51mm / tactix® 8 47mm / 51mm / quatix® 8 47mm / 51mm", 4775, true) \ + MODEL("fēnix® 8 Pro 47mm / 51mm / MicroLED / quatix® 8 Pro 47mm / 51mm", 4631, true) \ + MODEL("fēnix® 8 Solar 47mm", 4532, true) \ + MODEL("fēnix® 8 Solar 51mm / tactix® 8 Solar 51mm APAC", 4533, true) \ + MODEL("fēnix® 8 Solar 51mm / tactix® 8 Solar 51mm", 4776, true) + +extern const garmin_model_t garmin_models[]; + +#endif /* GARMIN_MODELS_H */ diff --git a/src/garmin.c b/src/garmin.c index 22482685..d83aec82 100644 --- a/src/garmin.c +++ b/src/garmin.c @@ -56,6 +56,7 @@ #include "platform.h" #include "garmin.h" +#include "garmin-models.h" #include "context-private.h" #include "device-private.h" #include "array.h" @@ -65,8 +66,6 @@ #define GARMIN_VENDOR 0x091E -#define DESCENT_MK2 3258 - // deal with ancient libmpt found on older Linux distros #ifndef LIBMTP_FILES_AND_FOLDERS_ROOT #define LIBMTP_FILES_AND_FOLDERS_ROOT 0xffffffff @@ -84,30 +83,33 @@ typedef struct garmin_device_t { #endif } garmin_device_t; -// Ids can be found at https://developer.garmin.com/connect-iq/device-reference/ -// (look for 'Part Number') - +#define GARMIN_MODEL(name, id, mtp_capable) {name, id, mtp_capable}, const garmin_model_t garmin_models[] = { - { "Descent™ G1 / G1 Solar", 4005, true }, - { "Descent™ G2", 4588, true }, - { "Descent™ Mk1", 2859, false }, - { "Descent™ Mk1 APAC", 2991, false }, - { "Descent™ Mk2(i)", 3258, true }, - { "Descent™ Mk2(i) APAC", 3702, true }, - { "Descent™ Mk2 S", 3542, true }, - { "Descent™ Mk2 S APAC", 3930, true }, - { "Descent™ Mk3(i) 43mm", 4222, true }, - { "Descent™ Mk3(i) 51mm", 4223, true }, - { "Descent™ X50i", 4518, true }, - { "fēnix® 8 43mm", 4534, true }, - { "fēnix® 8 47mm / 51mm / tactix® 8 47mm / 51mm / quatix® 8 47mm / 51mm APAC", 4536, true }, - { "fēnix® 8 47mm / 51mm / tactix® 8 47mm / 51mm / quatix® 8 47mm / 51mm", 4775, true }, - { "fēnix® 8 Pro 47mm / 51mm / MicroLED / quatix® 8 Pro 47mm / 51mm", 4631, true }, - { "fēnix® 8 Solar 47mm", 4532, true }, - { "fēnix® 8 Solar 51mm / tactix® 8 Solar 51mm APAC", 4533, true }, - { "fēnix® 8 Solar 51mm / tactix® 8 Solar 51mm", 4776, true }, + GARMIN_MODEL_LIST(GARMIN_MODEL) { NULL, 0, false } }; +#undef GARMIN_MODEL + +// AI-generated (Claude) +#ifdef HAVE_LIBMTP +static const garmin_model_t *garmin_model_find(unsigned int id) +{ + unsigned int i; + + for (i = 0; garmin_models[i].name; i++) + if (garmin_models[i].id == id) + return &garmin_models[i]; + + return NULL; +} + +static bool garmin_model_is_mtp_capable(unsigned int id) +{ + const garmin_model_t *model = garmin_model_find(id); + + return model && model->mtp_capable; +} +#endif 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); @@ -146,10 +148,8 @@ garmin_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *ios device->model = model; #ifdef HAVE_LIBMTP - // for a Descent Mk2/Mk2i, we have to use MTP to access its storage; - // for Garmin devices, the model number corresponds to the lower three nibbles of the USB product ID - // in order to have only one entry for the Mk2, we don't use the Mk2/APAC model number in our code - device->use_mtp = model == DESCENT_MK2; + // Garmin USB product IDs are 0x4000 | FIT model IDs. + device->use_mtp = garmin_model_is_mtp_capable(model); device->mtp_device = NULL; #endif @@ -428,23 +428,15 @@ mtp_get_file_list(dc_device_t *abstract, struct file_list *files) /* iterate through connected MTP devices */ for (i = 0; i < numrawdevices; i++) { LIBMTP_devicestorage_t *storage; - // we only want to read from a Garmin Descent Mk2 device at this point + // We only read supported Garmin devices at this point. if (rawdevices[i].device_entry.vendor_id != GARMIN_VENDOR) { DEBUG(abstract->context, "Garmin/mtp: skipping raw device %04x/%04x", rawdevices[i].device_entry.vendor_id, rawdevices[i].device_entry.product_id); continue; } - bool mtp_capable = false; - for (unsigned j = 0; garmin_models[j].name; j++) { - if ((garmin_models[j].id | 0x4000) == rawdevices[i].device_entry.product_id) { - mtp_capable = garmin_models[j].mtp_capable; - - break; - } - } - - if (!mtp_capable) { + if (!(rawdevices[i].device_entry.product_id & 0x4000u) || + !garmin_model_is_mtp_capable(rawdevices[i].device_entry.product_id & ~0x4000u)) { DEBUG(abstract->context, "Garmin/mtp: skipping Garmin raw device %04x/%04x, as it is not a dive computer / does not support MTP", rawdevices[i].device_entry.vendor_id, rawdevices[i].device_entry.product_id); continue; diff --git a/src/garmin.h b/src/garmin.h index ca2ca8b4..3924aba4 100644 --- a/src/garmin.h +++ b/src/garmin.h @@ -22,8 +22,6 @@ #ifndef GARMIN_H #define GARMIN_H -#include - #include #include #include @@ -33,20 +31,15 @@ extern "C" { #endif /* __cplusplus */ -typedef struct { - const char *name; - int id; - bool mtp_capable; -} garmin_model_t; - -extern const garmin_model_t garmin_models[]; - dc_status_t garmin_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream, unsigned int model); dc_status_t garmin_parser_create (dc_parser_t **parser, dc_context_t *context, const unsigned char data[], size_t size); +dc_status_t +garmin_parser_get_device_info (dc_parser_t *abstract, dc_event_devinfo_t *devinfo); + // we need to be able to call into the parser to check if the // files that we find are actual dives int diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 8f5f83f2..bc71c2d5 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -25,6 +25,7 @@ #include #include "garmin.h" +#include "garmin-models.h" #include "context-private.h" #include "parser-private.h" #include "array.h" @@ -1662,6 +1663,19 @@ garmin_parser_is_dive (dc_parser_t *abstract, dc_event_devinfo_t *devinfo_p) } } +// AI-generated (Claude) +dc_status_t +garmin_parser_get_device_info (dc_parser_t *abstract, dc_event_devinfo_t *devinfo) +{ + garmin_parser_t *garmin = (garmin_parser_t *) abstract; + + devinfo->firmware = garmin->dive.firmware; + devinfo->serial = garmin->dive.serial; + devinfo->model = garmin->dive.product; + + return DC_STATUS_SUCCESS; +} + static void add_sensor_string(garmin_parser_t *garmin, const char *desc, const struct garmin_sensor *sensor) { dc_field_add_string_fmt(&garmin->cache, desc, "%x", sensor->sensor_id); diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols index d1b78917..42e9c32b 100644 --- a/src/libdivecomputer.symbols +++ b/src/libdivecomputer.symbols @@ -100,6 +100,7 @@ dc_parser_set_atmospheric dc_parser_set_density dc_parser_get_type dc_parser_get_datetime +dc_parser_get_device_info dc_parser_get_field dc_parser_samples_foreach dc_parser_destroy diff --git a/src/parser.c b/src/parser.c index fd4a263b..f975c2d1 100644 --- a/src/parser.c +++ b/src/parser.c @@ -380,6 +380,22 @@ dc_parser_get_datetime (dc_parser_t *parser, dc_datetime_t *datetime) return parser->vtable->datetime (parser, datetime); } +dc_status_t +dc_parser_get_device_info (dc_parser_t *parser, dc_event_devinfo_t *devinfo) +{ + if (parser == NULL || devinfo == NULL) + return DC_STATUS_INVALIDARGS; + + memset (devinfo, 0, sizeof (*devinfo)); + + switch (parser->vtable->type) { + case DC_FAMILY_GARMIN: + return garmin_parser_get_device_info (parser, devinfo); + default: + return DC_STATUS_UNSUPPORTED; + } +} + dc_status_t dc_parser_get_field (dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value) { diff --git a/test/fixtures/manifest.txt b/test/fixtures/manifest.txt index ed0e83ee..0ba8efd9 100644 --- a/test/fixtures/manifest.txt +++ b/test/fixtures/manifest.txt @@ -5,4 +5,4 @@ # the library licence. No GPS or personal location data is present. shearwater_petrel2-0001.bin|Shearwater|Petrel 2 hw_ostc5-0001.bin|Heinrichs Weikamp|OSTC 5 -garmin_descent_mk1-0001.bin|Garmin|Descent Mk1 +garmin_descent_mk1-0001.bin|Garmin|Descent™ Mk1