diff --git a/src/suunto_nautic.c b/src/suunto_nautic.c index f79443f..9dcb78f 100644 --- a/src/suunto_nautic.c +++ b/src/suunto_nautic.c @@ -22,9 +22,9 @@ #include #include #include -#include #include "suunto_nautic.h" +#include #include "context-private.h" #include "device-private.h" #include "platform.h" @@ -33,110 +33,89 @@ #include "hdlc.h" #include "heatshrink/heatshrink_decoder.h" +#include + // See suunto_nautic.h for a description of the transport and format. -#define RPC_OP_GET 0x0A -#define RPC_OP_RELEASE 0x0B // host -> watch: release a handle a GET resolved (answered by 0x03) -#define RPC_OP_FETCH 0x0D -#define RPC_OP_STREAM_FETCH2 0x10 -#define RPC_OP_STREAM_STOP 0x11 // host -> watch: close the stream -#define RPC_OP_STREAM_CHUNK 0x01 -#define RPC_OP_ACK 0x02 -#define RPC_OP_ACK_ALT 0x03 -#define RPC_OP_DATA 0x05 -#define RPC_OP_STREAM_START 0x08 // watch -> host: stream accepted (200) or refused (e.g. 423 Locked) -#define RPC_OP_STREAM_END 0x09 // watch -> host: the stream is closed (answers STREAM_STOP) -#define RPC_OP_HELLO_ACK 0x13 // watch -> host: answers the EVA (Hello, 0x12) handshake - -// The /Data "stream" is a Whiteboard subscription: 0x10 subscribes to a -// resource handle (answered by 0x08 with the current value), the watch then -// pushes 0x01 notifications on that handle, and 0x11 unsubscribes (answered by -// 0x09). Value resources such as /Logbook/UnsynchronisedLogs work the same way. -#define RPC_OP_SUBSCRIBE RPC_OP_STREAM_FETCH2 -#define RPC_OP_UNSUBSCRIBE RPC_OP_STREAM_STOP -#define RPC_OP_SUBSCRIBE_REPLY RPC_OP_STREAM_START -#define RPC_OP_UNSUBSCRIBE_REPLY RPC_OP_STREAM_END -#define RPC_OP_NOTIFICATION RPC_OP_STREAM_CHUNK +#define RPC_OP_NOTIFICATION 0x01 // watch -> host: one chunk of a subscribed stream +#define RPC_OP_GET_REPLY 0x02 +#define RPC_OP_RELEASE_REPLY 0x03 +#define RPC_OP_DATA 0x05 // watch -> host: answers FETCH +#define RPC_OP_SUBSCRIBE_REPLY 0x08 +#define RPC_OP_UNSUBSCRIBE_REPLY 0x09 +#define RPC_OP_GET 0x0A // host -> watch: resolve a path to a handle +#define RPC_OP_RELEASE 0x0B // host -> watch: release a handle bound by a GET +#define RPC_OP_FETCH 0x0D +#define RPC_OP_SUBSCRIBE 0x10 +#define RPC_OP_UNSUBSCRIBE 0x11 +#define RPC_OP_HELLO 0x12 +#define RPC_OP_HELLO_REPLY 0x13 + +// Packet: 0xA5 OPCODE LENGTH(2) SEQNUM(2) PAYLOAD(LENGTH) CRC32(4) +#define RPC_START 0xA5 +#define RPC_OVERHEAD 10 +#define MAX_PAYLOAD 512 +#define MAX_PATH 240 + +// HTTP-like status codes carried in the replies. +#define STATUS_OK 200 +#define STATUS_CONTINUE 100 // more pages follow // 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, -// 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. -#define RPC_PARAM_INT32 0x06 -#define RPC_PARAM_UINT32 0x07 - -#define RPC_HEADER_SIZE 10 // magic(1) + opcode(1) + sublen(2) + seq(2) + 0x01 + 0x80 + 0x00 + pathlen(1) -#define RPC_CRC_SIZE 4 - -// Offset of the 16-bit LE message id; a reply echoes its request's. -#define RPC_MSGID_OFFSET 4 -// Offset of the 3-byte session handle inside an ACK (0x02) or DATA (0x05) -// frame: magic(1) + opcode(1) + sublen(2) + msgid(2). -#define RPC_HANDLE_OFFSET 6 -// Offset of the 16-bit LE HTTP-like status inside a DATA (0x05) frame: -// magic(1) + opcode(1) + sublen(2) + msgid(2) + handle(3) + flags(3). -#define RPC_STATUS_OFFSET 12 -#define RPC_STATUS_OK 200 -#define RPC_STATUS_CONTINUE 100 // more pages follow (paginated fetch) - -#define MAX_PATH 240 -#define MAX_PACKET 512 - -// Dive IDs are UNIX timestamps. /Logbook/Entries embeds them as 4-aligned -// little-endian uint32 values in a small SBEM payload among handle/flag/ -// count/CRC fields; filtering to a plausible timestamp window (2017 .. 2036) -// isolates them. Must scan 4-aligned -- the IDs are packed adjacently, so an -// unaligned read straddling two can invent a phantom dive. -#define DIVE_ID_MIN 1500000000u -#define DIVE_ID_MAX 2100000000u - -// Each entry stores start then end timestamp adjacently; an end is always -// within a day of its start. Used to pair (start, end) so a dive's end isn't -// listed as a second dive. -#define DIVE_ENTRY_MAX_PAIR_GAP 86400u - -// Runaway guard on stream frames, not a protocol limit: the stream ends on -// inter-frame silence or STREAM_END. ~278 B per chunk, so 65536 is ~18 MB; the -// old 4096 cap (~1.14 MB) truncated the end of most real dives. -#define MAX_CHUNKS 65536 - -// How many times to issue a dive's stream when the watch refuses it (423). -#define STREAM_ATTEMPTS 3 - -// How many times to download a dive whose size doesn't match its listing. -#define DOWNLOAD_ATTEMPTS 2 - -// Each /Summary page is [header:11][data][crc:4]; the header carries the data -// length as a u16 at offset 3, and the paging offset counts data bytes only. -#define SUMMARY_PAGE_HEADER_SIZE 11 -#define SUMMARY_PAGE_LENGTH_OFFSET 3 - -// A /Summary is ~2-2.4 KB; bounds the listed-size check when it is missing. +// 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 an unknown path with FF FF FF; neither is +// released. The slot is the first byte on the wire. +#define HANDLE_SLOT(handle) ((handle) & 0xFF) +#define SLOT_DEFAULT 0x00 +#define SLOT_ALT 0x10 +#define SLOT_STATIC 0xF0 +#define SLOT_UNKNOWN 0xFF + +// Fetch parameter types: the /Summary page offset is an int32, the +// /Logbook/Entries start id a uint32. +#define PARAM_INT32 0x06 +#define PARAM_UINT32 0x07 + +// Reply payloads start with HANDLE(3) FLAGS(3) STATUS(2); a DATA reply then +// carries the content. +#define REPLY_STATUS 6 +#define REPLY_CONTENT 8 + +// Stream notification payload: +// HANDLE(3) FLAGS(3) UNKNOWN(4) OFFSET(4) LENGTH(2) UNKNOWN(6) DATA(LENGTH) +// OFFSET is the position of the chunk in the stream. The last chunk has a +// zero length, at an offset equal to the total size. +#define CHUNK_OFFSET 10 +#define CHUNK_LENGTH 14 +#define CHUNK_DATA 22 + +// A /Summary page is HEADER(11) DATA, with the data length at offset 3 of +// the header. The paging offset counts data bytes only. +#define PAGE_HEADER 11 +#define PAGE_LENGTH 3 + +// /Logbook/Entries is HEADER(18) RECORD(24) x COUNT, with the count at offset +// 6 of the header. A record is START(4) END(4) UNKNOWN(8) SIZE(4) UNKNOWN(4): +// the start time is the dive id, and the size is the compressed /Data plus +// the /Summary data bytes. An empty logbook can omit the end of the header. +#define ENTRIES_HEADER 18 +#define ENTRIES_COUNT 6 +#define ENTRY_SIZE 24 +#define ENTRY_START 0 +#define ENTRY_LENGTH 16 + +// A /Summary is about 2.3 KB; bounds the size check when it is missing. #define SUMMARY_MAX_SIZE 8192 -// A /Logbook/Entries record is [start][end][w1][w2][size][pad] (u32 LE each); -// size = compressed /Data + /Summary data bytes, exactly. -#define ENTRY_SIZE_OFFSET 16 - -// Safety cap on paginated-fetch pages (a Summary is a handful of pages, -// /Logbook/Entries one page per ~18 dives, and a dive's Descriptors schema -// about 97). -#define MAX_PAGES 512 +// How many times to issue a dive's stream when the watch refuses it with +// status 423 (Locked), and to download a dive whose size doesn't match the +// listing. +#define STREAM_ATTEMPTS 3 +#define DOWNLOAD_ATTEMPTS 2 -// The Suunto "MDS" chunk header wrapping each compressed block: 28 bytes, -// with the true payload size as a u16 LE at offset 20 and the compressed -// payload starting at offset 28. -#define MDS_HEADER_SIZE 28 -#define MDS_CHUNK_SIZE_OFFSET 20 +#define TIMEOUT_DEFAULT 5000 +#define TIMEOUT_STREAM 4000 // the gap between two chunks reaches about 2s // Heatshrink (LZSS) parameters used by the Nautic/Ocean's MDS stream. #define HEATSHRINK_WINDOW_SZ2 7 @@ -145,25 +124,30 @@ 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; +// The sequence number to continue from on the next connection. The Suunto +// app's carry on across connections too, so a reconnect never repeats one +// the watch still holds from the dropped session. +static unsigned int g_suunto_nautic_seqnum = 0; typedef struct suunto_nautic_device_t { dc_device_t base; dc_iostream_t *iostream; // HDLC-framed - unsigned int sequence; - // The dive ID (a UNIX timestamp, see suunto_nautic_device_foreach) of - // the most recently downloaded dive, little-endian, as returned via - // dc_dive_callback_t's fingerprint parameter. All-zero means "no - // fingerprint set" (a real dive ID is never 0 -- that would be a - // 1970 timestamp), matching every other driver's convention. + unsigned int seqnum; + // Whether the watch answered the hello (extension API). + int acknowledged; + // The dive id (a UNIX timestamp) of the most recently downloaded dive, + // little-endian. All-zero means no fingerprint. unsigned char fingerprint[4]; - // Whether the watch answered the EVA handshake; the open carries on without it. - int handshake_acknowledged; } suunto_nautic_device_t; +// Dive ids and their listed sizes, newest first. +typedef struct suunto_nautic_entries_t { + unsigned int *ids; + unsigned int *sizes; + unsigned int count; + unsigned int capacity; +} suunto_nautic_entries_t; + static dc_status_t suunto_nautic_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size); static dc_status_t suunto_nautic_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); static dc_status_t suunto_nautic_device_close (dc_device_t *abstract); @@ -180,6 +164,19 @@ static const dc_device_vtable_t suunto_nautic_device_vtable = { suunto_nautic_device_close, /* close */ }; +// The Hello payload: the protocol versions (9, 9), the host identity (the one +// the Suunto app sends, a packed 12-byte serial), the Whiteboard version +// (4.1.2) and capability flags. The watch answers with the same fields for +// itself. +static const unsigned char suunto_nautic_hello[] = { + 0x09, 0x09, 0x20, 0x16, 0x45, 0x56, 0x41, 0x10, + 0x04, 0x41, 0x10, 0x0C, 0x00, 0x00, 0x00, 0x04, + 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const unsigned char suunto_nautic_noparams[] = {0x00}; + static dc_status_t suunto_nautic_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size) { @@ -196,245 +193,153 @@ suunto_nautic_device_set_fingerprint (dc_device_t *abstract, const unsigned char return DC_STATUS_SUCCESS; } -/* - * The "EVA" handshake is the Whiteboard protocol's Hello message (message - * type 0x12). The payload carries a SuuntoSerial identity, a fixed - * protocol-version block, a capability-flags byte and a trailing CRC32. - * The identity has no cryptographic tie to a specific phone, and the watch - * has only been confirmed to answer the captured template, so it is sent - * verbatim. - */ -static const unsigned char suunto_nautic_eva_handshake[] = { - 0xA5, 0x12, 0x20, 0x00, 0x00, 0x00, 0x09, 0x09, 0x20, 0x16, 0x45, 0x56, - 0x41, 0x10, 0x04, 0x41, 0x10, 0x0C, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x63, 0x1B, 0x47, 0x1B -}; +static unsigned int +suunto_nautic_seqnum (suunto_nautic_device_t *device) +{ + // Zero is the sequence number of the stream notifications. + device->seqnum = (device->seqnum + 1) & 0xFFFF; + if (device->seqnum == 0) + device->seqnum = 1; -#define EVA_HANDSHAKE_SIZE (sizeof (suunto_nautic_eva_handshake)) + return device->seqnum; +} -// Build a generic path-addressed GET request for an arbitrary endpoint. static dc_status_t -suunto_nautic_build_get (unsigned char packet[], unsigned int size, unsigned int *out_len, unsigned int seq, const char *path) +suunto_nautic_send (suunto_nautic_device_t *device, unsigned char opcode, unsigned int seqnum, const unsigned char data[], size_t size) { - size_t pathlen = strlen (path); - if (pathlen == 0 || pathlen > MAX_PATH) - return DC_STATUS_INVALIDARGS; + dc_status_t status = DC_STATUS_SUCCESS; + unsigned char packet[RPC_OVERHEAD + MAX_PAYLOAD] = {0}; - unsigned int len = RPC_HEADER_SIZE + (unsigned int) pathlen + RPC_CRC_SIZE; - if (len > size) + if (size > MAX_PAYLOAD) return DC_STATUS_INVALIDARGS; - unsigned int sublen = (unsigned int) pathlen + 4; + packet[0] = RPC_START; + packet[1] = opcode; + array_uint16_le_set (packet + 2, size); + array_uint16_le_set (packet + 4, seqnum); + if (size) + memcpy (packet + 6, data, size); - packet[0] = 0xA5; - packet[1] = RPC_OP_GET; - array_uint16_le_set (packet + 2, (unsigned short) sublen); - array_uint16_le_set (packet + 4, (unsigned short) seq); - packet[6] = 0x01; - packet[7] = 0x80; - packet[8] = 0x00; - packet[9] = (unsigned char) pathlen; - memcpy (packet + 10, path, pathlen); + unsigned int crc = checksum_crc32r (packet, 6 + size); + array_uint32_le_set (packet + 6 + size, crc); - unsigned int crc = checksum_crc32r (packet, RPC_HEADER_SIZE + (unsigned int) pathlen); - array_uint32_le_set (packet + RPC_HEADER_SIZE + (unsigned int) pathlen, crc); + status = dc_iostream_write (device->iostream, packet, RPC_OVERHEAD + size, NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to send the packet."); + return status; + } - *out_len = len; - return DC_STATUS_SUCCESS; + return status; } -// Build a RELEASE (0x0B) frame: [seq:2 LE][handle:3][01 80 00]. +// Receive the packet with the given opcode and sequence number. The watch +// serves every app connected to it over the same link, so the replies to +// another app's requests can arrive in between. Those are recognized by their +// sequence number and skipped. static dc_status_t -suunto_nautic_build_release (unsigned char packet[], unsigned int size, unsigned int *out_len, - unsigned int seq, const unsigned char handle[3]) +suunto_nautic_recv (suunto_nautic_device_t *device, unsigned char opcode, unsigned int seqnum, unsigned char data[], size_t size, size_t *actual) { - static const unsigned char tail[] = { 0x01, 0x80, 0x00 }; - unsigned int payload = 3 + (unsigned int) sizeof (tail); - unsigned int len = 4 + 2 + payload + RPC_CRC_SIZE; - if (len > size) - return DC_STATUS_INVALIDARGS; - - packet[0] = 0xA5; - packet[1] = RPC_OP_RELEASE; - array_uint16_le_set (packet + 2, (unsigned short) payload); - array_uint16_le_set (packet + 4, (unsigned short) seq); - memcpy (packet + 6, handle, 3); - memcpy (packet + 9, tail, sizeof (tail)); - - unsigned int crc = checksum_crc32r (packet, 6 + payload); - array_uint32_le_set (packet + 6 + payload, crc); - - *out_len = len; - return DC_STATUS_SUCCESS; -} + dc_status_t status = DC_STATUS_SUCCESS; + unsigned char packet[RPC_OVERHEAD + MAX_PAYLOAD] = {0}; + size_t length = 0; -// Build the "short" fetch (opcode 0x0D) the official app uses to read a -// small whole resource in one shot, e.g. /Logbook/Entries. Payload is -// [seq:2 LE][handle:3][01 80 00 00] -- the trailing 01 80 00 00 is the -// no-range form. NOT the ranged fetch used for large paginated resources -// (Summary/Data), whose payload ends 01 80 00 01 06 00 [offset:4]; sending -// that ranged form to /Logbook/Entries makes the watch reject it with a -// 400 Bad Request. -static dc_status_t -suunto_nautic_build_handle_op (unsigned char packet[], unsigned int size, unsigned int *out_len, - unsigned int seq, unsigned char opcode, const unsigned char handle[3]) -{ - static const unsigned char tail[] = { 0x01, 0x80, 0x00, 0x00 }; - unsigned int payload = 3 + (unsigned int) sizeof (tail); // handle(3) + tail - unsigned int len = 4 + 2 + payload + RPC_CRC_SIZE; // magic+opcode+sublen(4) + seq(2) + payload + crc - if (len > size) + if (size > MAX_PAYLOAD) return DC_STATUS_INVALIDARGS; - packet[0] = 0xA5; - packet[1] = opcode; - // sublen counts seq(2)+payload minus 2, i.e. payload itself. - array_uint16_le_set (packet + 2, (unsigned short) payload); - array_uint16_le_set (packet + 4, (unsigned short) seq); - memcpy (packet + 6, handle, 3); - memcpy (packet + 9, tail, sizeof (tail)); - - unsigned int crc = checksum_crc32r (packet, 6 + payload); - array_uint32_le_set (packet + 6 + payload, crc); + while (1) { + status = dc_iostream_read (device->iostream, packet, sizeof(packet), &length); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to receive the packet."); + return status; + } - *out_len = len; - return DC_STATUS_SUCCESS; -} + if (length == 0) { + // Two flags in a row, e.g. after the tail of a packet that was + // cut off when an earlier link dropped. + continue; + } -static dc_status_t -suunto_nautic_build_short_fetch (unsigned char packet[], unsigned int size, unsigned int *out_len, - unsigned int seq, const unsigned char handle[3]) -{ - return suunto_nautic_build_handle_op (packet, size, out_len, seq, RPC_OP_FETCH, handle); -} + if (length < RPC_OVERHEAD) { + ERROR (device->base.context, "Unexpected packet size (" DC_PRINTF_SIZE ").", length); + return DC_STATUS_PROTOCOL; + } -// Build a 0x0D fetch carrying one 4-byte parameter. Payload is -// [seq:2 LE][handle:3][01 80 00][count=01][type:2 LE][value:4 LE]. Used for the -// paged /Summary (int32 offset, in data bytes) and for /Logbook/Entries pages -// after the first (uint32 StartAfterId). The watch answers each page with -// status 100 (more pages) or 200 (last page). -static dc_status_t -suunto_nautic_build_param_fetch (unsigned char packet[], unsigned int size, unsigned int *out_len, - unsigned int seq, const unsigned char handle[3], unsigned int type, unsigned int value) -{ - unsigned char flags[] = { 0x01, 0x80, 0x00, 0x01, (unsigned char) type, 0x00 }; - unsigned int payload = 3 + (unsigned int) sizeof (flags) + 4; // handle(3) + flags(6) + value(4) - unsigned int len = 4 + 2 + payload + RPC_CRC_SIZE; - if (len > size) - return DC_STATUS_INVALIDARGS; + unsigned int crc = array_uint32_le (packet + length - 4); + unsigned int ccrc = checksum_crc32r (packet, length - 4); + if (crc != ccrc) { + ERROR (device->base.context, "Unexpected packet checksum (%08x %08x).", crc, ccrc); + return DC_STATUS_PROTOCOL; + } - packet[0] = 0xA5; - packet[1] = RPC_OP_FETCH; - array_uint16_le_set (packet + 2, (unsigned short) payload); - array_uint16_le_set (packet + 4, (unsigned short) seq); - memcpy (packet + 6, handle, 3); - memcpy (packet + 9, flags, sizeof (flags)); - array_uint32_le_set (packet + 9 + (unsigned int) sizeof (flags), value); + if (packet[0] != RPC_START) { + ERROR (device->base.context, "Unexpected packet start byte (%02x).", packet[0]); + return DC_STATUS_PROTOCOL; + } - unsigned int crc = checksum_crc32r (packet, 6 + payload); - array_uint32_le_set (packet + 6 + payload, crc); + unsigned int rsp_length = array_uint16_le (packet + 2); + if (rsp_length != length - RPC_OVERHEAD) { + ERROR (device->base.context, "Unexpected packet payload length (%u " DC_PRINTF_SIZE ").", rsp_length, length - RPC_OVERHEAD); + return DC_STATUS_PROTOCOL; + } - *out_len = len; - return DC_STATUS_SUCCESS; -} + unsigned int rsp_opcode = packet[1]; + unsigned int rsp_seqnum = array_uint16_le (packet + 4); + if (rsp_opcode != opcode || rsp_seqnum != seqnum) { + if (rsp_opcode == RPC_OP_NOTIFICATION) { + // A leftover chunk of a stream that was stopped. + DEBUG (device->base.context, "Skipping a stream notification."); + } else { + WARNING (device->base.context, "Skipping a packet for another request (%02x %u).", rsp_opcode, rsp_seqnum); + } + continue; + } -// A well-formed RPC frame from the watch starts with the 0xA5 magic and is long -// enough to carry an opcode. Anything shorter is line noise or a truncated read. -static int -suunto_nautic_frame_wellformed (const unsigned char *packet, size_t len) -{ - return len >= 2 && packet[0] == 0xA5; -} + if (rsp_length > size) { + ERROR (device->base.context, "Unexpected packet payload length (%u " DC_PRINTF_SIZE ").", rsp_length, size); + return DC_STATUS_PROTOCOL; + } -// The reply to a GET: an ACK echoing the request's message id. Anything else -// (a leftover 0x01 chunk of an unfinished stream, another client's traffic) is -// not ours, and taking its handle would address the wrong resource. -static int -suunto_nautic_frame_is_reply (const unsigned char *packet, size_t len, unsigned int msgid) -{ - return len >= RPC_HANDLE_OFFSET + 3 && packet[0] == 0xA5 && - (packet[1] == RPC_OP_ACK || packet[1] == RPC_OP_ACK_ALT) && - array_uint16_le (packet + RPC_MSGID_OFFSET) == msgid; -} + memcpy (data, packet + 6, rsp_length); + *actual = rsp_length; -// The DATA (0x05) frame a fetch is waiting for: our opcode, carrying the 3-byte -// session handle this fetch was issued against. -static int -suunto_nautic_frame_is_our_data (const unsigned char *packet, size_t len, const unsigned char handle[3]) -{ - return len >= RPC_HANDLE_OFFSET + 3 && - packet[0] == 0xA5 && packet[1] == RPC_OP_DATA && - memcmp (packet + RPC_HANDLE_OFFSET, handle, 3) == 0; + return status; + } } -// A single BLE link is shared: another client (most often the official Suunto -// app holding a live logbook subscription) can flood it with its own frames -- -// notably Whiteboard 0x07 subscribe-result traffic on a different handle. Those -// are well-formed frames that just aren't ours, so skip them freely rather than -// treating them as a data error. This cap only guards against an unbounded loop; -// in practice the read below times out first (a clean DC_STATUS_TIMEOUT) when -// our DATA frame never gets a turn on the contended link. -#define MAX_FOREIGN_SKIPS 256 -// A tight cap on genuinely malformed/truncated frames: those do signal a real -// data-format problem, so give up quickly with DC_STATUS_DATAFORMAT. -#define MAX_MALFORMED_SKIPS 8 - +// Send a request on a handle and receive its reply: the payload is the handle +// and its 01 80 00 resource flags, followed by the parameters, if any. static dc_status_t -suunto_nautic_transfer (suunto_nautic_device_t *device, const unsigned char req[], unsigned int rsize, dc_buffer_t *response) +suunto_nautic_transfer (suunto_nautic_device_t *device, unsigned char opcode, unsigned char rsp_opcode, + unsigned int handle, const unsigned char params[], size_t nparams, unsigned char data[], size_t size, size_t *actual) { dc_status_t status = DC_STATUS_SUCCESS; - dc_device_t *abstract = (dc_device_t *) device; - - if (device_is_cancelled (abstract)) - return DC_STATUS_CANCELLED; + unsigned char request[6 + 7] = {0}; - status = dc_iostream_write (device->iostream, req, rsize, NULL); - if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to send the RPC request."); - return status; - } + if (nparams > sizeof(request) - 6) + return DC_STATUS_INVALIDARGS; - if (response) { - unsigned int msgid = array_uint16_le (req + RPC_MSGID_OFFSET); - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - unsigned int foreign_skips = 0; - unsigned int malformed_skips = 0; - for (;;) { - status = dc_iostream_read (device->iostream, packet, sizeof (packet), &len); - if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to receive the RPC response."); - return status; - } + array_uint24_le_set (request, handle); + request[3] = 0x01; + request[4] = 0x80; + request[5] = 0x00; + if (nparams) + memcpy (request + 6, params, nparams); - HEXDUMP (abstract->context, DC_LOGLEVEL_DEBUG, "RPC RSP", packet, len); + unsigned int seqnum = suunto_nautic_seqnum (device); + status = suunto_nautic_send (device, opcode, seqnum, request, 6 + nparams); + if (status != DC_STATUS_SUCCESS) + return status; - if (suunto_nautic_frame_is_reply (packet, len, msgid)) - break; - if (suunto_nautic_frame_wellformed (packet, len)) { - if (++foreign_skips >= MAX_FOREIGN_SKIPS) { - ERROR (abstract->context, "No reply to message %u among the frames on the link.", msgid); - return DC_STATUS_TIMEOUT; - } - WARNING (abstract->context, "Skipping a frame that is not the reply to message %u (op 0x%02x).", - msgid, packet[1]); - continue; - } - if (++malformed_skips >= MAX_MALFORMED_SKIPS) { - ERROR (abstract->context, "Too many malformed frames waiting for the reply to message %u.", msgid); - return DC_STATUS_DATAFORMAT; - } - WARNING (abstract->context, "Skipping a malformed frame (" DC_PRINTF_SIZE " bytes).", len); - } + status = suunto_nautic_recv (device, rsp_opcode, seqnum, data, size, actual); + if (status != DC_STATUS_SUCCESS) + return status; - dc_buffer_clear (response); - if (!dc_buffer_append (response, packet, len)) { - ERROR (abstract->context, "Failed to allocate memory."); - return DC_STATUS_NOMEMORY; - } + if (*actual < REPLY_CONTENT) { + ERROR (device->base.context, "Unexpected reply size (" DC_PRINTF_SIZE ").", *actual); + return DC_STATUS_PROTOCOL; } - return DC_STATUS_SUCCESS; + return status; } dc_status_t @@ -452,13 +357,14 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream return DC_STATUS_NOMEMORY; } - 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; + // Start at a different sequence number on every connection. After a + // dropped link the watch can still hold the previous session, and leaves a + // request unanswered that reuses one of its sequence numbers. + device->seqnum = ((unsigned int) dc_datetime_now () * 2654435761u) & 0xFFFF; + if (g_suunto_nautic_seqnum) + device->seqnum = g_suunto_nautic_seqnum; memset (device->fingerprint, 0, sizeof (device->fingerprint)); - device->handshake_acknowledged = 0; + device->acknowledged = 0; status = dc_hdlc_open (&device->iostream, context, iostream, 244, 244); if (status != DC_STATUS_SUCCESS) { @@ -466,7 +372,7 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream goto error_free; } - status = dc_iostream_set_timeout (device->iostream, 5000); + status = dc_iostream_set_timeout (device->iostream, TIMEOUT_DEFAULT); if (status != DC_STATUS_SUCCESS) { ERROR (context, "Failed to set the timeout."); goto error_close; @@ -474,49 +380,25 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream dc_iostream_purge (device->iostream, DC_DIRECTION_ALL); - // EVA handshake. The response content can't be validated (its format isn't - // understood), but its arrival can: the watch answers with a Hello-Ack. - HEXDUMP (context, DC_LOGLEVEL_DEBUG, "EVA REQ", suunto_nautic_eva_handshake, EVA_HANDSHAKE_SIZE); - - status = dc_iostream_write (device->iostream, suunto_nautic_eva_handshake, EVA_HANDSHAKE_SIZE, NULL); + status = suunto_nautic_send (device, RPC_OP_HELLO, 0, suunto_nautic_hello, sizeof (suunto_nautic_hello)); if (status != DC_STATUS_SUCCESS) { - ERROR (context, "Failed to send the EVA handshake."); + ERROR (context, "Failed to send the hello."); goto error_close; } - // On a quick reconnect the watch is still sending the tail of the stream the - // dropped link was reading, ahead of the Hello-Ack, and a request sent before - // the Ack has arrived can go unanswered. The purge above runs too early to - // catch those frames, so skip them here until the Ack turns up. If the link - // goes quiet without one, carry on as before: the stale frames are drained. - unsigned char handshake_rsp[MAX_PACKET] = {0}; - size_t handshake_len = 0; - unsigned int handshake_skips = 0; - for (;;) { - status = dc_iostream_read (device->iostream, handshake_rsp, sizeof (handshake_rsp), &handshake_len); - if (status == DC_STATUS_TIMEOUT && handshake_skips > 0) { - WARNING (context, "No EVA handshake response after %u other frames; continuing.", handshake_skips); - break; - } - if (status != DC_STATUS_SUCCESS) { - ERROR (context, "Failed to receive the EVA handshake response. The device may not " - "support this protocol, or the handshake payload may need updating " - "(see suunto_nautic.h)."); - goto error_close; - } - - HEXDUMP (context, DC_LOGLEVEL_DEBUG, "EVA RSP", handshake_rsp, handshake_len); - - if (handshake_len >= 2 && handshake_rsp[0] == 0xA5 && handshake_rsp[1] == RPC_OP_HELLO_ACK) { - device->handshake_acknowledged = 1; - break; - } - if (++handshake_skips >= MAX_FOREIGN_SKIPS) { - ERROR (context, "No EVA handshake response among the frames on the link."); - status = DC_STATUS_TIMEOUT; - goto error_close; - } - WARNING (context, "Skipping a frame ahead of the EVA handshake response (" DC_PRINTF_SIZE " bytes).", handshake_len); + unsigned char hello[sizeof (suunto_nautic_hello)] = {0}; + size_t hello_len = 0; + status = suunto_nautic_recv (device, RPC_OP_HELLO_REPLY, 0, hello, sizeof (hello), &hello_len); + if (status == DC_STATUS_TIMEOUT) { + // A sleeping watch doesn't answer. Carry on, so the host can tell + // the diver to wake it when the first request times out. + WARNING (context, "No reply to the hello; continuing."); + status = DC_STATUS_SUCCESS; + } else if (status != DC_STATUS_SUCCESS) { + ERROR (context, "Failed to receive the hello."); + goto error_close; + } else { + device->acknowledged = 1; } *out = (dc_device_t *) device; @@ -535,41 +417,95 @@ 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; + g_suunto_nautic_seqnum = device->seqnum ? device->seqnum : 1; return dc_iostream_close (device->iostream); } -int -suunto_nautic_device_handshake_acknowledged (dc_device_t *abstract) +// Resolve a path to a handle. The GET reply is HANDLE(3) FLAGS(3) STATUS(2). +static dc_status_t +suunto_nautic_get (suunto_nautic_device_t *device, const char *path, unsigned int *handle, unsigned int *result) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC) - return 0; + dc_status_t status = DC_STATUS_SUCCESS; + + size_t pathlen = strlen (path); + if (pathlen == 0 || pathlen > MAX_PATH) + return DC_STATUS_INVALIDARGS; + + unsigned char request[4 + MAX_PATH] = {0}; + request[0] = 0x01; + request[1] = 0x80; + request[2] = 0x00; + request[3] = pathlen; + memcpy (request + 4, path, pathlen); + + unsigned int seqnum = suunto_nautic_seqnum (device); + status = suunto_nautic_send (device, RPC_OP_GET, seqnum, request, 4 + pathlen); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to send the request for %s.", path); + return status; + } + + unsigned char reply[REPLY_CONTENT] = {0}; + size_t reply_len = 0; + status = suunto_nautic_recv (device, RPC_OP_GET_REPLY, seqnum, reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS) { + ERROR (device->base.context, "Failed to receive the reply for %s.", path); + return status; + } + + if (reply_len != sizeof (reply)) { + ERROR (device->base.context, "Unexpected reply size (" DC_PRINTF_SIZE ").", reply_len); + return DC_STATUS_PROTOCOL; + } + + *handle = array_uint24_le (reply); + *result = array_uint16_le (reply + REPLY_STATUS); - return ((suunto_nautic_device_t *) abstract)->handshake_acknowledged; + return status; } -dc_status_t -suunto_nautic_device_request (dc_device_t *abstract, const char *path, dc_buffer_t *response) +// Release a handle bound by a GET, so the next GET of the same resource binds +// its own id to the slot. A failure only leaves the slot bound. +static void +suunto_nautic_release (suunto_nautic_device_t *device, unsigned int handle) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || path == NULL) - return DC_STATUS_INVALIDARGS; + if (HANDLE_SLOT (handle) == SLOT_STATIC || HANDLE_SLOT (handle) == SLOT_UNKNOWN) + return; - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + unsigned char reply[REPLY_CONTENT] = {0}; + size_t reply_len = 0; + dc_status_t status = suunto_nautic_transfer (device, RPC_OP_RELEASE, RPC_OP_RELEASE_REPLY, + handle, NULL, 0, reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS || array_uint16_le (reply + REPLY_STATUS) != STATUS_OK) + WARNING (device->base.context, "Failed to release handle %06x.", handle); +} - unsigned char packet[RPC_HEADER_SIZE + MAX_PATH + RPC_CRC_SIZE]; - unsigned int len = 0; - dc_status_t status = suunto_nautic_build_get (packet, sizeof (packet), &len, device->sequence, path); +// Resolve a path that must exist. +static dc_status_t +suunto_nautic_get_handle (suunto_nautic_device_t *device, const char *path, unsigned int *handle) +{ + unsigned int result = 0; + dc_status_t status = suunto_nautic_get (device, path, handle, &result); if (status != DC_STATUS_SUCCESS) return status; - device->sequence++; - return suunto_nautic_transfer (device, packet, len, response); + if (result != STATUS_OK) { + ERROR (device->base.context, "Failed to resolve %s (status %u).", path, result); + return DC_STATUS_PROTOCOL; + } + + // Every handle is released before the next GET, so a still bound 00 slot + // was left by a link that dropped during a transfer. + if (HANDLE_SLOT (*handle) == SLOT_ALT) { + WARNING (device->base.context, "Releasing the stale slot of %s.", path); + suunto_nautic_release (device, (*handle & ~0xFFu) | SLOT_DEFAULT); + } + + return status; } -// Decompress a Heatshrink (LZSS) stream, per the parameters documented -// above. Verified byte-for-byte against a reference implementation using -// real captured data (see suunto_nautic.h). +// Decompress a Heatshrink (LZSS) stream, with the parameters above. static dc_status_t suunto_nautic_heatshrink_decompress (dc_context_t *context, const unsigned char *input, size_t input_size, dc_buffer_t *output) { @@ -637,513 +573,228 @@ suunto_nautic_heatshrink_decompress (dc_context_t *context, const unsigned char return status; } -// Append one MDS chunk frame's sub-payload to `raw`. A short or inconsistent -// frame is skipped with a warning; only an allocation failure is fatal. -static dc_status_t -suunto_nautic_append_chunk (dc_context_t *context, dc_buffer_t *raw, const unsigned char *packet, size_t len) -{ - if (len < MDS_HEADER_SIZE) { - WARNING (context, "MDS chunk shorter than the header (" DC_PRINTF_SIZE ").", len); - return DC_STATUS_SUCCESS; - } - - unsigned int chunk_size = array_uint16_le (packet + MDS_CHUNK_SIZE_OFFSET); - if (MDS_HEADER_SIZE + chunk_size > len) { - WARNING (context, "MDS chunk size (%u) exceeds the frame (" DC_PRINTF_SIZE ").", chunk_size, len); - return DC_STATUS_SUCCESS; - } - - if (!dc_buffer_append (raw, packet + MDS_HEADER_SIZE, chunk_size)) { - ERROR (context, "Failed to allocate memory."); - return DC_STATUS_NOMEMORY; - } - - return DC_STATUS_SUCCESS; -} - -static dc_status_t suunto_nautic_get_handle (dc_device_t *abstract, const char *path, unsigned char handle[3]); - -// Release a handle a GET resolved, so the next GET of the same resource binds -// its own id to the slot. Best-effort: a failure only leaves the slot bound. -static void -suunto_nautic_release_handle (dc_device_t *abstract, const unsigned char handle[3]) -{ - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - - if (handle[0] == RPC_HANDLE_STATIC || handle[0] == RPC_HANDLE_UNKNOWN) - return; - - unsigned char packet[32]; - unsigned int len = 0; - if (suunto_nautic_build_release (packet, sizeof (packet), &len, device->sequence, handle) != DC_STATUS_SUCCESS) - return; - device->sequence++; - - dc_buffer_t *reply = dc_buffer_new (0); - if (reply == NULL) - return; - if (suunto_nautic_transfer (device, packet, len, reply) != DC_STATUS_SUCCESS) - WARNING (abstract->context, "Failed to release handle %02x %02x %02x.", handle[0], handle[1], handle[2]); - dc_buffer_free (reply); -} - -// Pull a large streamed resource (dive data): GET -> ACK(handle) -> subscribe -// on that handle -> collect the chunk stream -> unsubscribe -> release. Returns -// the raw, MDS-chunk-stripped, still-Heatshrink-compressed bytes. Small listing -// endpoints use suunto_nautic_device_short_fetch() instead. Returns -// DC_STATUS_PROTOCOL when the watch refuses the stream (e.g. 423 Locked), so -// the caller can back off and retry. +// Download a streamed resource (the dive profile): GET, SUBSCRIBE, the +// notifications up to the end marker, UNSUBSCRIBE and RELEASE. Returns the +// still compressed stream. Returns DC_STATUS_PROTOCOL when the watch refuses +// the stream (status 423 while the previous stream's handle is still bound). static dc_status_t -suunto_nautic_device_stream_fetch (dc_device_t *abstract, const char *path, dc_buffer_t *raw) +suunto_nautic_stream (suunto_nautic_device_t *device, const char *path, dc_buffer_t *buffer) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + dc_device_t *abstract = (dc_device_t *) device; dc_status_t status = DC_STATUS_SUCCESS; + dc_status_t rc = DC_STATUS_SUCCESS; - // 1. Resolve the path. The handle in the ACK is bound to this dive; a - // fixed handle would address whichever dive an earlier GET left bound. - unsigned char handle[3]; - status = suunto_nautic_get_handle (abstract, path, handle); + unsigned int handle = 0; + status = suunto_nautic_get_handle (device, path, &handle); if (status != DC_STATUS_SUCCESS) return status; - // 2. Subscribe to the handle; the watch answers 0x08 and streams. - unsigned char req[32]; - unsigned int req_len = 0; - status = suunto_nautic_build_handle_op (req, sizeof (req), &req_len, device->sequence, RPC_OP_SUBSCRIBE, handle); - if (status != DC_STATUS_SUCCESS) - goto release; - device->sequence++; - - status = dc_iostream_write (device->iostream, req, req_len, NULL); + unsigned char reply[REPLY_CONTENT + 2] = {0}; + size_t reply_len = 0; + status = suunto_nautic_transfer (device, RPC_OP_SUBSCRIBE, RPC_OP_SUBSCRIBE_REPLY, + handle, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); if (status != DC_STATUS_SUCCESS) { ERROR (abstract->context, "Failed to subscribe to %s.", path); - goto release; + goto error_release; + } + + unsigned int result = array_uint16_le (reply + REPLY_STATUS); + if (result != STATUS_OK) { + ERROR (abstract->context, "The watch refused the stream for %s (status %u).", path, result); + status = DC_STATUS_PROTOCOL; + goto error_release; } - // 3. Capture the MDS chunk frames (opcode 0x01). For a compressed - // endpoint, the concatenation of their sub-payloads is one continuous - // Heatshrink stream; chunk boundaries are a transport artifact. - // - // The watch is not ACKed per chunk: once subscribed it streams the - // entire response continuously, and the host buffers until a 2.0s - // inter-frame silence. MAX_CHUNKS is only a runaway guard. - status = dc_iostream_set_timeout (device->iostream, 2000); + status = dc_iostream_set_timeout (device->iostream, TIMEOUT_STREAM); if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to set the stream timeout."); - goto release; + ERROR (abstract->context, "Failed to set the timeout."); + goto error_unsubscribe; } - // A long dive streams for minutes, so a cancel is honoured between frames; - // the stream is still stopped below so the watch doesn't keep sending it. - unsigned int cancelled = 0; - unsigned int nframes = 0; - for (nframes = 0; nframes < MAX_CHUNKS; nframes++) { + // The watch streams the whole resource without acknowledgements, and + // repeats its last chunk until the subscription ends. + dc_buffer_clear (buffer); + while (1) { if (device_is_cancelled (abstract)) { - cancelled = 1; + status = DC_STATUS_CANCELLED; break; } - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = dc_iostream_read (device->iostream, packet, sizeof (packet), &len); + unsigned char chunk[MAX_PAYLOAD] = {0}; + size_t chunk_len = 0; + status = suunto_nautic_recv (device, RPC_OP_NOTIFICATION, 0, chunk, sizeof (chunk), &chunk_len); + if (status == DC_STATUS_TIMEOUT) { + WARNING (abstract->context, "The stream for %s ended without an end marker.", path); + status = DC_STATUS_SUCCESS; + break; + } if (status != DC_STATUS_SUCCESS) { - if (status == DC_STATUS_TIMEOUT) - break; - ERROR (abstract->context, "Failed to receive a stream chunk."); - return status; + ERROR (abstract->context, "Failed to receive the stream for %s.", path); + break; } - if (len == 0) + if (chunk_len < CHUNK_DATA) { + ERROR (abstract->context, "Unexpected chunk size (" DC_PRINTF_SIZE ").", chunk_len); + status = DC_STATUS_PROTOCOL; break; + } - if (len >= 2 && packet[0] == 0xA5 && packet[1] == RPC_OP_STREAM_END) + unsigned int offset = array_uint32_le (chunk + CHUNK_OFFSET); + unsigned int length = array_uint16_le (chunk + CHUNK_LENGTH); + if (CHUNK_DATA + length > chunk_len) { + ERROR (abstract->context, "Unexpected chunk length (%u " DC_PRINTF_SIZE ").", length, chunk_len); + status = DC_STATUS_PROTOCOL; break; - - // A non-200 stream-start frame is a refusal, notably 423 Locked when - // the previous stream's handle hasn't been released yet. - if (len >= RPC_STATUS_OFFSET + 2 && packet[0] == 0xA5 && packet[1] == RPC_OP_STREAM_START) { - unsigned int frame_status = array_uint16_le (packet + RPC_STATUS_OFFSET); - if (frame_status != RPC_STATUS_OK) { - ERROR (abstract->context, "Watch refused the stream for %s (status %u).", path, frame_status); - status = DC_STATUS_PROTOCOL; - goto restore; - } } - if (len >= 2 && packet[0] == 0xA5 && packet[1] == RPC_OP_STREAM_CHUNK) { - status = suunto_nautic_append_chunk (abstract->context, raw, packet, len); - if (status != DC_STATUS_SUCCESS) - return status; + size_t received = dc_buffer_get_size (buffer); + if (offset < received) { + // A repeated chunk. + continue; } - } - status = cancelled ? DC_STATUS_CANCELLED : DC_STATUS_SUCCESS; - - if (nframes >= MAX_CHUNKS) - WARNING (abstract->context, "Stream for %s hit the %u-frame guard; the dive may be truncated.", path, MAX_CHUNKS); - - // 4. Close the stream. Without it the watch keeps streaming on the handle, - // so the next GET can be answered by a leftover chunk or refused with 423. - // The watch answers with STREAM_END; a trailing chunk or two can still - // arrive first and belongs to this dive. - unsigned char stop[32]; - unsigned int stop_len = 0; - int stopped = suunto_nautic_build_handle_op (stop, sizeof (stop), &stop_len, device->sequence, RPC_OP_UNSUBSCRIBE, handle) == DC_STATUS_SUCCESS; - if (stopped) { - device->sequence++; - stopped = dc_iostream_write (device->iostream, stop, stop_len, NULL) == DC_STATUS_SUCCESS; - } - if (stopped) { - for (unsigned int i = 0; i < 16; i++) { - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - if (dc_iostream_read (device->iostream, packet, sizeof (packet), &len) != DC_STATUS_SUCCESS || len == 0) - break; - if (len >= 2 && packet[0] == 0xA5 && packet[1] == RPC_OP_STREAM_END) - break; - if (len >= 2 && packet[0] == 0xA5 && packet[1] == RPC_OP_STREAM_CHUNK) { - status = suunto_nautic_append_chunk (abstract->context, raw, packet, len); - if (status != DC_STATUS_SUCCESS) - return status; - } + if (offset > received) { + ERROR (abstract->context, "Missing stream data (%u " DC_PRINTF_SIZE ").", offset, received); + status = DC_STATUS_PROTOCOL; + break; } - } else { - WARNING (abstract->context, "Failed to send the stream stop for %s.", path); - } - -restore: - if (dc_iostream_set_timeout (device->iostream, 5000) != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to restore the timeout."); - return DC_STATUS_IO; - } - -release: - suunto_nautic_release_handle (abstract, handle); - return status; -} -// GET `path` and return the 3-byte session handle from its ACK; the fetches -// that read the resource are issued against that handle. -static dc_status_t -suunto_nautic_get_handle (dc_device_t *abstract, const char *path, unsigned char handle[3]) -{ - dc_buffer_t *ack = dc_buffer_new (0); - if (ack == NULL) - return DC_STATUS_NOMEMORY; + if (length == 0) + break; - dc_status_t status = suunto_nautic_device_request (abstract, path, ack); - if (status != DC_STATUS_SUCCESS) { - dc_buffer_free (ack); - ERROR (abstract->context, "Failed to request %s.", path); - return status; + if (!dc_buffer_append (buffer, chunk + CHUNK_DATA, length)) { + ERROR (abstract->context, "Failed to allocate memory."); + status = DC_STATUS_NOMEMORY; + break; + } } - const unsigned char *ack_data = dc_buffer_get_data (ack); - size_t ack_size = dc_buffer_get_size (ack); - if (ack_size < RPC_HANDLE_OFFSET + 3) { - dc_buffer_free (ack); - ERROR (abstract->context, "ACK too short for a handle (" DC_PRINTF_SIZE ").", ack_size); - return DC_STATUS_DATAFORMAT; - } - 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); +error_unsubscribe: + // Without the UNSUBSCRIBE the watch keeps streaming, and the next GET of a + // stream is refused with status 423. + rc = suunto_nautic_transfer (device, RPC_OP_UNSUBSCRIBE, RPC_OP_UNSUBSCRIBE_REPLY, + handle, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); + if (rc != DC_STATUS_SUCCESS) + WARNING (abstract->context, "Failed to unsubscribe from %s.", path); + + rc = dc_iostream_set_timeout (device->iostream, TIMEOUT_DEFAULT); + if (rc != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to restore the timeout."); + dc_status_set_error (&status, rc); } - return DC_STATUS_SUCCESS; +error_release: + suunto_nautic_release (device, handle); + return status; } -// Send a fetch frame, then read until the DATA (0x05) frame for `handle` -// arrives. The watch multiplexes unsolicited frames (a re-sent Hello, an -// analytics stream) on the link, and another client (the Suunto app's logbook -// subscription) can flood it, so anything else is skipped rather than spliced -// in. With skip_non_data == 0 the first frame is returned as-is (raw capture). +// Read a paged resource (/Summary). Each page is requested by the offset of +// its data, and the last page has status 200. static dc_status_t -suunto_nautic_fetch_frame (dc_device_t *abstract, const char *path, const unsigned char fetch[], unsigned int fetch_len, - const unsigned char handle[3], int skip_non_data, unsigned char packet[MAX_PACKET], size_t *out_len) +suunto_nautic_fetch_pages (suunto_nautic_device_t *device, const char *path, dc_buffer_t *buffer) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t status = DC_STATUS_SUCCESS; - dc_status_t status = dc_iostream_write (device->iostream, fetch, fetch_len, NULL); - if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to send the fetch for %s.", path); + unsigned int handle = 0; + status = suunto_nautic_get_handle (device, path, &handle); + if (status != DC_STATUS_SUCCESS) return status; - } - size_t len = 0; - unsigned int foreign_skips = 0; - unsigned int malformed_skips = 0; - for (;;) { - status = dc_iostream_read (device->iostream, packet, MAX_PACKET, &len); + dc_buffer_clear (buffer); + unsigned int offset = 0; + while (1) { + unsigned char params[7] = {0x01, PARAM_INT32, 0x00}; + array_uint32_le_set (params + 3, offset); + + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + status = suunto_nautic_transfer (device, RPC_OP_FETCH, RPC_OP_DATA, + handle, params, sizeof (params), reply, sizeof (reply), &reply_len); if (status != DC_STATUS_SUCCESS) { - // A contended link means our DATA never gets a turn and this read - // times out; surface DC_STATUS_TIMEOUT rather than DATAFORMAT. - ERROR (abstract->context, "Failed to receive the data for %s.", path); - return status; - } - HEXDUMP (abstract->context, DC_LOGLEVEL_DEBUG, "FETCH RSP", packet, len); - if (!skip_non_data) + ERROR (abstract->context, "Failed to fetch %s (offset %u).", path, offset); break; - if (suunto_nautic_frame_is_our_data (packet, len, handle)) + } + + unsigned int result = array_uint16_le (reply + REPLY_STATUS); + if (result != STATUS_OK && result != STATUS_CONTINUE) { + ERROR (abstract->context, "Failed to fetch %s (status %u).", path, result); + status = DC_STATUS_PROTOCOL; break; - if (suunto_nautic_frame_wellformed (packet, len)) { - if (++foreign_skips >= MAX_FOREIGN_SKIPS) { - ERROR (abstract->context, "Link saturated by another client while fetching %s; giving up.", path); - return DC_STATUS_TIMEOUT; - } - WARNING (abstract->context, "Skipping frame from another client while fetching %s (op 0x%02x).", - path, packet[1]); - continue; } - if (++malformed_skips >= MAX_MALFORMED_SKIPS) { - ERROR (abstract->context, "Too many malformed frames while fetching %s (" DC_PRINTF_SIZE " bytes).", path, len); - return DC_STATUS_DATAFORMAT; + + if (reply_len < REPLY_CONTENT + PAGE_HEADER) { + ERROR (abstract->context, "Unexpected page size (" DC_PRINTF_SIZE ").", reply_len); + status = DC_STATUS_PROTOCOL; + break; } - WARNING (abstract->context, "Skipping malformed frame while fetching %s (" DC_PRINTF_SIZE " bytes).", path, len); - } - *out_len = len; - return DC_STATUS_SUCCESS; -} + unsigned int length = array_uint16_le (reply + REPLY_CONTENT + PAGE_LENGTH); + if (REPLY_CONTENT + PAGE_HEADER + length > reply_len) { + ERROR (abstract->context, "Unexpected page length (%u " DC_PRINTF_SIZE ").", length, reply_len); + status = DC_STATUS_PROTOCOL; + break; + } -// Status of a DATA frame: 200 (complete / last page) or 100 (more pages). -static dc_status_t -suunto_nautic_data_status (dc_device_t *abstract, const char *path, const unsigned char *packet, size_t len, unsigned int *out) -{ - if (len < RPC_STATUS_OFFSET + 2 || packet[0] != 0xA5 || packet[1] != RPC_OP_DATA) { - ERROR (abstract->context, "Unexpected data frame for %s (" DC_PRINTF_SIZE " bytes).", path, len); - return DC_STATUS_DATAFORMAT; - } - - unsigned int frame_status = array_uint16_le (packet + RPC_STATUS_OFFSET); - if (frame_status != RPC_STATUS_OK && frame_status != RPC_STATUS_CONTINUE) { - ERROR (abstract->context, "Watch returned status %u for %s (200/100 expected).", frame_status, path); - return DC_STATUS_PROTOCOL; - } - - *out = frame_status; - return DC_STATUS_SUCCESS; -} - -// Fetch a resource the watch paginates (e.g. /Logbook/byId//Summary): -// GET -> ACK(handle) -> repeated ranged 0x0D fetch, looping while the page -// status is 100 (more pages) until 200 (last page). Each DATA frame is -// [A5 05 sublen:2][msgid:2][handle:3][flags:3][status:2] followed by a page -// [header:11][data][crc:4]; only the data is kept, and the next offset -// advances by the data length (the watch's offset counts data bytes only). -static dc_status_t -suunto_nautic_fetch_pages (dc_device_t *abstract, const char *path, const unsigned char handle[3], dc_buffer_t *response) -{ - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - dc_status_t status = DC_STATUS_SUCCESS; - - dc_buffer_clear (response); - unsigned int offset = 0; - const unsigned int header = 4 + 10; // A5 05 sublen(2) + 10-byte REST sub-header - - for (unsigned int page = 0; page < MAX_PAGES; page++) { - unsigned char fetch[32]; - unsigned int fetch_len = 0; - status = suunto_nautic_build_param_fetch (fetch, sizeof (fetch), &fetch_len, - device->sequence, handle, RPC_PARAM_INT32, offset); - if (status != DC_STATUS_SUCCESS) - return status; - device->sequence++; - - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = suunto_nautic_fetch_frame (abstract, path, fetch, fetch_len, handle, 1, packet, &len); - if (status != DC_STATUS_SUCCESS) - return status; - - unsigned int frame_status = 0; - status = suunto_nautic_data_status (abstract, path, packet, len, &frame_status); - if (status != DC_STATUS_SUCCESS) - return status; - - if (len < header + SUMMARY_PAGE_HEADER_SIZE + RPC_CRC_SIZE) { - ERROR (abstract->context, "Page %u of %s too short for its header (" DC_PRINTF_SIZE " bytes).", page, path, len); - return DC_STATUS_DATAFORMAT; - } - - const unsigned char *page_data = packet + header + SUMMARY_PAGE_HEADER_SIZE; - unsigned int available = (unsigned int) (len - header - SUMMARY_PAGE_HEADER_SIZE - RPC_CRC_SIZE); - unsigned int length = array_uint16_le (packet + header + SUMMARY_PAGE_LENGTH_OFFSET); - if (length != available) { - WARNING (abstract->context, "Page %u of %s: header length %u, frame holds %u.", page, path, length, available); - if (length > available) - length = available; - } - - if (!dc_buffer_append (response, page_data, length)) { + if (!dc_buffer_append (buffer, reply + REPLY_CONTENT + PAGE_HEADER, length)) { ERROR (abstract->context, "Failed to allocate memory."); - return DC_STATUS_NOMEMORY; + status = DC_STATUS_NOMEMORY; + break; } offset += length; - if (frame_status == RPC_STATUS_OK) { - DEBUG (abstract->context, "Paginated fetch done: %u page(s), " DC_PRINTF_SIZE " bytes for %s.", - page + 1, dc_buffer_get_size (response), path); - return DC_STATUS_SUCCESS; - } + if (result == STATUS_OK) + break; if (length == 0) { - ERROR (abstract->context, "Empty page %u of %s with more pages pending.", page, path); - return DC_STATUS_PROTOCOL; + ERROR (abstract->context, "Empty page with more pages pending."); + status = DC_STATUS_PROTOCOL; + break; } } - WARNING (abstract->context, "Paginated fetch hit the page limit for %s -- data may be truncated.", path); - return DC_STATUS_SUCCESS; -} - -static dc_status_t -suunto_nautic_device_paginated_fetch (dc_device_t *abstract, const char *path, dc_buffer_t *response) -{ - unsigned char handle[3]; - dc_status_t status = suunto_nautic_get_handle (abstract, path, handle); - if (status != DC_STATUS_SUCCESS) - return status; - - status = suunto_nautic_fetch_pages (abstract, path, handle, response); - suunto_nautic_release_handle (abstract, handle); + suunto_nautic_release (device, handle); return status; } -// GET -> ACK(handle) -> 0x0D short fetch (no range) -> one DATA frame, returned -// whole in `frame`. The listing endpoints don't answer the 0x0B/0x10 stream -// triggers, only this form. With skip_non_data == 0 the very first frame is -// returned, whatever it is, so a tester can export what the watch sent. +// Read a small resource that fits in a single reply (/Info). static dc_status_t -suunto_nautic_short_fetch_frame (dc_device_t *abstract, const char *path, dc_buffer_t *frame, int skip_non_data) +suunto_nautic_fetch (suunto_nautic_device_t *device, const char *path, dc_buffer_t *buffer) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - - unsigned char handle[3]; - dc_status_t status = suunto_nautic_get_handle (abstract, path, handle); - if (status != DC_STATUS_SUCCESS) - return status; - - unsigned char fetch[32]; - unsigned int fetch_len = 0; - status = suunto_nautic_build_short_fetch (fetch, sizeof (fetch), &fetch_len, device->sequence, handle); - if (status != DC_STATUS_SUCCESS) - return status; - device->sequence++; + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t status = DC_STATUS_SUCCESS; - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = suunto_nautic_fetch_frame (abstract, path, fetch, fetch_len, handle, skip_non_data, packet, &len); - suunto_nautic_release_handle (abstract, handle); + unsigned int handle = 0; + status = suunto_nautic_get_handle (device, path, &handle); if (status != DC_STATUS_SUCCESS) return status; - dc_buffer_clear (frame); - if (!dc_buffer_append (frame, packet, len)) { - ERROR (abstract->context, "Failed to allocate memory."); - return DC_STATUS_NOMEMORY; - } - - return DC_STATUS_SUCCESS; -} - -// Fetch a small whole resource and return its content (everything after -// A5 05 sublen). A status-100 answer is only its first page; /Logbook/Entries -// is paged by suunto_nautic_device_list_entries() instead. -static dc_status_t -suunto_nautic_device_short_fetch (dc_device_t *abstract, const char *path, dc_buffer_t *response) -{ - dc_buffer_t *frame = dc_buffer_new (0); - if (frame == NULL) - return DC_STATUS_NOMEMORY; - - dc_status_t status = suunto_nautic_short_fetch_frame (abstract, path, frame, 1); + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + status = suunto_nautic_transfer (device, RPC_OP_FETCH, RPC_OP_DATA, + handle, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); if (status != DC_STATUS_SUCCESS) { - dc_buffer_free (frame); - return status; + ERROR (abstract->context, "Failed to fetch %s.", path); + goto error_release; } - const unsigned char *packet = dc_buffer_get_data (frame); - size_t len = dc_buffer_get_size (frame); - - unsigned int frame_status = 0; - status = suunto_nautic_data_status (abstract, path, packet, len, &frame_status); - if (status != DC_STATUS_SUCCESS) { - dc_buffer_free (frame); - return status; + unsigned int result = array_uint16_le (reply + REPLY_STATUS); + if (result != STATUS_OK) { + ERROR (abstract->context, "Failed to fetch %s (status %u).", path, result); + status = DC_STATUS_PROTOCOL; + goto error_release; } - dc_buffer_clear (response); - int ok = dc_buffer_append (response, packet + 4, len - 4); - dc_buffer_free (frame); - if (!ok) { + dc_buffer_clear (buffer); + if (!dc_buffer_append (buffer, reply + REPLY_CONTENT, reply_len - REPLY_CONTENT)) { ERROR (abstract->context, "Failed to allocate memory."); - return DC_STATUS_NOMEMORY; + status = DC_STATUS_NOMEMORY; } - return DC_STATUS_SUCCESS; -} - -dc_status_t -suunto_nautic_device_fetch (dc_device_t *abstract, const char *path, dc_buffer_t *response) -{ - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || path == NULL || response == NULL) - return DC_STATUS_INVALIDARGS; - - return suunto_nautic_device_short_fetch (abstract, path, response); -} - -dc_status_t -suunto_nautic_device_fetch_raw (dc_device_t *abstract, const char *path, dc_buffer_t *response) -{ - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || path == NULL || response == NULL) - return DC_STATUS_INVALIDARGS; - - // Diagnostic: the whole first DATA frame of the short fetch, without - // status validation. The ranged form is for /Summary only; /Entries - // rejects it. - return suunto_nautic_short_fetch_frame (abstract, path, response, 1); -} - -dc_status_t -suunto_nautic_device_fetch_paged (dc_device_t *abstract, const char *path, dc_buffer_t *response) -{ - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || path == NULL || response == NULL) - return DC_STATUS_INVALIDARGS; - - return suunto_nautic_device_paginated_fetch (abstract, path, response); -} - -dc_status_t -suunto_nautic_device_download_summary (dc_device_t *abstract, const char *logbook_id, dc_buffer_t *summary) -{ - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || logbook_id == NULL || summary == NULL) - return DC_STATUS_INVALIDARGS; - - char path[128]; - int n = snprintf (path, sizeof (path), "/Logbook/byId/%s/Summary", logbook_id); - if (n < 0 || (size_t) n >= sizeof (path)) - return DC_STATUS_INVALIDARGS; - - // /Summary uses the paginated 0x0D fetch and is NOT compressed -- the - // result is raw SBEM0103 (the caller locates the signature and reads - // its fields, e.g. gradient factors and gas mix). - return suunto_nautic_device_paginated_fetch (abstract, path, summary); +error_release: + suunto_nautic_release (device, handle); + return status; } -// Dive ids and their listed sizes from /Logbook/Entries. -typedef struct suunto_nautic_entries_t { - unsigned int *ids; - unsigned int *sizes; - unsigned int count; - unsigned int capacity; -} suunto_nautic_entries_t; - static void suunto_nautic_entries_free (suunto_nautic_entries_t *entries) { @@ -1176,114 +827,95 @@ suunto_nautic_entries_add (suunto_nautic_entries_t *entries, unsigned int id, un entries->ids[entries->count] = id; entries->sizes[entries->count] = size; entries->count++; - return 1; -} - -// Scan one /Logbook/Entries page for (start, end) pairs, in wire order. Each -// record is [start][end][w1][w2][size][pad]: start and end are 4-aligned LE -// uint32s in the dive-ID window, end within a day after start. A lone in-range -// value with no paired end is a header field (the response's own "current -// time"), not a dive. `sizes` may be NULL; a size past the buffer reads as 0. -static unsigned int -suunto_nautic_scan_entries (const unsigned char *data, size_t size, - unsigned int *ids, unsigned int *sizes, unsigned int max_ids) -{ - unsigned int count = 0; - for (size_t i = 0; i + 8 <= size && count < max_ids; i += 4) { - unsigned int v = array_uint32_le (data + i); - if (v < DIVE_ID_MIN || v > DIVE_ID_MAX) - continue; - unsigned int next = array_uint32_le (data + i + 4); - if (next >= DIVE_ID_MIN && next <= DIVE_ID_MAX && - next > v && next - v <= DIVE_ENTRY_MAX_PAIR_GAP) { - if (sizes) - sizes[count] = i + ENTRY_SIZE_OFFSET + 4 <= size ? array_uint32_le (data + i + ENTRY_SIZE_OFFSET) : 0; - ids[count++] = v; - i += 4; // skip the paired end timestamp - } - } - return count; -} -// Extract dive-start ids from a /Logbook/Entries page, newest-first. -unsigned int -suunto_nautic_extract_entry_ids (const unsigned char *data, size_t size, - unsigned int *ids, unsigned int max_ids) -{ - unsigned int count = suunto_nautic_scan_entries (data, size, ids, NULL, max_ids); - // Sort descending (newest first); insertion sort is fine at logbook scale. - for (unsigned int a = 1; a < count; a++) { - unsigned int key = ids[a]; - int b = (int) a - 1; - while (b >= 0 && ids[b] < key) { ids[b + 1] = ids[b]; b--; } - ids[b + 1] = key; - } - return count; + return 1; } -// List every dive on the watch, newest-first, with its listed size. The list -// is served oldest-first in pages: a status-100 page is followed by a fetch -// with StartAfterId = the last id received, until a status-200 page. Taking -// page 1 alone drops the newest dives. +// List the dives, newest first. The list is served oldest first, in pages: a +// page with status 100 is followed by a fetch for the dives after the last id +// received, until a page with status 200. static dc_status_t -suunto_nautic_device_list_entries (dc_device_t *abstract, suunto_nautic_entries_t *entries) +suunto_nautic_list (suunto_nautic_device_t *device, suunto_nautic_entries_t *entries) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t status = DC_STATUS_SUCCESS; const char *path = "/Logbook/Entries"; memset (entries, 0, sizeof (*entries)); - unsigned char handle[3]; - dc_status_t status = suunto_nautic_get_handle (abstract, path, handle); + unsigned int handle = 0; + status = suunto_nautic_get_handle (device, path, &handle); if (status != DC_STATUS_SUCCESS) return status; - unsigned int start_after = 0; - for (unsigned int page = 0; page < MAX_PAGES; page++) { - unsigned char fetch[32]; - unsigned int fetch_len = 0; + unsigned int last = 0; + for (unsigned int page = 0; ; page++) { + unsigned char params[7] = {0x01, PARAM_UINT32, 0x00}; + array_uint32_le_set (params + 3, last); + + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; if (page == 0) - status = suunto_nautic_build_short_fetch (fetch, sizeof (fetch), &fetch_len, device->sequence, handle); + status = suunto_nautic_transfer (device, RPC_OP_FETCH, RPC_OP_DATA, + handle, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); else - status = suunto_nautic_build_param_fetch (fetch, sizeof (fetch), &fetch_len, - device->sequence, handle, RPC_PARAM_UINT32, start_after); - if (status != DC_STATUS_SUCCESS) - goto error; - device->sequence++; + status = suunto_nautic_transfer (device, RPC_OP_FETCH, RPC_OP_DATA, + handle, params, sizeof (params), reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to fetch %s.", path); + break; + } - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = suunto_nautic_fetch_frame (abstract, path, fetch, fetch_len, handle, 1, packet, &len); - if (status != DC_STATUS_SUCCESS) - goto error; + unsigned int result = array_uint16_le (reply + REPLY_STATUS); + if (result != STATUS_OK && result != STATUS_CONTINUE) { + ERROR (abstract->context, "Failed to fetch %s (status %u).", path, result); + status = DC_STATUS_PROTOCOL; + break; + } - unsigned int frame_status = 0; - status = suunto_nautic_data_status (abstract, path, packet, len, &frame_status); - if (status != DC_STATUS_SUCCESS) - goto error; + const unsigned char *data = reply + REPLY_CONTENT; + size_t size = reply_len - REPLY_CONTENT; + if (size < ENTRIES_COUNT + 2) { + ERROR (abstract->context, "Unexpected entries size (" DC_PRINTF_SIZE ").", size); + status = DC_STATUS_PROTOCOL; + break; + } + + unsigned int count = array_uint16_le (data + ENTRIES_COUNT); + if (count && ENTRIES_HEADER + count * ENTRY_SIZE > size) { + ERROR (abstract->context, "Unexpected number of entries (%u " DC_PRINTF_SIZE ").", count, size); + status = DC_STATUS_PROTOCOL; + break; + } - unsigned int ids[MAX_PACKET / 8]; - unsigned int sizes[MAX_PACKET / 8]; - unsigned int count = suunto_nautic_scan_entries (packet + 4, len - 4, ids, sizes, MAX_PACKET / 8); + unsigned int previous = last; for (unsigned int i = 0; i < count; i++) { - if (!suunto_nautic_entries_add (entries, ids[i], sizes[i])) { + const unsigned char *record = data + ENTRIES_HEADER + i * ENTRY_SIZE; + last = array_uint32_le (record + ENTRY_START); + if (!suunto_nautic_entries_add (entries, last, array_uint32_le (record + ENTRY_LENGTH))) { ERROR (abstract->context, "Failed to allocate memory."); status = DC_STATUS_NOMEMORY; - goto error; + break; } } + if (status != DC_STATUS_SUCCESS) + break; - if (frame_status == RPC_STATUS_OK) + if (result == STATUS_OK) break; - if (count == 0 || ids[count - 1] == start_after) { - WARNING (abstract->context, "%s page %u has no new entries; stopping the listing.", path, page + 1); + if (count == 0 || last == previous) { + ERROR (abstract->context, "No new entries with more pages pending."); + status = DC_STATUS_PROTOCOL; break; } - start_after = ids[count - 1]; + } - if (page + 1 == MAX_PAGES) - WARNING (abstract->context, "%s hit the page limit; the newest dives may be missing.", path); + suunto_nautic_release (device, handle); + + if (status != DC_STATUS_SUCCESS) { + suunto_nautic_entries_free (entries); + return status; } // Sort descending (newest first), keeping each size with its id. @@ -1299,527 +931,642 @@ suunto_nautic_device_list_entries (dc_device_t *abstract, suunto_nautic_entries_ entries->sizes[b + 1] = size; } - return DC_STATUS_SUCCESS; - -error: - suunto_nautic_entries_free (entries); return status; } -// Public: list dive ids (each a UNIX-timestamp LogId) newest-first, without -// downloading the dives. Writes the ids as packed little-endian uint32 into -// `out`; the caller reads them as a plain array (no protocol knowledge needed). -dc_status_t -suunto_nautic_device_list (dc_device_t *abstract, dc_buffer_t *out) -{ - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || out == NULL) - return DC_STATUS_INVALIDARGS; - - suunto_nautic_entries_t entries; - dc_status_t status = suunto_nautic_device_list_entries (abstract, &entries); - if (status != DC_STATUS_SUCCESS) - return status; - - dc_buffer_clear (out); - for (unsigned int i = 0; i < entries.count; i++) { - unsigned char le[4]; - array_uint32_le_set (le, entries.ids[i]); - if (!dc_buffer_append (out, le, sizeof (le))) { - suunto_nautic_entries_free (&entries); - return DC_STATUS_NOMEMORY; - } - } - - suunto_nautic_entries_free (&entries); - return DC_STATUS_SUCCESS; -} - -// Download one dive: the compressed /Data stream (retried while the watch -// refuses it), decompressed, with the /Summary appended. When `listed_size` -// is known (non-zero) the download is checked against it: the listed size is -// compressed /Data + /Summary data bytes, exactly. A mismatch is retried once -// and then reported through `*incomplete`, with the dive left in `raw`. -// `*unreadable` is set on a failure where the transfer completed and matched -// the listing but holds no decodable dive (an empty/aborted logbook entry): -// downloading it again cannot help. Any other failure may succeed later. +// Download one dive: the decompressed /Data stream with the /Summary +// appended. The listed size is the compressed /Data plus the /Summary data +// bytes, exactly. A mismatch is retried once and then reported through +// `incomplete`, with the dive left in `buffer`. `unreadable` is set when the +// transfer matched the listing but holds no dive (an empty or aborted +// logbook entry), so downloading it again cannot help. static dc_status_t -suunto_nautic_device_download_dive (dc_device_t *abstract, const char *logbook_id, unsigned int listed_size, - dc_buffer_t *raw, int *incomplete, int *unreadable) +suunto_nautic_download (suunto_nautic_device_t *device, unsigned int id, unsigned int listed, + dc_buffer_t *buffer, int *incomplete, int *unreadable) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + dc_device_t *abstract = (dc_device_t *) device; dc_status_t status = DC_STATUS_SUCCESS; *incomplete = 0; *unreadable = 0; - char path[128]; - int n = snprintf (path, sizeof (path), "/Logbook/byId/%s/Data", logbook_id); - if (n < 0 || (size_t) n >= sizeof (path)) - return DC_STATUS_INVALIDARGS; + char data_path[64], summary_path[64]; + snprintf (data_path, sizeof (data_path), "/Logbook/byId/%u/Data", id); + snprintf (summary_path, sizeof (summary_path), "/Logbook/byId/%u/Summary", id); dc_buffer_t *compressed = dc_buffer_new (0); dc_buffer_t *summary = dc_buffer_new (0); if (compressed == NULL || summary == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); status = DC_STATUS_NOMEMORY; - goto done; + goto error_free; } for (unsigned int attempt = 0; attempt < DOWNLOAD_ATTEMPTS; attempt++) { - for (unsigned int s = 0; s < STREAM_ATTEMPTS; s++) { - if (s > 0) { - WARNING (abstract->context, "Retrying the stream of %s (attempt %u/%u).", logbook_id, s + 1, STREAM_ATTEMPTS); - dc_iostream_sleep (device->iostream, 1500 * s); + for (unsigned int i = 0; i < STREAM_ATTEMPTS; i++) { + if (i > 0) { + WARNING (abstract->context, "Retrying the stream of dive %u.", id); + dc_iostream_sleep (device->iostream, 1500 * i); } - dc_buffer_clear (compressed); - status = suunto_nautic_device_stream_fetch (abstract, path, compressed); + status = suunto_nautic_stream (device, data_path, compressed); if (status != DC_STATUS_PROTOCOL) break; } if (status != DC_STATUS_SUCCESS) - goto done; + goto error_free; - DEBUG (abstract->context, "Captured " DC_PRINTF_SIZE " compressed bytes for logbook entry %s.", - dc_buffer_get_size (compressed), logbook_id); - - // Best-effort: the profile alone is still a valid dive without it. + // The profile is still a valid dive without the /Summary. size_t summary_size = 0; - if (suunto_nautic_device_download_summary (abstract, logbook_id, summary) == DC_STATUS_SUCCESS) + if (suunto_nautic_fetch_pages (device, summary_path, summary) == DC_STATUS_SUCCESS) { summary_size = dc_buffer_get_size (summary); - else { + } else { + WARNING (abstract->context, "Failed to fetch the summary of dive %u.", id); dc_buffer_clear (summary); - WARNING (abstract->context, "Failed to fetch the Summary for %s; GF/gas will be unavailable.", logbook_id); } - if (listed_size == 0) + if (listed == 0) break; size_t data_size = dc_buffer_get_size (compressed); if (summary_size) - *incomplete = data_size + summary_size != listed_size; + *incomplete = data_size + summary_size != listed; else - *incomplete = data_size > listed_size || listed_size - data_size > SUMMARY_MAX_SIZE; + *incomplete = data_size > listed || listed - data_size > SUMMARY_MAX_SIZE; if (!*incomplete) break; - WARNING (abstract->context, "Incomplete download of %s: /Data " DC_PRINTF_SIZE " + /Summary " DC_PRINTF_SIZE - " bytes, listed %u (attempt %u/%u).", logbook_id, data_size, summary_size, listed_size, - attempt + 1, DOWNLOAD_ATTEMPTS); + WARNING (abstract->context, "Incomplete download of dive %u (" DC_PRINTF_SIZE " + " DC_PRINTF_SIZE " bytes, listed %u).", + id, data_size, summary_size, listed); } status = suunto_nautic_heatshrink_decompress (abstract->context, - dc_buffer_get_data (compressed), dc_buffer_get_size (compressed), raw); + dc_buffer_get_data (compressed), dc_buffer_get_size (compressed), buffer); if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to decompress the logbook entry."); + ERROR (abstract->context, "Failed to decompress dive %u.", id); *unreadable = status == DC_STATUS_DATAFORMAT && !*incomplete; - goto done; + goto error_free; } - if (dc_buffer_get_size (raw) < sizeof (SBEM_MAGIC) || - memcmp (dc_buffer_get_data (raw), SBEM_MAGIC, sizeof (SBEM_MAGIC)) != 0) { - ERROR (abstract->context, "Unexpected magic in the decompressed data."); + if (dc_buffer_get_size (buffer) < sizeof (SBEM_MAGIC) || + memcmp (dc_buffer_get_data (buffer), SBEM_MAGIC, sizeof (SBEM_MAGIC)) != 0) { + ERROR (abstract->context, "Unexpected magic in dive %u.", id); status = DC_STATUS_DATAFORMAT; *unreadable = !*incomplete; - goto done; + goto error_free; } - DEBUG (abstract->context, "Decompressed " DC_PRINTF_SIZE " bytes for logbook entry %s.", - dc_buffer_get_size (raw), logbook_id); - - // The /Summary SBEM (gradient factors, gas mix) goes after the profile - // so the parser can expose them; they aren't in the profile stream. - if (dc_buffer_get_size (summary) && - !dc_buffer_append (raw, dc_buffer_get_data (summary), dc_buffer_get_size (summary))) - WARNING (abstract->context, "Failed to append the Summary; GF/gas will be unavailable."); + // The /Summary goes after the profile, so the parser can read the fields + // that are not in the profile (gradient factors, gas mixes). + if (!dc_buffer_append (buffer, dc_buffer_get_data (summary), dc_buffer_get_size (summary))) { + ERROR (abstract->context, "Failed to allocate memory."); + status = DC_STATUS_NOMEMORY; + } -done: +error_free: if (status != DC_STATUS_SUCCESS) - dc_buffer_clear (raw); + dc_buffer_clear (buffer); dc_buffer_free (summary); dc_buffer_free (compressed); return status; } -dc_status_t -suunto_nautic_device_download (dc_device_t *abstract, const char *logbook_id, dc_buffer_t *raw) +// True for a token that is exactly "..", each part +// < 256; on success fills a/b/c. +static int +suunto_nautic_parse_version (const char *tok, size_t len, unsigned int *a, unsigned int *b, unsigned int *c) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || logbook_id == NULL || raw == NULL) - return DC_STATUS_INVALIDARGS; - - // Look up the listed size to verify against; best-effort, since an - // unlisted or unlistable dive can still be downloaded. - unsigned int listed_size = 0; - suunto_nautic_entries_t entries; - if (suunto_nautic_device_list_entries (abstract, &entries) == DC_STATUS_SUCCESS) { - unsigned long id = strtoul (logbook_id, NULL, 10); - for (unsigned int i = 0; i < entries.count; i++) { - if (entries.ids[i] == id) - listed_size = entries.sizes[i]; + unsigned int part[3] = {0}, idx = 0, digits = 0; + for (size_t i = 0; i < len; i++) { + char ch = tok[i]; + if (ch >= '0' && ch <= '9') { + part[idx] = part[idx] * 10 + (unsigned int) (ch - '0'); + if (part[idx] > 255 || ++digits > 3) + return 0; + } else if (ch == '.') { + if (digits == 0 || ++idx > 2) + return 0; + digits = 0; + } else { + return 0; } - suunto_nautic_entries_free (&entries); } + if (idx != 2 || digits == 0) + return 0; + *a = part[0]; *b = part[1]; *c = part[2]; + return 1; +} - int incomplete = 0, unreadable = 0; - dc_status_t status = suunto_nautic_device_download_dive (abstract, logbook_id, listed_size, raw, &incomplete, &unreadable); - if (status == DC_STATUS_SUCCESS && incomplete) - return DC_STATUS_DATAFORMAT; - return status; +// True for a token that is exactly 12 chars, all [0-9A-F] -- the watch serial +// form, e.g. "2604C3003306". +static int +suunto_nautic_is_serial (const char *tok, size_t len) +{ + if (len != 12) + return 0; + for (size_t i = 0; i < len; i++) { + char ch = tok[i]; + if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F'))) + return 0; + } + return 1; } -// True for a token that is exactly "..", each part -// < 256; on success fills a/b/c. -// Send a handle-addressed request (subscribe/unsubscribe) and read until the -// reply with `reply_op` echoing its message id; other frames on the link are -// skipped. On success `reply` holds the whole reply frame. +// /Info is a run of NUL-terminated strings mixed with binary fields: +// +// Suunto\0 Nautic\0 Vaasa\0 T\0 2604C3003306\0 ... 2.55.46\0 ... +// BLE Mac Address\0 \0 WiFi Mac Address\0 \0 +// +// The serial is the first 12-character uppercase hex string, and the firmware +// the first "N.N.N" string, which follows the serial. The MAC addresses after +// it are also 12 hex characters. The serial is reported as its low 32 bits. static dc_status_t -suunto_nautic_handle_request (dc_device_t *abstract, unsigned char opcode, unsigned char reply_op, - const unsigned char handle[3], dc_buffer_t *reply) +suunto_nautic_devinfo (suunto_nautic_device_t *device) { - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - - if (device_is_cancelled (abstract)) - return DC_STATUS_CANCELLED; + dc_device_t *abstract = (dc_device_t *) device; + dc_status_t status = DC_STATUS_SUCCESS; - unsigned char req[32]; - unsigned int req_len = 0; - unsigned int msgid = device->sequence & 0xFFFF; - dc_status_t status = suunto_nautic_build_handle_op (req, sizeof (req), &req_len, msgid, opcode, handle); - if (status != DC_STATUS_SUCCESS) - return status; - device->sequence++; + dc_buffer_t *info = dc_buffer_new (0); + if (info == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + return DC_STATUS_NOMEMORY; + } - status = dc_iostream_write (device->iostream, req, req_len, NULL); + status = suunto_nautic_fetch (device, "/Info", info); if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to send the 0x%02x request.", opcode); + ERROR (abstract->context, "Failed to read the device info."); + dc_buffer_free (info); return status; } - unsigned int skips = 0; - for (;;) { - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = dc_iostream_read (device->iostream, packet, sizeof (packet), &len); - if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to receive the reply to the 0x%02x request.", opcode); - return status; - } - if (len >= RPC_STATUS_OFFSET + 2 && packet[0] == 0xA5 && packet[1] == reply_op && - array_uint16_le (packet + RPC_MSGID_OFFSET) == msgid) { - dc_buffer_clear (reply); - if (!dc_buffer_append (reply, packet, len)) - return DC_STATUS_NOMEMORY; - return DC_STATUS_SUCCESS; - } - if (++skips >= MAX_FOREIGN_SKIPS) { - ERROR (abstract->context, "No reply to the 0x%02x request among the frames on the link.", opcode); - return DC_STATUS_TIMEOUT; + const unsigned char *data = dc_buffer_get_data (info); + size_t size = dc_buffer_get_size (info); + + dc_event_devinfo_t devinfo; + memset (&devinfo, 0, sizeof (devinfo)); + + size_t start = 0; + for (size_t i = 0; i < size; i++) { + if (data[i] != 0) + continue; + const char *token = (const char *) (data + start); + size_t length = i - start; + unsigned int a, b, c; + if (!devinfo.firmware && suunto_nautic_parse_version (token, length, &a, &b, &c)) { + devinfo.firmware = (a << 16) | (b << 8) | c; + } else if (!devinfo.firmware && !devinfo.serial && suunto_nautic_is_serial (token, length)) { + char serial[13]; + memcpy (serial, token, 12); + serial[12] = 0; + devinfo.serial = (unsigned int) strtoul (serial, NULL, 16); } + start = i + 1; } + + dc_buffer_free (info); + + if (devinfo.firmware == 0 || devinfo.serial == 0) { + ERROR (abstract->context, "Failed to find the firmware and serial in the device info."); + return DC_STATUS_DATAFORMAT; + } + + device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); + + return status; } -dc_status_t -suunto_nautic_device_subscribe (dc_device_t *abstract, const char *path, unsigned char handle[3], dc_buffer_t *value) +static dc_status_t +suunto_nautic_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || path == NULL || handle == NULL) - return DC_STATUS_INVALIDARGS; + dc_status_t status = DC_STATUS_SUCCESS; + suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + progress.maximum = 2; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); - dc_status_t status = suunto_nautic_get_handle (abstract, path, handle); + status = suunto_nautic_devinfo (device); if (status != DC_STATUS_SUCCESS) return status; - // An unknown path is acknowledged with the FF FF FF handle (and a 404). - if (handle[0] == 0xFF && handle[1] == 0xFF && handle[2] == 0xFF) { - WARNING (abstract->context, "The watch has no %s resource.", path); - return DC_STATUS_UNSUPPORTED; + progress.current = 1; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + suunto_nautic_entries_t entries; + status = suunto_nautic_list (device, &entries); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to list the dives."); + return status; } - dc_buffer_t *reply = dc_buffer_new (0); - if (reply == NULL) + progress.maximum = (entries.count + 1) * 2; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + dc_buffer_t *buffer = dc_buffer_new (0); + if (buffer == NULL) { + ERROR (abstract->context, "Failed to allocate memory."); + suunto_nautic_entries_free (&entries); return DC_STATUS_NOMEMORY; + } - status = suunto_nautic_handle_request (abstract, RPC_OP_SUBSCRIBE, RPC_OP_SUBSCRIBE_REPLY, handle, reply); - if (status == DC_STATUS_SUCCESS) { - const unsigned char *packet = dc_buffer_get_data (reply); - size_t len = dc_buffer_get_size (reply); - unsigned int frame_status = array_uint16_le (packet + RPC_STATUS_OFFSET); - if (frame_status != RPC_STATUS_OK) { - ERROR (abstract->context, "Watch refused the subscription to %s (status %u).", path, frame_status); - status = DC_STATUS_PROTOCOL; - } else if (value) { - // The reply carries the current value: [type:u16][value], then the CRC. - size_t start = RPC_STATUS_OFFSET + 2; - dc_buffer_clear (value); - if (len > start + RPC_CRC_SIZE && !dc_buffer_append (value, packet + start, len - start - RPC_CRC_SIZE)) - status = DC_STATUS_NOMEMORY; + for (unsigned int i = 0; i < entries.count; i++) { + unsigned char fingerprint[4]; + array_uint32_le_set (fingerprint, entries.ids[i]); + + // The dives are newest first, so everything from here on was + // downloaded before. + if (memcmp (fingerprint, device->fingerprint, sizeof (fingerprint)) == 0) + break; + + if (device_is_cancelled (abstract)) { + status = DC_STATUS_CANCELLED; + break; } + + int incomplete = 0, unreadable = 0; + status = suunto_nautic_download (device, entries.ids[i], entries.sizes[i], buffer, &incomplete, &unreadable); + if (status != DC_STATUS_SUCCESS && unreadable) { + // An empty or aborted entry: listed, but without a dive. + WARNING (abstract->context, "Skipping dive %u without any data.", entries.ids[i]); + status = DC_STATUS_SUCCESS; + continue; + } + if (status != DC_STATUS_SUCCESS) { + // Stop here: the fingerprint of a newer dive would mark this one + // as downloaded. + ERROR (abstract->context, "Failed to download dive %u.", entries.ids[i]); + break; + } + + // Delivered anyway: skipping it would move the fingerprint past it. + if (incomplete) + WARNING (abstract->context, "Dive %u doesn't match its listed size and may be truncated.", entries.ids[i]); + + progress.current += 2; + device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + + if (callback && !callback (dc_buffer_get_data (buffer), dc_buffer_get_size (buffer), fingerprint, sizeof (fingerprint), userdata)) + break; } - dc_buffer_free (reply); + dc_buffer_free (buffer); + suunto_nautic_entries_free (&entries); + return status; } -dc_status_t -suunto_nautic_device_unsubscribe (dc_device_t *abstract, const unsigned char handle[3]) +/* + * Extension API (include/libdivecomputer/suunto_nautic.h): direct access for + * tools that list, download or explore the watch outside dc_device_foreach(). + */ + +static int +suunto_nautic_is_device (dc_device_t *abstract) +{ + return abstract != NULL && abstract->vtable->type == DC_FAMILY_SUUNTO_NAUTIC; +} + +// Rebuild a received packet around its payload. The payload passed the +// checksum, so the packet is byte for byte the one the watch sent. +static dc_status_t +suunto_nautic_packet (dc_buffer_t *buffer, unsigned char opcode, unsigned int seqnum, const unsigned char data[], size_t size) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || handle == NULL) + unsigned char packet[RPC_OVERHEAD + MAX_PAYLOAD] = {0}; + + if (size > MAX_PAYLOAD) return DC_STATUS_INVALIDARGS; - dc_buffer_t *reply = dc_buffer_new (0); - if (reply == NULL) + packet[0] = RPC_START; + packet[1] = opcode; + array_uint16_le_set (packet + 2, size); + array_uint16_le_set (packet + 4, seqnum); + memcpy (packet + 6, data, size); + array_uint32_le_set (packet + 6 + size, checksum_crc32r (packet, 6 + size)); + + dc_buffer_clear (buffer); + if (!dc_buffer_append (buffer, packet, RPC_OVERHEAD + size)) return DC_STATUS_NOMEMORY; - 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; + return DC_STATUS_SUCCESS; +} + +int +suunto_nautic_device_handshake_acknowledged (dc_device_t *abstract) +{ + if (!suunto_nautic_is_device (abstract)) + return 0; + + return ((suunto_nautic_device_t *) abstract)->acknowledged; } dc_status_t -suunto_nautic_device_wait_notification (dc_device_t *abstract, unsigned int timeout, unsigned char handle[3], dc_buffer_t *value) +suunto_nautic_device_request (dc_device_t *abstract, const char *path, dc_buffer_t *response) { - if (abstract == NULL || abstract->vtable->type != DC_FAMILY_SUUNTO_NAUTIC || handle == NULL || value == NULL) + if (!suunto_nautic_is_device (abstract) || path == NULL || response == NULL) return DC_STATUS_INVALIDARGS; suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + dc_status_t status = DC_STATUS_SUCCESS; - if (device_is_cancelled (abstract)) - return DC_STATUS_CANCELLED; + size_t pathlen = strlen (path); + if (pathlen == 0 || pathlen > MAX_PATH) + return DC_STATUS_INVALIDARGS; - dc_status_t status = dc_iostream_set_timeout (device->iostream, (int) timeout); + unsigned char request[4 + MAX_PATH] = {0x01, 0x80, 0x00}; + request[3] = pathlen; + memcpy (request + 4, path, pathlen); + + unsigned int seqnum = suunto_nautic_seqnum (device); + status = suunto_nautic_send (device, RPC_OP_GET, seqnum, request, 4 + pathlen); if (status != DC_STATUS_SUCCESS) return status; - // A value notification is [A5 01 sublen:2][msgid:2][handle:3][01 80 00][type:u16][value...][crc:4]. - const size_t header = RPC_HANDLE_OFFSET + 3 + 3; - for (unsigned int skips = 0; skips < MAX_FOREIGN_SKIPS; skips++) { - unsigned char packet[MAX_PACKET] = {0}; - size_t len = 0; - status = dc_iostream_read (device->iostream, packet, sizeof (packet), &len); - if (status != DC_STATUS_SUCCESS) - break; - HEXDUMP (abstract->context, DC_LOGLEVEL_DEBUG, "NOTIFY", packet, len); - if (len < header + RPC_CRC_SIZE || packet[0] != 0xA5 || packet[1] != RPC_OP_NOTIFICATION) - continue; - memcpy (handle, packet + RPC_HANDLE_OFFSET, 3); - dc_buffer_clear (value); - if (!dc_buffer_append (value, packet + header, len - header - RPC_CRC_SIZE)) + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + status = suunto_nautic_recv (device, RPC_OP_GET_REPLY, seqnum, reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS) + return status; + + // The whole reply packet. + return suunto_nautic_packet (response, RPC_OP_GET_REPLY, seqnum, reply, reply_len); +} + +// GET, a FETCH without parameters, RELEASE. Returns the whole reply packet. +static dc_status_t +suunto_nautic_fetch_packet (suunto_nautic_device_t *device, const char *path, dc_buffer_t *packet, unsigned int *result) +{ + unsigned int handle = 0; + dc_status_t status = suunto_nautic_get_handle (device, path, &handle); + if (status != DC_STATUS_SUCCESS) + return status; + + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + status = suunto_nautic_transfer (device, RPC_OP_FETCH, RPC_OP_DATA, + handle, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); + unsigned int seqnum = device->seqnum; + suunto_nautic_release (device, handle); + if (status != DC_STATUS_SUCCESS) + return status; + + *result = array_uint16_le (reply + REPLY_STATUS); + return suunto_nautic_packet (packet, RPC_OP_DATA, seqnum, reply, reply_len); +} + +dc_status_t +suunto_nautic_device_fetch (dc_device_t *abstract, const char *path, dc_buffer_t *response) +{ + if (!suunto_nautic_is_device (abstract) || path == NULL || response == NULL) + return DC_STATUS_INVALIDARGS; + + dc_buffer_t *packet = dc_buffer_new (0); + if (packet == NULL) + return DC_STATUS_NOMEMORY; + + unsigned int result = 0; + dc_status_t status = suunto_nautic_fetch_packet ((suunto_nautic_device_t *) abstract, path, packet, &result); + if (status == DC_STATUS_SUCCESS && result != STATUS_OK && result != STATUS_CONTINUE) { + ERROR (abstract->context, "Failed to fetch %s (status %u).", path, result); + status = DC_STATUS_PROTOCOL; + } + + // Everything after the start byte, opcode and length. A status 100 reply + // is the first page only. + if (status == DC_STATUS_SUCCESS) { + dc_buffer_clear (response); + if (!dc_buffer_append (response, dc_buffer_get_data (packet) + 4, dc_buffer_get_size (packet) - 4)) status = DC_STATUS_NOMEMORY; - break; } - dc_status_t restore = dc_iostream_set_timeout (device->iostream, 5000); - return status != DC_STATUS_SUCCESS ? status : restore; + dc_buffer_free (packet); + return status; } -static int -suunto_nautic_parse_version (const char *tok, size_t len, unsigned int *a, unsigned int *b, unsigned int *c) +dc_status_t +suunto_nautic_device_fetch_raw (dc_device_t *abstract, const char *path, dc_buffer_t *response) { - unsigned int part[3] = {0}, idx = 0, digits = 0; - for (size_t i = 0; i < len; i++) { - char ch = tok[i]; - if (ch >= '0' && ch <= '9') { - part[idx] = part[idx] * 10 + (unsigned int) (ch - '0'); - if (part[idx] > 255 || ++digits > 3) - return 0; - } else if (ch == '.') { - if (digits == 0 || ++idx > 2) - return 0; - digits = 0; - } else { - return 0; + if (!suunto_nautic_is_device (abstract) || path == NULL || response == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int result = 0; + return suunto_nautic_fetch_packet ((suunto_nautic_device_t *) abstract, path, response, &result); +} + +dc_status_t +suunto_nautic_device_list (dc_device_t *abstract, dc_buffer_t *ids) +{ + if (!suunto_nautic_is_device (abstract) || ids == NULL) + return DC_STATUS_INVALIDARGS; + + suunto_nautic_entries_t entries; + dc_status_t status = suunto_nautic_list ((suunto_nautic_device_t *) abstract, &entries); + if (status != DC_STATUS_SUCCESS) + return status; + + dc_buffer_clear (ids); + for (unsigned int i = 0; i < entries.count; i++) { + unsigned char id[4]; + array_uint32_le_set (id, entries.ids[i]); + if (!dc_buffer_append (ids, id, sizeof (id))) { + status = DC_STATUS_NOMEMORY; + break; } } - if (idx != 2 || digits == 0) - return 0; - *a = part[0]; *b = part[1]; *c = part[2]; - return 1; + + suunto_nautic_entries_free (&entries); + return status; } -// True for a token that is exactly 12 chars, all [0-9A-F] -- the watch serial -// form, e.g. "2604C3003306". -static int -suunto_nautic_is_serial (const char *tok, size_t len) +// Scans a raw /Logbook/Entries response for 4-aligned (start, end) timestamp +// pairs. The driver parses the records instead; kept for existing callers. +unsigned int +suunto_nautic_extract_entry_ids (const unsigned char data[], size_t size, unsigned int *ids, unsigned int max_ids) { - if (len != 12) - return 0; - for (size_t i = 0; i < len; i++) { - char ch = tok[i]; - if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F'))) - return 0; + unsigned int count = 0; + for (size_t i = 0; i + 8 <= size && count < max_ids; i += 4) { + unsigned int start = array_uint32_le (data + i); + if (start < 1500000000u || start > 2100000000u) + continue; + unsigned int end = array_uint32_le (data + i + 4); + if (end >= 1500000000u && end <= 2100000000u && end > start && end - start <= 86400u) { + ids[count++] = start; + i += 4; + } } - return 1; + + for (unsigned int a = 1; a < count; a++) { + unsigned int key = ids[a]; + int b = (int) a - 1; + while (b >= 0 && ids[b] < key) { + ids[b + 1] = ids[b]; + b--; + } + ids[b + 1] = key; + } + + return count; } -/* - * Best-effort device info. A short fetch of /Info returns a device-identity - * record: a run of NUL-separated strings mixed in with binary framing bytes, - * - * Suunto\0 Nautic\0 Vaasa\0 T\0 2604C3003306\0 01385D42<...>\0 - * 2.55.46\0 \0 ... SIM\0 \0 BID\0 \0 - * BLE Mac Address\0 \0 WiFi Mac Address\0 \0 - * - * (confirmed on a live Nautic, firmware 2.55.46, via fetch_device_info.py). - * The watch serial is the first 12-char uppercase-hex token and the firmware - * the first "N.N.N" token; the serial always precedes the firmware, and the - * BLE/WiFi MACs (also 12 hex) come after it -- so stop looking for a serial - * once the firmware token is seen. Layered on top of the download; every - * failure path here is non-fatal. - * - * dc_event_devinfo_t carries unsigned ints only: the firmware is packed - * (a << 16) | (b << 8) | c and the hex serial truncated to its low 32 bits. - * A consumer wanting the faithful strings should read the BLE advertised - * name (serial) or GET /Info directly (firmware). - */ -static void -suunto_nautic_emit_devinfo (dc_device_t *abstract) +dc_status_t +suunto_nautic_device_fetch_paged (dc_device_t *abstract, const char *path, dc_buffer_t *response) { - dc_buffer_t *info = dc_buffer_new (0); - if (info == NULL) - return; + if (!suunto_nautic_is_device (abstract) || path == NULL || response == NULL) + return DC_STATUS_INVALIDARGS; - if (suunto_nautic_device_short_fetch (abstract, "/Info", info) != DC_STATUS_SUCCESS) { - dc_buffer_free (info); - return; // not fatal -- just no device info this time - } + return suunto_nautic_fetch_pages ((suunto_nautic_device_t *) abstract, path, response); +} - const unsigned char *d = dc_buffer_get_data (info); - size_t n = dc_buffer_get_size (info); +dc_status_t +suunto_nautic_device_download_summary (dc_device_t *abstract, const char *logbook_id, dc_buffer_t *summary) +{ + if (!suunto_nautic_is_device (abstract) || logbook_id == NULL || summary == NULL) + return DC_STATUS_INVALIDARGS; - dc_event_devinfo_t devinfo; - memset (&devinfo, 0, sizeof (devinfo)); + char path[128]; + int n = snprintf (path, sizeof (path), "/Logbook/byId/%s/Summary", logbook_id); + if (n < 0 || (size_t) n >= sizeof (path)) + return DC_STATUS_INVALIDARGS; - // Walk NUL-delimited tokens; the record's strings are NUL-terminated, and - // the binary framing bytes between them fail both tests. - size_t start = 0; - for (size_t i = 0; i < n; i++) { - if (d[i] != 0) - continue; - const char *tok = (const char *) (d + start); - size_t toklen = i - start; - unsigned int a, b, c; - if (!devinfo.firmware && suunto_nautic_parse_version (tok, toklen, &a, &b, &c)) - devinfo.firmware = (a << 16) | (b << 8) | c; - else if (!devinfo.firmware && !devinfo.serial && suunto_nautic_is_serial (tok, toklen)) { - char buf[13]; - memcpy (buf, tok, 12); - buf[12] = 0; - devinfo.serial = (unsigned int) strtoul (buf, NULL, 16); + return suunto_nautic_fetch_pages ((suunto_nautic_device_t *) abstract, path, summary); +} + +dc_status_t +suunto_nautic_device_download (dc_device_t *abstract, const char *logbook_id, dc_buffer_t *raw) +{ + if (!suunto_nautic_is_device (abstract) || logbook_id == NULL || raw == NULL) + return DC_STATUS_INVALIDARGS; + + suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + + char *end = NULL; + unsigned long id = strtoul (logbook_id, &end, 10); + if (end == logbook_id || *end != 0 || id == 0 || id > 0xFFFFFFFFul) + return DC_STATUS_INVALIDARGS; + + // The listed size to check the download against; a dive that isn't + // listed is downloaded without the check. + unsigned int listed = 0; + suunto_nautic_entries_t entries; + if (suunto_nautic_list (device, &entries) == DC_STATUS_SUCCESS) { + for (unsigned int i = 0; i < entries.count; i++) { + if (entries.ids[i] == id) + listed = entries.sizes[i]; } - start = i + 1; + suunto_nautic_entries_free (&entries); } - dc_buffer_free (info); + int incomplete = 0, unreadable = 0; + dc_status_t status = suunto_nautic_download (device, (unsigned int) id, listed, raw, &incomplete, &unreadable); + if (status == DC_STATUS_SUCCESS && incomplete) + return DC_STATUS_DATAFORMAT; - if (devinfo.firmware || devinfo.serial) { - INFO (abstract->context, "Device info: firmware=%u.%u.%u serial=0x%08x", - (devinfo.firmware >> 16) & 0xFF, (devinfo.firmware >> 8) & 0xFF, - devinfo.firmware & 0xFF, devinfo.serial); - device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); - } + return status; } -static dc_status_t -suunto_nautic_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +dc_status_t +suunto_nautic_device_subscribe (dc_device_t *abstract, const char *path, unsigned char handle[3], dc_buffer_t *value) { - dc_status_t status = DC_STATUS_SUCCESS; - suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - - dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; - progress.maximum = 2; - device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + if (!suunto_nautic_is_device (abstract) || path == NULL || handle == NULL) + return DC_STATUS_INVALIDARGS; - // Connectivity/auth check. Any path works here; /System/Mode is a - // fixed, id-less endpoint so it works identically on every unit. - dc_buffer_t *mode = dc_buffer_new (0); - if (mode == NULL) - return DC_STATUS_NOMEMORY; + suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - status = suunto_nautic_device_request (abstract, "/System/Mode", mode); - if (status != DC_STATUS_SUCCESS) { - dc_buffer_free (mode); - ERROR (abstract->context, "Failed to reach /System/Mode. The EVA handshake or RPC " - "framing may need updating for this device (see suunto_nautic.h)."); + unsigned int h = 0, result = 0; + dc_status_t status = suunto_nautic_get (device, path, &h, &result); + if (status != DC_STATUS_SUCCESS) return status; + + if (HANDLE_SLOT (h) == SLOT_UNKNOWN) { + WARNING (abstract->context, "The watch has no %s resource.", path); + return DC_STATUS_UNSUPPORTED; } + if (result != STATUS_OK) { + ERROR (abstract->context, "Failed to resolve %s (status %u).", path, result); + return DC_STATUS_PROTOCOL; + } + array_uint24_le_set (handle, h); - dc_event_vendor_t vendor; - vendor.data = dc_buffer_get_data (mode); - vendor.size = (unsigned int) dc_buffer_get_size (mode); - device_event_emit (abstract, DC_EVENT_VENDOR, &vendor); - dc_buffer_free (mode); + // A still bound 00 slot was left by a link that dropped. + if (HANDLE_SLOT (h) == SLOT_ALT) { + WARNING (abstract->context, "Releasing the stale slot of %s.", path); + suunto_nautic_release (device, (h & ~0xFFu) | SLOT_DEFAULT); + } - // Best-effort firmware / serial via DC_EVENT_DEVINFO; never fatal. - suunto_nautic_emit_devinfo (abstract); + // The reply carries the current value: TYPE(2) VALUE. + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + status = suunto_nautic_transfer (device, RPC_OP_SUBSCRIBE, RPC_OP_SUBSCRIBE_REPLY, + h, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS) + return status; - progress.current = 1; - device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + result = array_uint16_le (reply + REPLY_STATUS); + if (result != STATUS_OK) { + ERROR (abstract->context, "The watch refused the subscription to %s (status %u).", path, result); + return DC_STATUS_PROTOCOL; + } - // Every listed dive, newest-first, with its listed size (all pages). - suunto_nautic_entries_t entries; - status = suunto_nautic_device_list_entries (abstract, &entries); - if (status != DC_STATUS_SUCCESS) { - ERROR (abstract->context, "Failed to fetch /Logbook/Entries."); - return status; + if (value) { + dc_buffer_clear (value); + if (!dc_buffer_append (value, reply + REPLY_CONTENT, reply_len - REPLY_CONTENT)) + return DC_STATUS_NOMEMORY; } - unsigned int count = entries.count; - progress.maximum = (count + 1) * 2; - device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + return status; +} - dc_buffer_t *raw = dc_buffer_new (0); - if (raw == NULL) { - suunto_nautic_entries_free (&entries); - return DC_STATUS_NOMEMORY; - } +dc_status_t +suunto_nautic_device_unsubscribe (dc_device_t *abstract, const unsigned char handle[3]) +{ + if (!suunto_nautic_is_device (abstract) || handle == NULL) + return DC_STATUS_INVALIDARGS; - for (unsigned int i = 0; i < count; i++) { - unsigned char fingerprint[4]; - array_uint32_le_set (fingerprint, entries.ids[i]); + suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; + unsigned int h = array_uint24_le (handle); - // Walking newest-first, so the first fingerprint match means - // everything from here on was already downloaded in a - // previous session. - if (memcmp (fingerprint, device->fingerprint, sizeof (fingerprint)) == 0) - break; + unsigned char reply[MAX_PAYLOAD] = {0}; + size_t reply_len = 0; + dc_status_t status = suunto_nautic_transfer (device, RPC_OP_UNSUBSCRIBE, RPC_OP_UNSUBSCRIBE_REPLY, + h, suunto_nautic_noparams, sizeof (suunto_nautic_noparams), reply, sizeof (reply), &reply_len); + if (status != DC_STATUS_SUCCESS) + return status; - char logbook_id[16]; - int n = snprintf (logbook_id, sizeof (logbook_id), "%u", entries.ids[i]); - if (n < 0 || (size_t) n >= sizeof (logbook_id)) - continue; + suunto_nautic_release (device, h); + return status; +} - dc_buffer_clear (raw); - int incomplete = 0, unreadable = 0; - status = suunto_nautic_device_download_dive (abstract, logbook_id, entries.sizes[i], raw, &incomplete, &unreadable); - if (status != DC_STATUS_SUCCESS && unreadable) { - // An empty/aborted entry: listed, but with nothing to decode. - WARNING (abstract->context, "Skipping logbook entry %s (no decodable dive data).", logbook_id); - status = DC_STATUS_SUCCESS; - continue; - } - if (status != DC_STATUS_SUCCESS) { - // Stop at the first dive that may download later: the host takes - // its fingerprint from the newest dive, so carrying on past a gap - // would mark this dive as synced. - ERROR (abstract->context, "Failed to download logbook entry %s; it and any older new dives are left for the next sync.", - logbook_id); - break; - } +dc_status_t +suunto_nautic_device_wait_notification (dc_device_t *abstract, unsigned int timeout, unsigned char handle[3], dc_buffer_t *value) +{ + if (!suunto_nautic_is_device (abstract) || handle == NULL || value == NULL) + return DC_STATUS_INVALIDARGS; - // Delivered anyway: skipping it would move the fingerprint past a - // dive that a later sync could never fetch again. - if (incomplete) - WARNING (abstract->context, "Logbook entry %s does not match its listed size; the dive may be truncated.", logbook_id); + suunto_nautic_device_t *device = (suunto_nautic_device_t *) abstract; - progress.current += 2; - device_event_emit (abstract, DC_EVENT_PROGRESS, &progress); + if (device_is_cancelled (abstract)) + return DC_STATUS_CANCELLED; - if (callback && !callback (dc_buffer_get_data (raw), (unsigned int) dc_buffer_get_size (raw), - fingerprint, sizeof (fingerprint), userdata)) - break; + dc_status_t status = dc_iostream_set_timeout (device->iostream, (int) timeout); + if (status != DC_STATUS_SUCCESS) + return status; + + // A value notification is HANDLE(3) FLAGS(3) TYPE(2) VALUE. + unsigned char data[MAX_PAYLOAD] = {0}; + size_t size = 0; + status = suunto_nautic_recv (device, RPC_OP_NOTIFICATION, 0, data, sizeof (data), &size); + if (status == DC_STATUS_SUCCESS && size < 6) { + ERROR (abstract->context, "Unexpected notification size (" DC_PRINTF_SIZE ").", size); + status = DC_STATUS_PROTOCOL; + } + if (status == DC_STATUS_SUCCESS) { + memcpy (handle, data, 3); + dc_buffer_clear (value); + if (!dc_buffer_append (value, data + 6, size - 6)) + status = DC_STATUS_NOMEMORY; } - dc_buffer_free (raw); - suunto_nautic_entries_free (&entries); + dc_status_t rc = dc_iostream_set_timeout (device->iostream, TIMEOUT_DEFAULT); + if (status == DC_STATUS_SUCCESS) + status = rc; return status; } diff --git a/src/suunto_nautic.h b/src/suunto_nautic.h index fb73c38..ffe2d65 100644 --- a/src/suunto_nautic.h +++ b/src/suunto_nautic.h @@ -25,25 +25,27 @@ * Transport and framing: * - BLE, with HDLC framing (flag 0x7E, escape 0x7D / XOR 0x20; no * separate checksum at this layer). - * - Path-addressed GET requests use the RPC frame envelope: - * 0xA5, opcode, sublen(u16 LE), seq(u16 LE), 0x01, 0x80, 0x00, - * pathlen(u8), path bytes..., crc32(u32 LE) - * where crc32 is the reflected CRC-32 (checksum_crc32r) of every - * preceding byte. A single monotonic per-connection sequence counter - * is used for all requests. + * - Every packet is 0xA5, opcode, length(u16 LE), seqnum(u16 LE), + * payload(length), crc32(u32 LE), where crc32 is the reflected CRC-32 + * (checksum_crc32r) of every preceding byte. A reply echoes the + * request's sequence number. The watch serves every app connected to + * it over the same link, so replies to another app's requests are + * recognized by their sequence number and skipped. + * - A GET (0x0A) resolves a path to a handle; the other requests (FETCH + * 0x0D, SUBSCRIBE 0x10, UNSUBSCRIBE 0x11, RELEASE 0x0B) address that + * handle. A per-dive handle stays bound until it is released. * * Dive download: - * - GET /Data (opcode 0x0A) -> ack carrying the handle bound to - * that dive -> subscribe on that handle (0x10) -> chunk stream (opcode - * 0x01, repeated). The chunk stream is unacknowledged and continuous; - * the host buffers until a 2.0s silence timeout, then closes it with - * STREAM_STOP (opcode 0x11) and releases the handle (opcode 0x0B), as - * a still-bound handle makes the next GET of /Data bind another slot. - * - Each 0x01 frame carries a 28-byte MDS header: payload size is a - * u16 LE at offset 20, payload starts at offset 28. - * - Payloads are Heatshrink-compressed (LZSS variant, see + * - GET /Logbook/byId//Data, SUBSCRIBE: the watch streams the + * compressed dive in unacknowledged notifications (0x01), each with + * its offset in the stream, and ends with a zero length chunk at the + * total size. Then UNSUBSCRIBE and RELEASE. + * - The stream is Heatshrink-compressed (LZSS variant, see * src/heatshrink/; window_sz2 = 7, lookahead_sz2 = 5). The decoded * stream is an SBEM0103 container. + * - GET /Logbook/byId//Summary, FETCH page by page: an uncompressed + * SBEM0103 container with the fields that are not in the profile + * (gradient factors, gas mixes). It is appended to the profile. * * SBEM0103 container: * - A numeric Type-Length-Value stream: [id: 1 byte][length: 1 byte] @@ -59,15 +61,10 @@ * led by a VENDOR_KIND_* byte. * * Enumeration: - * - suunto_nautic_device_foreach() fetches /Logbook/Entries (via the - * short 0x0D fetch; the listing endpoint rejects the stream fetch - * used for dive data) and extracts each dive's LogId, which is a - * UNIX timestamp, from the SBEM payload. The list is served - * oldest-first in pages (status 100 = more); later pages are fetched - * with a uint32 StartAfterId parameter. Dives are downloaded - * newest-first, stopping at the first id matching the fingerprint. - * - Each entry's listed size equals the compressed /Data plus the - * /Summary data bytes, which verifies a download is complete. + * - /Logbook/Entries lists the dives, oldest first and in pages. Each + * record holds the dive id (its start time, a UNIX timestamp) and its + * size, which is checked against the download. Dives are downloaded + * newest first, stopping at the first id matching the fingerprint. * * Datetime: * - The stream carries no wall-clock timestamp except in GPS fixes