diff --git a/include/libdivecomputer/suunto_nautic.h b/include/libdivecomputer/suunto_nautic.h index 6ddee9d..005b704 100644 --- a/include/libdivecomputer/suunto_nautic.h +++ b/include/libdivecomputer/suunto_nautic.h @@ -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); diff --git a/src/suunto_nautic.c b/src/suunto_nautic.c index e8e7e12..278ee9f 100644 --- a/src/suunto_nautic.c +++ b/src/suunto_nautic.c @@ -22,6 +22,7 @@ #include #include #include +#include #include "suunto_nautic.h" #include "context-private.h" @@ -58,11 +59,14 @@ // A GET of a per-id path (/Logbook/byId//...) binds the id to one of the // resource's handle slots, [00|10 24 ], 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. @@ -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 @@ -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); @@ -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); } @@ -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; } @@ -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; } diff --git a/src/suunto_nautic_parser.c b/src/suunto_nautic_parser.c index 1c73053..8b44cfa 100644 --- a/src/suunto_nautic_parser.c +++ b/src/suunto_nautic_parser.c @@ -51,6 +51,7 @@ #include #include #include +#include #include "suunto_nautic.h" #include "context-private.h" @@ -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; @@ -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 @@ -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; @@ -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;