/* * 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 #include "context-private.h" #include "parser-private.h" #include "array.h" #define C_ARRAY_SIZE(array) (sizeof (array) / sizeof *(array)) #define COCHRAN_MODEL_COMMANDER_AIR_NITROX 0 #define COCHRAN_MODEL_EMC_14 1 #define COCHRAN_MODEL_EMC_16 2 #define COCHRAN_MODEL_EMC_20 3 #define UNSUPPORTED 0xFFFFFFFF typedef enum cochran_sample_format_t { SAMPLE_CMDR, SAMPLE_EMC, } cochran_sample_format_t; typedef struct cochran_parser_layout_t { cochran_sample_format_t format; unsigned int headersize; unsigned int samplesize; unsigned int second, minute, hour, day, month, year; 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_parser_layout = { SAMPLE_CMDR, // type 256, // headersize 2, // samplesize 1, 0, 3, 2, 5, 4, // second, minute, hour, day, month, year, 1 byte each 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, // type 512, // headersize 3, // samplesize 0, 1, 2, 3, 4, 5, // second, minute, hour, day, month, year, 1 byte each 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 {0xBD, 1, SAMPLE_EVENT_NONE, SAMPLE_FLAGS_NONE}, // Switched to nomal PO2 setting {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 {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 {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_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; if (datetime) { datetime->second = data[layout->second]; datetime->minute = data[layout->minute]; datetime->hour = data[layout->hour]; datetime->day = data[layout->day]; datetime->month = data[layout->month]; datetime->year = data[layout->year] + (data[layout->year] > 91 ? 1900 : 2000); } 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: *((unsigned int*) 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; *((unsigned int*) value) = (data[layout->min_temp] / 2.0 + 20 - 32) / 1.8; break; case DC_FIELD_TEMPERATURE_MAXIMUM: if (data[layout->max_temp] == 0xFF) return DC_STATUS_UNSUPPORTED; *((unsigned int*) 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 ((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. *(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; } 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; 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; WARNING(abstract->context, "Incomplete dive on %02d/%02d/%02d at %02d:%02d:%02d, trying to parse samples", data[layout->year], data[layout->month], data[layout->day], data[layout->hour], data[layout->minute], data[layout->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) { // 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); 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); if (layout->format == SAMPLE_EMC) { // EMC models have events indicating change in deco status // Commander may have them but I don't have example data 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 + layout->samplesize) + 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 + layout->samplesize) + 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 sample.gasmix = 1; if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata); break; case 0xF3: // Switched to gas blend 1 sample.gasmix = 0; if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata); 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; }