diff --git a/examples/common.c b/examples/common.c index d8b4d0d..a4927cd 100644 --- a/examples/common.c +++ b/examples/common.c @@ -93,6 +93,7 @@ static const backend_table_t g_backends[] = { {"descentmk1", DC_FAMILY_GARMIN, 0}, {"cosmiq", DC_FAMILY_DEEPBLU, 0}, {"mclean", DC_FAMILY_MCLEAN_EXTREME, 0}, + {"oceans", 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 d09cd47..065925e 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -114,6 +114,8 @@ typedef enum dc_family_t { DC_FAMILY_GARMIN = (17 << 16), /* Deepblu */ DC_FAMILY_DEEPBLU = (18 << 16), + /* Oceans S1 */ + DC_FAMILY_OCEANS_S1 = (19 << 16), } dc_family_t; #ifdef __cplusplus diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index daac0ef..91f015d 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -530,6 +530,14 @@ RelativePath="..\src\mclean_extreme_parser.c" > + + + + @@ -892,6 +900,10 @@ RelativePath="..\src\mclean_extreme.h" > + + diff --git a/src/Makefile.am b/src/Makefile.am index 8c6ec5e..37da7e8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -76,6 +76,7 @@ libdivecomputer_la_SOURCES = \ garmin.h garmin.c garmin_parser.c \ deepblu.h deepblu.c deepblu_parser.c \ mclean_extreme.h mclean_extreme.c mclean_extreme_parser.c \ + oceans_s1.h oceans_s1.c oceans_s1_parser.c \ socket.h socket.c \ irda.c \ usbhid.c \ diff --git a/src/descriptor.c b/src/descriptor.c index 3cec2fe..bd4458d 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -51,6 +51,7 @@ static int dc_filter_divesystem (dc_transport_t transport, const void *userdata) static int dc_filter_oceanic (dc_transport_t transport, const void *userdata); static int dc_filter_deepblu (dc_transport_t transport, const void *userdata); static int dc_filter_mclean (dc_transport_t transport, const void *userdata); +static int dc_filter_oceans(dc_transport_t transport, const void *userdata); static dc_status_t dc_descriptor_iterator_next (dc_iterator_t *iterator, void *item); @@ -385,6 +386,8 @@ static const dc_descriptor_t g_descriptors[] = { {"Deepblu", "Cosmiq+", DC_FAMILY_DEEPBLU, 0, DC_TRANSPORT_BLE, dc_filter_deepblu}, /* McLean Extreme */ { "McLean", "Extreme", DC_FAMILY_MCLEAN_EXTREME, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_mclean}, + /* Oceans S1 */ + { "Oceans", "S1", DC_FAMILY_OCEANS_S1, 0, DC_TRANSPORT_BLE, dc_filter_oceans }, }; static int @@ -679,6 +682,19 @@ static int dc_filter_mclean(dc_transport_t transport, const void *userdata) return 1; } +static int dc_filter_oceans(dc_transport_t transport, const void* userdata) +{ + static const char* const ble[] = { + "S1", + }; + + if (transport == DC_TRANSPORT_BLE) { + return DC_FILTER_INTERNAL(userdata, ble, 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 0a3d4e4..17cc4ed 100644 --- a/src/device.c +++ b/src/device.c @@ -60,6 +60,7 @@ #include "garmin.h" #include "deepblu.h" #include "mclean_extreme.h" +#include "oceans_s1.h" #include "device-private.h" #include "context-private.h" @@ -223,6 +224,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_MCLEAN_EXTREME: rc = mclean_extreme_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/field-cache.c b/src/field-cache.c index 890320a..ddb706a 100644 --- a/src/field-cache.c +++ b/src/field-cache.c @@ -62,3 +62,41 @@ dc_status_t dc_field_get_string(dc_field_cache_t *cache, unsigned idx, dc_field_ } return DC_STATUS_UNSUPPORTED; } + + +/* + * Use this generic "pick fields from the field cache" helper + * after you've handled all the ones you do differently + */ +dc_status_t +dc_field_get(dc_field_cache_t *cache, dc_field_type_t type, unsigned int flags, void* value) +{ + if (!(cache->initialized & (1 << type))) + return DC_STATUS_UNSUPPORTED; + + switch (type) { + case DC_FIELD_DIVETIME: + return DC_FIELD_VALUE(*cache, value, DIVETIME); + case DC_FIELD_MAXDEPTH: + return DC_FIELD_VALUE(*cache, value, MAXDEPTH); + case DC_FIELD_AVGDEPTH: + return DC_FIELD_VALUE(*cache, value, AVGDEPTH); + case DC_FIELD_GASMIX_COUNT: + case DC_FIELD_TANK_COUNT: + return DC_FIELD_VALUE(*cache, value, GASMIX_COUNT); + case DC_FIELD_GASMIX: + if (flags >= MAXGASES) + break; + return DC_FIELD_INDEX(*cache, value, GASMIX, flags); + case DC_FIELD_SALINITY: + return DC_FIELD_VALUE(*cache, value, SALINITY); + case DC_FIELD_DIVEMODE: + return DC_FIELD_VALUE(*cache, value, DIVEMODE); + case DC_FIELD_STRING: + return dc_field_get_string(cache, flags, (dc_field_string_t *)value); + default: + break; + } + + return DC_STATUS_UNSUPPORTED; +} diff --git a/src/field-cache.h b/src/field-cache.h index ec8a7f4..6f5c487 100644 --- a/src/field-cache.h +++ b/src/field-cache.h @@ -36,6 +36,7 @@ typedef struct dc_field_cache { dc_status_t dc_field_add_string(dc_field_cache_t *, const char *desc, const char *data); dc_status_t dc_field_add_string_fmt(dc_field_cache_t *, const char *desc, const char *fmt, ...); dc_status_t dc_field_get_string(dc_field_cache_t *, unsigned idx, dc_field_string_t *value); +dc_status_t dc_field_get(dc_field_cache_t *, dc_field_type_t, unsigned int, void *); /* * Macro to make it easy to set DC_FIELD_xyz values. diff --git a/src/oceans_s1.c b/src/oceans_s1.c new file mode 100644 index 0000000..e70e6ee --- /dev/null +++ b/src/oceans_s1.c @@ -0,0 +1,649 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2020 Linus Torvalds + +#include // memcmp, memcpy +#include // malloc, free +#include +#include +#include +#include + +#include "oceans_s1.h" +#include "context-private.h" +#include "device-private.h" +#include "array.h" + +#define S1_FINGERPRINT 32 + +typedef struct oceans_s1_device_t { + dc_device_t base; + dc_iostream_t* iostream; + unsigned char fingerprint[S1_FINGERPRINT]; +} 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_close(dc_device_t *abstract); +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 */ + oceans_s1_device_close, /* close */ +}; + +static dc_status_t +oceans_s1_write(oceans_s1_device_t *s1, const char *msg) +{ + return dc_iostream_write(s1->iostream, msg, strlen(msg), NULL); +} + +static dc_status_t +oceans_s1_read(oceans_s1_device_t *s1, char *buf, size_t bufsz) +{ + size_t nbytes; + dc_status_t status; + + status = dc_iostream_read(s1->iostream, buf, bufsz, &nbytes); + if (status != DC_STATUS_SUCCESS) + return status; + if (nbytes < bufsz) + buf[nbytes] = 0; + return status; +} + +#define BUFSZ 64 + +// Note how we don't rely on the return value of 'vsnprintf(), or on +// NUL termination because it's not portable. +static dc_status_t oceans_s1_printf(oceans_s1_device_t *s1, const char *fmt, ...) +{ + va_list ap; + char buffer[BUFSZ]; + + va_start(ap, fmt); + vsnprintf(buffer, BUFSZ, fmt, ap); + va_end(ap); + buffer[BUFSZ-1] = 0; + + return oceans_s1_write(s1, buffer); +} + +static dc_status_t oceans_s1_expect(oceans_s1_device_t *s1, const char *result) +{ + char buffer[BUFSZ]; + dc_status_t status; + + status = oceans_s1_read(s1, buffer, BUFSZ); + if (status != DC_STATUS_SUCCESS) + return status; + + if (strncmp(buffer, result, strlen(result))) { + ERROR(s1->base.context, "Expected '%s' got '%s'", result, buffer); + return DC_STATUS_IO; + } + + return DC_STATUS_SUCCESS; +} + +/* + * The "blob mode" is sends stuff in bigger chunks with some binary + * header and trailer. + * + * It seems to be a sequence of packets with 517 bytes of payload: + * three bytes of header, 512 bytes of ASCII data, and two bytes of + * trailer (data checksum?). + * + * We're supposed to start the sequence with a 'C' packet, and reply + * to each 517-byte packet sequence with a '\006' packet. + * + * When there is no more data, the S1 will send us a '\004' packet, + * which we'll ack with a final '\006' packet. + * + * The header is '\001' followed by block number (starting at 1), + * followed by (255-block) number. So we can 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 that '\006' packet for each of those + * entries. + * + * NOTE! The above is not in single BLE packets, although the + * sequence blocks always start at a packet boundary. + */ +#define BLOB_BUFSZ 256 +static dc_status_t oceans_s1_get_sequence(oceans_s1_device_t *s1, unsigned char seq, dc_buffer_t *res) +{ + unsigned char buffer[BLOB_BUFSZ]; + dc_status_t status; + size_t nbytes; + + status = dc_iostream_read(s1->iostream, buffer, BLOB_BUFSZ, &nbytes); + if (status != DC_STATUS_SUCCESS) + return status; + if (!nbytes) + return DC_STATUS_IO; + + if (buffer[0] == 4) + return DC_STATUS_DONE; + + if (nbytes <= 3 || buffer[0] != 1) + return DC_STATUS_IO; + + if (buffer[1] != seq || buffer[2]+seq != 255) + return DC_STATUS_IO; + + nbytes -= 3; + dc_buffer_append(res, buffer+3, nbytes); + while (nbytes < 512) { + size_t got; + + status = dc_iostream_read(s1->iostream, buffer, BLOB_BUFSZ, &got); + if (status != DC_STATUS_SUCCESS) + return status; + + if (!got) + return DC_STATUS_IO; + + // We should check the checksum if it is that? + if (got + nbytes > 512) + got = 512-nbytes; + dc_buffer_append(res, buffer, got); + nbytes += got; + } + return DC_STATUS_SUCCESS; +} + +static dc_status_t oceans_s1_get_blob(oceans_s1_device_t *s1, const unsigned char **result) +{ + dc_status_t status; + dc_buffer_t *res; + unsigned char *data; + size_t size; + unsigned char seq; + + res = dc_buffer_new(0); + if (!res) + return DC_STATUS_NOMEMORY; + + // Tell the Oceans S1 to into some kind of block mode.. + // + // The Oceans Android app uses a "Write Command" rather than + // a "Write Request" for this, but it seems to not matter + + status = dc_iostream_write(s1->iostream, "C", 1, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + + seq = 1; + for (;;) { + status = oceans_s1_get_sequence(s1, seq, res); + if (status == DC_STATUS_DONE) + break; + + if (status != DC_STATUS_SUCCESS) { + dc_buffer_free(res); + return status; + } + + // Ack the packet sequence, and go look for the next one + status = dc_iostream_write(s1->iostream, "\006", 1, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + seq++; + } + + + + // Tell the Oceans S1 to exit block mode (??) + status = dc_iostream_write(s1->iostream, "\006", 1, NULL); + if (status != DC_STATUS_SUCCESS) + return status; + + size = dc_buffer_get_size(res); + + // NUL-terminate before getting buffer + dc_buffer_append(res, "", 1); + data = dc_buffer_get_data(res); + + /* Remove trailing whitespace */ + while (size && isspace(data[size-1])) + data[--size] = 0; + + *result = data; + return DC_STATUS_SUCCESS; +} + +/* + * The Oceans S1 uses the normal UNIX epoch time format: seconds + * since 1-1-1970. In UTC format (so converting local time to UTC). + */ +static dc_status_t oceans_s1_device_timesync(dc_device_t *abstract, const dc_datetime_t *datetime) +{ + oceans_s1_device_t *s1 = (oceans_s1_device_t *) abstract; + dc_ticks_t timestamp; + dc_status_t status; + + timestamp = dc_datetime_mktime(datetime); + if (timestamp < 0) + return DC_STATUS_INVALIDARGS; + + timestamp += datetime->timezone; + + status = oceans_s1_printf(s1, "utc %lld\n", (long long) timestamp); + if (status != DC_STATUS_SUCCESS) + return status; + + return oceans_s1_expect(s1, "utc>ok"); +} + + +/* + * Oceans S1 initial sequence (all ASCII text with newlines): + * + * Cmd Reply Comments + * + * "utc" "utc>ok 1592912375" // TZ=UTC date -d"@1592912375" + * "battery" "battery>ok 59%" + * "version" "version>ok 1.1 42a7e564" // Odd hex contant. Device ID? + * "utc 1592912364" "utc>ok" TZ=UTC date -d"@1592912364" + * "units 0" "units>ok" + * "name TGludXM=" "name>ok" // WTF? + * "dllist" "dllist>xmr" + * + * At this point, the sequence changed and is no longer single packets + * with a full line with newline termination. + * + * We send a single 'C' character as a GATT "Write Command" - 0x53 (so + * not "Write Request" - 0x12). + * + * The dive computer replies with GATT packets that contains: + * + * - binary three bytes: "\x01\x01\xfe" + * + * - followed by ASCII text blob (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 + * + * Followed by a lot of newlines to pad out the packets. + * + * NOTE! The newlines are probably because the way the Nordic Semi UART + * buffering works: it will buffer the packets until they are full, or + * until a newline. + * + * Then some odd data: write a single '\x06' character and get a single + * character reply of '\x04' (!?). Repeat, get a '\x13' byte back. + * + * NOTE! Again these single-byte things are GATT "write command", not + * GATT "write request" things. They may be commands to the UART, not + * data. Some kind of flow control? Or UART buffer control? + * + * Then it seems to go back to line-mode with the usual Write Request: + * + * "dlget 4 5" "dlget>xmr" + * + * which puts us in that "blob" mode again, and we send a singler 'C' + * character again, and now get that same '\x01\x01\xfe' binary data + * followed by ASCII text blob (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' (??) + * - 'hex value flags' (??) + * + * Repeat with 'dlget 3 4', 'dlget 2 3', 'dlget 1 2'. + * + * Done. + */ +dc_status_t +oceans_s1_device_open(dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream) +{ + char buffer[128]; + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *s1 = NULL; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + s1 = (oceans_s1_device_t*)dc_device_allocate(context, &oceans_s1_device_vtable); + if (s1 == NULL) { + ERROR(context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } + + // Set the default values. + s1->iostream = iostream; + memset(s1->fingerprint, 0, sizeof(s1->fingerprint)); + + *out = (dc_device_t*)s1; + + // Do minimal verification that we can talk to it + // as part of the open. + status = oceans_s1_write(s1, "utc\n"); + if (status != DC_STATUS_SUCCESS) + return status; + + status = oceans_s1_read(s1, buffer, sizeof(buffer)); + if (status != DC_STATUS_SUCCESS) + return status; + if (memcmp(buffer, "utc>ok", 6)) + return DC_STATUS_IO; + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_device_close(dc_device_t *abstract) +{ + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *s1 = (oceans_s1_device_t*)abstract; + + // Fill in + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_device_set_fingerprint(dc_device_t *abstract, const unsigned char data[], unsigned int size) +{ + oceans_s1_device_t *s1 = (oceans_s1_device_t*)abstract; + + if (size > sizeof(s1->fingerprint)) + return DC_STATUS_INVALIDARGS; + + memset(s1->fingerprint, 0, sizeof(s1->fingerprint)); + memcpy(s1->fingerprint, data, size); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t +get_dive_list(oceans_s1_device_t *s1, const unsigned char **list) +{ + dc_status_t status; + + status = oceans_s1_write(s1, "dllist\n"); + if (status != DC_STATUS_SUCCESS) + return status; + + status = oceans_s1_expect(s1, "dllist>xmr"); + if (status != DC_STATUS_SUCCESS) + return status; + + return oceans_s1_get_blob(s1, list); +} + +static dc_status_t +get_one_dive(oceans_s1_device_t *s1, int nr, const unsigned char **dive) +{ + dc_status_t status; + + status = oceans_s1_printf(s1, "dlget %d %d\n", nr, nr+1); + if (status != DC_STATUS_SUCCESS) + return status; + + status = oceans_s1_expect(s1, "dlget>xmr"); + if (status != DC_STATUS_SUCCESS) + return status; + + return oceans_s1_get_blob(s1, dive); +} + +static const unsigned char *get_string_line(const unsigned char **blob) +{ + const unsigned char *in = *blob; + const unsigned char *line; + unsigned char c; + + *blob = NULL; + if (!in) + return NULL; + + while (isspace(*in)) + in++; + + if (!*in) + return NULL; + + line = in; + while ((c = *in) != 0) { + if (c == '\r' || c == '\n') + break; + in++; + } + *blob = in; + return line; +} + +// The 'unknown' field is probably the dive mode +// 'date' is seconds since UNIX epoch (the usual "local time as GMT") +// depth and duration are in cm and seconds +// The fingerprint is just the 'dive' line padded with NUL characters +struct s1_dive { + int nr, unknown, o2; + long long date; + unsigned maxdepth, duration; + unsigned char fingerprint[S1_FINGERPRINT]; + struct s1_dive *next; +}; + +/* + * React to the "dive x,y,z,date" line. + * + * Allocate the dive. + */ +static struct s1_dive * +s1_alloc_dive(const unsigned char *line, size_t len) +{ + struct s1_dive *dive; + int nr, unknown, o2; + long long date; + + if (sscanf(line, "dive %d,%d,%d,%lld", &nr, &unknown, &o2, &date) != 4) + return NULL; + + dive = malloc(sizeof(*dive)); + if (dive) { + memset(dive, 0, sizeof(*dive)); + + dive->nr = nr; + dive->unknown = unknown; + dive->o2 = o2; + dive->date = date; + dive->next = NULL; + + if (len >= S1_FINGERPRINT) + len = S1_FINGERPRINT-1; + memcpy(dive->fingerprint, line, len); + } + return dive; +} + +/* + * React to the "enddive x,y" line + * + * 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. + * + * If it always comes sorted from the Oceans S1, the while () loop + * here won't ever actually loop, so there's no real cost to this + * (not that CPU time here matters). + */ +static int +s1_add_dive(struct s1_dive *dive, struct s1_dive **list, const unsigned char *line, size_t len) +{ + unsigned maxdepth, duration; + struct s1_dive *next; + + if (!dive) + return 0; + + if (sscanf(line, "enddive %u,%u", &maxdepth, &duration) != 2) + return 0; + + dive->maxdepth = maxdepth; + dive->duration = duration; + while ((next = *list) != NULL) { + if (dive->nr >= next->nr) + break; + list = &next->next; + } + dive->next = next; + *list = dive; + return 1; +} + +static dc_status_t +oceans_s1_device_foreach(dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + int nr; + struct s1_dive *divelist, *current_dive; + const unsigned char *blob, *line; + dc_status_t status = DC_STATUS_SUCCESS; + oceans_s1_device_t *s1 = (oceans_s1_device_t*)abstract; + + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + status = get_dive_list(s1, &blob); + if (status != DC_STATUS_SUCCESS) + return status; + + nr = 0; + divelist = NULL; + current_dive = NULL; + while ((line = get_string_line(&blob)) != NULL) { + int linelen = blob - line; + const unsigned char *dive; + + /* We only care about 'dive' and 'enddive' lines */ + if (linelen < 8) + continue; + + if (!memcmp(line, "dive ", 5)) { + current_dive = s1_alloc_dive(line, linelen); + continue; + } + + if (memcmp(line, "enddive ", 8)) + continue; + + if (s1_add_dive(current_dive, &divelist, line, linelen)) + nr++; + current_dive = NULL; + } + if (!nr) + return DC_STATUS_SUCCESS; + + progress.current = 0; + progress.maximum = nr; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + for (current_dive = divelist; current_dive; current_dive = current_dive->next) { + const unsigned char *blob; + + if (!memcmp(current_dive->fingerprint, s1->fingerprint, S1_FINGERPRINT)) + break; + + if (device_is_cancelled(&s1->base)) + break; + + status = get_one_dive(s1, current_dive->nr, &blob); + if (status != DC_STATUS_SUCCESS) + return status; + + progress.current++; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + if (callback && !callback(blob, strlen(blob), current_dive->fingerprint, S1_FINGERPRINT, userdata)) + break; + } + + return status; +} diff --git a/src/oceans_s1.h b/src/oceans_s1.h new file mode 100644 index 0000000..483b144 --- /dev/null +++ b/src/oceans_s1.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2020 Linus Torvalds + +#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_parser.c b/src/oceans_s1_parser.c new file mode 100644 index 0000000..b2e012f --- /dev/null +++ b/src/oceans_s1_parser.c @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2020 Linus Torvalds + +#include +#include +#include +#include +#include + +#include "oceans_s1.h" +#include "context-private.h" +#include "parser-private.h" +#include "field-cache.h" +#include "array.h" + +typedef struct oceans_s1_parser_t oceans_s1_parser_t; + +struct oceans_s1_parser_t { + dc_parser_t base; + int divenr; + unsigned int maxdepth, duration; + long long date; + struct dc_field_cache cache; +}; + +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 */ + 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; + } + + *out = (dc_parser_t*)parser; + + return DC_STATUS_SUCCESS; +} + +static const unsigned char *get_string_line(const unsigned char *in, const unsigned char **next) +{ + const unsigned char *line; + unsigned char c; + + if (!in) { + *next = NULL; + return NULL; + } + + while (isspace(*in)) + in++; + + if (!*in) { + *next = NULL; + return NULL; + } + + line = in; + while ((c = *in) != 0) { + if (c == '\r' || c == '\n') + break; + in++; + } + *next = in; + return line; +} + +static dc_status_t +oceans_s1_parse_dive(struct oceans_s1_parser_t *s1, const unsigned char *data, dc_sample_callback_t callback, void *userdata) +{ + const unsigned char *line; + unsigned int sample_interval = 10; + unsigned int sample_time = 0; + + memset(&s1->cache, 0, sizeof(s1->cache)); + + while ((line = get_string_line(data, &data)) != NULL) { + dc_sample_value_t sample = {0}; + int depth = 0, temp = 0, flags = 0; + + if (!strncmp(line, "divelog ", 8)) { + sscanf(line, "divelog v1,%us/sample", &sample_interval); + continue; + } + if (!strncmp(line, "dive ", 5)) { + int nr, unknown, o2; + long long date; + + sscanf(line, "dive %d,%d,%d,%lld", &nr, &unknown, &o2, &date); + s1->divenr = nr; + s1->date = date; + // I think "unknown" is dive mode + if (o2) { + dc_gasmix_t mix = { 0 }; + mix.oxygen = o2 / 100.0; + DC_ASSIGN_FIELD(s1->cache, GASMIX_COUNT, 1); + DC_ASSIGN_IDX(s1->cache, GASMIX, 0, mix); + } + continue; + } + if (!strncmp(line, "continue ", 9)) { + int depth = 0, seconds = 0; + sscanf(line, "continue %d,%d", &depth, &seconds); + + // Create surface samples for the surface time, + // and then a depth sample at the stated depth + if (callback) { + if (seconds >= sample_interval*2) { + dc_sample_value_t sample = {0}; + sample.time = sample_time + sample_interval; + callback(DC_SAMPLE_TIME, sample, userdata); + sample.depth = 0; + callback(DC_SAMPLE_DEPTH, sample, userdata); + + sample.time = sample_time + seconds - sample_interval; + callback(DC_SAMPLE_TIME, sample, userdata); + sample.depth = 0; + callback(DC_SAMPLE_DEPTH, sample, userdata); + } + sample.time = sample_time + seconds; + callback(DC_SAMPLE_TIME, sample, userdata); + sample.depth = depth / 100.0; + callback(DC_SAMPLE_DEPTH, sample, userdata); + } + sample_time += seconds; + continue; + } + if (!strncmp(line, "enddive ", 8)) { + int maxdepth = 0, duration = 0; + sscanf(line, "enddive %d,%d", &maxdepth, &duration); + DC_ASSIGN_FIELD(s1->cache, MAXDEPTH, maxdepth / 100.0); + DC_ASSIGN_FIELD(s1->cache, DIVETIME, duration); + s1->maxdepth = maxdepth; + s1->duration = duration; + continue; + } + if (sscanf(line, "%d,%d,%d", &depth, &temp, &flags) != 3) + continue; + + sample_time += sample_interval; + if (callback) { + dc_sample_value_t sample = {0}; + sample.time = sample_time; + callback(DC_SAMPLE_TIME, sample, userdata); + sample.depth = depth / 100.0; + callback(DC_SAMPLE_DEPTH, sample, userdata); + sample.temperature = temp; + callback (DC_SAMPLE_TEMPERATURE, sample, userdata); + } + } + return DC_STATUS_SUCCESS; +} + +static dc_status_t +oceans_s1_parser_set_data(dc_parser_t *abstract, const unsigned char *data, unsigned int size) +{ + struct oceans_s1_parser_t *s1 = (struct oceans_s1_parser_t *)abstract; + + return oceans_s1_parse_dive(s1, data, NULL, NULL); +} + +static dc_status_t +oceans_s1_parser_get_datetime(dc_parser_t *abstract, dc_datetime_t *datetime) +{ + oceans_s1_parser_t *s1 = (oceans_s1_parser_t *)abstract; + + dc_datetime_gmtime(datetime, s1->date); + 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 *s1 = (oceans_s1_parser_t *)abstract; + + return dc_field_get(&s1->cache, type, flags, value); +} + +static dc_status_t +oceans_s1_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) +{ + struct oceans_s1_parser_t *s1 = (struct oceans_s1_parser_t *)abstract; + + return oceans_s1_parse_dive(s1, s1->base.data, callback, userdata); +} diff --git a/src/parser.c b/src/parser.c index dffb517..e76cb63 100644 --- a/src/parser.c +++ b/src/parser.c @@ -60,6 +60,7 @@ #include "garmin.h" #include "deepblu.h" #include "mclean_extreme.h" +#include "oceans_s1.h" #include "context-private.h" #include "parser-private.h" @@ -184,6 +185,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa case DC_FAMILY_MCLEAN_EXTREME: rc = mclean_extreme_parser_create (&parser, context); break; + case DC_FAMILY_OCEANS_S1: + rc = oceans_s1_parser_create(&parser, context); + break; default: return DC_STATUS_INVALIDARGS; }