The McLean Extreme records a sample every 10 seconds instead of every 20 seconds. This resulted in dive durations that were twice as long as expected. Reported-by: David Carron <david_de_carron@hotmail.com>
317 lines
9.0 KiB
C
317 lines
9.0 KiB
C
/*
|
|
* libdivecomputer
|
|
*
|
|
* Copyright (C) 2020 Jef Driesen, David Carron
|
|
*
|
|
* 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 <string.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "mclean_extreme.h"
|
|
#include "context-private.h"
|
|
#include "parser-private.h"
|
|
#include "array.h"
|
|
|
|
#define ISINSTANCE(parser) dc_device_isinstance((parser), &mclean_extreme_parser_vtable)
|
|
|
|
#define SZ_CFG 0x002D
|
|
#define SZ_COMPUTER (SZ_CFG + 0x6A)
|
|
#define SZ_HEADER (SZ_CFG + 0x31)
|
|
#define SZ_SAMPLE 0x0004
|
|
|
|
#define EPOCH 946684800 // 2000-01-01 00:00:00 UTC
|
|
|
|
#define REC 0
|
|
#define TEC 1
|
|
#define CCR 2
|
|
#define GAUGE 3
|
|
|
|
#define INVALID 0xFFFFFFFF
|
|
|
|
#define NGASMIXES 8
|
|
|
|
typedef struct mclean_extreme_parser_t mclean_extreme_parser_t;
|
|
|
|
struct mclean_extreme_parser_t {
|
|
dc_parser_t base;
|
|
|
|
// Cached fields.
|
|
unsigned int cached;
|
|
unsigned int ngasmixes;
|
|
unsigned int gasmix[NGASMIXES];
|
|
};
|
|
|
|
static dc_status_t mclean_extreme_parser_set_data(dc_parser_t *abstract, const unsigned char *data, unsigned int size);
|
|
static dc_status_t mclean_extreme_parser_get_datetime(dc_parser_t *abstract, dc_datetime_t *datetime);
|
|
static dc_status_t mclean_extreme_parser_get_field(dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
|
|
static dc_status_t mclean_extreme_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata);
|
|
|
|
static const dc_parser_vtable_t mclean_extreme_parser_vtable = {
|
|
sizeof(mclean_extreme_parser_t),
|
|
DC_FAMILY_MCLEAN_EXTREME,
|
|
mclean_extreme_parser_set_data, /* set_data */
|
|
mclean_extreme_parser_get_datetime, /* datetime */
|
|
mclean_extreme_parser_get_field, /* fields */
|
|
mclean_extreme_parser_samples_foreach, /* samples_foreach */
|
|
NULL /* destroy */
|
|
};
|
|
|
|
dc_status_t
|
|
mclean_extreme_parser_create(dc_parser_t **out, dc_context_t *context)
|
|
{
|
|
mclean_extreme_parser_t *parser = NULL;
|
|
|
|
if (out == NULL) {
|
|
return DC_STATUS_INVALIDARGS;
|
|
}
|
|
|
|
// Allocate memory.
|
|
parser = (mclean_extreme_parser_t *)dc_parser_allocate(context, &mclean_extreme_parser_vtable);
|
|
if (parser == NULL) {
|
|
ERROR(context, "Failed to allocate memory.");
|
|
return DC_STATUS_NOMEMORY;
|
|
}
|
|
|
|
// Set the default values.
|
|
parser->cached = 0;
|
|
parser->ngasmixes = 0;
|
|
for (unsigned int i = 0; i < NGASMIXES; ++i) {
|
|
parser->gasmix[i] = INVALID;
|
|
}
|
|
|
|
*out = (dc_parser_t *)parser;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
mclean_extreme_parser_set_data(dc_parser_t *abstract, const unsigned char *data, unsigned int size)
|
|
{
|
|
mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract;
|
|
|
|
// Reset the cache.
|
|
parser->cached = 0;
|
|
parser->ngasmixes = 0;
|
|
for (unsigned int i = 0; i < NGASMIXES; ++i) {
|
|
parser->gasmix[i] = INVALID;
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
mclean_extreme_parser_get_datetime(dc_parser_t *abstract, dc_datetime_t *datetime)
|
|
{
|
|
if (abstract->size < SZ_HEADER) {
|
|
ERROR(abstract->context, "Corrupt dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
|
|
unsigned int timestamp = array_uint32_le(abstract->data + SZ_CFG + 0x0000);
|
|
dc_ticks_t ticks = (dc_ticks_t) timestamp + EPOCH;
|
|
|
|
if (!dc_datetime_gmtime (datetime, ticks))
|
|
return DC_STATUS_DATAFORMAT;
|
|
|
|
datetime->timezone = DC_TIMEZONE_NONE;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
mclean_extreme_parser_get_field(dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
|
|
{
|
|
mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract;
|
|
|
|
if (abstract->size < SZ_HEADER) {
|
|
ERROR(abstract->context, "Corrupt dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
|
|
if (!parser->cached) {
|
|
dc_status_t rc = mclean_extreme_parser_samples_foreach (abstract, NULL, NULL);
|
|
if (rc != DC_STATUS_SUCCESS)
|
|
return rc;
|
|
}
|
|
|
|
dc_gasmix_t *gasmix = (dc_gasmix_t *)value;
|
|
dc_salinity_t *salinity = (dc_salinity_t *)value;
|
|
|
|
const unsigned int psurf = array_uint16_le(abstract->data + 0x001E);
|
|
const unsigned int density_index = abstract->data[0x0023];
|
|
double density = 0;
|
|
|
|
switch (density_index) {
|
|
case 0:
|
|
density = 1.000;
|
|
break;
|
|
case 1:
|
|
density = 1.020;
|
|
break;
|
|
case 2:
|
|
density = 1.030;
|
|
break;
|
|
default:
|
|
ERROR(abstract->context, "Corrupt density index in dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
|
|
if (value) {
|
|
switch (type) {
|
|
case DC_FIELD_DIVETIME:
|
|
*((unsigned int *)value) = array_uint32_le(abstract->data + SZ_CFG + 0x000C) - array_uint32_le(abstract->data + SZ_CFG + 0x0000);
|
|
break;
|
|
case DC_FIELD_MAXDEPTH:
|
|
*((double *)value) = 0.01 * (array_uint16_le(abstract->data + SZ_CFG + 0x0016) - psurf) / density;
|
|
break;
|
|
case DC_FIELD_AVGDEPTH:
|
|
*((double *)value) = 0.01 * (array_uint16_le(abstract->data + SZ_CFG + 0x0018) - psurf) / density;
|
|
break;
|
|
case DC_FIELD_SALINITY:
|
|
salinity->density = density * 1000.0;
|
|
salinity->type = density_index == 0 ? DC_WATER_FRESH : DC_WATER_SALT;
|
|
break;
|
|
case DC_FIELD_ATMOSPHERIC:
|
|
*((double *)value) = 0.001 * array_uint16_le(abstract->data + 0x001E);
|
|
break;
|
|
case DC_FIELD_TEMPERATURE_MINIMUM:
|
|
*((double *)value) = (double)abstract->data[SZ_CFG + 0x0010];
|
|
break;
|
|
case DC_FIELD_TEMPERATURE_MAXIMUM:
|
|
*((double *)value) = (double)abstract->data[SZ_CFG + 0x0011];
|
|
break;
|
|
case DC_FIELD_DIVEMODE:
|
|
switch (abstract->data[0x002C]) {
|
|
case REC:
|
|
case TEC:
|
|
*((dc_divemode_t *)value) = DC_DIVEMODE_OC;
|
|
break;
|
|
case CCR:
|
|
*((dc_divemode_t *)value) = DC_DIVEMODE_CCR;
|
|
break;
|
|
case GAUGE:
|
|
*((dc_divemode_t *)value) = DC_DIVEMODE_GAUGE;
|
|
break;
|
|
default:
|
|
ERROR(abstract->context, "Corrupt dive mode in dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
break;
|
|
case DC_FIELD_GASMIX_COUNT:
|
|
*((unsigned int *)value) = parser->ngasmixes;
|
|
break;
|
|
case DC_FIELD_GASMIX:
|
|
gasmix->helium = 0.01 * abstract->data[0x0001 + 1 + 2 * parser->gasmix[flags]];
|
|
gasmix->oxygen = 0.01 * abstract->data[0x0001 + 0 + 2 * parser->gasmix[flags]];
|
|
gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
|
|
break;
|
|
default:
|
|
return DC_STATUS_UNSUPPORTED;
|
|
}
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
mclean_extreme_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
|
|
{
|
|
mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract;
|
|
|
|
if (abstract->size < SZ_HEADER) {
|
|
ERROR(abstract->context, "Corrupt dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
|
|
const unsigned int nsamples = array_uint16_le(abstract->data + 0x005C);
|
|
|
|
if (abstract->size != SZ_HEADER + nsamples * SZ_SAMPLE) {
|
|
ERROR(abstract->context, "Corrupt dive data");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
|
|
unsigned int ngasmixes = 0;
|
|
unsigned int gasmix[NGASMIXES] = {0};
|
|
unsigned int gasmix_previous = INVALID;
|
|
|
|
const unsigned int interval = 10;
|
|
unsigned int time = 0;
|
|
size_t offset = SZ_HEADER;
|
|
for (unsigned int i = 0; i < nsamples; ++i) {
|
|
dc_sample_value_t sample = { 0 };
|
|
|
|
const unsigned int depth = array_uint16_le(abstract->data + offset + 0);
|
|
const unsigned int temperature = abstract->data[offset + 2];
|
|
const unsigned int flags = abstract->data[offset + 3];
|
|
const unsigned int ccr = flags & 0x80;
|
|
const unsigned int gasmix_id = (flags & 0x1C) >> 2;
|
|
const unsigned int sp_index = (flags & 0x60) >> 5;
|
|
const unsigned int setpoint = abstract->data[0x0013 + sp_index];
|
|
|
|
time += interval;
|
|
sample.time = time;
|
|
if (callback) callback(DC_SAMPLE_TIME, sample, userdata);
|
|
|
|
sample.depth = 0.1 * depth;
|
|
if (callback) callback(DC_SAMPLE_DEPTH, sample, userdata);
|
|
|
|
sample.temperature = temperature;
|
|
if (callback) callback(DC_SAMPLE_TEMPERATURE, sample, userdata);
|
|
|
|
if (gasmix_id != gasmix_previous) {
|
|
// Find the gasmix in the list.
|
|
unsigned int idx = 0;
|
|
while (idx < ngasmixes) {
|
|
if (gasmix_id == gasmix[idx])
|
|
break;
|
|
idx++;
|
|
}
|
|
|
|
// Add it to list if not found.
|
|
if (idx >= ngasmixes) {
|
|
if (idx >= NGASMIXES) {
|
|
ERROR (abstract->context, "Maximum number of gas mixes reached.");
|
|
return DC_STATUS_DATAFORMAT;
|
|
}
|
|
gasmix[idx] = gasmix_id;
|
|
ngasmixes = idx + 1;
|
|
}
|
|
|
|
sample.gasmix = idx;
|
|
if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
|
|
gasmix_previous = gasmix_id;
|
|
}
|
|
|
|
if (ccr) {
|
|
sample.setpoint = 0.01 * setpoint;
|
|
if (callback) callback(DC_SAMPLE_SETPOINT, sample, userdata);
|
|
}
|
|
|
|
offset += SZ_SAMPLE;
|
|
}
|
|
|
|
// Cache the data for later use.
|
|
for (unsigned int i = 0; i < ngasmixes; ++i) {
|
|
parser->gasmix[i] = gasmix[i];
|
|
}
|
|
parser->ngasmixes = ngasmixes;
|
|
parser->cached = 1;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|