From 34785f55ff40553f096307cc68ac82c92935e19f Mon Sep 17 00:00:00 2001 From: Dirk Hohndel Date: Sat, 8 Sep 2018 09:58:08 -0700 Subject: [PATCH] Shearwater Petrel Native Format parsing This will allow parsing dives from the Shearwater Teric, but depending on the firmware could also be used on older models. Signed-off-by: Dirk Hohndel --- src/shearwater_predator_parser.c | 202 ++++++++++++++++++++++++------- 1 file changed, 160 insertions(+), 42 deletions(-) diff --git a/src/shearwater_predator_parser.c b/src/shearwater_predator_parser.c index 46f0ff0..86f2310 100644 --- a/src/shearwater_predator_parser.c +++ b/src/shearwater_predator_parser.c @@ -40,6 +40,30 @@ dc_parser_isinstance((parser), &shearwater_predator_parser_vtable) || \ dc_parser_isinstance((parser), &shearwater_petrel_parser_vtable)) +// Petrel Native Format constants +#define PNF_BLOCKSIZE 0x20 +#define LOG_RECORD_DIVE_SAMPLE 0x01 +#define LOG_RECORD_FREEDIVE_SAMPLE 0x02 +#define LOG_RECORD_OPENING_0 0x10 +#define LOG_RECORD_OPENING_1 0x11 +#define LOG_RECORD_OPENING_2 0x12 +#define LOG_RECORD_OPENING_3 0x13 +#define LOG_RECORD_OPENING_4 0x14 +#define LOG_RECORD_OPENING_5 0x15 +#define LOG_RECORD_OPENING_6 0x16 +#define LOG_RECORD_OPENING_7 0x17 +#define LOG_RECORD_CLOSING_0 0x20 +#define LOG_RECORD_CLOSING_1 0x21 +#define LOG_RECORD_CLOSING_2 0x22 +#define LOG_RECORD_CLOSING_3 0x23 +#define LOG_RECORD_CLOSING_4 0x24 +#define LOG_RECORD_CLOSING_5 0x25 +#define LOG_RECORD_CLOSING_6 0x26 +#define LOG_RECORD_CLOSING_7 0x27 +#define LOG_RECORD_FINAL 0xFF +#define NUM_BLOCK_IDS 0x28 + +// constant for the older Predator and Predator-like formats #define SZ_BLOCK 0x80 #define SZ_SAMPLE_PREDATOR 0x10 #define SZ_SAMPLE_PETREL 0x20 @@ -65,6 +89,7 @@ struct shearwater_predator_parser_t { dc_parser_t base; unsigned int model; unsigned int petrel; + unsigned int pnf; unsigned int samplesize; // Cached fields. unsigned int cached; @@ -79,6 +104,9 @@ struct shearwater_predator_parser_t { unsigned int serial; dc_divemode_t mode; + /* Block addresses for PNF */ + unsigned int block_offset[NUM_BLOCK_IDS]; + /* String fields */ dc_field_string_t strings[MAXSTRINGS]; }; @@ -220,6 +248,7 @@ shearwater_predator_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *d { const unsigned char *data = abstract->data; unsigned int size = abstract->size; + shearwater_predator_parser_t *parser = (shearwater_predator_parser_t *) abstract; if (size < 2 * SZ_BLOCK) return DC_STATUS_DATAFORMAT; @@ -332,18 +361,21 @@ add_battery_info(shearwater_predator_parser_t *parser, const char *desc, unsigne static void add_deco_model(shearwater_predator_parser_t *parser, const unsigned char *data) { - switch (data[67]) { + unsigned int idx_deco_model = parser->pnf ? parser->block_offset[LOG_RECORD_OPENING_2] + 18 : 67; + unsigned int idx_gfs = parser->pnf ? parser->block_offset[LOG_RECORD_OPENING_3] + 5 : 85; + + switch (data[idx_deco_model]) { case 0: add_string_fmt(parser, "Deco model", "GF %u/%u", data[4], data[5]); break; case 1: - add_string_fmt(parser, "Deco model", "VPM-B +%u", data[68]); + add_string_fmt(parser, "Deco model", "VPM-B +%u", data[idx_deco_model + 1]); break; case 2: - add_string_fmt(parser, "Deco model", "VPM-B/GFS +%u %u%%", data[68], data[85]); + add_string_fmt(parser, "Deco model", "VPM-B/GFS +%u %u%%", data[idx_deco_model + 1], data[idx_gfs]); break; default: - add_string_fmt(parser, "Deco model", "Unknown model %d", data[67]); + add_string_fmt(parser, "Deco model", "Unknown model %d", data[idx_deco_model]); } } @@ -353,7 +385,8 @@ add_battery_type(shearwater_predator_parser_t *parser, const unsigned char *data if (parser->logversion < 7) return; - switch (data[120]) { + unsigned int idx_battery_type = parser->pnf ? parser->block_offset[LOG_RECORD_OPENING_4] + 9 : 120; + switch (data[idx_battery_type]) { case 1: add_string(parser, "Battery type", "1.5V Alkaline"); break; @@ -370,7 +403,7 @@ add_battery_type(shearwater_predator_parser_t *parser, const unsigned char *data add_string(parser, "Battery type", "3.7V Li-Ion"); break; default: - add_string_fmt(parser, "Battery type", "unknown type %d", data[120]); + add_string_fmt(parser, "Battery type", "unknown type %d", data[idx_battery_type]); break; } } @@ -386,24 +419,66 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) return DC_STATUS_SUCCESS; } + // the log formats are very similar - but the Petrel Native Format (PNF) + // is organized differently. There everything is in 32 byte (PNF_BLOCKSIZE) blocks + // and the offsets of various fields are different. It still seems to make sense + // to just all parse it in one place + + // header and footer are concepts of the Predator and Predator-like formats unsigned int headersize = SZ_BLOCK; unsigned int footersize = SZ_BLOCK; + if (size < headersize + footersize) { ERROR (abstract->context, "Invalid data length."); return DC_STATUS_DATAFORMAT; } + // remember if this is a Petrel Native Format download + // if yes, we need different ways to access the various data fields + // for samples it's simple, they are just offset by one (so we can use pnf as offset) + // for header and footer data it's more complicated because of the new block structure + unsigned int pnf = parser->pnf = data[0] == 0x10 ? 1 : 0; + + // sanity check on the log format + // is this a Predator-like or Petrel-native (or Teric style) log? + if (parser->petrel == 0 && pnf) { + ERROR (abstract->context, "This is a Petrel-native log, but we claim this is a Predator"); + return DC_STATUS_DATAFORMAT; + } + + memset (parser->block_offset, 0, NUM_BLOCK_IDS * sizeof(unsigned int)); + if (pnf) { + // find the offsets of the various header and footer blocks + int i = 0, j = 0; + while (i < size) { + for (j = LOG_RECORD_OPENING_0; j < NUM_BLOCK_IDS; j++) { + if (data[i] == j) + parser->block_offset[j] = i; + if (j == LOG_RECORD_OPENING_7) + j = LOG_RECORD_CLOSING_0 - 1; + } + i += PNF_BLOCKSIZE; + } + } + // there is a small risk we are taking here... if the log were damaged and one or + // more of the blocks were missing, we'll default to looking into block 0 and + // report bogus data. This may be worth testing for? + // Log versions before 6 weren't reliably stored in the data, but // 6 is also the oldest version that we assume in our code unsigned int logversion = 6; - if (data[127] > 6) + if (!pnf && data[127] > 6) logversion = data[127]; + if (pnf) + logversion = data[parser->block_offset[LOG_RECORD_OPENING_4] + 16]; + INFO(abstract->context, "Shearwater log version %u\n", logversion); memset(parser->strings, 0, sizeof(parser->strings)); + add_string_fmt(parser, "Logversion", "%d%s", logversion, pnf ? "(PNF)" : ""); // Adjust the footersize for the final block. - if (parser->petrel || array_uint16_be (data + size - footersize) == 0xFFFD) { + if (parser->petrel == 1 || array_uint16_be (data + size - footersize) == 0xFFFD) { footersize += SZ_BLOCK; if (size < headersize + footersize) { ERROR (abstract->context, "Invalid data length."); @@ -423,9 +498,16 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) // Transmitter battery levels unsigned int t1_battery = 0, t2_battery = 0; - unsigned int offset = headersize; - unsigned int length = size - footersize; + // the indices in the sample block are offset by 1 in PNF + unsigned int offset = pnf ? 0 : headersize; + unsigned int length = pnf ? size : size - footersize; + while (offset < length) { + // Ignore blocks that aren't dive samples + if (pnf && data[offset] != LOG_RECORD_DIVE_SAMPLE) { + offset += parser->samplesize; + continue; + } // Ignore empty samples. if (array_isequal (data + offset, parser->samplesize, 0x00)) { offset += parser->samplesize; @@ -433,14 +515,14 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) } // Status flags. - unsigned int status = data[offset + 11]; + unsigned int status = data[offset + 11 + pnf]; if ((status & OC) == 0) { mode = DC_DIVEMODE_CCR; } // Gaschange. - unsigned int o2 = data[offset + 7]; - unsigned int he = data[offset + 8]; + unsigned int o2 = data[offset + 7 + pnf]; + unsigned int he = data[offset + 8 + pnf]; if (o2 != o2_previous || he != he_previous) { // Find the gasmix in the list. unsigned int idx = 0; @@ -468,17 +550,24 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) // Transmitter battery levels if (logversion >= 7) { // T1 at offset 27, T2 at offset 19 - t1_battery |= battery_state(data + offset + 27); - t2_battery |= battery_state(data + offset + 19); + t1_battery |= battery_state(data + offset + 27 + pnf); + t2_battery |= battery_state(data + offset + 19 + pnf); } offset += parser->samplesize; } + // for header and footer indices we use a variable base that is set to the + // correct value based on the log type + unsigned int base = 0; + // Cache sensor calibration for later use unsigned int nsensors = 0, ndefaults = 0; + + // calibration value for sensors + base = pnf ? parser->block_offset[LOG_RECORD_OPENING_3] + 7 : 87; for (size_t i = 0; i < 3; ++i) { - unsigned int calibration = array_uint16_be(data + 87 + i * 2); + unsigned int calibration = array_uint16_be(data + base + i * 2); parser->calibration[i] = calibration / 100000.0; if (parser->model == PREDATOR) { // The Predator expects the mV output of the cells to be @@ -487,7 +576,7 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) // sensors lines up and matches the average. parser->calibration[i] *= 2.2; } - if (data[86] & (1 << i)) { + if (data[base - 1] & (1 << i)) { if (calibration == 2100) { ndefaults++; } @@ -505,7 +594,7 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) if (mode != DC_DIVEMODE_OC) add_string(parser, "PPO2 source", "voted/averaged"); } else { - parser->calibrated = data[86]; + parser->calibrated = data[base - 1]; if (mode != DC_DIVEMODE_OC) add_string(parser, "PPO2 source", "cells"); } @@ -521,6 +610,7 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) } parser->mode = mode; add_string_fmt(parser, "Serial", "%08x", parser->serial); + // bytes 1-31 are identical in all formats add_string_fmt(parser, "FW Version", "%2x", data[19]); add_deco_model(parser, data); add_battery_type(parser, data); @@ -556,16 +646,26 @@ shearwater_predator_parser_get_field (dc_parser_t *abstract, dc_field_type_t typ dc_field_string_t *string = (dc_field_string_t *) value; unsigned int density = 0; + // the first 32 bytes of the footer and closing block 0 are identical + unsigned int block_start = parser->pnf ? parser->block_offset[LOG_RECORD_CLOSING_0] : footer; if (value) { + unsigned int idx; switch (type) { case DC_FIELD_DIVETIME: - *((unsigned int *) value) = array_uint16_be (data + footer + 6) * 60; + // FIXME: this is wrong based on the documentation I received + // it should be a 3 byte value in offsets 6-8 that is dive length in seconds + *((unsigned int *) value) = array_uint16_be (data + block_start + 6) * 60; break; case DC_FIELD_MAXDEPTH: if (units == IMPERIAL) - *((double *) value) = array_uint16_be (data + footer + 4) * FEET; + *((double *) value) = array_uint16_be (data + block_start + 4) * FEET; else - *((double *) value) = array_uint16_be (data + footer + 4); + *((double *) value) = array_uint16_be (data + block_start + 4); + // according to the documentation this should have been in tenth of a meter + // before, but the existing code for the Predator-like format didn't have + // that adjustment, so let's just do that for PNF (where we definitely need it). + if (parser->pnf) + *((double *)value) /= 10.0; break; case DC_FIELD_GASMIX_COUNT: *((unsigned int *) value) = parser->ngasmixes; @@ -576,7 +676,8 @@ shearwater_predator_parser_get_field (dc_parser_t *abstract, dc_field_type_t typ gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium; break; case DC_FIELD_SALINITY: - density = array_uint16_be (data + 83); + idx = parser->pnf ? parser->block_offset[LOG_RECORD_OPENING_3] + 3 : 83; + density = array_uint16_be (data + idx); if (density == 1000) water->type = DC_WATER_FRESH; else @@ -584,6 +685,7 @@ shearwater_predator_parser_get_field (dc_parser_t *abstract, dc_field_type_t typ water->density = density; break; case DC_FIELD_ATMOSPHERIC: + idx = parser->pnf ? parser->block_offset[LOG_RECORD_OPENING_1] + 16 : 47; *((double *) value) = array_uint16_be (data + 47) / 1000.0; break; case DC_FIELD_DIVEMODE: @@ -627,12 +729,27 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal unsigned int o2_previous = 0, he_previous = 0; unsigned int time = 0; - unsigned int offset = parser->headersize; - unsigned int length = size - parser->footersize; + unsigned int pnf = parser->pnf; + unsigned int offset = pnf ? 0 : parser->headersize; + unsigned int length = pnf ? size : size - parser->footersize; + unsigned int time_increment = 10; + + // the time increment is now given in ms. not sure how we'll deal with that since all we do is full seconds + if (pnf && parser->logversion >= 9) + time_increment = array_uint16_be (data + parser->block_offset[LOG_RECORD_OPENING_5] + 23) / 1000; while (offset < length) { dc_sample_value_t sample = {0}; + // stop parsing if we see the end block + if (pnf && data[offset] == LOG_RECORD_FINAL && data[offset + 1] == 0xFD) + break; + + // Ignore blocks that aren't dive samples + if (pnf && data[offset] != LOG_RECORD_DIVE_SAMPLE) { + offset += parser->samplesize; + continue; + } // Ignore empty samples. if (array_isequal (data + offset, parser->samplesize, 0x00)) { offset += parser->samplesize; @@ -640,12 +757,12 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal } // Time (seconds). - time += 10; + time += time_increment; sample.time = time; if (callback) callback (DC_SAMPLE_TIME, sample, userdata); // Depth (1/10 m or ft). - unsigned int depth = array_uint16_be (data + offset); + unsigned int depth = array_uint16_be (data + pnf + offset); if (units == IMPERIAL) sample.depth = depth * FEET / 10.0; else @@ -653,7 +770,7 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata); // Temperature (°C or °F). - int temperature = (signed char) data[offset + 13]; + int temperature = (signed char) data[offset + pnf + 13]; if (temperature < 0) { // Fix negative temperatures. temperature += 102; @@ -668,30 +785,31 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata); // Status flags. - unsigned int status = data[offset + 11]; + unsigned int status = data[offset + pnf + 11]; if ((status & OC) == 0) { // PPO2 if ((status & PPO2_EXTERNAL) == 0) { if (!parser->calibrated) { - sample.ppo2 = data[offset + 6] / 100.0; + sample.ppo2 = data[offset + pnf + 6] / 100.0; if (callback) callback (DC_SAMPLE_PPO2, sample, userdata); } else { - sample.ppo2 = data[offset + 12] * parser->calibration[0]; + sample.ppo2 = data[offset + pnf + 12] * parser->calibration[0]; if (callback && (parser->calibrated & 0x01)) callback (DC_SAMPLE_PPO2, sample, userdata); - sample.ppo2 = data[offset + 14] * parser->calibration[1]; + sample.ppo2 = data[offset + pnf + 14] * parser->calibration[1]; if (callback && (parser->calibrated & 0x02)) callback (DC_SAMPLE_PPO2, sample, userdata); - sample.ppo2 = data[offset + 15] * parser->calibration[2]; + sample.ppo2 = data[offset + pnf + 15] * parser->calibration[2]; if (callback && (parser->calibrated & 0x04)) callback (DC_SAMPLE_PPO2, sample, userdata); } } // Setpoint if (parser->petrel) { - sample.setpoint = data[offset + 18] / 100.0; + sample.setpoint = data[offset + pnf + 18] / 100.0; } else { + // this will only ever be called for the actual Predator, so no adjustment needed for PNF if (status & SETPOINT_HIGH) { sample.setpoint = data[18] / 100.0; } else { @@ -703,13 +821,13 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal // CNS if (parser->petrel) { - sample.cns = data[offset + 22] / 100.0; + sample.cns = data[offset + pnf + 22] / 100.0; if (callback) callback (DC_SAMPLE_CNS, sample, userdata); } // Gaschange. - unsigned int o2 = data[offset + 7]; - unsigned int he = data[offset + 8]; + unsigned int o2 = data[offset + pnf + 7]; + unsigned int he = data[offset + pnf + 8]; if (o2 != o2_previous || he != he_previous) { unsigned int idx = shearwater_predator_find_gasmix (parser, o2, he); if (idx >= parser->ngasmixes) { @@ -724,7 +842,7 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal } // Deco stop / NDL. - unsigned int decostop = array_uint16_be (data + offset + 2); + unsigned int decostop = array_uint16_be (data + offset + pnf + 2); if (decostop) { sample.deco.type = DC_DECO_DECOSTOP; if (units == IMPERIAL) @@ -735,7 +853,7 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal sample.deco.type = DC_DECO_NDL; sample.deco.depth = 0.0; } - sample.deco.time = data[offset + 9] * 60; + sample.deco.time = data[offset + pnf + 9] * 60; if (callback) callback (DC_SAMPLE_DECO, sample, userdata); // for logversion 7 and newer (introduced for Perdix AI) @@ -750,14 +868,14 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal // For regular values, the top 4 bits contain the battery // level (0=normal, 1=critical, 2=warning), and the lower 12 // bits the tank pressure in units of 2 psi. - unsigned int pressure = array_uint16_be (data + offset + 27); + unsigned int pressure = array_uint16_be (data + offset + pnf + 27); if (pressure < 0xFFF0) { pressure &= 0x0FFF; sample.pressure.tank = 0; sample.pressure.value = pressure * 2 * PSI / BAR; if (callback) callback (DC_SAMPLE_PRESSURE, sample, userdata); } - pressure = array_uint16_be (data + offset + 19); + pressure = array_uint16_be (data + offset + pnf + 19); if (pressure < 0xFFF0) { pressure &= 0x0FFF; sample.pressure.tank = 1; @@ -772,8 +890,8 @@ shearwater_predator_parser_samples_foreach (dc_parser_t *abstract, dc_sample_cal // 0xFD Not available in current mode // 0xFC Not available because of DECO // 0xFB Tank size or max pressure haven’t been set up - if (data[offset + 21] < 0xF0) { - sample.rbt = data[offset + 21]; + if (data[offset + pnf + 21] < 0xF0) { + sample.rbt = data[offset + pnf + 21]; if (callback) callback (DC_SAMPLE_RBT, sample, userdata); } }