libdivecomputer/src/cochran_commander_parser.c
Jef Driesen 9e169c9a3f Use the correct data type for the temperature
Temperatures are reported as a floating point values and not as
(unsigned) integers.
2018-01-30 21:20:53 +01:00

892 lines
30 KiB
C

/*
* libdivecomputer
*
* Copyright (C) 2014 John Van Ostrand
*
* 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 <math.h>
#include <libdivecomputer/units.h>
#include "cochran_commander.h"
#include "context-private.h"
#include "parser-private.h"
#include "array.h"
#define C_ARRAY_SIZE(array) (sizeof (array) / sizeof *(array))
#define COCHRAN_MODEL_COMMANDER_TM 0
#define COCHRAN_MODEL_COMMANDER_PRE21000 1
#define COCHRAN_MODEL_COMMANDER_AIR_NITROX 2
#define COCHRAN_MODEL_EMC_14 3
#define COCHRAN_MODEL_EMC_16 4
#define COCHRAN_MODEL_EMC_20 5
// Cochran time stamps start at Jan 1, 1992
#define COCHRAN_EPOCH 694242000
#define UNSUPPORTED 0xFFFFFFFF
typedef enum cochran_sample_format_t {
SAMPLE_TM,
SAMPLE_CMDR,
SAMPLE_EMC,
} cochran_sample_format_t;
typedef enum cochran_date_encoding_t {
DATE_ENCODING_MSDHYM,
DATE_ENCODING_SMHDMY,
DATE_ENCODING_TICKS,
} cochran_date_encoding_t;
typedef struct cochran_parser_layout_t {
cochran_sample_format_t format;
unsigned int headersize;
unsigned int samplesize;
unsigned int pt_sample_interval;
cochran_date_encoding_t date_encoding;
unsigned int datetime;
unsigned int pt_profile_begin;
unsigned int water_conductivity;
unsigned int pt_profile_pre;
unsigned int start_temp;
unsigned int start_depth;
unsigned int dive_number;
unsigned int altitude;
unsigned int pt_profile_end;
unsigned int end_temp;
unsigned int divetime;
unsigned int max_depth;
unsigned int avg_depth;
unsigned int oxygen;
unsigned int helium;
unsigned int min_temp;
unsigned int max_temp;
} cochran_parser_layout_t;
typedef struct cochran_events_t {
unsigned char code;
unsigned int data_bytes;
parser_sample_event_t type;
parser_sample_flags_t flag;
} cochran_events_t;
typedef struct event_size_t {
unsigned int code;
unsigned int size;
} event_size_t;
typedef struct cochran_commander_parser_t {
dc_parser_t base;
unsigned int model;
const cochran_parser_layout_t *layout;
const event_size_t *events;
unsigned int nevents;
} cochran_commander_parser_t ;
static dc_status_t cochran_commander_parser_set_data (dc_parser_t *parser, const unsigned char *data, unsigned int size);
static dc_status_t cochran_commander_parser_get_datetime (dc_parser_t *parser, dc_datetime_t *datetime);
static dc_status_t cochran_commander_parser_get_field (dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value);
static dc_status_t cochran_commander_parser_samples_foreach (dc_parser_t *parser, dc_sample_callback_t callback, void *userdata);
static const dc_parser_vtable_t cochran_commander_parser_vtable = {
sizeof(cochran_commander_parser_t),
DC_FAMILY_COCHRAN_COMMANDER,
cochran_commander_parser_set_data, /* set_data */
cochran_commander_parser_get_datetime, /* datetime */
cochran_commander_parser_get_field, /* fields */
cochran_commander_parser_samples_foreach, /* samples_foreach */
NULL /* destroy */
};
static const cochran_parser_layout_t cochran_cmdr_tm_parser_layout = {
SAMPLE_TM, // format
90, // headersize
1, // samplesize
72, // pt_sample_interval
DATE_ENCODING_TICKS, // date_encoding
15, // datetime, 4 bytes
0, // pt_profile_begin, 4 bytes
UNSUPPORTED, // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
0, // pt_profile_pre, 4 bytes
83, // start_temp, 1 byte, F
UNSUPPORTED, // start_depth, 2 bytes, /4=ft
20, // dive_number, 2 bytes
UNSUPPORTED, // altitude, 1 byte, /4=kilofeet
UNSUPPORTED, // pt_profile_end, 4 bytes
UNSUPPORTED, // end_temp, 1 byte F
57, // divetime, 2 bytes, minutes
49, // max_depth, 2 bytes, /4=ft
51, // avg_depth, 2 bytes, /4=ft
74, // oxygen, 4 bytes (2 of) 2 bytes, /256=%
UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
82, // min_temp, 1 byte, /2+20=F
UNSUPPORTED, // max_temp, 1 byte, /2+20=F
};
static const cochran_parser_layout_t cochran_cmdr_1_parser_layout = {
SAMPLE_CMDR, // format
256, // headersize
2, // samplesize
UNSUPPORTED, // pt_sample_interval
DATE_ENCODING_TICKS, // date_encoding
8, // datetime, 4 bytes
0, // pt_profile_begin, 4 bytes
24, // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
28, // pt_profile_pre, 4 bytes
43, // start_temp, 1 byte, F
54, // start_depth, 2 bytes, /4=ft
68, // dive_number, 2 bytes
73, // altitude, 1 byte, /4=kilofeet
128, // pt_profile_end, 4 bytes
153, // end_temp, 1 byte F
166, // divetime, 2 bytes, minutes
168, // max_depth, 2 bytes, /4=ft
170, // avg_depth, 2 bytes, /4=ft
210, // oxygen, 4 bytes (2 of) 2 bytes, /256=%
UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
232, // min_temp, 1 byte, /2+20=F
233, // max_temp, 1 byte, /2+20=F
};
static const cochran_parser_layout_t cochran_cmdr_parser_layout = {
SAMPLE_CMDR, // format
256, // headersize
2, // samplesize
UNSUPPORTED, // pt_sample_interval
DATE_ENCODING_MSDHYM, // date_encoding
0, // datetime, 6 bytes
6, // pt_profile_begin, 4 bytes
24, // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
30, // pt_profile_pre, 4 bytes
45, // start_temp, 1 byte, F
56, // start_depth, 2 bytes, /4=ft
70, // dive_number, 2 bytes
73, // altitude, 1 byte, /4=kilofeet
128, // pt_profile_end, 4 bytes
153, // end_temp, 1 byte F
166, // divetime, 2 bytes, minutes
168, // max_depth, 2 bytes, /4=ft
170, // avg_depth, 2 bytes, /4=ft
210, // oxygen, 4 bytes (2 of) 2 bytes, /256=%
UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
232, // min_temp, 1 byte, /2+20=F
233, // max_temp, 1 byte, /2+20=F
};
static const cochran_parser_layout_t cochran_emc_parser_layout = {
SAMPLE_EMC, // format
512, // headersize
3, // samplesize
UNSUPPORTED, // pt_sample_interval
DATE_ENCODING_SMHDMY, // date_encoding
0, // datetime, 6 bytes
6, // pt_profile_begin, 4 bytes
24, // water_conductivity, 1 byte 0=low(fresh), 2=high(sea)
30, // pt_profile_pre, 4 bytes
55, // start_temp, 1 byte, F
42, // start_depth, 2 bytes, /256=ft
86, // dive_number, 2 bytes,
89, // altitude, 1 byte /4=kilofeet
256, // pt_profile_end, 4 bytes
293, // end_temp, 1 byte, F
304, // divetime, 2 bytes, minutes
306, // max_depth, 2 bytes, /4=ft
310, // avg_depth, 2 bytes, /4=ft
144, // oxygen, 6 bytes (3 of) 2 bytes, /256=%
164, // helium, 6 bytes (3 of) 2 bytes, /256=%
403, // min_temp, 1 byte, /2+20=F
407, // max_temp, 1 byte, /2+20=F
};
static const cochran_events_t cochran_events[] = {
{0xA8, 1, SAMPLE_EVENT_SURFACE, SAMPLE_FLAGS_BEGIN}, // Entered PDI mode
{0xA9, 1, SAMPLE_EVENT_SURFACE, SAMPLE_FLAGS_END}, // Exited PDI mode
{0xAB, 5, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Ceiling decrease
{0xAD, 5, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Ceiling increase
{0xB5, 1, SAMPLE_EVENT_AIRTIME, SAMPLE_FLAGS_BEGIN}, // Air < 5 mins deco
{0xBD, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to nomal PO2 setting
{0xBE, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Ceiling > 60 ft
{0xC0, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to FO2 21% mode
{0xC1, 1, SAMPLE_EVENT_ASCENT, SAMPLE_FLAGS_BEGIN}, // Ascent rate greater than limit
{0xC2, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Low battery warning
{0xC3, 1, SAMPLE_EVENT_OLF, SAMPLE_FLAGS_NONE}, // CNS Oxygen toxicity warning
{0xC4, 1, SAMPLE_EVENT_MAXDEPTH, SAMPLE_FLAGS_NONE}, // Depth exceeds user set point
{0xC5, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_BEGIN}, // Entered decompression mode
{0xC7, 1, SAMPLE_EVENT_VIOLATION,SAMPLE_FLAGS_BEGIN}, // Entered Gauge mode (e.g. locked out)
{0xC8, 1, SAMPLE_EVENT_PO2, SAMPLE_FLAGS_BEGIN}, // PO2 too high
{0xCC, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_BEGIN}, // Low Cylinder 1 pressure
{0xCE, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_BEGIN}, // Non-decompression warning
{0xCF, 1, SAMPLE_EVENT_OLF, SAMPLE_FLAGS_BEGIN}, // O2 Toxicity
{0xCD, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to deco blend
{0xD0, 1, SAMPLE_EVENT_WORKLOAD, SAMPLE_FLAGS_BEGIN}, // Breathing rate alarm
{0xD3, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Low gas 1 flow rate
{0xD6, 1, SAMPLE_EVENT_CEILING, SAMPLE_FLAGS_BEGIN}, // Depth is less than ceiling
{0xD8, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_END}, // End decompression mode
{0xE1, 1, SAMPLE_EVENT_ASCENT, SAMPLE_FLAGS_END}, // End ascent rate warning
{0xE2, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Low SBAT battery warning
{0xE3, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to FO2 mode
{0xE5, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to PO2 mode
{0xEE, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_END}, // End non-decompresison warning
{0xEF, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switch to blend 2
{0xF0, 1, SAMPLE_EVENT_WORKLOAD, SAMPLE_FLAGS_END}, // Breathing rate alarm
{0xF3, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switch to blend 1
{0xF6, 1, SAMPLE_EVENT_CEILING, SAMPLE_FLAGS_END}, // End Depth is less than ceiling
};
static const event_size_t cochran_cmdr_event_bytes[] = {
{0x00, 17}, {0x01, 21}, {0x02, 18},
{0x03, 17}, {0x06, 19}, {0x07, 19},
{0x08, 19}, {0x09, 19}, {0x0a, 19},
{0x0b, 21}, {0x0c, 19}, {0x0d, 19},
{0x0e, 19}, {0x10, 21},
};
static const event_size_t cochran_emc_event_bytes[] = {
{0x00, 19}, {0x01, 23}, {0x02, 20},
{0x03, 19}, {0x06, 21}, {0x07, 21},
{0x0a, 21}, {0x0b, 21}, {0x0f, 19},
{0x10, 21},
};
static unsigned int
cochran_commander_handle_event (cochran_commander_parser_t *parser, unsigned char code, dc_sample_callback_t callback, void *userdata)
{
dc_parser_t *abstract = (dc_parser_t *) parser;
const cochran_events_t *event = NULL;
for (unsigned int i = 0; i < C_ARRAY_SIZE(cochran_events); ++i) {
if (cochran_events[i].code == code) {
event = cochran_events + i;
break;
}
}
if (event == NULL) {
// Unknown event, send warning so we know we missed something
WARNING(abstract->context, "Unknown event 0x%02x", code);
return 1;
}
switch (code) {
case 0xAB: // Ceiling decrease
// Indicated to lower ceiling by 10 ft (deeper)
// Bytes 1-2: first stop duration (min)
// Bytes 3-4: total stop duration (min)
// Handled in calling function
break;
case 0xAD: // Ceiling increase
// Indicates to raise ceiling by 10 ft (shallower)
// Handled in calling function
break;
case 0xC0: // Switched to FO2 21% mode (surface)
// Event seen upon surfacing
// handled in calling function
break;
case 0xCD: // Switched to deco blend
case 0xEF: // Switched to gas blend 2
case 0xF3: // Switched to gas blend 1
// handled in calling function
break;
default:
// Don't send known events of type NONE
if (event->type != SAMPLE_EVENT_NONE) {
dc_sample_value_t sample = {0};
sample.event.type = event->type;
sample.event.time = 0;
sample.event.value = 0;
sample.event.flags = event->flag;
if (callback) callback (DC_SAMPLE_EVENT, sample, userdata);
}
}
return event->data_bytes;
}
/*
* Used to find the end of a dive that has an incomplete dive-end
* block. It parses backwards past inter-dive events.
*/
static int
cochran_commander_backparse(cochran_commander_parser_t *parser, const unsigned char *samples, int size)
{
int result = size, best_result = size;
for (unsigned int i = 0; i < parser->nevents; i++) {
int ptr = size - parser->events[i].size;
if (ptr > 0 && samples[ptr] == parser->events[i].code) {
// Recurse to find the largest match. Because we are parsing backwards
// and the events vary in size we can't be sure the byte that matches
// the event code is an event code or data from inside a longer or shorter
// event.
result = cochran_commander_backparse(parser, samples, ptr);
}
if (result < best_result) {
best_result = result;
}
}
return best_result;
}
dc_status_t
cochran_commander_parser_create (dc_parser_t **out, dc_context_t *context, unsigned int model)
{
cochran_commander_parser_t *parser = NULL;
dc_status_t status = DC_STATUS_SUCCESS;
if (out == NULL)
return DC_STATUS_INVALIDARGS;
// Allocate memory.
parser = (cochran_commander_parser_t *) dc_parser_allocate (context, &cochran_commander_parser_vtable);
if (parser == NULL) {
ERROR (context, "Failed to allocate memory.");
return DC_STATUS_NOMEMORY;
}
parser->model = model;
switch (model) {
case COCHRAN_MODEL_COMMANDER_TM:
parser->layout = &cochran_cmdr_tm_parser_layout;
parser->events = NULL; // No inter-dive events on this model
parser->nevents = 0;
break;
case COCHRAN_MODEL_COMMANDER_PRE21000:
parser->layout = &cochran_cmdr_1_parser_layout;
parser->events = cochran_cmdr_event_bytes;
parser->nevents = C_ARRAY_SIZE(cochran_cmdr_event_bytes);
break;
case COCHRAN_MODEL_COMMANDER_AIR_NITROX:
parser->layout = &cochran_cmdr_parser_layout;
parser->events = cochran_cmdr_event_bytes;
parser->nevents = C_ARRAY_SIZE(cochran_cmdr_event_bytes);
break;
case COCHRAN_MODEL_EMC_14:
case COCHRAN_MODEL_EMC_16:
case COCHRAN_MODEL_EMC_20:
parser->layout = &cochran_emc_parser_layout;
parser->events = cochran_emc_event_bytes;
parser->nevents = C_ARRAY_SIZE(cochran_emc_event_bytes);
break;
default:
status = DC_STATUS_UNSUPPORTED;
goto error_free;
}
*out = (dc_parser_t *) parser;
return DC_STATUS_SUCCESS;
error_free:
dc_parser_deallocate ((dc_parser_t *) parser);
return status;
}
static dc_status_t
cochran_commander_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
{
return DC_STATUS_SUCCESS;
}
static dc_status_t
cochran_commander_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime)
{
cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
const cochran_parser_layout_t *layout = parser->layout;
const unsigned char *data = abstract->data;
if (abstract->size < layout->headersize)
return DC_STATUS_DATAFORMAT;
dc_ticks_t ts = 0;
if (datetime) {
switch (layout->date_encoding)
{
case DATE_ENCODING_MSDHYM:
datetime->second = data[layout->datetime + 1];
datetime->minute = data[layout->datetime + 0];
datetime->hour = data[layout->datetime + 3];
datetime->day = data[layout->datetime + 2];
datetime->month = data[layout->datetime + 5];
datetime->year = data[layout->datetime + 4] + (data[layout->datetime + 4] > 91 ? 1900 : 2000);
datetime->timezone = DC_TIMEZONE_NONE;
break;
case DATE_ENCODING_SMHDMY:
datetime->second = data[layout->datetime + 0];
datetime->minute = data[layout->datetime + 1];
datetime->hour = data[layout->datetime + 2];
datetime->day = data[layout->datetime + 3];
datetime->month = data[layout->datetime + 4];
datetime->year = data[layout->datetime + 5] + (data[layout->datetime + 5] > 91 ? 1900 : 2000);
datetime->timezone = DC_TIMEZONE_NONE;
break;
case DATE_ENCODING_TICKS:
ts = array_uint32_le(data + layout->datetime) + COCHRAN_EPOCH;
dc_datetime_localtime(datetime, ts);
break;
}
}
return DC_STATUS_SUCCESS;
}
static dc_status_t
cochran_commander_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
{
const cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
const cochran_parser_layout_t *layout = parser->layout;
const unsigned char *data = abstract->data;
unsigned int minutes = 0, qfeet = 0;
dc_gasmix_t *gasmix = (dc_gasmix_t *) value;
dc_salinity_t *water = (dc_salinity_t *) value;
if (abstract->size < layout->headersize)
return DC_STATUS_DATAFORMAT;
if (value) {
switch (type) {
case DC_FIELD_TEMPERATURE_SURFACE:
*((double *) value) = (data[layout->start_temp] - 32.0) / 1.8;
break;
case DC_FIELD_TEMPERATURE_MINIMUM:
if (data[layout->min_temp] == 0xFF)
return DC_STATUS_UNSUPPORTED;
*((double *) value) = (data[layout->min_temp] / 2.0 + 20 - 32) / 1.8;
break;
case DC_FIELD_TEMPERATURE_MAXIMUM:
if (layout->max_temp == UNSUPPORTED)
return DC_STATUS_UNSUPPORTED;
if (data[layout->max_temp] == 0xFF)
return DC_STATUS_UNSUPPORTED;
*((double *) value) = (data[layout->max_temp] / 2.0 + 20 - 32) / 1.8;
break;
case DC_FIELD_DIVETIME:
minutes = array_uint16_le(data + layout->divetime);
if (minutes == 0xFFFF)
return DC_STATUS_UNSUPPORTED;
*((unsigned int *) value) = minutes * 60;
break;
case DC_FIELD_MAXDEPTH:
qfeet = array_uint16_le(data + layout->max_depth);
if (qfeet == 0xFFFF)
return DC_STATUS_UNSUPPORTED;
*((double *) value) = qfeet / 4.0 * FEET;
break;
case DC_FIELD_AVGDEPTH:
qfeet = array_uint16_le(data + layout->avg_depth);
if (qfeet == 0xFFFF)
return DC_STATUS_UNSUPPORTED;
*((double *) value) = qfeet / 4.0 * FEET;
break;
case DC_FIELD_GASMIX_COUNT:
*((unsigned int *) value) = 2;
break;
case DC_FIELD_GASMIX:
// Gas percentages are decimal and encoded as
// highbyte = integer portion
// lowbyte = decimal portion, divide by 256 to get decimal value
gasmix->oxygen = array_uint16_le (data + layout->oxygen + 2 * flags) / 256.0 / 100;
if (layout->helium == UNSUPPORTED) {
gasmix->helium = 0;
} else {
gasmix->helium = array_uint16_le (data + layout->helium + 2 * flags) / 256.0 / 100;
}
gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
break;
case DC_FIELD_SALINITY:
// 0x00 = low conductivity, 0x10 = high, maybe there's a 0x01 and 0x11?
// Assume Cochran's conductivity ranges from 0 to 3
// 0 is fresh water, anything else is sea water
// for density assume
// 0 = 1000kg/m³, 2 = 1025kg/m³
// and other values are linear
if (layout->water_conductivity == UNSUPPORTED)
return DC_STATUS_UNSUPPORTED;
if ((data[layout->water_conductivity] & 0x3) == 0)
water->type = DC_WATER_FRESH;
else
water->type = DC_WATER_SALT;
water->density = 1000.0 + 12.5 * (data[layout->water_conductivity] & 0x3);
break;
case DC_FIELD_ATMOSPHERIC:
// Cochran measures air pressure and stores it as altitude.
// Convert altitude (measured in 1/4 kilofeet) back to pressure.
if (layout->altitude == UNSUPPORTED)
return DC_STATUS_UNSUPPORTED;
*(double *) value = ATM / BAR * pow(1 - 0.0000225577 * data[layout->altitude] * 250.0 * FEET, 5.25588);
break;
default:
return DC_STATUS_UNSUPPORTED;
}
}
return DC_STATUS_SUCCESS;
}
/*
* Parse early Commander computers
*/
static dc_status_t
cochran_commander_parser_samples_foreach_tm (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
const cochran_parser_layout_t *layout = parser->layout;
const unsigned char *data = abstract->data;
const unsigned char *samples = data + layout->headersize;
if (abstract->size < layout->headersize)
return DC_STATUS_DATAFORMAT;
unsigned int size = abstract->size - layout->headersize;
unsigned int sample_interval = data[layout->pt_sample_interval];
dc_sample_value_t sample = {0};
unsigned int time = 0, last_sample_time = 0;
unsigned int offset = 2;
unsigned int deco_ceiling = 0;
unsigned int temp = samples[0]; // Half degrees F
unsigned int depth = samples[1]; // Half feet
last_sample_time = sample.time = time;
if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
sample.depth = (depth / 2.0) * FEET;
if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
sample.temperature = (temp / 2.0 - 32.0) / 1.8;
if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
sample.gasmix = 0;
if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
while (offset < size) {
const unsigned char *s = samples + offset;
sample.time = time;
if (last_sample_time != sample.time) {
// We haven't issued this time yet.
last_sample_time = sample.time;
if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
}
if (*s & 0x80) {
// Event or temperate change byte
if (*s & 0x60) {
// Event byte
switch (*s) {
case 0xC5: // Deco obligation begins
break;
case 0xD8: // Deco obligation ends
break;
case 0xAB: // Decrement ceiling (deeper)
deco_ceiling += 10; // feet
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.time = 60; // We don't know the duration
sample.deco.depth = deco_ceiling * FEET;
if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
break;
case 0xAD: // Increment ceiling (shallower)
deco_ceiling -= 10; // feet
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.depth = deco_ceiling * FEET;
sample.deco.time = 60; // We don't know the duration
if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
break;
default:
cochran_commander_handle_event(parser, s[0], callback, userdata);
break;
}
} else {
// Temp change
if (*s & 0x10)
temp -= (*s & 0x0f);
else
temp += (*s & 0x0f);
sample.temperature = (temp / 2.0 - 32.0) / 1.8;
if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
}
offset++;
continue;
}
// Depth sample
if (s[0] & 0x40)
depth -= s[0] & 0x3f;
else
depth += s[0] & 0x3f;
sample.depth = (depth / 2.0) * FEET;
if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
offset++;
time += sample_interval;
}
return DC_STATUS_SUCCESS;
}
/*
* Parse Commander I (Pre-21000 s/n), II and EMC computers
*/
static dc_status_t
cochran_commander_parser_samples_foreach_emc (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
const cochran_parser_layout_t *layout = parser->layout;
const unsigned char *data = abstract->data;
const unsigned char *samples = data + layout->headersize;
const unsigned char *last_sample = NULL;
if (abstract->size < layout->headersize)
return DC_STATUS_DATAFORMAT;
unsigned int size = abstract->size - layout->headersize;
dc_sample_value_t sample = {0};
unsigned int time = 0, last_sample_time = 0;
unsigned int offset = 0;
double start_depth = 0;
int depth = 0;
unsigned int deco_obligation = 0;
unsigned int deco_ceiling = 0;
unsigned int corrupt_dive = 0;
// In rare circumstances Cochran computers won't record the end-of-dive
// log entry block. When the end-sample pointer is 0xFFFFFFFF it's corrupt.
// That means we don't really know where the dive samples end and we don't
// know what the dive summary values are (i.e. max depth, min temp)
if (array_uint32_le(data + layout->pt_profile_end) == 0xFFFFFFFF) {
corrupt_dive = 1;
dc_datetime_t d;
cochran_commander_parser_get_datetime(abstract, &d);
WARNING(abstract->context, "Incomplete dive on %02d/%02d/%02d at %02d:%02d:%02d, trying to parse samples",
d.year, d.month, d.day, d.hour, d.minute, d.second);
// Eliminate inter-dive events
size = cochran_commander_backparse(parser, samples, size);
}
// Cochran samples depth every second and varies between ascent rate
// and temp every other second.
// Prime values from the dive log section
if (parser->model == COCHRAN_MODEL_COMMANDER_AIR_NITROX ||
parser->model == COCHRAN_MODEL_COMMANDER_PRE21000) {
// Commander stores start depth in quarter-feet
start_depth = array_uint16_le (data + layout->start_depth) / 4.0;
} else {
// EMC stores start depth in 256ths of a foot.
start_depth = array_uint16_le (data + layout->start_depth) / 256.0;
}
last_sample_time = sample.time = time;
if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
sample.depth = start_depth * FEET;
if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
sample.temperature = (data[layout->start_temp] - 32.0) / 1.8;
if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
sample.gasmix = 0;
if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
unsigned int last_gasmix = sample.gasmix;
while (offset < size) {
const unsigned char *s = samples + offset;
sample.time = time;
if (last_sample_time != sample.time) {
// We haven't issued this time yet.
last_sample_time = sample.time;
if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
}
// If corrupt_dive end before offset
if (corrupt_dive) {
// When we aren't sure where the sample data ends we can
// look for events that shouldn't be in the sample data.
// 0xFF is unwritten memory
// 0xA8 indicates start of post-dive interval
// 0xE3 (switch to FO2 mode) and 0xF3 (switch to blend 1) occur
// at dive start so when we see them after the first second we
// found the beginning of the next dive.
if (s[0] == 0xFF || s[0] == 0xA8) {
DEBUG(abstract->context, "Used corrupt dive breakout 1 on event %02x", s[0]);
break;
}
if (time > 1 && (s[0] == 0xE3 || s[0] == 0xF3)) {
DEBUG(abstract->context, "Used corrupt dive breakout 2 on event %02x", s[0]);
break;
}
}
// Check for event
if (s[0] & 0x80) {
offset += cochran_commander_handle_event(parser, s[0], callback, userdata);
// Events indicating change in deco status
switch (s[0]) {
case 0xC5: // Deco obligation begins
deco_obligation = 1;
break;
case 0xD8: // Deco obligation ends
deco_obligation = 0;
break;
case 0xAB: // Decrement ceiling (deeper)
deco_ceiling += 10; // feet
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.time = (array_uint16_le(s + 3) + 1) * 60;
sample.deco.depth = deco_ceiling * FEET;
if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
break;
case 0xAD: // Increment ceiling (shallower)
deco_ceiling -= 10; // feet
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.depth = deco_ceiling * FEET;
sample.deco.time = (array_uint16_le(s + 3) + 1) * 60;
if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
break;
case 0xC0: // Switched to FO2 21% mode (surface)
// Event seen upon surfacing
break;
case 0xCD: // Switched to deco blend
case 0xEF: // Switched to gas blend 2
if (last_gasmix != 1) {
sample.gasmix = 1;
if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
last_gasmix = sample.gasmix;
}
break;
case 0xF3: // Switched to gas blend 1
if (last_gasmix != 0) {
sample.gasmix = 0;
if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
last_gasmix = sample.gasmix;
}
break;
}
continue;
}
// Make sure we have a full sample
if (offset + layout->samplesize > size)
break;
// Depth is logged as change in feet, bit 0x40 means negative depth
if (s[0] & 0x40)
depth -= (s[0] & 0x3f);
else
depth += (s[0] & 0x3f);
sample.depth = (start_depth + depth / 4.0) * FEET;
if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
// Ascent rate is logged in the 0th sample, temp in the 1st, repeat.
if (time % 2 == 0) {
// Ascent rate
double ascent_rate = 0.0;
if (s[1] & 0x80)
ascent_rate = (s[1] & 0x7f);
else
ascent_rate = -(s[1] & 0x7f);
ascent_rate *= FEET / 4.0;
} else {
// Temperature logged in half degrees F above 20
double temperature = s[1] / 2.0 + 20.0;
sample.temperature = (temperature - 32.0) / 1.8;
if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
}
// Cochran EMC models store NDL and deco stop time
// in the 20th to 23rd sample
if (layout->format == SAMPLE_EMC) {
// Tissue load is recorded across 20 samples, we ignore them
// NDL and deco stop time is recorded across the next 4 samples
// The first 2 are either NDL or stop time at deepest stop (if in deco)
// The next 2 are total deco stop time.
unsigned int deco_time = 0;
switch (time % 24) {
case 21:
deco_time = last_sample[2] + s[2] * 256 + 1;
if (deco_obligation) {
/* Deco time for deepest stop, unused */
} else {
/* Send deco NDL sample */
sample.deco.type = DC_DECO_NDL;
sample.deco.time = deco_time * 60;
sample.deco.depth = 0;
if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
}
break;
case 23:
/* Deco time, total obligation */
deco_time = last_sample[2] + s[2] * 256 + 1;
if (deco_obligation) {
sample.deco.type = DC_DECO_DECOSTOP;
sample.deco.depth = deco_ceiling * FEET;
sample.deco.time = deco_time * 60;
if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
}
break;
}
last_sample = s;
}
time++;
offset += layout->samplesize;
}
return DC_STATUS_SUCCESS;
}
static dc_status_t
cochran_commander_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
if (parser->model == COCHRAN_MODEL_COMMANDER_TM)
return cochran_commander_parser_samples_foreach_tm (abstract, callback, userdata);
else
return cochran_commander_parser_samples_foreach_emc (abstract, callback, userdata);
}