Merge branch 'Oceans-S1' into Subsurface-DS9
Add initial support for the Oceans S1. This expands a bit on the generic functions for the field-cache code, and uses that to then add a fairly minimal Oceans S1 downloader. And while it's minimal, it downloads about everything the S1 offers, which is mainly just depth and temperature. There are a few fields that it currently doesn't use, notably the events and NDL information that the dive computer presumably reports in the auxiliary data that comes in the sample, but without documentation and more testing I'm not comfortable parsing that. There's also some "current dive computer state" that isn't imported, like the battery status. I know how to read it, but it's not per-dive data that could be added as extra fields: it's literally just the current dive computer battery state at the time of the download. The Oceans team said they'll provide more information about the download, so this might be expanded in the future, but it seems fairly usable even in this form. Thanks to Dhaval Giani for sending me his Oceans S1 as a loaner, and to Seth Garrison for doing the initial BLE packet dumps that made me think it was fairly easily doable. * Oceans-S1: Oceans S1: polish up the downloading logic for usability Oceans S1: actually download all dives and parse them Oceans S1: fill out core download protocol details Oceans S1: start filling in protocol details Oceans S1: start documenting the download format and first packets Add skeleton for Oceans S1 downloader Add generic dc_field_get() helper
This commit is contained in:
commit
6935fe717a
@ -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[] = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -530,6 +530,14 @@
|
||||
RelativePath="..\src\mclean_extreme_parser.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\oceans_s1.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\oceans_s1_parser.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\field-cache.c"
|
||||
>
|
||||
@ -892,6 +900,10 @@
|
||||
RelativePath="..\src\mclean_extreme.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\oceans_s1.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\timer.h"
|
||||
>
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
649
src/oceans_s1.c
Normal file
649
src/oceans_s1.c
Normal file
@ -0,0 +1,649 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2020 Linus Torvalds
|
||||
|
||||
#include <string.h> // memcmp, memcpy
|
||||
#include <stdlib.h> // malloc, free
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
25
src/oceans_s1.h
Normal file
25
src/oceans_s1.h
Normal file
@ -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 <libdivecomputer/context.h>
|
||||
#include <libdivecomputer/iostream.h>
|
||||
#include <libdivecomputer/device.h>
|
||||
#include <libdivecomputer/parser.h>
|
||||
|
||||
#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 */
|
||||
206
src/oceans_s1_parser.c
Normal file
206
src/oceans_s1_parser.c
Normal file
@ -0,0 +1,206 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (C) 2020 Linus Torvalds
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user