diff --git a/examples/common.c b/examples/common.c index fd8ed4c..6c9a957 100644 --- a/examples/common.c +++ b/examples/common.c @@ -97,6 +97,7 @@ static const backend_table_t g_backends[] = { {"excursion", DC_FAMILY_DEEPSIX_EXCURSION, 0}, {"screen", DC_FAMILY_SEAC_SCREEN, 0}, {"cosmiq", DC_FAMILY_DEEPBLU_COSMIQ, 0}, + {"s1", DC_FAMILY_OCEANS_S1, 0}, }; static const transport_table_t g_transports[] = { diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index c2b1fcd..c05bee1 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -116,6 +116,8 @@ typedef enum dc_family_t { DC_FAMILY_SEAC_SCREEN = (20 << 16), /* Deepblu Cosmiq */ DC_FAMILY_DEEPBLU_COSMIQ = (21 << 16), + /* Oceans S1 */ + DC_FAMILY_OCEANS_S1 = (22 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/msvc/libdivecomputer.vcxproj b/msvc/libdivecomputer.vcxproj index 536dc76..b3b370f 100644 --- a/msvc/libdivecomputer.vcxproj +++ b/msvc/libdivecomputer.vcxproj @@ -219,6 +219,9 @@ + + + @@ -333,6 +336,8 @@ + + diff --git a/src/Makefile.am b/src/Makefile.am index aabbe19..72961b8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -78,6 +78,8 @@ libdivecomputer_la_SOURCES = \ deepsix_excursion.h deepsix_excursion.c deepsix_excursion_parser.c \ seac_screen.h seac_screen.c seac_screen_parser.c \ 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 \ socket.h socket.c \ irda.c \ usb.c \ diff --git a/src/descriptor.c b/src/descriptor.c index 9b3be61..e41b016 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -64,6 +64,7 @@ static int dc_filter_mclean (dc_transport_t transport, const void *userdata, voi static int dc_filter_atomic (dc_transport_t transport, const void *userdata, void *params); 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 dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -448,6 +449,8 @@ static const dc_descriptor_t g_descriptors[] = { {"Seac", "Action", DC_FAMILY_SEAC_SCREEN, 0, DC_TRANSPORT_SERIAL, NULL}, /* Deepblu Cosmiq */ {"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}, }; static int @@ -782,6 +785,19 @@ static int dc_filter_deepblu (dc_transport_t transport, const void *userdata, vo return 1; } +static int dc_filter_oceans (dc_transport_t transport, const void *userdata, void *params) +{ + static const char * const bluetooth[] = { + "S1", + }; + + 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 b6bbfc2..e930ff1 100644 --- a/src/device.c +++ b/src/device.c @@ -63,6 +63,7 @@ #include "deepsix_excursion.h" #include "seac_screen.h" #include "deepblu_cosmiq.h" +#include "oceans_s1.h" #include "device-private.h" #include "context-private.h" @@ -235,6 +236,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_DEEPBLU_COSMIQ: rc = deepblu_cosmiq_device_open (&device, context, iostream); break; + case DC_FAMILY_OCEANS_S1: + rc = oceans_s1_device_open (&device, context, iostream); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/oceans_s1.c b/src/oceans_s1.c new file mode 100644 index 0000000..e26979e --- /dev/null +++ b/src/oceans_s1.c @@ -0,0 +1,717 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Linus Torvalds + * Copyright (C) 2022 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 + +#include "oceans_s1.h" +#include "oceans_s1_common.h" +#include "context-private.h" +#include "device-private.h" +#include "platform.h" +#include "checksum.h" +#include "array.h" + +#define SOH 0x01 +#define EOT 0x04 +#define ACK 0x06 +#define NAK 0x15 +#define CAN 0x18 +#define CRC 0x43 + +#define SZ_PACKET 256 +#define SZ_XMODEM 512 + +#define SZ_FINGERPRINT 8 + +typedef struct oceans_s1_dive_t { + struct oceans_s1_dive_t *next; + dc_ticks_t timestamp; + unsigned int number; +} oceans_s1_dive_t; + +typedef struct oceans_s1_device_t { + dc_device_t base; + dc_iostream_t *iostream; + dc_ticks_t timestamp; +} oceans_s1_device_t; + +static dc_status_t oceans_s1_device_set_fingerprint(dc_device_t *abstract, const unsigned char data[], unsigned int size); +static dc_status_t oceans_s1_device_foreach(dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); +static dc_status_t oceans_s1_device_timesync(dc_device_t *abstract, const dc_datetime_t *datetime); + +static const dc_device_vtable_t oceans_s1_device_vtable = { + sizeof(oceans_s1_device_t), + DC_FAMILY_OCEANS_S1, + oceans_s1_device_set_fingerprint, /* set_fingerprint */ + NULL, /* read */ + NULL, /* write */ + NULL, /* dump */ + oceans_s1_device_foreach, /* foreach */ + oceans_s1_device_timesync, /* timesync */ + NULL, /* close */ +}; + +/* + * Oceans S1 initial sequence (all ASCII text with newlines): + * + * Cmd Reply + * + * utc\n utc>ok 1592912375\r\n + * battery\n battery>ok 59%\r\n + * version\n version>ok 1.1 42a7e564\r\n + * utc 1592912364\n utc>ok\r\n + * units 1\n units>ok\r\n + * dllist\n dllist>xmr\r\n + * + * At this point, the dive computer switches to the XMODEM protocol and + * the sequence is no longer single packets with a full line with newline + * termination. + * + * The actual payload remains ASCII text (note the single space indentation): + * + * divelog v1,10s/sample + * dive 1,0,21,1591372057 + * continue 612,10 + * enddive 3131,496 + * dive 2,0,21,1591372925 + * enddive 1535,277 + * dive 3,0,32,1591463368 + * enddive 1711,4515 + * dive 4,0,32,1591961688 + * continue 300,45 + * continue 391,238 + * continue 420,126 + * continue 236,17 + * enddive 1087,2636 + * endlog + * + * Because the XMODEM protocol uses fixed size packets (512 bytes), the last + * packet is padded with newline characters. + * + * Then it goes back to line-mode: + * + * dlget 4 5\n dlget>xmr\r\n + * + * and the data is again transferred using the XMODEM protocol. The payload is + * also ASCII text (note the space indentation again): + * + * divelog v1,10s/sample + * dive 4,0,32,1591961688 + * 365,13,1 + * 382,13,51456 + * 367,13,16640 + * 381,13,49408 + * 375,13,24576 + * 355,13,16384 + * 346,13,16384 + * 326,14,16384 + * 355,14,16384 + * 394,14,24576 + * 397,14,16384 + * 434,14,49152 + * 479,14,49152 + * 488,14,16384 + * 556,14,57344 + * 616,14,49152 + * 655,14,49152 + * 738,14,49152 + * 800,14,57344 + * 800,14,49408 + * 834,14,16640 + * 871,14,24832 + * 860,14,16640 + * 860,14,16640 + * 815,14,24832 + * 738,14,16640 + * 707,14,16640 + * 653,14,24832 + * 647,13,16640 + * 670,13,16640 + * 653,13,24832 + * ... + * continue 236,17 + * 227,13,57600 + * 238,14,16640 + * 267,14,24832 + * 283,14,16384 + * 272,14,16384 + * 303,14,24576 + * 320,14,16384 + * 318,14,16384 + * 318,14,16384 + * 335,14,24576 + * 332,14,16384 + * 386,14,16384 + * 417,14,24576 + * 244,14,16640 + * 71,14,16640 + * enddive 1087,2636 + * endlog + * + * Where the samples seem to be + * - depth in cm + * - temperature in °C + * - events + * + * Repeat with 'dlget 3 4', 'dlget 2 3', 'dlget 1 2'. + * + * Done. + */ + +/* + * Add a dive to the dive list, sorted with newest dive first + * + * I'm not sure if the dive list is always presented sorted by the + * Oceans S1, but it arrives in the reverse order of what we want + * (we want newest first, it lists them oldest first). So we need + * to switch the order, and we might as well make sure it's sorted + * while doing that. + */ +static void +oceans_s1_list_add (oceans_s1_dive_t **head, oceans_s1_dive_t *dive) +{ + if (head == NULL) + return; + + oceans_s1_dive_t *current = *head, *previous = NULL; + while (current) { + if (dive->number >= current->number) + break; + previous = current; + current = current->next; + } + + if (previous) { + dive->next = previous->next; + previous->next = dive; + } else { + dive->next = *head; + *head = dive; + } +} + +static void +oceans_s1_list_free (oceans_s1_dive_t *head) +{ + oceans_s1_dive_t *current = head; + while (current) { + oceans_s1_dive_t *next = current->next; + free (current); + current = next; + } +} + +/* + * The main data is transferred using the XMODEM-CRC protocol. + * + * This variant of the XMODEM protocol uses a sequence of 517 byte packets, + * where each packet has a three byte header, 512 bytes of payload data and a + * two byte CRC checksum. The header is a 'SOH' byte, followed by the block + * number (starting at 1), and the inverse block number (255-block). + * + * We're supposed to start the sequence with a 'CRC' byte, and reply to each + * packet with a 'ACK' byte. When there is no more data, the device will + * send us a 'EOT' packet, which we'll ack with a final 'ACK' byte. + * + * So we get a sequence of: + * + * 01 01 fe <512 bytes> xx xx + * 01 02 fd <512 bytes> xx xx + * 01 03 fc <512 bytes> xx xx + * 01 04 fb <512 bytes> xx xx + * 01 05 fa <512 bytes> xx xx + * 01 06 f9 <512 bytes> xx xx + * 01 07 f8 <512 bytes> xx xx + * 04 + * + * And we should reply with an 'ACK' byte for each of those entries. + * + * NOTE! The above is not in single BLE packets, although the + * sequence blocks always start at a packet boundary. + * + * NOTE! The Oceans Android app uses GATT "Write Commands" (0x53), and not + * GATT "Write Requests" (0x12) for sending the XMODEM single byte commands, + * but this difference does not seem to matter. + */ +static dc_status_t +oceans_s1_xmodem_packet (oceans_s1_device_t *device, unsigned char seq, unsigned char data[], size_t size) +{ + dc_status_t status = DC_STATUS_SUCCESS; + unsigned char packet[3 + SZ_XMODEM + 2] = {0}; + size_t nbytes = 0; + + if (size < SZ_XMODEM) + return DC_STATUS_INVALIDARGS; + + status = dc_iostream_read (device->iostream, packet, sizeof(packet), &nbytes); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to receive the packet."); + return status; + } + + if (nbytes < 1) { + ERROR (device->base.context, "Unexpected packet length (" DC_PRINTF_SIZE ").", nbytes); + return DC_STATUS_PROTOCOL; + } + + if (packet[0] == EOT) { + return DC_STATUS_DONE; + } + + if (nbytes < 3) { + ERROR (device->base.context, "Unexpected packet length (" DC_PRINTF_SIZE ").", nbytes); + return DC_STATUS_PROTOCOL; + } + + if (packet[0] != SOH || packet[1] != seq || packet[1] + packet[2] != 0xFF) { + ERROR (device->base.context, "Unexpected packet header."); + return DC_STATUS_PROTOCOL; + } + + while (nbytes < sizeof(packet)) { + size_t received = 0; + status = dc_iostream_read (device->iostream, packet + nbytes, sizeof(packet) - nbytes, &received); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to receive the packet."); + return status; + } + + nbytes += received; + } + + unsigned short crc = array_uint16_be (packet + nbytes - 2); + unsigned short ccrc = checksum_crc16_ccitt (packet + 3, nbytes - 5, 0x0000); + if (crc != ccrc) { + ERROR (device->base.context, "Unexpected answer checksum (%04x %04x).", crc, ccrc); + return DC_STATUS_PROTOCOL; + } + + memcpy (data, packet + 3, SZ_XMODEM); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_xmodem_recv (oceans_s1_device_t *device, dc_buffer_t *buffer) +{ + dc_status_t status = DC_STATUS_SUCCESS; + const unsigned char crc = CRC; + const unsigned char ack = ACK; + + dc_buffer_clear (buffer); + + // Request XMODEM-CRC mode. + status = dc_iostream_write (device->iostream, &crc, 1, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + + unsigned char seq = 1; + while (1) { + // Receive the XMODEM data packet. + unsigned char packet[SZ_XMODEM] = {0}; + status = oceans_s1_xmodem_packet (device, seq, packet, sizeof(packet)); + if (status != DC_STATUS_SUCCESS) { + if (status == DC_STATUS_DONE) + break; + return status; + } + + dc_buffer_append (buffer, packet, sizeof(packet)); + + // Ack the data packet. + status = dc_iostream_write (device->iostream, &ack, 1, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + + seq++; + } + + // Ack the EOT packet. + status = dc_iostream_write (device->iostream, &ack, 1, NULL); + if (status != DC_STATUS_SUCCESS) { + return status; + } + + // Find trailing newline(s). + size_t size = dc_buffer_get_size (buffer); + unsigned char *data = dc_buffer_get_data (buffer); + while (size > 1 && (data[size - 2] == '\r' || data[size - 2] == '\n')) + size--; + + // Remove trailing newline(s). + dc_buffer_slice (buffer, 0, size); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t DC_ATTR_FORMAT_PRINTF(6, 7) +oceans_s1_transfer (oceans_s1_device_t *device, dc_buffer_t *buffer, char data[], size_t size, const char *cmd, const char *params, ...) +{ + dc_status_t status = DC_STATUS_SUCCESS; + char buf[SZ_PACKET + 1] = {0}; + size_t buflen = 0; + + if (device_is_cancelled (&device->base)) + return DC_STATUS_CANCELLED; + + size_t cmdlen = strlen (cmd); + if (buflen + cmdlen > sizeof(buf) - 1) { + ERROR (device->base.context, "Not enough space for the command string."); + return DC_STATUS_NOMEMORY; + } + + // Copy the command string. + memcpy (buf, cmd, cmdlen); + buflen += cmdlen; + + // Null terminate the buffer. + buf[buflen] = 0; + + if (params) { + if (buflen + 1 > sizeof(buf) - 1) { + ERROR (device->base.context, "Not enough space for the separator."); + return DC_STATUS_NOMEMORY; + } + + // Append a space. + buf[buflen++] = ' '; + + // Null terminate the buffer. + buf[buflen] = 0; + + // Append the arguments. + va_list ap; + va_start (ap, params); + int n = dc_platform_vsnprintf (buf + buflen, sizeof(buf) - buflen, params, ap); + va_end (ap); + if (n < 0) { + ERROR (device->base.context, "Not enough space for the arguments."); + return DC_STATUS_NOMEMORY; + } + + buflen += n; + } + + DEBUG(device->base.context, "cmd: %s", buf); + + if (buflen + 1 > sizeof(buf) - 1) { + ERROR (device->base.context, "Not enough space for the newline."); + return DC_STATUS_NOMEMORY; + } + + // Append a newline. + buf[buflen++] = '\n'; + + // Null terminate the buffer. + buf[buflen] = 0; + + // Send the command. + status = dc_iostream_write (device->iostream, buf, buflen, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to send the command."); + return status; + } + + // Receive the response. + size_t nbytes = 0; + status = dc_iostream_read (device->iostream, buf, sizeof(buf) - 1, &nbytes); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to receive the response."); + return status; + } + + // Remove trailing newline(s). + while (nbytes && (buf[nbytes - 1] == '\r' || buf[nbytes - 1] == '\n')) + nbytes--; + + // Null terminate the buffer. + buf[nbytes] = 0; + + DEBUG (device->base.context, "rcv: %s", buf); + + // Verify the response. + if (strncmp (buf, cmd, cmdlen) != 0) { + ERROR (device->base.context, "Received unexpected packet data ('%s').", buf); + return DC_STATUS_PROTOCOL; + } + + // Check the type of response. + // If the response indicates "ok", the payload data is send inline in + // the remainder of the response packet. If the response indicates "xmr", + // the payload data is send separately using the XMODEM protocol. + if (strncmp (buf + cmdlen, ">ok", 3) == 0) { + // Ignore leading whitespace. + const char *line = buf + cmdlen + 3; + while (*line == ' ') + line++; + + // Copy the payload data. + size_t len = nbytes - (line - buf); + if (size) { + if (len + 1 > size) { + ERROR (device->base.context, "Unexpected packet length (" DC_PRINTF_SIZE ").", len); + return DC_STATUS_PROTOCOL; + } + memcpy (data, line, len + 1); + } else { + if (len != 0) { + ERROR (device->base.context, "Unexpected packet length (" DC_PRINTF_SIZE ").", len); + return DC_STATUS_PROTOCOL; + } + } + } else if (strncmp (buf + cmdlen, ">xmr", 4) == 0) { + if (nbytes > cmdlen + 4) { + WARNING (device->base.context, "Packet contains extra data ('%s').", buf + cmdlen + 4); + } + return oceans_s1_xmodem_recv (device, buffer); + } else { + ERROR (device->base.context, "Received unexpected packet data ('%s').", buf); + return DC_STATUS_PROTOCOL; + } + + return status; +} + +dc_status_t +oceans_s1_device_open(dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream) +{ + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *device = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + device = (oceans_s1_device_t *) dc_device_allocate (context, &oceans_s1_device_vtable); + if (device == NULL) { + ERROR(context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + device->iostream = iostream; + device->timestamp = 0; + + // Set the timeout for receiving data (4000 ms). + status = dc_iostream_set_timeout (device->iostream, 4000); + if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Failed to set the timeout."); + goto error_free; + } + + dc_iostream_purge (device->iostream, DC_DIRECTION_ALL); + + *out = (dc_device_t *) device; + + return DC_STATUS_SUCCESS; + +error_free: + dc_device_deallocate ((dc_device_t *) device); + return status; +} + +static dc_status_t +oceans_s1_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size) +{ + oceans_s1_device_t *device = (oceans_s1_device_t *) abstract; + + if (size && size != SZ_FINGERPRINT) + return DC_STATUS_INVALIDARGS; + + if (size) + device->timestamp = array_uint64_be (data); + else + device->timestamp = 0; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *device = (oceans_s1_device_t *) abstract; + + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + char version[SZ_PACKET] = {0}; + status = oceans_s1_transfer (device, NULL, version, sizeof(version), "version", NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the version."); + return status; + } + + unsigned int major = 0, minor = 0, unknown = 0; + if (sscanf (version, "%u.%u %x", &major, &minor, &unknown) != 3) { + ERROR (abstract->context, "Failed to parse the version response."); + return DC_STATUS_PROTOCOL; + } + + // Emit a device info event. + dc_event_devinfo_t devinfo; + devinfo.model = 0; + devinfo.firmware = major << 16 | minor; + devinfo.serial = 0; + device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); + + dc_buffer_t *buffer = dc_buffer_new (4096); + if (buffer == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + status = DC_STATUS_NOMEMORY; + goto error_exit; + } + + status = oceans_s1_transfer (device, buffer, NULL, 0, "dllist", NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to download the dive list."); + goto error_free_buffer; + } + + const unsigned char *data = dc_buffer_get_data (buffer); + size_t size = dc_buffer_get_size (buffer); + + oceans_s1_dive_t *logbook = NULL, *dive = NULL; + unsigned int ndives = 0; + + char *ptr = NULL; + size_t len = 0; + int n = 0; + while ((n = oceans_s1_getline (&ptr, &len, &data, &size)) != -1) { + // Ignore empty lines. + if (n == 0) + continue; + + // Ignore leading whitespace. + const char *line = ptr; + while (*line == ' ') + line++; + + if (strncmp (line, "divelog", 7) == 0 || + strncmp (line, "endlog", 6) == 0 || + strncmp (line, "continue", 8) == 0) { + // Nothing to do. + } else if (strncmp (line, "dive", 4) == 0) { + if (dive != NULL) { + ERROR (abstract->context, "Skipping dive without 'enddive' line."); + free (dive); + dive = NULL; + } + + unsigned int number = 0, divemode = 0, o2 = 0; + dc_ticks_t timestamp = 0; + if (sscanf (line, "dive %u,%u,%u," DC_FORMAT_INT64, &number, &divemode, &o2, ×tamp) != 4) { + ERROR (abstract->context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free_list; + } + + dive = (oceans_s1_dive_t *) malloc (sizeof (oceans_s1_dive_t)); + if (dive == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + status = DC_STATUS_NOMEMORY; + goto error_free_list; + } + + dive->next = NULL; + dive->timestamp = timestamp; + dive->number = number; + } else if (strncmp (line, "enddive", 7) == 0) { + if (dive) { + if (dive->timestamp > device->timestamp) { + oceans_s1_list_add (&logbook, dive); + ndives++; + } else { + free (dive); + } + dive = NULL; + } else { + WARNING (abstract->context, "Unexpected line '%s'.", line); + } + } else { + WARNING (abstract->context, "Unexpected line '%s'.", line); + } + } + + if (dive != NULL) { + WARNING (abstract->context, "Skipping dive without 'enddive' line."); + free (dive); + dive = NULL; + } + + progress.current = 1; + progress.maximum = 1 + ndives; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + for (dive = logbook; dive; dive = dive->next) { + status = oceans_s1_transfer (device, buffer, NULL, 0, "dlget", "%u %u", dive->number, dive->number + 1); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to download the dive."); + goto error_free_list; + } + + progress.current++; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + unsigned char fingerprint[SZ_FINGERPRINT] = {0}; + array_uint64_be_set (fingerprint, dive->timestamp); + + if (callback && !callback (dc_buffer_get_data (buffer), dc_buffer_get_size (buffer), fingerprint, sizeof(fingerprint), userdata)) + break; + } + +error_free_list: + oceans_s1_list_free (logbook); + free (ptr); +error_free_buffer: + dc_buffer_free (buffer); +error_exit: + return status; +} + +static dc_status_t +oceans_s1_device_timesync (dc_device_t *abstract, const dc_datetime_t *datetime) +{ + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *device = (oceans_s1_device_t *) abstract; + + // Ignore the timezone offset. + dc_datetime_t dt = *datetime; + dt.timezone = DC_TIMEZONE_NONE; + + dc_ticks_t timestamp = dc_datetime_mktime (&dt); + if (timestamp < 0) { + ERROR (abstract->context, "Invalid date/time value specified."); + return DC_STATUS_INVALIDARGS; + } + + status = oceans_s1_transfer (device, NULL, NULL, 0, "utc", DC_FORMAT_INT64, timestamp); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to set the date/time."); + return status; + } + + return status; +} diff --git a/src/oceans_s1.h b/src/oceans_s1.h new file mode 100644 index 0000000..825d309 --- /dev/null +++ b/src/oceans_s1.h @@ -0,0 +1,44 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Linus Torvalds + * Copyright (C) 2022 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 OCEANS_S1_H +#define OCEANS_S1_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t +oceans_s1_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream); + +dc_status_t +oceans_s1_parser_create (dc_parser_t **parser, dc_context_t *context); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* OCEANS_S1_H */ diff --git a/src/oceans_s1_common.c b/src/oceans_s1_common.c new file mode 100644 index 0000000..0522891 --- /dev/null +++ b/src/oceans_s1_common.c @@ -0,0 +1,68 @@ +/* + * libdivecomputer + * + * Copyright (C) 2022 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 "oceans_s1_common.h" + +int +oceans_s1_getline (char **line, size_t *linelen, const unsigned char **data, size_t *size) +{ + if (line == NULL || linelen == NULL || data == NULL || size == NULL) + return -1; + + if (*size == 0) + return -1; + + // Find the end of the line. + unsigned int strip = 0; + const unsigned char *p = *data, *end = p + *size; + while (p != end) { + unsigned char c = *p++; + if (c == '\r' || c == '\n') { + strip = 1; + break; + } + } + + // Get the length of the line. + size_t len = p - *data; + + // Resize the buffer (if necessary). + if (*line == NULL || len + 1 > *linelen) { + char *buf = (char *) malloc (len + 1); + if (buf == NULL) + return -1; + free (*line); + *line = buf; + *linelen = len + 1; + } + + // Copy the data. + memcpy (*line, *data, len - strip); + (*line)[len - strip] = 0; + *data += len; + *size -= len; + + return len - strip; +} diff --git a/src/oceans_s1_common.h b/src/oceans_s1_common.h new file mode 100644 index 0000000..37a530c --- /dev/null +++ b/src/oceans_s1_common.h @@ -0,0 +1,35 @@ +/* + * libdivecomputer + * + * Copyright (C) 2022 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 OCEANS_S1_COMMON_H +#define OCEANS_S1_COMMON_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +int +oceans_s1_getline (char **line, size_t *linelen, const unsigned char **data, size_t *size); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* OCEANS_S1_COMMON_H */ diff --git a/src/oceans_s1_parser.c b/src/oceans_s1_parser.c new file mode 100644 index 0000000..8b4cfbd --- /dev/null +++ b/src/oceans_s1_parser.c @@ -0,0 +1,317 @@ +/* + * libdivecomputer + * + * Copyright (C) 2020 Linus Torvalds + * Copyright (C) 2022 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 "oceans_s1.h" +#include "oceans_s1_common.h" +#include "context-private.h" +#include "parser-private.h" +#include "platform.h" +#include "array.h" + +#define SCUBA 0 +#define APNEA 1 + +#define EVENT_DIVE_STARTED 0x0001 +#define EVENT_DIVE_ENDED 0x0002 +#define EVENT_DIVE_RESUMED 0x0004 +#define EVENT_PING_SENT 0x0008 +#define EVENT_PING_RECEIVED 0x0010 +#define EVENT_DECO_STOP 0x0020 +#define EVENT_SAFETY_STOP 0x0040 +#define EVENT_BATTERY_LOW 0x0080 +#define EVENT_BACKLIGHT_ON 0x0100 + +typedef struct oceans_s1_parser_t oceans_s1_parser_t; + +struct oceans_s1_parser_t { + dc_parser_t base; + // Cached fields. + dc_ticks_t timestamp; + unsigned int cached; + unsigned int number; + unsigned int divemode; + unsigned int oxygen; + unsigned int maxdepth; + unsigned int divetime; +}; + +static dc_status_t oceans_s1_parser_set_data(dc_parser_t *abstract, const unsigned char *data, unsigned int size); +static dc_status_t oceans_s1_parser_get_datetime(dc_parser_t *abstract, dc_datetime_t *datetime); +static dc_status_t oceans_s1_parser_get_field(dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value); +static dc_status_t oceans_s1_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata); + +static const dc_parser_vtable_t oceans_s1_parser_vtable = { + sizeof(oceans_s1_parser_t), + DC_FAMILY_OCEANS_S1, + oceans_s1_parser_set_data, /* set_data */ + NULL, /* set_clock */ + NULL, /* set_atmospheric */ + NULL, /* set_density */ + oceans_s1_parser_get_datetime, /* datetime */ + oceans_s1_parser_get_field, /* fields */ + oceans_s1_parser_samples_foreach, /* samples_foreach */ + NULL /* destroy */ +}; + +dc_status_t +oceans_s1_parser_create (dc_parser_t **out, dc_context_t *context) +{ + oceans_s1_parser_t *parser = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + parser = (oceans_s1_parser_t *) dc_parser_allocate (context, &oceans_s1_parser_vtable); + if (parser == NULL) { + ERROR (context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + parser->cached = 0; + parser->timestamp = 0; + parser->number = 0; + parser->divemode = 0; + parser->oxygen = 0; + parser->maxdepth = 0; + parser->divetime = 0; + + *out = (dc_parser_t *) parser; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size) +{ + oceans_s1_parser_t *parser = (oceans_s1_parser_t *) abstract; + + // Reset the cache. + parser->cached = 0; + parser->timestamp = 0; + parser->number = 0; + parser->divemode = 0; + parser->oxygen = 0; + parser->maxdepth = 0; + parser->divetime = 0; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime) +{ + oceans_s1_parser_t *parser = (oceans_s1_parser_t *) abstract; + + if (!parser->cached) { + dc_status_t status = oceans_s1_parser_samples_foreach (abstract, NULL, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + } + + if (!dc_datetime_gmtime (datetime, parser->timestamp)) + return DC_STATUS_DATAFORMAT; + + datetime->timezone = DC_TIMEZONE_NONE; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value) +{ + oceans_s1_parser_t *parser = (oceans_s1_parser_t *) abstract; + + if (!parser->cached) { + dc_status_t status = oceans_s1_parser_samples_foreach (abstract, NULL, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + } + + dc_gasmix_t *gasmix = (dc_gasmix_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_GASMIX_COUNT: + *((unsigned int *) value) = parser->divemode == SCUBA; + break; + case DC_FIELD_GASMIX: + gasmix->helium = 0.0; + gasmix->oxygen = parser->oxygen / 100.0; + gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium; + break; + case DC_FIELD_DIVEMODE: + switch (parser->divemode) { + case SCUBA: + *((dc_divemode_t *) value) = DC_DIVEMODE_OC; + break; + case APNEA: + *((dc_divemode_t *) value) = DC_DIVEMODE_FREEDIVE; + break; + default: + return DC_STATUS_DATAFORMAT; + } + break; + default: + return DC_STATUS_UNSUPPORTED; + } + } + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) +{ + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_parser_t *parser = (oceans_s1_parser_t *) abstract; + const unsigned char *data = abstract->data; + size_t size = abstract->size; + + dc_ticks_t timestamp = 0; + unsigned int number = 0, divemode = 0, oxygen = 0; + unsigned int maxdepth = 0, divetime = 0; + unsigned int interval = 10; + unsigned int time = 0; + + char *ptr = NULL; + size_t len = 0; + int n = 0; + while ((n = oceans_s1_getline (&ptr, &len, &data, &size)) != -1) { + dc_sample_value_t sample = {0}; + + // Ignore empty lines. + if (n == 0) + continue; + + // Ignore leading whitespace. + const char *line = ptr; + while (*line == ' ') + line++; + + if (strncmp (line, "divelog", 7) == 0) { + if (sscanf (line, "divelog v1,%us/sample", &interval) != 1) { + ERROR (parser->base.context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + if (interval == 0) { + ERROR (parser->base.context, "Invalid sample interval (%u).", interval); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + } else if (strncmp (line, "dive", 4) == 0) { + if (sscanf (line, "dive %u,%u,%u," DC_FORMAT_INT64, &number, &divemode, &oxygen, ×tamp) != 4) { + ERROR (parser->base.context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + } else if (strncmp (line, "continue", 8) == 0) { + unsigned int depth = 0, seconds = 0; + if (sscanf (line, "continue %u,%u", &depth, &seconds) != 2) { + ERROR (parser->base.context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + + // Create surface samples for the surface time, + // and then a depth sample at the stated depth. + unsigned int nsamples = seconds / interval; + for (unsigned int i = 0; i < nsamples; ++i) { + time += interval; + sample.time = time; + if (callback) callback (DC_SAMPLE_TIME, sample, userdata); + + sample.depth = 0; + if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata); + } + + time += interval; + sample.time = time; + if (callback) callback (DC_SAMPLE_TIME, sample, userdata); + + sample.depth = depth / 100.0; + if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata); + } else if (strncmp(line, "enddive", 7) == 0) { + if (sscanf(line, "enddive %u,%u", &maxdepth, &divetime) != 2) { + ERROR (parser->base.context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + } else if (strncmp (line, "endlog", 6) == 0) { + // Nothing to do. + } else { + unsigned int depth = 0, events = 0; + int temperature = 0; + if (sscanf (line, "%u,%d,%u", &depth, &temperature, &events) != 3) { + ERROR (parser->base.context, "Failed to parse the line '%s'.", line); + status = DC_STATUS_DATAFORMAT; + goto error_free; + } + + time += interval; + sample.time = time; + if (callback) callback (DC_SAMPLE_TIME, sample, userdata); + + sample.depth = depth / 100.0; + if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata); + + sample.temperature = temperature; + if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata); + + if (events & EVENT_DECO_STOP) { + sample.deco.type = DC_DECO_DECOSTOP; + } else if (events & EVENT_SAFETY_STOP) { + sample.deco.type = DC_DECO_SAFETYSTOP; + } else { + sample.deco.type = DC_DECO_NDL; + } + sample.deco.depth = 0.0; + sample.deco.time = 0; + if (callback) callback (DC_SAMPLE_DECO, sample, userdata); + } + } + + // Cache the data for later use. + parser->timestamp = timestamp; + parser->number = number; + parser->divemode = divemode; + parser->oxygen = oxygen; + parser->maxdepth = maxdepth; + parser->divetime = divetime; + parser->cached = 1; + +error_free: + free (ptr); + return status; +} diff --git a/src/parser.c b/src/parser.c index 2016d05..961f59e 100644 --- a/src/parser.c +++ b/src/parser.c @@ -62,6 +62,7 @@ #include "deepsix_excursion.h" #include "seac_screen.h" #include "deepblu_cosmiq.h" +#include "oceans_s1.h" #include "context-private.h" #include "parser-private.h" @@ -195,6 +196,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_DEEPBLU_COSMIQ: rc = deepblu_cosmiq_parser_create (&parser, context); break; + case DC_FAMILY_OCEANS_S1: + rc = oceans_s1_parser_create (&parser, context); + break; default: return DC_STATUS_INVALIDARGS; } diff --git a/src/platform.h b/src/platform.h index a652503..e4a5cd5 100644 --- a/src/platform.h +++ b/src/platform.h @@ -36,8 +36,10 @@ extern "C" { #ifdef _WIN32 #define DC_PRINTF_SIZE "%Iu" +#define DC_FORMAT_INT64 "%I64d" #else #define DC_PRINTF_SIZE "%zu" +#define DC_FORMAT_INT64 "%lld" #endif #ifdef _MSC_VER