Added parsing of the CCR setpoint information for Garmin Descent computers.

This is adding support for parsing the setpoint information in logfiles downloaded from Garmin Descent devices.

The Garmin devices do not have support for ppO2 sensor input, so they only work in 'fixed setpoint' mode for CCR dives, and dive data records do not contain actual ppO2 values.
The ppO2 values are retrofitted to the dive data based on 'setpoint change' events reported by the device. With this change CCR dives downloaded from a Garmin device are correctly classified as CCR dive, and the calculated ceiling / tissue loading graphs are accurate and match the deco stops reported by the device.
Before this change, CCR dives were classified as open circuit dives, often resulting in a massively overstated calculated ceiling.

This has been tested for logs with only automated setpoint changes - more test dives are needed to reverse engineer the log file format for manual setpoint changes, as the setpoint fields are not documented in Garmin's documentation for the FIT file format.

Signed-off-by: Michael Keller <github@ike.ch>
This commit is contained in:
Michael Keller 2022-11-24 11:13:08 +13:00
parent 28c27e2392
commit 1e47f597fa
3 changed files with 58 additions and 5 deletions

View File

@ -449,6 +449,7 @@ 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 */
{"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},

View File

@ -9,7 +9,7 @@
* The field cache 'string' interface has some simple rules:
* the "descriptor" part is assumed to be a static allocation,
* while the "value" is something that this interface will
* alway sallocate with 'strdup()', so you can generate it
* always allocate with 'strdup()', so you can generate it
* dynamically on the stack or whatever without having to
* worry about it.
*/

View File

@ -88,6 +88,9 @@ struct record_data {
// RECORD_TANK_UPDATE
unsigned int sensor, pressure;
// RECORD_SETPOINT_CHANGE
unsigned int setpoint_actual_cbar;
};
#define RECORD_GASMIX 1
@ -97,6 +100,7 @@ struct record_data {
#define RECORD_DECO_MODEL 16
#define RECORD_SENSOR_PROFILE 32
#define RECORD_TANK_UPDATE 64
#define RECORD_SETPOINT_CHANGE 128
typedef struct garmin_parser_t {
dc_parser_t base;
@ -121,6 +125,8 @@ typedef struct garmin_parser_t {
int utc_offset, time_offset;
unsigned int nr_sensor;
struct garmin_sensor sensor[MAX_SENSORS];
unsigned int setpoint_low_cbar, setpoint_high_cbar;
unsigned int setpoint_low_switch_depth_mm, setpoint_high_switch_depth_mm;
} dive;
// I count nine (!) different GPS fields Hmm.
@ -194,6 +200,8 @@ static void garmin_event(struct garmin_parser_t *garmin,
[21] = { 3, "Battry Critical" },
[22] = { 1, "Safety stop begin" },
[23] = { 1, "Approaching deco stop" },
[24] = { 1, "Switched to low setpoint" },
[25] = { 1, "Switched to high setpoint" },
[32] = { 1, "Tank battery low" }, // No way to know which tank
};
dc_sample_value_t sample = {0};
@ -210,6 +218,13 @@ static void garmin_event(struct garmin_parser_t *garmin,
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 (data == 24 || data == 25) {
// Update the actual setpoint used during the dive and report it
garmin->record_data.setpoint_actual_cbar = data == 24 ? garmin->dive.setpoint_low_cbar : garmin->dive.setpoint_high_cbar;
garmin->record_data.pending |= RECORD_SETPOINT_CHANGE;
}
if (!sample.event.name)
return;
garmin->callback(DC_SAMPLE_EVENT, sample, garmin->userdata);
@ -292,6 +307,13 @@ static void flush_pending_record(struct garmin_parser_t *garmin)
sample.pressure.value = record->pressure / 100.0;
garmin->callback(DC_SAMPLE_PRESSURE, sample, garmin->userdata);
}
if (pending & RECORD_SETPOINT_CHANGE) {
dc_sample_value_t sample = {0};
sample.setpoint = record->setpoint_actual_cbar / 100.0;
garmin->callback(DC_SAMPLE_SETPOINT, sample, garmin->userdata);
}
}
@ -671,7 +693,27 @@ 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) { }
DECLARE_FIELD(DIVE_SETTINGS, heart_rate_device_type, UINT8) { }
DECLARE_FIELD(DIVE_SETTINGS, setpoint_low_cbar, UINT8)
{
garmin->dive.setpoint_low_cbar = data;
// The initial setpoint at the start of the dive is the low setpoint
garmin->record_data.setpoint_actual_cbar = garmin->dive.setpoint_low_cbar;
garmin->record_data.pending |= RECORD_SETPOINT_CHANGE;
}
DECLARE_FIELD(DIVE_SETTINGS, setpoint_low_switch_depth_mm, UINT32)
{
garmin->dive.setpoint_low_switch_depth_mm = data;
}
DECLARE_FIELD(DIVE_SETTINGS, setpoint_high_cbar, UINT8)
{
garmin->dive.setpoint_high_cbar = data;
}
DECLARE_FIELD(DIVE_SETTINGS, setpoint_high_switch_depth_mm, UINT32)
{
garmin->dive.setpoint_high_switch_depth_mm = data;
}
// SENSOR_PROFILE record for each ANT/BLE sensor.
// We only care about sensor type 28 - Garmin tank pod.
@ -793,7 +835,7 @@ DECLARE_MESG(ZONES_TARGET) = { };
DECLARE_MESG(SPORT) = {
.maxfield = 2,
.field = {
SET_FIELD(SPORT, 1, sub_sport, ENUM), // 53 - 57 are dive activities
SET_FIELD(SPORT, 1, sub_sport, ENUM), // 53 - 57 and 63 are dive activities
}
};
@ -909,7 +951,7 @@ DECLARE_MESG(ACTIVITY) = { };
DECLARE_MESG(FILE_CREATOR) = { };
DECLARE_MESG(DIVE_SETTINGS) = {
.maxfield = 21,
.maxfield = 28,
.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
@ -931,7 +973,11 @@ DECLARE_MESG(DIVE_SETTINGS) = {
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)
SET_FIELD(DIVE_SETTINGS, 20, heart_rate_device_type, UINT8), // device type depending on heart_rate_source_type (ignorable for now)
SET_FIELD(DIVE_SETTINGS, 23, setpoint_low_cbar, UINT8), // CCR low setpoint [centibar]
SET_FIELD(DIVE_SETTINGS, 24, setpoint_low_switch_depth_mm, UINT32), // CCR low setpoint switch depth [mm]
SET_FIELD(DIVE_SETTINGS, 26, setpoint_high_cbar, UINT8), // CCR high setpoint [centibar]
SET_FIELD(DIVE_SETTINGS, 27, setpoint_high_switch_depth_mm, UINT32), // CCR high setpoint switch depth [mm]
}
};
DECLARE_MESG(DIVE_ALARM) = { };
@ -1469,6 +1515,12 @@ garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsign
add_sensor_string(garmin, name[i], garmin->dive.sensor+i);
}
dc_field_add_string_fmt(&garmin->cache, "Setpoint low auto switch depth [m]", "%u.%01u",
garmin->dive.setpoint_low_switch_depth_mm / 1000, (garmin->dive.setpoint_low_switch_depth_mm % 1000) / 100);
dc_field_add_string_fmt(&garmin->cache, "Setpoint high auto switch depth [m]", "%u.%01u",
garmin->dive.setpoint_high_switch_depth_mm / 1000, (garmin->dive.setpoint_high_switch_depth_mm % 1000) / 100);
return DC_STATUS_SUCCESS;
}