From 1991fc8acdd06383954193c8847010c534a15bf7 Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Wed, 14 Oct 2026 01:07:48 +1300 Subject: [PATCH 1/2] garmin: report per-tank gas-consumption usage via DC_FIELD_TANK Garmin FIT logs can mark each tank-pod sensor's SENSOR_PROFILE field 78 ("used for SAC calculations?") to say whether the dive computer counts that tank toward the diver's own gas consumption. The parser decoded the field into garmin_sensor.sensor_used_for_gas_rate but never exposed it, and DC_FIELD_TANK was not populated for Garmin at all. The field's meaning is confirmed by Garmin's own Descent T1/T2 manuals (the transceiver "SAC/RMV/ATR" setting) and independently by the public FIT file attached to subsurface#4465: field 78 is 1 on the reporter's own tank and 0 on a second, watched sensor that carries a different user-assigned transmitter name, consistent with both pressure traces. This is a single dive on Mk2i/Mk3i-era firmware and has not been verified across other Garmin dive computer models. Add DC_TANK_USAGE_INCLUDED/DC_TANK_USAGE_EXCLUDED to dc_tank_usage_t, documented in terms of gas consumption rather than ownership, with DC_TANK_USAGE_NONE kept as the 0/default value. Storage has to be tri-state: the field-decoding machinery drops absent/invalid (0xff) values before the handler runs, and the sensor struct is memset to zero per dive, so a raw 0 is ambiguous between "device said not counted" and "device said nothing". Track a separate sensor_used_for_gas_rate_valid flag so a log that omits field 78 reports DC_TANK_USAGE_NONE, never EXCLUDED. The per-tank index matches the one already used for DC_SAMPLE_PRESSURE (both walk garmin->dive.sensor[]). Volume/type/workpressure are left at their defaults; populating them from TANK_SUMMARY is a separate concern. Also make field-cache.c's generic DC_FIELD_TANK case set usage from the cache and explicitly default beginpressure/endpressure to 0, so a cache-backed backend can never return stale caller memory for those members. No other backend currently reaches this generic path for DC_FIELD_TANK without first overriding usage/pressures itself, so this does not change their behaviour. Signed-off-by: Michael Keller --- include/libdivecomputer/parser.h | 8 +++++++- src/field-cache.c | 3 +++ src/garmin_parser.c | 35 +++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/include/libdivecomputer/parser.h b/include/libdivecomputer/parser.h index 5c36ab96..9df2b91f 100644 --- a/include/libdivecomputer/parser.h +++ b/include/libdivecomputer/parser.h @@ -230,8 +230,14 @@ typedef unsigned int dc_tankinfo_t; */ typedef enum dc_tank_usage_t { - DC_TANK_USAGE_NONE, + DC_TANK_USAGE_NONE, /* Not specified, or unknown. */ DC_TANK_USAGE_SIDEMOUNT, + DC_TANK_USAGE_INCLUDED, /* The dive computer counts this tank towards + the diver's own gas consumption. */ + DC_TANK_USAGE_EXCLUDED, /* The dive computer does not count this tank + towards the diver's own gas consumption + (for example a buddy's or a stage/deco + bottle that is tracked but not included). */ } dc_tank_usage_t; typedef struct dc_tank_t { diff --git a/src/field-cache.c b/src/field-cache.c index c003c7d6..877fdd82 100644 --- a/src/field-cache.c +++ b/src/field-cache.c @@ -124,6 +124,9 @@ dc_field_get(dc_field_cache_t *cache, dc_field_type_t type, unsigned int flags, tank->gasmix = flags; tank->workpressure = cache->tankworkingpressure[flags]; tank->type = cache->tankinfo[flags]; + tank->usage = cache->tankusage[flags]; + tank->beginpressure = 0; + tank->endpressure = 0; return DC_STATUS_SUCCESS; case DC_FIELD_STRING: diff --git a/src/garmin_parser.c b/src/garmin_parser.c index bc71c2d5..6169553a 100644 --- a/src/garmin_parser.c +++ b/src/garmin_parser.c @@ -54,6 +54,13 @@ struct garmin_sensor { unsigned int sensor_id; const char *sensor_name; unsigned char sensor_enabled, sensor_units, sensor_used_for_gas_rate; + // SENSOR_PROFILE field 78 (used_for_gas_rate) is only decoded when + // the log actually contains it: the field-decoding machinery drops + // absent/invalid (0xff) values before the handler runs, and this + // whole struct is memset to zero otherwise. So a raw 0 is ambiguous + // between "device said not counted" and "device said nothing", and + // we need this separate flag to tell the two apart. + unsigned char sensor_used_for_gas_rate_valid; unsigned int sensor_rated_pressure, sensor_reserve_pressure, sensor_volume; }; @@ -855,7 +862,9 @@ DECLARE_FIELD(SENSOR_PROFILE, volume, UINT16) } DECLARE_FIELD(SENSOR_PROFILE, used_for_gas_rate, ENUM) { - current_sensor(garmin)->sensor_used_for_gas_rate = data; + struct garmin_sensor *sensor = current_sensor(garmin); + sensor->sensor_used_for_gas_rate = data; + sensor->sensor_used_for_gas_rate_valid = 1; } DECLARE_FIELD(TANK_UPDATE, sensor, UINT32Z) @@ -1725,12 +1734,28 @@ garmin_parser_set_data (garmin_parser_t *garmin, const unsigned char *data, unsi add_gps_string(garmin, "Record GPS", &garmin->gps.RECORD); - // We have no idea about gas mixes vs tanks + // We have no idea about gas mixes vs tanks, so for now the only + // DC_FIELD_TANK member we report is "usage": whether the dive + // computer counted this tank pod's gas towards the diver's own + // consumption (SENSOR_PROFILE field 78, "used for SAC calculations"). + // Volume/type/workpressure are left at their field-cache defaults. + // + // The index here matches the one used for DC_SAMPLE_PRESSURE + // (see find_tank_index()): both walk garmin->dive.sensor[]. for (int i = 0; i < garmin->dive.nr_sensor; i++) { - // DC_ASSIGN_IDX(garmin->cache, tankinfo, i, ..); - // DC_ASSIGN_IDX(garmin->cache, tanksize, i, ..); - // DC_ASSIGN_IDX(garmin->cache, tankworkingpressure, i, ..); + const struct garmin_sensor *sensor = garmin->dive.sensor + i; + dc_tank_usage_t usage = DC_TANK_USAGE_NONE; + + // Field 78 is only decoded when it is actually present in the + // log; a sensor that never saw it must stay NONE here, never + // EXCLUDED. + if (sensor->sensor_used_for_gas_rate_valid) + usage = sensor->sensor_used_for_gas_rate ? DC_TANK_USAGE_INCLUDED : DC_TANK_USAGE_EXCLUDED; + + garmin->cache.tankusage[i] = usage; } + if (garmin->dive.nr_sensor) + garmin->cache.initialized |= 1u << DC_FIELD_TANK; // Hate hate hate gasmix vs tank counts. // From 590d5854efc2a33c16f56e0106e0c5475770b26e Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Wed, 14 Oct 2026 06:38:07 +1300 Subject: [PATCH 2/2] examples: fix tank usage XML labels to match dc_tank_usage_t The tank-reporting block in output_xml.c reused the gas-mix usage[] array ({"none", "oxygen", "diluent", "sidemount"}), which maps dc_usage_t (NONE/OXYGEN/DILUENT/OPEN_CIRCUIT). dc_tank_usage_t has a different enumerator order (NONE/SIDEMOUNT/INCLUDED/EXCLUDED), so a tank with usage DC_TANK_USAGE_SIDEMOUNT was printed as oxygen, and DC_TANK_USAGE_INCLUDED/EXCLUDED had no matching label at all. Add a tank-specific usage[] array distinct from the gas-mix one, so the XML output names tank usage correctly. The gas-mix block's own dc_usage_t mapping is unchanged. Signed-off-by: Michael Keller --- examples/output_xml.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/output_xml.c b/examples/output_xml.c index 72ab8181..5bc074e2 100644 --- a/examples/output_xml.c +++ b/examples/output_xml.c @@ -414,7 +414,7 @@ dctool_xml_output_write (dctool_output_t *abstract, dc_parser_t *parser, const u tank.gasmix); } if (tank.usage) { - const char *usage[] = {"none", "oxygen", "diluent", "sidemount"}; + const char *usage[] = {"none", "sidemount", "included", "excluded"}; fprintf (output->ostream, " %s\n", usage[tank.usage]);