diff --git a/examples/common.c b/examples/common.c index d8c7262..e305536 100644 --- a/examples/common.c +++ b/examples/common.c @@ -90,6 +90,7 @@ static const backend_table_t g_backends[] = { {"idive", DC_FAMILY_DIVESYSTEM_IDIVE, 0x03}, {"cochran", DC_FAMILY_COCHRAN_COMMANDER, 0}, {"divecomputereu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 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 7f94f14..a8703ab 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -104,6 +104,8 @@ typedef enum dc_family_t { DC_FAMILY_COCHRAN_COMMANDER = (14 << 16), /* Tecdiving */ DC_FAMILY_TECDIVING_DIVECOMPUTEREU = (15 << 16), + /* McLean */ + DC_FAMILY_MCLEAN_EXTREME = (16 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index 507140e..1c8b772 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -350,6 +350,14 @@ RelativePath="..\src\mares_puck.c" > + + + + @@ -696,6 +704,10 @@ RelativePath="..\src\mares_puck.h" > + + diff --git a/src/Makefile.am b/src/Makefile.am index 4d19b48..f2bcd46 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -72,6 +72,7 @@ libdivecomputer_la_SOURCES = \ buffer.c \ cochran_commander.h cochran_commander.c cochran_commander_parser.c \ tecdiving_divecomputereu.h tecdiving_divecomputereu.c tecdiving_divecomputereu_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 74d0f8a..54ae5c9 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -48,6 +48,7 @@ static int dc_filter_tecdiving (dc_transport_t transport, const void *userdata); 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_mclean (dc_transport_t transport, const void *userdata); static dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -376,6 +377,8 @@ static const dc_descriptor_t g_descriptors[] = { {"Cochran", "EMC-20H", DC_FAMILY_COCHRAN_COMMANDER, 5, DC_TRANSPORT_SERIAL, NULL}, /* Tecdiving DiveComputer.eu */ {"Tecdiving", "DiveComputer.eu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_tecdiving}, + /* McLean Extreme */ + { "McLean", "Extreme", DC_FAMILY_MCLEAN_EXTREME, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_mclean}, }; static int @@ -629,6 +632,21 @@ static int dc_filter_oceanic (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 9da9cd9..92cfa36 100644 --- a/src/device.c +++ b/src/device.c @@ -57,6 +57,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "mclean_extreme.h" #include "device-private.h" #include "context-private.h" @@ -211,6 +212,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_TECDIVING_DIVECOMPUTEREU: rc = tecdiving_divecomputereu_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..5785a24 --- /dev/null +++ b/src/mclean_extreme.c @@ -0,0 +1,591 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Jef Driesen, David Carron + * + * 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 "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_SERIALNUMBER 0x91 +#define CMD_COMPUTER 0xA0 +#define CMD_SET_COMPUTER 0xA1 +#define CMD_DIVE 0xA3 +#define CMD_CLOSE 0xAA +#define CMD_SET_TIME 0xAC +#define CMD_FIRMWARE 0xAD + +#define SZ_PACKET 512 +#define SZ_FINGERPRINT 7 +#define SZ_CFG 0x002D +#define SZ_COMPUTER (SZ_CFG + 0x6A) +#define SZ_HEADER (SZ_CFG + 0x31) +#define SZ_SAMPLE 0x0004 + +#define EPOCH 946684800 // 2000-01-01 00:00:00 UTC + +#define NSTEPS 1000 +#define STEP(i,n) (NSTEPS * (i) / (n)) + +typedef struct mclean_extreme_device_t { + dc_device_t base; + dc_iostream_t *iostream; + unsigned char fingerprint[SZ_FINGERPRINT]; +} 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_timesync(dc_device_t *abstract, const dc_datetime_t *datetime); +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 */ + mclean_extreme_device_timesync, /* timesync */ + mclean_extreme_device_close, /* close */ +}; + +static unsigned int +hashcode (const unsigned char data[], size_t size) +{ + unsigned int result = 0; + + for (size_t i = 0; i < size; ++i) { + result *= 31; + result += data[i]; + } + + return result; +} + +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 size, size_t *actual) +{ + 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 > size) { + ERROR(abstract->context, "Unexpected packet length (%u).", length); + 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 == NULL) { + // Verify the actual length. + if (length != size) { + ERROR (abstract->context, "Unexpected packet length (%u).", length); + return DC_STATUS_PROTOCOL; + } + } else { + // Return the actual length. + *actual = length; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_transfer(mclean_extreme_device_t *device, unsigned char cmd, const unsigned char data[], size_t size, unsigned char answer[], size_t asize, size_t *actual) +{ + dc_status_t status = DC_STATUS_SUCCESS; + + // Send the command + status = mclean_extreme_send(device, cmd, data, size); + if (status != DC_STATUS_SUCCESS) { + return status; + } + + // Receive the answer + if (asize) { + status = mclean_extreme_receive(device, cmd, answer, asize, actual); + if (status != DC_STATUS_SUCCESS) { + return status; + } + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_readdive (dc_device_t *abstract, dc_event_progress_t *progress, unsigned int idx, dc_buffer_t *buffer) +{ + dc_status_t status = DC_STATUS_SUCCESS; + mclean_extreme_device_t *device = (mclean_extreme_device_t *) abstract; + + // Erase the buffer. + dc_buffer_clear (buffer); + + // Encode the logbook ID. + unsigned char id[] = { + (idx ) & 0xFF, + (idx >> 8) & 0xFF, + }; + + // Update and emit a progress event. + unsigned int initial = 0; + if (progress) { + initial = progress->current; + device_event_emit (abstract, DC_EVENT_PROGRESS, progress); + } + + // Request the dive. + status = mclean_extreme_send (device, CMD_DIVE, id, sizeof(id)); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to send the dive command."); + return status; + } + + // Read the dive header. + unsigned char header[SZ_HEADER]; + status = mclean_extreme_receive (device, CMD_DIVE, header, sizeof(header), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to receive the dive header."); + return status; + } + + // Verify the format version. + unsigned int format = header[0x0000]; + if (format != 0) { + ERROR(abstract->context, "Unrecognised dive format."); + return DC_STATUS_DATAFORMAT; + } + + // Get the number of samples. + unsigned int nsamples = array_uint16_le (header + 0x005C); + + // Calculate the total size. + unsigned int size = sizeof(header) + nsamples * SZ_SAMPLE; + + // Update and emit a progress event. + if (progress) { + progress->current = initial + STEP(sizeof(header), size); + device_event_emit (abstract, DC_EVENT_PROGRESS, progress); + } + + // Allocate memory for the dive. + if (!dc_buffer_resize (buffer, size)) { + ERROR (abstract->context, "Insufficient buffer space available."); + return DC_STATUS_NOMEMORY; + } + + // Cache the pointer. + unsigned char *data = dc_buffer_get_data(buffer); + + // Append the header. + memcpy (data, header, sizeof(header)); + + unsigned int nbytes = sizeof(header); + while (nbytes < size) { + // Get the maximum packet size. + size_t len = size - nbytes; + + // Read the dive samples. + status = mclean_extreme_receive (device, CMD_DIVE, data + nbytes, len, &len); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to receive the dive samples."); + return status; + } + + nbytes += len; + + // Update and emit a progress event. + if (progress) { + progress->current = initial + STEP(nbytes, size); + device_event_emit (abstract, DC_EVENT_PROGRESS, progress); + } + } + + 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; + } + + *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_timesync(dc_device_t *abstract, const dc_datetime_t *datetime) +{ + mclean_extreme_device_t *device = (mclean_extreme_device_t *)abstract; + + if (datetime == NULL) { + ERROR(abstract->context, "Invalid parameter specified."); + return DC_STATUS_INVALIDARGS; + } + + // Get the UTC timestamp. + dc_ticks_t ticks = dc_datetime_mktime(datetime); + if (ticks == -1 || ticks < EPOCH || ticks - EPOCH > 0xFFFFFFFF) { + ERROR (abstract->context, "Invalid date/time value specified."); + return DC_STATUS_INVALIDARGS; + } + + // Adjust the epoch. + unsigned int timestamp = ticks - EPOCH; + + // Send the command. + const unsigned char cmd[] = { + (timestamp ) & 0xFF, + (timestamp >> 8) & 0xFF, + (timestamp >> 16) & 0xFF, + (timestamp >> 24) & 0xFF + }; + dc_status_t status = mclean_extreme_send(device, CMD_SET_TIME, cmd, sizeof(cmd)); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to send the set time command."); + return status; + } + + 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; + + // Enable progress notifications. + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Read the firmware version. + unsigned char firmware[4] = {0}; + status = mclean_extreme_transfer(device, CMD_FIRMWARE, NULL, 0, firmware, sizeof(firmware), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to read the firmware version."); + return status; + } + + // Read the serial number. + size_t serial_len = 0; + unsigned char serial[SZ_PACKET] = {0}; + status = mclean_extreme_transfer(device, CMD_SERIALNUMBER, NULL, 0, serial, sizeof(serial), &serial_len); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to read serial number."); + return status; + } + + // Emit a device info event. + dc_event_devinfo_t devinfo; + devinfo.model = 0; + devinfo.firmware = array_uint32_le (firmware); + devinfo.serial = hashcode (serial, serial_len); + device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); + + // Read the computer configuration. + unsigned char computer[SZ_COMPUTER]; + status = mclean_extreme_transfer(device, CMD_COMPUTER, NULL, 0, computer, sizeof(computer), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR(abstract->context, "Failed to read the computer configuration."); + return status; + } + + // Verify the format version. + unsigned int format = computer[0x0000]; + if (format != 0) { + ERROR(abstract->context, "Unsupported device format."); + return DC_STATUS_DATAFORMAT; + } + + // Get the number of dives. + unsigned int ndives = array_uint16_le(computer + 0x0019); + + // Update and emit a progress event. + progress.current = 1 * NSTEPS; + progress.maximum = (ndives + 1) * NSTEPS; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Allocate a memory buffer for a single dive. + dc_buffer_t *buffer = dc_buffer_new(0); + if (buffer == NULL) { + status = DC_STATUS_NOMEMORY; + goto error_exit; + } + + for (unsigned int i = 0; i < ndives; ++i) { + // Download in reverse order (newest first). + unsigned int idx = ndives - 1 - i; + + // Read the dive. + status = mclean_extreme_readdive (abstract, &progress, idx, buffer); + if (status != DC_STATUS_SUCCESS) { + goto error_buffer_free; + } + + // Cache the pointer. + unsigned char *data = dc_buffer_get_data(buffer); + unsigned int size = dc_buffer_get_size(buffer); + + if (memcmp(data, device->fingerprint, sizeof(device->fingerprint)) == 0) + break; + + if (callback && !callback (data, size, data, sizeof(device->fingerprint), userdata)) { + break; + } + } + +error_buffer_free: + dc_buffer_free (buffer); +error_exit: + return status; +} diff --git a/src/mclean_extreme.h b/src/mclean_extreme.h new file mode 100644 index 0000000..c388bd4 --- /dev/null +++ b/src/mclean_extreme.h @@ -0,0 +1,43 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Jef Driesen, David Carron + * + * 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..e696ee8 --- /dev/null +++ b/src/mclean_extreme_parser.c @@ -0,0 +1,316 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Jef Driesen, David Carron + * + * 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 "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 +#define SZ_COMPUTER (SZ_CFG + 0x6A) +#define SZ_HEADER (SZ_CFG + 0x31) +#define SZ_SAMPLE 0x0004 + +#define EPOCH 946684800 // 2000-01-01 00:00:00 UTC + +#define REC 0 +#define TEC 1 +#define CCR 2 +#define GAUGE 3 + +#define INVALID 0xFFFFFFFF + +#define NGASMIXES 8 + +typedef struct mclean_extreme_parser_t mclean_extreme_parser_t; + +struct mclean_extreme_parser_t { + dc_parser_t base; + + // Cached fields. + unsigned int cached; + unsigned int ngasmixes; + unsigned int gasmix[NGASMIXES]; +}; + +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; + } + + // Set the default values. + parser->cached = 0; + parser->ngasmixes = 0; + for (unsigned int i = 0; i < NGASMIXES; ++i) { + parser->gasmix[i] = INVALID; + } + + *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) +{ + mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract; + + // Reset the cache. + parser->cached = 0; + parser->ngasmixes = 0; + for (unsigned int i = 0; i < NGASMIXES; ++i) { + parser->gasmix[i] = INVALID; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +mclean_extreme_parser_get_datetime(dc_parser_t *abstract, dc_datetime_t *datetime) +{ + if (abstract->size < SZ_HEADER) { + ERROR(abstract->context, "Corrupt dive data"); + return DC_STATUS_DATAFORMAT; + } + + unsigned int timestamp = array_uint32_le(abstract->data + SZ_CFG + 0x0000); + dc_ticks_t ticks = (dc_ticks_t) timestamp + EPOCH; + + if (!dc_datetime_gmtime (datetime, ticks)) + return DC_STATUS_DATAFORMAT; + + datetime->timezone = DC_TIMEZONE_NONE; + + 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) +{ + mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract; + + if (abstract->size < SZ_HEADER) { + ERROR(abstract->context, "Corrupt dive data"); + return DC_STATUS_DATAFORMAT; + } + + if (!parser->cached) { + dc_status_t rc = mclean_extreme_parser_samples_foreach (abstract, NULL, NULL); + if (rc != DC_STATUS_SUCCESS) + return rc; + } + + dc_gasmix_t *gasmix = (dc_gasmix_t *)value; + dc_salinity_t *salinity = (dc_salinity_t *)value; + + const unsigned int psurf = array_uint16_le(abstract->data + 0x001E); + const unsigned int density_index = abstract->data[0x0023]; + double density = 0; + + switch (density_index) { + case 0: + density = 1.000; + break; + case 1: + density = 1.020; + break; + case 2: + density = 1.030; + break; + default: + ERROR(abstract->context, "Corrupt density index in dive data"); + return DC_STATUS_DATAFORMAT; + } + + if (value) { + switch (type) { + case DC_FIELD_DIVETIME: + *((unsigned int *)value) = array_uint32_le(abstract->data + SZ_CFG + 0x000C) - array_uint32_le(abstract->data + SZ_CFG + 0x0000); + break; + case DC_FIELD_MAXDEPTH: + *((double *)value) = 0.01 * (array_uint16_le(abstract->data + SZ_CFG + 0x0016) - psurf) / density; + break; + case DC_FIELD_AVGDEPTH: + *((double *)value) = 0.01 * (array_uint16_le(abstract->data + SZ_CFG + 0x0018) - psurf) / density; + break; + case DC_FIELD_SALINITY: + salinity->density = density * 1000.0; + salinity->type = density_index == 0 ? DC_WATER_FRESH : DC_WATER_SALT; + break; + case DC_FIELD_ATMOSPHERIC: + *((double *)value) = 0.001 * array_uint16_le(abstract->data + 0x001E); + break; + case DC_FIELD_TEMPERATURE_MINIMUM: + *((double *)value) = (double)abstract->data[SZ_CFG + 0x0010]; + break; + case DC_FIELD_TEMPERATURE_MAXIMUM: + *((double *)value) = (double)abstract->data[SZ_CFG + 0x0011]; + break; + case DC_FIELD_DIVEMODE: + switch (abstract->data[0x002C]) { + case REC: + case TEC: + *((dc_divemode_t *)value) = DC_DIVEMODE_OC; + break; + case CCR: + *((dc_divemode_t *)value) = DC_DIVEMODE_CCR; + break; + case GAUGE: + *((dc_divemode_t *)value) = DC_DIVEMODE_GAUGE; + break; + default: + ERROR(abstract->context, "Corrupt dive mode in dive data"); + return DC_STATUS_DATAFORMAT; + } + break; + case DC_FIELD_GASMIX_COUNT: + *((unsigned int *)value) = parser->ngasmixes; + break; + case DC_FIELD_GASMIX: + gasmix->helium = 0.01 * abstract->data[0x0001 + 1 + 2 * parser->gasmix[flags]]; + gasmix->oxygen = 0.01 * abstract->data[0x0001 + 0 + 2 * parser->gasmix[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) +{ + mclean_extreme_parser_t *parser = (mclean_extreme_parser_t *)abstract; + + if (abstract->size < SZ_HEADER) { + ERROR(abstract->context, "Corrupt dive data"); + return DC_STATUS_DATAFORMAT; + } + + const unsigned int nsamples = array_uint16_le(abstract->data + 0x005C); + + if (abstract->size != SZ_HEADER + nsamples * SZ_SAMPLE) { + ERROR(abstract->context, "Corrupt dive data"); + return DC_STATUS_DATAFORMAT; + } + + unsigned int ngasmixes = 0; + unsigned int gasmix[NGASMIXES] = {0}; + unsigned int gasmix_previous = INVALID; + + const unsigned int interval = 20; + unsigned int time = 0; + size_t offset = SZ_HEADER; + for (unsigned int i = 0; i < nsamples; ++i) { + dc_sample_value_t sample = { 0 }; + + const unsigned int depth = array_uint16_le(abstract->data + offset + 0); + const unsigned int temperature = abstract->data[offset + 2]; + const unsigned int flags = abstract->data[offset + 3]; + const unsigned int ccr = flags & 0x80; + const unsigned int gasmix_id = (flags & 0x1C) >> 2; + const unsigned int sp_index = (flags & 0x60) >> 5; + const unsigned int setpoint = abstract->data[0x0013 + sp_index]; + + time += interval; + sample.time = time; + if (callback) callback(DC_SAMPLE_TIME, sample, userdata); + + sample.depth = 0.1 * depth; + if (callback) callback(DC_SAMPLE_DEPTH, sample, userdata); + + sample.temperature = temperature; + if (callback) callback(DC_SAMPLE_TEMPERATURE, sample, userdata); + + if (gasmix_id != gasmix_previous) { + // Find the gasmix in the list. + unsigned int idx = 0; + while (idx < ngasmixes) { + if (gasmix_id == gasmix[idx]) + break; + idx++; + } + + // Add it to list if not found. + if (idx >= ngasmixes) { + if (idx >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_DATAFORMAT; + } + gasmix[idx] = gasmix_id; + ngasmixes = idx + 1; + } + + sample.gasmix = idx; + if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata); + gasmix_previous = gasmix_id; + } + + if (ccr) { + sample.setpoint = 0.01 * setpoint; + if (callback) callback(DC_SAMPLE_SETPOINT, sample, userdata); + } + + offset += SZ_SAMPLE; + } + + // Cache the data for later use. + for (unsigned int i = 0; i < ngasmixes; ++i) { + parser->gasmix[i] = gasmix[i]; + } + parser->ngasmixes = ngasmixes; + parser->cached = 1; + + return DC_STATUS_SUCCESS; +} diff --git a/src/parser.c b/src/parser.c index 9203490..977db14 100644 --- a/src/parser.c +++ b/src/parser.c @@ -57,6 +57,7 @@ #include "divesystem_idive.h" #include "cochran_commander.h" #include "tecdiving_divecomputereu.h" +#include "mclean_extreme.h" #include "context-private.h" #include "parser-private.h" @@ -172,6 +173,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_TECDIVING_DIVECOMPUTEREU: rc = tecdiving_divecomputereu_parser_create (&parser, context); break; + case DC_FAMILY_MCLEAN_EXTREME: + rc = mclean_extreme_parser_create (&parser, context); + break; default: return DC_STATUS_INVALIDARGS; }