diff --git a/contrib/android/Android.mk b/contrib/android/Android.mk index d2d8cd1..6c61f26 100644 --- a/contrib/android/Android.mk +++ b/contrib/android/Android.mk @@ -34,6 +34,8 @@ LOCAL_SRC_FILES := \ src/device.c \ src/diverite_nitekq.c \ src/diverite_nitekq_parser.c \ + src/divesoft_freedom.c \ + src/divesoft_freedom_parser.c \ src/divesystem_idive.c \ src/divesystem_idive_parser.c \ src/hdlc.c \ diff --git a/contrib/msvc/libdivecomputer.vcxproj b/contrib/msvc/libdivecomputer.vcxproj index 6497705..8b3ceab 100644 --- a/contrib/msvc/libdivecomputer.vcxproj +++ b/contrib/msvc/libdivecomputer.vcxproj @@ -202,6 +202,8 @@ + + @@ -331,6 +333,7 @@ + diff --git a/examples/common.c b/examples/common.c index 6c9a957..bccb8bd 100644 --- a/examples/common.c +++ b/examples/common.c @@ -98,6 +98,7 @@ static const backend_table_t g_backends[] = { {"screen", DC_FAMILY_SEAC_SCREEN, 0}, {"cosmiq", DC_FAMILY_DEEPBLU_COSMIQ, 0}, {"s1", DC_FAMILY_OCEANS_S1, 0}, + {"freedom", DC_FAMILY_DIVESOFT_FREEDOM, 19}, }; static const transport_table_t g_transports[] = { diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index c05bee1..6bf3556 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -118,6 +118,8 @@ typedef enum dc_family_t { DC_FAMILY_DEEPBLU_COSMIQ = (21 << 16), /* Oceans S1 */ DC_FAMILY_OCEANS_S1 = (22 << 16), + /* Divesoft Freedom */ + DC_FAMILY_DIVESOFT_FREEDOM = (23 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/src/Makefile.am b/src/Makefile.am index 1b37d9c..ef44da3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -80,6 +80,7 @@ libdivecomputer_la_SOURCES = \ deepblu_cosmiq.h deepblu_cosmiq.c deepblu_cosmiq_parser.c \ oceans_s1_common.h oceans_s1_common.c \ oceans_s1.h oceans_s1.c oceans_s1_parser.c \ + divesoft_freedom.h divesoft_freedom.c divesoft_freedom_parser.c \ hdlc.h hdlc.c \ socket.h socket.c \ irda.c \ diff --git a/src/descriptor.c b/src/descriptor.c index e41b016..9858da6 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -65,6 +65,7 @@ static int dc_filter_atomic (dc_transport_t transport, const void *userdata, voi static int dc_filter_deepsix (dc_transport_t transport, const void *userdata, void *params); static int dc_filter_deepblu (dc_transport_t transport, const void *userdata, void *params); static int dc_filter_oceans (dc_transport_t transport, const void *userdata, void *params); +static int dc_filter_divesoft (dc_transport_t transport, const void *userdata, void *params); static dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -451,6 +452,9 @@ static const dc_descriptor_t g_descriptors[] = { {"Deepblu", "Cosmiq+", DC_FAMILY_DEEPBLU_COSMIQ, 0, DC_TRANSPORT_BLE, dc_filter_deepblu}, /* Oceans S1 */ {"Oceans", "S1", DC_FAMILY_OCEANS_S1, 0, DC_TRANSPORT_BLE, dc_filter_oceans}, + /* Divesoft Freedom */ + {"Divesoft", "Freedom", DC_FAMILY_DIVESOFT_FREEDOM, 19, DC_TRANSPORT_BLE, dc_filter_divesoft}, + {"Divesoft", "Liberty", DC_FAMILY_DIVESOFT_FREEDOM, 10, DC_TRANSPORT_BLE, dc_filter_divesoft}, }; static int @@ -798,6 +802,20 @@ static int dc_filter_oceans (dc_transport_t transport, const void *userdata, voi return 1; } +static int dc_filter_divesoft (dc_transport_t transport, const void *userdata, void *params) +{ + static const char * const bluetooth[] = { + "Freedom", + "Liberty", + }; + + if (transport == DC_TRANSPORT_BLE) { + return DC_FILTER_INTERNAL (userdata, bluetooth, 0, dc_match_prefix); + } + + return 1; +} + dc_status_t dc_descriptor_iterator (dc_iterator_t **out) { diff --git a/src/device.c b/src/device.c index e930ff1..6979f22 100644 --- a/src/device.c +++ b/src/device.c @@ -64,6 +64,7 @@ #include "seac_screen.h" #include "deepblu_cosmiq.h" #include "oceans_s1.h" +#include "divesoft_freedom.h" #include "device-private.h" #include "context-private.h" @@ -239,6 +240,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_OCEANS_S1: rc = oceans_s1_device_open (&device, context, iostream); break; + case DC_FAMILY_DIVESOFT_FREEDOM: + rc = divesoft_freedom_device_open (&device, context, iostream); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/divesoft_freedom.c b/src/divesoft_freedom.c new file mode 100644 index 0000000..ddf9162 --- /dev/null +++ b/src/divesoft_freedom.c @@ -0,0 +1,595 @@ +/* + * libdivecomputer + * + * Copyright (C) 2023 Jan Matoušek, 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 "divesoft_freedom.h" +#include "context-private.h" +#include "device-private.h" +#include "platform.h" +#include "checksum.h" +#include "array.h" +#include "hdlc.h" + +#define MAXDATA 256 + +#define HEADER_SIGNATURE_V1 0x45766944 // "DivE" +#define HEADER_SIGNATURE_V2 0x45566944 // "DiVE" + +#define HEADER_SIZE_V1 32 +#define HEADER_SIZE_V2 64 + +#define RECORD_SIZE 16 +#define FINGERPRINT_SIZE 20 + +#define INVALID 0xFFFFFFFF +#define COMPRESSION 1 +#define DIRECTION 1 +#define NRECORDS 100 + +#define DEVICE_CCR_CU 1 // Liberty HW rev. 1.X +#define DEVICE_FREEDOM 2 // Freedom HW rev. 2.X +#define DEVICE_FREEDOM3 5 // Freedom HW rev. 3.X +#define DEVICE_CCR_CU15 10 // Liberty HW rev. 2.X, Bluetooth enabled +#define DEVICE_FREEDOM4 19 // Freedom HW rev. 4.X, Bluetooth enabled + +typedef enum message_t { + MSG_ECHO = 0, + MSG_RESULT = 1, + MSG_CONNECT = 2, + MSG_CONNECTED = 3, + MSG_VERSION = 4, + MSG_VERSION_RSP = 5, + MSG_DIVE_DATA = 64, + MSG_DIVE_DATA_RSP = 65, + MSG_DIVE_LIST = 66, + MSG_DIVE_LIST_V1 = 67, + MSG_DIVE_LIST_V2 = 71, +} message_t; + +typedef struct divesoft_freedom_device_t { + dc_device_t base; + dc_iostream_t *iostream; + unsigned char fingerprint[FINGERPRINT_SIZE]; + unsigned int seqnum; +} divesoft_freedom_device_t; + +static dc_status_t divesoft_freedom_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size); +static dc_status_t divesoft_freedom_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); +static dc_status_t divesoft_freedom_device_close (dc_device_t *device); + +static const dc_device_vtable_t divesoft_freedom_device_vtable = { + sizeof(divesoft_freedom_device_t), + DC_FAMILY_DIVESOFT_FREEDOM, + divesoft_freedom_device_set_fingerprint, /* set_fingerprint */ + NULL, /* read */ + NULL, /* write */ + NULL, /* dump */ + divesoft_freedom_device_foreach, /* foreach */ + NULL, /* timesync */ + divesoft_freedom_device_close, /* close */ +}; + +static dc_status_t +divesoft_freedom_send (divesoft_freedom_device_t *device, message_t message, const unsigned char data[], size_t size) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t *abstract = (dc_device_t *) device; + + size_t nbytes = 0, count = 0; + while (1) { + size_t len = size - nbytes; + if (len > MAXDATA) + len = MAXDATA; + + unsigned int islast = nbytes + len == size; + + unsigned char packet[6 + MAXDATA + 2] = {0}; + packet[0] = ((count & 0x0F) << 4) | (device->seqnum & 0x0F); + packet[1] = 0x80 | (islast << 6); + array_uint16_le_set (packet + 2, message); + array_uint16_le_set (packet + 4, len); + if (len) { + memcpy (packet + 6, data + nbytes, len); + } + unsigned short crc = checksum_crc16r_ccitt (packet, len + 6, 0xFFFF, 0xFFFF); + array_uint16_le_set (packet + 6 + len, crc); + + HEXDUMP (abstract->context, DC_LOGLEVEL_DEBUG, "cmd", packet, 6 + len + 2); + + status = dc_iostream_write (device->iostream, packet, 6 + len + 2, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to send the packet."); + return status; + } + + nbytes += len; + count++; + + if (islast) + break; + } + + return status; +} + +static dc_status_t +divesoft_freedom_recv (divesoft_freedom_device_t *device, dc_event_progress_t *progress, message_t *message, dc_buffer_t *buffer) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t *abstract = (dc_device_t *) device; + unsigned int msg = INVALID; + + unsigned int count = 0; + while (1) { + size_t len = 0; + unsigned char packet[6 + MAXDATA + 2] = {0}; + status = dc_iostream_read (device->iostream, packet, sizeof(packet), &len); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to receive the packet."); + return status; + } + + HEXDUMP (abstract->context, DC_LOGLEVEL_DEBUG, "rcv", packet, len); + + if (len < 8) { + ERROR (abstract->context, "Unexpected packet length (" DC_PRINTF_SIZE ").", len); + return DC_STATUS_PROTOCOL; + } + + unsigned int seqnum = packet[0]; + unsigned int flags = packet[1]; + unsigned int type = array_uint16_le (packet + 2); + unsigned int length = array_uint16_le (packet + 4); + + unsigned int expected = ((count & 0x0F) << 4) | (device->seqnum & 0x0F); + if (seqnum != expected) { + ERROR (abstract->context, "Unexpected packet sequence number (%u %u).", seqnum, expected); + return DC_STATUS_PROTOCOL; + } + + if ((flags & ~0x40) != 0) { + ERROR (abstract->context, "Unexpected packet flags (%u).", flags); + return DC_STATUS_PROTOCOL; + } + + if (length != len - 8) { + ERROR (abstract->context, "Unexpected packet length (%u " DC_PRINTF_SIZE ").", length, len - 8); + return DC_STATUS_PROTOCOL; + } + + if (msg == INVALID) { + msg = type; + } else if (msg != type) { + ERROR (abstract->context, "Unexpected packet type (%u).", msg); + return DC_STATUS_PROTOCOL; + } + + unsigned short crc = array_uint16_le (packet + len - 2); + unsigned short ccrc = checksum_crc16r_ccitt (packet, len - 2, 0xFFFF, 0xFFFF); + if (crc != ccrc) { + ERROR (abstract->context, "Unexpected packet checksum (%04x %04x).", crc, ccrc); + return DC_STATUS_PROTOCOL; + } + + // Update and emit a progress event. + if (progress) { + progress->current += len - 8; + // Limit the progress to the maximum size. This could happen if the + // dive computer sends more data than requested for some reason. + if (progress->current > progress->maximum) { + WARNING (abstract->context, "Progress exceeds the maximum size."); + progress->current = progress->maximum; + } + device_event_emit (abstract, DC_EVENT_PROGRESS, progress); + } + + if (!dc_buffer_append (buffer, packet + 6, len - 8)) { + ERROR (abstract->context, "Insufficient buffer space available."); + return DC_STATUS_NOMEMORY; + } + + count++; + + if (flags & 0x40) + break; + } + + if (message) + *message = msg; + + return status; +} + +static dc_status_t +divesoft_freedom_transfer (divesoft_freedom_device_t *device, dc_event_progress_t *progress, message_t cmd, const unsigned char data[], size_t size, message_t *msg, dc_buffer_t *buffer) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t *abstract = (dc_device_t *) device; + + if (device_is_cancelled (abstract)) + return DC_STATUS_CANCELLED; + + device->seqnum++; + + status = divesoft_freedom_send (device, cmd, data, size); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to send the command."); + return status; + } + + status = divesoft_freedom_recv (device, progress, msg, buffer); + if(status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to receive response."); + return status; + } + + return status; +} + +static dc_status_t +divesoft_freedom_download (divesoft_freedom_device_t *device, message_t cmd, const unsigned char cdata[], size_t csize, unsigned char rdata[], size_t rsize) +{ + dc_status_t status = DC_STATUS_SUCCESS; + dc_device_t *abstract = (dc_device_t *) device; + + dc_buffer_t *buffer = dc_buffer_new (rsize); + if (buffer == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + status = DC_STATUS_NOMEMORY; + goto error_exit; + } + + message_t msg = MSG_ECHO; + status = divesoft_freedom_transfer (device, NULL, cmd, cdata, csize, &msg, buffer); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to transfer the packet."); + goto error_free; + } + + if (msg != cmd + 1) { + ERROR (abstract->context, "Unexpected response message (%u).", msg); + status = DC_STATUS_PROTOCOL; + goto error_free; + } + + size_t length = dc_buffer_get_size (buffer); + if (length != rsize) { + ERROR (abstract->context, "Unexpected response length (" DC_PRINTF_SIZE " " DC_PRINTF_SIZE ").", length, rsize); + status = DC_STATUS_PROTOCOL; + goto error_free; + } + + if (rsize) { + memcpy (rdata, dc_buffer_get_data (buffer), rsize); + } + +error_free: + dc_buffer_free (buffer); +error_exit: + return status; +} + +dc_status_t +divesoft_freedom_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream) +{ + dc_status_t status = DC_STATUS_SUCCESS; + divesoft_freedom_device_t *device = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + device = (divesoft_freedom_device_t *) dc_device_allocate (context, &divesoft_freedom_device_vtable); + if (device == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + device->iostream = NULL; + memset(device->fingerprint, 0, sizeof(device->fingerprint)); + device->seqnum = 0; + + // Setup the HDLC communication. + status = dc_hdlc_open (&device->iostream, context, iostream, 244, 244); + if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Failed to create the HDLC stream."); + goto error_free; + } + + // 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_hdlc; + } + + // Set the timeout for receiving data (3000ms). + status = dc_iostream_set_timeout (device->iostream, 3000); + if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Failed to set the timeout."); + goto error_free_hdlc; + } + + // Initiate the connection with the dive computer. + const char client[] = "libdivecomputer"; + unsigned char cmd_connect[2 + sizeof(client) - 1] = {0}; + array_uint16_le_set (cmd_connect, COMPRESSION); + memcpy (cmd_connect + 2, client, sizeof(client) - 1); + unsigned char rsp_connect[36] = {0}; + status = divesoft_freedom_download (device, MSG_CONNECT, cmd_connect, sizeof(cmd_connect), rsp_connect, sizeof(rsp_connect)); + if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Failed to connect to the device."); + goto error_free_hdlc; + } + + DEBUG (context, "Connection: compression=%u, protocol=%u.%u, serial=%.16s", + array_uint16_le (rsp_connect), + rsp_connect[2], rsp_connect[3], + rsp_connect + 4); + + *out = (dc_device_t *) device; + + return DC_STATUS_SUCCESS; + +error_free_hdlc: + dc_iostream_close (device->iostream); +error_free: + dc_device_deallocate ((dc_device_t *) device); + return status; +} + +static dc_status_t +divesoft_freedom_device_close (dc_device_t *abstract) +{ + divesoft_freedom_device_t *device = (divesoft_freedom_device_t *) abstract; + + return dc_iostream_close (device->iostream); +} + +static dc_status_t +divesoft_freedom_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size) +{ + divesoft_freedom_device_t *device = (divesoft_freedom_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 +divesoft_freedom_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + divesoft_freedom_device_t *device = (divesoft_freedom_device_t *) abstract; + + // Enable progress notifications. + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + // Read the device information. + unsigned char rsp_version[26] = {0}; + status = divesoft_freedom_download (device, MSG_VERSION, NULL, 0, rsp_version, sizeof(rsp_version)); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the device information."); + goto error_exit; + } + + DEBUG (abstract->context, "Device: model=%u, hw=%u.%u, sw=%u.%u.%u.%u serial=%.16s", + rsp_version[0], + rsp_version[1], rsp_version[2], + rsp_version[3], rsp_version[4], rsp_version[5], + array_uint32_le (rsp_version + 6), + rsp_version + 10); + + // Emit a device info event. + dc_event_devinfo_t devinfo; + devinfo.model = rsp_version[0]; + devinfo.firmware = array_uint24_be (rsp_version + 3); + devinfo.serial = array_convert_str2num (rsp_version + 10 + 5, 11); + device_event_emit(abstract, DC_EVENT_DEVINFO, &devinfo); + + // Allocate memory for the dive list. + dc_buffer_t *divelist = dc_buffer_new (0); + if (divelist == NULL) { + status = DC_STATUS_NOMEMORY; + goto error_exit; + } + + // Allocate memory for the download buffer. + dc_buffer_t *buffer = dc_buffer_new (NRECORDS * (4 + FINGERPRINT_SIZE + HEADER_SIZE_V2)); + if (buffer == NULL) { + status = DC_STATUS_NOMEMORY; + goto error_free_divelist; + } + + // Record version and size. + unsigned int version = 0; + unsigned int headersize = 0; + unsigned int recordsize = 0; + + // Download the dive list. + unsigned int ndives = 0; + unsigned int total = 0; + unsigned int maxsize = 0; + unsigned int current = INVALID; + while (1) { + // Clear the buffer. + dc_buffer_clear (buffer); + + // Prepare the command. + unsigned char cmd_list[6] = {0}; + array_uint32_le_set (cmd_list, current); + cmd_list[4] = DIRECTION; + cmd_list[5] = NRECORDS; + + // Download the dive list records. + message_t msg_list = MSG_ECHO; + status = divesoft_freedom_transfer (device, &progress, MSG_DIVE_LIST, cmd_list, sizeof(cmd_list), &msg_list, buffer); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to download the dive list."); + goto error_free_buffer; + } + + // Check the response message type. + if (msg_list != MSG_DIVE_LIST_V1 && msg_list != MSG_DIVE_LIST_V2) { + ERROR (abstract->context, "Unexpected response message (%u).", msg_list); + status = DC_STATUS_PROTOCOL; + goto error_free_buffer; + } + + // Store/check the version. + if (version == 0) { + version = msg_list; + headersize = version == MSG_DIVE_LIST_V1 ? + HEADER_SIZE_V1 : HEADER_SIZE_V2; + recordsize = 4 + FINGERPRINT_SIZE + headersize; + } else if (version != msg_list) { + ERROR (abstract->context, "Unexpected response message (%u).", msg_list); + status = DC_STATUS_PROTOCOL; + goto error_free_buffer; + } + + const unsigned char *data = dc_buffer_get_data (buffer); + size_t size = dc_buffer_get_size (buffer); + + // Process the records. + size_t offset = 0, count = 0; + while (offset + recordsize <= size) { + // Get the record data. + unsigned int handle = array_uint32_le (data + offset); + const unsigned char *fingerprint = data + offset + 4; + const unsigned char *header = data + offset + 4 + FINGERPRINT_SIZE; + + // Check the fingerprint data. + if (memcmp (device->fingerprint, fingerprint, sizeof(device->fingerprint)) == 0) { + break; + } + + // Get the length of the dive. + unsigned int nrecords = version == MSG_DIVE_LIST_V1 ? + array_uint32_le (header + 16) & 0x3FFFF : + array_uint32_le (header + 20); + unsigned int length = headersize + nrecords * RECORD_SIZE; + + // Calculate the total and maximum size. + if (length > maxsize) + maxsize = length; + total += length; + + // Set the handle for the next request. + current = handle; + + offset += recordsize; + count++; + ndives++; + } + + // Append the records to the dive list buffer. + if (!dc_buffer_append (divelist, data, count * recordsize)) { + ERROR (abstract->context, "Insufficient buffer space available."); + status = DC_STATUS_NOMEMORY; + goto error_free_buffer; + } + + // Stop downloading if there are no more records. + if (count < NRECORDS) + break; + } + + // Update and emit a progress event. + progress.maximum = progress.current + total; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + // Reserve memory for the largest dive. + dc_buffer_reserve (buffer, maxsize); + + const unsigned char *data = dc_buffer_get_data (divelist); + size_t size = dc_buffer_get_size (divelist); + + size_t offset = 0; + while (offset + recordsize <= size) { + // Get the record data. + unsigned int handle = array_uint32_le (data + offset); + const unsigned char *fingerprint = data + offset + 4; + const unsigned char *header = data + offset + 4 + FINGERPRINT_SIZE; + + // Get the length of the dive. + unsigned int nrecords = version == MSG_DIVE_LIST_V1 ? + array_uint32_le (header + 16) & 0x3FFFF : + array_uint32_le (header + 20); + unsigned int length = headersize + nrecords * RECORD_SIZE; + + // Clear the buffer. + dc_buffer_clear (buffer); + + // Prepare the command. + unsigned char cmd_dive[12] = {0}; + array_uint32_le_set (cmd_dive + 0, handle); + array_uint32_le_set (cmd_dive + 4, 0); + array_uint32_le_set (cmd_dive + 8, length); + + // Download the dive. + message_t msg_dive = MSG_ECHO; + status = divesoft_freedom_transfer (device, &progress, MSG_DIVE_DATA, cmd_dive, sizeof(cmd_dive), &msg_dive, buffer); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to download the dive."); + goto error_free_buffer; + } + + // Check the response message type. + if (msg_dive != MSG_DIVE_DATA_RSP) { + ERROR (abstract->context, "Unexpected response message (%u).", msg_dive); + status = DC_STATUS_PROTOCOL; + goto error_free_buffer; + } + + // Verify both dive headers are identical. + if (dc_buffer_get_size (buffer) < headersize || + memcmp (header, dc_buffer_get_data (buffer), headersize) != 0) { + ERROR (abstract->context, "Unexpected profile header."); + status = DC_STATUS_PROTOCOL; + goto error_free_buffer; + } + + if (callback && !callback (dc_buffer_get_data(buffer), dc_buffer_get_size(buffer), fingerprint, sizeof (device->fingerprint), userdata)) { + break; + } + + offset += recordsize; + } + +error_free_buffer: + dc_buffer_free (buffer); +error_free_divelist: + dc_buffer_free (divelist); +error_exit: + return status; +} diff --git a/src/divesoft_freedom.h b/src/divesoft_freedom.h new file mode 100644 index 0000000..db04edb --- /dev/null +++ b/src/divesoft_freedom.h @@ -0,0 +1,43 @@ +/* + * libdivecomputer + * + * Copyright (C) 2023 Jan Matoušek, 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 DIVESOFT_FREEDOM_H +#define DIVESOFT_FREEDOM_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t +divesoft_freedom_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream); + +dc_status_t +divesoft_freedom_parser_create (dc_parser_t **parser, dc_context_t *context); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* DIVESOFT_FREEDOM_H */ diff --git a/src/divesoft_freedom_parser.c b/src/divesoft_freedom_parser.c new file mode 100644 index 0000000..b595e63 --- /dev/null +++ b/src/divesoft_freedom_parser.c @@ -0,0 +1,1070 @@ +/* + * libdivecomputer + * + * Copyright (C) 2023 Jan Matoušek, 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 "divesoft_freedom.h" +#include "context-private.h" +#include "parser-private.h" +#include "checksum.h" +#include "array.h" + +#define UNDEFINED 0xFFFFFFFF + +#define EPOCH 946684800 // 2000-01-01 00:00:00 UTC + +#define OC 0 +#define OXYGEN 1 +#define DILUENT 2 + +#define NSENSORS 4 +#define NGASMIXES 12 +#define NTANKS 12 + +#define HEADER_SIGNATURE_V1 0x45766944 // "DivE" +#define HEADER_SIGNATURE_V2 0x45566944 // "DiVE" + +#define HEADER_SIZE_V1 32 +#define HEADER_SIZE_V2 64 + +#define RECORD_SIZE 16 + +#define SEAWATER 1028 +#define FRESHWATER 1000 + +typedef enum logrecord_t { + LREC_POINT = 0, + LREC_MANIPULATION = 1, + LREC_AUTO = 2, + LREC_DIVER_ERROR = 3, + LREC_INTERNAL_ERROR = 4, + LREC_ACTIVITY = 5, + LREC_CONFIGURATION = 6, + LREC_MEASURE = 7, + LREC_STATE = 8, + LREC_INFO = 9, +} logrecord_t; + +typedef enum point_id_t { + POINT_1 = 0, + POINT_2 = 1, + POINT_1_OLD = 0x3FF, +} point_id_t; + +typedef enum configuration_id_t { + CFG_ID_TEST_CCR_FULL = 0, + CFG_ID_TEST_CCR_PARTIAL = 1, + CFG_ID_OXYGEN_CALIBRATION = 2, + CFG_ID_SERIAL = 3, + CFG_ID_DECO = 4, + CFG_ID_VERSION = 5, + CFG_ID_ASCENT = 6, + CFG_ID_AI = 7, + CFG_ID_CCR = 8, + CFG_ID_DILUENTS = 9, +} configuration_id_t; + +typedef enum measure_id_t { + MEASURE_ID_OXYGEN = 0, + MEASURE_ID_BATTERY = 1, + MEASURE_ID_HELIUM = 2, + MEASURE_ID_OXYGEN_MV = 3, + MEASURE_ID_GPS = 4, + MEASURE_ID_PRESSURE = 5, + MEASURE_ID_AI_SAC = 6, + MEASURE_ID_AI_PRESSURE = 7, + MEASURE_ID_BRIGHTNESS = 8, + MEASURE_ID_AI_STAT = 9, +} measure_id_t; + +typedef enum state_id_t { + STATE_ID_DECO_N2LOW = 0, + STATE_ID_DECO_N2HIGH = 1, + STATE_ID_DECO_HELOW = 2, + STATE_ID_DECO_HEHIGH = 3, + STATE_ID_PLAN_STEPS = 4, +} state_id_t; + +typedef enum event_t { + EVENT_DUMMY = 0, + EVENT_SETPOINT_MANUAL = 1, + EVENT_SETPOINT_AUTO = 2, + EVENT_OC = 3, + EVENT_CCR = 4, + EVENT_MIX_CHANGED = 5, + EVENT_START = 6, + EVENT_TOO_FAST = 7, + EVENT_ABOVE_CEILING = 8, + EVENT_TOXIC = 9, + EVENT_HYPOX = 10, + EVENT_CRITICAL = 11, + EVENT_SENSOR_DISABLED = 12, + EVENT_SENSOR_ENABLED = 13, + EVENT_O2_BACKUP = 14, + EVENT_PEER_DOWN = 15, + EVENT_HS_DOWN = 16, + EVENT_INCONSISTENT = 17, + EVENT_KEYDOWN = 18, + EVENT_SCR = 19, + EVENT_ABOVE_STOP = 20, + EVENT_SAFETY_MISS = 21, + EVENT_FATAL = 22, + EVENT_DILUENT = 23, + EVENT_CHANGE_MODE = 24, + EVENT_SOLENOID = 25, + EVENT_BOOKMARK = 26, + EVENT_GF_SWITCH = 27, + EVENT_PEER_UP = 28, + EVENT_HS_UP = 29, + EVENT_CNS = 30, + EVENT_BATTERY_LOW = 31, + EVENT_PPO2_LOST = 32, + EVENT_SENSOR_VALUE_BAD = 33, + EVENT_SAFETY_STOP_END = 34, + EVENT_DECO_STOP_END = 35, + EVENT_DEEP_STOP_END = 36, + EVENT_NODECO_END = 37, + EVENT_DEPTH_REACHED = 38, + EVENT_TIME_ELAPSED = 39, + EVENT_STACK_USAGE = 40, + EVENT_GAS_SWITCH_INFO = 41, + EVENT_PRESSURE_SENS_WARN = 42, + EVENT_PRESSURE_SENS_FAIL = 43, + EVENT_CHECK_O2_SENSORS = 44, + EVENT_SWITCH_TO_COMP_SCR = 45, + EVENT_GAS_LOST = 46, + EVENT_AIRBREAK = 47, + EVENT_AIRBREAK_END = 48, + EVENT_AIRBREAK_MISSED = 49, + EVENT_BORMT_EXPIRATION = 50, + EVENT_BORMT_EXPIRED = 51, + EVENT_SENSOR_EXCLUDED = 52, + EVENT_PREBR_SKIPPED = 53, + EVENT_BOCCR_BORMT_EXPIRED = 54, + EVENT_WAYPOINT = 55, + EVENT_TURNAROUND = 56, + EVENT_SOLENOID_FAILURE = 57, + EVENT_SM_CYL_PRESS_DIFF = 58, + EVENT_BAILOUT_MOD_EXCEEDED = 59, +} event_t; + +typedef enum divemode_t { + STMODE_UNKNOWN = 0, + STMODE_OC = 1, + STMODE_CCR = 2, + STMODE_MCCR = 3, + STMODE_FREE = 4, + STMODE_GAUGE = 5, + STMODE_ASCR = 6, + STMODE_PSCR = 7, + STMODE_BOCCR = 8, +} divemode_t; + +typedef enum setpoint_change_t { + SP_MANUAL = 0, + SP_AUTO_START = 1, + SP_AUTO_HYPOX = 2, + SP_AUTO_TIMEOUT = 3, + SP_AUTO_ASCENT = 4, + SP_AUTO_STALL = 5, + SP_AUTO_SPLOW = 6, + SP_AUTO_DEPTH_DESC = 7, + SP_AUTO_DEPTH_ASC = 8, +} setpoint_change_t; + +typedef enum sensor_state_t { + SENSTAT_NORMAL = 0, + SENSTAT_OVERRANGE = 1, + SENSTAT_DISABLED = 2, + SENSTAT_EXCLUDED = 3, + SENSTAT_UNCALIBRATED = 4, + SENSTAT_ERROR = 5, + SENSTAT_OFFLINE = 6, + SENSTAT_INHIBITED = 7, + SENSTAT_NOT_EXIST = 8, +} sensor_state_t; + +typedef enum battery_state_t { + BATSTATE_NO_BATTERY = 0, + BATSTATE_UNKNOWN = 1, + BATSTATE_DISCHARGING = 2, + BATSTATE_CHARGING = 3, + BATSTATE_FULL = 4, +} battery_state_t; + +typedef struct divesoft_freedom_gasmix_t { + unsigned int oxygen; + unsigned int helium; + unsigned int type; + unsigned int id; +} divesoft_freedom_gasmix_t; + +typedef struct divesoft_freedom_tank_t { + unsigned int volume; + unsigned int workpressure; + unsigned int beginpressure; + unsigned int endpressure; + unsigned int transmitter; + unsigned int active; +} divesoft_freedom_tank_t; + +typedef struct divesoft_freedom_parser_t { + dc_parser_t base; + // Cached fields. + unsigned int cached; + unsigned int version; + unsigned int headersize; + unsigned int divetime; + unsigned int divemode; + int temperature_min; + unsigned int maxdepth; + unsigned int atmospheric; + unsigned int avgdepth; + unsigned int ngasmixes; + divesoft_freedom_gasmix_t gasmix[NGASMIXES]; + unsigned int diluent; + unsigned int ntanks; + divesoft_freedom_tank_t tank[NTANKS]; + unsigned int vpm; + unsigned int gf_lo; + unsigned int gf_hi; + unsigned int seawater; + unsigned int calibration[NSENSORS]; + unsigned int calibrated; +} divesoft_freedom_parser_t; + +static dc_status_t divesoft_freedom_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size); +static dc_status_t divesoft_freedom_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime); +static dc_status_t divesoft_freedom_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value); +static dc_status_t divesoft_freedom_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata); + +static const dc_parser_vtable_t divesoft_freedom_parser_vtable = { + sizeof(divesoft_freedom_parser_t), + DC_FAMILY_DIVESOFT_FREEDOM, + divesoft_freedom_parser_set_data, /* set_data */ + NULL, /* set_clock */ + NULL, /* set_atmospheric */ + NULL, /* set_density */ + divesoft_freedom_parser_get_datetime, /* datetime */ + divesoft_freedom_parser_get_field, /* fields */ + divesoft_freedom_parser_samples_foreach, /* samples_foreach */ + NULL /* destroy */ +}; + +static unsigned int +divesoft_freedom_find_gasmix (divesoft_freedom_gasmix_t gasmix[], unsigned int count, unsigned int oxygen, unsigned int helium, unsigned int type) +{ + unsigned int i = 0; + while (i < count) { + if (oxygen == gasmix[i].oxygen && + helium == gasmix[i].helium && + type == gasmix[i].type) + break; + i++; + } + + return i; +} + +static unsigned int +divesoft_freedom_find_tank (divesoft_freedom_tank_t tank[], unsigned int count, unsigned int transmitter) +{ + unsigned int i = 0; + while (i < count) { + if (transmitter == tank[i].transmitter) + break; + i++; + } + + return i; +} + +static unsigned int +divesoft_freedom_is_ccr (unsigned int divemode) +{ + return divemode == STMODE_CCR || divemode == STMODE_MCCR || + divemode == STMODE_ASCR || divemode == STMODE_PSCR || + divemode == STMODE_BOCCR; +} + +static dc_status_t +divesoft_freedom_cache (divesoft_freedom_parser_t *parser) +{ + dc_parser_t *abstract = (dc_parser_t *) parser; + const unsigned char *data = abstract->data; + unsigned int size = abstract->size; + + if (parser->cached) { + return DC_STATUS_SUCCESS; + } + + unsigned int headersize = 4; + if (size < headersize) { + ERROR (abstract->context, "Unexpected header size (%u).", size); + return DC_STATUS_DATAFORMAT; + } + + unsigned int version = array_uint32_le (data); + if (version == HEADER_SIGNATURE_V1) { + headersize = HEADER_SIZE_V1; + } else if (version == HEADER_SIGNATURE_V2) { + headersize = HEADER_SIZE_V2; + } else { + ERROR (abstract->context, "Unexpected header version (%08x).", version); + return DC_STATUS_DATAFORMAT; + } + + if (size < headersize) { + ERROR (abstract->context, "Unexpected header size (%u).", size); + return DC_STATUS_DATAFORMAT; + } + + unsigned short crc = array_uint16_le (data + 4); + unsigned short ccrc = checksum_crc16r_ansi (data + 6, headersize - 6, 0xFFFF, 0x0000); + if (crc != ccrc) { + ERROR (abstract->context, "Invalid header checksum (%04x %04x).", crc, ccrc); + return DC_STATUS_DATAFORMAT; + } + + // Parse the dive header. + unsigned int divetime = 0; + unsigned int divemode = 0; + unsigned int temperature_min = 0; + unsigned int maxdepth = 0; + unsigned int atmospheric = 0; + unsigned int avgdepth = 0; + unsigned int diluent_o2 = 0, diluent_he = 0; + if (version == HEADER_SIGNATURE_V1) { + unsigned int misc1 = array_uint32_le (data + 12); + unsigned int misc2 = array_uint32_le (data + 16); + divetime = misc1 & 0x1FFFF; + divemode = (misc1 & 0x38000000) >> 27; + temperature_min = (signed int) signextend ((misc2 & 0xFFC0000) >> 18, 10); + maxdepth = array_uint16_le (data + 20); + atmospheric = array_uint16_le (data + 24); + avgdepth = 0; + diluent_o2 = data[26]; + diluent_he = data[27]; + } else { + divetime = array_uint32_le (data + 12); + divemode = data[18]; + temperature_min = (signed short) array_uint16_le (data + 24); + maxdepth = array_uint16_le (data + 28); + atmospheric = array_uint16_le (data + 32); + avgdepth = array_uint16_le (data + 38); + diluent_o2 = 0; + diluent_he = 0; + + DEBUG (abstract->context, "Device: serial=%.4s-%.8s", + data + 52, data + 56); + } + + divesoft_freedom_gasmix_t gasmix_ai[NGASMIXES] = {0}, + gasmix_diluent[NGASMIXES] = {0}, + gasmix_event[NGASMIXES] = {0}; + unsigned int ngasmix_ai = 0, + ngasmix_diluent = 0, + ngasmix_event = 0; + divesoft_freedom_tank_t tank[NTANKS] = {0}; + unsigned int ntanks = 0; + + unsigned int vpm = 0, gf_lo = 0, gf_hi = 0; + unsigned int seawater = 0; + unsigned int calibration[NSENSORS] = {0}; + unsigned int calibrated = 0; + + unsigned int gasmixid_previous = UNDEFINED; + + + // Parse the dive profile. + unsigned int offset = headersize; + while (offset + RECORD_SIZE <= size) { + if (array_isequal(data + offset, RECORD_SIZE, 0xFF)) { + WARNING (abstract->context, "Skipping empty sample."); + offset += RECORD_SIZE; + continue; + } + + unsigned int flags = array_uint32_le (data + offset); + unsigned int type = (flags & 0x0000000F) >> 0; + unsigned int id = (flags & 0x7FE00000) >> 21; + + if (type == LREC_CONFIGURATION) { + // Configuration record. + if (id == CFG_ID_DECO) { + unsigned int misc = array_uint16_le (data + offset + 4); + gf_lo = data[offset + 8]; + gf_hi = data[offset + 9]; + seawater = misc & 0x02; + vpm = misc & 0x20; + } else if (id == CFG_ID_VERSION) { + DEBUG (abstract->context, "Device: model=%u, hw=%u.%u, sw=%u.%u.%u.%u flags=%u", + data[offset + 4], + data[offset + 5], data[offset + 6], + data[offset + 7], data[offset + 8], data[offset + 9], + array_uint32_le (data + offset + 12), + array_uint16_le (data + offset + 10)); + } else if (id == CFG_ID_SERIAL) { + DEBUG (abstract->context, "Device: serial=%.4s-%.8s", + data + offset + 4, data + offset + 8); + } else if (id == CFG_ID_DILUENTS) { + for (unsigned int i = 0; i < 4; ++i) { + unsigned int o2 = data[offset + 4 + i * 3 + 0]; + unsigned int he = data[offset + 4 + i * 3 + 1]; + unsigned int state = data[offset + 4 + i * 3 + 2]; + if (state & 0x01) { + if (ngasmix_diluent >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix_diluent[ngasmix_diluent].oxygen = o2; + gasmix_diluent[ngasmix_diluent].helium = he; + gasmix_diluent[ngasmix_diluent].type = DILUENT; + gasmix_diluent[ngasmix_diluent].id = (state & 0xFE) >> 1; + ngasmix_diluent++; + } + } + } else if (id == CFG_ID_OXYGEN_CALIBRATION) { + for (unsigned int i = 0; i < NSENSORS; ++i) { + calibration[i] = array_uint16_le (data + offset + 4 + i * 2); + } + calibrated = 1; + } else if (id == CFG_ID_AI) { + unsigned int o2 = data[offset + 4]; + unsigned int he = data[offset + 5]; + unsigned int volume = array_uint16_le (data + offset + 6); + unsigned int workpressure = array_uint16_le (data + offset + 8); + unsigned int transmitter = data[offset + 10]; + unsigned int gasmixid = data[offset + 11]; + + // Workaround for a bug in some pre-release firmware versions, + // where the ID of the CCR gas mixes (oxygen and diluent) is + // not stored correctly. + if (gasmixid < 10 && gasmixid <= gasmixid_previous && gasmixid_previous != UNDEFINED) { + WARNING (abstract->context, "Fixed the CCR gas mix id (%u -> %u) for tank %u.", + gasmixid, gasmixid + 10, ntanks); + gasmixid += 10; + } + gasmixid_previous = gasmixid; + + // Add the gas mix. + if (ngasmix_ai >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix_ai[ngasmix_ai].oxygen = o2; + gasmix_ai[ngasmix_ai].helium = he; + if (gasmixid == 10) { + gasmix_ai[ngasmix_ai].type = OXYGEN; + } else if (gasmixid == 11) { + gasmix_ai[ngasmix_ai].type = DILUENT; + } else { + gasmix_ai[ngasmix_ai].type = OC; + } + gasmix_ai[ngasmix_ai].id = gasmixid; + ngasmix_ai++; + + // Add the tank. + if (ntanks >= NTANKS) { + ERROR (abstract->context, "Maximum number of tanks reached."); + return DC_STATUS_NOMEMORY; + } + tank[ntanks].volume = volume; + tank[ntanks].workpressure = workpressure; + tank[ntanks].transmitter = transmitter; + ntanks++; + } + } else if ((type >= LREC_MANIPULATION && type <= LREC_ACTIVITY) || type == LREC_INFO) { + // Event record. + unsigned int event = array_uint16_le (data + offset + 4); + + if (event == EVENT_MIX_CHANGED || event == EVENT_DILUENT || event == EVENT_CHANGE_MODE) { + unsigned int o2 = data[offset + 6]; + unsigned int he = data[offset + 7]; + unsigned int mixtype = OC; + if (event == EVENT_DILUENT) { + mixtype = DILUENT; + } else if (event == EVENT_CHANGE_MODE) { + unsigned int mode = data[offset + 8]; + if (divesoft_freedom_is_ccr (mode)) { + mixtype = DILUENT; + } + } + + unsigned int idx = divesoft_freedom_find_gasmix (gasmix_event, ngasmix_event, o2, he, mixtype); + if (idx >= ngasmix_event) { + if (ngasmix_event >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix_event[ngasmix_event].oxygen = o2; + gasmix_event[ngasmix_event].helium = he; + gasmix_event[ngasmix_event].type = mixtype; + gasmix_event[ngasmix_event].id = UNDEFINED; + ngasmix_event++; + } + } + } else if (type == LREC_MEASURE) { + // Measurement record. + if (id == MEASURE_ID_AI_PRESSURE) { + for (unsigned int i = 0; i < NTANKS; ++i) { + unsigned int pressure = data[offset + 4 + i]; + if (pressure == 0 || pressure == 0xFF) + continue; + + unsigned int idx = divesoft_freedom_find_tank (tank, ntanks, i); + if (idx >= ntanks) { + ERROR (abstract->context, "Tank %u not found.", idx); + return DC_STATUS_DATAFORMAT; + } + + if (!tank[idx].active) { + tank[idx].active = 1; + tank[idx].beginpressure = pressure; + tank[idx].endpressure = pressure; + } + tank[idx].endpressure = pressure; + } + } + } + + offset += RECORD_SIZE; + } + + unsigned int ngasmixes = 0; + divesoft_freedom_gasmix_t gasmix[NGASMIXES] = {0}; + unsigned int diluent = UNDEFINED; + + // Add the gas mixes from the AI integration records. + for (unsigned int i = 0; i < ngasmix_ai; ++i) { + gasmix[ngasmixes] = gasmix_ai[i]; + ngasmixes++; + } + + // Add the gas mixes from the diluent records. + for (unsigned int i = 0; i < ngasmix_diluent; ++i) { + unsigned int idx = divesoft_freedom_find_gasmix (gasmix, ngasmixes, + gasmix_diluent[i].oxygen, gasmix_diluent[i].helium, gasmix_diluent[i].type); + if (idx >= ngasmixes) { + if (ngasmixes >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix[ngasmixes] = gasmix_diluent[i]; + ngasmixes++; + } + } + + // Add the initial diluent. + if (divesoft_freedom_is_ccr (divemode) && + (diluent_o2 != 0 || diluent_he != 0)) { + unsigned int idx = divesoft_freedom_find_gasmix (gasmix, ngasmixes, + diluent_o2, diluent_he, DILUENT); + if (idx >= ngasmixes) { + if (ngasmixes >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix[ngasmixes].oxygen = diluent_o2; + gasmix[ngasmixes].helium = diluent_he; + gasmix[ngasmixes].type = DILUENT; + gasmix[ngasmixes].id = UNDEFINED; + ngasmixes++; + } + + // Index of the initial diluent. + diluent = idx; + } + + // Add the gas mixes from the gas change events. + for (unsigned int i = 0; i < ngasmix_event; ++i) { + unsigned int idx = divesoft_freedom_find_gasmix (gasmix, ngasmixes, + gasmix_event[i].oxygen, gasmix_event[i].helium, gasmix_event[i].type); + if (idx >= ngasmixes) { + if (ngasmixes >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasmix[ngasmixes] = gasmix_event[i]; + ngasmixes++; + } + } + + // Cache the data for later use. + parser->cached = 1; + parser->version = version; + parser->headersize = headersize; + parser->divetime = divetime; + parser->divemode = divemode; + parser->temperature_min = temperature_min; + parser->maxdepth = maxdepth; + parser->atmospheric = atmospheric; + parser->avgdepth = avgdepth; + parser->ngasmixes = ngasmixes; + for (unsigned int i = 0; i < ngasmixes; ++i) { + parser->gasmix[i] = gasmix[i]; + } + parser->diluent = diluent; + parser->ntanks = ntanks; + for (unsigned int i = 0; i < ntanks; ++i) { + parser->tank[i] = tank[i]; + } + parser->vpm = vpm; + parser->gf_lo = gf_lo; + parser->gf_hi = gf_hi; + parser->seawater = seawater; + for (unsigned int i = 0; i < NSENSORS; ++i) { + parser->calibration[i] = calibration[i]; + } + parser->calibrated = calibrated; + + return DC_STATUS_SUCCESS; +} + +dc_status_t +divesoft_freedom_parser_create (dc_parser_t **out, dc_context_t *context) +{ + divesoft_freedom_parser_t *parser = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + parser = (divesoft_freedom_parser_t *) dc_parser_allocate (context, &divesoft_freedom_parser_vtable); + if (parser == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + parser->cached = 0; + parser->version = 0; + parser->headersize = 0; + parser->divetime = 0; + parser->divemode = 0; + parser->temperature_min = 0; + parser->maxdepth = 0; + parser->atmospheric = 0; + parser->avgdepth = 0; + parser->ngasmixes = 0; + for (unsigned int i = 0; i < NGASMIXES; ++i) { + parser->gasmix[i].oxygen = 0; + parser->gasmix[i].helium = 0; + parser->gasmix[i].type = 0; + parser->gasmix[i].id = 0; + } + parser->diluent = UNDEFINED; + parser->ntanks = 0; + for (unsigned int i = 0; i < NTANKS; ++i) { + parser->tank[i].volume = 0; + parser->tank[i].workpressure = 0; + parser->tank[i].beginpressure = 0; + parser->tank[i].endpressure = 0; + parser->tank[i].transmitter = 0; + parser->tank[i].active = 0; + } + parser->vpm = 0; + parser->gf_lo = 0; + parser->gf_hi = 0; + parser->seawater = 0; + for (unsigned int i = 0; i < NSENSORS; ++i) { + parser->calibration[i] = 0; + } + parser->calibrated = 0; + + *out = (dc_parser_t *) parser; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +divesoft_freedom_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size) +{ + divesoft_freedom_parser_t *parser = (divesoft_freedom_parser_t *) abstract; + + // Reset the cache. + parser->cached = 0; + parser->version = 0; + parser->headersize = 0; + parser->divetime = 0; + parser->divemode = 0; + parser->temperature_min = 0; + parser->maxdepth = 0; + parser->atmospheric = 0; + parser->avgdepth = 0; + parser->ngasmixes = 0; + for (unsigned int i = 0; i < NGASMIXES; ++i) { + parser->gasmix[i].oxygen = 0; + parser->gasmix[i].helium = 0; + parser->gasmix[i].type = 0; + parser->gasmix[i].id = 0; + } + parser->diluent = UNDEFINED; + parser->ntanks = 0; + for (unsigned int i = 0; i < NTANKS; ++i) { + parser->tank[i].volume = 0; + parser->tank[i].workpressure = 0; + parser->tank[i].beginpressure = 0; + parser->tank[i].endpressure = 0; + parser->tank[i].transmitter = 0; + parser->tank[i].active = 0; + } + parser->vpm = 0; + parser->gf_lo = 0; + parser->gf_hi = 0; + parser->seawater = 0; + for (unsigned int i = 0; i < NSENSORS; ++i) { + parser->calibration[i] = 0; + } + parser->calibrated = 0; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +divesoft_freedom_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) +{ + dc_status_t status = DC_STATUS_SUCCESS; + divesoft_freedom_parser_t *parser = (divesoft_freedom_parser_t *) abstract; + const unsigned char *data = abstract->data; + + // Cache the header data. + status = divesoft_freedom_cache (parser); + if (status != DC_STATUS_SUCCESS) + return status; + + unsigned int timestamp = array_uint32_le (data + 8); + dc_ticks_t ticks = (dc_ticks_t) timestamp + EPOCH; + + if (!dc_datetime_gmtime (datetime, ticks)) + return DC_STATUS_DATAFORMAT; + + if (parser->version == HEADER_SIGNATURE_V2) { + datetime->timezone = ((signed short) array_uint16_le (data + 40)) * 60; + } else { + datetime->timezone = DC_TIMEZONE_NONE; + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +divesoft_freedom_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) +{ + dc_status_t status = DC_STATUS_SUCCESS; + divesoft_freedom_parser_t *parser = (divesoft_freedom_parser_t *) abstract; + + // Cache the header data. + status = divesoft_freedom_cache (parser); + if (status != DC_STATUS_SUCCESS) + return status; + + dc_salinity_t *water = (dc_salinity_t *) value; + dc_gasmix_t *gasmix = (dc_gasmix_t *) value; + dc_tank_t *tank = (dc_tank_t *) value; + dc_decomodel_t *decomodel = (dc_decomodel_t *) value; + + if (value) { + switch (type) { + case DC_FIELD_DIVETIME: + *((unsigned int *) value) = parser->divetime; + break; + case DC_FIELD_MAXDEPTH: + *((double *) value) = parser->maxdepth / 100.0; + break; + case DC_FIELD_AVGDEPTH: + if (parser->version != HEADER_SIGNATURE_V2) + return DC_STATUS_UNSUPPORTED; + *((double *) value) = parser->avgdepth / 100.0; + break; + case DC_FIELD_TEMPERATURE_MINIMUM: + *((double *) value) = parser->temperature_min / 10.0; + break; + case DC_FIELD_ATMOSPHERIC: + *((double *) value) = parser->atmospheric * 10.0 / BAR; + break; + case DC_FIELD_SALINITY: + water->type = parser->seawater ? DC_WATER_SALT : DC_WATER_FRESH; + water->density = parser->seawater ? SEAWATER : FRESHWATER; + break; + case DC_FIELD_DIVEMODE: + switch (parser->divemode) { + case STMODE_OC: + *((dc_divemode_t *) value) = DC_DIVEMODE_OC; + break; + case STMODE_CCR: + case STMODE_MCCR: + case STMODE_BOCCR: + *((dc_divemode_t *) value) = DC_DIVEMODE_CCR; + break; + case STMODE_FREE: + *((dc_divemode_t *) value) = DC_DIVEMODE_FREEDIVE; + break; + case STMODE_GAUGE: + *((dc_divemode_t *) value) = DC_DIVEMODE_GAUGE; + break; + case STMODE_ASCR: + case STMODE_PSCR: + *((dc_divemode_t *) value) = DC_DIVEMODE_SCR; + break; + case STMODE_UNKNOWN: + return DC_STATUS_UNSUPPORTED; + default: + return DC_STATUS_DATAFORMAT; + } + break; + case DC_FIELD_GASMIX_COUNT: + *((unsigned int *) value) = parser->ngasmixes; + break; + case DC_FIELD_GASMIX: + gasmix->helium = parser->gasmix[flags].helium / 100.0; + gasmix->oxygen = parser->gasmix[flags].oxygen / 100.0; + gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium; + break; + case DC_FIELD_TANK_COUNT: + *((unsigned int *) value) = parser->ntanks; + break; + case DC_FIELD_TANK: + if (parser->tank[flags].volume > 990 || + parser->tank[flags].workpressure > 400) { + tank->type = DC_TANKVOLUME_NONE; + tank->volume = 0.0; + tank->workpressure = 0.0; + } else { + tank->type = DC_TANKVOLUME_METRIC; + tank->volume = parser->tank[flags].volume / 10.0; + tank->workpressure = parser->tank[flags].workpressure; + } + tank->beginpressure = parser->tank[flags].beginpressure * 2.0; + tank->endpressure = parser->tank[flags].endpressure * 2.0; + tank->gasmix = flags; + break; + case DC_FIELD_DECOMODEL: + if (parser->vpm) { + decomodel->type = DC_DECOMODEL_VPM; + decomodel->conservatism = 0; + } else { + decomodel->type = DC_DECOMODEL_BUHLMANN; + decomodel->conservatism = 0; + decomodel->params.gf.low = parser->gf_lo; + decomodel->params.gf.high = parser->gf_hi; + } + break; + default: + return DC_STATUS_UNSUPPORTED; + } + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +divesoft_freedom_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + divesoft_freedom_parser_t *parser = (divesoft_freedom_parser_t *) abstract; + const unsigned char *data = abstract->data; + unsigned int size = abstract->size; + + // Cache the header data. + status = divesoft_freedom_cache (parser); + if (status != DC_STATUS_SUCCESS) + return status; + + unsigned int time = UNDEFINED; + unsigned int initial = 0; + unsigned int offset = parser->headersize; + while (offset + RECORD_SIZE <= size) { + dc_sample_value_t sample = {0}; + + if (array_isequal(data + offset, RECORD_SIZE, 0xFF)) { + WARNING (abstract->context, "Skipping empty sample."); + offset += RECORD_SIZE; + continue; + } + + unsigned int flags = array_uint32_le (data + offset); + unsigned int type = (flags & 0x0000000F) >> 0; + unsigned int timestamp = (flags & 0x001FFFF0) >> 4; + unsigned int id = (flags & 0x7FE00000) >> 21; + + if (timestamp != time) { + if (timestamp < time && time != UNDEFINED) { + // The timestamp are supposed to be monotonically increasing, + // but occasionally there are small jumps back in time with just + // 1 or 2 seconds. To get back in sync, those samples are + // skipped. Larger jumps are treated as errors. + if (time - timestamp > 5) { + ERROR (abstract->context, "Timestamp moved backwards (%u %u).", timestamp, time); + return DC_STATUS_DATAFORMAT; + } + WARNING (abstract->context, "Timestamp moved backwards (%u %u).", timestamp, time); + offset += RECORD_SIZE; + continue; + } + time = timestamp; + sample.time = time; + if (callback) callback(DC_SAMPLE_TIME, sample, userdata); + } + + // Initial diluent. + if (!initial) { + if (parser->diluent != UNDEFINED) { + sample.gasmix = parser->diluent; + if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata); + } + initial = 1; + } + + if (type == LREC_POINT) { + // General log record. + unsigned int depth = array_uint16_le (data + offset + 4); + unsigned int ppo2 = array_uint16_le (data + offset + 6); + + sample.depth = depth / 100.0; + if (callback) callback(DC_SAMPLE_DEPTH, sample, userdata); + + if (ppo2) { + sample.ppo2 = ppo2 * 10.0 / BAR; + if (callback) callback(DC_SAMPLE_PPO2, sample, userdata); + } + + if (id == POINT_2) { + unsigned int orientation = array_uint32_le (data + offset + 8); + unsigned int heading = orientation & 0x1FF; + sample.bearing = heading; + if (callback) callback (DC_SAMPLE_BEARING, sample, userdata); + } else if (id == POINT_1 || id == POINT_1_OLD) { + unsigned int misc = array_uint32_le (data + offset + 8); + unsigned int ceiling = array_uint16_le (data + offset + 12); + unsigned int setpoint = data[offset + 15]; + unsigned int ndl = (misc & 0x000003FF); + unsigned int tts = (misc & 0x000FFC00) >> 10; + unsigned int temp = (misc & 0x3FF00000) >> 20; + + // Temperature + sample.temperature = (signed int) signextend (temp, 10) / 10.0; + if (callback) callback(DC_SAMPLE_TEMPERATURE, sample, userdata); + + // Deco / NDL + if (ceiling) { + sample.deco.type = DC_DECO_DECOSTOP; + sample.deco.time = 0; + sample.deco.depth = ceiling / 100.0; + } else { + sample.deco.type = DC_DECO_NDL; + sample.deco.time = ndl * 60; + sample.deco.depth = 0.0; + } + if (callback) callback(DC_SAMPLE_DECO, sample, userdata); + + // Setpoint + if (setpoint) { + sample.setpoint = setpoint / 100.0; + if (callback) callback(DC_SAMPLE_SETPOINT, sample, userdata); + } + } + } else if ((type >= LREC_MANIPULATION && type <= LREC_ACTIVITY) || type == LREC_INFO) { + // Event record. + unsigned int event = array_uint16_le (data + offset + 4); + + if (event == EVENT_BOOKMARK) { + sample.event.type = SAMPLE_EVENT_BOOKMARK; + sample.event.time = 0; + sample.event.flags = 0; + sample.event.value = 0; + if (callback) callback(DC_SAMPLE_EVENT, sample, userdata); + } else if (event == EVENT_MIX_CHANGED || event == EVENT_DILUENT || event == EVENT_CHANGE_MODE) { + unsigned int o2 = data[offset + 6]; + unsigned int he = data[offset + 7]; + unsigned int mixtype = OC; + if (event == EVENT_DILUENT) { + mixtype = DILUENT; + } else if (event == EVENT_CHANGE_MODE) { + unsigned int mode = data[offset + 8]; + if (divesoft_freedom_is_ccr (mode)) { + mixtype = DILUENT; + } + } + + unsigned int idx = divesoft_freedom_find_gasmix (parser->gasmix, parser->ngasmixes, o2, he, mixtype); + if (idx >= parser->ngasmixes) { + ERROR (abstract->context, "Gas mix (%u/%u) not found.", o2, he); + return DC_STATUS_DATAFORMAT; + } + sample.gasmix = idx; + if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata); + } else if (event == EVENT_CNS) { + sample.cns = array_uint16_le (data + offset + 6) / 100.0; + if (callback) callback(DC_SAMPLE_CNS, sample, userdata); + } else if (event == EVENT_SETPOINT_MANUAL || event == EVENT_SETPOINT_AUTO) { + sample.setpoint = data[6] / 100.0; + if (callback) callback(DC_SAMPLE_SETPOINT, sample, userdata); + } + } else if (type == LREC_MEASURE) { + // Measurement record. + if (id == MEASURE_ID_AI_PRESSURE) { + for (unsigned int i = 0; i < NTANKS; ++i) { + unsigned int pressure = data[offset + 4 + i]; + if (pressure == 0 || pressure == 0xFF) + continue; + + unsigned int idx = divesoft_freedom_find_tank (parser->tank, parser->ntanks, i); + if (idx >= parser->ntanks) { + ERROR (abstract->context, "Tank %u not found.", idx); + return DC_STATUS_DATAFORMAT; + } + + sample.pressure.tank = idx; + sample.pressure.value = pressure * 2.0; + if (callback) callback(DC_SAMPLE_PRESSURE, sample, userdata); + } + } else if (id == MEASURE_ID_OXYGEN) { + for (unsigned int i = 0; i < NSENSORS; ++i) { + unsigned int ppo2 = array_uint16_le (data + offset + 4 + i * 2); + if (ppo2 == 0 || ppo2 == 0xFFFF) + continue; + sample.ppo2 = ppo2 * 10.0 / BAR; + if (callback) callback(DC_SAMPLE_PPO2, sample, userdata); + } + } else if (id == MEASURE_ID_OXYGEN_MV) { + for (unsigned int i = 0; i < NSENSORS; ++i) { + unsigned int value = array_uint16_le (data + offset + 4 + i * 2); + unsigned int state = data[offset + 12 + i]; + if (!parser->calibrated || state == SENSTAT_UNCALIBRATED || + state == SENSTAT_NOT_EXIST) + continue; + sample.ppo2 = value / 100.0 * parser->calibration[i] / BAR; + if (callback) callback(DC_SAMPLE_PPO2, sample, userdata); + } + } + } else if (type == LREC_STATE) { + // Tissue saturation record. + } + + offset += RECORD_SIZE; + } + + return DC_STATUS_SUCCESS; +} diff --git a/src/parser.c b/src/parser.c index 961f59e..63485d2 100644 --- a/src/parser.c +++ b/src/parser.c @@ -63,6 +63,7 @@ #include "seac_screen.h" #include "deepblu_cosmiq.h" #include "oceans_s1.h" +#include "divesoft_freedom.h" #include "context-private.h" #include "parser-private.h" @@ -199,6 +200,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_OCEANS_S1: rc = oceans_s1_parser_create (&parser, context); break; + case DC_FAMILY_DIVESOFT_FREEDOM: + rc = divesoft_freedom_parser_create (&parser, context); + break; default: return DC_STATUS_INVALIDARGS; }