diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 51ce07238..c3c1e5991 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -116,10 +116,12 @@ set(SUBSURFACE_CORE_LIB_SRCS globals.h imagedownloader.cpp imagedownloader.h + import-asd.cpp import-cobalt.cpp import-csv.cpp import-csv.h import-divinglog.cpp + import-logtrak.cpp import-shearwater.cpp import-suunto.cpp import-seac.cpp diff --git a/core/file.cpp b/core/file.cpp index 658d793ec..234c09fc3 100644 --- a/core/file.cpp +++ b/core/file.cpp @@ -370,5 +370,15 @@ int parse_file(const char *filename, struct divelog *log) return ostctools_import(buffer, log); } + /* Scubapro Logtrak files */ + if (fmt && (!strcasecmp(fmt+1, "script"))) { + return logtrak_import(mem, log); + } + + /* Scubapro ASD files */ + if (fmt && (!strcasecmp(fmt + 1, "asd"))) { + return scubapro_asd_import(mem, log); + } + return parse_file_buffer(filename, mem, log); } diff --git a/core/file.h b/core/file.h index 93692f5c3..5aa1e8355 100644 --- a/core/file.h +++ b/core/file.h @@ -33,5 +33,7 @@ extern std::pair readfile(const char *filename); // return dat extern int try_to_open_cochran(const char *filename, std::string &mem, struct divelog *log); extern int try_to_open_liquivision(const char *filename, std::string &mem, struct divelog *log); extern int datatrak_import(std::string &mem, std::string &wl_mem, struct divelog *log); +extern int logtrak_import(const std::string &mem, struct divelog *log); +extern int scubapro_asd_import(const std::string &mem, struct divelog *log); #endif // FILE_H diff --git a/core/import-asd.cpp b/core/import-asd.cpp new file mode 100644 index 000000000..cfb1c4af4 --- /dev/null +++ b/core/import-asd.cpp @@ -0,0 +1,693 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* .asd file format is the dive import/export format for Scubapro/Uwatec software + * divelogs. It enables export from SmartTrak and import/export from LogTrak. + * It's a binary format with remembrances to ancient DataTrak format in the way + * strings are stored, and it keeps the samples from the DC as they are, so we can + * simply pass them to libdivecomputer to parse. + * The bad news: + * - Dives aren't fixed size, thus we can't avoid a sequential parsing. + * - There is a header area for each dive but this header is not the same + * libdc expects as the expected headers are different based on DC model or family. + * Thus faking a header as it's expected by libdc is a must. This has to be done per + * dive, as a single file may have dives downloaded from different DCs. + * - The byte sequence signaling the beginning of a new dive varies depending + * on the origin of the file (SmartTrak or LogTrak). + * - Files coming from SmartTrak can include dives imported from yet older + * DataTrak software. I don't know if these dives coming from serial devices can + * even be imported into LogTrak. ATM we can safely assume we are only finding + * them in SmartTrak exported dives. + * - ... Probably some more I just haven't found yet. + */ + +#include +#include +#include +#include +#include + +#include "dive.h" +#include "device.h" +#include "errorhelper.h" +#include "gettext.h" +#include "divelist.h" +#include "file.h" +#include "libdivecomputer.h" +#include "divesite.h" +#include "equipment.h" +#include "divelog.h" +#include "tag.h" + +extern dc_status_t dt_libdc_buffer(unsigned char *ptr, int prf_length, int dc_model, unsigned char *compl_buffer); + +/* + * dc model definitions + */ +#define SMARTPRO 0x10 +#define GALILEO 0x11 +#define ALADINTEC 0x12 +#define ALADINTEC2G 0x13 +#define SMARTCOM 0x14 +#define ALADIN2G 0x15 +#define ALADINSPORTMATRIX 0x17 +#define SMARTTEC 0x18 +#define GALILEOTRIMIX 0x19 +#define SMARTZ 0x1C +#define MERIDIAN 0x20 +#define ALADINSQUARE 0x22 +#define CHROMIS 0x24 +#define ALADINA1 0x25 +#define MANTIS2 0x26 +#define ALADINA2 0x28 +#define G2TEK 0x31 +#define G2 0x32 +#define G3 0x34 +#define G2HUD 0x42 +#define LUNA2AI 0x50 +#define LUNA2 0x51 + +/* + * Data positions in serial stream as expected by libdc + */ +#define LIBDC_MAX_DEPTH 22 +#define LIBDC_DIVE_TIME 26 +#define LIBDC_MAX_TEMP 28 +#define LIBDC_MIN_TEMP 30 +#define LIBDC_SURF_TEMP 32 +#define LIBDC_GASMIX 44 +#define LIBDC_TANK_PRESS 50 +#define LIBDC_SETTINGS 92 +#define LIBDC_MAX_DEPTH_SMART 18 +#define LIBDC_DIVE_TIME_SMART 20 +#define LIBDC_MIN_TEMP_SMART 22 +#define LIBDC_GASMIX_SMART 24 +#define LIBDC_GASMIX_SMARTZ 28 +#define LIBDC_TANK_PRESS_SMARTZ 34 +#define LIBDC_TANK_PRESS_SMARTCOM 30 +#define LIBDC_DIVE_TIME_ALADINTEC 24 +#define LIBDC_SETTINGS_ALADINTEC 52 +#define LIBDC_MIN_TEMP_ALADINTEC 26 +#define LIBDC_GASMIX_ALADINTEC 30 +#define LIBDC_GASMIX_ALADINTEC2G 34 +#define LIBDC_SETTINGS_ALADINTEC2G 60 +#define LIBDC_SETTINGS_TRIMIX 68 + +#define LIBDC_SAMPLES_MANTIS 152 +#define LIBDC_SAMPLES_G2 84 +#define LIBDC_SAMPLES_ALADINTEC 108 +#define LIBDC_SAMPLES_ALADINTEC2G 116 +#define LIBDC_SAMPLES_SMART 92 +#define LIBDC_SAMPLES_SMARTCOM 100 +#define LIBDC_SAMPLES_SMARTZ 132 + +/* + * Data positions in asd raw data buffer passed to build_dc_data(). + * There are 12 more bytes which include 2bytes for dc model and 4bytes for + * device serial number. + */ +#define ASD_SAMPLES 183 +#define ASD_DIVE_TIME 32 +#define ASD_MAX_TEMP 21 +#define ASD_MIN_TEMP 34 +#define ASD_SURF_TEMP 18 +#define ASD_GAS_MIX 36 // 2 bytes. Should be x3 at least. Assume consecutive. +#define ASD_MAXDEPTH 30 +#define ASD_SETTINGS 70 // Get 4 bytes although only 2 are really needed +#define ASD_TANK_PRESS_INIT 42 +#define ASD_TANK_PRESS_END 44 + +#define G2_MAX_TEMP 148 + +/* + * Returns a dc_descriptor_t structure based on dc model's number. + * This ensures the model pased to libdc_buffer_parser() is a supported model and avoids + * problems with shared model num devices by taking the family into account. + * AFAIK only DC_FAMILY_UWATEC_SMART is avaliable in libdc; UWATEC_MERIDIAN kept just in case ... + */ +extern "C" dc_descriptor_t *get_data_descriptor(int data_model, dc_family_t data_fam) +{ + dc_descriptor_t *descriptor = NULL, *current = NULL; + dc_iterator_t *iterator = NULL; + dc_status_t rc; + + rc = dc_descriptor_iterator(&iterator); + if (rc != DC_STATUS_SUCCESS) { + report_error("[libdc]\t\t\tCreating the device descriptor iterator.\n"); + return current; + } + while ((dc_iterator_next(iterator, &descriptor)) == DC_STATUS_SUCCESS) { + int desc_model = dc_descriptor_get_model(descriptor); + dc_family_t desc_fam = dc_descriptor_get_type(descriptor); + + if (data_model == desc_model && (data_fam == desc_fam)) { + current = descriptor; + break; + } + dc_descriptor_free(descriptor); + } + dc_iterator_free(iterator); + return current; +} + +/* + * Fills a device_data_t structure to pass to libdivecomputer. + * Detects if a device is supported or not. + */ +extern "C" int prepare_data(int data_model, dc_family_t dc_fam, device_data_t *dev_data) +{ + dev_data->device = NULL; + dev_data->context = NULL; + dev_data->descriptor = get_data_descriptor(data_model, dc_fam); + if (dev_data->descriptor) { + dev_data->vendor = dc_descriptor_get_vendor(dev_data->descriptor); + dev_data->product = dc_descriptor_get_product(dev_data->descriptor); + std::string tmp (dev_data->vendor); + tmp += " "; + tmp += dev_data->product; + dev_data->model = tmp; + return DC_STATUS_SUCCESS; + } else { + report_info("Warning [prepare_data]: Unsupported DC model 0x%2x\n", data_model); + return DC_STATUS_UNSUPPORTED; + } +} + +/* + * ASD provides the size of the buffer ... well, most times. Sometimes it's not the full buffer + * size, but just the samples size. I've been unable to find a pattern for this (although samples + * size seems to be limited to galileo devices coming from SmartTrak exported dives). Thus, we can't + * trust this data and we need to calculate the correct buffer size, this being the previously found + * size of the ASD dive data minus the ASD header plus the device header. + * The default option in the switch conditional shouldn't be reached. If so, we have forgotten to add + * some device. + */ +extern "C" unsigned char *allocate_libdc_buffer(int model, int asd_size, int *size) +{ + unsigned char *buf; + int sample_size = asd_size - ASD_SAMPLES; + + switch (model) { + case GALILEO: + case ALADIN2G: + case MERIDIAN: + case CHROMIS: + case MANTIS2: + case ALADINSQUARE: + *size = sample_size + LIBDC_SAMPLES_MANTIS; + break; + case GALILEOTRIMIX: + case G2: + case G2HUD: + case G2TEK: + case G3: + case ALADINSPORTMATRIX: + case ALADINA1: + case ALADINA2: + case LUNA2AI: + case LUNA2: + *size = sample_size + LIBDC_SAMPLES_G2; + break; + case ALADINTEC2G: + *size = sample_size + LIBDC_SAMPLES_ALADINTEC2G; + break; + case ALADINTEC: + *size = sample_size + LIBDC_SAMPLES_ALADINTEC; + break; + case SMARTPRO: + *size = sample_size + LIBDC_SAMPLES_SMART; + break; + case SMARTCOM: + *size = sample_size + LIBDC_SAMPLES_SMARTCOM; + break; + default: + report_error("[allocate_libdc_buffer]: Unsupported model 0x%.2X\n", model); + return NULL; + } + buf = (unsigned char *)calloc(*size, 1); + if (!buf) + report_error("[allocate_libdc_buffer]: Failed to place enough memory for libdc buffer. %d bytes.\n", *size); + return buf; +} + +/* + * This function returns an allocated memory buffer with the completed dc data. + * The buffer has to be padded in the beginning with the header libdivecomputer expects to find, + * this is, a5a55a5a following two bytes with the buffer size. + * BTW we need to manually relocate those parts of the header needed by libdivecomputer. The + * rest of the buffer is filled with the samples. + * For older serial Aladin DCs, just use dt_libdc_buffer() from datatrak.cpp importer. + */ +extern "C" unsigned char *build_dc_data(int model, dc_family_t dc_fam, char *input, int max, int *out_size) +{ + unsigned char *ptr = (unsigned char*) input; + unsigned char *buffer; + const unsigned char head_begin[] = {0xa5, 0xa5, 0x5a, 0x5a}; + int buf_size = 0; + + // Older serial Aladin profile imported to + // SmarTrak from Datatrak + if (dc_fam == DC_FAMILY_UWATEC_ALADIN) { + *out_size = max + 18; + buffer = (unsigned char *)calloc(*out_size, 1); + dt_libdc_buffer(ptr, max, model, buffer); + return buffer; + } + + buffer = allocate_libdc_buffer(model, max, &buf_size); + if (!buffer) + return NULL; // we are OOM or with unknown DC + *out_size = buf_size; + memcpy(buffer, &head_begin, 4); // place header begining + buffer[4] = buf_size & 0xFF; // calculated buffer size + buffer[5] = buf_size >> 8; + memcpy(buffer + 6, ptr + 2, 11); // initial block (unchanged) + + switch (model) { + case GALILEO: // untested with LogTrak native dives and somehow faulty + case GALILEOTRIMIX: + memcpy(buffer + LIBDC_MAX_DEPTH, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MAX_TEMP, ptr + ASD_MAX_TEMP, 2); + memcpy(buffer + LIBDC_MIN_TEMP, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_SURF_TEMP, ptr + ASD_SURF_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX, ptr + ASD_GAS_MIX, 2); + memcpy(buffer + LIBDC_TANK_PRESS, ptr + ASD_TANK_PRESS_INIT, 2); + if (model == GALILEOTRIMIX) + buffer[43] = 0x80; // libdc checks this byte to apply trimix's parsing model or galileo's + (model == GALILEO) ? memcpy(buffer + LIBDC_SETTINGS, ptr + ASD_SETTINGS, 4) : memcpy(buffer + LIBDC_SETTINGS_TRIMIX, ptr + ASD_SETTINGS, 4); + (model == GALILEO) ? memcpy(buffer + LIBDC_SAMPLES_MANTIS, ptr + ASD_SAMPLES, max - ASD_SAMPLES) : memcpy(buffer + LIBDC_SAMPLES_G2, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case ALADINTEC2G: + memcpy(buffer + LIBDC_MAX_DEPTH_SMART, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME_SMART, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MAX_TEMP, ptr + ASD_MAX_TEMP, 2); + memcpy(buffer + LIBDC_MIN_TEMP_ALADINTEC, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_SURF_TEMP, ptr + ASD_SURF_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX_ALADINTEC2G, ptr + ASD_GAS_MIX, 6); + memcpy(buffer + LIBDC_SETTINGS_ALADINTEC2G, ptr + ASD_SETTINGS, 4); + memcpy(buffer + LIBDC_SAMPLES_ALADINTEC2G, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case ALADIN2G: // tested Logtrak native Aladin 2G and Mantis 2 + case MERIDIAN: + case CHROMIS: + case MANTIS2: + case ALADINSQUARE: + memcpy(buffer + LIBDC_MAX_DEPTH, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MAX_TEMP, ptr + ASD_MAX_TEMP, 2); + memcpy(buffer + LIBDC_MIN_TEMP, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_SURF_TEMP, ptr + ASD_SURF_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX, ptr + ASD_GAS_MIX, 2); + memcpy(buffer + LIBDC_TANK_PRESS, ptr + ASD_TANK_PRESS_INIT, 2); + memcpy(buffer + LIBDC_SETTINGS, ptr + ASD_SETTINGS, 4); + memcpy(buffer + LIBDC_SAMPLES_MANTIS, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case G2: // only G2 tested, bold assumption + case G2HUD: + case G2TEK: + case G3: + case ALADINSPORTMATRIX: + case ALADINA1: + case ALADINA2: + case LUNA2AI: + case LUNA2: + memcpy(buffer + LIBDC_MAX_DEPTH, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MAX_TEMP, ptr + G2_MAX_TEMP, 2); + memcpy(buffer + LIBDC_MIN_TEMP, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_SURF_TEMP, ptr + ASD_SURF_TEMP, 2); + memcpy(buffer + LIBDC_SETTINGS_TRIMIX, ptr + ASD_SETTINGS, 4); + memcpy(buffer + LIBDC_SAMPLES_G2, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case ALADINTEC: + memcpy(buffer + LIBDC_MAX_DEPTH, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME_ALADINTEC, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MAX_TEMP, ptr + ASD_SURF_TEMP, 2); + memcpy(buffer + LIBDC_MIN_TEMP_ALADINTEC, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX_ALADINTEC, ptr + ASD_GAS_MIX, 2); + memcpy(buffer + LIBDC_SETTINGS_ALADINTEC, ptr + ASD_SETTINGS, 4); + memcpy(buffer + LIBDC_SAMPLES_ALADINTEC, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case SMARTPRO: + memcpy(buffer + LIBDC_MAX_DEPTH_SMART, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME_SMART, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MIN_TEMP_SMART, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX_SMART, ptr + ASD_GAS_MIX, 2); + memcpy(buffer + LIBDC_SAMPLES_SMART, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + case SMARTCOM: + memcpy(buffer + LIBDC_MAX_DEPTH_SMART, ptr + ASD_MAXDEPTH, 2); + memcpy(buffer + LIBDC_DIVE_TIME_SMART, ptr + ASD_DIVE_TIME, 2); + memcpy(buffer + LIBDC_MIN_TEMP_SMART, ptr + ASD_MIN_TEMP, 2); + memcpy(buffer + LIBDC_GASMIX_SMART, ptr + ASD_GAS_MIX, 2); + memcpy(buffer + LIBDC_TANK_PRESS_SMARTCOM, ptr + ASD_TANK_PRESS_INIT, 4); + memcpy(buffer + LIBDC_SAMPLES_SMARTCOM, ptr + ASD_SAMPLES, max - ASD_SAMPLES); + break; + default: + report_error("Unsupported DC model 0x%2x\n", model); + free(buffer); + buffer = NULL; + } + return buffer; +} + +/* + * Search for a given sequence of bytes in a string. + * Returns the position of the first byte of the sequence or string::npos if + * not found. + * Parameter s is the size of the sequence. + */ + +std::size_t uchar_find(const std::string &in, const unsigned char *seq, const int s) +{ + return in.find(std::string(reinterpret_cast(seq), s)); +} + +/* + * Return a utf-8 string from an .asd string. + * String fields in .asd file begin with a 3 bytes BOM followed by a 1 byte + * number, e.g. FF FE FF nn, where nn is the length of string following, + * in chars (two bytes unicode chars). May be zero, meaning an empty field. + * If anything fails the pointer to next field will be set to "", signaling + * a weirdness. The caller must handle it (aborting seems preferable as we can + * have data corruption issues). + */ +std::string asd_to_string(const std::string &input, std::string &output) +{ + if (input.empty()) { + output = input; + return input; + } + + const auto tmp = reinterpret_cast(input.data()) ; + const int size = tmp[3] * 2; // worst case, all chars are 2 bytes long + + // this is not a failure but an empty string + if (size == 0) { + output = input.substr(4); + return ""; + } + + // convert string to UTF-8 + std::vector buffer(size, 0); + for (int i = 0, j = 0; i < size; i += 2, ++j) { + const unsigned char c = (tmp[i + 5] << 4) + tmp[i + 4]; // 4 is the BOM size + if (c <= 0x7F) { + buffer[j] = c; + } else { + buffer[j] = (c >> 6) | 0xC0; + buffer[++j] = (c & 0x3F) | 0x80; + } + } + output = input.substr(size + 4); + + buffer.erase(std::remove(buffer.begin(), buffer.end(), 0), buffer.end()); + const std::string ret = std::string(buffer.begin(), buffer.end()); + // drop meaningless strings comming from SmartTrak if any + return ret.compare(" ") && ret.compare("-") && ret.compare("???") && ret.compare("---") ? ret : ""; +} + +/* + * Build a dive site with coords and name if it doesn't exist yet and place it in the table. + */ +static void asd_build_dive_site(const std::string &instring, const std::string &coords, struct divelog *log, struct dive_site **asd_site) +{ + struct dive_site *site; + double gpsX = 0, gpsY = 0; + location_t gps_loc; + + if (!coords.empty()) + sscanf(coords.data(), "%lf %lf", &gpsX, &gpsY); + + gps_loc = create_location(gpsX, gpsY); + site = log->sites.get_by_name(instring); + if (!site) { + if (!has_location(&gps_loc)) + site = log->sites.create(instring); + else + site = log->sites.create(instring, gps_loc); + } + *asd_site = site; +} + +/* + * Check if file is an export from SmartTrak divelog. + * ASD files have a unicode string at the beginning with some info, including + * the string "SmartTRAK" if generated with this software, + */ +bool is_smtk(const std::string &p) +{ + const unsigned char smart[10] = {0x53,0x00,0x6d,0x00,0x61,0x00,0x72,0x00,0x74,0x00}; + return uchar_find(p, smart, 10) < p.find("CLogEntry"); +} + +/* + * A block of buddies or equipment consist of a integer n (2 bytes big endian) followed by + * n asd strings, n = 0 meaning no strings follow. Caller func passes n (if n > 0) and points + * to the beginning of strings. Buddies are placed in dive buddies list and equipment in a string, + * returning a pointer to next byte after the parsed block. + * These are only used in SmartTrak exported .asd files, as LogTrak doesn't support such fields. + */ +std::string asd_build_others(const std::string &input, int idx, std::string &output) +{ + std::string head; + std::string ptr = input; + int i = idx; + + if (input.empty()) { + output = ""; + return ""; + } + + while (i > 0) { + std::string tail = asd_to_string(ptr, ptr); + if (! head.empty()) + head += ", "; + head += tail; + i--; + } + output = head; + return ptr; +} + +/* + * Parse a dive in a mem buffer and return 0 for correct ending or negative + * values signaling an error. -1 would be a recoverable error and -2 + * a weird issue (abort further parsing would be preferred). + */ +static int asd_dive_parser(const std::string &input, struct dive *asd_dive, struct divelog *log)//, const device_table &devices) +{ + int dc_model, s = 0, rc = 0, k, j; + dc_family_t dc_fam; + long dc_serial; + const unsigned char str_seq[] = {0xff, 0xfe, 0xff}, dc_profile_begin[4] = {0x01, 0x00, 0x00, 0xFF}; + unsigned char *dc_data; + auto devdata = std::make_unique(); + asd_dive->dcs[0].serial.resize(64); + weightsystem_t ws; + std::string tmp, d_locat, d_point, d_coords, notes, viz, w_type, w_surf, weather, buddies, equipment; + std::size_t size; + + //input should point to dc model integer + dc_model = input[0]; + dc_fam = (input[1] > 0x00) ? DC_FAMILY_UWATEC_ALADIN : DC_FAMILY_UWATEC_SMART; + tmp = input.substr(8); + + dc_serial = (tmp[3] << 24) + (tmp[2] << 16) + (tmp[1] << 8) + tmp[0]; + + if (!dc_model) { // this would be the manual dive case (coming from SmartTrak) + asd_dive->dcs[0].model = "Manually entered dive"; + } else { + rc = prepare_data(dc_model, dc_fam, devdata.get()); + if (rc != DC_STATUS_SUCCESS) + goto bailout; + asd_dive->dcs[0].model = devdata->model; + tmp = tmp.substr(4); + + if (dc_fam == DC_FAMILY_UWATEC_ALADIN) { + std::size_t pos = uchar_find(tmp, dc_profile_begin, 4); + tmp = tmp.substr(pos + 4); + } + size = uchar_find(tmp, str_seq, 3); // size of DC data + dc_data = build_dc_data(dc_model, dc_fam, tmp.data(), size, &s); + if (!dc_data) + goto bailout; + + rc = libdc_buffer_parser(asd_dive, devdata.get(), dc_data, s); + if (rc != DC_STATUS_SUCCESS) { + delete[] dc_data; + goto bailout; + } + delete[] dc_data; + // set serial in DC info, and set a node for the device. + asd_dive->dcs[0].serial = std::to_string(dc_serial); + create_device_node(log->devices, asd_dive->dcs[0].model, asd_dive->dcs[0].serial, asd_dive->dcs[0].model); + + // Now the non DC data fields. + tmp = tmp.substr(size); + } + + // There are no string fields merged with string ones, making things a bit more dificult. + d_locat = asd_to_string(tmp, tmp); + if (tmp.empty()) + goto buffail; + + d_point = asd_to_string(tmp, tmp); + if (tmp.empty()) + goto buffail; + + if (!d_point.empty() && d_locat.compare(d_point)) + d_locat += ", " + d_point; + d_coords = asd_to_string(tmp, tmp); + if (tmp.empty()) + goto buffail; + + asd_build_dive_site(d_locat, d_coords, log, &asd_dive->dive_site); + // next two bytes are the tank volume (mililiters) and two following are + // unknown (always zero, may be a 2º tank). + asd_dive->get_or_create_cylinder(0)->type.size.mliter = (tmp[1] << 8) + tmp[0]; + tmp = tmp.substr(4); + asd_dive->get_cylinder(0)->type.description = asd_to_string(tmp, tmp); + if (tmp.empty()) + goto buffail; + + asd_dive->suit = asd_to_string(tmp, tmp); + if (tmp.empty()) + goto buffail; + + // next two bytes are the weight in gr. Two following are zeroed. + ws = { { .grams = (tmp[1] << 8) + tmp[0] }, translate("gettextFromC", "unknown"), false }; + asd_dive->weightsystems.push_back(std::move(ws)); + tmp = tmp.substr(4); + + // weather + weather = asd_to_string(tmp, tmp); + if (!weather.empty()) + taglist_add_tag(asd_dive->tags, weather); + if (tmp.empty()) + goto buffail; + + // water surface + w_surf = asd_to_string(tmp,tmp); + if (!w_surf.empty()) + taglist_add_tag(asd_dive->tags, w_surf); + if (tmp.empty()) + goto buffail; + + // water type (localized string; can't be used for salinity) + w_type = asd_to_string(tmp, tmp); + if (!w_type.empty()) + taglist_add_tag(asd_dive->tags, w_type); + if (tmp.empty()) + goto buffail; + + // visibility (localized string; can't be used for rating viz) + viz = asd_to_string(tmp, tmp); + if (!viz.empty()) + taglist_add_tag(asd_dive->tags, viz); + if (tmp.empty()) + goto buffail; + + tmp = tmp.substr(2); + // Nº of buddies strings following + j = (tmp[1] << 8 ) + tmp[0]; + tmp = tmp.substr(2); + if (j > 0) { + tmp = asd_build_others(tmp, j, buddies); + asd_dive->buddy = buddies; + } + if (tmp.empty()) + goto buffail; + + // Nº of equipment items strings following + k = (tmp[1] << 8 ) + tmp[0]; + tmp = tmp.substr(2); + if (k > 0) + tmp = asd_build_others(tmp, k, equipment); + if (tmp.empty()) + goto buffail; + + // Notes if any. Include equipment in notes. + notes = asd_to_string(tmp, tmp); + if (! equipment.empty()) { + if (notes.size() > 0) { + notes += "\n"; + } + notes += (translate("gettextFromC", "Equipment: ") + equipment); + } + asd_dive->notes = notes; + if (tmp.empty()) + goto buffail; + + return 0; +bailout: // give a pointer, we can parse other dives yet + report_error("Warning [asd_dive_parser]: Non critical error in dive %d. Keep on parsing next dive.\n", asd_dive->number); + return -1; +buffail: // quit caller func, something weird is going on + report_error("[asd_dive_parser()]\tMemory corruption or file damaged. Dive %d\n", asd_dive->number); + return -2; +} + +/* + * Main function + */ +int scubapro_asd_import(const std::string &mem, struct divelog *log) +{ + std::string runner; + const char init_seq[4] = {0x07, 0x00, 0x10, 0x00}; + std::string first_dive_seq = {"CLogEntry"}; + unsigned char dive_seq_lt[4] = {0x00, 0x00, 0x80, 0x01}; + unsigned char dive_seq_smtk[4] = {0x00, 0x00, 0x01, 0x80}; + const unsigned char *dive_seq = dive_seq_lt; + int dive_count = 0; + + setlocale(LC_NUMERIC, "POSIX"); + setlocale(LC_CTYPE, ""); + + // check header + if (! mem.compare(0, 4, init_seq)) { + report_error("This doesn't look like an .asd file. Please check it.\n"); + return 1; + } + // is this .asd coming from SmartTrak ? + if (is_smtk(mem)) + dive_seq = dive_seq_smtk; + + // Jump to initial dive data secuence + std::size_t pos = mem.find(first_dive_seq); + if (pos != std::string::npos) { + runner = mem.substr(pos + 9); + } else { + report_error("This doesn't look like an .asd file. Please check it.\n"); + return 1; + } + + // We are on the first byte of the first dive (DC model). Subsequent dives in log + // will begin with 0x80 0x01 or 0x01 0x80 depending on original software. + while (pos < std::string::npos) { + auto asd_dive = std::make_unique(); + dive_count++; + asd_dive->number = dive_count; + pos = 0; + int rc = asd_dive_parser(runner, asd_dive.get(), log); + switch (rc) { + case 0: { + log->dives.record_dive(std::move(asd_dive)); + break; + } + case -1: { // recoverable error + report_error("Error parsing dive %d\n", dive_count); + break; + } + case -2: { // no recoverable error + report_error("Error parsing dive %d\n", dive_count); + return 1; + } + } + // Look for next dive and jump to it + pos = uchar_find(runner, dive_seq, 4); + runner = runner.substr(pos + 4); + }; + log->dives.sort(); + return 0; +} diff --git a/core/import-logtrak.cpp b/core/import-logtrak.cpp new file mode 100644 index 000000000..56bc8ce33 --- /dev/null +++ b/core/import-logtrak.cpp @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Scubapro's LogTRAK is a Java based program running on Windows and MacOS. + * It seems to be a development on older TravelTRAK software, and shares with + * it the exportable .asd binary file format. + * More recent versions of LogTRAK seem to support downloading data from + * older IR based devices (Galileo, etc), not just Bluetooth BLE devices. + * + * Dive log data are kept (only valid for Windows) in a directory structure + * on the user folder like this: + * - user directory -|- .jtrak -|- DB_V4 -|- jtrak.properties + * | |- jtrak.script + * |- raw_data -|(empty) + * |- DBok.asd + * |- logtrak.log + * + * For us the interesting file is the one with the .script extension. + * + * LogTRAK uses HSQLdb under the hood, and fortunately, stores all the data in + * the .script file; being this file an script to build on the fly the in-memory + * HSQLDB database each time the software is run. + * + * The .script file is, thus, a plain text file containing a serie of HSQLDB + * commands which contain themselves the full dive info (including full raw DC + * data in plain ascii text), and some other LogTRAK produced information. + * + * LogTRAK (in my opinion) is a very limited software, even difficult to name it + * a true divelog. It doesn't support manual dives insertion or addition; most + * of the information it supports comes from the DC device and very (very, very) + * limited info addition is supported (not even buddies). + * + * It can import data from SmartTrak via .asd files, although this should be + * avoided as a big amount of data is lost in the process. From a Subsurface + * point of view, SmartTrak import is always better than LogTrak import as, for + * a concerned diver, first one can store much more data. + */ + +#include +#include +#include +#include + +#include "gettext.h" +#include "dive.h" +#include "divelist.h" +#include "file.h" +#include "device.h" +#include "subsurface-string.h" +#include "libdivecomputer.h" +#include "divesite.h" +#include "locale.h" +#include "errorhelper.h" +#include "divelog.h" +#include "tag.h" +#include "format.h" + +/* + * Defined in import-asd.cpp and shared here. + */ +extern "C" dc_descriptor_t *get_data_descriptor(int data_model, dc_family_t data_fam); + +struct T_BOTTLE +{ + std::string equipment_id; + int tank_vol; // mililiters + int tanknum; // tank number in dive + int startp; // start pressure, bar + int endp; // end pressure, bar + int o2_mix; // O2 percent + int he_mix; // HE percent + std::string tankmat; // tank material +}; + +struct T_EQUIPMENT +{ + std::string equipment_id; + std::string suit; + float weight = 0; +}; + +struct T_LOCATION +{ + std::string loc_id; + std::string loc_name; +}; + +struct T_SITE +{ + std::string site_id; + std::string site_name; + float GPSx; + float GPSy; + std::string comment; + struct T_LOCATION site_loc; +}; + +std::vector bottle_db; +std::vector equipment_db; +std::vector location_db; +std::vector site_db; +std::string db_version; + +/* + * This macro seems weird in the negative part of condition. But it's meant to manage + * ascii chars ranging 0..9 and a..z, no others. Thus a string "0a1f" becomes + * 0x00 0x0a 0x01 0x0f. + */ +#define ASCII_CHR_TO_BYTE(_char) ((_char < 58) ? _char - 48: _char - 87) + +/* + * Returns a sigle line string from the full buffer. + */ +static std::string get_single_line(std::string &begin) +{ + std::string line; + + auto ss = std::stringstream{begin}; + std::getline(ss, line, '\n'); + + return line; +} + +/* + * Most strings in LogTrak are sigle-quoted. This function removes initial and + * ending single quotes and returns a clean string. + */ +static std::string lt_remove_quotes(const std::string &input) +{ + std::string tmp = input; + + if (input.front() == 0x27) + tmp.erase(tmp.begin()); + + if (tmp.back() == 0x27) + tmp.pop_back(); + + return tmp; +} + +/* + * We can catch some strings "NULL" or some other weird strings coming from + * SmartTrak, "???" or "---" (default strings in mandatory fields). + * Check the string. If true, the caller must remove its content. + */ +static bool is_null(const std::string &input) { + return (input == "NULL" || input == "???" || input == "---"); +} + +/* + * LogTrak scapes single quotes with another quote. + * Remove one of them if we find two in a string. + */ +static std::string trim_quotes(const std::string &in) { + std::string tmp = in; + + if (in.empty()) + return in; + size_t s = tmp.find("''"); + while (s != std::string::npos) { + tmp.erase(s, 1); + s = tmp.find("''", s); + } + return tmp; +} + +/* + * Utility to convert an UCN syntax string \\uxxyy in another one + * containing an 1, 2 or 3 bytes utf-8 char. + * AFAIK LogTrak only supports 2 byte unicode, so we don't + * expect integers bigger than 0xFFFF. + */ +static std::string u_str_to_utf8(const std::string &in) +{ + if (in.empty()) + return in; + + std::string tmp; + int c = (ASCII_CHR_TO_BYTE(in[2]) << 12) + (ASCII_CHR_TO_BYTE(in[3]) << 8) + (ASCII_CHR_TO_BYTE(in[4]) << 4) + ASCII_CHR_TO_BYTE(in[5]); + + if (c <= 0x7F) { + tmp.push_back(static_cast(c)); + } + else if (c <= 0x7FF) { + tmp.push_back(static_cast((c >> 6) | 0xC0)); + tmp.push_back(static_cast((c & 0x3F) | 0x80)); + } else { + tmp.push_back(static_cast((c >> 12) | 0xE0)); + tmp.push_back(static_cast(((c >> 6) & 0x3F) | 0x80)); + tmp.push_back(static_cast((c & 0x3F) | 0x80)); + } + return tmp; +} + +/* + * Parse a string containing unicode chars in + * UCN syntax "\\u000a" and convert them to utf-8. + */ +static std::string to_utf8(const std::string &in) +{ + if (in.empty()) + return in; + + std::string tmp = in; + std::size_t pos = tmp.find("\\u"); + while (pos != std::string::npos) { + std::string u_str = u_str_to_utf8(tmp.substr(pos, 6)); + tmp.replace(pos, 6, u_str); + pos = tmp.find("\\u", pos + u_str.length()); + } + return tmp; +} + +/* + * Process a string removing single quotes, escaped quotes, catching "NULL" + * and converting unicode characters to utf-8 if any. + */ +static std::string process(std::string &input) +{ + if (input.empty()) + return input; + + if (is_null(input)) { + input.clear(); + return input; + } + input = lt_remove_quotes(input); + input = to_utf8(input); + return trim_quotes(input); +} + +/* + * A class to manage LogTrak strings. + * The constructor just "cleans" the input string and converts it to UTF8 + * using previosusly defined functions. + */ +class Lt_String { + std::string str; + +public: + Lt_String() = default; + Lt_String(std::string input); + Lt_String(const char *in); + ~Lt_String(); + const std::string& string() { return str; } +}; + +Lt_String::Lt_String(std::string in) +{ + this->str = process(in); +} + +Lt_String::Lt_String(const char *in) +{ + std::string tmp(in); + this->str = process(tmp); +} + +Lt_String::~Lt_String() +{ + str.clear(); +} + +/* + * LogTrak, like SmartTrak, stores the whole data downloaded from the DC. + * It is stored as a plain ascii sequence of chars (e.g. "a5a50eff..."); this + * function process an string in such format and returns a buffer with bytes. + */ +extern "C" bool lt_convert_profile(unsigned char *input, unsigned char *output) +{ + unsigned char *runner = input; + int i = 0; + + if (!runner || !*runner) + return false; + + while (runner && *runner) { + output[i] = ( ASCII_CHR_TO_BYTE(runner[0]) << 4 ) + ASCII_CHR_TO_BYTE(runner[1]); + i++; + runner += 2; + } + return true; +} + +/* + * Extract an ascii text string of unknown format from a logtrak db line. + * String must be pointed to its first char. + * Places the string on the passed variable ref and returns a pointer to the + * following text to parse. + */ + +static std::string get_lt_string(const std::string &input, Lt_String &output) +{ + if (input.empty()){ + return input; + } + std::size_t pos = input.find("',"); + if (pos == std::string::npos) + pos = input.find("')"); + if (pos == std::string::npos){ + return input; + } + Lt_String tmp(input.substr(0, pos)); + output = tmp; + return input.substr(pos + 2); +} + +/* + * Load auxiliary tables data to our vectors. Main T_DIVE table will + * be parsed in logtrak_import() function. + */ +static void lt_auxiliary_parser(const std::string &buffer) +{ + std::size_t pos = buffer.find("INSERT INTO "); + std::string runner = buffer.substr(pos); + while (pos < std::string::npos) { + std::string line = get_single_line(runner); + std::size_t lpos = line.find(" VALUES"); + std::string ltable = line.substr(12, lpos - 12); + lpos = line.find('('); + line = line.substr(lpos + 1); + if (ltable == "T_BOTTLE") { + char *tankmat, *eq_ref; + int tankvol = 0, o2mix = 0, startp = 0, endp = 0, hemix = 0, tanknum = 0; + std::sscanf(line.c_str(), "%*[0-9],%d,%d,%d,%d,%m[a-zA-Z0-9-_'],%*d,%*[0-9a-zA-Z'],%d,%*d,%d,%m[0-9],", &tankvol, &o2mix, &startp, &endp, &tankmat, &hemix, &tanknum, &eq_ref); + Lt_String tmp(tankmat); + bottle_db.push_back({eq_ref, tankvol, tanknum, startp, endp, o2mix, hemix, tmp.string()}); + free(tankmat); + free(eq_ref); + } else if (ltable == "T_EQUIPMENT") { + Lt_String suit; + char *eq_ref; + float weight_s = 0; + std::sscanf(line.c_str(), "%m[0-9],%*[0-9a-zA-Z'],%*[0-9a-zA-Z' \\],%f", &eq_ref, &weight_s); + lpos = line.find(",'"); + line = line.substr(lpos + 2); + get_lt_string(line, suit); + equipment_db.push_back({eq_ref, suit.string(), weight_s}); + free(eq_ref); + } else if (ltable == "T_LOCATION") { + char *id; + Lt_String name; + std::sscanf(line.c_str(), "%m[0-9],", &id); + lpos = line.find(",'"); + line = line.substr(lpos + 2); + get_lt_string(line, name); + std::string loc_id(id); + location_db.push_back({loc_id, name.string()}); + free(id); + } else if (ltable == "T_SITE") { + char *id = NULL, *locid = NULL; + float GPSx = 0, GPSy = 0; + Lt_String name, comm; + std::sscanf(line.c_str(), "%m[0-9],", &id); + lpos = line.find(",'"); + line = line.substr(lpos + 2); + line = get_lt_string(line, name); + std::sscanf(line.c_str(),"%f,%f,", &GPSx, &GPSy); + lpos = line.find(",'"); + line = line.substr(lpos + 2); + line = get_lt_string(line, comm); + std::sscanf(line.c_str(), "%*m[a-zA-Z0-9],%m[0-9])", &locid); + std::string loc_id(locid); + int i = 0; + while (location_db[i].loc_id != loc_id) + i++; + site_db.push_back({id, name.string(), GPSx, GPSy, comm.string(), {loc_id, location_db[i].loc_name}}); + free(id); + free(locid); + } else if (ltable == "TABLE_DBVERSION") { + Lt_String db_ver; + lpos = line.find(",'"); + line = line.substr(lpos + 2); + get_lt_string(line, db_ver); + db_version = db_ver.string(); + } + runner = runner.substr(2); + pos = runner.find("INSERT INTO "); + if (pos < std::string::npos) + runner = runner.substr(pos); + } +} + +/* + * Build tank info for a given dive. + * AFAIK there is no chance in LogTrak to manually add tanks, so there should + * be no difference between DC data and divelog data, just the complementary + * data (e.g. tank volume, tank material ...) not in DC. + */ +static void lt_get_tank_info(char *eq_id, struct dive *ltd) +{ + std::string eqid(eq_id); + + auto it = std::find_if(bottle_db.begin(), bottle_db.end(), [eqid](const T_BOTTLE &bottle) { + return bottle.equipment_id == eqid; + }); + + const T_BOTTLE &bottle = *it; + int tanknum = bottle.tanknum - 1; + if (bottle.tank_vol) + ltd->get_or_create_cylinder(tanknum)->type.size.mliter = bottle.tank_vol; + // Always prefer data from DC over data from Logtrak + if (bottle.startp && !ltd->get_or_create_cylinder(tanknum)->start.mbar) + ltd->get_or_create_cylinder(tanknum)->start.mbar = bottle.startp * 1000; + if (bottle.endp && !ltd->get_or_create_cylinder(tanknum)->end.mbar) + ltd->get_or_create_cylinder(tanknum)->end.mbar = bottle.endp * 1000; + if (bottle.o2_mix && !ltd->get_or_create_cylinder(tanknum)->gasmix.o2.permille) + ltd->get_or_create_cylinder(tanknum)->gasmix.o2.permille = bottle.o2_mix * 10; + if (bottle.he_mix && !ltd->get_or_create_cylinder(tanknum)->gasmix.he.permille) + ltd->get_or_create_cylinder(tanknum)->gasmix.he.permille = bottle.he_mix * 10; + if (!bottle.tankmat.empty()) + ltd->get_or_create_cylinder(tanknum)->type.description = bottle.tankmat; +} + +/* + * Build a site for a given dive. + * Check if it exist, to avoid duplicities. + */ +static void lt_build_dive_site(const char *site_id, struct divelog *log, struct dive_site **lt_site) +{ + /* Abort if we don't have a site_id */ + if (empty_string(site_id)) { + lt_site = NULL; + return; + } + + auto it = std::find_if(site_db.begin(), site_db.end(), [site_id](const T_SITE &site) { + return site.site_id == site_id; + }); + + if (it == site_db.end()) { + *lt_site = nullptr; + return; + } + + // LogTrak enforces location/site structure, but lazy user may set just one, + // resulting in the same name for both, location and site. Ensure we just use + // one in this case. + const T_SITE &site_data = *it; + std::string built_name = !site_data.site_name.empty() && site_data.site_name != site_data.site_loc.loc_name ? site_data.site_loc.loc_name + ", " + site_data.site_name : site_data.site_loc.loc_name; + // build gps position + location_t gps_loc = create_location(site_data.GPSx, site_data.GPSy); + + // build the dive site structure + struct dive_site *site = log->sites.get_by_name(built_name); + if (!site) { + site = has_location(&gps_loc) ? log->sites.create(built_name, gps_loc) : log->sites.create(built_name); + } + if (!site_data.comment.empty()) + site->notes = site_data.comment; + + *lt_site = site; +} + +/* + * Main function. + * Runs along recived buffer importing T_DIVE data to subsurface's dives. + * WARNING!! LogTrak supports more than a divelog in a single db, so we may end up + * with a mixed divelog. + * Input: a std::string buffer produced by readfile() an a dive_table list. + * Output: Luckily adds LogTrak dives to the dive_table list. + * Returns: Integer (0 as default or negative values on error). + */ +int logtrak_import(const std::string &mem, struct divelog *log) +{ + double ltd_temp = 0, ltd_mintemp = 0; + int ltd_max_depth = 0, dive_count = 0; + std::string tmpstr; + std::size_t pos; + + setlocale(LC_NUMERIC, "POSIX"); + setlocale(LC_CTYPE, ""); + + // Set auxiliary DBs + lt_auxiliary_parser(mem); + + pos = mem.find("INSERT INTO T_DIVE "); + std::string runner = mem.substr(pos); + + while (pos < std::string::npos) { + char *ltd_id = NULL, *ltd_type = NULL, *ltd_weather = NULL, *ltd_ref_eq = NULL, *ltd_ref_site = NULL, + *ltd_dc = NULL, *ltd_dc_id = NULL, *ltd_dive = NULL, *ltd_dc_soft = NULL, *ltd_gf_low = NULL, + *ltd_gf_high = NULL, *ltd_log_id = NULL, *ltd_airtemp = NULL; + auto lt_dive = std::make_unique(); + auto devdata = std::make_unique(); + dive_count++; + Lt_String ltd_notes; + int rc; + + tmpstr=get_single_line(runner); + // break the loop if we can't get a line or empty, something went wrong + if (tmpstr.empty()) { + report_error("[LogTrak import] Couldn't get any dive. Check the selected file."); + return -1; + } + lt_dive->number = dive_count; + pos = tmpstr.find('('); + tmpstr = tmpstr.substr(pos + 1); + // Read up to time zone. Discard this one. + std::sscanf(tmpstr.c_str(), "%m[0-9],'%m[a-zA-Z0-9]',%lf,'%m[a-zA-Z0-9]',%d,%d,]",\ + <d_id, <d_type, <d_temp, <d_weather, <_dive->visibility, <_dive->rating); + + if (ltd_temp) + lt_dive->watertemp.mkelvin = C_to_mkelvin(ltd_temp); + if (!empty_string(ltd_weather)) { + taglist_add_tag(lt_dive->tags, ltd_weather); + free(ltd_weather); + } + + // Move past time zone + for (int i = 0; i < 3; i++){ + pos = tmpstr.find("',") + 2; + tmpstr = tmpstr.substr(pos); + } + // The notes string will be manually parsed as it can contain every printable caracter + // and even non printable in unicode format; e.g. "\n" will show as \u000a. The notes + // string is 256 char long, at most. + tmpstr = get_lt_string(tmpstr, ltd_notes); + lt_dive->notes = ltd_notes.string().c_str(); + // Continue with sscanf. Send useless trends to devnull + // There are, at least two different versions of DB with different order + if (db_version != "1.3.5") + rc = std::sscanf(tmpstr.c_str(),"%m[0-9A-Z],%m[0-9A-Z],%*m[-0-9A-Z],%m[0-9A-Z],%m[0-9A-Z],%m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%d,%*m[-0-9A-Z],%lf,%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%m[0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%m[0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%m[0-9A-Z],%m[0-9A-Z],%m[0-9A-Z]",<d_ref_eq, <d_ref_site, <d_dc, <d_dc_id, <d_dive, <d_max_depth, <d_mintemp, <d_airtemp, <d_dc_soft, <d_gf_low, <d_gf_high, <d_log_id); + else + rc = std::sscanf(tmpstr.c_str(),"%m[0-9A-Z],%m[0-9A-Z],%*m[-0-9A-Z],%m[0-9A-Z],%m[0-9A-Z],%m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%d,%*m[-0-9A-Z],%lf,%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%m[0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[-0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%*m[-0-9A-Z],%*m[-0-9A-Z],%m[0-9A-Z],%*m[0-9A-Za-z'],%*m[0-9A-Za-z'],%m[0-9A-Z],%m[0-9A-Z],%m[0-9A-Z],",<d_ref_eq, <d_ref_site, <d_dc, <d_dc_id, <d_dive, <d_max_depth, <d_mintemp, <d_airtemp, <d_log_id, <d_dc_soft, <d_gf_low, <d_gf_high); + if (rc < 12) { + report_error("[LogTrak import] Only %d var parsed by sscanf for dive %d with id %s. Please, check it\n", rc, lt_dive->number, ltd_id); + runner = runner.substr(pos + 1); + pos = runner.find("INSERT INTO T_DIVE "); + runner = runner.substr(pos + 1); + free(ltd_id); + continue; + } + // Unused ATM + free(ltd_log_id); + free(ltd_id); + // Process DC data + std::string d(ltd_dive); + if (!d.empty() && !is_null(d)) { + d = lt_remove_quotes(d); + int prf_size = (int)ceil(d.length() / 2); + std::vector prf_buffer(prf_size); + if (!lt_convert_profile(reinterpret_cast(d.data()), prf_buffer.data())) + report_error("[lt_convert_profile] FAILED for dive %d\n", lt_dive->number); + free(ltd_dive); + int dc_model = 0; + if (!empty_string(ltd_dc)) + dc_model = lrint(strtod(ltd_dc, NULL)) & 0xFF; + free(ltd_dc); + // No DC_FAMILY_UWATEC_ALADIN coming from LogTrak, they aren't supported there + devdata->descriptor = get_data_descriptor(dc_model, DC_FAMILY_UWATEC_SMART); + if (devdata->descriptor) { + // No need to check vendor or product if we got a correct descriptor + devdata->vendor = dc_descriptor_get_vendor(devdata->descriptor); + devdata->product = dc_descriptor_get_product(devdata->descriptor); + devdata->model = format_string_std("%s %s", devdata->vendor.c_str(), devdata->product.c_str()).c_str(); + lt_dive->dcs[0].model = devdata->model; + // Galileo TMX devices use a different data set and parsing model in libdc. + // Libdc checks buffer's byte #43 to know which model to use, and fails if + // it's not set to 0x80, but has Galileo TMX data set. Thus we need to + // ensure this byte value, as some Galileo devices didn't set it. + if (dc_model == 0x19) + prf_buffer[43] = 0x80; + + libdc_buffer_parser(lt_dive.get(), devdata.get(), prf_buffer.data(), prf_size); + + lt_dive->dcs[0].serial = ltd_dc_id; + Lt_String soft(ltd_dc_soft); + lt_dive->dcs[0].fw_version = soft.string().c_str(); + create_device_node(log->devices, lt_dive->dcs[0].model, lt_dive->dcs[0].serial, ""); + } else { + report_error("Unsuported DC model 0x%X. Dive num %d\n", dc_model, lt_dive->number); + } + } + // Get some DC related data, but always prefer libdc processed data + if (lt_dive->maxdepth.mm == 0 && ltd_max_depth > 0) + lt_dive->maxdepth.mm = ltd_max_depth * 10; + if (ltd_mintemp) { + if (lt_dive->mintemp.mkelvin == 0) + lt_dive->mintemp.mkelvin = C_to_mkelvin(ltd_mintemp / 10); + if (lt_dive->dcs[0].watertemp.mkelvin == 0) + lt_dive->dcs[0].watertemp.mkelvin = C_to_mkelvin(ltd_mintemp / 10); + } + if (lt_dive->airtemp.mkelvin == 0) { + if (!is_null(ltd_airtemp)) { + lt_dive->airtemp.mkelvin = C_to_mkelvin(strtod(ltd_airtemp, NULL) / 10); + free(ltd_airtemp); + } + } + + // Get suit and weight, suit may be freely edited field + if (!empty_string(ltd_ref_eq)) { + std::string eq_ref(ltd_ref_eq); + int i = 0; + while (equipment_db[i].equipment_id != eq_ref) + i++; + lt_dive->suit = equipment_db[i].suit; + if (equipment_db[i].weight > 0) { + weightsystem_t ws = { { .grams = (int)lroundf(equipment_db[i].weight * 1000) }, translate("gettextFromC", "unknown"), false }; + lt_dive->weightsystems.push_back(std::move(ws)); + } + // Get tanks info. Tanks are refered to dive via the T_EQUIPMENT id + lt_get_tank_info(ltd_ref_eq, lt_dive.get()); + } + free(ltd_ref_eq); + + // Get site/location info. Refered via T_SITE + lt_build_dive_site(ltd_ref_site, log, <_dive->dive_site); + free(ltd_ref_site); + + // Set some extra data which can be interesting + add_extra_data(<_dive->dcs[0], "LogTrak Version", db_version); + if (!empty_string(ltd_type)) + add_extra_data(<_dive->dcs[0], "DC Type", ltd_type); + free(ltd_type); + if (!lt_dive->dcs[0].fw_version.empty() && strcmp(lt_dive->dcs[0].fw_version.c_str(), "0")) + add_extra_data(<_dive->dcs[0], "DC Firmware Version", lt_dive->dcs[0].fw_version); + Lt_String gfl(ltd_gf_low); + Lt_String gfh(ltd_gf_high); + if (gfl.string() != "" && gfl.string() != "0") + add_extra_data(<_dive->dcs[0], "GF Low", gfl.string().c_str()); + if (gfh.string() != "" && gfh.string() != "0") + add_extra_data(<_dive->dcs[0], "GF High", gfh.string().c_str()); + add_extra_data(<_dive->dcs[0], "DC Serial", lt_dive->dcs[0].serial); + free(ltd_gf_low); + free(ltd_gf_high); + + log->dives.record_dive(std::move(lt_dive)); + runner = runner.substr(pos + 1); + pos = runner.find("INSERT INTO T_DIVE "); + runner = runner.substr(pos + 1); + } + + // Clean DB + bottle_db.resize(0); + equipment_db.resize(0); + location_db.resize(0); + site_db.resize(0); + return 0; +} diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp index 806b0d428..774cd2a8e 100644 --- a/desktop-widgets/mainwindow.cpp +++ b/desktop-widgets/mainwindow.cpp @@ -898,6 +898,8 @@ QString MainWindow::filter_open() " *.apd" " *.dive" " *.zxu *.zxl" + " *.script" + " *.asd" ");;"; f += tr("Subsurface files") + " (*.ssrf *.xml);;"; @@ -914,7 +916,9 @@ QString MainWindow::filter_open() f += tr("MkVI files") + " (*.txt);;"; f += tr("APD log viewer") + " (*.apd);;"; f += tr("OSTCtools") + " (*.dive);;"; - f += tr("DAN DL7") + " (*.zxu *.zxl)"; + f += tr("DAN DL7") + " (*.zxu *.zxl);;"; + f += tr("LogTrak/JTrak") + " (*.script);;"; + f += tr("Scubapro ASD") + " (*.asd)"; return f; } @@ -940,6 +944,8 @@ QString MainWindow::filter_import() " *.apd" " *.dive" " *.zxu *.zxl" + " *.script" + " *.asd" ");;"; f += tr("Subsurface files") + " (*.ssrf *.xml);;"; @@ -958,6 +964,8 @@ QString MainWindow::filter_import() f += tr("APD log viewer") + " (*.apd);;"; f += tr("OSTCtools") + " (*.dive);;"; f += tr("DAN DL7") + " (*.zxu *.zxl);;"; + f += tr("LogTrak/JTrak") + " (*.script);;"; + f += tr("Scubapro ASD") + " (*.asd);;"; f += tr("All files") + " (*.*)"; return f;