This converts most of the cached data to the field cache, leaving some garmin-specific fields alone (but removing them from the "cache" structure in the process). This means that all of the users of the string fields have been converted, and we don't have the duplicate string interfaces any more. Some of the other "dc_field_cache_t" fields could easily be used by other backends (including some of the partial conversions like the Shearwater one, but also backends that don't do any string fields at all), but this conversion was a fairly minimal "set up the infrastructure, and convert the easy parts". Considering that the string field stuff still isn't upstream, I'm not going to push any other backends to do more conversions. On the whole, the string code de-duplication was a fairly nice cleanup: 8 files changed, 340 insertions(+), 484 deletions(-) and perhaps more importantly will make it easier to do new backends in the future with smaller diffs against upstream. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
1319 lines
40 KiB
C
1319 lines
40 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 C_ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a)))
|
|
|
|
#define MAXFIELDS 128
|
|
|
|
struct msg_desc;
|
|
|
|
// Local types
|
|
struct type_desc {
|
|
const char *msg_name;
|
|
const struct msg_desc *msg_desc;
|
|
unsigned char nrfields;
|
|
unsigned char fields[MAXFIELDS][3];
|
|
};
|
|
|
|
// Positions are signed 32-bit values, turning
|
|
// into 180 * val // 2**31 degrees.
|
|
struct pos {
|
|
int lat, lon;
|
|
};
|
|
|
|
#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;
|
|
|
|
// 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;
|
|
};
|
|
|
|
#define RECORD_GASMIX 1
|
|
#define RECORD_DECO 2
|
|
#define RECORD_EVENT 4
|
|
#define RECORD_DEVICE_INFO 8
|
|
#define RECORD_DECO_MODEL 16
|
|
|
|
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
|
|
unsigned int initialized;
|
|
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;
|
|
|
|
// 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;
|
|
} garmin_parser_t;
|
|
|
|
typedef int (*garmin_data_cb_t)(unsigned char type, const unsigned char *data, int len, void *user);
|
|
|
|
/*
|
|
* 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" },
|
|
};
|
|
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 (!sample.event.name)
|
|
return;
|
|
garmin->callback(DC_SAMPLE_EVENT, sample, garmin->userdata);
|
|
return;
|
|
|
|
case 57:
|
|
sample.gasmix = data - 1;
|
|
garmin->callback(DC_SAMPLE_GASMIX, sample, garmin->userdata);
|
|
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->firmware = record->firmware;
|
|
garmin->serial = record->serial;
|
|
garmin->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);
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
static dc_status_t garmin_parser_set_data (dc_parser_t *abstract, 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,
|
|
garmin_parser_set_data, /* set_data */
|
|
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)
|
|
{
|
|
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);
|
|
if (parser == NULL) {
|
|
ERROR (context, "Failed to allocate memory.");
|
|
return DC_STATUS_NOMEMORY;
|
|
}
|
|
|
|
*out = (dc_parser_t *) parser;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
#define DECLARE_FIT_TYPE(name, ctype, inval) \
|
|
typedef ctype name; \
|
|
static const name name##_INVAL = inval
|
|
|
|
DECLARE_FIT_TYPE(ENUM, unsigned char, 0xff);
|
|
DECLARE_FIT_TYPE(UINT8, unsigned char, 0xff);
|
|
DECLARE_FIT_TYPE(UINT16, unsigned short, 0xffff);
|
|
DECLARE_FIT_TYPE(UINT32, unsigned int, 0xffffffff);
|
|
DECLARE_FIT_TYPE(UINT64, unsigned long long, 0xffffffffffffffffull);
|
|
|
|
DECLARE_FIT_TYPE(UINT8Z, unsigned char, 0);
|
|
DECLARE_FIT_TYPE(UINT16Z, unsigned short, 0);
|
|
DECLARE_FIT_TYPE(UINT32Z, unsigned int, 0);
|
|
|
|
DECLARE_FIT_TYPE(SINT8, signed char, 0x7f);
|
|
DECLARE_FIT_TYPE(SINT16, signed short, 0x7fff);
|
|
DECLARE_FIT_TYPE(SINT32, signed int, 0x7fffffff);
|
|
DECLARE_FIT_TYPE(SINT64, signed long long, 0x7fffffffffffffffll);
|
|
|
|
DECLARE_FIT_TYPE(FLOAT, unsigned int, 0xffffffff);
|
|
DECLARE_FIT_TYPE(DOUBLE, unsigned long long, 0xffffffffffffffffll);
|
|
DECLARE_FIT_TYPE(STRING, char *, NULL);
|
|
|
|
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) \
|
|
{ \
|
|
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 *)p; \
|
|
if (val == type##_INVAL) return; \
|
|
DEBUG(g->base.context, "%s (%s): %lld", #name, #type, (long long)val); \
|
|
parse_##name(g, *(type *)p); \
|
|
} \
|
|
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)
|
|
{
|
|
if (garmin->callback) {
|
|
dc_sample_value_t sample = {0};
|
|
|
|
// Turn the timestamp relative to the beginning of the dive
|
|
if (data < garmin->time)
|
|
return;
|
|
data -= garmin->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;
|
|
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) { }
|
|
|
|
// SESSION msg
|
|
DECLARE_FIELD(SESSION, start_time, UINT32) { garmin->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, distance, UINT32) { } // Distance in 100 * m? WTF?
|
|
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
|
|
|
|
// DEVICE_SETTINGS
|
|
DECLARE_FIELD(DEVICE_SETTINGS, utc_offset, UINT32) { garmin->utc_offset = (SINT32) data; } // wrong type in FIT
|
|
DECLARE_FIELD(DEVICE_SETTINGS, time_offset, UINT32) { garmin->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;
|
|
}
|
|
|
|
// SPORT
|
|
DECLARE_FIELD(SPORT, sub_sport, ENUM) { garmin->sub_sport = (ENUM) data; }
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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); }
|
|
|
|
// 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, hear_rate_device_type, UINT8) { }
|
|
|
|
// 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;
|
|
}
|
|
|
|
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 = 8,
|
|
.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),
|
|
}
|
|
};
|
|
|
|
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(ZONES_TARGET) = { };
|
|
|
|
DECLARE_MESG(SPORT) = {
|
|
.maxfield = 2,
|
|
.field = {
|
|
SET_FIELD(SPORT, 1, sub_sport, ENUM), // 53 - 57 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 = 99,
|
|
.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, 5, distance, UINT32), // Distance in 100 * m? WTF?
|
|
SET_FIELD(RECORD, 13, temperature, SINT8), // degrees C
|
|
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
|
|
}
|
|
};
|
|
|
|
DECLARE_MESG(DIVE_GAS) = {
|
|
.maxfield = 3,
|
|
.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),
|
|
}
|
|
};
|
|
|
|
DECLARE_MESG(DIVE_SUMMARY) = {
|
|
.maxfield = 12,
|
|
.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
|
|
}
|
|
};
|
|
|
|
DECLARE_MESG(EVENT) = {
|
|
.maxfield = 16,
|
|
.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),
|
|
}
|
|
};
|
|
|
|
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) = { };
|
|
DECLARE_MESG(FILE_CREATOR) = { };
|
|
|
|
DECLARE_MESG(DIVE_SETTINGS) = {
|
|
.maxfield = 21,
|
|
.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, hear_rate_device_type, UINT8), // device type depending on heart_rate_source_type (ignorable for now)
|
|
}
|
|
};
|
|
DECLARE_MESG(DIVE_ALARM) = { };
|
|
|
|
// 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( 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(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),
|
|
};
|
|
|
|
#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 traverse_compressed(struct garmin_parser_t *garmin,
|
|
const unsigned char *data, unsigned int size,
|
|
unsigned char type, unsigned int time)
|
|
{
|
|
fprintf(stderr, "Compressed record for local type %d:\n", type);
|
|
return -1;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
DEBUG(garmin->base.context, "%s/%d %s '%s'", msg_name, field_nr, base_type_info[base_type].type_name, 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 -1;
|
|
}
|
|
|
|
if (size < len) {
|
|
ERROR(garmin->base.context, "Data traversal size bigger than remaining data (%d vs %d)\n", len, size);
|
|
return -1;
|
|
}
|
|
|
|
if (base_type > 16) {
|
|
ERROR(garmin->base.context, "Unknown base type %d\n", base_type);
|
|
data += size;
|
|
len -= size;
|
|
total_len += size;
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
// String
|
|
if (base_type == 7) {
|
|
int string_len = strnlen(data, size);
|
|
if (string_len >= size) {
|
|
ERROR(garmin->base.context, "Data traversal string bigger than remaining data\n");
|
|
return -1;
|
|
}
|
|
if (len <= string_len) {
|
|
ERROR(garmin->base.context, "field length %d, string length %d\n", len, string_len + 1);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
msg = array_uint16_le(data+2);
|
|
desc->msg_desc = lookup_msg_desc(msg, type, &desc->msg_name);
|
|
fields = data[4];
|
|
|
|
DEBUG(garmin->base.context, "Define local type %d: %02x %02x %04x %02x %s",
|
|
type, data[0], data[1], msg, fields, desc->msg_name);
|
|
|
|
if (data[1]) {
|
|
ERROR(garmin->base.context, "Only handling little-endian definitions\n");
|
|
return -1;
|
|
}
|
|
|
|
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;
|
|
if (record & 0x20) {
|
|
devfields = data[len];
|
|
len += 1 + devfields*3;
|
|
ERROR(garmin->base.context, "NO support for developer fields yet\n");
|
|
return -1;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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", 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);
|
|
datasize = array_uint32_le(data+4);
|
|
if (memcmp(data+8, ".FIT", 4))
|
|
return DC_STATUS_IO;
|
|
if (hdrsize < 12 || datasize > len || datasize + hdrsize + 2 > len)
|
|
return DC_STATUS_IO;
|
|
|
|
garmin->protocol = protocol;
|
|
garmin->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;
|
|
|
|
len = traverse_compressed(garmin, data, datasize, type, time);
|
|
} else if (record & 0x40) { // Definition record?
|
|
len = traverse_definition(garmin, data, datasize, record);
|
|
} else { // Normal data 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, const unsigned char *data, unsigned int size, dc_event_devinfo_t *devinfo_p)
|
|
{
|
|
// set up the parser and extract data
|
|
dc_parser_set_data(abstract, data, size);
|
|
garmin_parser_t *garmin = (garmin_parser_t *) abstract;
|
|
|
|
if (devinfo_p) {
|
|
devinfo_p->firmware = garmin->firmware;
|
|
devinfo_p->serial = garmin->serial;
|
|
devinfo_p->model = garmin->product;
|
|
}
|
|
switch (garmin->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 dc_status_t
|
|
garmin_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
|
|
{
|
|
garmin_parser_t *garmin = (garmin_parser_t *) abstract;
|
|
|
|
/* Walk the data once without a callback to set up the core fields */
|
|
garmin->callback = NULL;
|
|
garmin->userdata = NULL;
|
|
memset(&garmin->cache, 0, sizeof(garmin->cache));
|
|
|
|
traverse_data(garmin);
|
|
// 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);
|
|
|
|
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->time;
|
|
|
|
// Show local time (time_offset)
|
|
dc_datetime_gmtime(datetime, time + garmin->time_offset);
|
|
datetime->timezone = DC_TIMEZONE_NONE;
|
|
|
|
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;
|
|
|
|
if (!value)
|
|
return DC_STATUS_INVALIDARGS;
|
|
if (type == DC_FIELD_TANK_COUNT)
|
|
type = DC_FIELD_GASMIX_COUNT;
|
|
|
|
/* This whole sequence should be standardized */
|
|
if (!(garmin->cache.initialized & (1 << type)))
|
|
return DC_STATUS_UNSUPPORTED;
|
|
|
|
switch (type) {
|
|
case DC_FIELD_DIVETIME:
|
|
return DC_FIELD_VALUE(garmin->cache, value, DIVETIME);
|
|
case DC_FIELD_MAXDEPTH:
|
|
return DC_FIELD_VALUE(garmin->cache, value, MAXDEPTH);
|
|
case DC_FIELD_AVGDEPTH:
|
|
return DC_FIELD_VALUE(garmin->cache, value, AVGDEPTH);
|
|
case DC_FIELD_GASMIX_COUNT:
|
|
return DC_FIELD_VALUE(garmin->cache, value, GASMIX_COUNT);
|
|
case DC_FIELD_GASMIX:
|
|
if (flags >= MAXGASES)
|
|
return DC_STATUS_UNSUPPORTED;
|
|
return DC_FIELD_INDEX(garmin->cache, value, GASMIX, flags);
|
|
case DC_FIELD_SALINITY:
|
|
return DC_FIELD_VALUE(garmin->cache, value, SALINITY);
|
|
case DC_FIELD_ATMOSPHERIC:
|
|
return DC_STATUS_UNSUPPORTED;
|
|
case DC_FIELD_DIVEMODE:
|
|
return DC_STATUS_UNSUPPORTED;
|
|
case DC_FIELD_TANK:
|
|
return DC_STATUS_UNSUPPORTED;
|
|
case DC_FIELD_STRING:
|
|
return dc_field_get_string(&garmin->cache, flags, (dc_field_string_t *)value);
|
|
default:
|
|
return DC_STATUS_UNSUPPORTED;
|
|
}
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
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);
|
|
}
|