Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/libdivecomputer/suunto_nautic.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ suunto_nautic_device_download (dc_device_t *device, const char *logbook_id, dc_b
* Whiteboard-encoded as [type:u16 LE][value...]. wait_notification returns the
* next pushed value on any subscribed handle, or DC_STATUS_TIMEOUT after
* `timeout` milliseconds. Not valid while a dive download is in progress.
* unsubscribe also releases the handle when it is a per-id slot.
*/
dc_status_t
suunto_nautic_device_subscribe (dc_device_t *device, const char *path, unsigned char handle[3], dc_buffer_t *value);
Expand Down
37 changes: 31 additions & 6 deletions src/suunto_nautic.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include "suunto_nautic.h"
#include "context-private.h"
Expand Down Expand Up @@ -58,11 +59,14 @@

// A GET of a per-id path (/Logbook/byId/<id>/...) binds the id to one of the
// resource's handle slots, [00|10 24 <resource>], until a RELEASE frees it; a
// second GET while the 00 slot is still bound gets the 10 slot. Static paths
// answer with an F0 handle and the unknown-path handle is FF FF FF; neither is
// released.
#define RPC_HANDLE_STATIC 0xF0
#define RPC_HANDLE_UNKNOWN 0xFF
// second GET while the 00 slot is still bound gets the 10 slot. Static paths,
// the value resources included (/Logbook/UnsynchronisedLogs = F0 24 0D,
// /Sync/BusyState = F0 4C 00 in the Suunto app captures), answer with an F0
// handle and the unknown-path handle is FF FF FF; neither is released.
#define RPC_HANDLE_SLOT 0x00
#define RPC_HANDLE_SLOT_ALT 0x10
#define RPC_HANDLE_STATIC 0xF0
#define RPC_HANDLE_UNKNOWN 0xFF

// Fetch parameter types (Whiteboard type ids): the paged /Summary offset is an
// int32, /Logbook/Entries' StartAfterId a uint32.
Expand Down Expand Up @@ -139,6 +143,11 @@

static const unsigned char SBEM_MAGIC[8] = {'S','B','E','M','0','1','0','3'};

// Message ids carry on from the previous connection instead of restarting at
// 1, as the Suunto app's do: after a dropped link the watch can keep the old
// session, and a request reusing one of its recent ids goes unanswered.
static unsigned int g_suunto_nautic_next_msgid = 0;

typedef struct suunto_nautic_device_t {
dc_device_t base;
dc_iostream_t *iostream; // HDLC-framed
Expand Down Expand Up @@ -439,7 +448,11 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream
return DC_STATUS_NOMEMORY;
}

device->sequence = 1;
if (g_suunto_nautic_next_msgid == 0)
g_suunto_nautic_next_msgid = (unsigned int) time (NULL) * 2654435761u;
device->sequence = g_suunto_nautic_next_msgid & 0xFFFF;
if (device->sequence == 0)
device->sequence = 1;
memset (device->fingerprint, 0, sizeof (device->fingerprint));

status = dc_hdlc_open (&device->iostream, context, iostream, 244, 244);
Expand Down Expand Up @@ -494,6 +507,8 @@ suunto_nautic_device_close (dc_device_t *abstract)
{
suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract;

g_suunto_nautic_next_msgid = device->sequence | 0x10000;

return dc_iostream_close (device->iostream);
}

Expand Down Expand Up @@ -787,6 +802,14 @@ suunto_nautic_get_handle (dc_device_t *abstract, const char *path, unsigned char
memcpy (handle, ack_data + RPC_HANDLE_OFFSET, 3);
dc_buffer_free (ack);

// Every handle is released before the next GET, so a still-bound 00 slot
// was left by a link that dropped mid-transfer; free it.
if (handle[0] == RPC_HANDLE_SLOT_ALT) {
const unsigned char stale[3] = {RPC_HANDLE_SLOT, handle[1], handle[2]};
WARNING (abstract->context, "%s resolved to the 10 slot; releasing the stale 00 slot.", path);
suunto_nautic_release_handle (abstract, stale);
}

return DC_STATUS_SUCCESS;
}

Expand Down Expand Up @@ -1492,6 +1515,8 @@ suunto_nautic_device_unsubscribe (dc_device_t *abstract, const unsigned char han

dc_status_t status = suunto_nautic_handle_request (abstract, RPC_OP_UNSUBSCRIBE, RPC_OP_UNSUBSCRIBE_REPLY, handle, reply);
dc_buffer_free (reply);
if (status == DC_STATUS_SUCCESS)
suunto_nautic_release_handle (abstract, handle);
return status;
}

Expand Down
58 changes: 54 additions & 4 deletions src/suunto_nautic_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>

#include "suunto_nautic.h"
#include "context-private.h"
Expand Down Expand Up @@ -139,6 +140,11 @@
#define SUMMARY_GAS_O2 1 // uint8, %
#define SUMMARY_GAS_HE 2 // uint8, %
#define SUMMARY_GAS_VOLUME 9 // float32 LE, m^3
// The dive record, the last /Summary record long enough to hold these, carries
// the watch's own dive time and maximum depth (the app's DiveTime and
// Depth.Max), at the same payload offsets on 2040, 2056 and 2342 B summaries.
#define SUMMARY_DIVE_DIVETIME 321 // float32 LE, s, fractional
#define SUMMARY_DIVE_MAXDEPTH 330 // float32 LE, m

typedef struct suunto_nautic_tank_t {
unsigned int used;
Expand Down Expand Up @@ -300,6 +306,32 @@ suunto_nautic_find_summary (const unsigned char *data, size_t size)
return size;
}

// The /Summary's dive record: its records are plain [id][len] TLVs (the
// profile's fixed-length table doesn't apply), ending in trailing bytes that
// don't form a whole record.
static const unsigned char *
suunto_nautic_summary_dive_record (const unsigned char *sbem, size_t size)
{
const unsigned char *record = NULL;
size_t pos = SBEM_MAGIC_SIZE;
while (pos + 2 <= size) {
size_t length = sbem[pos + 1];
size_t header = 2;
if (length == 255) {
if (pos + 6 > size)
break;
length = array_uint32_le (sbem + pos + 2);
header = 6;
}
if (length > size - pos - header)
break;
if (length >= SUMMARY_DIVE_MAXDEPTH + 4)
record = sbem + pos + header;
pos += header + length;
}
return record;
}

// Parse gradient factors and gas mixes from the /Summary section, whose
// "SBEM0103" signature is at `sbem` (length `size`). Offsets are relative
// to that signature (confirmed on real hardware). Gases are validated by
Expand Down Expand Up @@ -845,10 +877,11 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback

// Dive time = total time in the Diving state (seconds). Fall back to the
// full elapsed time if no DiveState markers were seen.
// Truncated, as the app shows it: 4680.5 s is 78:00.
if (total_dive_ms > 0)
parser->divetime = (unsigned int) ((total_dive_ms + 500) / 1000);
parser->divetime = (unsigned int) (total_dive_ms / 1000);
else
parser->divetime = (unsigned int) ((time_ms + 500) / 1000);
parser->divetime = time_ms > 0 ? (unsigned int) (time_ms / 1000) : 0;
parser->maxdepth = maxdepth;
parser->avgdepth = depth_count ? depth_sum / depth_count : 0.0;
parser->have_temperature = have_temperature;
Expand All @@ -871,8 +904,25 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
memset (parser->gasvolume, 0, sizeof (parser->gasvolume));
memset (&parser->decomodel, 0, sizeof (parser->decomodel));
if (profile_size < abstract->size) {
suunto_nautic_parse_summary (parser, abstract->data + profile_size,
abstract->size - profile_size);
const unsigned char *sbem = abstract->data + profile_size;
size_t sbem_size = abstract->size - profile_size;
suunto_nautic_parse_summary (parser, sbem, sbem_size);

// The watch's maximum is deeper than any logged sample (the app shows
// it). Trusted only on a record whose dive time agrees with the
// profile's, and when it is close to the sampled maximum.
const unsigned char *record = suunto_nautic_summary_dive_record (sbem, sbem_size);
if (record && parser->divetime > 0) {
double divetime = array_float_le (record + SUMMARY_DIVE_DIVETIME);
double depth = array_float_le (record + SUMMARY_DIVE_MAXDEPTH);
double computed = parser->divetime;
double tolerance = computed * 0.1 > 30.0 ? computed * 0.1 : 30.0;
if (divetime > 0.0 && divetime < 86400.0 && fabs (divetime - computed) <= tolerance) {
parser->divetime = (unsigned int) divetime;
if (maxdepth > 0.0 && depth >= maxdepth - 0.5 && depth <= maxdepth + 3.0)
parser->maxdepth = depth;
}
}
}

parser->cached = 1;
Expand Down
Loading