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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions src/suunto_nautic.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#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
Expand Down Expand Up @@ -469,8 +470,8 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream

dc_iostream_purge (device->iostream, DC_DIRECTION_ALL);

// Best-effort EVA handshake. The response content can't be validated
// (its format isn't understood), so only the I/O round-trip is required.
// 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);
Expand All @@ -479,17 +480,38 @@ suunto_nautic_device_open (dc_device_t **out, dc_context_t *context, dc_iostream
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;
status = dc_iostream_read (device->iostream, handshake_rsp, sizeof (handshake_rsp), &handshake_len);
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;
}
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);

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)
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);
}

*out = (dc_device_t *) device;

Expand Down
129 changes: 113 additions & 16 deletions src/suunto_nautic_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,29 +128,45 @@
// gives low > high.
#define SUMMARY_GF_LOW 0x35 // uint16 LE, %
#define SUMMARY_GF_HIGH 0x33 // uint16 LE, %
// Water type (uint8): 0 fresh, 1 EN13319, 2 salt.
#define SUMMARY_WATER_TYPE 0x3E
// Gas/cylinder records: an array starting at SUMMARY_GAS_BASE, one 45-byte
// record per configured gas. The record index equals the cylinder slot /
// GasNumber, so tank i uses gas i. Within a record: O2% at +1, He% at +2,
// PO2-max (float32 LE, bar) at +5, cylinder water capacity (float32 LE, m^3)
// at +9. Confirmed on two real dual-gas dives (Air 12 L + NX26 5.7 L, two
// GasNumber, so tank i uses gas i. Within a record: O2% (int16 LE) at +1,
// He% (int16 LE) at +3, PO2-max (float32 LE, bar) at +5, cylinder water
// capacity (float32 LE, m^3) at +9, as the watch's own /Summary schema lays
// them out. Confirmed on two real dual-gas dives (Air 12 L + NX26 5.7 L, two
// transmitters -- issue #29/#33/#34); a lone gas dive stops at the first
// implausible slot, as before.
#define SUMMARY_GAS_BASE 0xC7
#define SUMMARY_GAS_STRIDE 45
#define SUMMARY_GAS_O2 1 // uint8, %
#define SUMMARY_GAS_HE 2 // uint8, %
#define SUMMARY_GAS_O2 1 // int16 LE, %
#define SUMMARY_GAS_HE 3 // int16 LE, %
#define SUMMARY_GAS_VOLUME 9 // float32 LE, m^3
// The dive record, the last /Summary record long enough to hold these, carries
// the watch's own dive time and maximum depth (the app's DiveTime and
// Depth.Max), at the same payload offsets on 2040, 2056 and 2342 B summaries.
// Depth.Max), at the same payload offsets on 2040, 2056 and 2342 B summaries,
// along with the app's DepthAverage (diving only) and Temperature.Min/Max.
#define SUMMARY_DIVE_TEMP_MIN 313 // float32 LE, K
#define SUMMARY_DIVE_TEMP_MAX 317 // float32 LE, K
#define SUMMARY_DIVE_DIVETIME 321 // float32 LE, s, fractional
#define SUMMARY_DIVE_MAXDEPTH 330 // float32 LE, m
#define SUMMARY_DIVE_AVGDEPTH 338 // float32 LE, m
// The DiveFooter (the second record) repeats the gas records from +282, with
// each transmitter's start/end pressure: tank 1 at +25/+29, tank 2 at +33/+37
// (int32 LE, Pa). The end pressures are the app's; the start pressures keep a
// transmitter's pre-pairing dropout value (9 bar on 1788683271), so they aren't used.
#define SUMMARY_FOOTER_RECORD 1
#define SUMMARY_FOOTER_GAS_BASE 282
#define SUMMARY_FOOTER_GAS_END1 29
#define SUMMARY_FOOTER_GAS_END2 37

typedef struct suunto_nautic_tank_t {
unsigned int used;
double beginpressure; // bar
double endpressure; // bar
unsigned int gasmix; // gas index (== cylinder slot / GasNumber)
unsigned int field; // 0 = Pressure (first transmitter), 1 = Pressure2
} suunto_nautic_tank_t;

typedef struct suunto_nautic_parser_t {
Expand Down Expand Up @@ -178,6 +194,8 @@ typedef struct suunto_nautic_parser_t {
double gasvolume[MAX_GASMIXES]; // cylinder water capacity, litres (0 = unknown)
unsigned int have_decomodel;
dc_decomodel_t decomodel;
unsigned int have_salinity;
dc_salinity_t salinity;
} suunto_nautic_parser_t;

typedef struct sbem_chunk_t {
Expand Down Expand Up @@ -306,9 +324,34 @@ suunto_nautic_find_summary (const unsigned char *data, size_t size)
return size;
}

// The /Summary's dive record: its records are plain [id][len] TLVs (the
// profile's fixed-length table doesn't apply), ending in trailing bytes that
// don't form a whole record.
// The /Summary's records are plain [id][len] TLVs (the profile's fixed-length
// table doesn't apply), ending in trailing bytes that don't form a whole
// record. Record ids differ between models, so records are found by position.
static const unsigned char *
suunto_nautic_summary_record (const unsigned char *sbem, size_t size, unsigned int index, size_t *out_length)
{
size_t pos = SBEM_MAGIC_SIZE;
for (unsigned int i = 0; pos + 2 <= size; i++) {
size_t length = sbem[pos + 1];
size_t header = 2;
if (length == 255) {
if (pos + 6 > size)
break;
length = array_uint32_le (sbem + pos + 2);
header = 6;
}
if (length > size - pos - header)
break;
if (i == index) {
*out_length = length;
return sbem + pos + header;
}
pos += header + length;
}
return NULL;
}

// The /Summary's dive record: the last record long enough to hold its fields.
static const unsigned char *
suunto_nautic_summary_dive_record (const unsigned char *sbem, size_t size)
{
Expand All @@ -325,7 +368,7 @@ suunto_nautic_summary_dive_record (const unsigned char *sbem, size_t size)
}
if (length > size - pos - header)
break;
if (length >= SUMMARY_DIVE_MAXDEPTH + 4)
if (length >= SUMMARY_DIVE_AVGDEPTH + 4)
record = sbem + pos + header;
pos += header + length;
}
Expand All @@ -350,13 +393,23 @@ suunto_nautic_parse_summary (suunto_nautic_parser_t *parser, const unsigned char
parser->have_decomodel = 1;
}

if (size > SUMMARY_WATER_TYPE) {
unsigned int type = sbem[SUMMARY_WATER_TYPE];
if (type <= 2) {
static const double density[] = {1000.0, 1020.0, 1030.0};
parser->salinity.type = type ? DC_WATER_SALT : DC_WATER_FRESH;
parser->salinity.density = density[type];
parser->have_salinity = 1;
}
}

for (unsigned int i = 0; i < MAX_GASMIXES; i++) {
size_t base = SUMMARY_GAS_BASE + (size_t) i * SUMMARY_GAS_STRIDE;
if (base + SUMMARY_GAS_O2 >= size)
if (base + SUMMARY_GAS_HE + 2 > size)
break;
unsigned int o2 = sbem[base + SUMMARY_GAS_O2];
unsigned int he = sbem[base + SUMMARY_GAS_HE];
if (o2 < 1 || o2 > 100 || he > 100 || o2 + he > 100)
int o2 = (int16_t) array_uint16_le (sbem + base + SUMMARY_GAS_O2);
int he = (int16_t) array_uint16_le (sbem + base + SUMMARY_GAS_HE);
if (o2 < 1 || o2 > 100 || he < 0 || he > 100 || o2 + he > 100)
break; // unused/implausible slot -- stop
unsigned int g = parser->ngasmixes;
parser->gasmix[g].oxygen = o2 / 100.0;
Expand Down Expand Up @@ -486,10 +539,15 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
// it has read non-zero at least twice (p2_first holds the first value).
int tankmap[MAX_TANKS * 2];
int p2_first[MAX_TANKS];
// The last reading of each tank while Diving: readings taken on the surface
// after the dive can be of a closed, purged regulator (~1 bar).
double diving_end[MAX_TANKS];
for (unsigned int ti = 0; ti < MAX_TANKS * 2; ti++)
tankmap[ti] = -1;
for (unsigned int ti = 0; ti < MAX_TANKS; ti++)
for (unsigned int ti = 0; ti < MAX_TANKS; ti++) {
p2_first[ti] = -1;
diving_end[ti] = -1.0;
}

unsigned int have_location = 0;
dc_location_t location = {0};
Expand Down Expand Up @@ -621,6 +679,7 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
// Cylinder slot index == GasNumber, so this tank
// breathes gas i (linked to gasmix[i] below).
tank[ntanks].gasmix = i;
tank[ntanks].field = field;
// Seed begin from the first observed reading (the deferred
// one for Pressure2); refined to the max below.
tank[ntanks].beginpressure = (field == 1 && p2_first[i] >= 0)
Expand All @@ -629,6 +688,8 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
}
unsigned int t = (unsigned int) tankmap[key];
tank[t].endpressure = bar;
if (dive_state == DIVE_STATE_DIVING)
diving_end[t] = bar;
// Begin pressure = the highest reading seen: a cylinder only
// loses pressure during a dive, so the max is the true start.
// This ignores an initial dropout where a transmitter that
Expand Down Expand Up @@ -888,6 +949,10 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
parser->temperature_surface = temperature_surface;
parser->temperature_minimum = temperature_minimum;
parser->temperature_maximum = temperature_maximum;
for (unsigned int t = 0; t < ntanks; t++) {
if (diving_end[t] >= 0.0)
tank[t].endpressure = diving_end[t];
}
parser->ntanks = ntanks;
memcpy (parser->tank, tank, sizeof (tank));
parser->have_location = have_location;
Expand All @@ -900,6 +965,8 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
// Gradient factors and gas mixes from the appended /Summary section.
parser->ngasmixes = 0;
parser->have_decomodel = 0;
parser->have_salinity = 0;
memset (&parser->salinity, 0, sizeof (parser->salinity));
memset (parser->gasmix, 0, sizeof (parser->gasmix));
memset (parser->gasvolume, 0, sizeof (parser->gasvolume));
memset (&parser->decomodel, 0, sizeof (parser->decomodel));
Expand All @@ -910,7 +977,10 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback

// The watch's maximum is deeper than any logged sample (the app shows
// it). Trusted only on a record whose dive time agrees with the
// profile's, and when it is close to the sampled maximum.
// profile's, and when it is close to the sampled maximum. The same
// record's average depth and water temperature range are the app's
// figures too; the temperatures are taken only as a sane pair, since
// one Ocean dive records its minimum above its maximum.
const unsigned char *record = suunto_nautic_summary_dive_record (sbem, sbem_size);
if (record && parser->divetime > 0) {
double divetime = array_float_le (record + SUMMARY_DIVE_DIVETIME);
Expand All @@ -921,6 +991,28 @@ suunto_nautic_parser_parse (dc_parser_t *abstract, dc_sample_callback_t callback
parser->divetime = (unsigned int) divetime;
if (maxdepth > 0.0 && depth >= maxdepth - 0.5 && depth <= maxdepth + 3.0)
parser->maxdepth = depth;
double avgdepth = array_float_le (record + SUMMARY_DIVE_AVGDEPTH);
if (avgdepth > 0.0 && avgdepth <= parser->maxdepth)
parser->avgdepth = avgdepth;
double tmin = array_float_le (record + SUMMARY_DIVE_TEMP_MIN);
double tmax = array_float_le (record + SUMMARY_DIVE_TEMP_MAX);
if (have_temperature && tmin > 250.0 && tmax < 330.0 && tmin <= tmax) {
parser->temperature_minimum = tmin - 273.15;
parser->temperature_maximum = tmax - 273.15;
}

size_t footer_size = 0;
const unsigned char *footer = suunto_nautic_summary_record (sbem, sbem_size, SUMMARY_FOOTER_RECORD, &footer_size);
for (unsigned int t = 0; footer && t < parser->ntanks; t++) {
suunto_nautic_tank_t *tk = &parser->tank[t];
size_t base = SUMMARY_FOOTER_GAS_BASE + (size_t) tk->gasmix * SUMMARY_GAS_STRIDE;
if (base + SUMMARY_GAS_STRIDE > footer_size)
continue;
int pa = (int) array_uint32_le (footer + base + (tk->field ? SUMMARY_FOOTER_GAS_END2 : SUMMARY_FOOTER_GAS_END1));
double bar = pa / 100000.0;
if (bar > 0.0 && bar <= tk->beginpressure + 1.0)
tk->endpressure = bar;
}
}
}
}
Expand Down Expand Up @@ -1047,6 +1139,11 @@ suunto_nautic_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, uns
case DC_FIELD_DIVEMODE:
*((dc_divemode_t *) value) = parser->divemode;
break;
case DC_FIELD_SALINITY:
if (!parser->have_salinity)
return DC_STATUS_UNSUPPORTED;
*((dc_salinity_t *) value) = parser->salinity;
break;
default:
return DC_STATUS_UNSUPPORTED;
}
Expand Down
Loading