libdc/src/garmin_parser.c
Michael Keller 39bc2fe05c Import: Clean up gasmix / tank usage.
Separate the usage enums and remove the unneeded values from each.

Signed-off-by: Michael Keller <github@ike.ch>
2024-02-14 12:01:51 +13:00

1743 lines
56 KiB
C

/*
* 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 <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "garmin.h"
#include "context-private.h"
#include "parser-private.h"
#include "array.h"
#include "field-cache.h"
#define MAXFIELDS 160
struct msg_desc;
// Local types
struct type_desc {
const char *msg_name;
const struct msg_desc *msg_desc;
unsigned char nrfields, devfields;
unsigned char fields[MAXFIELDS][3];
};
// Positions are signed 32-bit values, turning
// into 180 * val // 2**31 degrees.
struct pos {
int lat, lon;
};
#define MAX_SENSORS 6
struct garmin_sensor {
unsigned int sensor_id;
const char *sensor_name;
unsigned char sensor_enabled, sensor_units, sensor_used_for_gas_rate;
unsigned int sensor_rated_pressure, sensor_reserve_pressure, sensor_volume;
};
#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;
unsigned int timestamp;
// 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;
// RECORD_SENSOR_PROFILE has no data, fills in dive.sensor[nr_sensor]
// RECORD_TANK_UPDATE
unsigned int sensor, pressure;
// RECORD_SETPOINT_CHANGE
unsigned int setpoint_actual_cbar;
};
#define RECORD_GASMIX 1
#define RECORD_DECO 2
#define RECORD_EVENT 4
#define RECORD_DEVICE_INFO 8
#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;
dc_sample_callback_t callback;
void *userdata;
// Multi-value record data
struct record_data record_data;
struct type_desc type_desc[MAXTYPE];
// Field cache
struct {
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;
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;
unsigned int setpoint_low_switch_mode, setpoint_high_switch_mode;
dc_gasmix_t *current_gasmix;
} dive;
// 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;
unsigned char is_big_endian; // instead of bool
} garmin_parser_t;
typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user);
static inline struct garmin_sensor *current_sensor(garmin_parser_t *garmin)
{
return garmin->dive.sensor + garmin->dive.nr_sensor;
}
static int find_tank_index(garmin_parser_t *garmin, unsigned int sensor_id)
{
for (int i = 0; i < garmin->dive.nr_sensor; i++) {
if (garmin->dive.sensor[i].sensor_id == sensor_id)
return i;
}
return 0;
}
/*
* 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" },
[24] = { 1, "Automatic switch to low setpoint" },
[25] = { 1, "Automatic switch to high setpoint" },
[26] = { 2, "Manual switch to low setpoint" },
[27] = { 2, "Manual switch to high setpoint" },
[32] = { 1, "Tank battery low" }, // No way to know which tank
};
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 (data == 24 || data == 25 || data == 26 || data == 27) {
// Update the actual setpoint used during the dive and report it
garmin->record_data.setpoint_actual_cbar = (data == 24 || data == 26) ? 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);
return;
case 57:
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_sample_value_t sample2 = {0};
sample2.event.type = SAMPLE_EVENT_STRING;
if (gasmix->usage == DC_USAGE_DILUENT) {
sample2.event.name = "Switched to closed circuit";
} else {
sample2.event.name = "Switched to open circuit bailout";
}
sample2.event.flags = 2 << SAMPLE_FLAGS_SEVERITY_SHIFT;
garmin->callback(DC_SAMPLE_EVENT, &sample2, garmin->userdata);
garmin->dive.current_gasmix = gasmix;
}
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->dive.firmware = record->firmware;
garmin->dive.serial = record->serial;
garmin->dive.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);
// End of sensor record just increments nr_sensor,
// so that the next sensor record will start
// filling in the next one.
//
// NOTE! This only happens for tank pods, other
// sensors will just overwrite each other.
//
// Also note that the last sensor is just for
// scratch use, so that the sensor record can
// always fill in dive.sensor[nr_sensor] with
// no checking.
if (pending & RECORD_SENSOR_PROFILE) {
if (garmin->dive.nr_sensor < MAX_SENSORS-1)
garmin->dive.nr_sensor++;
}
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);
}
if (pending & RECORD_TANK_UPDATE) {
dc_sample_value_t sample = {0};
sample.pressure.tank = find_tank_index(garmin, record->sensor);
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);
}
}
static dc_status_t garmin_parser_set_data (garmin_parser_t *garmin, 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,
NULL, /* set_clock */
NULL, /* set_atmospheric */
NULL, /* set_density */
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, const unsigned char data[], size_t size)
{
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, data, size);
if (parser == NULL) {
ERROR (context, "Failed to allocate memory.");
return DC_STATUS_NOMEMORY;
}
garmin_parser_set_data(parser, data, size);
*out = (dc_parser_t *) parser;
return DC_STATUS_SUCCESS;
}
/*
* We really shouldn't use array_uint_be/le, since they
* can't deal with 64-bit types.
*
* But we've not actually seen any yet, so..
*/
static inline unsigned long long garmin_value(struct garmin_parser_t *g, const unsigned char *p, unsigned int type_size)
{
if (g->is_big_endian)
return array_uint_be(p, type_size);
else
return array_uint_le(p, type_size);
}
#define FMTSIZE 64
#define DECLARE_FIT_TYPE(name, ctype, inval, fmt) \
typedef ctype name; \
static const name name##_INVAL = inval; \
static name name##_VALUE(garmin_parser_t *g, const void *p) \
{ return (name) garmin_value(g, p, sizeof(name)); } \
static void name##_FORMAT(name val, char *buf) \
{ snprintf(buf, FMTSIZE, fmt, val); }
DECLARE_FIT_TYPE(ENUM, unsigned char, 0xff, "%u");
DECLARE_FIT_TYPE(UINT8, unsigned char, 0xff, "%u");
DECLARE_FIT_TYPE(UINT16, unsigned short, 0xffff, "%u");
DECLARE_FIT_TYPE(UINT32, unsigned int, 0xffffffff, "%u");
DECLARE_FIT_TYPE(UINT64, unsigned long long, 0xffffffffffffffffull, "%llu");
DECLARE_FIT_TYPE(UINT8Z, unsigned char, 0, "%u");
DECLARE_FIT_TYPE(UINT16Z, unsigned short, 0, "%u");
DECLARE_FIT_TYPE(UINT32Z, unsigned int, 0, "%u");
DECLARE_FIT_TYPE(SINT8, signed char, 0x7f, "%d");
DECLARE_FIT_TYPE(SINT16, signed short, 0x7fff, "%d");
DECLARE_FIT_TYPE(SINT32, signed int, 0x7fffffff, "%d");
DECLARE_FIT_TYPE(SINT64, signed long long, 0x7fffffffffffffffll, "%lld");
DECLARE_FIT_TYPE(FLOAT, unsigned int, 0xffffffff, "%u");
DECLARE_FIT_TYPE(DOUBLE, unsigned long long, 0xffffffffffffffffll, "%llu");
DECLARE_FIT_TYPE(STRING, char *, NULL, "\"%s\"");
// Override string value function - it's the pointer itself
#define STRING_VALUE(g, p) ((char *)(p))
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) \
{ \
char fmtbuf[FMTSIZE]; \
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##_VALUE(g, p); \
if (val == type##_INVAL) return; \
type##_FORMAT(val, fmtbuf); \
DEBUG(g->base.context, "%s (%s): %s", #name, #type, fmtbuf); \
parse_##name(g, val); \
} \
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)
{
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)
return;
data -= garmin->dive.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 * 1000;
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) { }
DECLARE_FIELD(FILE, product_name, STRING) { }
// SESSION msg
DECLARE_FIELD(SESSION, start_time, UINT32) { garmin->dive.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, cadence, UINT8) { } // cadence
DECLARE_FIELD(RECORD, fract_cadence, UINT8) { } // fractional cadence
DECLARE_FIELD(RECORD, distance, UINT32) { } // Distance in 100 * m? WTF?
DECLARE_FIELD(RECORD, speed, UINT16) { } // Speed (m/s?)
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
DECLARE_FIELD(RECORD, air_time_remaining, UINT32) { } // seconds
DECLARE_FIELD(RECORD, pressure_sac, UINT16) { } // 100 * bar/min/pressure
DECLARE_FIELD(RECORD, volume_sac, UINT16) { } // 100 * l/min/pressure
DECLARE_FIELD(RECORD, rmv, UINT16) { } // 100 * l/min
DECLARE_FIELD(RECORD, ascent_rate, SINT32) { } // mm/s (negative is down)
// DEVICE_SETTINGS
DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->dive.utc_offset = (SINT32) data; } // wrong type in FIT
DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->dive.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;
}
// ACTIVITY
DECLARE_FIELD(ACTIVITY, total_timer_time, UINT32) { }
DECLARE_FIELD(ACTIVITY, num_sessions, UINT16) { }
DECLARE_FIELD(ACTIVITY, type, ENUM) { }
DECLARE_FIELD(ACTIVITY, event, ENUM) { }
DECLARE_FIELD(ACTIVITY, event_type, ENUM) { }
DECLARE_FIELD(ACTIVITY, local_timestamp, UINT32)
{
int time_offset = data - garmin->record_data.timestamp;
garmin->dive.time_offset = time_offset;
}
DECLARE_FIELD(ACTIVITY, event_group, UINT8) { }
// SPORT
DECLARE_FIELD(SPORT, sub_sport, ENUM) {
garmin->dive.sub_sport = (ENUM) data;
dc_divemode_t val;
switch (data) {
case 55: val = DC_DIVEMODE_GAUGE;
break;
case 56:
case 57: val = DC_DIVEMODE_FREEDIVE;
break;
case 63: val = DC_DIVEMODE_CCR;
break;
default: val = DC_DIVEMODE_OC;
}
DC_ASSIGN_FIELD(garmin->cache, DIVEMODE, val);
}
// 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;
}
DECLARE_FIELD(DIVE_GAS, type, ENUM)
{
// 0 - open circuit, 1 - CCR diluent
if (data == 1)
garmin->record_data.gasmix.usage = DC_USAGE_DILUENT;
else
garmin->record_data.gasmix.usage = DC_USAGE_OPEN_CIRCUIT;
garmin->record_data.pending |= RECORD_GASMIX;
}
// 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); }
DECLARE_FIELD(DIVE_SUMMARY, avg_pressure_sac, UINT16) { } // 100 * bar/min/pressure
DECLARE_FIELD(DIVE_SUMMARY, avg_volume_sac, UINT16) { } // 100 * L/min/pressure
DECLARE_FIELD(DIVE_SUMMARY, avg_rmv, UINT16) { } // 100 * L/min
// 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, heart_rate_device_type, UINT8) { }
DECLARE_FIELD(DIVE_SETTINGS, setpoint_low_switch_mode, ENUM)
{
// 0 - manual, 1 - auto
garmin->dive.setpoint_low_switch_mode = data;
}
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_switch_mode, ENUM)
{
// 0 - manual, 1 - auto
garmin->dive.setpoint_high_switch_mode = 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.
DECLARE_FIELD(SENSOR_PROFILE, ant_channel_id, UINT32Z)
{
current_sensor(garmin)->sensor_id = data;
}
DECLARE_FIELD(SENSOR_PROFILE, name, STRING) { }
DECLARE_FIELD(SENSOR_PROFILE, enabled, ENUM)
{
current_sensor(garmin)->sensor_enabled = data;
}
DECLARE_FIELD(SENSOR_PROFILE, sensor_type, UINT8)
{
// 28 is tank pod
// start filling in next sensor after this record
if (data == 28)
garmin->record_data.pending |= RECORD_SENSOR_PROFILE;
}
DECLARE_FIELD(SENSOR_PROFILE, pressure_units, ENUM)
{
// 0 is PSI, 1 is KPA (unused), 2 is Bar
current_sensor(garmin)->sensor_units = data;
}
DECLARE_FIELD(SENSOR_PROFILE, rated_pressure, UINT16)
{
current_sensor(garmin)->sensor_rated_pressure = data;
}
DECLARE_FIELD(SENSOR_PROFILE, reserve_pressure, UINT16)
{
current_sensor(garmin)->sensor_reserve_pressure = data;
}
DECLARE_FIELD(SENSOR_PROFILE, volume, UINT16)
{
current_sensor(garmin)->sensor_volume = data;
}
DECLARE_FIELD(SENSOR_PROFILE, used_for_gas_rate, ENUM)
{
current_sensor(garmin)->sensor_used_for_gas_rate = data;
}
DECLARE_FIELD(TANK_UPDATE, sensor, UINT32Z)
{
garmin->record_data.sensor = data;
}
DECLARE_FIELD(TANK_UPDATE, pressure, UINT16)
{
garmin->record_data.pressure = data;
garmin->record_data.pending |= RECORD_TANK_UPDATE;
}
DECLARE_FIELD(TANK_SUMMARY, sensor, UINT32Z) { } // sensor ID
DECLARE_FIELD(TANK_SUMMARY, start_pressure, UINT16) { } // Bar * 100
DECLARE_FIELD(TANK_SUMMARY, end_pressure, UINT16) { } // Bar * 100
DECLARE_FIELD(TANK_SUMMARY, volume_used, UINT32) { } // L * 100
// 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;
}
DECLARE_FIELD(EVENT, tank_pressure_reserve, UINT32Z) { } // sensor ID
DECLARE_FIELD(EVENT, tank_pressure_critical, UINT32Z) { } // sensor ID
DECLARE_FIELD(EVENT, tank_pressure_lost, UINT32Z) { } // sensor ID
// "Field description" (for developer fields)
DECLARE_FIELD(FIELD_DESCRIPTION, data_index, UINT8) { }
DECLARE_FIELD(FIELD_DESCRIPTION, field_definition, UINT8) { }
DECLARE_FIELD(FIELD_DESCRIPTION, base_type, UINT8) { }
DECLARE_FIELD(FIELD_DESCRIPTION, name, STRING) { } // "Depth"
DECLARE_FIELD(FIELD_DESCRIPTION, scale, UINT8) { }
DECLARE_FIELD(FIELD_DESCRIPTION, offset, SINT8) { }
DECLARE_FIELD(FIELD_DESCRIPTION, unit, STRING) { } // "feet"
DECLARE_FIELD(FIELD_DESCRIPTION, original_mesg, UINT16) { }
DECLARE_FIELD(FIELD_DESCRIPTION, original_field, UINT8) { }
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 = 9,
.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),
SET_FIELD(FILE, 8, product_name, STRING),
}
};
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(HRM_PROFILE) = { };
DECLARE_MESG(ZONES_TARGET) = { };
DECLARE_MESG(SPORT) = {
.maxfield = 2,
.field = {
SET_FIELD(SPORT, 1, sub_sport, ENUM), // 53 - 57 and 63 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 = 128,
.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, 4, cadence, UINT8), // cadence
SET_FIELD(RECORD, 5, distance, UINT32), // Distance in 100 * m? WTF?
SET_FIELD(RECORD, 6, speed, UINT16), // m/s? Who knows..
SET_FIELD(RECORD, 13, temperature, SINT8), // degrees C
SET_FIELD(RECORD, 53, fract_cadence, UINT8), // fractional cadence
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
SET_FIELD(RECORD, 123, air_time_remaining, UINT32), // seconds
SET_FIELD(RECORD, 124, pressure_sac, UINT16), // 100 * bar/min/pressure
SET_FIELD(RECORD, 125, volume_sac, UINT16), // 100 * l/min/pressure
SET_FIELD(RECORD, 126, rmv, UINT16), // 100 * l/min
SET_FIELD(RECORD, 127, ascent_rate, SINT32), // mm/s (negative is down)
}
};
DECLARE_MESG(DIVE_GAS) = {
.maxfield = 4,
.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),
SET_FIELD(DIVE_GAS, 3, type, ENUM),
}
};
DECLARE_MESG(DIVE_SUMMARY) = {
.maxfield = 15,
.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
SET_FIELD(DIVE_SUMMARY, 12, avg_pressure_sac, UINT16), // 100 * bar/min/pressure
SET_FIELD(DIVE_SUMMARY, 13, avg_volume_sac, UINT16), // 100 * L/min/pressure
SET_FIELD(DIVE_SUMMARY, 14, avg_rmv, UINT16), // 100 * L/min
}
};
DECLARE_MESG(EVENT) = {
.maxfield = 74,
.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),
SET_FIELD(EVENT, 71, tank_pressure_reserve, UINT32Z), // sensor ID
SET_FIELD(EVENT, 72, tank_pressure_critical, UINT32Z), // sensor ID
SET_FIELD(EVENT, 73, tank_pressure_lost, UINT32Z), // sensor ID
}
};
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) = {
.maxfield = 7,
.field = {
SET_FIELD(ACTIVITY, 0, total_timer_time, UINT32),
SET_FIELD(ACTIVITY, 1, num_sessions, UINT16),
SET_FIELD(ACTIVITY, 2, type, ENUM),
SET_FIELD(ACTIVITY, 3, event, ENUM),
SET_FIELD(ACTIVITY, 4, event_type, ENUM),
SET_FIELD(ACTIVITY, 5, local_timestamp, UINT32),
SET_FIELD(ACTIVITY, 6, event_group, UINT8),
}
};
DECLARE_MESG(FILE_CREATOR) = { };
DECLARE_MESG(DIVE_SETTINGS) = {
.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
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, heart_rate_device_type, UINT8), // device type depending on heart_rate_source_type (ignorable for now)
SET_FIELD(DIVE_SETTINGS, 22, setpoint_low_switch_mode, ENUM), // CCR low setpoint switching mode
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, 25, setpoint_high_switch_mode, ENUM), // CCR high setpoint switching mode
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) = { };
DECLARE_MESG(SENSOR_PROFILE) = {
.maxfield = 79,
.field = {
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, 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),
SET_FIELD(SENSOR_PROFILE, 77, volume, UINT16), // CuFt * 10 (PSI) or L * 10 (Bar)
SET_FIELD(SENSOR_PROFILE, 78, used_for_gas_rate, ENUM), // was used for SAC calculations?
}
};
DECLARE_MESG(TANK_UPDATE) = {
.maxfield = 2,
.field = {
SET_FIELD(TANK_UPDATE, 0, sensor, UINT32Z), // sensor ID
SET_FIELD(TANK_UPDATE, 1, pressure, UINT16), // pressure in Bar * 100
}
};
DECLARE_MESG(TANK_SUMMARY) = {
.maxfield = 4,
.field = {
SET_FIELD(TANK_SUMMARY, 0, sensor, UINT32Z), // sensor ID
SET_FIELD(TANK_SUMMARY, 1, start_pressure, UINT16), // Bar * 100
SET_FIELD(TANK_SUMMARY, 2, end_pressure, UINT16), // Bar * 100
SET_FIELD(TANK_SUMMARY, 3, volume_used, UINT32), // L * 100
}
};
DECLARE_MESG(FIELD_DESCRIPTION) = {
.maxfield = 16,
.field = {
SET_FIELD(FIELD_DESCRIPTION, 0, data_index, UINT8),
SET_FIELD(FIELD_DESCRIPTION, 1, field_definition, UINT8),
SET_FIELD(FIELD_DESCRIPTION, 2, base_type, UINT8),
SET_FIELD(FIELD_DESCRIPTION, 3, name, STRING), // "Depth"
SET_FIELD(FIELD_DESCRIPTION, 6, scale, UINT8),
SET_FIELD(FIELD_DESCRIPTION, 7, offset, SINT8),
SET_FIELD(FIELD_DESCRIPTION, 8, unit, STRING), // "feet"
// Some kind of pointer to original field?
SET_FIELD(FIELD_DESCRIPTION, 14, original_mesg, UINT16),
SET_FIELD(FIELD_DESCRIPTION, 15, original_field, UINT8),
}
};
// 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( 4, HRM_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(147, SENSOR_PROFILE),
SET_MESG(206, FIELD_DESCRIPTION),
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),
SET_MESG(319, TANK_UPDATE),
SET_MESG(323, TANK_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 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;
}
/* Avoid debug printing limit */
len = sizeof(buffer);
}
DEBUG(garmin->base.context, "%s/%d %s '%.*s'", msg_name, field_nr, base_type_info[base_type].type_name, len, 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 total_len + size;
}
if (size < len) {
ERROR(garmin->base.context, "Data traversal size bigger than remaining data (%d vs %d)\n", len, size);
return total_len + size;
}
if (base_type > 16) {
ERROR(garmin->base.context, "Unknown base type %d\n", base_type);
return total_len + size;
}
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;
}
// 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;
}
for (int i = 0; i < desc->devfields; i++) {
int desc_idx = i + desc->nrfields;
const unsigned char *field = desc->fields[desc_idx];
unsigned int field_nr = field[0];
unsigned int len = field[1];
unsigned int type = field[2];
DEBUG(garmin->base.context, "Developer field %d %02x type %02x", i, field_nr, type);
if (!len) {
ERROR(garmin->base.context, " developer field with zero length\n");
return -1;
}
if (size < len) {
ERROR(garmin->base.context, " developer field bigger than remaining data (%d vs %d)\n", len, size);
return -1;
}
HEXDUMP(garmin->base.context, DC_LOGLEVEL_DEBUG, "data", data, 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;
// data[1] tells us if this is big or little endian
garmin->is_big_endian = data[1] != 0;
msg = garmin_value(garmin, data + 2, 2);
desc->msg_desc = lookup_msg_desc(msg, type, &desc->msg_name);
fields = data[4];
DEBUG(garmin->base.context, "Define local type %d: %02x %s %04x %02x %s",
type, data[0], data[1] ? "big-endian" : "little-endian", msg, fields, desc->msg_name);
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;
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]);
}
data += len;
/* Developer fields after the regular ones */
if (record & 0x20) {
devfields = data[0];
DEBUG(garmin->base.context, "Developer field (rec=%02x len=%d, devfields=%d)",
record, len, devfields);
/*
* one byte of dev field numbers, each three bytes in size
* (number/size/index)
*/
len += 1 + 3*devfields;
for (int i = 0; i < devfields; i++) {
int idx = fields + i;
unsigned char *field = desc->fields[idx];
if (idx > MAXFIELDS) {
ERROR(garmin->base.context, "Too many dev fields in description: %d+%d (max %d)\n", fields, i, MAXFIELDS);
return -1;
}
memcpy(field, data + (1+i*3), 3);
DEBUG(garmin->base.context, " %d: %02x %02x %02x", i, field[0], field[1], field[2]);
}
}
desc->devfields = devfields;
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", FIT_NAME_SIZE, 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); // these two fields are always little endian
datasize = array_uint32_le(data+4);
if (memcmp(data+8, ".FIT", 4)) {
DEBUG(garmin->base.context, " missing .FIT marker");
return DC_STATUS_IO;
}
if (hdrsize < 12 || datasize > len || datasize + hdrsize + 2 > len) {
DEBUG(garmin->base.context, " inconsistent size information hdrsize %d datasize %d len %d", hdrsize, datasize, len);
return DC_STATUS_IO;
}
garmin->dive.protocol = protocol;
garmin->dive.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;
// 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);
len = traverse_regular(garmin, data, datasize, type, &time);
} else if (record & 0x40) { // Definition record?
len = traverse_definition(garmin, data, datasize, record);
} else { // Normal data record
DEBUG(garmin->base.context, "Regular record for type %d", 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, dc_event_devinfo_t *devinfo_p)
{
garmin_parser_t *garmin = (garmin_parser_t *) abstract;
if (devinfo_p) {
devinfo_p->firmware = garmin->dive.firmware;
devinfo_p->serial = garmin->dive.serial;
devinfo_p->model = garmin->dive.product;
}
switch (garmin->dive.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 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);
}
static dc_status_t
garmin_parser_set_data (garmin_parser_t *garmin, const unsigned char *data, unsigned int size)
{
/* Walk the data once without a callback to set up the core fields */
garmin->callback = NULL;
garmin->userdata = NULL;
memset(&garmin->gps, 0, sizeof(garmin->gps));
memset(&garmin->dive, 0, sizeof(garmin->dive));
memset(&garmin->cache, 0, sizeof(garmin->cache));
traverse_data(garmin);
// Device information
if (garmin->dive.serial)
dc_field_add_string_fmt(&garmin->cache, "Serial", "%u", garmin->dive.serial);
if (garmin->dive.firmware)
dc_field_add_string_fmt(&garmin->cache, "Firmware", "%u.%02u",
garmin->dive.firmware / 100, garmin->dive.firmware % 100);
// 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);
// We have no idea about gas mixes vs tanks
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, ..);
}
// Hate hate hate gasmix vs tank counts.
//
// There's no way to match them up unless they are an identity
// mapping, so having two different ones doesn't actually work.
if (garmin->dive.nr_sensor > garmin->cache.GASMIX_COUNT)
DC_ASSIGN_FIELD(garmin->cache, GASMIX_COUNT, garmin->dive.nr_sensor);
for (int i = 0; i < garmin->dive.nr_sensor; i++) {
static const char *name[] = { "Sensor 1", "Sensor 2", "Sensor 3", "Sensor 4", "Sensor 5" };
add_sensor_string(garmin, name[i], garmin->dive.sensor+i);
}
if (garmin->cache.DIVEMODE == DC_DIVEMODE_CCR) {
dc_field_add_string_fmt(&garmin->cache, "Setpoint low [bar]", "%u.%02u",
garmin->dive.setpoint_low_cbar / 100, (garmin->dive.setpoint_low_cbar % 100));
dc_field_add_string(&garmin->cache, "Setpoint low mode", garmin->dive.setpoint_low_switch_mode ? "auto" : "manual");
if (garmin->dive.setpoint_low_switch_mode) {
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 [bar]", "%u.%02u",
garmin->dive.setpoint_high_cbar / 100, (garmin->dive.setpoint_high_cbar % 100));
dc_field_add_string(&garmin->cache, "Setpoint high mode", garmin->dive.setpoint_high_switch_mode ? "auto" : "manual");
if (garmin->dive.setpoint_high_switch_mode) {
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;
}
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->dive.time;
int timezone = DC_TIMEZONE_NONE;
// Show local time (time_offset)
dc_datetime_gmtime(datetime, time + garmin->dive.time_offset);
/* See if we might have a valid timezone offset */
if (garmin->dive.time_offset || garmin->dive.utc_offset) {
int offset = garmin->dive.time_offset - garmin->dive.utc_offset;
/* 15-minute (900-second) offsets are real */
if ((offset % 900) == 0 &&
offset >= -12*60*60 &&
offset <= 14*60*60)
timezone = offset;
}
datetime->timezone = timezone;
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;
return dc_field_get(&garmin->cache, type, flags, value);
}
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);
}