/* * 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 #include #include #include "cochran_commander.h" #include "context-private.h" #include "parser-private.h" #include "array.h" #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 */ NULL, /* set_clock */ NULL, /* set_atmospheric */ NULL, /* set_density */ 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); }