Add suport to import Scubapro's LogTrak files

Import can be done from .script files generated by LogTrak software or
from .asd files generated by SmartTrak and LogTrak export option.

This code was writen in C, and has just been "ported" to some extent to
C++, so it can work with recent rework of Subsurface to C++.

I'm not a C++ guy, so this is mostly C with some make-up.

Signed-off-by: Salvador Cuñat <salvador.cunat@gmail.com>
This commit is contained in:
Salvador Cuñat 2024-12-22 18:49:43 +01:00 committed by Michael Keller
parent 9baf36da45
commit 9e6ed55cf5
6 changed files with 1355 additions and 1 deletions

View file

@ -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

View file

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

View file

@ -33,5 +33,7 @@ extern std::pair<std::string, int> 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

693
core/import-asd.cpp Normal file
View file

@ -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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <stdbool.h>
#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<const char *>(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<const unsigned char *>(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<char> 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<device_data_t>();
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>();
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;
}

639
core/import-logtrak.cpp Normal file
View file

@ -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 <stdlib.h>
#include <string.h>
#include <sstream>
#include <vector>
#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<T_BOTTLE> bottle_db;
std::vector<T_EQUIPMENT> equipment_db;
std::vector<T_LOCATION> location_db;
std::vector<T_SITE> 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<char>(c));
}
else if (c <= 0x7FF) {
tmp.push_back(static_cast<char>((c >> 6) | 0xC0));
tmp.push_back(static_cast<char>((c & 0x3F) | 0x80));
} else {
tmp.push_back(static_cast<char>((c >> 12) | 0xE0));
tmp.push_back(static_cast<char>(((c >> 6) & 0x3F) | 0x80));
tmp.push_back(static_cast<char>((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<dive>();
auto devdata = std::make_unique<device_data_t>();
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,]",\
&ltd_id, &ltd_type, &ltd_temp, &ltd_weather, &lt_dive->visibility, &lt_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]",&ltd_ref_eq, &ltd_ref_site, &ltd_dc, &ltd_dc_id, &ltd_dive, &ltd_max_depth, &ltd_mintemp, &ltd_airtemp, &ltd_dc_soft, &ltd_gf_low, &ltd_gf_high, &ltd_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],",&ltd_ref_eq, &ltd_ref_site, &ltd_dc, &ltd_dc_id, &ltd_dive, &ltd_max_depth, &ltd_mintemp, &ltd_airtemp, &ltd_log_id, &ltd_dc_soft, &ltd_gf_low, &ltd_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<unsigned char> prf_buffer(prf_size);
if (!lt_convert_profile(reinterpret_cast<unsigned char *>(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, &lt_dive->dive_site);
free(ltd_ref_site);
// Set some extra data which can be interesting
add_extra_data(&lt_dive->dcs[0], "LogTrak Version", db_version);
if (!empty_string(ltd_type))
add_extra_data(&lt_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(&lt_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(&lt_dive->dcs[0], "GF Low", gfl.string().c_str());
if (gfh.string() != "" && gfh.string() != "0")
add_extra_data(&lt_dive->dcs[0], "GF High", gfh.string().c_str());
add_extra_data(&lt_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;
}

View file

@ -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;