diff --git a/examples/common.c b/examples/common.c index fb7f73e..0b4f95b 100644 --- a/examples/common.c +++ b/examples/common.c @@ -76,6 +76,7 @@ static const backend_table_t g_backends[] = { {"nitekq", DC_FAMILY_DIVERITE_NITEKQ}, {"aqualand", DC_FAMILY_CITIZEN_AQUALAND}, {"idive", DC_FAMILY_DIVESYSTEM_IDIVE}, + {"cochran", DC_FAMILY_COCHRAN_COMMANDER}, }; const char * diff --git a/include/libdivecomputer/Makefile.am b/include/libdivecomputer/Makefile.am index 71887d5..778037f 100644 --- a/include/libdivecomputer/Makefile.am +++ b/include/libdivecomputer/Makefile.am @@ -53,4 +53,6 @@ libdivecomputer_HEADERS = \ citizen.h \ citizen_aqualand.h \ divesystem.h \ - divesystem_idive.h + divesystem_idive.h \ + cochran.h \ + cochran_commander.h diff --git a/include/libdivecomputer/cochran.h b/include/libdivecomputer/cochran.h new file mode 100644 index 0000000..a2a0962 --- /dev/null +++ b/include/libdivecomputer/cochran.h @@ -0,0 +1,27 @@ +/* + * 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 + */ + +#ifndef COCHRAN_H +#define COCHRAN_H + +#include "cochran_commander.h" + +#endif /* COCHRAN_H */ diff --git a/include/libdivecomputer/cochran_commander.h b/include/libdivecomputer/cochran_commander.h new file mode 100644 index 0000000..0cbaf7e --- /dev/null +++ b/include/libdivecomputer/cochran_commander.h @@ -0,0 +1,43 @@ +/* + * 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 + */ + +#ifndef COCHRAN_COMMANDER_H +#define COCHRAN_COMMANDER_H + +#include "context.h" +#include "device.h" +#include "parser.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t +cochran_commander_device_open (dc_device_t **device, dc_context_t *context, const char *name); + +dc_status_t +cochran_commander_parser_create (dc_parser_t **parser, dc_context_t *context, unsigned int model); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* COCHRAN_COMMANDER_H */ diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index 8bede4d..67398d1 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -88,6 +88,8 @@ typedef enum dc_family_t { DC_FAMILY_CITIZEN_AQUALAND = (12 << 16), /* DiveSystem */ DC_FAMILY_DIVESYSTEM_IDIVE = (13 << 16), + /* Cochran */ + DC_FAMILY_COCHRAN_COMMANDER = (14 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/libdivecomputer.pc.in b/libdivecomputer.pc.in index aa45720..9f2e28b 100644 --- a/libdivecomputer.pc.in +++ b/libdivecomputer.pc.in @@ -8,4 +8,5 @@ Description: A library for communication with various dive computers. Version: @VERSION@ Requires.private: @DEPENDENCIES@ Libs: -L${libdir} -ldivecomputer +Libs.private: -lm Cflags: -I${includedir} diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index 3da8750..52781f2 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -210,6 +210,14 @@ RelativePath="..\src\citizen_aqualand_parser.c" > + + + + @@ -524,6 +532,14 @@ RelativePath="..\include\libdivecomputer\citizen_aqualand.h" > + + + + diff --git a/src/Makefile.am b/src/Makefile.am index c029a79..ed7b6f8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -3,7 +3,7 @@ AM_CFLAGS = $(LIBUSB_CFLAGS) -DENABLE_DEPRECATED lib_LTLIBRARIES = libdivecomputer.la -libdivecomputer_la_LIBADD = $(LIBUSB_LIBS) +libdivecomputer_la_LIBADD = $(LIBUSB_LIBS) -lm libdivecomputer_la_LDFLAGS = \ -version-info $(DC_VERSION_LIBTOOL) \ -no-undefined \ @@ -64,7 +64,9 @@ libdivecomputer_la_SOURCES = \ ringbuffer.h ringbuffer.c \ checksum.h checksum.c \ array.h array.c \ - buffer.c + buffer.c \ + cochran_commander.h cochran_commander.c \ + cochran_commander_parser.c if OS_WIN32 libdivecomputer_la_SOURCES += serial.h serial_win32.c diff --git a/src/cochran_commander.c b/src/cochran_commander.c new file mode 100644 index 0000000..02ec1cb --- /dev/null +++ b/src/cochran_commander.c @@ -0,0 +1,888 @@ +/* + * 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 // memcpy, memcmp +#include // malloc, free +#include // assert + +#include + +#include "context-private.h" +#include "device-private.h" +#include "serial.h" +#include "array.h" + +#define C_ARRAY_SIZE(array) (sizeof (array) / sizeof *(array)) + +#define EXITCODE(rc) \ +( \ + rc == -1 ? DC_STATUS_IO : DC_STATUS_TIMEOUT \ +) + +#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 + +typedef enum cochran_endian_t { + ENDIAN_LE, + ENDIAN_BE, +} cochran_endian_t; + +typedef struct cochran_commander_model_t { + unsigned char id[8 + 1]; + unsigned int model; +} cochran_commander_model_t; + +typedef struct cochran_data_t { + unsigned char config[1024]; + unsigned char *logbook; + unsigned char *sample; + + unsigned short int dive_count; + int fp_dive_num; + + unsigned int logbook_size; + + unsigned int sample_data_offset; + unsigned int sample_size; +} cochran_data_t; + +typedef struct cochran_device_layout_t { + unsigned int model; + unsigned int address_bits; + cochran_endian_t endian; + unsigned int baudrate; + // Config data. + unsigned int cf_dive_count; + unsigned int cf_last_log; + unsigned int cf_last_interdive; + unsigned int cf_serial_number; + // Logbook ringbuffer. + unsigned int rb_logbook_begin; + unsigned int rb_logbook_end; + unsigned int rb_logbook_entry_size; + unsigned int rb_logbook_entry_count; + // Profile ringbuffer. + unsigned int rb_profile_begin; + unsigned int rb_profile_end; + // Profile pointers. + unsigned int pt_profile_pre; + unsigned int pt_profile_begin; + unsigned int pt_profile_end; +} cochran_device_layout_t; + +typedef struct cochran_commander_device_t { + dc_device_t base; + serial_t *port; + const cochran_device_layout_t *layout; + unsigned char id[67]; + unsigned char fingerprint[6]; +} cochran_commander_device_t; + +static dc_status_t cochran_commander_device_set_fingerprint (dc_device_t *device, const unsigned char data[], unsigned int size); +static dc_status_t cochran_commander_device_read (dc_device_t *device, unsigned int address, unsigned char data[], unsigned int size); +static dc_status_t cochran_commander_device_dump (dc_device_t *device, dc_buffer_t *data); +static dc_status_t cochran_commander_device_foreach (dc_device_t *device, dc_dive_callback_t callback, void *userdata); +static dc_status_t cochran_commander_device_close (dc_device_t *device); + +static const dc_device_vtable_t cochran_commander_device_vtable = { + sizeof (cochran_commander_device_t), + DC_FAMILY_COCHRAN_COMMANDER, + cochran_commander_device_set_fingerprint,/* set_fingerprint */ + cochran_commander_device_read, /* read */ + NULL, /* write */ + cochran_commander_device_dump, /* dump */ + cochran_commander_device_foreach, /* foreach */ + cochran_commander_device_close /* close */ +}; + +// Cochran Commander Nitrox +static const cochran_device_layout_t cochran_cmdr_device_layout = { + COCHRAN_MODEL_COMMANDER_AIR_NITROX, // model + 24, // address_bits + ENDIAN_BE, // endian + 115200, // baudrate + 0x046, // cf_dive_count + 0x06E, // cf_last_log + 0x200, // cf_last_interdive + 0x0AA, // cf_serial_number + 0x00000000, // rb_logbook_begin + 0x00020000, // rb_logbook_end + 256, // rb_logbook_entry_size + 512, // rb_logbook_entry_count + 0x00020000, // rb_profile_begin + 0x00100000, // rb_profile_end + 30, // pt_profile_pre + 6, // pt_profile_begin + 128, // pt_profile_end +}; + +// Cochran EMC-14 +static const cochran_device_layout_t cochran_emc14_device_layout = { + COCHRAN_MODEL_EMC_14, // model + 32, // address_bits + ENDIAN_LE, // endian + 806400, // baudrate + 0x0D2, // cf_dive_count + 0x13E, // cf_last_log + 0x142, // cf_last_interdive + 0x1E6, // cf_serial_number + 0x00000000, // rb_logbook_begin + 0x00020000, // rb_logbook_end + 512, // rb_logbook_entry_size + 256, // rb_logbook_entry_count + 0x00022000, // rb_profile_begin + 0x00200000, // rb_profile_end + 30, // pt_profile_pre + 6, // pt_profile_begin + 256, // pt_profile_end +}; + +// Cochran EMC-16 +static const cochran_device_layout_t cochran_emc16_device_layout = { + COCHRAN_MODEL_EMC_16, // model + 32, // address_bits + ENDIAN_LE, // endian + 806400, // baudrate + 0x0D2, // cf_dive_count + 0x13E, // cf_last_log + 0x142, // cf_last_interdive + 0x1E6, // cf_serial_number + 0x00000000, // rb_logbook_begin + 0x00080000, // rb_logbook_end + 512, // rb_logbook_entry_size + 1024, // rb_logbook_entry_count + 0x00094000, // rb_profile_begin + 0x00800000, // rb_profile_end + 30, // pt_profile_pre + 6, // pt_profile_begin + 256, // pt_profile_end +}; + +// Cochran EMC-20 +static const cochran_device_layout_t cochran_emc20_device_layout = { + COCHRAN_MODEL_EMC_20, // model + 32, // address_bits + ENDIAN_LE, // endian + 806400, // baudrate + 0x0D2, // cf_dive_count + 0x13E, // cf_last_log + 0x142, // cf_last_interdive + 0x1E6, // cf_serial_number + 0x00000000, // rb_logbook_begin + 0x00080000, // rb_logbook_end + 512, // rb_logbook_entry_size + 1024, // rb_logbook_entry_count + 0x00094000, // rb_profile_begin + 0x01000000, // rb_profile_end + 30, // pt_profile_pre + 6, // pt_profile_begin + 256, // pt_profile_end +}; + + +// Determine model descriptor number from model string +static unsigned int +cochran_commander_get_model (cochran_commander_device_t *device) +{ + const cochran_commander_model_t models[] = { + {"AM\x11""2212\x02", COCHRAN_MODEL_COMMANDER_AIR_NITROX}, + {"AM7303\x8b\x43", COCHRAN_MODEL_EMC_14}, + {"AMA315\xC3\xC5", COCHRAN_MODEL_EMC_16}, + {"AM2315\xA3\x71", COCHRAN_MODEL_EMC_20}, + }; + + unsigned int model = 0xFFFFFFFF; + for (unsigned int i = 0; i < C_ARRAY_SIZE(models); ++i) { + if (memcmp (device->id + 0x3B, models[i].id, sizeof(models[i].id) - 1) == 0) { + model = models[i].model; + break; + } + } + + return model; +} + + +static dc_status_t +cochran_commander_serial_setup (cochran_commander_device_t *device) +{ + // Set the serial communication protocol (9600 8N2, no FC). + int rc = serial_configure (device->port, 9600, 8, SERIAL_PARITY_NONE, 2, SERIAL_FLOWCONTROL_NONE); + if (rc == -1) { + ERROR (device->base.context, "Failed to set the terminal attributes."); + return DC_STATUS_IO; + } + + // Set the timeout for receiving data (5000 ms). + if (serial_set_timeout (device->port, 5000) == -1) { + ERROR (device->base.context, "Failed to set the timeout."); + return DC_STATUS_IO; + } + + // Wake up DC and trigger heartbeat + serial_set_break(device->port, 1); + serial_sleep(device->port, 16); + serial_set_break(device->port, 0); + + // Clear old heartbeats + serial_flush (device->port, SERIAL_QUEUE_BOTH); + + // Wait for heartbeat byte before send + unsigned char answer = 0; + int n = serial_read(device->port, &answer, 1); + if (n != 1) { + ERROR (device->base.context, "Failed to receive device heartbeat."); + return EXITCODE (n); + } + + if (answer != 0xAA) { + ERROR (device->base.context, "Received bad hearbeat byte (%02x).", answer); + return DC_STATUS_PROTOCOL; + } + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_packet (cochran_commander_device_t *device, dc_event_progress_t *progress, + const unsigned char command[], unsigned int csize, + unsigned char answer[], unsigned int asize, int high_speed) +{ + dc_device_t *abstract = (dc_device_t *) device; + + if (device_is_cancelled (abstract)) + return DC_STATUS_CANCELLED; + + // Send the command to the device, one byte at a time + // If sent all at once the command is ignored. It's like the DC + // has no buffering. + for (unsigned int i = 0; i < csize; i++) { + // Give the DC time to read the character. + if (i) serial_sleep(device->port, 16); // 16 ms + + unsigned int n = serial_write(device->port, command + i, 1); + if (n != 1) { + ERROR (abstract->context, "Failed to send the command."); + return EXITCODE (n); + } + } + + if (high_speed) { + // Give the DC time to process the command. + serial_sleep(device->port, 45); + + // Rates are odd, like 806400 for the EMC, 115200 for commander + int rc = serial_configure(device->port, device->layout->baudrate, 8, SERIAL_PARITY_NONE, 2, SERIAL_FLOWCONTROL_NONE); + if (rc == -1) { + ERROR (abstract->context, "Failed to set the high baud rate."); + return DC_STATUS_IO; + } + } + + // Receive the answer from the device. + // Use 1024 byte "packets" so we can display progress. + unsigned int nbytes = 0; + while (nbytes < asize) { + unsigned int len = asize - nbytes; + if (len > 1024) + len = 1024; + + int n = serial_read (device->port, answer + nbytes, len); + if (n != len) { + ERROR (abstract->context, "Failed to receive data, expected %u," + "read %u.",len, n); + return EXITCODE (n); + } + + nbytes += n; + + if (progress) { + progress->current += n; + device_event_emit (abstract, DC_EVENT_PROGRESS, progress); + } + } + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_read_id (cochran_commander_device_t *device, unsigned char id[], unsigned int size) +{ + dc_status_t rc = DC_STATUS_SUCCESS; + + unsigned char command[6] = {0x05, 0x9D, 0xFF, 0x00, 0x43, 0x00}; + + rc = cochran_commander_packet(device, NULL, command, sizeof(command), id, size, 0); + if (rc != DC_STATUS_SUCCESS) + return rc; + + if (memcmp(id, "(C)", 3) != 0) { + // It's a Commander, read a different location + command[1] = 0xBD; + command[2] = 0x7F; + + rc = cochran_commander_packet(device, NULL, command, sizeof(command), id, size, 0); + if (rc != DC_STATUS_SUCCESS) + return rc; + } + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_read_config (cochran_commander_device_t *device, dc_event_progress_t *progress, unsigned char data[], unsigned int size) +{ + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t rc = DC_STATUS_SUCCESS; + + // Read two 512 byte blocks into one 1024 byte buffer + for (unsigned int i = 0; i < 2; i++) { + const unsigned int len = size / 2; + + unsigned char command[2] = {0x96, i}; + rc = cochran_commander_packet(device, progress, command, sizeof(command), data + i * len, len, 0); + if (rc != DC_STATUS_SUCCESS) + return rc; + + dc_event_vendor_t vendor; + vendor.data = data + i * len; + vendor.size = len; + device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); + } + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_read (cochran_commander_device_t *device, dc_event_progress_t *progress, unsigned int address, unsigned char data[], unsigned int size) +{ + dc_status_t rc = DC_STATUS_SUCCESS; + + // Build the command + unsigned char command[10]; + unsigned char command_size; + + switch (device->layout->address_bits) { + case 32: + // EMC uses 32 bit addressing + command[0] = 0x15; + command[1] = (address ) & 0xff; + command[2] = (address >> 8) & 0xff; + command[3] = (address >> 16) & 0xff; + command[4] = (address >> 24) & 0xff; + command[5] = (size ) & 0xff; + command[6] = (size >> 8 ) & 0xff; + command[7] = (size >> 16 ) & 0xff; + command[8] = (size >> 24 ) & 0xff; + command[9] = 0x05; + command_size = 10; + break; + case 24: + // Commander uses 24 byte addressing + command[0] = 0x15; + command[1] = (address ) & 0xff; + command[2] = (address >> 8) & 0xff; + command[3] = (address >> 16) & 0xff; + command[4] = (size ) & 0xff; + command[5] = (size >> 8 ) & 0xff; + command[6] = (size >> 16 ) & 0xff; + command[7] = 0x04; + command_size = 8; + break; + default: + return DC_STATUS_UNSUPPORTED; + } + + serial_sleep(device->port, 800); + + // set back to 9600 baud + rc = cochran_commander_serial_setup(device); + if (rc != DC_STATUS_SUCCESS) + return rc; + + // Read data at high speed + rc = cochran_commander_packet (device, progress, command, command_size, data, size, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + + return DC_STATUS_SUCCESS; +} + + +static void +cochran_commander_find_fingerprint(cochran_commander_device_t *device, cochran_data_t *data) +{ + // Skip to fingerprint to reduce time + if (data->dive_count < device->layout->rb_logbook_entry_count) + data->fp_dive_num = data->dive_count; + else + data->fp_dive_num = device->layout->rb_logbook_entry_count; + data->fp_dive_num--; + + while (data->fp_dive_num >= 0 && memcmp(device->fingerprint, + data->logbook + data->fp_dive_num * device->layout->rb_logbook_entry_size, + sizeof(device->fingerprint))) + data->fp_dive_num--; +} + + +static void +cochran_commander_get_sample_parms(cochran_commander_device_t *device, cochran_data_t *data) +{ + dc_device_t *abstract = (dc_device_t *) device; + unsigned int pre_dive_offset = 0, end_dive_offset = 0; + + unsigned int dive_count = 0; + if (data->dive_count < device->layout->rb_logbook_entry_count) + dive_count = data->dive_count; + else + dive_count = device->layout->rb_logbook_entry_count; + + // Find lowest and highest offsets into sample data + unsigned int low_offset = 0xFFFFFFFF; + unsigned int high_offset = 0; + + for (int i = data->fp_dive_num + 1; i < dive_count; i++) { + pre_dive_offset = array_uint32_le (data->logbook + i * device->layout->rb_logbook_entry_size + + device->layout->pt_profile_pre); + end_dive_offset = array_uint32_le (data->logbook + i * device->layout->rb_logbook_entry_size + + device->layout->pt_profile_end); + + // Validate offsets, allow 0xFFFFFFF for end_dive_offset + // because we handle that as a special case. + if (pre_dive_offset < device->layout->rb_profile_begin || + pre_dive_offset > device->layout->rb_profile_end) { + ERROR(abstract->context, "Invalid pre-dive offset (%08x) on dive %d.", pre_dive_offset, i); + continue; + } + + if (end_dive_offset < device->layout->rb_profile_begin || + (end_dive_offset > device->layout->rb_profile_end && + end_dive_offset != 0xFFFFFFFF)) { + ERROR(abstract->context, "Invalid end-dive offset (%08x) on dive %d.", end_dive_offset, i); + continue; + } + + // Check for ring buffer wrap-around. + if (pre_dive_offset > end_dive_offset) + break; + + if (pre_dive_offset < low_offset) + low_offset = pre_dive_offset; + if (end_dive_offset > high_offset && end_dive_offset != 0xFFFFFFFF ) + high_offset = end_dive_offset; + } + + if (pre_dive_offset > end_dive_offset) { + high_offset = device->layout->rb_profile_end; + low_offset = device->layout->rb_profile_begin; + data->sample_data_offset = low_offset; + data->sample_size = high_offset - low_offset; + } else if (low_offset < 0xFFFFFFFF && high_offset > 0) { + data->sample_data_offset = low_offset; + data->sample_size = high_offset - data->sample_data_offset; + } else { + data->sample_data_offset = 0; + data->sample_size = 0; + } +} + + +/* + * For corrupt dives the end-of-samples pointer is 0xFFFFFFFF + * search for a reasonable size, e.g. using next dive start sample + * or end-of-samples to limit searching for recoverable samples + */ +static unsigned int +cochran_commander_guess_sample_end_address(cochran_commander_device_t *device, cochran_data_t *data, unsigned int log_num) +{ + const unsigned char *log_entry = data->logbook + device->layout->rb_logbook_entry_size * log_num; + + if (log_num == data->dive_count) + // Return next usable address from config page + return array_uint32_le(data->config + device->layout->rb_profile_end); + + // Next log's start address + return array_uint32_le(log_entry + device->layout->rb_logbook_entry_size + device->layout->pt_profile_begin); +} + + +static dc_status_t +cochran_commander_read_all (cochran_commander_device_t *device, cochran_data_t *data) +{ + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t rc = DC_STATUS_SUCCESS; + + // Calculate max data sizes + unsigned int max_config = sizeof(data->config); + unsigned int max_logbook = device->layout->rb_logbook_end - device->layout->rb_logbook_begin; + unsigned int max_sample = device->layout->rb_profile_end - device->layout->rb_profile_begin; + + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + progress.maximum = max_config + max_logbook + max_sample; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Emit ID block + dc_event_vendor_t vendor; + vendor.data = device->id; + vendor.size = sizeof (device->id); + device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); + + // Read config + rc = cochran_commander_read_config(device, &progress, data->config, sizeof(data->config)); + if (rc != DC_STATUS_SUCCESS) + return rc; + + // Determine size of dive list to read. + if (device->layout->endian == ENDIAN_LE) + data->dive_count = array_uint16_le (data->config + device->layout->cf_dive_count); + else + data->dive_count = array_uint16_be (data->config + device->layout->cf_dive_count); + + if (data->dive_count > device->layout->rb_logbook_entry_count) { + data->logbook_size = device->layout->rb_logbook_entry_count * device->layout->rb_logbook_entry_size; + } else { + data->logbook_size = data->dive_count * device->layout->rb_logbook_entry_size; + } + + progress.maximum -= max_logbook - data->logbook_size; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Allocate space for log book. + data->logbook = (unsigned char *) malloc(data->logbook_size); + if (data->logbook == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Request log book + rc = cochran_commander_read(device, &progress, 0, data->logbook, data->logbook_size); + if (rc != DC_STATUS_SUCCESS) + return rc; + + // Determine sample memory to read + cochran_commander_find_fingerprint(device, data); + cochran_commander_get_sample_parms(device, data); + + progress.maximum -= max_sample - data->sample_size; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + if (data->sample_size > 0) { + data->sample = (unsigned char *) malloc(data->sample_size); + if (data->sample == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Read the sample data + rc = cochran_commander_read (device, &progress, data->sample_data_offset, data->sample, data->sample_size); + if (rc != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the sample data."); + return rc; + } + } + + return DC_STATUS_SUCCESS; +} + +dc_status_t +cochran_commander_device_open (dc_device_t **out, dc_context_t *context, const char *name) +{ + dc_status_t status = DC_STATUS_SUCCESS; + cochran_commander_device_t *device = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + device = (cochran_commander_device_t *) dc_device_allocate (context, &cochran_commander_device_vtable); + if (device == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + device->port = NULL; + cochran_commander_device_set_fingerprint((dc_device_t *) device, NULL, 0); + + // Open the device. + int rc = serial_open (&device->port, device->base.context, name); + if (rc == -1) { + ERROR (device->base.context, "Failed to open the serial port."); + status = DC_STATUS_IO; + goto error_free; + } + + status = cochran_commander_serial_setup(device); + if (status != DC_STATUS_SUCCESS) { + goto error_close; + } + + // Read ID from the device + status = cochran_commander_read_id (device, device->id, sizeof(device->id)); + if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Device not responding."); + goto error_close; + } + + unsigned int model = cochran_commander_get_model(device); + switch (model) { + case COCHRAN_MODEL_COMMANDER_AIR_NITROX: + device->layout = &cochran_cmdr_device_layout; + break; + case COCHRAN_MODEL_EMC_14: + device->layout = &cochran_emc14_device_layout; + break; + case COCHRAN_MODEL_EMC_16: + device->layout = &cochran_emc16_device_layout; + break; + case COCHRAN_MODEL_EMC_20: + device->layout = &cochran_emc20_device_layout; + break; + default: + ERROR (context, "Unknown model"); + status = DC_STATUS_UNSUPPORTED; + goto error_close; + } + + *out = (dc_device_t *) device; + + return DC_STATUS_SUCCESS; + +error_close: + serial_close (device->port); +error_free: + dc_device_deallocate ((dc_device_t *) device); + return status; +} + +static dc_status_t +cochran_commander_device_close (dc_device_t *abstract) +{ + dc_status_t status = DC_STATUS_SUCCESS; + cochran_commander_device_t *device = (cochran_commander_device_t *) abstract; + + // Close the device. + if (serial_close (device->port) == -1) { + dc_status_set_error(&status, DC_STATUS_IO); + } + + return status; +} + +static dc_status_t +cochran_commander_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size) +{ + cochran_commander_device_t *device = (cochran_commander_device_t *) abstract; + + if (size && size != sizeof (device->fingerprint)) + return DC_STATUS_INVALIDARGS; + + if (size) + memcpy (device->fingerprint, data, sizeof (device->fingerprint)); + else + memset (device->fingerprint, 0xFF, sizeof (device->fingerprint)); + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size) +{ + cochran_commander_device_t *device = (cochran_commander_device_t *) abstract; + + return cochran_commander_read(device, NULL, address, data, size); +} + + +static dc_status_t +cochran_commander_device_dump (dc_device_t *abstract, dc_buffer_t *buffer) +{ + cochran_commander_device_t *device = (cochran_commander_device_t *) abstract; + dc_status_t rc = DC_STATUS_SUCCESS; + unsigned char config[1024]; + + // Make sure buffer is good. + if (!dc_buffer_clear(buffer)) { + ERROR (abstract->context, "Uninitialized buffer."); + return DC_STATUS_INVALIDARGS; + } + + // Reserve space + if (!dc_buffer_resize(buffer, device->layout->rb_profile_end)) { + ERROR(abstract->context, "Insufficient buffer space available."); + return DC_STATUS_NOMEMORY; + } + + // Determine size for progress + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + progress.maximum = sizeof(config) + device->layout->rb_profile_end; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Emit ID block + dc_event_vendor_t vendor; + vendor.data = device->id; + vendor.size = sizeof (device->id); + device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); + + rc = cochran_commander_read_config (device, &progress, config, sizeof(config)); + if (rc != DC_STATUS_SUCCESS) + return rc; + + // Read the sample data, from 0 to sample end will include logbook + rc = cochran_commander_read (device, &progress, 0, dc_buffer_get_data(buffer), device->layout->rb_profile_end); + if (rc != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the sample data."); + return rc; + } + + return DC_STATUS_SUCCESS; +} + + +static dc_status_t +cochran_commander_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + cochran_commander_device_t *device = (cochran_commander_device_t *) abstract; + dc_status_t status = DC_STATUS_SUCCESS; + + cochran_data_t data; + data.logbook = NULL; + data.sample = NULL; + status = cochran_commander_read_all (device, &data); + if (status != DC_STATUS_SUCCESS) + goto error; + + // Emit a device info event. + dc_event_devinfo_t devinfo; + devinfo.model = device->layout->model; + devinfo.firmware = 0; // unknown + devinfo.serial = array_uint32_le(data.config + device->layout->cf_serial_number); + device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); + + // Calculate profile RB effective head pointer + // Cochran seems to erase 8K chunks so round up. + unsigned int last_start_address = (array_uint32_le(data.config + device->layout->cf_last_interdive) & 0xfffff000) + 0x2000; + if (last_start_address < device->layout->rb_profile_begin || last_start_address > device->layout->rb_profile_end) { + ERROR(abstract->context, "Invalid profile ringbuffer head pointer in Cochran config block."); + status = DC_STATUS_DATAFORMAT; + goto error; + } + + // We track profile ringbuffer usage to determine which dives have profile data + int profile_capacity_remaining = device->layout->rb_profile_end - device->layout->rb_profile_begin; + + unsigned int dive_count = 0; + if (data.dive_count < device->layout->rb_logbook_entry_count) + dive_count = data.dive_count; + else + dive_count = device->layout->rb_logbook_entry_count; + + // Loop through each dive + for (int i = dive_count - 1; i > data.fp_dive_num; i--) { + unsigned char *log_entry = data.logbook + i * device->layout->rb_logbook_entry_size; + + unsigned int sample_start_address = array_uint32_le (log_entry + device->layout->pt_profile_begin); + unsigned int sample_end_address = array_uint32_le (log_entry + device->layout->pt_profile_end); + + // Validate + if (sample_start_address < device->layout->rb_profile_begin || + sample_start_address > device->layout->rb_profile_end || + sample_end_address < device->layout->rb_profile_begin || + (sample_end_address > device->layout->rb_profile_end && + sample_end_address != 0xFFFFFFFF)) { + continue; + } + + if (sample_end_address == 0xFFFFFFFF) + // Corrupt dive, guess the end address + sample_end_address = cochran_commander_guess_sample_end_address(device, &data, i); + + // Determine if sample exists + if (profile_capacity_remaining > 0) { + // Subtract this dive's profile size including post-dive events + profile_capacity_remaining -= (last_start_address - sample_start_address); + // Adjust for a dive that wraps the buffer + if (sample_start_address > last_start_address) + profile_capacity_remaining -= device->layout->rb_profile_end - device->layout->rb_profile_begin; + } + last_start_address = sample_start_address; + + unsigned char *sample = NULL; + int sample_size = 0; + if (profile_capacity_remaining < 0) { + // There is no profile for this dive + sample = NULL; + sample_size = 0; + } else { + // Calculate the size of the profile only + sample = data.sample + sample_start_address - data.sample_data_offset; + sample_size = sample_end_address - sample_start_address; + + if (sample_size < 0) + // Adjust for ring buffer wrap-around + sample_size += device->layout->rb_profile_end - device->layout->rb_profile_begin; + } + + // Build dive blob + unsigned int dive_size = device->layout->rb_logbook_entry_size + sample_size; + unsigned char *dive = (unsigned char *) malloc(dive_size); + if (dive == NULL) { + status = DC_STATUS_NOMEMORY; + goto error; + } + + memcpy(dive, log_entry, device->layout->rb_logbook_entry_size); // log + + // Copy profile data + if (sample_size) { + if (sample_start_address <= sample_end_address) { + memcpy(dive + device->layout->rb_logbook_entry_size, sample, sample_size); + } else { + // It wrapped the buffer, copy two sections + unsigned int size = device->layout->rb_profile_end - sample_start_address; + + memcpy(dive + device->layout->rb_logbook_entry_size, sample, size); + memcpy(dive + device->layout->rb_logbook_entry_size + size, + data.sample, sample_end_address - device->layout->rb_profile_begin); + } + } + + if (callback && !callback (dive, dive_size, dive, sizeof(device->fingerprint), userdata)) { + free(dive); + break; + } + + free(dive); + } + +error: + free(data.logbook); + free(data.sample); + return status; +} diff --git a/src/cochran_commander_parser.c b/src/cochran_commander_parser.c new file mode 100644 index 0000000..7802ea1 --- /dev/null +++ b/src/cochran_commander_parser.c @@ -0,0 +1,651 @@ +/* + * 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); + + 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; +} diff --git a/src/descriptor.c b/src/descriptor.c index e967d90..23a7fec 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -263,6 +263,10 @@ static const dc_descriptor_t g_descriptors[] = { {"DiveSystem", "iX3M Deep", DC_FAMILY_DIVESYSTEM_IDIVE, 0x23}, {"DiveSystem", "iX3M Tec", DC_FAMILY_DIVESYSTEM_IDIVE, 0x24}, {"DiveSystem", "iX3M Reb", DC_FAMILY_DIVESYSTEM_IDIVE, 0x25}, + {"Cochran", "Commander", DC_FAMILY_COCHRAN_COMMANDER, 0}, + {"Cochran", "EMC-14", DC_FAMILY_COCHRAN_COMMANDER, 1}, + {"Cochran", "EMC-16", DC_FAMILY_COCHRAN_COMMANDER, 2}, + {"Cochran", "EMC-20H", DC_FAMILY_COCHRAN_COMMANDER, 3}, }; typedef struct dc_descriptor_iterator_t { diff --git a/src/device.c b/src/device.c index 31ec8b8..3687463 100644 --- a/src/device.c +++ b/src/device.c @@ -36,6 +36,7 @@ #include #include #include +#include #include "device-private.h" #include "context-private.h" @@ -184,6 +185,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_DIVESYSTEM_IDIVE: rc = divesystem_idive_device_open2 (&device, context, name, dc_descriptor_get_model (descriptor)); break; + case DC_FAMILY_COCHRAN_COMMANDER: + rc = cochran_commander_device_open (&device, context, name); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols index a45c06a..e59888d 100644 --- a/src/libdivecomputer.symbols +++ b/src/libdivecomputer.symbols @@ -71,6 +71,7 @@ diverite_nitekq_parser_create citizen_aqualand_parser_create divesystem_idive_parser_create divesystem_idive_parser_create2 +cochran_commander_parser_create dc_device_open dc_device_close @@ -177,3 +178,4 @@ diverite_nitekq_extract_dives citizen_aqualand_device_open divesystem_idive_device_open divesystem_idive_device_open2 +cochran_commander_device_open diff --git a/src/parser.c b/src/parser.c index 5fb790f..2498a8e 100644 --- a/src/parser.c +++ b/src/parser.c @@ -35,6 +35,7 @@ #include #include #include +#include #include "context-private.h" #include "parser-private.h" @@ -144,6 +145,9 @@ dc_parser_new (dc_parser_t **out, dc_device_t *device) case DC_FAMILY_DIVESYSTEM_IDIVE: rc = divesystem_idive_parser_create2 (&parser, context, device->devinfo.model); break; + case DC_FAMILY_COCHRAN_COMMANDER: + rc = cochran_commander_parser_create (&parser, context, device->devinfo.model); + break; default: return DC_STATUS_INVALIDARGS; }