[PATCH] Added support for Cochran EMC and Commander Air import

John Van Ostrand john at vanostrand.com
Tue Oct 7 09:09:31 PDT 2014


Specifically tested with EMC-20H and Commander Air_Nitrox this
commit should support the families for the most part however
there are may be differences in the DC signature, memory size,
and other factors that will prevent opening the device and
obtaining a clean import. Try it with other EMC and Commander
devices and check library ERRORs for the signature information
needed to add support.

Also missing is tank pressure (for Gemini) and support for logbooks
that have rolled old dives off. There is support for dive samples
that have rolled off.
---
 examples/Makefile.am                |   6 +-
 examples/cochran_download.c         | 299 +++++++++++++
 examples/universal.c                |   8 +-
 include/libdivecomputer/Makefile.am |   3 +-
 include/libdivecomputer/cochran.h   |  61 +++
 include/libdivecomputer/common.h    |   3 +
 include/libdivecomputer/parser.h    |  20 +-
 src/Makefile.am                     |   7 +-
 src/cochran_cmdr.c                  |  66 +++
 src/cochran_cmdr.h                  |  83 ++++
 src/cochran_cmdr_parser.c           | 202 +++++++++
 src/cochran_common.c                | 861 ++++++++++++++++++++++++++++++++++++
 src/cochran_common.h                | 109 +++++
 src/cochran_common_parser.c         | 157 +++++++
 src/cochran_common_parser.h         |  80 ++++
 src/cochran_emc.c                   |  67 +++
 src/cochran_emc.h                   | 180 ++++++++
 src/cochran_emc_parser.c            | 399 +++++++++++++++++
 src/descriptor.c                    |   3 +
 src/device.c                        |   7 +
 src/libdivecomputer.symbols         |   2 +
 src/parser.c                        |   7 +
 22 files changed, 2617 insertions(+), 13 deletions(-)
 create mode 100644 examples/cochran_download.c
 create mode 100644 include/libdivecomputer/cochran.h
 create mode 100644 src/cochran_cmdr.c
 create mode 100644 src/cochran_cmdr.h
 create mode 100644 src/cochran_cmdr_parser.c
 create mode 100644 src/cochran_common.c
 create mode 100644 src/cochran_common.h
 create mode 100644 src/cochran_common_parser.c
 create mode 100644 src/cochran_common_parser.h
 create mode 100644 src/cochran_emc.c
 create mode 100644 src/cochran_emc.h
 create mode 100644 src/cochran_emc_parser.c

diff --git a/examples/Makefile.am b/examples/Makefile.am
index d77db45..10bc2cf 100644
--- a/examples/Makefile.am
+++ b/examples/Makefile.am
@@ -1,9 +1,11 @@
 AM_CPPFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include
 LDADD = $(top_builddir)/src/libdivecomputer.la
+AM_LDFLAGS = -lm
 
 bin_PROGRAMS = \
 	universal \
-	ostc-fwupdate
+	ostc-fwupdate \
+	cochran_download
 
 COMMON = common.c common.h \
 	utils.c utils.h
@@ -11,3 +13,5 @@ COMMON = common.c common.h \
 universal_SOURCES = universal.c $(COMMON)
 
 ostc_fwupdate_SOURCES = hw_ostc_fwupdate.c $(COMMON)
+
+cochran_download_SOURCES = cochran_download.c
diff --git a/examples/cochran_download.c b/examples/cochran_download.c
new file mode 100644
index 0000000..b42eb91
--- /dev/null
+++ b/examples/cochran_download.c
@@ -0,0 +1,299 @@
+/*
+ * cochran_download
+ *
+ * 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 <stdio.h>	// fopen, fwrite, fclose
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+
+#include <libdivecomputer/context.h>
+#include <libdivecomputer/device.h>
+#include <libdivecomputer/parser.h>
+#include <libdivecomputer/cochran.h>
+
+
+volatile sig_atomic_t g_cancel = 0;
+
+void
+sighandler (int signum)
+{
+	// Restore the default signal handler.
+	signal (signum, SIG_DFL);
+
+	g_cancel = 1;
+}
+
+static int
+cancel_cb (void *userdata)
+{
+	return g_cancel;
+}
+
+static void
+usage (const char *filename)
+{
+	fprintf (stderr, "Usage:\n\n");
+	fprintf (stderr, "   %s [options] devname\n\n", filename);
+	fprintf (stderr, "Options:\n\n");
+	fprintf (stderr, "   -b name        Set backend name (required).\n");
+	fprintf (stderr, "   -d dirname     Dump data to dirname.\n");
+	fprintf (stderr, "   -f             Force dump despite download errors\n");
+	fprintf (stderr, "   -h             Show this help message.\n\n");
+
+	fprintf (stderr, "Supported backends:\n\n");
+	fprintf (stderr, " emc,       Cochran EMC family\n");
+	fprintf (stderr, " commander, Cochran Commander family\n");
+	fprintf (stderr, "\n\n");
+}
+
+static dc_status_t
+search (dc_descriptor_t **out, const char *name, dc_family_t backend, unsigned int model)
+{
+	dc_status_t rc = DC_STATUS_SUCCESS;
+
+	dc_iterator_t *iterator = NULL;
+	rc = dc_descriptor_iterator (&iterator);
+	if (rc != DC_STATUS_SUCCESS) {
+		printf ("Error creating the device descriptor iterator.\n");
+		return rc;
+	}
+
+	dc_descriptor_t *descriptor = NULL, *current = NULL;
+	while ((rc = dc_iterator_next (iterator, &descriptor)) == DC_STATUS_SUCCESS)
+	{
+		if (backend == dc_descriptor_get_type (descriptor)) {
+			dc_descriptor_free (current);
+			current = descriptor;
+			break;
+		}
+
+		dc_descriptor_free (descriptor);
+	}
+
+	if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_DONE) {
+		dc_descriptor_free (current);
+		dc_iterator_free (iterator);
+		printf ("Error iterating the device descriptors.\n");
+		return rc;
+	}
+
+	dc_iterator_free (iterator);
+
+	*out = current;
+
+	return DC_STATUS_SUCCESS;
+}
+
+static dc_status_t
+write_dump(dc_buffer_t *dump, const char *fname)
+{
+	int fd, size = 0, written;
+
+	if (dump)
+		size = dc_buffer_get_size(dump);
+
+	if (size > 0) {
+		fd = open(fname, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
+		if (fd == -1) {
+			printf("unable to open %s for writing\n", fname);
+			exit;
+		}
+		written = write(fd, dc_buffer_get_data(dump), size);
+		if (written != size) {
+			printf("Error writing %s. Wrote %d expected %d.\n", fname, written, size);
+			return (DC_STATUS_UNSUPPORTED);
+		}
+	}
+	return (DC_STATUS_SUCCESS);
+}
+
+static dc_status_t
+dowork (dc_context_t *context, dc_descriptor_t *descriptor, const char *devname, const char *dirname, unsigned int force_flag)
+{
+	dc_status_t rc = DC_STATUS_SUCCESS;
+
+	struct stat dstat;
+
+	// Open the device.
+	dc_device_t *device = NULL;
+	rc = dc_device_open (&device, context, descriptor, devname);
+	if (rc != DC_STATUS_SUCCESS) {
+		printf ("Error opening %s.\n", devname);
+		return rc;
+	}
+
+	// Register the cancellation handler.
+	rc = dc_device_set_cancel (device, cancel_cb, NULL);
+	if (rc != DC_STATUS_SUCCESS) {
+		printf ("Error registering the cancellation handler.\n");
+		dc_device_close (device);
+		return rc;
+	}
+
+	// Create directory if needed
+	stat(dirname, &dstat);
+
+	if ( ! (dstat.st_mode & S_IFDIR) ) {
+		if (mkdir(dirname, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) {
+			printf("Unable to create directory\n");
+			exit;
+		}
+	}
+
+
+	// TODO Run dump and write files
+	struct cochran_data_dump dump = {0};
+	rc = dc_device_dump (device, (dc_buffer_t *) &dump);
+
+	if (rc == DC_STATUS_SUCCESS || force_flag) {
+		char fname[128];
+
+		sprintf(fname, "%s/info-x05x9dxffx00x43x00.bin", dirname);
+		write_dump(dump.id0, fname);
+
+		if (dc_buffer_get_size(dump.id0) > 0)
+			sprintf(fname, "%s/info-xdbx7fxffx00x43x00.bin", dirname);
+		else
+			sprintf(fname, "%s/info-x05x9dxffx00x43x00.bin", dirname);
+		write_dump(dump.id1, fname);
+
+		sprintf(fname, "%s/config-x96x00.bin", dirname);
+		write_dump(dump.config1, fname);
+
+		sprintf(fname, "%s/config-x96x01.bin", dirname);
+		write_dump(dump.config2, fname);
+
+		sprintf(fname, "%s/config-x96x02.bin", dirname);
+		write_dump(dump.config3, fname);
+
+		sprintf(fname, "%s/config-x96x03.bin", dirname);
+		write_dump(dump.config4, fname);
+
+		sprintf(fname, "%s/misc-x89x05x00x00x00xdcx05.bin", dirname);
+		write_dump(dump.misc, fname);
+
+		sprintf(fname, "%s/logbook.bin", dirname);
+		write_dump(dump.logbook, fname);
+
+		sprintf(fname, "%s/sample.bin", dirname);
+		write_dump(dump.sample, fname);
+
+		dc_buffer_free (dump.id0);
+		dc_buffer_free (dump.id1);
+		dc_buffer_free (dump.config1);
+		dc_buffer_free (dump.config2);
+		dc_buffer_free (dump.config3);
+		dc_buffer_free (dump.config4);
+		dc_buffer_free (dump.misc);
+		dc_buffer_free (dump.logbook);
+		dc_buffer_free (dump.sample);
+	}
+
+	// Close the device.
+	rc = dc_device_close (device);
+	if (rc != DC_STATUS_SUCCESS) {
+		printf ("Error closing the device.\n");
+		return rc;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+int
+main (int argc, char *argv[])
+{
+	extern char *optarg;
+	extern int optind, opterr, optopt;
+
+	// Default values.
+	dc_family_t backend = DC_FAMILY_NULL;
+
+	// Command line arguments
+	const char *devname = NULL, *dirname;
+	unsigned int force_flag = 0;
+
+
+	// Parse command-line options.
+	int opt = 0;
+	while ((opt = getopt (argc, argv, "b:d:ls:h")) != -1) {
+		switch (opt) {
+		case 'b':	// backend
+			if ( ! strcmp ("emc", optarg) )
+				backend = DC_FAMILY_COCHRAN_EMC;
+			else if ( ! strcmp( "commander", optarg) )
+				backend = DC_FAMILY_COCHRAN_COMMANDER;
+			break;
+		case 'd':	// dump data to directory
+			dirname = optarg;
+			break;
+		case 'f':	// Force dump despite download error
+			force_flag = 1;
+			break;
+		case '?':
+		case 'h':
+		default:
+			usage (argv[0]);
+			return EXIT_FAILURE;
+		}
+	}
+
+	if (optind < argc)
+		devname = argv[optind];
+
+
+	signal (SIGINT, sighandler);
+
+	dc_context_t *context = NULL;
+	dc_status_t rc = dc_context_new (&context);
+	if (rc != DC_STATUS_SUCCESS) {
+		return EXIT_FAILURE;
+	}
+
+	dc_context_set_loglevel (context, DC_LOGLEVEL_NONE);
+	//dc_context_set_logfunc (context, logfunc, NULL);
+
+	/* Search for a matching device descriptor. */
+	dc_descriptor_t *descriptor = NULL;
+	rc = search (&descriptor, NULL, backend, 0);
+	if (rc != DC_STATUS_SUCCESS) {
+		return EXIT_FAILURE;
+	}
+
+	/* Fail if no device descriptor found. */
+	if (descriptor == NULL) {
+		printf ("No matching device found.\n");
+		usage (argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	rc = dowork (context, descriptor, devname, dirname, force_flag);
+
+	dc_descriptor_free (descriptor);
+	dc_context_free (context);
+
+	return rc != DC_STATUS_SUCCESS ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/examples/universal.c b/examples/universal.c
index 1e0307b..56445b7 100644
--- a/examples/universal.c
+++ b/examples/universal.c
@@ -102,6 +102,8 @@ static const backend_table_t g_backends[] = {
 	{"predator",	DC_FAMILY_SHEARWATER_PREDATOR},
 	{"petrel",      DC_FAMILY_SHEARWATER_PETREL},
 	{"nitekq",      DC_FAMILY_DIVERITE_NITEKQ},
+	{"commander",	DC_FAMILY_COCHRAN_COMMANDER},
+	{"emc",			DC_FAMILY_COCHRAN_EMC},
 };
 
 static dc_family_t
@@ -245,7 +247,7 @@ sample_cb (dc_sample_type_t type, dc_sample_value_t value, void *userdata)
 		"safety stop (voluntary)", "safety stop (mandatory)", "deepstop",
 		"ceiling (safety stop)", "floor", "divetime", "maxdepth",
 		"OLF", "PO2", "airtime", "rgbm", "heading", "tissue level warning",
-		"gaschange2"};
+		"gaschange2", "battery"};
 	static const char *decostop[] = {
 		"ndl", "safety", "deco", "deep"};
 
@@ -307,6 +309,10 @@ sample_cb (dc_sample_type_t type, dc_sample_value_t value, void *userdata)
 		fprintf (sampledata->fp, "   <deco time=\"%u\" depth=\"%.2f\">%s</deco>\n",
 			value.deco.time, value.deco.depth, decostop[value.deco.type]);
 		break;
+	case DC_SAMPLE_ASCENT_RATE:
+		fprintf (sampledata->fp, "   <ascent_rate>%.2f</ascent_rate>\n",
+			value.ascent_rate);
+		break;
 	default:
 		break;
 	}
diff --git a/include/libdivecomputer/Makefile.am b/include/libdivecomputer/Makefile.am
index 891f4e0..59b30a5 100644
--- a/include/libdivecomputer/Makefile.am
+++ b/include/libdivecomputer/Makefile.am
@@ -49,4 +49,5 @@ libdivecomputer_HEADERS = 	\
 	shearwater_petrel.h \
 	shearwater_predator.h \
 	diverite.h \
-	diverite_nitekq.h
+	diverite_nitekq.h \
+	cochran.h
diff --git a/include/libdivecomputer/cochran.h b/include/libdivecomputer/cochran.h
new file mode 100644
index 0000000..27fdcc5
--- /dev/null
+++ b/include/libdivecomputer/cochran.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "context.h"
+#include "device.h"
+#include "parser.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+struct cochran_data_dump {
+	dc_buffer_t *id0;
+	dc_buffer_t *id1;
+	dc_buffer_t *config1;
+	dc_buffer_t *config2;
+	dc_buffer_t *config3;
+	dc_buffer_t *config4;
+	dc_buffer_t *misc;
+	dc_buffer_t *logbook;
+	dc_buffer_t *sample;
+};
+
+dc_status_t
+cochran_cmdr_device_open (dc_device_t **device, dc_context_t *context, const char *name);
+
+dc_status_t
+cochran_cmdr_parser_create (dc_parser_t **parser, dc_context_t *context);
+
+dc_status_t
+cochran_emc_device_open (dc_device_t **device, dc_context_t *context, const char *name);
+
+dc_status_t
+cochran_emc_parser_create (dc_parser_t **parser, dc_context_t *context);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* COCHRAN_H */
diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h
index 983ec07..ef86272 100644
--- a/include/libdivecomputer/common.h
+++ b/include/libdivecomputer/common.h
@@ -83,6 +83,9 @@ typedef enum dc_family_t {
 	DC_FAMILY_SHEARWATER_PETREL,
 	/* Dive Rite */
 	DC_FAMILY_DIVERITE_NITEKQ = (11 << 16),
+	/* Cochran */
+	DC_FAMILY_COCHRAN_COMMANDER = (12 << 16),
+	DC_FAMILY_COCHRAN_EMC,
 } dc_family_t;
 
 #ifdef __cplusplus
diff --git a/include/libdivecomputer/parser.h b/include/libdivecomputer/parser.h
index 65b18c9..0f7021b 100644
--- a/include/libdivecomputer/parser.h
+++ b/include/libdivecomputer/parser.h
@@ -43,7 +43,8 @@ typedef enum dc_sample_type_t {
 	DC_SAMPLE_SETPOINT,
 	DC_SAMPLE_PPO2,
 	DC_SAMPLE_CNS,
-	DC_SAMPLE_DECO
+	DC_SAMPLE_DECO,
+	DC_SAMPLE_ASCENT_RATE,
 } dc_sample_type_t;
 
 typedef enum dc_field_type_t {
@@ -53,7 +54,8 @@ typedef enum dc_field_type_t {
 	DC_FIELD_GASMIX_COUNT,
 	DC_FIELD_GASMIX,
 	DC_FIELD_SALINITY,
-	DC_FIELD_ATMOSPHERIC
+	DC_FIELD_ATMOSPHERIC,
+	DC_FIELD_TEMPERATURE
 } dc_field_type_t;
 
 typedef enum parser_sample_event_t {
@@ -85,6 +87,7 @@ typedef enum parser_sample_event_t {
 	SAMPLE_EVENT_GASCHANGE2, /* The event value contains the O2 and He
 	                            percentages, packed as two 16bit integers in
 	                            respectively the low and high part. */
+	SAMPLE_EVENT_BATTERY	/* Battery low */
 } parser_sample_event_t;
 
 /* For backwards compatibility */
@@ -129,20 +132,20 @@ typedef struct dc_gasmix_t {
 } dc_gasmix_t;
 
 typedef union dc_sample_value_t {
-	unsigned int time;
-	double depth;
+	unsigned int time;				// seconds
+	double depth;					// metres
 	struct {
 		unsigned int tank;
 		double value;
 	} pressure;
-	double temperature;
+	double temperature;				// celcius
 	struct {
 		unsigned int type;
 		unsigned int time;
 		unsigned int flags;
 		unsigned int value;
 	} event;
-	unsigned int rbt;
+	unsigned int rbt;				// minutes
 	unsigned int heartbeat;
 	unsigned int bearing;
 	struct {
@@ -155,9 +158,10 @@ typedef union dc_sample_value_t {
 	double cns;
 	struct {
 		unsigned int type;
-		unsigned int time;
-		double depth;
+		unsigned int time;			// minutes
+		double depth;				// metres
 	} deco;
+	double ascent_rate;				// m/sec
 } dc_sample_value_t;
 
 typedef struct dc_parser_t dc_parser_t;
diff --git a/src/Makefile.am b/src/Makefile.am
index 595f0c7..d8098f6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,5 +1,5 @@
 AM_CPPFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include
-AM_CFLAGS = $(LIBUSB_CFLAGS)
+AM_CFLAGS = $(LIBUSB_CFLAGS) -g
 
 lib_LTLIBRARIES = libdivecomputer.la
 
@@ -55,7 +55,10 @@ libdivecomputer_la_SOURCES = \
 	ringbuffer.h ringbuffer.c \
 	checksum.h checksum.c \
 	array.h array.c \
-	buffer.c
+	buffer.c \
+	cochran_common.h cochran_common.c cochran_common_parser.h cochran_common_parser.c \
+	cochran_cmdr.h cochran_cmdr.c cochran_cmdr_parser.c \
+	cochran_emc.h cochran_emc.c cochran_emc_parser.c
 
 if OS_WIN32
 libdivecomputer_la_SOURCES += serial.h serial_win32.c
diff --git a/src/cochran_cmdr.c b/src/cochran_cmdr.c
new file mode 100644
index 0000000..fb6e1e0
--- /dev/null
+++ b/src/cochran_cmdr.c
@@ -0,0 +1,66 @@
+/*
+ * libdivecomputer
+ *
+ * Copyright (C) 2014 John Van Ostrand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+#include <time.h>
+
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "serial.h"
+
+#include "cochran_common.h"
+#include "cochran_cmdr.h"
+
+
+static const dc_device_vtable_t cochran_cmdr_device_vtable = {
+	DC_FAMILY_COCHRAN_COMMANDER,
+	cochran_common_device_set_fingerprint,	/* set_fingerprint */
+	cochran_common_device_read,				/* read */
+	NULL,									/* write */
+	cochran_common_device_dump,				/* dump */
+	cochran_common_device_foreach,			/* foreach */
+	cochran_common_device_close				/* close */
+};
+
+
+dc_status_t
+cochran_cmdr_device_open (dc_device_t **out, dc_context_t *context, const char *name)
+{
+	dc_status_t rc;
+	rc = cochran_common_device_open(out, context, name, &cochran_cmdr_device_vtable);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	cochran_device_t *device = (cochran_device_t *) *out;
+
+	// Check family
+	if ((device->data.model & 0xFF0000) != COCHRAN_MODEL_COMMANDER_FAMILY) {
+		ERROR (context, "Device not recognized.");
+		serial_close (device->port);
+		free (device->data.id);
+		free (device);
+		return DC_STATUS_UNSUPPORTED;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
diff --git a/src/cochran_cmdr.h b/src/cochran_cmdr.h
new file mode 100644
index 0000000..bac5f57
--- /dev/null
+++ b/src/cochran_cmdr.h
@@ -0,0 +1,83 @@
+/*
+ * 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
+ */
+
+
+struct cochran_cmdr_log_t {
+	// Pre-dive 128 bytes
+	unsigned char minutes, seconds;			// 3 bytes
+	unsigned char day, hour, year, month;	// 3 bytes
+ 	unsigned char sample_start_offset[4];	// 4 bytes
+	unsigned char start_timestamp[4];		// 4 bytes
+	unsigned char pre_dive_timestamp[4];	// 4 bytes
+	unsigned char unknown1[6];				// 6 bytes
+	unsigned char water_conductivity;		// 1 byte [0=low, 2=high]
+	unsigned char unknown2[5];				// 5 bytes
+// 30
+	unsigned char sample_pre_event_offset[4];// 4 bytes
+	unsigned char unknown3[4];				// 4 bytes
+	unsigned char start_battery_voltage[2];	// 2 bytes [/256]
+//40
+	unsigned char unknown4[4]; 				// 4 bytes
+	unsigned char entered_or_computed_po[2];// 2 bytes ???
+	unsigned char unknown5[8];				// 8 bytes
+	unsigned char start_depth[2];			// 2 byte [/4]
+//56
+	unsigned char unknown6[12];				// 12 bytes
+	unsigned char sit[2];					// 2 bytes
+//70
+	unsigned char number[2];				// 2 bytes
+	unsigned char unknown7[1];				// 1 byte
+	unsigned char altitude;					// 1 byte [/4 = kft]
+	unsigned char unknown8[28];				// 27 bytes
+	unsigned char alarm_depth[2];			// 2 bytes
+	unsigned char unknown9[4];				// 5 bytes
+//108
+	unsigned char repetitive_dive;			// 1 byte
+	unsigned char unknown10[3];				// 3 bytes
+	unsigned char start_tissue_nsat[16];	// 16 bytes [/256]
+
+	// Post-dive 128 bytes
+	unsigned char sample_end_offset[4];		// 4 bytes
+	unsigned char unknown11[21];			// 21 bytes
+	unsigned char temp;						// 1 byte
+	unsigned char unknown12[12];			// 12 bytes
+	unsigned char bt[2];					// 2 bytes [minutes]
+	unsigned char max_depth[2];				// 2 bytes [/4]
+	unsigned char avg_depth[2];				// 2 bytes
+	unsigned char unknown13[38];			// 38 bytes
+	unsigned char o2_percent[4][2];			// 8 bytes
+	unsigned char unknown14[22];			// 22 bytes
+	unsigned char end_tissue_nsat[16];		// 16 bytes [/256]
+} __attribute__((packed));
+
+ 
+typedef struct cochran_cmdr_log_t cochran_cmdr_log_t;
+
+
+struct cochran_cmdr_config1_t {
+	unsigned char unknown1[209];
+	unsigned short int dive_count;
+	unsigned char unknown2[274];
+	unsigned short int serial_num; // @170
+	unsigned char unknown3[24];
+} __attribute__((packed));
+
+typedef struct cochran_emc_config1_t cochran_emc_config1_t;
diff --git a/src/cochran_cmdr_parser.c b/src/cochran_cmdr_parser.c
new file mode 100644
index 0000000..8c74705
--- /dev/null
+++ b/src/cochran_cmdr_parser.c
@@ -0,0 +1,202 @@
+/*
+ * libdivecomputer
+ *
+ * Copyright (C) 2014 John Van Ostrand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+#include <math.h>
+
+#include <libdivecomputer/units.h>
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "parser-private.h"
+#include "serial.h"
+#include "array.h"
+
+#include "cochran_common.h"
+#include "cochran_cmdr.h"
+#include "cochran_common_parser.h"
+
+#define SZ_SAMPLE	2
+
+static dc_status_t cochran_cmdr_parser_get_field(dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
+static dc_status_t cochran_cmdr_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata);
+
+static const dc_parser_vtable_t cochran_cmdr_parser_vtable = {
+	DC_FAMILY_COCHRAN_COMMANDER,
+	cochran_common_parser_set_data,			/* set_data */
+	cochran_common_parser_get_datetime,		/* datetime */
+	cochran_cmdr_parser_get_field,			/* fields */
+	cochran_cmdr_parser_samples_foreach,	/* samples_foreach */
+	cochran_common_parser_destroy			/* destroy */
+};
+
+
+dc_status_t
+cochran_cmdr_parser_create (dc_parser_t **out, dc_context_t *context)
+{
+	return cochran_common_parser_create(out, context, &cochran_cmdr_parser_vtable);
+}
+
+
+static dc_status_t
+cochran_cmdr_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	cochran_cmdr_log_t *log = (cochran_cmdr_log_t *) data->current_log;
+
+	dc_gasmix_t *gasmix = (dc_gasmix_t *) value;
+	dc_salinity_t *water = (dc_salinity_t *) value;
+
+	if (value) {
+		switch (type) {
+		case DC_FIELD_TEMPERATURE:
+			*((unsigned int*) value) = (log->temp - 32) / 1.8;
+		case DC_FIELD_DIVETIME:
+			*((unsigned int *) value) = array_uint16_le (log->bt) * 60;
+			break;
+		case DC_FIELD_MAXDEPTH:
+			*((double *) value) = array_uint16_le (log->max_depth) / 4 * FEET;
+			break;
+		case DC_FIELD_AVGDEPTH:
+			*((double *) value) = array_uint16_le (log->avg_depth) / 4 * FEET;
+			break;
+		case DC_FIELD_GASMIX_COUNT:
+			*((unsigned int *) value) = 2;
+			break;
+		case DC_FIELD_GASMIX:
+			gasmix->oxygen = (double) array_uint16_le ((char *)log->o2_percent + 2 * flags) / 256 / 100;
+			gasmix->helium = 0;
+			gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
+			break;
+		case DC_FIELD_SALINITY:
+			// 0 = low conductivity, 1 = high, maybe there's a 2?
+			water->type = ( log->water_conductivity == 0 ? DC_WATER_FRESH : DC_WATER_SALT );
+			water->density = 1000 + 12.5 * log->water_conductivity;
+			break;
+		case DC_FIELD_ATMOSPHERIC:
+			*(double *) value = ATM / BAR * pow(1 - 0.0000225577 * (double) log->altitude * 250 * FEET, 5.25588);
+			break;
+		default:
+			return DC_STATUS_UNSUPPORTED;
+		}
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+static dc_status_t
+cochran_cmdr_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
+{
+	cochran_data_t *data = (cochran_data_t *)abstract->data;
+	unsigned char *sdata = data->current_sample;
+	cochran_cmdr_log_t *log = (cochran_cmdr_log_t *) data->current_log;
+	unsigned int size = data->current_sample_size;
+	unsigned char *s;
+
+	dc_sample_value_t sample = {0}, empty_sample = {0};
+	unsigned int time = 0, last_sample_time;
+	unsigned int offset = 0;
+	double temperature;
+	double depth;
+	double ascent_rate;
+	unsigned char deco_obligation = 0;
+	unsigned int deco_time = 0;
+
+	// Cochran samples depth every second and varies between ascent rate
+	// and temp ever other second.
+
+	// Prime values from the dive log section
+	depth = array_uint16_le (log->start_depth) / 256;
+
+	last_sample_time = sample.time = time;
+	if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
+
+	sample.depth = depth * FEET;
+	if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
+
+	// TODO: sample.temperature = (log->start_temperature - 32) / 1.8;
+	// TODO: if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
+
+	while (offset < size) {
+		sample = empty_sample;
+
+		sample.time = time;
+		if (callback && last_sample_time != sample.time) {
+			// We haven't issued this time yet.
+			last_sample_time = sample.time;
+			callback (DC_SAMPLE_TIME, sample, userdata);
+		}
+
+		if (offset > data->sample_memory_end_address)
+			s = sdata + offset - data->sample_memory_end_address - 1;
+		else
+			s = sdata + offset;
+
+		// If corrupt_dive end before offset
+		if (data->corrupt_dive) {
+			if (   s[0] == 0x10
+				|| s[0] == 0xFF
+				|| s[0] == 0xA8) {
+					break;
+			}
+			if (time > 1 && (s[0] == 0xE3 || s[0] == 0xF3))
+				break;
+		}
+
+		// Check for event
+		if (s[0] & 0x80) {
+			offset += cochran_common_handle_event(abstract, callback, userdata, s[0], offset, time);
+			if (s[0] == 0xC5)
+				deco_obligation = 1;	// Deco obligation begins
+			else if (s[0] == 0xC8) 
+				deco_obligation = 0;	// Deco obligation ends
+
+			continue;
+		}
+
+			
+		// Depth is logged as change in feet, bit 0x40 means negative depth
+		depth += (float) (s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1);
+		sample.depth = depth * 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
+			ascent_rate = (s[1] & 0x7f) / 4 * (s[0] & 0x80 ? 1 : -1);
+			sample.ascent_rate = ascent_rate * FEET;
+			if (callback) callback (DC_SAMPLE_ASCENT_RATE, sample, userdata);
+		} else {
+			// Temperature logged in half degrees F above 20
+			temperature = s[1] / 2 + 20;
+			sample.temperature = (temperature - 32.0) / 1.8;
+
+			if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
+		}
+
+		time ++;
+		offset += SZ_SAMPLE;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
diff --git a/src/cochran_common.c b/src/cochran_common.c
new file mode 100644
index 0000000..877491d
--- /dev/null
+++ b/src/cochran_common.c
@@ -0,0 +1,861 @@
+/*
+ * 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 <string.h> // memcpy, memcmp
+#include <stdlib.h> // malloc, free
+#include <assert.h> // assert
+#include <time.h>
+
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "serial.h"
+#include "array.h"
+
+#include "cochran_common.h"
+
+
+#define EXITCODE(rc) \
+( \
+	rc == -1 ? DC_STATUS_IO : DC_STATUS_TIMEOUT \
+)
+
+
+dc_status_t
+cochran_packet (cochran_device_t *device, const unsigned char command[], unsigned int csize, unsigned char answer[], unsigned int asize, int high_speed)
+{
+	dc_device_t *abstract = (dc_device_t *) device;
+	unsigned int bytes_read = 0, n, read_size;
+	unsigned int ptr;
+
+	if (device_is_cancelled (abstract))
+		return DC_STATUS_CANCELLED;
+
+	struct timespec tr, ts = {0};
+	ts.tv_nsec = 16 * 1000000;	// 16 ms
+	// Send the command to the device, one byte at a time
+	for (ptr = 0; ptr < csize; ptr++) {
+		if (ptr) nanosleep(&ts, &tr);
+		n = serial_write(device->port, command + ptr, 1);
+		if (n != 1) {
+			ERROR (abstract->context, "Failed to send the command.");
+			return EXITCODE (n);
+		}
+	}
+
+	if (high_speed) {
+		struct timespec tr, ts = {0};
+		ts.tv_nsec = 45 * 1000000;  // 45 ms
+		nanosleep(&ts, &tr);
+
+		// Weird but I only get the right result when I do it twice
+		// Rates are odd, like 825600 for the EMC, 115200 for commander
+		serial_configure(device->port, device->data.high_baud, 8,
+						SERIAL_PARITY_NONE, 2, SERIAL_FLOWCONTROL_NONE);
+		serial_configure(device->port, device->data.high_baud, 8,
+						SERIAL_PARITY_NONE, 2, SERIAL_FLOWCONTROL_NONE);
+	}
+
+	// Receive the answer from the device.
+	// Use 1024 byte "packets" so we can display progress.
+	while (bytes_read < asize) {
+		if (asize - bytes_read > 1024)
+			read_size = 1024;
+		else
+			read_size = asize - bytes_read;
+
+		n = serial_read (device->port, answer + bytes_read, read_size);
+		if (n != read_size) {
+			ERROR (abstract->context, "Failed to receive data, expected %u, read %u.", read_size, n);
+			return EXITCODE (n);
+		}
+
+		bytes_read += n;
+
+		if (device->progress) {
+			device->progress->current = bytes_read;
+			device_event_emit (abstract, DC_EVENT_PROGRESS, device->progress);
+		}
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_serial_open(cochran_device_t *device, dc_context_t *context)
+{
+	// Open the device.
+	int rc = serial_open (&device->port, context, device->name);
+	if (rc == -1) {
+		ERROR (context, "Failed to open the serial port.");
+		free (device);
+		return DC_STATUS_IO;
+	}
+
+	// Set the serial communication protocol (9600 8N2, no FC).
+	rc = serial_configure (device->port, 9600, 8, SERIAL_PARITY_NONE,
+							2, SERIAL_FLOWCONTROL_NONE);
+	if (rc == -1) {
+		ERROR (context, "Failed to set the terminal attributes.");
+		serial_close (device->port);
+		free (device);
+		return DC_STATUS_IO;
+	}
+
+	serial_set_queue_size(device->port, 4096, 4096);
+
+	// Make sure everything is in a sane state.
+	// Mimicing Analyst software with excessive flushes
+	serial_flush (device->port, SERIAL_QUEUE_OUTPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+	serial_flush (device->port, SERIAL_QUEUE_INPUT);
+
+	serial_set_break(device->port, 1);
+	struct timespec tr, ts = {0};
+	ts.tv_nsec = 16 * 1000000;	// 16ms
+	nanosleep(&ts, &tr);
+	
+	serial_set_break(device->port, 0);
+
+	// Set the timeout for receiving data (5000 ms).
+	if (serial_set_timeout (device->port, 5000) == -1) {
+		ERROR (context, "Failed to set the timeout.");
+		serial_close (device->port);
+		free (device);
+		return DC_STATUS_IO;
+	}
+
+	// Wait for heartbeat byte before send
+	int n;
+	char answer[1];
+	if ((n = serial_read(device->port, answer, 1)) != 1) {
+		ERROR (context, "Failed to receive device heartbeat.");
+		return EXITCODE (n);
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_device_open (dc_device_t **out, dc_context_t *context, const char *name, const dc_device_vtable_t *vtable)
+{
+	dc_status_t rc;
+
+	if (out == NULL)
+		return DC_STATUS_INVALIDARGS;
+
+	// Allocate memory.
+	cochran_device_t *device = (cochran_device_t *) malloc (sizeof (cochran_device_t));
+	if (device == NULL) {
+		ERROR (context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	// Initialize the base class.
+	device_init (&device->base, context, vtable);
+
+	// Set the default values.
+	device->port = NULL;
+	device->name = name;
+	device->progress = NULL;
+	device->data.logbook = NULL;
+	device->data.sample = NULL;
+	cochran_common_device_set_fingerprint((dc_device_t *) device, "", 0);
+
+	if ((rc = cochran_common_serial_open(device, context)) != DC_STATUS_SUCCESS)
+		return rc;
+
+	// Read ID from the device
+	rc = cochran_read_id((dc_device_t *) device);
+
+	if (rc != DC_STATUS_SUCCESS) {
+		ERROR (context, "Device not responding.");
+		serial_close (device->port);
+		free (device->data.id);
+		free (device->data.id0);
+		free (device);
+		return rc;
+	}
+
+	*out = (dc_device_t *) device;
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_device_close (dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t*) abstract;
+
+	// Close the device.
+	if (serial_close (device->port) == -1) {
+		free (device);
+		return DC_STATUS_IO;
+	}
+
+	// Free memory.
+	free (device->data.id);
+	free (device->data.id0);
+	free (device->data.config1);
+	free (device->data.config2);
+	free (device->data.config3);
+	free (device->data.config4);
+	free (device->data.misc);
+	free (device->data.logbook);
+	free (device->data.sample);
+	free (device);
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = &(device->data);
+
+	if (size && size != sizeof (d->fingerprint))
+		return DC_STATUS_INVALIDARGS;
+
+	if (size)
+		memcpy (&(d->fingerprint), data, sizeof (d->fingerprint));
+	else
+		memset (&(d->fingerprint), 0xFF, sizeof (d->fingerprint));
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size)
+{
+	cochran_device_t *device = (cochran_device_t*) abstract;
+
+	// Build the command
+	unsigned char command[10];
+	unsigned char command_size;
+
+	switch (device->data.address_length)
+	{
+	case COCHRAN_ADDRESS_LENGTH_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 COCHRAN_ADDRESS_LENGTH_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;
+	}
+
+	// Read data at high speed
+	dc_status_t rc = cochran_packet (device, command, command_size, data, size, 1);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+static void
+cochran_set_device_config (cochran_device_t *device)
+{
+	dc_device_t *abstract = (dc_device_t *) device;
+	// Determine model
+	if (memcmp(device->data.id + 0x3B, "AM2315\xA3\x71", 8) == 0)
+	{
+		device->data.model = COCHRAN_MODEL_EMC_20;
+		device->data.log_size = 512;
+		device->data.sample_memory_start_address = 0x94000;
+		device->data.dive_num_ptr = 0x56;
+		device->data.dive_count_ptr = 0xD2;
+		device->data.dive_count_endian = COCHRAN_LE_TYPE;
+		device->data.sample_end_ptr = 256;
+		device->data.log_pre_dive_ptr = 30;
+		device->data.log_end_dive_ptr = 256;
+		device->data.last_interdive_ptr = 233;
+		device->data.last_entry_ptr = 194;
+		device->data.date_format = COCHRAN_DATE_FORMAT_SMHDMY;
+		device->data.address_length = COCHRAN_ADDRESS_LENGTH_32;
+		device->data.high_baud = 825600;
+	}
+	else if (memcmp(device->data.id + 0x3B, "AMA315\xC3\xC5", 8) == 0)
+	{
+		device->data.model = COCHRAN_MODEL_EMC_16;
+		device->data.log_size = 512;
+		device->data.sample_memory_start_address = 0x94000;
+		device->data.dive_num_ptr = 0x56;
+		device->data.dive_count_ptr = 0xD2;
+		device->data.dive_count_endian = COCHRAN_LE_TYPE;
+		device->data.sample_end_ptr = 256;
+		device->data.log_pre_dive_ptr = 30;
+		device->data.log_end_dive_ptr = 256;
+		device->data.last_interdive_ptr = 233;
+		device->data.last_entry_ptr = 194;
+		device->data.date_format = COCHRAN_DATE_FORMAT_SMHDMY;
+		device->data.address_length = COCHRAN_ADDRESS_LENGTH_32;
+		device->data.high_baud = 825600;
+	}
+	else if (memcmp(device->data.id + 0x3B, "AM\x11\x32\x32\x31\x32\x02", 8) == 0)
+	{
+		device->data.model = COCHRAN_MODEL_COMMANDER_AIR_NITROX;
+		device->data.log_size = 256;
+		device->data.sample_memory_start_address = 0x20000;
+		device->data.dive_num_ptr = 0x46;
+		device->data.dive_count_ptr = 0x46;
+		device->data.dive_count_endian = COCHRAN_BE_TYPE;
+		device->data.sample_end_ptr = 256;
+		device->data.log_pre_dive_ptr = 30;
+		device->data.log_end_dive_ptr = 128;
+		device->data.last_interdive_ptr = 167;
+		device->data.last_entry_ptr = -1;
+		device->data.date_format = COCHRAN_DATE_FORMAT_MSDHYM;
+		device->data.address_length = COCHRAN_ADDRESS_LENGTH_24;
+		device->data.high_baud = 115200;
+	}
+	else
+	{
+		device->data.model = 0;
+		ERROR (abstract->context,
+			"Unknown Cochran model %02x %02x %02x %02x %02x %02x %02x %02x",
+			*(device->data.id + 0x3B),     *(device->data.id + 0x3B + 1),
+			*(device->data.id + 0x3B + 2), *(device->data.id + 0x3B + 3),
+			*(device->data.id + 0x3B + 4), *(device->data.id + 0x3B + 5),
+			*(device->data.id + 0x3B + 6), *(device->data.id + 0x3B + 7));
+	}
+
+	return;
+}
+
+
+static dc_status_t
+cochran_read_id (dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	dc_status_t rc;
+	unsigned char command[6] = {0x05, 0x9D, 0xFF, 0x00, 0x43, 0x00};
+
+	device->data.id = (unsigned char *) malloc(67);
+	if (device->data.id == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	rc = cochran_packet(device, command, 6, device->data.id, 67, 0);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	if (strncmp(device->data.id, "(C)", 3) != 0) {
+		// It's a Commander, read again
+		device->data.id0 = device->data.id;
+
+		command[1] = 0xBD;
+		command[2] = 0x7F;
+
+		device->data.id = (unsigned char *) malloc(67);
+		if (device->data.id == NULL) {
+			ERROR (abstract->context, "Failed to allocate memory.");
+			return DC_STATUS_NOMEMORY;
+		}
+
+		rc = cochran_packet(device, command, 6, device->data.id, 67, 0);
+		if (rc != DC_STATUS_SUCCESS)
+			return rc;
+	}
+
+	cochran_set_device_config(device);
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+static dc_status_t
+cochran_read_config (dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *data = &device->data;
+	dc_status_t rc;
+	unsigned char command[2] = { 0x96, 0x00 };
+
+	data->config1 = (unsigned char *) malloc(512);
+	if (data->config1 == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	rc = cochran_packet(device, command, 2, data->config1, 512, 0);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	data->last_interdive_offset = 
+		array_uint32_le (data->config1 + data->last_interdive_ptr);
+	data->last_entry_offset =
+		array_uint32_le (data->config1 + data->last_entry_ptr);
+
+	// Read second page
+	command[1] = 0x01;
+
+	data->config2 = (unsigned char *) malloc(512);
+	if (data->config2 == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	rc = cochran_packet(device, command, 2, data->config2, 512, 0);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	// EMC only seems to have two config pages
+	if ((data->model & 0xFF0000) == COCHRAN_MODEL_EMC_FAMILY)
+		return DC_STATUS_SUCCESS;
+
+	// Go on reading the extra configs for a Commander
+	// Read third page
+	command[1] = 0x02;
+
+	data->config3 = (unsigned char *) malloc(512);
+	if (data->config3 == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	rc = cochran_packet(device, command, 2, data->config3, 512, 0);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	// Read fourth page
+	command[1] = 0x03;
+
+	data->config4 = (unsigned char *) malloc(512);
+	if (data->config4 == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	rc = cochran_packet(device, command, 2, data->config4, 512, 0);
+
+	return rc;
+}
+
+
+static dc_status_t
+cochran_read_misc (dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+
+	unsigned char command[7] = { 0x89, 0x05, 0x00, 0x00, 0x00, 0xDC, 0x05 };
+
+	switch (device->data.model & 0xFF0000)
+	{
+	case COCHRAN_MODEL_COMMANDER_FAMILY:
+		command[2] = 0xCA;
+		command[3] = 0xFD;
+		break;
+	case COCHRAN_MODEL_EMC_FAMILY:
+		command[2] = 0xE0;
+		command[3] = 0x03;
+		break;
+	default:
+		return DC_STATUS_UNSUPPORTED;
+	}
+
+	device->data.misc = (unsigned char *) malloc(1500);
+	if (device->data.misc == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	// Send first byte then wait for heartbeat before sding the rest
+	serial_write(device->port, command, 1);
+
+	int n;
+	char answer[1];
+	if ((n = serial_read(device->port, answer, 1)) != 1) {
+		ERROR (abstract->context, "Failed to receive device heartbeat.");
+		return EXITCODE (n);
+	}
+
+	return cochran_packet(device, command + 1, 6, device->data.misc, 1500, 0);
+}
+
+
+static dc_status_t
+cochran_read_logbook (dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = &(device->data);
+	dc_status_t rc;
+
+	// Determine size of dive list to read. Round up to nearest 16K
+	if (d->dive_count_endian == COCHRAN_LE_TYPE)
+		d->dive_count = array_uint16_le (d->config1 + d->dive_count_ptr);
+	else 
+		d->dive_count = array_uint16_be (d->config1 + d->dive_count_ptr);
+	
+	d->logbook_size = ((d->dive_count * d->log_size) & 0xFFFFC000)
+		 + 0x4000;
+
+	// Allocate space for log book.
+	d->logbook = (unsigned char *) malloc(d->logbook_size);
+	if (device == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	// Enable progress notifications.
+	device->progress = malloc(sizeof(dc_event_progress_t));
+	if (device->progress == NULL) {
+		ERROR (abstract->context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	device->progress->current = 0;
+	device->progress->maximum = d->logbook_size;
+	device_event_emit (abstract, DC_EVENT_PROGRESS, device->progress);
+
+	// We have to close and restart to get the DC's attention
+	serial_close(device->port);
+
+	struct timespec tr, ts = {0, 800 * 1000000}; // 800ms
+	nanosleep(&ts, &tr);
+
+	cochran_common_serial_open(device, abstract->context);
+
+	// Request log book
+	rc = cochran_common_device_read(abstract, 0, d->logbook, d->logbook_size);
+
+	device->progress->current = d->logbook_size;
+	device_event_emit (abstract, DC_EVENT_PROGRESS, device->progress);
+	free (device->progress);
+
+	return rc;
+}
+
+
+static void
+cochran_find_fingerprint(dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = (cochran_data_t *) &(device->data);
+
+	// Skip to fingerprint to reduce time
+	d->fp_dive_num = d->dive_count - 1;
+
+	while (d->fp_dive_num >= 0 && memcmp(&(d->fingerprint),
+						 d->logbook + d->fp_dive_num * d->log_size
+						+ d->dive_num_ptr,
+						 sizeof(d->fingerprint)))
+		d->fp_dive_num--;
+}
+
+
+static void
+cochran_read_parms(dc_device_t *abstract, int *low_offset, int *high_offset)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = (cochran_data_t *) &(device->data);
+	unsigned int pre_dive_offset, end_dive_offset;
+
+	// Find lowest and highest offsets into sample data
+	*low_offset = 0xFFFFFFFF;
+	*high_offset = 0;
+
+	int i;
+	for (i = d->fp_dive_num + 1; i < d->dive_count; i++) {
+		pre_dive_offset = array_uint32_le (&(d->logbook[i * d->log_size
+				+ d->log_pre_dive_ptr]));
+		end_dive_offset = array_uint32_le (&(d->logbook[i * d->log_size
+				+ d->log_end_dive_ptr]));
+
+		// 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) {
+		// Since I can't tell how much memory it has, I'll round.
+		// I'll round to 128K, dives longer than 12 hrs aren't likely
+		// and memory in sizes not rounded to 128K might be odd.
+		*high_offset = ((pre_dive_offset - 1) & 0xE0000) + 0x20000;
+		d->sample_memory_end_address = *high_offset;
+		*low_offset = d->sample_memory_start_address;
+		d->sample_data_offset = *low_offset;
+		d->sample_size = *high_offset - *low_offset;
+	} else if (*low_offset < 0xFFFFFFFF && *high_offset > 0) {
+		// Round offset and size to 16K boundary
+		d->sample_data_offset = *low_offset & 0xFFFFC000;
+		*high_offset = ((*high_offset  - 1) & 0xFFFFC000) + 0x4000;
+		d->sample_size = *high_offset - d->sample_data_offset;
+	} else {
+		d->sample_data_offset = 0;
+		d->sample_size = 0;
+	}
+}
+
+
+static dc_status_t
+cochran_read_samples(dc_device_t *abstract)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = (cochran_data_t *) &(device->data);
+	int low_offset, high_offset;
+	dc_status_t rc;
+
+
+	cochran_find_fingerprint(abstract);	
+	cochran_read_parms(abstract, &low_offset, &high_offset);
+
+	if (d->sample_size > 0) {
+		d->sample = (unsigned char *) malloc(d->sample_size);
+		if (device == NULL) {
+			ERROR (abstract->context, "Failed to allocate memory.");
+			return DC_STATUS_NOMEMORY;
+		}
+
+		// We have to close the serial port to get the DC's attention
+		serial_close(device->port);
+
+		// Enable progress notifications.
+		device->progress = malloc(sizeof(dc_event_progress_t));
+		if (device->progress == NULL) {
+			ERROR (abstract->context, "Failed to allocate memory.");
+			return DC_STATUS_NOMEMORY;
+		}
+
+		device->progress->current = 0;
+		device->progress->maximum = d->sample_size;
+		device_event_emit (abstract, DC_EVENT_PROGRESS, device->progress);
+
+		struct timespec tr, ts = {0, 800* 1000000}; // 800 ms
+		nanosleep(&ts, &tr);
+
+		cochran_common_serial_open(device, abstract->context);
+
+		// Read the sample data
+		rc = cochran_common_device_read (abstract, d->sample_data_offset, d->sample, d->sample_size);
+		if (rc != DC_STATUS_SUCCESS) {
+			free (device->progress);
+			ERROR (abstract->context, "Failed to read the sample data.");
+			return rc;
+		}
+
+		device->progress->current = d->sample_size;
+		device_event_emit (abstract, DC_EVENT_PROGRESS, device->progress);
+		free (device->progress);
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+static dc_status_t
+cochran_common_device_read_all (dc_device_t *abstract)
+{
+	dc_status_t rc;
+
+	// Read config
+	rc = cochran_read_config(abstract);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	rc = cochran_read_misc(abstract);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	rc = cochran_read_logbook(abstract);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	rc = cochran_read_samples(abstract);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_device_dump (dc_device_t *abstract, dc_buffer_t *data)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = (cochran_data_t *) &(device->data);
+
+	struct cochran_data_dump *dump = (struct cochran_data_dump *) data;
+
+	dc_status_t rc;
+
+	rc = cochran_common_device_read_all (abstract);
+
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	// Copy data for permanency.
+
+	if (d->id0) {
+		dump->id0 = dc_buffer_new(67);
+		dc_buffer_append (dump->id0, d->id0, 67);
+	}
+	
+	if (d->id) {
+		dump->id1 = dc_buffer_new(67);
+		dc_buffer_append (dump->id1, d->id, 67);
+	}
+	
+	if (d->config1) {
+		dump->config1 = dc_buffer_new(512);
+		dc_buffer_append (dump->config1, d->config1, 512);
+	}
+	
+	if (d->config2) {
+		dump->config2 = dc_buffer_new(512);
+		dc_buffer_append (dump->config2, d->config2, 512);
+	}
+	
+	if (d->config3) {
+		dump->config3 = dc_buffer_new(512);
+		dc_buffer_append (dump->config3, d->config3, 512);
+	}
+	
+	if (d->config4) {
+		dump->config4 = dc_buffer_new(512);
+		dc_buffer_append (dump->config4, d->config4, 512);
+	}
+	
+	if (d->misc) {
+		dump->misc = dc_buffer_new(1500);
+		dc_buffer_append (dump->misc, d->misc, 1500);
+	}
+	
+	if (d->logbook) {
+		dump->logbook = dc_buffer_new(d->logbook_size);
+		dc_buffer_append (dump->logbook, d->logbook, d->logbook_size);
+	}
+	
+	if (d->sample) {
+		dump->sample = dc_buffer_new(d->sample_size);
+		dc_buffer_append (dump->sample, d->sample, d->sample_size);
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+
+dc_status_t
+cochran_common_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata)
+{
+	cochran_device_t *device = (cochran_device_t *) abstract;
+	cochran_data_t *d = &(device->data);
+	unsigned int sample_start_offset, sample_end_offset;
+	struct tm t;
+	dc_status_t rc;
+
+	rc = cochran_common_device_read_all (abstract);
+
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	// Loop through each dive
+	int i;
+	for (i = d->dive_count - 1; i > d->fp_dive_num; i--) {
+
+		d->current_log = d->logbook + i * d->log_size;
+
+		sample_start_offset = array_uint32_le (d->current_log + 6);
+		sample_end_offset = array_uint32_le (d->current_log
+				+ d->log_size/2);
+
+		d->current_sample = d->sample + sample_start_offset
+				- d->sample_data_offset;
+
+		// Check for corrupt post-dive section
+		if (array_uint32_le(d->current_log + d->log_size/2) == 0xFFFFFFFF)
+			d->corrupt_dive = 1;
+		else
+			d->corrupt_dive = 0;
+
+		// Check for ring buffer wrap
+		if (sample_start_offset > sample_end_offset)
+			d->current_sample_size = d->sample_memory_end_address
+				- sample_start_offset + 1 + sample_end_offset
+				- d->sample_memory_start_address + 1;
+		else
+			d->current_sample_size = sample_end_offset - sample_start_offset;
+
+		d->current_fingerprint = d->current_log + d->dive_num_ptr;
+
+		if (d->date_format == COCHRAN_DATE_FORMAT_SMHDMY) {
+			t.tm_sec = d->current_log[0];
+			t.tm_min = d->current_log[1];
+			t.tm_hour = d->current_log[2];
+			t.tm_mday = d->current_log[3];
+			t.tm_mon = d->current_log[4];
+			t.tm_year = d->current_log[5];
+			t.tm_wday = t.tm_yday = t.tm_isdst = 0;
+		} else {
+			t.tm_sec = d->current_log[1];
+			t.tm_min = d->current_log[0];
+			t.tm_hour = d->current_log[3];
+			t.tm_mday = d->current_log[2];
+			t.tm_mon = d->current_log[5];
+			t.tm_year = d->current_log[4];
+			t.tm_wday = t.tm_yday = t.tm_isdst = 0;
+		}
+		d->current_dive_start_time = mktime(&t);
+
+		if (callback && !callback ((unsigned char *) d, sizeof(device->data), d->current_fingerprint, sizeof (d->fingerprint), userdata))
+			return DC_STATUS_SUCCESS;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
diff --git a/src/cochran_common.h b/src/cochran_common.h
new file mode 100644
index 0000000..136f153
--- /dev/null
+++ b/src/cochran_common.h
@@ -0,0 +1,109 @@
+/*
+ * 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
+ */
+
+// seconds to add to Cochran time stamps to get unix time
+// Equivalent to Jan 1, 1992 00:00:00
+#define COCHRAN_TIMESTAMP_OFFSET 694224000
+
+#define COCHRAN_LE_TYPE 0
+#define COCHRAN_BE_TYPE 1
+
+#define COCHRAN_DATE_FORMAT_SMHDMY 0
+#define COCHRAN_DATE_FORMAT_MSDHYM 1
+
+#define COCHRAN_ADDRESS_LENGTH_32 0
+#define COCHRAN_ADDRESS_LENGTH_24 1
+
+typedef enum cochran_model_t {
+	COCHRAN_MODEL_UNKNOWN = 0,
+	COCHRAN_MODEL_EMC_FAMILY = 1 << 16,
+	COCHRAN_MODEL_EMC_14,
+	COCHRAN_MODEL_EMC_16,
+	COCHRAN_MODEL_EMC_20,
+	COCHRAN_MODEL_COMMANDER_FAMILY = 2 << 16,
+	COCHRAN_MODEL_COMMANDER_AIR_NITROX,
+} cochran_model_t;
+
+typedef struct cochran_data_t {
+	cochran_model_t model;
+
+	unsigned char *id0;
+	unsigned char *id;
+	unsigned char *config1;
+	unsigned char *config2;
+	unsigned char *config3;
+	unsigned char *config4;
+	unsigned char *misc;
+	unsigned char *logbook;
+	unsigned char *sample;
+
+	unsigned short int dive_count;
+	unsigned char fingerprint[2];
+	int fp_dive_num;
+
+	unsigned int logbook_size;
+	unsigned int current_sample_size;
+
+	unsigned int sample_data_offset;
+	unsigned int sample_size;
+	unsigned int last_interdive_offset;
+	unsigned int last_entry_offset;
+
+	unsigned char *current_fingerprint;
+	unsigned char *current_log;
+	unsigned char *current_sample;
+	time_t current_dive_start_time;
+
+	// Config items
+	int log_size;
+	int sample_memory_start_address;
+	int sample_memory_end_address;
+	int dive_num_ptr;
+	int dive_count_ptr;
+	int dive_count_endian;
+	int sample_end_ptr;
+	int log_pre_dive_ptr;
+	int log_end_dive_ptr;
+	int last_interdive_ptr;
+	int last_entry_ptr;
+	int date_format;
+	int address_length;
+	int high_baud; 			// baud rate to switch to for log/sample download
+
+	unsigned char corrupt_dive;
+} cochran_data_t;
+
+typedef struct cochran_device_t {
+	dc_device_t base;
+	const char *name;				// serial port name
+	serial_t *port;
+	cochran_data_t data;			// dive data used in parsing
+	dc_event_progress_t *progress;	// We have to do progress in the _read function
+} cochran_device_t;
+
+dc_status_t cochran_packet (cochran_device_t *device, const unsigned char command[], unsigned int csize, unsigned char answer[], unsigned int asize, int high_speed);
+dc_status_t cochran_common_device_open (dc_device_t **out, dc_context_t *context, const char *name, const dc_device_vtable_t *vtable);
+dc_status_t cochran_common_device_close (dc_device_t *abstract);
+dc_status_t cochran_common_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size);
+dc_status_t cochran_common_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size);
+static dc_status_t cochran_read_id (dc_device_t *abstract);
+dc_status_t cochran_common_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata);
+dc_status_t cochran_common_device_dump (dc_device_t *abstract, dc_buffer_t *data);
diff --git a/src/cochran_common_parser.c b/src/cochran_common_parser.c
new file mode 100644
index 0000000..744e5e7
--- /dev/null
+++ b/src/cochran_common_parser.c
@@ -0,0 +1,157 @@
+/*
+ * libdivecomputer
+ *
+ * Copyright (C) 2014 John Van Ostrand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+
+#include <libdivecomputer/units.h>
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "parser-private.h"
+#include "serial.h"
+#include "array.h"
+
+#include "cochran_common.h"
+#include "cochran_common_parser.h"
+
+
+dc_status_t cochran_common_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size);
+dc_status_t cochran_common_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime);
+dc_status_t cochran_common_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
+dc_status_t cochran_common_parser_destroy (dc_parser_t *abstract);
+int cochran_common_handle_event (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata, unsigned char code, unsigned int offset, unsigned int time);
+
+
+dc_status_t
+cochran_common_parser_create (dc_parser_t **out, dc_context_t *context, const dc_parser_vtable_t *vtable)
+{
+	if (out == NULL)
+		return DC_STATUS_INVALIDARGS;
+
+	// Allocate memory.
+	dc_parser_t *parser = (dc_parser_t *) malloc (sizeof (dc_parser_t));
+	if (parser == NULL) {
+		ERROR (context, "Failed to allocate memory.");
+		return DC_STATUS_NOMEMORY;
+	}
+
+	parser_init (parser, context, vtable);
+
+	*out = parser;
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_parser_destroy (dc_parser_t *abstract)
+{
+	// Free memory.
+	free (abstract);
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+dc_status_t
+cochran_common_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
+{
+	abstract->data = data;
+	abstract->size = size;
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+// There are two date formats used by Cochran
+dc_status_t
+cochran_common_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	const unsigned char *log = data->current_log;
+
+	if (data->date_format == COCHRAN_DATE_FORMAT_SMHDMY) {
+		datetime->second = log[0];
+		datetime->minute = log[1];
+		datetime->hour = log[2];
+		datetime->day = log[3];
+		datetime->month = log[4];
+		datetime->year = log[5] + (log[5] > 91 ? 1900 : 2000);
+	} else {
+		datetime->second = log[1];
+		datetime->minute = log[0];
+		datetime->hour = log[3];
+		datetime->day = log[2];
+		datetime->month = log[5];
+		datetime->year = log[4] + (log[5] > 91 ? 1900 : 2000);
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+
+int
+cochran_common_handle_event (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata, unsigned char code, unsigned int offset, unsigned int time)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	cochran_events_t *e = cochran_events;
+
+	dc_sample_value_t sample = {0};
+
+	unsigned char event_ptr = 0;
+
+	while (e[event_ptr].code && e[event_ptr].code != code)
+		event_ptr++;
+
+	sample.event.time = 0;
+
+	if (e[event_ptr].code) {
+		switch (e[event_ptr].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;
+		default:
+			// Don't send known events of type NONE
+			if (! e[event_ptr].type == SAMPLE_EVENT_NONE) {
+				sample.event.type = e[event_ptr].type;
+				sample.event.flags = e[event_ptr].flag;
+				if (callback) callback (DC_SAMPLE_EVENT, sample, userdata);
+			}
+		}
+	} else {
+		// Unknown event, send it so we know we missed something
+		sample.event.type = SAMPLE_EVENT_NONE;
+		sample.event.flags = SAMPLE_FLAGS_NONE;
+		sample.event.value = code;
+		if (callback) callback (DC_SAMPLE_EVENT, sample, userdata);
+	}
+
+	return e[event_ptr].data_bytes;
+}
diff --git a/src/cochran_common_parser.h b/src/cochran_common_parser.h
new file mode 100644
index 0000000..d19a619
--- /dev/null
+++ b/src/cochran_common_parser.h
@@ -0,0 +1,80 @@
+/*
+ * 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
+ */
+
+typedef struct cochran_events_t {
+	unsigned char code;
+	unsigned char data_bytes;
+	char *name;
+	parser_sample_event_t type;
+	parser_sample_flags_t flag;
+} cochran_events_t;
+
+static cochran_events_t cochran_events[] = {
+	{ 0xA8, 1, "Entered PDI mode",
+					SAMPLE_EVENT_SURFACE,			SAMPLE_FLAGS_BEGIN },
+	{ 0xA9, 1, "Exited PDI mode",
+					SAMPLE_EVENT_SURFACE,			SAMPLE_FLAGS_END },
+	{ 0xAB, 5, "Ceiling decrease",
+					SAMPLE_EVENT_NONE,				SAMPLE_FLAGS_NONE },
+	{ 0xAD, 5, "Ceiling increase",
+					SAMPLE_EVENT_NONE,				SAMPLE_FLAGS_NONE },
+	{ 0xC0, 1, "Switched to FO2 21% mode",
+					SAMPLE_EVENT_GASCHANGE,			SAMPLE_FLAGS_NONE },
+	{ 0xC1, 1, "Ascent rate greater than limit",
+					SAMPLE_EVENT_ASCENT,			SAMPLE_FLAGS_BEGIN },
+	{ 0xC2, 1, "Low battery warning",
+					SAMPLE_EVENT_BATTERY,			SAMPLE_FLAGS_NONE },
+	{ 0xC3, 1, "CNS Oxygen toxicity warning",
+					SAMPLE_EVENT_OLF,				SAMPLE_FLAGS_NONE },
+	{ 0xC4, 1, "Depth exceeds user set point",
+					SAMPLE_EVENT_MAXDEPTH,			SAMPLE_FLAGS_NONE },
+	{ 0xC5, 1, "Entered decompression mode",
+					SAMPLE_EVENT_DEEPSTOP,			SAMPLE_FLAGS_BEGIN },
+	{ 0xC8, 1, "PO2 too high",
+					SAMPLE_EVENT_FLOOR, 			SAMPLE_FLAGS_BEGIN },
+	{ 0xCE, 1, "Non-decompression warning",
+					SAMPLE_EVENT_RBT,				SAMPLE_FLAGS_BEGIN },
+	{ 0xD6, 1, "Depth is less than ceiling",
+					SAMPLE_EVENT_CEILING, 			SAMPLE_FLAGS_BEGIN },
+	{ 0xD8, 1, "End decompression mode",
+					SAMPLE_EVENT_DEEPSTOP,			SAMPLE_FLAGS_END },
+	{ 0xE1, 1, "End ascent rate warning",
+					SAMPLE_EVENT_ASCENT,			SAMPLE_FLAGS_END },
+	{ 0xE3, 1, "Switched to FO2 mode",
+					SAMPLE_EVENT_NONE,				SAMPLE_FLAGS_NONE },
+	{ 0xEE, 1, "End non-decompresison warning",
+					SAMPLE_EVENT_RBT,				SAMPLE_FLAGS_END },
+	{ 0xEF, 1, "Switch to blend 2",
+					SAMPLE_EVENT_GASCHANGE2,		SAMPLE_FLAGS_NONE },
+	{ 0xF3, 1, "Switch to blend 1",
+					SAMPLE_EVENT_GASCHANGE2,		SAMPLE_FLAGS_NONE },
+	{ 0xF6, 1, "End Depth is less than ceiling",
+					SAMPLE_EVENT_CEILING,			SAMPLE_FLAGS_END },
+	{ 0x00, 1, NULL,
+					SAMPLE_EVENT_NONE,				SAMPLE_FLAGS_NONE }
+};
+
+
+dc_status_t cochran_common_parser_create (dc_parser_t **out, dc_context_t *context, const dc_parser_vtable_t *vtable);
+dc_status_t cochran_common_parser_destroy (dc_parser_t *abstract);
+dc_status_t cochran_common_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size);
+dc_status_t cochran_common_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime);
+int cochran_common_handle_event (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata, unsigned char code, unsigned int offset, unsigned int time);
diff --git a/src/cochran_emc.c b/src/cochran_emc.c
new file mode 100644
index 0000000..c4b0367
--- /dev/null
+++ b/src/cochran_emc.c
@@ -0,0 +1,67 @@
+/*
+ * libdivecomputer
+ *
+ * Copyright (C) 2014 John Van Ostrand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+#include <time.h>
+
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "serial.h"
+
+#include "cochran_common.h"
+#include "cochran_emc.h"
+
+
+static const dc_device_vtable_t cochran_emc_device_vtable = {
+	DC_FAMILY_COCHRAN_EMC,
+	cochran_common_device_set_fingerprint,	/* set_fingerprint */
+	cochran_common_device_read,				/* read */
+	NULL,									/* write */
+	cochran_common_device_dump, 			/* dump */
+	cochran_common_device_foreach,			/* foreach */
+	cochran_common_device_close				/* close */
+};
+
+
+dc_status_t
+cochran_emc_device_open (dc_device_t **out, dc_context_t *context, const char *name)
+{
+	dc_status_t rc;
+
+	rc = cochran_common_device_open(out, context, name, &cochran_emc_device_vtable);
+	if (rc != DC_STATUS_SUCCESS)
+		return rc;
+
+	cochran_device_t *device = (cochran_device_t *) *out;
+
+	// Check family
+	if ((device->data.model & 0xFF0000) != COCHRAN_MODEL_EMC_FAMILY) {
+		ERROR (context, "Device not recognized.");
+		serial_close (device->port);
+		free (device->data.id);
+		free (device);
+		return DC_STATUS_UNSUPPORTED;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
diff --git a/src/cochran_emc.h b/src/cochran_emc.h
new file mode 100644
index 0000000..e21036b
--- /dev/null
+++ b/src/cochran_emc.h
@@ -0,0 +1,180 @@
+/*
+ * 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
+ */
+
+// 512 bytes for each dive in the log book
+struct cochran_emc_log_t {
+	// Pre-dive 256 bytes
+	unsigned char seconds, minutes, hour;		// 3 bytes
+	unsigned char day, month, year;				// 3 bytes
+	unsigned char sample_start_offset[4];		// 4 bytes
+	unsigned char start_timestamp[4];			// 4 bytes [secs from jan 1,92]
+	unsigned char pre_dive_timestamp[4];		// 4 bytes [secs from Jan 1,92]
+	unsigned char unknown1[6];					// 6 bytes
+	unsigned char water_conductivity;			// 1 byte  [0 =low, 2-high]
+	unsigned char unknown2[5];					// 5 bytes
+//30
+	unsigned char sample_pre_event_offset[4];	// 4 bytes
+	unsigned char config_bitfield[6];			// 6 bytes
+	unsigned char unknown3[2];					// 2 bytes
+	unsigned char start_depth[2];				// 2 bytes [/256]
+	unsigned char unknown4[2];					// 2 bytes
+	unsigned char start_battery_voltage[2];		// 2 bytes [/256]
+//48
+	unsigned char unknown5[7];					// 7 bytes
+	unsigned char start_temperature;			// 1 byte  [F]
+	unsigned char unknown6[28];					// 28 bytes
+	unsigned char sit[2];						// 2 bytes [minutes]
+	unsigned char number[2];					// 2 bytes
+	unsigned char unknown7[1]; 					// 1 bytes
+	unsigned char altitude;						// 1 byte [/4 = kft]
+	unsigned char start_nofly[2];				// 2 bytes [/256 = hours]
+//92
+	unsigned char unknown8[18]; 				// 18 bytes
+	unsigned char post_dive_sit[2];				// 2 bytes  [seconds]
+	unsigned char po2_set_point[9][2];			// 18 bytes	[/256 = %]
+	unsigned char unknown9[12]; 				// 12 bytes
+	unsigned char po2_alarm[2]; 				// 2 bytes  [/256 = %]
+//144
+	unsigned char o2_percent[10][2];			// 20 bytes	[/256 = %]
+	unsigned char he_percent[10][2];			// 20 bytes	[/256 = %]
+	unsigned char alarm_depth[2];				// 2 bytes
+	unsigned char unknown10[14]; 				// 14 bytes
+	unsigned char conservatism;					// 1 bytes [/256 = fraction]
+	unsigned char unknown11[2];					// 2 bytes
+	unsigned char repetitive_dive;				// 1 byte
+	unsigned char unknown12[12];				// 12 bytes
+	unsigned char start_tissue_nsat[20][2];		// 40 bytes [/256]
+
+	// Post-dive 256 bytes
+	unsigned char sample_end_offset[4];			// 4 bytes
+	unsigned char unknown13[33];				// 33 bytes
+	unsigned char temp;							// 1 byte  [F]
+	unsigned char unknown14[10];				// 10 bytes
+// 48
+	unsigned char bt[2];						// 2 bytes [minutes]
+	unsigned char max_depth[2];					// 2 bytes [/4 = ft]
+	unsigned char unknown15[2];					// 2 bytes
+	unsigned char avg_depth[2];					// 2 bytes [/4 = ft]
+	unsigned char min_ndc[2];					// 2 bytes [minutes]
+	unsigned char min_ndx_bt[2];				// 2 bytes [minutes]
+	unsigned char max_forecast_deco[2];			// 2 bytes [minutes]
+	unsigned char max_forecast_deco_bt[2];		// 2 bytes [minutes]
+//64
+	unsigned char max_ceiling[2];				// 2 bytes [*10 = ft]
+	unsigned char max_ceiling_bt[2];			// 2 bytes [minutes]
+	unsigned char unknown16[10];				// 18 bytes
+	unsigned char max_ascent_rate; 				// 1 byte  [ft/min]
+	unsigned char unknown17[3];					// 3 bytes
+	unsigned char max_ascent_rate_bt[2];		// 2 bytes [seconds]
+//84
+	unsigned char unknown18[54];				// 54 bytes
+//138
+	unsigned char end_battery_voltage[2];		// 2 bytes [/256 = v]
+	unsigned char unknown19[8];					// 8 bytes
+	unsigned char min_temp_bt[2];    			// 2 bytes [seconds]
+//150
+	unsigned char unknown20[22];				// 22 bytes
+//172
+	unsigned char end_nofly[2];					// 2 bytes	[/256 = hours]
+	unsigned char alarm_count[2];				// 2 byte
+	unsigned char actual_deco_time[2];			// 2 bytes [seconds]
+//178
+	unsigned char unknown21[38];				// 38 bytes
+//216
+	unsigned char end_tissue_nsat[20][2];		// 40 bytes [/256 = fraction]
+} __attribute__((packed));
+
+typedef struct cochran_emc_log_t cochran_emc_log_t;
+
+typedef enum cochran_emc_bitfield_config_t {
+	BF_TEMP_DEPENDENT_N2,
+	BF_ASCENT_RATE_BAR_GRAPH,
+	BF_BLEND_2_SWITCHING,
+	BF_ALTITUDE_AS_ONE_ZONE,
+	BF_DECOMPRESSION_TIME_DISPLAY,
+	BF_BLEND_3_SWITCHING,
+	BF_VARIABLE_ASCENT_RATE_ALARM,
+	BF_ASCENT_RATE_RESPONSE,
+	BF_REPETITIVE_DIVE_DEPENDENT_N2,
+	BF_TRAINING_MODE,
+	BF_CONSTANT_MODE_COMPUTATIONS,
+	BF_DISPLAYED_UNITS,
+	BF_AUDIBLE_ALARM,
+	BF_CLOCK,
+	BF_CEILING_DISPLAY_DIV_BY_10,
+	BF_GAS_2_AS_FIRST_GAS,
+	BF_ENABLE_HELIUM_COMPUTATIONS,
+	BF_AUTOMATIC_PO2_FO2_SWITCHING,
+	BF_TOUCH_PROGRAMMING_PO2_FO2_SWITCH,
+} cochran_emc_bitfield_config_t;
+
+
+typedef struct cochran_emc_bitfield_t {
+	cochran_emc_bitfield_config_t config;
+	unsigned char word;
+	unsigned char byte;
+	unsigned char mask;
+	unsigned char shift;
+} cochran_emc_bitfield_t;
+
+static cochran_emc_bitfield_t cochran_emc_bits[] = {
+// Word BD
+	{ BF_TEMP_DEPENDENT_N2, 0xBD, 0, 0x40, 6 }, 			// 0=normal,
+															// 1=reduced
+	{ BF_ASCENT_RATE_BAR_GRAPH, 0xBD, 0, 0x20, 5 },			// 0=fixed,
+															// 1=proportional
+	{ BF_BLEND_2_SWITCHING, 0xBD, 0, 0x04, 2 },				// 0=dis, 1=ena
+	{ BF_ALTITUDE_AS_ONE_ZONE, 0xBD, 0, 0x02, 1},			// 0=off, 1=on
+
+	{ BF_DECOMPRESSION_TIME_DISPLAY, 0xBD, 1, 0xC0, 5},		// 111=both,
+															// 011=stop,
+															// 001=total
+	{ BF_BLEND_3_SWITCHING, 0xBD, 1, 0x10, 4 },				// 0=dis, 1=ena
+	{ BF_VARIABLE_ASCENT_RATE_ALARM, 0xBD, 1, 0x04, 3},		// 0=off, 1=on
+	{ BF_ASCENT_RATE_RESPONSE, 0xBD, 1, 0x07, 0},	
+
+//WORD BE
+	{ BF_REPETITIVE_DIVE_DEPENDENT_N2, 0xBE, 0, 0x80, 7 },	// 0=off, 1=on
+	{ BF_TRAINING_MODE, 0xBE, 0, 0x04, 2 }, 				// 0=off, 1=on
+	{ BF_CONSTANT_MODE_COMPUTATIONS, 0xBE, 0, 0x04, 2 },	// 0=FO2, 1=PO2
+	{ BF_DISPLAYED_UNITS, 0xBE, 0, 0x01, 0 },				// 1=metric,
+															// 0=imperial
+
+// WORD BF
+	{ BF_AUDIBLE_ALARM, 0xBF, 0, 0x40, 6 }, 				// 0=on, 1=off ***
+	{ BF_CLOCK, 0xBF, 0, 0x20, 5 },							// 0=off, 1=on
+	{ BF_CEILING_DISPLAY_DIV_BY_10, 0xBF, 0, 0x10, 4 },		// 0=off, 1=on
+	{ BF_GAS_2_AS_FIRST_GAS, 0xBF, 0, 0x02, 1 },			// 0=dis, 1=ena
+	{ BF_ENABLE_HELIUM_COMPUTATIONS, 0xBF, 0, 0x01, 0 },	// 0=dis, 1=ena
+
+	{ BF_AUTOMATIC_PO2_FO2_SWITCHING, 0xBF, 1, 0x04, 2 },	// 0=dis, 1=ena
+	{ BF_TOUCH_PROGRAMMING_PO2_FO2_SWITCH, 0xBF, 1, 0x02, 1 },	// 0=dis, 1=ena
+};
+
+struct cochran_emc_config1_t {
+	unsigned char unknown1[209];
+	unsigned short int dive_count;
+	unsigned char unknown2[274];
+	unsigned short int serial_num;
+	unsigned char unknown3[24];
+} __attribute__((packed));
+
+typedef struct cochran_emc_config1_t cochran_emc_config1_t;
diff --git a/src/cochran_emc_parser.c b/src/cochran_emc_parser.c
new file mode 100644
index 0000000..cf6e9af
--- /dev/null
+++ b/src/cochran_emc_parser.c
@@ -0,0 +1,399 @@
+/*
+ * libdivecomputer
+ *
+ * Copyright (C) 2014 John Van Ostrand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <stdlib.h>
+#include <math.h>
+
+#include <libdivecomputer/units.h>
+#include <libdivecomputer/cochran.h>
+
+#include "context-private.h"
+#include "device-private.h"
+#include "parser-private.h"
+#include "serial.h"
+#include "array.h"
+
+#include "cochran_common.h"
+#include "cochran_emc.h"
+#include "cochran_common_parser.h"
+
+
+struct dive_stats {
+	unsigned int dive_time;
+	float max_depth;
+	float avg_depth;
+	float min_temp;
+};
+
+
+static dc_status_t cochran_emc_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
+static dc_status_t cochran_emc_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata);
+static void cochran_emc_parse_dive_stats (dc_parser_t *abstract, struct dive_stats *stats);
+
+
+static dc_parser_vtable_t cochran_emc_parser_vtable = {
+	DC_FAMILY_COCHRAN_EMC,
+	cochran_common_parser_set_data,		/* set_data */
+	cochran_common_parser_get_datetime,	/* datetime */
+	cochran_emc_parser_get_field,		/* fields */
+	cochran_emc_parser_samples_foreach,	/* samples_foreach */
+	cochran_common_parser_destroy			/* destroy */
+};
+
+
+dc_status_t
+cochran_emc_parser_create (dc_parser_t **out, dc_context_t *context)
+{
+	return cochran_common_parser_create(out, context, &cochran_emc_parser_vtable);
+}
+
+
+static dc_status_t
+cochran_emc_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	cochran_emc_log_t *log = (cochran_emc_log_t *) data->current_log;
+
+	dc_gasmix_t *gasmix = (dc_gasmix_t *) value;
+	dc_salinity_t *water = (dc_salinity_t *) value;
+
+	unsigned int dive_time;
+	float max_depth, avg_depth, min_temp;
+	struct dive_stats stats;
+
+	if (value) {
+		switch (type) {
+		case DC_FIELD_TEMPERATURE:
+			if (data->corrupt_dive) {
+				cochran_emc_parse_dive_stats(abstract, &stats);
+				*((unsigned int*) value) = min_temp;
+			} else
+				*((unsigned int*) value) = (log->start_temperature - 32) / 1.8;
+			break;
+		case DC_FIELD_DIVETIME:
+			if (data->corrupt_dive) {
+				cochran_emc_parse_dive_stats(abstract, &stats);
+				*((unsigned int*) value) = dive_time;
+			} else
+				*((unsigned int *) value) = array_uint16_le (log->bt) * 60;
+			break;
+		case DC_FIELD_MAXDEPTH:
+			if (data->corrupt_dive) {
+				cochran_emc_parse_dive_stats(abstract, &stats);
+				*((unsigned int*) value) = max_depth;
+			} else
+				*((double *) value) = array_uint16_le (log->max_depth) / 4 * FEET;
+			break;
+		case DC_FIELD_AVGDEPTH:
+			if (data->corrupt_dive) {
+				cochran_emc_parse_dive_stats(abstract, &stats);
+				*((unsigned int*) value) = avg_depth;
+			} else
+				*((double *) value) = array_uint16_le (log->avg_depth) / 4 * FEET;
+			break;
+		case DC_FIELD_GASMIX_COUNT:
+			*((unsigned int *) value) = 10;
+			break;
+		case DC_FIELD_GASMIX:
+			gasmix->oxygen = (double) array_uint16_le ((char *)log->o2_percent + 2 * flags) / 256 / 100;
+			gasmix->helium = (double) array_uint16_le ((char *)log->he_percent + 2 * flags) / 256 / 100;
+			gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
+			break;
+		case DC_FIELD_SALINITY:
+			// 0 = low conductivity, 2 = high, maybe there's a 1?
+			water->type = ( log->water_conductivity == 0 ? DC_WATER_FRESH : DC_WATER_SALT );
+			water->density = 1000 + 12.5 * log->water_conductivity;
+			break;
+		case DC_FIELD_ATMOSPHERIC:
+			*(double *) value = ATM / BAR * pow(1 - 0.0000225577 * (double) log->altitude * 250 * FEET, 5.25588);
+			break;
+		default:
+			return DC_STATUS_UNSUPPORTED;
+		}
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+/*
+*  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_emc_guess_sample_size(cochran_data_t *data)
+{
+	unsigned int size = 0;
+	unsigned int offset, next_offset, memory_start_offset, memory_end_offset;
+
+	// Get memory boundries
+	memory_start_offset = array_uint32_le(data->config1 
+		+ data->sample_memory_start_address);
+	memory_end_offset = array_uint32_le(data->config1 
+		+ data->sample_memory_end_address);
+
+	// see if there is a next sample
+	if (data->current_log - data->logbook + 512 >= data->logbook_size) {
+		// we are the last log entry, use maximum data
+		size = memory_end_offset - offset + 1;
+	} else {
+		offset = array_uint32_le(data->current_log + 6);
+		next_offset = array_uint32_le(data->current_log + 512 + 30);
+
+		if (next_offset >= offset) {
+			size = next_offset - offset + 1;
+		} else {
+			// samples wrap to begin of memory
+			size = memory_end_offset - offset + next_offset 
+				- memory_start_offset + 2;
+		}
+	}
+
+	return (size);
+}
+	
+
+
+static dc_status_t
+cochran_emc_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	unsigned char *sdata = data->current_sample;
+	cochran_emc_log_t *log = (cochran_emc_log_t *) data->current_log;
+	unsigned int size = data->current_sample_size;
+	unsigned char *s;
+
+	dc_sample_value_t sample = {0}, empty_sample = {0};
+	unsigned int time = 0, last_sample_time;
+	unsigned int offset = 0;
+	double temperature;
+	double depth;
+	double ascent_rate;
+	unsigned char deco_obligation = 0;
+	unsigned int deco_ceiling = 0;
+	unsigned int deco_time = 0;
+
+	if (data->corrupt_dive) {
+		// Size of sample data is wrong, make a guess
+		size = cochran_emc_guess_sample_size(data);
+	}
+
+	// Cochran samples depth every second and varies between ascent rate
+	// and temp ever other second.
+	// In the 21st, 22nd, 23rd, 24th samples are NDL remaining, deco time and ceiling
+
+	// Prime with values from the dive log section
+	depth = array_uint16_le (log->start_depth) / 256;
+
+	last_sample_time = sample.time = time;
+	if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
+
+	sample.depth = depth * FEET;
+	if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
+
+	sample.temperature = (log->start_temperature - 32) / 1.8;
+	if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
+
+	while (offset < size) {
+		sample = empty_sample;
+
+		sample.time = time;
+		if (callback && last_sample_time != sample.time) {
+			// We haven't issued this time yet.
+			last_sample_time = sample.time;
+			callback (DC_SAMPLE_TIME, sample, userdata);
+		}
+
+		s = sdata + offset;
+
+		// If corrupt_dive end before offset
+		if (data->corrupt_dive) {
+			if (   s[0] == 0x10
+				|| s[0] == 0xFF
+				|| s[0] == 0xA8) {
+					break;
+			}
+			if (time > 1 && (s[0] == 0xE3 || s[0] == 0xF3))
+				break;
+		}
+
+		// Check for event
+		if (s[0] & 0x80) {
+			offset += cochran_common_handle_event(abstract, callback, userdata, s[0], offset, time);
+			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;
+				deco_time = (array_uint16_le(s + 3) + 1) * 60;
+				if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
+				break;
+			}
+
+			continue;
+		}
+
+			
+		// Depth is logged as change in feet, bit 0x40 means negative depth
+		depth += (float) (s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1);
+		sample.depth = depth * 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
+			ascent_rate = (s[1] & 0x7f) / 4 * (s[0] & 0x80 ? 1 : -1);
+			sample.ascent_rate = ascent_rate * FEET;
+			if (callback) callback (DC_SAMPLE_ASCENT_RATE, sample, userdata);
+		} else {
+			// Temperature logged in half degrees F above 20
+			temperature = s[1] / 2 + 20;
+			sample.temperature = (temperature - 32.0) / 1.8;
+
+			if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
+		}
+
+		// Tissue load is in 20 samples, then 2 samples for RBT,
+		// and 2 for ceiling
+		switch (time % 24)
+		{
+		case 20:
+			if (deco_obligation) {
+				/* Deco time for deepest stop, unused */
+				deco_time = (s[2] + s[5] * 256 + 1) * 60;
+			} else {
+				/* Get RBT */
+				sample.rbt = s[2] + s[5] * 256 + 1;
+				if (callback) callback (DC_SAMPLE_RBT, sample, userdata);
+				/* Send deco NDL sample */
+				sample.deco.time = sample.rbt * 60; // seconds
+				sample.deco.depth = 0;
+				if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
+			}
+			break;
+		case 22:
+			/* Deco time, total obligation */
+			if (deco_obligation) {
+				sample.deco.type = DC_DECO_DECOSTOP;
+				sample.deco.depth = deco_ceiling * FEET;
+				sample.deco.time = (s[2] + s[5] * 256 + 1) * 60; // minutes
+				if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
+			}
+			break;
+		}
+
+		time ++;
+		offset += 3;
+	}
+
+	return DC_STATUS_SUCCESS;
+}
+
+void cochran_emc_parse_dive_stats (dc_parser_t *abstract, struct dive_stats *stats)
+{
+	cochran_data_t *data = (cochran_data_t *) abstract->data;
+	unsigned char *sdata = data->current_sample;
+	cochran_emc_log_t *log = (cochran_emc_log_t *) data->current_log;
+	unsigned int size = data->current_sample_size;
+	unsigned char *s;
+	unsigned int offset = 0;
+	cochran_events_t *e = cochran_events;
+	unsigned char event_ptr;
+	float depth_m = 0, depth, temp;
+	unsigned int time = 0; 
+
+	// Check for corrupt dive
+	if (data->corrupt_dive)
+	{
+		// It's corrupt, guess at the sample size
+		size = cochran_emc_guess_sample_size(data);
+	}
+
+	stats->max_depth = 0;
+	stats->min_temp = 999;
+	stats->avg_depth = 0;
+
+	while (offset < size) {
+		s = sdata + offset;
+
+		if (   s[0] == 0x10
+			|| s[0] == 0xFF
+			|| s[0] == 0xA8) {
+				// End corrupted dive
+				break;
+		}
+
+		if (time > 1 && (s[0] == 0xE3 || s[0] == 0xF3))
+			break;
+
+		// Check for event
+		if (s[0] & 0x80) {
+			event_ptr = 0;
+			while (e[event_ptr].code && e[event_ptr].code != s[0])
+				event_ptr++;
+
+			// Advance some bytes
+			if (e[event_ptr].code)
+				offset += e[event_ptr].data_bytes;
+			else
+				offset ++;
+
+			continue;
+		}
+
+		// Depth is logged as change in feet, bit 0x40 means negative depth
+		depth += (float) (s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1);
+		depth_m = depth * FEET;
+
+		if (depth_m > stats->max_depth) stats->max_depth = depth_m;
+
+		stats->avg_depth = (stats->avg_depth * (time - 1) + depth_m) / time;
+
+		if (time % 2 != 0) {
+			// Temperature logged in half degrees F above 20
+			temp = s[1] / 2 + 20;
+			temp = (temp - 32.0) / 1.8;
+
+			if (temp < stats->min_temp) stats->min_temp = temp;
+		}
+
+		time ++;
+		offset += 3;
+	}
+
+	stats->dive_time = time - 1;
+}
diff --git a/src/descriptor.c b/src/descriptor.c
index 403b67a..20f74e6 100644
--- a/src/descriptor.c
+++ b/src/descriptor.c
@@ -223,6 +223,9 @@ static const dc_descriptor_t g_descriptors[] = {
 	{"Shearwater", "Petrel",   DC_FAMILY_SHEARWATER_PETREL, 3},
 	/* Dive Rite NiTek Q */
 	{"Dive Rite", "NiTek Q",   DC_FAMILY_DIVERITE_NITEKQ, 0},
+	{"Cochran", "Commander",	DC_FAMILY_COCHRAN_COMMANDER, 0},
+	{"Cochran", "EMC-20H",		DC_FAMILY_COCHRAN_EMC, 0},
+	{"Cochran", "EMC-16",		DC_FAMILY_COCHRAN_EMC, 0},
 };
 
 typedef struct dc_descriptor_iterator_t {
diff --git a/src/device.c b/src/device.c
index 9fad7a2..e8661b0 100644
--- a/src/device.c
+++ b/src/device.c
@@ -34,6 +34,7 @@
 #include <libdivecomputer/atomics.h>
 #include <libdivecomputer/shearwater.h>
 #include <libdivecomputer/diverite.h>
+#include <libdivecomputer/cochran.h>
 
 #include "device-private.h"
 #include "context-private.h"
@@ -154,6 +155,12 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr
 	case DC_FAMILY_DIVERITE_NITEKQ:
 		rc = diverite_nitekq_device_open (&device, context, name);
 		break;
+	case DC_FAMILY_COCHRAN_COMMANDER:
+		rc = cochran_cmdr_device_open (&device, context, name);
+		break;
+	case DC_FAMILY_COCHRAN_EMC:
+		rc = cochran_emc_device_open (&device, context, name);
+		break;
 	default:
 		return DC_STATUS_INVALIDARGS;
 	}
diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols
index 14a96a8..681a5d6 100644
--- a/src/libdivecomputer.symbols
+++ b/src/libdivecomputer.symbols
@@ -67,6 +67,7 @@ atomics_cobalt_parser_set_calibration
 shearwater_predator_parser_create
 shearwater_petrel_parser_create
 diverite_nitekq_parser_create
+cochran_emc_parser_create
 
 dc_device_open
 dc_device_close
@@ -166,3 +167,4 @@ shearwater_predator_extract_dives
 shearwater_petrel_device_open
 diverite_nitekq_device_open
 diverite_nitekq_extract_dives
+cochran_emc_device_open
diff --git a/src/parser.c b/src/parser.c
index 7cc7f4e..dc6655b 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -32,6 +32,7 @@
 #include <libdivecomputer/atomics.h>
 #include <libdivecomputer/shearwater.h>
 #include <libdivecomputer/diverite.h>
+#include <libdivecomputer/cochran.h>
 
 #include "parser-private.h"
 #include "device-private.h"
@@ -131,6 +132,12 @@ dc_parser_new (dc_parser_t **out, dc_device_t *device)
 	case DC_FAMILY_DIVERITE_NITEKQ:
 		rc = diverite_nitekq_parser_create (&parser, context);
 		break;
+	case DC_FAMILY_COCHRAN_COMMANDER:
+		rc = cochran_cmdr_parser_create (&parser, context);
+		break;
+	case DC_FAMILY_COCHRAN_EMC:
+		rc = cochran_emc_parser_create (&parser, context);
+		break;
 	default:
 		return DC_STATUS_INVALIDARGS;
 	}
-- 
1.8.3.1



More information about the devel mailing list