diff --git a/doc/McLeanExtreme-wire-format.txt b/doc/McLeanExtreme-wire-format.txt new file mode 100644 index 0000000..d598cbc --- /dev/null +++ b/doc/McLeanExtreme-wire-format.txt @@ -0,0 +1,102 @@ + +COMPUTER CONFIGURATION +---------------------- + +The computer configuration (including gradient factors, gasses etc) is read and written as a single, fixed size block. + +Config section +-------------- + +Computer configuration and individual dive configurations both start with identical generic configuration sections of length 0x002d bytes + +0x0000 uint8_t format ZERO +0x0001 uint8_t gas[0]_pO2 oxygen content [0, 100] (percent) +0x0002 uint8_t gas[0]_pHe helium content [0, 100] (percent) +0x0003 uint8_t gas[1]_pO2 oxygen content [0, 100] (percent) +0x0004 uint8_t gas[1]_pHe helium content [0, 100] (percent) +0x0005 uint8_t gas[2]_pO2 oxygen content [0, 100] (percent) +0x0006 uint8_t gas[2]_pHe helium content [0, 100] (percent) +0x0007 uint8_t gas[3]_pO2 oxygen content [0, 100] (percent) +0x0008 uint8_t gas[3]_pHe helium content [0, 100] (percent) +0x0009 uint8_t gas[4]_pO2 oxygen content [0, 100] (percent) +0x000a uint8_t gas[4]_pHe helium content [0, 100] (percent) +0x000b uint8_t gas[5]_pO2 oxygen content [0, 100] (percent) +0x000c uint8_t gas[5]_pHe helium content [0, 100] (percent) +0x000d uint8_t gas[6]_pO2 oxygen content [0, 100] (percent) +0x000e uint8_t gas[6]_pHe helium content [0, 100] (percent) +0x000f uint8_t gas[7]_pO2 oxygen content [0, 100] (percent) +0x0010 uint8_t gas[7]_pHe helium content [0, 100] (percent) +0x0011 uint8_t gas_mask bitwise mask of enabled gasses +0x0012 uint8_t gas_active current gas index +0x0013 uint8_t setpoint[0] (centibar) +0x0014 uint8_t setpoint[1] (centibar) +0x0015 uint8_t setpoint[2] (centibar) +0x0016 uint8_t setpoint_mask bitwise mask of enabled setpoints +0x0017 uint8_t setpoint_active current setpoint index +0x0018 bool metric display units [true: metric, false: imperial] +0x0019 uint16_t name dive number +0x001b uint8_t laststop_index last stop depth enumeration [0, 3] (metric: 3m, 4m, 5m, 6m imperial: 10ft, 13ft, 16ft, 18ft) +0x001c uint16_t Vasc ascent speed limit (millibar/minute) +0x001e uint16_t Psurf surface pressure (millibar) +0x0020 uint8_t gfs_index predefined gf enumeration [0, 5] (only used in rec mode) +0x0021 uint8_t gf lo custom gf low [0, 255] (ignored in rec mode) +0x0022 uint8_t gf hi custom gf high [0, 255] (ignored in rec mode) +0x0023 uint8_t density_index predefined water density enumeration [0: 1.000, 1: 1.020, 2: 1.030] +0x0024 uint16_t ppN2_limit nitrogen partial pressure alert (millibars) +0x0026 uint16_t ppO2_limit oxygen partial pressure alert (millibars) +0x0028 uint16_t ppO2_bottomlimit oxygen partial pressure alert (millibars) +0x002a uint16_t density_limit (units ??) +0x002c uint8_t operatingmode operating mode enumeration [0: OC rec, 1: OC tec, 2: CC, 3: Gauge] + +Computer configuration +---------------------- + +The computer configuration is read and written as a single block of 0x006a bytes formatted thusly: + + +0x002d + +0x0000 int64_t epoch internal adjustment used to set computer time +0x0008 uint16_t inactive_timeout number of seconds of inactivity before the computer turns itself off +0x000a uint16_t dive_timeout number of seconds on surface before declaring dive finished +0x000c uint16_t log_period number of seconds between dive samples +0x000e uint16_t log_timeout not used +0x0010 uint16_t brightness_timeout number of seconds of inactivity before the computer dims the screen +0x0012 uint8_t brightness dimmed screen brightness [0, 10] +0x0013 uint8_t colorscheme computer color scheme enumeration [0, 12] +0x0014 uint8_t language computer display language enumeration [0, 1] +0x0015 uint8_t batterytype user-defined battery type enumeration [0, 4] +0x0016 uint16_t batterytime computer up time since last battery change +0x0018 <48 bytes> compass calibration data (cannot be written) +0x0048 uint8_t button_sensitivity piezo button sensitivity [0, 9] +0x0049 uint8_t orientation screen and button orientation +0x004a char[32] diver_name diver name (guaranteed at least one nul terminator) +0x006a + +Dive configuration +------------------ + +Individual dives are read as a single block of 0x002d bytes formatted thusly: + + +0x002d + +0x0000 uint32_t log_start log start (seconds since midnight 1 jan 2000) +0x0004 uint32_t dive_start dive start (seconds since midnight 1 jan 2000) +0x0008 uint32_t dive_end dive end (seconds since midnight 1 jan 2000) +0x000c uint32_t log_end log end (seconds since midnight 1 jan 2000) +0x0010 uint8_t temp_min minimum ambient temperature (degrees centigrade) +0x0011 uint8_t temp_max maximum ambient temperature (degrees centigrade) +0x0012 uint16_t pO2_min minimum inspired oxygen partial pressure during the dive (millibars) +0x0014 uint16_t pO2_max maximum inspired oxygen partial pressure during the dive (millibars) +0x0016 uint16_t Pmax maximum ambient pressure during the dive (millibars) +0x0018 uint16_t Pav average ambient pressure during the dive (millibars) +0x001a uint32_t ISS integral supersaturation of dive mbar*minutes +0x001e uint16_t CNS_start CNS at start of dive (percent) +0x0020 uint16_t CNS_max maximum CNS encountered during the dive (percent) +0x0022 uint16_t OTU OTU dive +0x0024 uint16_t tndl shortest NDL calculated during dive (seconds) +0x0026 uint32_t tdeco longest decompression time calculated during dive (seconds) +0x002a uint8_t ndeco deepest decompression stop index calculated during dive (seconds) +0x002b uint16_t tdesat desaturation time calculated at end of dive (seconds) +0x002d + + diff --git a/examples/common.c b/examples/common.c index ebc1542..7930a70 100644 --- a/examples/common.c +++ b/examples/common.c @@ -92,6 +92,7 @@ static const backend_table_t g_backends[] = { {"divecomputereu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0}, {"descentmk1", DC_FAMILY_GARMIN, 0}, {"cosmiq", DC_FAMILY_DEEPBLU, 0}, + {"mclean", DC_FAMILY_MCLEAN_EXTREME, 0}, }; static const transport_table_t g_transports[] = { diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index 0819843..b0aa67f 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -112,6 +112,8 @@ typedef enum dc_family_t { DC_FAMILY_GARMIN = (16 << 16), /* Deepblu */ DC_FAMILY_DEEPBLU = (17 << 16), + /* McLean */ + DC_FAMILY_MCLEAN_EXTREME = (18 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index 0ae0ed6..dcf3f49 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -514,6 +514,14 @@ RelativePath="..\src\deepblu_parser.c" > + + + + @@ -868,6 +876,10 @@ RelativePath="..\src\deepblu.h" > + + diff --git a/src/Makefile.am b/src/Makefile.am index af2bc6d..8c6ec5e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -75,6 +75,7 @@ libdivecomputer_la_SOURCES = \ tecdiving_divecomputereu.h tecdiving_divecomputereu.c tecdiving_divecomputereu_parser.c \ garmin.h garmin.c garmin_parser.c \ deepblu.h deepblu.c deepblu_parser.c \ + mclean_extreme.h mclean_extreme.c mclean_extreme_parser.c \ socket.h socket.c \ irda.c \ usbhid.c \ diff --git a/src/descriptor.c b/src/descriptor.c index 9836d52..a212def 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -50,6 +50,7 @@ static int dc_filter_mares (dc_transport_t transport, const void *userdata); static int dc_filter_divesystem (dc_transport_t transport, const void *userdata); static int dc_filter_oceanic (dc_transport_t transport, const void *userdata); static int dc_filter_deepblu (dc_transport_t transport, const void *userdata); +static int dc_filter_mclean(dc_transport_t transport, const void *userdata); static dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -382,6 +383,8 @@ static const dc_descriptor_t g_descriptors[] = { {"Garmin", "Descent Mk1", DC_FAMILY_GARMIN, 2859, DC_TRANSPORT_USBSTORAGE, dc_filter_garmin}, /* Deepblu */ {"Deepblu", "Cosmiq+", DC_FAMILY_DEEPBLU, 0, DC_TRANSPORT_BLE, dc_filter_deepblu}, + /* McLean Extreme */ + { "McLean", "Extreme", DC_FAMILY_MCLEAN_EXTREME, 0, DC_TRANSPORT_BLUETOOTH, dc_filter_mclean }, }; static int @@ -661,6 +664,22 @@ static int dc_filter_deepblu (dc_transport_t transport, const void *userdata) return 1; } +static int dc_filter_mclean(dc_transport_t transport, const void* userdata) +{ + static const char* const bluetooth[] = { + "Extreme", + }; + + if (transport == DC_TRANSPORT_BLUETOOTH) { + return DC_FILTER_INTERNAL(userdata, bluetooth, 0, dc_match_name); + } + else if (transport == DC_TRANSPORT_SERIAL) { + return DC_FILTER_INTERNAL(userdata, rfcomm, 1, dc_match_devname); + } + + return 1; +} + dc_status_t dc_descriptor_iterator (dc_iterator_t **out) { diff --git a/src/device.c b/src/device.c index 15bfcf3..3167f4b 100644 --- a/src/device.c +++ b/src/device.c @@ -59,6 +59,7 @@ #include "tecdiving_divecomputereu.h" #include "garmin.h" #include "deepblu.h" +#include "mclean_extreme.h" #include "device-private.h" #include "context-private.h" @@ -219,6 +220,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_DEEPBLU: rc = deepblu_device_open (&device, context, iostream); break; + case DC_FAMILY_MCLEAN_EXTREME: + rc = mclean_extreme_device_open(&device, context, iostream); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/mclean_extreme.c b/src/mclean_extreme.c new file mode 100644 index 0000000..c3499a5 --- /dev/null +++ b/src/mclean_extreme.c @@ -0,0 +1,496 @@ +/* + * libdivecomputer + * + * Copyright (C) 2018 Jef Driesen + * + * 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 // memcmp, memcpy +#include // malloc, free +#include + +#include "mclean_extreme.h" +#include "context-private.h" +#include "device-private.h" +#include "array.h" + +#define ISINSTANCE(device) dc_device_isinstance((device), &mclean_extreme_device_vtable) + +#define MAXRETRIES 14 + +#define STX 0x7E + +#define CMD_COMPUTER 0xa0 // download computer configuration +#define CMD_SETCOMPUTER 0xa1 // upload computer configuration +#define CMD_DIVE 0xa3 // download specified dive configuration and samples +#define CMD_CLOSE 0xaa // close connexion and turn off bluetooth + +#define SZ_PACKET 512 // maximum packe payload length +#define SZ_SUMMARY 7 // size of the device fingerprint +#define SZ_CFG 0x002d // size of the common dive/computer header +#define SZ_COMPUTER 0x0097 // size of the computer state dump +#define SZ_DIVE 0x005e // size of the dive state dump +#define SZ_SAMPLE 0x0004 // size of the sample state dump + +// private device parsing functions ////////////////////////////////////////////////////////////////////////////////////////// + +static uint16_t uint16(const unsigned char* buffer, int addr) { return (buffer[0 + addr] << 0) | (buffer[1 + addr] << 8); } +static uint32_t uint32(const unsigned char* buffer, int addr) { return (uint16(buffer, addr) << 0) | (uint16(buffer, addr + 2) << 16); } + +static uint8_t device_format(const unsigned char* device) { return device[0x0000]; } +// static uint8_t device_gas_pO2(const unsigned char* device, int value) { return device[0x0001 + value * 2]; } +// static uint8_t device_gas_pHe(const unsigned char* device, int value) { return device[0x0001 + 1 + value * 2]; } +// static bool device_gas_enabled(const unsigned char* device, int value) { return (device[0x0011] & (1 << value)) != 0; } +// static uint8_t device_setpoint(const unsigned char* device, int value) { return device[0x0013 + value]; } +// static bool device_setpoint_enabled(const unsigned char* device, int value) { return (device[device, 0x0016] & (1 << value)) != 0; } +// static bool device_metric(unsigned char* device) { return device[0x0018] != 0; } +static uint16_t device_name(const unsigned char* device) { return uint16(device, 0x0019); } +// static uint16_t device_Vasc(const unsigned char* device) { return uint16(device, 0x001c); } +// static uint16_t device_Psurf(const unsigned char* device) { return uint16(device, 0x001e); } +// static uint8_t device_gfs_index(const unsigned char* device) { return device[0x0020]; } +// static uint8_t device_gflo(const unsigned char* device) { return device[0x0021]; } +// static uint8_t device_gfhi(const unsigned char* device) { return device[0x0022]; } +// static uint8_t device_density_index(const unsigned char* device) { return device[0x0023]; } +// static uint16_t device_ppN2_limit(const unsigned char* device) { return uint16(device, 0x0024); } +// static uint16_t device_ppO2_limit(const unsigned char* device) { return uint16(device, 0x0026); } +// static uint16_t device_ppO2_bottomlimit(const unsigned char* device) { return uint16(device, 0x0028); } +// static uint16_t device_density_limit(const unsigned char* device) { return uint16(device, 0x002a); } +// static uint8_t device_operatingmode(const unsigned char* device) { return device[0x002c]; } + +// static uint16_t device_inactive_timeout(const unsigned char* device) { return uint16(device, SZ_CFG + 0x0008); } +// static uint16_t device_dive_timeout(const unsigned char* device) { return uint16(device, SZ_CFG + 0x000a); } +// static uint16_t device_log_period(const unsigned char* device) { return device[SZ_CFG + 0x000c]; } +// static uint16_t device_log_timeout(const unsigned char* device) { return uint16(device, SZ_CFG + 0x000e); } +// static uint8_t device_brightness_timeout(const unsigned char* device) { return device[SZ_CFG + 0x0010]; } +// static uint8_t device_brightness(const unsigned char* device) { return device[SZ_CFG + 0x0012]; } +// static uint8_t device_colorscheme(const unsigned char* device) { return device[SZ_CFG + 0x0013]; } +// static uint8_t device_language(const unsigned char* device) { return device[SZ_CFG + 0x0014]; } +// static uint8_t device_batterytype(const unsigned char* device) { return device[SZ_CFG + 0x0015]; } +// static uint16_t device_batterytime(const unsigned char* device) { return uint16(device, SZ_CFG + 0x0014); } +// static uint8_t device_button_sensitivity(const unsigned char* device) { return device[SZ_CFG + 0x0016]; } +// static uint8_t device_orientation(const unsigned char* device) { return device[SZ_CFG + 0x0049]; } +// static const char* device_owner(const unsigned char* device) { return (const char*)& device[SZ_CFG + 0x004a]; } + +// private dive parsing functions ////////////////////////////////////////////////////////////////////////////////////////// + +static uint8_t dive_format(const unsigned char* dive) { return dive[0x0000]; } +static uint16_t dive_samples_cnt(const unsigned char* dive) { return uint16(dive, 0x005c); } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct mclean_extreme_device_t { + dc_device_t base; + dc_iostream_t* iostream; + unsigned char fingerprint[SZ_SUMMARY]; + uint8_t data[SZ_COMPUTER]; +} mclean_extreme_device_t; + +static dc_status_t mclean_extreme_device_set_fingerprint(dc_device_t* abstract, const unsigned char data[], unsigned int size); +static dc_status_t mclean_extreme_device_foreach(dc_device_t* abstract, dc_dive_callback_t callback, void* userdata); +static dc_status_t mclean_extreme_device_close(dc_device_t* abstract); + +static const dc_device_vtable_t mclean_extreme_device_vtable = { + sizeof(mclean_extreme_device_t), + DC_FAMILY_MCLEAN_EXTREME, + mclean_extreme_device_set_fingerprint, /* set_fingerprint */ + NULL, /* read */ + NULL, /* write */ + NULL, /* dump */ + mclean_extreme_device_foreach, /* foreach */ + NULL, /* timesync */ + mclean_extreme_device_close, /* close */ +}; + +static unsigned short +checksum_crc(const unsigned char data[], unsigned int size, unsigned short init) +{ + unsigned short crc = init; + for (unsigned int i = 0; i < size; ++i) { + crc ^= data[i] << 8; + if (crc & 0x8000) { + crc <<= 1; + crc ^= 0x1021; + } + else { + crc <<= 1; + } + } + + return crc; +} + +static dc_status_t +mclean_extreme_send(mclean_extreme_device_t* device, unsigned char cmd, const unsigned char data[], size_t size) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t* abstract = (dc_device_t*)device; + unsigned short crc = 0; + + if (device_is_cancelled(abstract)) + return DC_STATUS_CANCELLED; + + if (size > SZ_PACKET) + return DC_STATUS_INVALIDARGS; + + // Setup the data packet + unsigned char packet[SZ_PACKET + 11] = { + STX, + 0x00, + (size >> 0) & 0xFF, + (size >> 8) & 0xFF, + (size >> 16) & 0xFF, + (size >> 24) & 0xFF, + cmd, + }; + if (size) { + memcpy(packet + 7, data, size); + } + crc = checksum_crc(packet + 1, size + 6, 0); + packet[size + 7] = (crc >> 8) & 0xFF; + packet[size + 8] = (crc) & 0xFF; + packet[size + 9] = 0x00; + packet[size + 10] = 0x00; + + // Give the dive computer some extra time. + dc_iostream_sleep(device->iostream, 300); + + // Send the data packet. + status = dc_iostream_write(device->iostream, packet, size + 11, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to send the command."); + return status; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_receive(mclean_extreme_device_t* device, unsigned char rsp, unsigned char data[], size_t max_size, size_t* actual_size) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t* abstract = (dc_device_t*)device; + unsigned char header[7]; + unsigned int nretries = 0; + + // Read the packet start byte. + // Unfortunately it takes a relative long time, about 6-8 seconds, + // before the STX byte arrives. Hence the standard timeout of one + // second is not sufficient, and we need to retry a few times on + // timeout. The advantage over using a single read operation with a + // large timeout is that we can give the user a chance to cancel the + // operation. + while (1) { + status = dc_iostream_read(device->iostream, header + 0, 1, NULL); + if (status != DC_STATUS_SUCCESS) { + if (status != DC_STATUS_TIMEOUT) { + ERROR(abstract->context, "Failed to receive the packet start byte."); + return status; + } + + // Abort if the maximum number of retries is reached. + if (nretries++ >= MAXRETRIES) + return status; + + // Cancel if requested by the user. + if (device_is_cancelled(abstract)) + return DC_STATUS_CANCELLED; + + // Try again. + continue; + } + + if (header[0] == STX) + break; + + // Reset the retry counter. + nretries = 0; + } + + // Read the packet header. + status = dc_iostream_read(device->iostream, header + 1, sizeof(header) - 1, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to receive the packet header."); + return status; + } + + // Verify the type byte. + unsigned int type = header[1]; + if (type != 0x00) { + ERROR(abstract->context, "Unexpected type byte (%02x).", type); + return DC_STATUS_PROTOCOL; + } + + // Verify the length. + unsigned int length = array_uint32_le(header + 2); + if (length > max_size) { + ERROR(abstract->context, "Unexpected packet length (%u for %zu).", length, max_size); + return DC_STATUS_PROTOCOL; + } + + // Get the command type. + unsigned int cmd = header[6]; + if (cmd != rsp) { + ERROR(abstract->context, "Unexpected command byte (%02x).", cmd); + return DC_STATUS_PROTOCOL; + } + + size_t nbytes = 0; + while (nbytes < length) { + // Set the maximum packet size. + size_t len = 1000; + + // Limit the packet size to the total size. + if (nbytes + len > length) + len = length - nbytes; + + // Read the packet payload. + status = dc_iostream_read(device->iostream, data + nbytes, len, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to receive the packet payload."); + return status; + } + + nbytes += len; + } + + // Read the packet checksum. + unsigned char checksum[4]; + status = dc_iostream_read(device->iostream, checksum, sizeof(checksum), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to receive the packet checksum."); + return status; + } + + // Verify the checksum. + unsigned short crc = array_uint16_be(checksum); + unsigned short ccrc = 0; + ccrc = checksum_crc(header + 1, sizeof(header) - 1, ccrc); + ccrc = checksum_crc(data, length, ccrc); + if (crc != ccrc || checksum[2] != 0x00 || checksum[3] != 0) { + ERROR(abstract->context, "Unexpected packet checksum."); + return DC_STATUS_PROTOCOL; + } + + if (actual_size != NULL) { + // Return the actual length. + *actual_size = length; + } + + return DC_STATUS_SUCCESS; +} + +dc_status_t +mclean_extreme_device_open(dc_device_t** out, dc_context_t* context, dc_iostream_t* iostream) +{ + dc_status_t status = DC_STATUS_SUCCESS; + mclean_extreme_device_t* device = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + device = (mclean_extreme_device_t*)dc_device_allocate(context, &mclean_extreme_device_vtable); + if (device == NULL) { + ERROR(context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + device->iostream = iostream; + memset(device->fingerprint, 0, sizeof(device->fingerprint)); + + // Set the serial communication protocol (115200 8N1). + status = dc_iostream_configure(device->iostream, 115200, 8, DC_PARITY_NONE, DC_STOPBITS_ONE, DC_FLOWCONTROL_NONE); + if (status != DC_STATUS_SUCCESS) { + ERROR(context, "Failed to set the terminal attributes."); + goto error_free; + } + + // Set the timeout for receiving data (1000ms). + status = dc_iostream_set_timeout(device->iostream, 1000); + if (status != DC_STATUS_SUCCESS) { + ERROR(context, "Failed to set the timeout."); + goto error_free; + } + + // Send the init command. + status = mclean_extreme_send(device, CMD_COMPUTER, NULL, 0); + if (status != DC_STATUS_SUCCESS) { + ERROR(context, "Failed to send the init command."); + goto error_free; + } + + // Read the device info. + status = mclean_extreme_receive(device, CMD_COMPUTER, device->data, sizeof(device->data), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(context, "Failed to receive the device info."); + goto error_free; + } + + if (device_format(device->data) != 0) { /* bad device format */ + status = DC_STATUS_DATAFORMAT; + ERROR(context, "Unsupported device format."); + goto error_free; + } + + *out = (dc_device_t*)device; + + return DC_STATUS_SUCCESS; + +error_free: + dc_device_deallocate((dc_device_t*)device); + return status; +} + +static dc_status_t +mclean_extreme_device_close(dc_device_t* abstract) +{ + dc_status_t status = DC_STATUS_SUCCESS; + mclean_extreme_device_t* device = (mclean_extreme_device_t*)abstract; + + status = mclean_extreme_send(device, CMD_CLOSE, NULL, 0); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to send the exit command."); + return status; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_device_set_fingerprint(dc_device_t* abstract, const unsigned char data[], unsigned int size) +{ + mclean_extreme_device_t* device = (mclean_extreme_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, 0, sizeof(device->fingerprint)); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_device_readsamples(dc_device_t* abstract, uint8_t* dive) +{ + dc_status_t status = DC_STATUS_SUCCESS; + mclean_extreme_device_t* device = (mclean_extreme_device_t*)abstract; + + int samples_cnt = (dive[0x005c] << 0) + (dive[0x005d] << 8); // number of samples to follow + dive = &dive[SZ_DIVE]; + + while (samples_cnt != 0) { + unsigned char data[SZ_PACKET]; // buffer for read packet data + size_t length; // buffer for read packet length + + status = mclean_extreme_receive(device, CMD_DIVE, data, SZ_PACKET, &length); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to receive dive samples."); + break; + } + + int packet_cnt = length / SZ_SAMPLE; // number of samples in the packet + if (packet_cnt > samples_cnt) { /* too many samples received */ + status = DC_STATUS_DATAFORMAT; + ERROR(abstract->context, "Too many dive samples received."); + break; + } + if (length != packet_cnt * SZ_SAMPLE) { /* not an integer number of samples */ + status = DC_STATUS_DATAFORMAT; + ERROR(abstract->context, "Partial samples received."); + break; + } + + memcpy(dive, data, packet_cnt * SZ_SAMPLE); // append samples to dive buffer + + dive = &dive[packet_cnt * SZ_SAMPLE]; // move buffer write cursor + samples_cnt -= packet_cnt; + } + + return status; +} + +static dc_status_t +mclean_extreme_device_foreach(dc_device_t* abstract, dc_dive_callback_t callback, void* userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + mclean_extreme_device_t* device = (mclean_extreme_device_t*)abstract; + + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + int dives_cnt = device_name(device->data); + + progress.current = 0; + progress.maximum = dives_cnt; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + for (int i = dives_cnt - 1; i >= 0; --i) { + unsigned char data[512]; // buffer for read packet data + size_t length; // buffer for read packet length + + unsigned char id[] = { (unsigned char)i, (unsigned char)(i >> 8) }; // payload for CMD_DIVE request + + status = mclean_extreme_send(device, CMD_DIVE, id, sizeof(id)); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to send the get dive command."); + break; + } + + status = mclean_extreme_receive(device, CMD_DIVE, data, 512, &length); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to receive dive header."); + break; + } + + if (dive_format(data) != 0) { /* can't understand the format */ + INFO(abstract->context, "Skipping unsupported dive format"); + break; + } + + int cnt_samples = dive_samples_cnt(data); // number of samples to follow + size_t size = SZ_DIVE + cnt_samples * SZ_SAMPLE; // total buffer size required for this dive + uint8_t* dive = (uint8_t *)malloc(size); // buffer for this dive + + if (dive == NULL) { + status = DC_STATUS_NOMEMORY; + break; + } + + memcpy(dive, data, SZ_DIVE); // copy data to dive buffer + status = mclean_extreme_device_readsamples(abstract, dive); // append samples to buffer + + if (status != DC_STATUS_SUCCESS) { /* failed to read dive samples */ + free(dive); // cleanup + break; // stop downloading + } + + if (callback && !callback(dive, size, dive, sizeof(device->fingerprint), userdata)) { /* cancelled by callback */ + free(dive); // cleanup + break; // stop downloading + } + + free(dive); + progress.current = dives_cnt - 1 - i; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + } + + return status; +} diff --git a/src/mclean_extreme.h b/src/mclean_extreme.h new file mode 100644 index 0000000..883061f --- /dev/null +++ b/src/mclean_extreme.h @@ -0,0 +1,43 @@ +/* + * libdivecomputer + * + * Copyright (C) 2018 Jef Driesen + * + * 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 MCLEAN_EXTREME_H +#define MCLEAN_EXTREME_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t +mclean_extreme_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream); + +dc_status_t +mclean_extreme_parser_create (dc_parser_t **parser, dc_context_t *context); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* mclean_extreme_H */ diff --git a/src/mclean_extreme_parser.c b/src/mclean_extreme_parser.c new file mode 100644 index 0000000..d8ff492 --- /dev/null +++ b/src/mclean_extreme_parser.c @@ -0,0 +1,278 @@ +/* + * libdivecomputer + * + * Copyright (C) 2018 Jef Driesen + * + * 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 "mclean_extreme.h" +#include "context-private.h" +#include "parser-private.h" +#include "array.h" + +#define ISINSTANCE(parser) dc_device_isinstance((parser), &mclean_extreme_parser_vtable) + +#define SZ_CFG 0x002d // size of the common dive/computer header +#define SZ_COMPUTER 0x006a // size of the computer state dump +#define SZ_DIVE 0x005e // size of the dive state dump +#define SZ_SAMPLE 0x0004 // size of the sample state dump + +// private dive parsing functions ////////////////////////////////////////////////////////////////////////////////////////// + +static uint16_t uint16(const unsigned char* buffer, int addr) { return (buffer[0 + addr] << 0) | (buffer[1 + addr] << 8); } +static uint32_t uint32(const unsigned char* buffer, int addr) { return (buffer[0 + addr] << 0) | (buffer[1 + addr] << 8) | (buffer[2 + addr] << 16) | (buffer[3 + addr] << 24); } + +static uint8_t dive_format(const unsigned char* dive) { return dive[0x0000]; } +static uint8_t dive_gas_pO2(const unsigned char* dive, int value) { return dive[0x0001 + value * 2]; } +static uint8_t dive_gas_pHe(const unsigned char* dive, int value) { return dive[0x0001 + 1 + value * 2]; } +// static bool dive_gas_enabled(const unsigned char* dive, int value) { return (dive[0x0011] & (1 << value)) != 0; } +static uint8_t dive_setpoint(const unsigned char* dive, int value) { return dive[0x0013 + value]; } +// static bool dive_setpoint_enabled(const unsigned char* dive, int value) { return (dive[dive, 0x0016] & (1 << value)) != 0; } +// static bool dive_metric(const unsigned char* dive) { return dive[0x0018] != 0; } +// static uint16_t dive_name(const unsigned char* dive) { return uint16(dive, 0x0019); } +// static uint16_t dive_Vasc(const unsigned char* dive) { return uint16(dive, 0x001c); } +static uint16_t dive_Psurf(const unsigned char* dive) { return uint16(dive, 0x001e); } +// static uint8_t dive_gfs_index(const unsigned char* dive) { return dive[0x0020]; } +// static uint8_t dive_gflo(const unsigned char* dive) { return dive[0x0021]; } +// static uint8_t dive_gfhi(const unsigned char* dive) { return dive[0x0022]; } +static uint8_t dive_density_index(const unsigned char* dive) { return dive[0x0023]; } +// static uint16_t dive_ppN2_limit(const unsigned char* dive) { return uint16(dive, 0x0024); } +// static uint16_t dive_ppO2_limit(const unsigned char* dive) { return uint16(dive, 0x0026); } +// static uint16_t dive_ppO2_bottomlimit(const unsigned char* dive) { return uint16(dive, 0x0028); } +// static uint16_t dive_density_limit(const unsigned char* dive) { return uint16(dive, 0x002a); } +static uint8_t dive_operatingmode(const unsigned char* dive) { return dive[0x002c]; } + +static uint32_t dive_logstart(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x0000); } +static uint32_t dive_divestart(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x0004); } +static uint32_t dive_diveend(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x0008); } +static uint32_t dive_logend(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x000c); } +static uint8_t dive_temp_min(const unsigned char* dive) { return dive[SZ_CFG + 0x0010]; } +static uint8_t dive_temp_max(const unsigned char* dive) { return dive[SZ_CFG + 0x0011]; } +// static uint16_t dive_pO2_min(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0012); } +// static uint16_t dive_pO2_max(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0014); } +static uint16_t dive_Pmax(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0016); } +static uint16_t dive_Pav(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0018); } +// static uint32_t ISS(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x001a); } +// static uint16_t CNS_start(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x001e); } +// static uint16_t CNS_max(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0020); } +// static uint16_t OTU(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0022); } +// static uint16_t tndl(const unsigned char* dive) { return uint16(dive, SZ_CFG + 0x0024); } +// static uint32_t tdeco(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x0026); } +// static uint8_t ndeco(const unsigned char* dive) { return dive[SZ_CFG + 0x002a]; } +// static uint32_t tdesat(const unsigned char* dive) { return uint32(dive, SZ_CFG + 0x002b); } +static uint16_t dive_samples_cnt(const unsigned char* dive) { return uint16(dive, 0x005c); } + +// private sample parsing functions ////////////////////////////////////////////////////////////////////////////////////////// + +static uint16_t sample_depth(const unsigned char* dive, int n) { return uint16(dive, SZ_DIVE + n * SZ_SAMPLE + 0); } +static uint8_t sample_temperature(const unsigned char* dive, int n) { return dive[SZ_DIVE + n * SZ_SAMPLE + 2]; } +static bool sample_ccr(const unsigned char* dive, int n) { return dive[SZ_DIVE + n * SZ_SAMPLE + 3] & 0b10000000; } +static uint8_t sample_sp_index(const unsigned char* dive, int n) { return (dive[SZ_DIVE + n * SZ_SAMPLE + 3] & 0b01100000) >> 5; } +static uint8_t sample_gas_index(const unsigned char* dive, int n) { return (dive[SZ_DIVE + n * SZ_SAMPLE + 3] & 0b00011100) >> 2; } + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct mclean_extreme_parser_t mclean_extreme_parser_t; + +struct mclean_extreme_parser_t { + dc_parser_t base; +}; + +static dc_status_t mclean_extreme_parser_set_data(dc_parser_t* abstract, const unsigned char* data, unsigned int size); +static dc_status_t mclean_extreme_parser_get_datetime(dc_parser_t* abstract, dc_datetime_t* datetime); +static dc_status_t mclean_extreme_parser_get_field(dc_parser_t* abstract, dc_field_type_t type, unsigned int flags, void* value); +static dc_status_t mclean_extreme_parser_samples_foreach(dc_parser_t* abstract, dc_sample_callback_t callback, void* userdata); + +static const dc_parser_vtable_t mclean_extreme_parser_vtable = { + sizeof(mclean_extreme_parser_t), + DC_FAMILY_MCLEAN_EXTREME, + mclean_extreme_parser_set_data, /* set_data */ + mclean_extreme_parser_get_datetime, /* datetime */ + mclean_extreme_parser_get_field, /* fields */ + mclean_extreme_parser_samples_foreach, /* samples_foreach */ + NULL /* destroy */ +}; + +dc_status_t +mclean_extreme_parser_create(dc_parser_t** out, dc_context_t* context) +{ + mclean_extreme_parser_t* parser = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + parser = (mclean_extreme_parser_t*)dc_parser_allocate(context, &mclean_extreme_parser_vtable); + if (parser == NULL) { + ERROR(context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + *out = (dc_parser_t*)parser; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_parser_set_data(dc_parser_t* abstract, const unsigned char* data, unsigned int size) +{ + dc_status_t status = DC_STATUS_SUCCESS; + + if (dive_format(data) != 0) { + status = DC_STATUS_DATAFORMAT; + ERROR(abstract->context, "Unsupported dive format."); + } + + const int samples_cnt = dive_samples_cnt(data); + + if (size != SZ_DIVE + samples_cnt * SZ_SAMPLE) { + ERROR(abstract->context, "Corrupt dive in memory."); + return DC_STATUS_DATAFORMAT; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_parser_get_datetime(dc_parser_t* abstract, dc_datetime_t* datetime) +{ + if (datetime) { + const unsigned char* dive = abstract->data; + dc_ticks_t dc_ticks = 946684800 + dive_logstart(dive); // raw times are offsets for 1/1/2000 + + dc_datetime_gmtime(datetime, dc_ticks); + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_parser_get_field(dc_parser_t* abstract, dc_field_type_t type, unsigned int flags, void* value) +{ + static const double densities[] = { 1.000, 1.020, 1.030 }; + static const dc_divemode_t divemodes[] = { DC_DIVEMODE_OC, DC_DIVEMODE_OC, DC_DIVEMODE_CCR, DC_DIVEMODE_GAUGE }; + + const unsigned char* dive = abstract->data; + dc_gasmix_t* gasmix = (dc_gasmix_t*)value; + + if (value) { + switch (type) { + case DC_FIELD_DIVETIME: + *((unsigned int*)value) = dive_logend(dive) - dive_logstart(dive); + break; + + case DC_FIELD_MAXDEPTH: + *((double*)value) = 0.01 * (dive_Pmax(dive) - dive_Psurf(dive)) / densities[dive_density_index(dive)]; + break; + + case DC_FIELD_AVGDEPTH: + *((double*)value) = 0.01 * (dive_Pav(dive) - dive_Psurf(dive)) / densities[dive_density_index(dive)]; + break; + + case DC_FIELD_SALINITY: + switch (dive_density_index(dive)) { + case 0: ((dc_salinity_t*)value)->type = DC_WATER_FRESH; ((dc_salinity_t*)value)->density = densities[dive_density_index(dive)]; break; + case 1: ((dc_salinity_t*)value)->type = DC_WATER_SALT; ((dc_salinity_t*)value)->density = densities[dive_density_index(dive)]; break; + case 2: ((dc_salinity_t*)value)->type = DC_WATER_SALT; ((dc_salinity_t*)value)->density = densities[dive_density_index(dive)]; break; + default: /* that's an error */ break; + } + break; + + case DC_FIELD_ATMOSPHERIC: + *((double*)value) = dive_Psurf(dive) / 1000.0; + break; + + // case DC_FIELD_TEMPERATURE_SURFACE: + + case DC_FIELD_TEMPERATURE_MINIMUM: + *((double*)value) = dive_temp_min(dive); + break; + + case DC_FIELD_TEMPERATURE_MAXIMUM: + *((double*)value) = dive_temp_max(dive); + break; + + case DC_FIELD_DIVEMODE: + *((dc_divemode_t*)value) = divemodes[dive_operatingmode(dive)]; + break; + + //case DC_FIELD_TANK: + //case DC_FIELD_TANK_COUNT: + + case DC_FIELD_GASMIX_COUNT: + *((unsigned int*)value) = 8; + break; + + case DC_FIELD_GASMIX: + gasmix->helium = 0.01 * dive_gas_pHe(dive, flags); + gasmix->oxygen = 0.01 * dive_gas_pO2(dive, flags); + gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium; + break; + + default: + return DC_STATUS_UNSUPPORTED; + } + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_parser_samples_foreach(dc_parser_t* abstract, dc_sample_callback_t callback, void* userdata) +{ + const unsigned char* dive = abstract->data; + const unsigned int size = abstract->size; + + const unsigned int interval = 20; // this should be dive[log end] - dive[log start]? + const int samples_cnt = dive_samples_cnt(dive); // number of samples to follow + + if (callback) { + unsigned int time = 0; + + for (int i = 0; i < samples_cnt; ++i) { + dc_sample_value_t sample = { 0 }; + + sample.time = time; + callback(DC_SAMPLE_TIME, sample, userdata); + + sample.depth = sample_depth(dive, i) * 0.1; + callback(DC_SAMPLE_DEPTH, sample, userdata); + + sample.temperature = sample_temperature(dive, i); + callback(DC_SAMPLE_TEMPERATURE, sample, userdata); + + sample.gasmix = sample_gas_index(dive, i); + callback(DC_SAMPLE_GASMIX, sample, userdata); + + if (sample_ccr(dive, i)) { + const uint8_t sp_index = sample_sp_index(dive, i); + + sample.setpoint = 100.0 * dive_setpoint(dive, sp_index); + if (callback) callback(DC_SAMPLE_SETPOINT, sample, userdata); + } + + time += interval; + } + } + + return DC_STATUS_SUCCESS; +} diff --git a/src/parser.c b/src/parser.c index 8fd16ee..df80efc 100644 --- a/src/parser.c +++ b/src/parser.c @@ -59,6 +59,7 @@ #include "tecdiving_divecomputereu.h" #include "garmin.h" #include "deepblu.h" +#include "mclean_extreme.h" #include "context-private.h" #include "parser-private.h" @@ -180,6 +181,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_DEEPBLU: rc = deepblu_parser_create (&parser, context); break; + case DC_FAMILY_MCLEAN_EXTREME: + rc = mclean_extreme_parser_create(&parser, context); + break; default: return DC_STATUS_INVALIDARGS; }