From 792090566e98d93a443ace28dc0c3978fd684ce8 Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Fri, 19 Apr 2024 15:29:28 +1200 Subject: [PATCH 1/2] Garmin: Add Support for Descent Mk3(i). Add support for the Garmin Descent Mk3(i) models. This pretty much just updates the entry in the device list to cover Mk2 and Mk3 models. It does nothing to add support for MTP when reading from an Mk3 model - I have not been able to get MTP support on linux going reliably, and I do not have an Mk2 / Mk3 to test with anyway. I suspect that MTP support is incomplete anyway, as the product IDs for models like the Mk2S and Mk2G are not detected. This change also adds an extra info field for the Model (based on a heuristic and incomplete list). It also fixes the data type for the sensor_type field. Signed-off-by: Michael Keller --- src/descriptor.c | 7 ++++--- src/garmin.c | 9 ++++++--- src/garmin_parser.c | 29 +++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index 7a824dc..6a83f28 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -471,10 +471,11 @@ static const dc_descriptor_t g_descriptors[] = { // 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 - the APAC model is 2991 */ - /* for the Mk2 we are using the model of the global model - the APAC model is 3702 */ + /* 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, dc_filter_garmin}, - {"Garmin", "Descent Mk2/Mk2i", DC_FAMILY_GARMIN, 3258, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin}, + {"Garmin", "Descent Mk2(i)/Mk3(i)", DC_FAMILY_GARMIN, 3258, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin}, {"FIT", "File import", DC_FAMILY_GARMIN, 0, DC_TRANSPORT_USBSTORAGE, NULL }, }; diff --git a/src/garmin.c b/src/garmin.c index f51e19a..16c7464 100644 --- a/src/garmin.c +++ b/src/garmin.c @@ -101,7 +101,6 @@ garmin_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *ios // 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 == (0x0FFF & DESCENT_MK2)); device->mtp_device = NULL; - DEBUG(context, "Found Garmin with model 0x%x which is a %s\n", model, (device->use_mtp ? "Mk2/Mk2i" : "Mk1")); #endif *out = (dc_device_t *) device; @@ -331,12 +330,16 @@ mtp_get_file_list(dc_device_t *abstract, struct file_list *files) for (i = 0; i < numrawdevices; i++) { LIBMTP_devicestorage_t *storage; // we only want to read from a Garmin Descent Mk2 device at this point - if (rawdevices[i].device_entry.vendor_id != GARMIN_VENDOR || - (rawdevices[i].device_entry.product_id != DESCENT_MK2 && rawdevices[i].device_entry.product_id != DESCENT_MK2_APAC)) { + 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; } + if (rawdevices[i].device_entry.product_id != DESCENT_MK2 && rawdevices[i].device_entry.product_id != DESCENT_MK2_APAC) { + 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; + } device->mtp_device = LIBMTP_Open_Raw_Device_Uncached(&rawdevices[i]); if (device->mtp_device == NULL) { DEBUG(abstract->context, "Garmin/mtp: unable to open raw device %d", i); diff --git a/src/garmin_parser.c b/src/garmin_parser.c index c2be625..7c4cff5 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -797,7 +797,7 @@ DECLARE_FIELD(SENSOR_PROFILE, enabled, ENUM) { current_sensor(garmin)->sensor_enabled = data; } -DECLARE_FIELD(SENSOR_PROFILE, sensor_type, UINT8) +DECLARE_FIELD(SENSOR_PROFILE, sensor_type, ENUM) { // 28 is tank pod // start filling in next sensor after this record @@ -1090,7 +1090,7 @@ DECLARE_MESG(SENSOR_PROFILE) = { SET_FIELD(SENSOR_PROFILE, 0, ant_channel_id, UINT32Z), // derived from the number engraved on the side SET_FIELD(SENSOR_PROFILE, 2, name, STRING), SET_FIELD(SENSOR_PROFILE, 3, enabled, ENUM), - SET_FIELD(SENSOR_PROFILE, 52, sensor_type, UINT8), // 28 is tank pod + SET_FIELD(SENSOR_PROFILE, 52, sensor_type, ENUM), // 28 is tank pod SET_FIELD(SENSOR_PROFILE, 74, pressure_units, ENUM), // 0 is PSI, 1 is KPA (unused), 2 is Bar SET_FIELD(SENSOR_PROFILE, 75, rated_pressure, UINT16), SET_FIELD(SENSOR_PROFILE, 76, reserve_pressure, UINT16), @@ -1615,6 +1615,20 @@ static void add_sensor_string(garmin_parser_t *garmin, const char *desc, const s static dc_status_t garmin_parser_set_data (garmin_parser_t *garmin, const unsigned char *data, unsigned int size) { + // This list is empirical and somewhat speculative + // will have to be confirmed with Garmin + static const struct { + int id; + const char *name; + } models[] = { + { 2859, "Descent Mk1" }, + { 2991, "Descent Mk1 APAC" }, + { 3258, "Descent Mk2(i)" }, + { 3542, "Descent Mk2s" }, + { 3702, "Descent Mk2 APAC" }, + { 4223, "Descent Mk3" }, + }; + /* Walk the data once without a callback to set up the core fields */ garmin->callback = NULL; garmin->userdata = NULL; @@ -1630,6 +1644,17 @@ garmin_parser_set_data (garmin_parser_t *garmin, const unsigned char *data, unsi if (garmin->dive.firmware) dc_field_add_string_fmt(&garmin->cache, "Firmware", "%u.%02u", garmin->dive.firmware / 100, garmin->dive.firmware % 100); + if (garmin->dive.product) { + int i = 0; + for (i = 0; i < C_ARRAY_SIZE(models); i++) + if (models[i].id == garmin->dive.product) + break; + + if (i < C_ARRAY_SIZE(models)) + dc_field_add_string_fmt(&garmin->cache, "Model", "%s", models[i].name); + else + dc_field_add_string_fmt(&garmin->cache, "Model", "Unknown model ID: %u", garmin->dive.product); + } // These seem to be the "real" GPS dive coordinates add_gps_string(garmin, "GPS1", &garmin->gps.SESSION.entry); From bb502c6d8b53153b6f43469f55f290e470efc105 Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Fri, 26 Apr 2024 01:02:09 +1200 Subject: [PATCH 2/2] Garmin: Fixes to Add Support .fit format version 2.1. Ignore timestamps on messages that we do not read any other fields from - with the new (firmware >= 16.x) .fit file format these messages seem to be sent out of order, causing other messages to be ignored if they come after them in the file but are before them in the timeline. Track the OC / CCR state at the beginning of the dive based on the dive type - this avoids spurious 'switched to ...' messages caused by new 'gas switch' messages at the beginning of dives. See also https://forums.garmin.com/developer/fit-sdk/b/news-announcements/posts/important-fit-activity-file-message-change Signed-off-by: Michael Keller --- src/garmin_parser.c | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/garmin_parser.c b/src/garmin_parser.c index 7c4cff5..543346b 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -129,7 +129,7 @@ typedef struct garmin_parser_t { unsigned int setpoint_low_cbar, setpoint_high_cbar; unsigned int setpoint_low_switch_depth_mm, setpoint_high_switch_depth_mm; unsigned int setpoint_low_switch_mode, setpoint_high_switch_mode; - dc_gasmix_t *current_gasmix; + dc_usage_t current_gasmix_usage; } dive; // I count nine (!) different GPS fields Hmm. @@ -239,11 +239,11 @@ static void garmin_event(struct garmin_parser_t *garmin, sample.gasmix = data; garmin->callback(DC_SAMPLE_GASMIX, &sample, garmin->userdata); - dc_gasmix_t *gasmix = &garmin->cache.GASMIX[data]; - if (!garmin->dive.current_gasmix || gasmix->usage != garmin->dive.current_gasmix->usage) { + dc_usage_t gasmix_usage = garmin->cache.GASMIX[data].usage; + if (gasmix_usage != garmin->dive.current_gasmix_usage) { dc_sample_value_t sample2 = {0}; sample2.event.type = SAMPLE_EVENT_STRING; - if (gasmix->usage == DC_USAGE_DILUENT) { + if (gasmix_usage == DC_USAGE_DILUENT) { sample2.event.name = "Switched to closed circuit"; } else { sample2.event.name = "Switched to open circuit bailout"; @@ -252,7 +252,7 @@ static void garmin_event(struct garmin_parser_t *garmin, garmin->callback(DC_SAMPLE_EVENT, &sample2, garmin->userdata); - garmin->dive.current_gasmix = gasmix; + garmin->dive.current_gasmix_usage = gasmix_usage; } return; @@ -486,19 +486,22 @@ DECLARE_FIELD(ANY, timestamp, UINT32) { garmin->record_data.timestamp = data; if (garmin->callback) { - dc_sample_value_t sample = {0}; - // Turn the timestamp relative to the beginning of the dive - if (data < garmin->dive.time) + if (data < garmin->dive.time) { + DEBUG(garmin->base.context, "Timestamp before dive start: %d (dive start: %d)", data, garmin->dive.time); + return; - data -= garmin->dive.time; + } + data -= garmin->dive.time - 1; // Did we already do this? - if (data < garmin->record_data.time) + if (data == garmin->record_data.time) return; + garmin->record_data.time = data; + // Now we're ready to actually update the sample times - garmin->record_data.time = data+1; + dc_sample_value_t sample = {0}; sample.time = data * 1000; garmin->callback(DC_SAMPLE_TIME, &sample, garmin->userdata); } @@ -656,6 +659,7 @@ DECLARE_FIELD(ACTIVITY, event_group, UINT8) { } // SPORT DECLARE_FIELD(SPORT, sub_sport, ENUM) { garmin->dive.sub_sport = (ENUM) data; + garmin->dive.current_gasmix_usage = DC_USAGE_OPEN_CIRCUIT; dc_divemode_t val; switch (data) { case 55: val = DC_DIVEMODE_GAUGE; @@ -663,7 +667,10 @@ DECLARE_FIELD(SPORT, sub_sport, ENUM) { case 56: case 57: val = DC_DIVEMODE_FREEDIVE; break; - case 63: val = DC_DIVEMODE_CCR; + case 63: + val = DC_DIVEMODE_CCR; + garmin->dive.current_gasmix_usage = DC_USAGE_DILUENT; + break; default: val = DC_DIVEMODE_OC; } @@ -1337,7 +1344,10 @@ static int traverse_regular(struct garmin_parser_t *garmin, } if (field_desc) { - field_desc->parse(garmin, base_type, data); + if (field_nr == 253 && !msg_desc->maxfield) + DEBUG(garmin->base.context, "Ignoring timestamp field for undefined message."); + else + field_desc->parse(garmin, base_type, data); } else { unknown_field(garmin, data, msg_name, field_nr, base_type, len); } @@ -1521,7 +1531,12 @@ traverse_data(struct garmin_parser_t *garmin) // Compressed records are like normal records // with that added relative timestamp DEBUG(garmin->base.context, "Compressed record for type %d", type); - parse_ANY_timestamp(garmin, time); + + if (!(garmin->type_desc + type)->msg_desc->maxfield) + DEBUG(garmin->base.context, "Ignoring timestamp field for undefined message."); + else + parse_ANY_timestamp(garmin, time); + len = traverse_regular(garmin, data, datasize, type, &time); } else if (record & 0x40) { // Definition record? len = traverse_definition(garmin, data, datasize, record);