libdc/src/suunto_eonsteel_parser.c
Linus Torvalds 8fb4018db3 Suunto EON Steel: add transmitter battery state reporting
I'm not sure how reliable it is, and older wireless tank pods had a bug
in the reporting (looks like the battery voltage range calibration was
done incorrectly, and it reports 0% battery for new batteries).

But depending on how well it actually works when the batteries start
getting weak, the transmitter battery reporting is potentially *very*
useful.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
2016-07-21 19:58:29 +09:00

1589 lines
42 KiB
C

/*
* libdivecomputer
*
* Copyright (C) 2014 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 <string.h>
#include <stdio.h>
#include <ctype.h>
#include <math.h>
#include <stdarg.h>
/* Wow. MSC is truly crap */
#ifdef _MSC_VER
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#endif
#include <libdivecomputer/suunto_eonsteel.h>
#include "context-private.h"
#include "parser-private.h"
#include "array.h"
#define C_ARRAY_SIZE(a) (sizeof(a) / sizeof(*(a)))
enum eon_sample {
ES_none = 0,
ES_dtime, // duint16,precision=3 (time delta in ms)
ES_depth, // uint16,precision=2,nillable=65535 (depth in cm)
ES_temp, // int16,precision=2,nillable=-3000 (temp in deci-Celsius)
ES_ndl, // int16,nillable=-1 (ndl in minutes)
ES_ceiling, // uint16,precision=2,nillable=65535 (ceiling in cm)
ES_tts, // uint16,nillable=65535 (time to surface)
ES_heading, // uint16,precision=4,nillable=65535 (heading in degrees)
ES_abspressure, // uint16,precision=0,nillable=65535 (abs presure in centibar)
ES_gastime, // int16,nillable=-1 (remaining gas time in minutes)
ES_ventilation, // uint16,precision=6,nillable=65535 ("x/6000000,x"? No idea)
ES_gasnr, // uint8
ES_pressure, // uint16,nillable=65535 (cylinder pressure in centibar)
ES_state, // enum:0=Wet Outside,1=Below Wet Activation Depth,2=Below Surface,3=Dive Active,4=Surface Calculation,5=Tank pressure available,6=Closed Circuit Mode
ES_state_active, // bool
ES_notify, // enum:0=NoFly Time,1=Depth,2=Surface Time,3=Tissue Level,4=Deco,5=Deco Window,6=Safety Stop Ahead,7=Safety Stop,8=Safety Stop Broken,9=Deep Stop Ahead,10=Deep Stop,11=Dive Time,12=Gas Available,13=SetPoint Switch,14=Diluent Hypoxia,15=Air Time,16=Tank Pressure
ES_notify_active, // bool
ES_warning, // enum:0=ICD Penalty,1=Deep Stop Penalty,2=Mandatory Safety Stop,3=OTU250,4=OTU300,5=CNS80%,6=CNS100%,7=Max.Depth,8=Air Time,9=Tank Pressure,10=Safety Stop Broken,11=Deep Stop Broken,12=Ceiling Broken,13=PO2 High
ES_warning_active, // bool
ES_alarm,
ES_alarm_active,
ES_gasswitch, // uint16
ES_setpoint_type, // enum:0=Low,1=High,2=Custom
ES_setpoint_po2, // uint32
ES_setpoint_automatic, // bool
ES_bookmark,
};
#define EON_MAX_GROUP 16
struct type_desc {
const char *desc, *format, *mod;
unsigned int size;
enum eon_sample type[EON_MAX_GROUP];
};
#define MAXTYPE 512
#define MAXGASES 16
#define MAXSTRINGS 16
typedef struct suunto_eonsteel_parser_t {
dc_parser_t base;
struct type_desc type_desc[MAXTYPE];
// field cache
struct {
unsigned int initialized;
unsigned int divetime;
double maxdepth;
double avgdepth;
unsigned int ngases;
dc_gasmix_t gasmix[MAXGASES];
dc_salinity_t salinity;
double surface_pressure;
dc_divemode_t divemode;
double lowsetpoint;
double highsetpoint;
double customsetpoint;
dc_field_string_t strings[MAXSTRINGS];
dc_tankinfo_t tankinfo[MAXGASES];
double tanksize[MAXGASES];
double tankworkingpressure[MAXGASES];
} cache;
} suunto_eonsteel_parser_t;
typedef int (*eon_data_cb_t)(unsigned short type, const struct type_desc *desc, const unsigned char *data, int len, void *user);
static const struct {
const char *name;
enum eon_sample type;
} type_translation[] = {
{ "+Time", ES_dtime },
{ "Depth", ES_depth },
{ "Temperature", ES_temp },
{ "NoDecTime", ES_ndl },
{ "Ceiling", ES_ceiling },
{ "TimeToSurface", ES_tts },
{ "Heading", ES_heading },
{ "DeviceInternalAbsPressure", ES_abspressure },
{ "GasTime", ES_gastime },
{ "Ventilation", ES_ventilation },
{ "Cylinders+Cylinder.GasNumber", ES_gasnr },
{ "Cylinders.Cylinder.Pressure", ES_pressure },
{ "Events+State.Type", ES_state },
{ "Events.State.Active", ES_state_active },
{ "Events+Notify.Type", ES_notify },
{ "Events.Notify.Active", ES_notify_active },
{ "Events+Warning.Type", ES_warning },
{ "Events.Warning.Active", ES_warning_active },
{ "Events+Alarm.Type", ES_alarm },
{ "Events.Alarm.Active", ES_alarm_active },
{ "Events.Bookmark.Name", ES_bookmark },
{ "Events.GasSwitch.GasNumber", ES_gasswitch },
{ "Events.SetPoint.Type", ES_setpoint_type },
{ "Events.Events.SetPoint.PO2", ES_setpoint_po2 },
{ "Events.SetPoint.Automatic", ES_setpoint_automatic },
{ "Events.DiveTimer.Active", ES_none },
{ "Events.DiveTimer.Time", ES_none },
};
static enum eon_sample lookup_descriptor_type(suunto_eonsteel_parser_t *eon, struct type_desc *desc)
{
int i;
const char *name = desc->desc;
// Not a sample type? Skip it
if (strncmp(name, "sml.DeviceLog.Samples", 21))
return ES_none;
// Skip the common base
name += 21;
// We have a "+Sample.Time", which starts a new
// sample and contains the time delta
if (!strcmp(name, "+Sample.Time"))
return ES_dtime;
// .. the rest should start with ".Sample."
if (strncmp(name, ".Sample.", 8))
return ES_none;
// Skip the ".Sample."
name += 8;
// .. and look it up in the table of sample type strings
for (i = 0; i < C_ARRAY_SIZE(type_translation); i++) {
if (!strcmp(name, type_translation[i].name))
return type_translation[i].type;
}
return ES_none;
}
static const char *desc_type_name(enum eon_sample type)
{
int i;
for (i = 0; i < C_ARRAY_SIZE(type_translation); i++) {
if (type == type_translation[i].type)
return type_translation[i].name;
}
return "Unknown";
}
static int lookup_descriptor_size(suunto_eonsteel_parser_t *eon, struct type_desc *desc)
{
const char *format = desc->format;
unsigned char c;
if (!format)
return 0;
if (!strncmp(format, "bool", 4))
return 1;
if (!strncmp(format, "enum", 4))
return 1;
if (!strncmp(format, "utf8", 4))
return 0;
// find the byte size (eg "float32" -> 4 bytes)
while ((c = *format) != 0) {
if (isdigit(c))
return atoi(format)/8;
format++;
}
return 0;
}
static int fill_in_group_details(suunto_eonsteel_parser_t *eon, struct type_desc *desc)
{
int subtype = 0;
const char *grp = desc->desc;
for (;;) {
struct type_desc *base;
char *end;
long index;
index = strtol(grp, &end, 10);
if (index < 0 || index > MAXTYPE || end == grp) {
ERROR(eon->base.context, "Group type descriptor '%s' does not parse", desc->desc);
break;
}
base = eon->type_desc + index;
if (!base->desc) {
ERROR(eon->base.context, "Group type descriptor '%s' has undescribed index %d", desc->desc, index);
break;
}
if (!base->size) {
ERROR(eon->base.context, "Group type descriptor '%s' uses unsized sub-entry '%s'", desc->desc, base->desc);
break;
}
if (!base->type[0]) {
ERROR(eon->base.context, "Group type descriptor '%s' has non-enumerated sub-entry '%s'", desc->desc, base->desc);
break;
}
if (base->type[1]) {
ERROR(eon->base.context, "Group type descriptor '%s' has a recursive group sub-entry '%s'", desc->desc, base->desc);
break;
}
if (subtype >= EON_MAX_GROUP-1) {
ERROR(eon->base.context, "Group type descriptor '%s' has too many sub-entries", desc->desc);
break;
}
desc->size += base->size;
desc->type[subtype++] = base->type[0];
switch (*end) {
case 0:
return 0;
case ',':
grp = end+1;
continue;
default:
ERROR(eon->base.context, "Group type descriptor '%s' has unparseable index %d", desc->desc, index);
return -1;
}
}
return -1;
}
/*
* Here we cache descriptor data so that we don't have
* to re-parse the string all the time. That way we can
* do it just once per type.
*
* Right now we only bother with the sample descriptors,
* which all start with "sml.DeviceLog.Samples" (for the
* base types) or are "GRP" types that are a group of said
* types and are a set of numbers.
*/
static int fill_in_desc_details(suunto_eonsteel_parser_t *eon, struct type_desc *desc)
{
if (!desc->desc)
return 0;
if (isdigit(desc->desc[0]))
return fill_in_group_details(eon, desc);
desc->size = lookup_descriptor_size(eon, desc);
desc->type[0] = lookup_descriptor_type(eon, desc);
return 0;
}
static void
desc_free (struct type_desc desc[], unsigned int count)
{
for (unsigned int i = 0; i < count; ++i) {
free((void *)desc[i].desc);
free((void *)desc[i].format);
free((void *)desc[i].mod);
}
}
static int record_type(suunto_eonsteel_parser_t *eon, unsigned short type, const char *name, int namelen)
{
struct type_desc desc;
const char *next;
memset(&desc, 0, sizeof(desc));
do {
int len;
char *p;
next = strchr(name, '\n');
if (next) {
len = next - name;
next++;
} else {
len = strlen(name);
if (!len)
break;
}
if (len < 5 || name[0] != '<' || name[4] != '>') {
ERROR(eon->base.context, "Unexpected type description: %.*s", len, name);
return -1;
}
p = (char *) malloc(len-4);
if (!p) {
ERROR(eon->base.context, "out of memory");
desc_free(&desc, 1);
return -1;
}
memcpy(p, name+5, len-5);
p[len-5] = 0;
// PTH, GRP, FRM, MOD
switch (name[1]) {
case 'P':
case 'G':
desc.desc = p;
break;
case 'F':
desc.format = p;
break;
case 'M':
desc.mod = p;
break;
default:
ERROR(eon->base.context, "Unknown type descriptor: %.*s", len, name);
desc_free(&desc, 1);
free(p);
return -1;
}
} while ((name = next) != NULL);
if (type > MAXTYPE) {
ERROR(eon->base.context, "Type out of range (%04x: '%s' '%s' '%s')",
type,
desc.desc ? desc.desc : "",
desc.format ? desc.format : "",
desc.mod ? desc.mod : "");
desc_free(&desc, 1);
return -1;
}
fill_in_desc_details(eon, &desc);
desc_free(eon->type_desc + type, 1);
eon->type_desc[type] = desc;
return 0;
}
static int traverse_entry(suunto_eonsteel_parser_t *eon, const unsigned char *p, int len, eon_data_cb_t callback, void *user)
{
const unsigned char *name, *data, *end, *last, *one_past_end = p + len;
int textlen, type;
int rc;
// First two bytes: zero and text length
if (p[0]) {
HEXDUMP(eon->base.context, DC_LOGLEVEL_DEBUG, "next", p, 8);
ERROR(eon->base.context, "Bad dive entry (%02x)", p[0]);
return -1;
}
textlen = p[1];
name = p + 2;
if (textlen == 0xff) {
textlen = array_uint32_le(name);
name += 4;
}
// Two bytes of 'type' followed by the name/descriptor, followed by the data
data = name + textlen;
type = array_uint16_le(name);
name += 2;
if (*name != '<') {
HEXDUMP(eon->base.context, DC_LOGLEVEL_DEBUG, "bad", p, 16);
return -1;
}
record_type(eon, type, (const char *) name, textlen-3);
end = data;
last = data;
while (end < one_past_end && *end) {
const unsigned char *begin = end;
unsigned int type = *end++;
unsigned int len;
if (type == 0xff) {
type = array_uint16_le(end);
end += 2;
}
len = *end++;
// I've never actually seen this case yet..
// Just assuming from the other cases.
if (len == 0xff) {
HEXDUMP(eon->base.context, DC_LOGLEVEL_DEBUG, "len-ff", end, 8);
len = array_uint32_le(end);
end += 4;
}
if (type > MAXTYPE || !eon->type_desc[type].desc) {
HEXDUMP(eon->base.context, DC_LOGLEVEL_DEBUG, "last", last, 16);
HEXDUMP(eon->base.context, DC_LOGLEVEL_DEBUG, "this", begin, 16);
} else {
rc = callback(type, eon->type_desc+type, end, len, user);
if (rc < 0)
return rc;
}
last = begin;
end += len;
}
return end - p;
}
static int traverse_data(suunto_eonsteel_parser_t *eon, eon_data_cb_t callback, void *user)
{
const unsigned char *data = eon->base.data;
int len = eon->base.size;
// Dive files start with "SBEM" and four NUL characters
// Additionally, we've prepended the time as an extra
// 4-byte pre-header
if (len < 12 || memcmp(data+4, "SBEM", 4))
return 0;
data += 12;
len -= 12;
while (len > 4) {
int i = traverse_entry(eon, data, len, callback, user);
if (i < 0)
return 1;
len -= i;
data += i;
}
return 0;
}
struct sample_data {
suunto_eonsteel_parser_t *eon;
dc_sample_callback_t callback;
void *userdata;
unsigned int time;
const char *state_type, *notify_type;
const char *warning_type, *alarm_type;
/* We gather up deco and cylinder pressure information */
int gasnr;
int tts, ndl;
double ceiling;
};
static void sample_time(struct sample_data *info, unsigned short time_delta)
{
dc_sample_value_t sample = {0};
info->time += time_delta;
sample.time = info->time / 1000;
if (info->callback) info->callback(DC_SAMPLE_TIME, sample, info->userdata);
}
static void sample_depth(struct sample_data *info, unsigned short depth)
{
dc_sample_value_t sample = {0};
if (depth == 0xffff)
return;
sample.depth = depth / 100.0;
if (info->callback) info->callback(DC_SAMPLE_DEPTH, sample, info->userdata);
}
static void sample_temp(struct sample_data *info, short temp)
{
dc_sample_value_t sample = {0};
if (temp < -3000)
return;
sample.temperature = temp / 10.0;
if (info->callback) info->callback(DC_SAMPLE_TEMPERATURE, sample, info->userdata);
}
static void sample_ndl(struct sample_data *info, short ndl)
{
dc_sample_value_t sample = {0};
info->ndl = ndl;
if (ndl < 0)
return;
sample.deco.type = DC_DECO_NDL;
sample.deco.time = ndl;
if (info->callback) info->callback(DC_SAMPLE_DECO, sample, info->userdata);
}
static void sample_tts(struct sample_data *info, unsigned short tts)
{
if (tts != 0xffff)
info->tts = tts;
}
static void sample_ceiling(struct sample_data *info, unsigned short ceiling)
{
if (ceiling != 0xffff)
info->ceiling = ceiling / 100.0;
}
static void sample_heading(struct sample_data *info, unsigned short heading)
{
dc_sample_value_t sample = {0};
if (heading == 0xffff)
return;
sample.event.type = SAMPLE_EVENT_HEADING;
sample.event.value = heading;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
static void sample_abspressure(struct sample_data *info, unsigned short pressure)
{
}
static void sample_gastime(struct sample_data *info, short gastime)
{
dc_sample_value_t sample = {0};
if (gastime < 0)
return;
// Hmm. We have no good way to report airtime remaining
}
/*
* Per-sample "ventilation" data.
*
* It's described as:
* - "uint16,precision=6,nillable=65535"
* - "x/6000000,x"
*/
static void sample_ventilation(struct sample_data *info, unsigned short unk)
{
}
static void sample_gasnr(struct sample_data *info, unsigned char idx)
{
info->gasnr = idx;
}
static void sample_pressure(struct sample_data *info, unsigned short pressure)
{
dc_sample_value_t sample = {0};
if (pressure == 0xffff)
return;
sample.pressure.tank = info->gasnr-1;
sample.pressure.value = pressure / 100.0;
if (info->callback) info->callback(DC_SAMPLE_PRESSURE, sample, info->userdata);
}
static void sample_bookmark_event(struct sample_data *info, unsigned short idx)
{
dc_sample_value_t sample = {0};
sample.event.type = SAMPLE_EVENT_BOOKMARK;
sample.event.value = idx;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
static void sample_gas_switch_event(struct sample_data *info, unsigned short idx)
{
suunto_eonsteel_parser_t *eon = info->eon;
dc_sample_value_t sample = {0};
if (idx < 1 || idx > eon->cache.ngases)
return;
sample.gasmix = idx - 1;
if (info->callback) info->callback(DC_SAMPLE_GASMIX, sample, info->userdata);
#ifdef ENABLE_DEPRECATED
unsigned int o2 = 100 * eon->cache.gasmix[idx-1].oxygen;
unsigned int he = 100 * eon->cache.gasmix[idx-1].helium;
sample.event.type = SAMPLE_EVENT_GASCHANGE2;
sample.event.time = 0;
sample.event.flags = idx;
sample.event.value = o2 | (he << 16);
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
#endif
}
/*
* Look up the string from an enumeration.
*
* Enumerations have the enum values in the "format" string,
* and all start with "enum:" followed by a comma-separated list
* of enumeration values and strings. Example:
*
* "enum:0=NoFly Time,1=Depth,2=Surface Time,3=..."
*/
static const char *lookup_enum(const struct type_desc *desc, unsigned char value)
{
const char *str = desc->format;
unsigned char c;
if (!str)
return NULL;
if (strncmp(str, "enum:", 5))
return NULL;
str += 5;
while ((c = *str) != 0) {
unsigned char n;
const char *begin, *end;
char *ret;
str++;
if (!isdigit(c))
continue;
n = c - '0';
// We only handle one or two digits
if (isdigit(*str)) {
n = n*10 + *str - '0';
str++;
}
begin = end = str;
while ((c = *str) != 0) {
str++;
if (c == ',')
break;
end = str;
}
// Verify that it has the 'n=string' format and skip the equals sign
if (*begin != '=')
continue;
begin++;
// Is it the value we're looking for?
if (n != value)
continue;
ret = malloc(end - begin + 1);
if (!ret)
break;
memcpy(ret, begin, end-begin);
ret[end-begin] = 0;
return ret;
}
return NULL;
}
/*
* The EON Steel has four different sample events: "state", "notification",
* "warning" and "alarm". All end up having two fields: type and a boolean value.
*
* The type enumerations are available as part of the type descriptor, and we
* *should* probably parse them dynamically, but this hardcodes the different
* type values.
*
* For event states, the types are:
*
* 0=Wet Outside
* 1=Below Wet Activation Depth
* 2=Below Surface
* 3=Dive Active
* 4=Surface Calculation
* 5=Tank pressure available
*
* FIXME! This needs to parse the actual type descriptor enum
*/
static void sample_event_state_type(const struct type_desc *desc, struct sample_data *info, unsigned char type)
{
info->state_type = lookup_enum(desc, type);
}
static void sample_event_state_value(const struct type_desc *desc, struct sample_data *info, unsigned char value)
{
dc_sample_value_t sample = {0};
const char *name;
name = info->state_type;
if (!name)
return;
sample.event.type = SAMPLE_EVENT_STRING;
sample.event.name = name;
sample.event.value = value ? SAMPLE_FLAGS_BEGIN : SAMPLE_FLAGS_END;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
static void sample_event_notify_type(const struct type_desc *desc, struct sample_data *info, unsigned char type)
{
info->notify_type = lookup_enum(desc, type);
}
// FIXME! This needs to parse the actual type descriptor enum
static void sample_event_notify_value(const struct type_desc *desc, struct sample_data *info, unsigned char value)
{
dc_sample_value_t sample = {0};
const char *name;
name = info->notify_type;
if (!name)
return;
sample.event.type = SAMPLE_EVENT_STRING;
sample.event.name = name;
sample.event.value = value ? SAMPLE_FLAGS_BEGIN : SAMPLE_FLAGS_END;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
static void sample_event_warning_type(const struct type_desc *desc, struct sample_data *info, unsigned char type)
{
info->warning_type = lookup_enum(desc, type);
}
static void sample_event_warning_value(const struct type_desc *desc, struct sample_data *info, unsigned char value)
{
dc_sample_value_t sample = {0};
const char *name;
name = info->warning_type;
if (!name)
return;
sample.event.type = SAMPLE_EVENT_STRING;
sample.event.name = name;
sample.event.value = value ? SAMPLE_FLAGS_BEGIN : SAMPLE_FLAGS_END;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
static void sample_event_alarm_type(const struct type_desc *desc, struct sample_data *info, unsigned char type)
{
info->alarm_type = lookup_enum(desc, type);
}
// FIXME! This needs to parse the actual type descriptor enum
static void sample_event_alarm_value(const struct type_desc *desc, struct sample_data *info, unsigned char value)
{
const char *name;
dc_sample_value_t sample = {0};
name = info->alarm_type;
if (!name)
return;
sample.event.type = SAMPLE_EVENT_STRING;
sample.event.name = name;
sample.event.value = value ? SAMPLE_FLAGS_BEGIN : SAMPLE_FLAGS_END;
if (info->callback) info->callback(DC_SAMPLE_EVENT, sample, info->userdata);
}
// enum:0=Low,1=High,2=Custom
static void sample_setpoint_type(const struct type_desc *desc, struct sample_data *info, unsigned char value)
{
dc_sample_value_t sample = {0};
switch (value) {
case 0:
sample.ppo2 = info->eon->cache.lowsetpoint;
break;
case 1:
sample.ppo2 = info->eon->cache.highsetpoint;
break;
case 2:
sample.ppo2 = info->eon->cache.customsetpoint;
break;
default:
DEBUG(info->eon->base.context, "sample_setpoint_type(%u)", value);
return;
}
if (info->callback) info->callback(DC_SAMPLE_SETPOINT, sample, info->userdata);
}
// uint32
static void sample_setpoint_po2(struct sample_data *info, unsigned int pressure)
{
// I *think* this just sets the custom SP, and then
// we'll get a setpoint_type(2) later.
info->eon->cache.customsetpoint = pressure / 100000.0; // Pascal to bar
}
static void sample_setpoint_automatic(struct sample_data *info, unsigned char value)
{
DEBUG(info->eon->base.context, "sample_setpoint_automatic(%u)", value);
}
static int handle_sample_type(const struct type_desc *desc, struct sample_data *info, enum eon_sample type, const unsigned char *data)
{
switch (type) {
case ES_dtime:
sample_time(info, array_uint16_le(data));
return 2;
case ES_depth:
sample_depth(info, array_uint16_le(data));
return 2;
case ES_temp:
sample_temp(info, array_uint16_le(data));
return 2;
case ES_ndl:
sample_ndl(info, array_uint16_le(data));
return 2;
case ES_ceiling:
sample_ceiling(info, array_uint16_le(data));
return 2;
case ES_tts:
sample_tts(info, array_uint16_le(data));
return 2;
case ES_heading:
sample_heading(info, array_uint16_le(data));
return 2;
case ES_abspressure:
sample_abspressure(info, array_uint16_le(data));
return 2;
case ES_gastime:
sample_gastime(info, array_uint16_le(data));
return 2;
case ES_ventilation:
sample_ventilation(info, array_uint16_le(data));
return 2;
case ES_gasnr:
sample_gasnr(info, *data);
return 1;
case ES_pressure:
sample_pressure(info, array_uint16_le(data));
return 2;
case ES_state:
sample_event_state_type(desc, info, data[0]);
return 1;
case ES_state_active:
sample_event_state_value(desc, info, data[0]);
return 1;
case ES_notify:
sample_event_notify_type(desc, info, data[0]);
return 1;
case ES_notify_active:
sample_event_notify_value(desc, info, data[0]);
return 1;
case ES_warning:
sample_event_warning_type(desc, info, data[0]);
return 1;
case ES_warning_active:
sample_event_warning_value(desc, info, data[0]);
return 1;
case ES_alarm:
sample_event_alarm_type(desc, info, data[0]);
return 1;
case ES_alarm_active:
sample_event_alarm_value(desc, info, data[0]);
return 1;
case ES_bookmark:
sample_bookmark_event(info, array_uint16_le(data));
return 2;
case ES_gasswitch:
sample_gas_switch_event(info, array_uint16_le(data));
return 2;
case ES_setpoint_type:
sample_setpoint_type(desc, info, data[0]);
return 1;
case ES_setpoint_po2:
sample_setpoint_po2(info, array_uint32_le(data));
return 4;
case ES_setpoint_automatic: // bool
sample_setpoint_automatic(info, data[0]);
return 1;
default:
return 0;
}
}
static int traverse_samples(unsigned short type, const struct type_desc *desc, const unsigned char *data, int len, void *user)
{
struct sample_data *info = (struct sample_data *) user;
suunto_eonsteel_parser_t *eon = info->eon;
int i, used = 0;
if (desc->size > len)
ERROR(eon->base.context, "Got %d bytes of data for '%s' that wants %d bytes", len, desc->desc, desc->size);
info->ndl = -1;
info->tts = 0;
info->ceiling = 0.0;
for (i = 0; i < EON_MAX_GROUP; i++) {
enum eon_sample type = desc->type[i];
int bytes = handle_sample_type(desc, info, type, data);
if (!bytes)
break;
if (bytes > len) {
ERROR(eon->base.context, "Wanted %d bytes of data, only had %d bytes ('%s' idx %d)", bytes, len, desc->desc, i);
break;
}
data += bytes;
len -= bytes;
used += bytes;
}
if (info->ndl < 0 && (info->tts || info->ceiling)) {
dc_sample_value_t sample = {0};
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.time = info->tts;
sample.deco.depth = info->ceiling;
if (info->callback) info->callback(DC_SAMPLE_DECO, sample, info->userdata);
}
// Warn if there are left-over bytes for something we did use part of
if (used && len)
ERROR(eon->base.context, "Entry for '%s' had %d bytes, only used %d", desc->desc, len+used, used);
return 0;
}
static dc_status_t
suunto_eonsteel_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
suunto_eonsteel_parser_t *eon = (suunto_eonsteel_parser_t *) abstract;
struct sample_data data = { eon, callback, userdata, 0 };
traverse_data(eon, traverse_samples, &data);
return DC_STATUS_SUCCESS;
}
static dc_status_t get_string_field(suunto_eonsteel_parser_t *eon, unsigned idx, dc_field_string_t *value)
{
if (idx < MAXSTRINGS) {
dc_field_string_t *res = eon->cache.strings+idx;
if (res->desc && res->value) {
*value = *res;
return DC_STATUS_SUCCESS;
}
}
return DC_STATUS_UNSUPPORTED;
}
// Ugly define thing makes the code much easier to read
// I'd love to use __typeof__, but that's a gcc'ism
#define field_value(p, set) \
memcpy((p), &(set), sizeof(set))
static dc_status_t
suunto_eonsteel_parser_get_field(dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value)
{
dc_tank_t *tank = (dc_tank_t *) value;
suunto_eonsteel_parser_t *eon = (suunto_eonsteel_parser_t *)parser;
if (!(eon->cache.initialized & (1 << type)))
return DC_STATUS_UNSUPPORTED;
switch (type) {
case DC_FIELD_DIVETIME:
field_value(value, eon->cache.divetime);
break;
case DC_FIELD_MAXDEPTH:
field_value(value, eon->cache.maxdepth);
break;
case DC_FIELD_AVGDEPTH:
field_value(value, eon->cache.avgdepth);
break;
case DC_FIELD_GASMIX_COUNT:
case DC_FIELD_TANK_COUNT:
field_value(value, eon->cache.ngases);
break;
case DC_FIELD_GASMIX:
if (flags >= MAXGASES)
return DC_STATUS_UNSUPPORTED;
field_value(value, eon->cache.gasmix[flags]);
break;
case DC_FIELD_SALINITY:
field_value(value, eon->cache.salinity);
break;
case DC_FIELD_ATMOSPHERIC:
field_value(value, eon->cache.surface_pressure);
break;
case DC_FIELD_DIVEMODE:
field_value(value, eon->cache.divemode);
break;
case DC_FIELD_TANK:
/*
* Sadly it seems that the EON Steel doesn't tell us whether
* we get imperial or metric data - the only indication is
* that metric is (at least so far) always whole liters
*/
tank->volume = eon->cache.tanksize[flags];
/*
* The pressure reported is NOT the pressure the user enters.
*
* So 3000psi turns into 206.700 bar instead of 206.843 bar;
* We report it as we get it and let the application figure out
* what to do with that
*/
tank->workpressure = eon->cache.tankworkingpressure[flags];
tank->type = eon->cache.tankinfo[flags];
/*
* See if we should call this imperial instead.
*
* We need to have workpressure and a valid tank. In that case,
* a fractional tank size implies imperial.
*/
if (tank->workpressure && (tank->type & DC_TANKINFO_METRIC)) {
if (fabs(tank->volume - rint(tank->volume)) > 0.001)
tank->type += DC_TANKINFO_IMPERIAL - DC_TANKINFO_METRIC;
}
break;
case DC_FIELD_STRING:
return get_string_field(eon, flags, (dc_field_string_t *)value);
default:
return DC_STATUS_UNSUPPORTED;
}
return DC_STATUS_SUCCESS;
}
/*
* The time of the dive is encoded in the filename,
* and we've saved it off as the four first bytes
* of the dive data (in little-endian format).
*/
static dc_status_t
suunto_eonsteel_parser_get_datetime(dc_parser_t *parser, dc_datetime_t *datetime)
{
if (parser->size < 4)
return DC_STATUS_UNSUPPORTED;
dc_datetime_gmtime(datetime, array_uint32_le(parser->data));
return DC_STATUS_SUCCESS;
}
// time in ms
static void add_time_field(suunto_eonsteel_parser_t *eon, unsigned short time_delta_ms)
{
eon->cache.divetime += time_delta_ms;
}
// depth in cm
static void set_depth_field(suunto_eonsteel_parser_t *eon, unsigned short d)
{
if (d != 0xffff) {
double depth = d / 100.0;
if (depth > eon->cache.maxdepth)
eon->cache.maxdepth = depth;
eon->cache.initialized |= 1 << DC_FIELD_MAXDEPTH;
}
}
// new gas:
// "sml.DeviceLog.Header.Diving.Gases+Gas.State"
//
// We eventually need to parse the descriptor for that 'enum type'.
// Two versions so far:
// "enum:0=Off,1=Primary,2=?,3=Diluent"
// "enum:0=Off,1=Primary,3=Diluent,4=Oxygen"
//
// We turn that into the DC_TANKINFO data here, but
// initially consider all non-off tanks to me METRIC.
//
// We may later turn the METRIC tank size into IMPERIAL if we
// get a working pressure and non-integral size
static int add_gas_type(suunto_eonsteel_parser_t *eon, const struct type_desc *desc, unsigned char type)
{
int idx = eon->cache.ngases;
dc_tankinfo_t tankinfo = DC_TANKINFO_METRIC;
if (idx >= MAXGASES)
return 0;
eon->cache.ngases = idx+1;
switch (type) {
case 0:
tankinfo = 0;
break;
case 3:
tankinfo |= DC_TANKINFO_CC_DILUENT;
break;
case 4:
tankinfo |= DC_TANKINFO_CC_O2;
break;
default:
break;
}
eon->cache.tankinfo[idx] = tankinfo;
eon->cache.initialized |= 1 << DC_FIELD_GASMIX_COUNT;
eon->cache.initialized |= 1 << DC_FIELD_TANK_COUNT;
return 0;
}
// "sml.DeviceLog.Header.Diving.Gases.Gas.Oxygen"
// O2 percentage as a byte
static int add_gas_o2(suunto_eonsteel_parser_t *eon, unsigned char o2)
{
int idx = eon->cache.ngases-1;
if (idx >= 0)
eon->cache.gasmix[idx].oxygen = o2 / 100.0;
eon->cache.initialized |= 1 << DC_FIELD_GASMIX;
return 0;
}
// "sml.DeviceLog.Header.Diving.Gases.Gas.Helium"
// He percentage as a byte
static int add_gas_he(suunto_eonsteel_parser_t *eon, unsigned char he)
{
int idx = eon->cache.ngases-1;
if (idx >= 0)
eon->cache.gasmix[idx].helium = he / 100.0;
eon->cache.initialized |= 1 << DC_FIELD_GASMIX;
return 0;
}
static int add_gas_size(suunto_eonsteel_parser_t *eon, float l)
{
int idx = eon->cache.ngases-1;
if (idx >= 0)
eon->cache.tanksize[idx] = l;
eon->cache.initialized |= 1 << DC_FIELD_TANK;
return 0;
}
static int add_gas_workpressure(suunto_eonsteel_parser_t *eon, float wp)
{
int idx = eon->cache.ngases-1;
if (idx >= 0)
eon->cache.tankworkingpressure[idx] = wp;
return 0;
}
static int add_string(suunto_eonsteel_parser_t *eon, const char *desc, const char *value)
{
int i;
eon->cache.initialized |= 1 << DC_FIELD_STRING;
for (i = 0; i < MAXSTRINGS; i++) {
dc_field_string_t *str = eon->cache.strings+i;
if (str->desc)
continue;
str->desc = desc;
str->value = strdup(value);
break;
}
return 0;
}
static int add_string_fmt(suunto_eonsteel_parser_t *eon, const char *desc, const char *fmt, ...)
{
char buffer[256];
va_list ap;
/*
* We ignore the return value from vsnprintf, and we
* always NUL-terminate the destination buffer ourselves.
*
* That way we don't have to worry about random bad legacy
* implementations.
*/
va_start(ap, fmt);
buffer[sizeof(buffer)-1] = 0;
(void) vsnprintf(buffer, sizeof(buffer)-1, fmt, ap);
va_end(ap);
return add_string(eon, desc, buffer);
}
static float get_le32_float(const unsigned char *src)
{
union {
unsigned int val;
float result;
} u;
u.val = array_uint32_le(src);
return u.result;
}
// "Device" fields are all utf8:
// Info.BatteryAtEnd
// Info.BatteryAtStart
// Info.BSL
// Info.HW
// Info.SW
// Name
// SerialNumber
static int traverse_device_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc,
const unsigned char *data, int len)
{
const char *name = desc->desc + strlen("sml.DeviceLog.Device.");
if (!strcmp(name, "SerialNumber"))
return add_string(eon, "Serial", data);
if (!strcmp(name, "Info.HW"))
return add_string(eon, "HW Version", data);
if (!strcmp(name, "Info.SW"))
return add_string(eon, "FW Version", data);
if (!strcmp(name, "Info.BatteryAtStart"))
return add_string(eon, "Battery at start", data);
if (!strcmp(name, "Info.BatteryAtEnd"))
return add_string(eon, "Battery at end", data);
return 0;
}
// "sml.DeviceLog.Header.Diving.Gases"
//
// +Gas.State (enum:0=Off,1=Primary,3=Diluent,4=Oxygen)
// .Gas.Oxygen (uint8,precision=2)
// .Gas.Helium (uint8,precision=2)
// .Gas.PO2 (uint32)
// .Gas.TransmitterID (utf8)
// .Gas.TankSize (float32,precision=5)
// .Gas.TankFillPressure (float32,precision=0)
// .Gas.StartPressure (float32,precision=0)
// .Gas.EndPressure (float32,precision=0)
// .Gas.TransmitterStartBatteryCharge (int8,precision=2)
// .Gas.TransmitterEndBatteryCharge (int8,precision=2)
static int traverse_gas_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc,
const unsigned char *data, int len)
{
const char *name = desc->desc + strlen("sml.DeviceLog.Header.Diving.Gases");
if (!strcmp(name, "+Gas.State"))
return add_gas_type(eon, desc, data[0]);
if (!strcmp(name, ".Gas.Oxygen"))
return add_gas_o2(eon, data[0]);
if (!strcmp(name, ".Gas.Helium"))
return add_gas_he(eon, data[0]);
if (!strcmp(name, ".Gas.TransmitterID"))
return add_string(eon, "Transmitter ID", data);
if (!strcmp(name, ".Gas.TankSize"))
return add_gas_size(eon, get_le32_float(data));
if (!strcmp(name, ".Gas.TankFillPressure"))
return add_gas_workpressure(eon, get_le32_float(data));
// There is a bug with older transmitters, where the transmitter
// battery charge returns zero. Rather than returning that bogus
// data, just don't return any battery charge information at all.
//
// Make sure to add all non-battery-charge field checks above this
// test, so that it doesn't trigger for anything else.
if (!data[0])
return 0;
if (!strcmp(name, ".Gas.TransmitterStartBatteryCharge"))
return add_string_fmt(eon, "Transmitter Battery at start", "%d %%", data[0]);
if (!strcmp(name, ".Gas.TransmitterEndBatteryCharge"))
return add_string_fmt(eon, "Transmitter Battery at end", "%d %%", data[0]);
return 0;
}
// "sml.DeviceLog.Header.Diving."
//
// SurfaceTime (uint32)
// NumberInSeries (uint32)
// Algorithm (utf8)
// SurfacePressure (uint32)
// Conservatism (int8)
// Altitude (uint16)
// AlgorithmTransitionDepth (uint8)
// DaysInSeries (uint32)
// PreviousDiveDepth (float32,precision=2)
// LowSetPoint (uint32)
// HighSetPoint (uint32)
// SwitchHighSetPoint.Enabled (bool)
// SwitchHighSetPoint.Depth (float32,precision=1)
// SwitchLowSetPoint.Enabled (bool)
// SwitchLowSetPoint.Depth (float32,precision=1)
// StartTissue.CNS (float32,precision=3)
// StartTissue.OTU (float32)
// StartTissue.OLF (float32,precision=3)
// StartTissue.Nitrogen+Pressure (uint32)
// StartTissue.Helium+Pressure (uint32)
// StartTissue.RgbmNitrogen (float32,precision=3)
// StartTissue.RgbmHelium (float32,precision=3)
// DiveMode (utf8)
// AlgorithmBottomTime (uint32)
// AlgorithmAscentTime (uint32)
// AlgorithmBottomMixture.Oxygen (uint8,precision=2)
// AlgorithmBottomMixture.Helium (uint8,precision=2)
// DesaturationTime (uint32)
// EndTissue.CNS (float32,precision=3)
// EndTissue.OTU (float32)
// EndTissue.OLF (float32,precision=3)
// EndTissue.Nitrogen+Pressure (uint32)
// EndTissue.Helium+Pressure (uint32)
// EndTissue.RgbmNitrogen (float32,precision=3)
// EndTissue.RgbmHelium (float32,precision=3)
static int traverse_diving_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc,
const unsigned char *data, int len)
{
const char *name = desc->desc + strlen("sml.DeviceLog.Header.Diving.");
if (!strncmp(name, "Gases", 5))
return traverse_gas_fields(eon, desc, data, len);
if (!strcmp(name, "SurfacePressure")) {
unsigned int pressure = array_uint32_le(data); // in SI units - Pascal
eon->cache.surface_pressure = pressure / 100000.0; // bar
eon->cache.initialized |= 1 << DC_FIELD_ATMOSPHERIC;
return 0;
}
if (!strcmp(name, "Algorithm"))
return add_string(eon, "Deco algorithm", data);
if (!strcmp(name, "DiveMode")) {
if (!strncmp(data, "CCR", 3)) {
eon->cache.divemode = DC_DIVEMODE_CC;
eon->cache.initialized |= 1 << DC_FIELD_DIVEMODE;
}
return add_string(eon, "Dive Mode", data);
}
/* Signed byte of conservatism (-2 .. +2) */
if (!strcmp(name, "Conservatism")) {
int val = *(signed char *)data;
return add_string_fmt(eon, "Personal Adjustment", "P%d", val);
}
if (!strcmp(name, "LowSetPoint")) {
unsigned int pressure = array_uint32_le(data); // in SI units - Pascal
eon->cache.lowsetpoint = pressure / 100000.0; // bar
return 0;
}
if (!strcmp(name, "HighSetPoint")) {
unsigned int pressure = array_uint32_le(data); // in SI units - Pascal
eon->cache.highsetpoint = pressure / 100000.0; // bar
return 0;
}
// Time recoded in seconds.
// Let's just agree to ignore seconds
if (!strcmp(name, "DesaturationTime")) {
unsigned int time = array_uint32_le(data) / 60;
return add_string_fmt(eon, "Desaturation Time", "%d:%02d", time / 60, time % 60);
}
if (!strcmp(name, "SurfaceTime")) {
unsigned int time = array_uint32_le(data) / 60;
return add_string_fmt(eon, "Surface Time", "%d:%02d", time / 60, time % 60);
}
return 0;
}
// "Header" fields are:
// Activity (utf8)
// DateTime (utf8)
// Depth.Avg (float32,precision=2)
// Depth.Max (float32,precision=2)
// Diving.*
// Duration (uint32)
// PauseDuration (uint32)
// SampleInterval (uint8)
static int traverse_header_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc,
const unsigned char *data, int len)
{
const char *name = desc->desc + strlen("sml.DeviceLog.Header.");
if (!strncmp(name, "Diving.", 7))
return traverse_diving_fields(eon, desc, data, len);
if (!strcmp(name, "Depth.Max")) {
double d = get_le32_float(data);
if (d > eon->cache.maxdepth)
eon->cache.maxdepth = d;
return 0;
}
if (!strcmp(name, "DateTime"))
return add_string(eon, "Dive ID", data);
return 0;
}
static int traverse_dynamic_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc, const unsigned char *data, int len)
{
const char *name = desc->desc;
if (!strncmp(name, "sml.", 4)) {
name += 4;
if (!strncmp(name, "DeviceLog.", 10)) {
name += 10;
if (!strncmp(name, "Device.", 7))
return traverse_device_fields(eon, desc, data, len);
if (!strncmp(name, "Header.", 7)) {
return traverse_header_fields(eon, desc, data, len);
}
}
}
return 0;
}
/*
* This is a simplified sample parser that only parses the depth and time
* samples. It also depends on the GRP entries always starting with time/depth,
* and just stops on anything else.
*/
static int traverse_sample_fields(suunto_eonsteel_parser_t *eon, const struct type_desc *desc, const unsigned char *data, int len)
{
int i;
for (i = 0; i < EON_MAX_GROUP; i++) {
enum eon_sample type = desc->type[i];
switch (type) {
case ES_dtime:
add_time_field(eon, array_uint16_le(data));
data += 2;
continue;
case ES_depth:
set_depth_field(eon, array_uint16_le(data));
data += 2;
continue;
}
break;
}
return 0;
}
static int traverse_fields(unsigned short type, const struct type_desc *desc, const unsigned char *data, int len, void *user)
{
suunto_eonsteel_parser_t *eon = (suunto_eonsteel_parser_t *) user;
// Sample type? Do basic maxdepth and time parsing
if (desc->type[0])
traverse_sample_fields(eon, desc, data, len);
else
traverse_dynamic_fields(eon, desc, data, len);
return 0;
}
static void initialize_field_caches(suunto_eonsteel_parser_t *eon)
{
memset(&eon->cache, 0, sizeof(eon->cache));
eon->cache.initialized = 1 << DC_FIELD_DIVETIME;
traverse_data(eon, traverse_fields, eon);
// The internal time fields are in ms and have to be added up
// like that. At the end, we translate it back to seconds.
eon->cache.divetime /= 1000;
}
static void show_descriptor(suunto_eonsteel_parser_t *eon, int nr, struct type_desc *desc)
{
int i;
if (!desc->desc)
return;
DEBUG(eon->base.context, "Descriptor %d: '%s', size %d bytes", nr, desc->desc, desc->size);
if (desc->format)
DEBUG(eon->base.context, " format '%s'", desc->format);
if (desc->mod)
DEBUG(eon->base.context, " mod '%s'", desc->mod);
for (i = 0; i < EON_MAX_GROUP; i++) {
enum eon_sample type = desc->type[i];
if (!type)
continue;
DEBUG(eon->base.context, " %d: %d (%s)", i, type, desc_type_name(type));
}
}
static void show_all_descriptors(suunto_eonsteel_parser_t *eon)
{
for (unsigned int i = 0; i < MAXTYPE; ++i)
show_descriptor(eon, i, eon->type_desc+i);
}
static dc_status_t
suunto_eonsteel_parser_set_data(dc_parser_t *parser, const unsigned char *data, unsigned int size)
{
suunto_eonsteel_parser_t *eon = (suunto_eonsteel_parser_t *) parser;
desc_free(eon->type_desc, MAXTYPE);
memset(eon->type_desc, 0, sizeof(eon->type_desc));
initialize_field_caches(eon);
show_all_descriptors(eon);
return DC_STATUS_SUCCESS;
}
static dc_status_t
suunto_eonsteel_parser_destroy(dc_parser_t *parser)
{
suunto_eonsteel_parser_t *eon = (suunto_eonsteel_parser_t *) parser;
desc_free(eon->type_desc, MAXTYPE);
return DC_STATUS_SUCCESS;
}
static const dc_parser_vtable_t suunto_eonsteel_parser_vtable = {
sizeof(suunto_eonsteel_parser_t),
DC_FAMILY_SUUNTO_EONSTEEL,
suunto_eonsteel_parser_set_data, /* set_data */
suunto_eonsteel_parser_get_datetime, /* datetime */
suunto_eonsteel_parser_get_field, /* fields */
suunto_eonsteel_parser_samples_foreach, /* samples_foreach */
suunto_eonsteel_parser_destroy /* destroy */
};
dc_status_t
suunto_eonsteel_parser_create(dc_parser_t **out, dc_context_t *context, unsigned int model)
{
suunto_eonsteel_parser_t *parser = NULL;
if (out == NULL)
return DC_STATUS_INVALIDARGS;
parser = (suunto_eonsteel_parser_t *) dc_parser_allocate (context, &suunto_eonsteel_parser_vtable);
if (parser == NULL) {
ERROR (context, "Failed to allocate memory.");
return DC_STATUS_NOMEMORY;
}
memset(&parser->type_desc, 0, sizeof(parser->type_desc));
memset(&parser->cache, 0, sizeof(parser->cache));
*out = (dc_parser_t *) parser;
return DC_STATUS_SUCCESS;
}