diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | Documentation/user-manual.txt | 15 | ||||
-rw-r--r-- | core/CMakeLists.txt | 1 | ||||
-rw-r--r-- | core/file.c | 10 | ||||
-rw-r--r-- | core/import-seac.c | 303 | ||||
-rw-r--r-- | core/parse.h | 1 | ||||
-rw-r--r-- | core/qthelper.cpp | 7 | ||||
-rw-r--r-- | core/qthelper.h | 2 | ||||
-rw-r--r-- | core/units.h | 5 | ||||
-rw-r--r-- | dives/TestDiveSeacSync.db | bin | 0 -> 73728 bytes | |||
-rw-r--r-- | packaging/ios/Subsurface-mobile.pro | 1 |
11 files changed, 345 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e92bb5e60..1b63acb77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ core: correctly recognize A1 as BLE dive computer planner: Honor the "O2 narcotic" preference when computing MND desktop: fix broken merging of dives with multiple cylinders mobile: add information about the cloud sync state to the Subsurface plate in the menu +core: Add parser for SeacSync db. Currently only Seac Action uses SeacSync program. --- * Always add new entries at the very top of this file above other existing entries and this note. diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index 30693cd52..e741d5507 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -1197,6 +1197,7 @@ available, as in dialog *B*, above. Currently these are: - OSTC Tools logs - JDiveLog - Suunto Dive Manager (DM3, DM4, DM5) + - Seac SeacSync - DL7 files used by Diver's Alert network (DAN) - Underwater technologies AV1 dive logs - Divesoft dive logs @@ -1373,6 +1374,20 @@ _Subsurface_ *Dive List* panel. image::images/Divelogs1.jpg["FIGURE:Download from Divelogs.de",align="center"] +[[S_ImportingSeacSync]] +==== Importing dives from SeacSync + +_Subsurface_ can directly import the database generated by the proprietary SeacSync +application. Simply locate the database file on your computer, then use the +<<Unified_import, universal import dialog.>> + +On Windows 10, The SeacSync database is typically found in: + C:\[USERNAME]\AppData\Roaming\SeacSync\divesDB.db + +_Subsurface_ will not modify the database during import, so it can be imported directly from this +location. + + [[S_ImportingCSVData]] ==== Importing data in CSV format diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 4a498cde8..fedba3614 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -110,6 +110,7 @@ set(SUBSURFACE_CORE_LIB_SRCS import-divinglog.c import-shearwater.c import-suunto.c + import-seac.c libdivecomputer.c libdivecomputer.h liquivision.c diff --git a/core/file.c b/core/file.c index ed00b7948..619ec5724 100644 --- a/core/file.c +++ b/core/file.c @@ -136,6 +136,7 @@ static int try_to_open_db(const char *filename, struct memblock *mem, struct div char shearwater_cloud_test[] = "select count(*) from sqlite_master where type='table' and name='SyncV3MetadataDiveLog' and sql like '%CreatedDevice%'"; char cobalt_test[] = "select count(*) from sqlite_master where type='table' and name='TrackPoints' and sql like '%DepthPressure%'"; char divinglog_test[] = "select count(*) from sqlite_master where type='table' and name='DBInfo' and sql like '%PrgName%'"; + char seacsync_test[] = "select count(*) from sqlite_master where type='table' and name='dive_data' and sql like '%ndl_tts_s%'"; int retval; retval = sqlite3_open(filename, &handle); @@ -193,6 +194,15 @@ static int try_to_open_db(const char *filename, struct memblock *mem, struct div return retval; } + /* Testing if DB schema resembles Seac database format */ + retval = sqlite3_exec(handle, seacsync_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_seac_buffer(handle, filename, mem->buffer, mem->size, table, trips, sites); + sqlite3_close(handle); + return retval; + } + + sqlite3_close(handle); return retval; diff --git a/core/import-seac.c b/core/import-seac.c new file mode 100644 index 000000000..4b34cbe99 --- /dev/null +++ b/core/import-seac.c @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifdef __clang__ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" +#endif + +#include "qthelper.h" +#include "ssrf.h" +#include "dive.h" +#include "subsurface-string.h" +#include "subsurface-time.h" +#include "parse.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "gettext.h" +#include "tag.h" +#include "errorhelper.h" +#include <string.h> + +/* Process gas change event for seac database. + * Create gas change event at the time of the + * current sample. + */ +static int seac_gaschange(void *param, sqlite3_stmt *sqlstmt) +{ + struct parser_state *state = (struct parser_state *)param; + + event_start(state); + state->cur_event.time.seconds = sqlite3_column_int(sqlstmt, 1); + strcpy(state->cur_event.name, "gaschange"); + state->cur_event.gas.mix.o2.permille = 10 * sqlite3_column_int(sqlstmt, 4); + event_end(state); + + return 0; +} + +/* Callback function to parse seac dives. Reads headers_dive table to read dive + * information into divecomputer struct. + */ +static int seac_dive(void *param, int columns, char **data, char **column) +{ + UNUSED(columns); + UNUSED(column); + int retval = 0, cylnum = 0; + int year, month, day, hour, min, sec, tz; + char isodatetime[30]; + time_t divetime; + struct gasmix lastgas, curgas; + struct parser_state *state = (struct parser_state *)param; + sqlite3 *handle = state->sql_handle; + sqlite3_stmt *sqlstmt; + + const char *get_samples = "SELECT dive_number, runtime_s, depth_cm, temperature_mCx10, active_O2_fr, first_stop_depth_cm, first_stop_time_s, ndl_tts_s, cns, gf_l, gf_h FROM dive_data WHERE dive_number = ? ORDER BY runtime_s ASC"; + /* 0 = dive_number + * 1 = runtime_s + * 2 = depth_cm + * 3 = temperature_mCx10 - eg dC + * 4 = active_O2_fr + * 5 = first_stop_depth_cm + * 6 = first_stop_time_s + * 7 = ndl_tts_s + * 8 = cns + * 9 = gf-l + * 10 = gf-h + */ + + dive_start(state); + state->cur_dive->number = atoi(data[0]); + + // Create first cylinder + cylinder_t *curcyl = get_or_create_cylinder(state->cur_dive, 0); + + // Get time and date + sscanf(data[2], "%d/%d/%2d", &day, &month, &year); + sscanf(data[4], "%2d:%2d:%2d", &hour, &min, &sec); + year += 2000; + + tz = atoi(data[3]); + // Timezone offset lookup array + const char timezoneoffset[][7] = + {"+12:00", // 0 + "+11:00", // 1 + "+10:00", // 2 + "+09:30", // 3 + "+09:00", // 4 + "+08:00", // 5 + "+07:00", // 6 + "+06:00", // 7 + "+05:00", // 8 + "+04:30", // 9 + "+04:00", // 10 + "+03:30", // 11 + "+03:00", // 12 + "+02:00", // 13 + "+01:00", // 14 + "+00:00", // 15 + "-01:00", // 16 + "-02:00", // 17 + "-03:00", // 18 + "-03:30", // 19 + "-04:00", // 20 + "-04:30", // 21 + "-05:00", // 22 + "-05:30", // 23 + "-05:45", // 24 + "-06:00", // 25 + "-06:30", // 26 + "-07:00", // 27 + "-08:00", // 28 + "-08:45", // 29 + "-09:00", // 30 + "-09:30", // 31 + "-09:45", // 32 + "-10:00", // 33 + "-10:30", // 34 + "-11:00", // 35 + "-11:30", // 36 + "-12:00", // 37 + "-12:45", // 38 + "-13:00", // 39 + "-13:45", // 40 + "-14:00"}; // 41 + + sprintf(isodatetime, "%4i-%02i-%02iT%02i:%02i:%02i%6s", year, month, day, hour, min, sec, timezoneoffset[tz]); + divetime = get_dive_datetime_from_isostring(isodatetime); + state->cur_dive->when = divetime; + + // 6 = dive_type + // Dive type 2? + if (data[6]) { + switch (atoi(data[6])) { + case 1: + state->cur_dive->dc.divemode = OC; + break; + // Gauge Mode + case 2: + state->cur_dive->dc.divemode = UNDEF_COMP_TYPE; + break; + case 3: + state->cur_dive->dc.divemode = FREEDIVE; + break; + default: + if (verbose) { + fprintf(stderr, "Unknown divetype %i", atoi(data[6])); + } + } + } + + // 9 = comments from seac app + if (data[9]) { + utf8_string(data[9], &state->cur_dive->notes); + } + + // 10 = dive duration + if (data[10]) { + state->cur_dive->dc.duration.seconds = atoi(data[10]); + } + + // 8 = water_type + /* TODO: Seac only offers fresh / salt, and doesn't + * seem to record correctly currently. I have both + * fresh and saltwater dives and water type is reported + * as 150 for both. + */ + if (data[8]) { + switch (atoi(data[8])) { + case 150: + state->cur_dive->salinity = 0; + break; + case 100: + state->cur_dive->salinity = 1; + break; + default: + if (verbose) { + fprintf(stderr, "Unknown salinity %i", atoi(data[8])); + } + } + } + + + if (data[11]) { + state->cur_dive->dc.maxdepth.mm = 10 * atoi(data[11]); + } + + // Create sql_stmt type to query DB + retval = sqlite3_prepare_v2(handle, get_samples, -1, &sqlstmt, 0); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Preparing SQL object failed when getting SeacSync dives.\n"); + return 1; + } + + // Bind current dive number to sql statement + sqlite3_bind_int(sqlstmt, 1, state->cur_dive->number); + + // Catch a bad query + retval = sqlite3_step(sqlstmt); + if (retval == SQLITE_ERROR) { + fprintf(stderr, "%s", "Getting dive data from SeacSync DB failed.\n"); + return 1; + } + + settings_start(state); + dc_settings_start(state); + + utf8_string(data[1], &state->cur_dive->dc.serial); + utf8_string(data[12], &state->cur_dive->dc.fw_version); + state->cur_dive->dc.model = strdup("Seac Action"); + // TODO: Calculate device hash from string + state->cur_dive->dc.deviceid = 0xffffffff; + add_extra_data(&state->cur_dive->dc, "GF-Lo", (const char*)sqlite3_column_text(sqlstmt, 9)); + add_extra_data(&state->cur_dive->dc, "GF-Hi", (const char*)sqlite3_column_text(sqlstmt, 10)); + + dc_settings_end(state); + settings_end(state); + + if (data[11]) { + state->cur_dive->dc.maxdepth.mm = 10 * atoi(data[11]); + } + + curcyl->gasmix.o2.permille = 10 * sqlite3_column_int(sqlstmt, 4); + + + // Track gasses to tell when switch occurs + lastgas = curcyl->gasmix; + curgas = curcyl->gasmix; + + // Read samples + while (retval == SQLITE_ROW) { + sample_start(state); + state->cur_sample->time.seconds = sqlite3_column_int(sqlstmt, 1); + state->cur_sample->depth.mm = 10 * sqlite3_column_int(sqlstmt, 2); + state->cur_sample->temperature.mkelvin = cC_to_mkelvin(sqlite3_column_int(sqlstmt, 3)); + curgas.o2.permille = 10 * sqlite3_column_int(sqlstmt, 4); + if (!same_gasmix(lastgas, curgas)) { + seac_gaschange(state, sqlstmt); + lastgas = curgas; + cylnum ^= 1; // Only need to toggle between two cylinders + curcyl = get_or_create_cylinder(state->cur_dive, cylnum); + curcyl->gasmix.o2.permille = 10 * sqlite3_column_int(sqlstmt, 4); + } + state->cur_sample->stopdepth.mm = 10 * sqlite3_column_int(sqlstmt, 5); + state->cur_sample->stoptime.seconds = sqlite3_column_int(sqlstmt, 6); + state->cur_sample->ndl.seconds = sqlite3_column_int(sqlstmt, 7); + state->cur_sample->cns = sqlite3_column_int(sqlstmt, 8); + sample_end(state); + retval = sqlite3_step(sqlstmt); + } + + sqlite3_finalize(sqlstmt); + dive_end(state); + + return SQLITE_OK; +} + +/** Read SeacSync divesDB.db sqlite3 database into dive and samples. + * + * Each row returned in the query of headers_dive creates a new dive. + * The callback function performs another SQL query on the other + * table, to read in the sample values. + */ +int parse_seac_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites) +{ + UNUSED(buffer); + UNUSED(size); + + int retval; + char *err = NULL; + struct parser_state state; + + init_parser_state(&state); + state.target_table = table; + state.trips = trips; + state.sites = sites; + state.sql_handle = handle; + + const char *get_dives = "SELECT dive_number, device_sn, date, timezone, time, elapsed_surface_time, dive_type, start_mode, water_type, comment, total_dive_time, max_depth, firmware_version FROM headers_dive"; + /* 0 = dive_number + * 1 = device_sn + * 2 = date + * 3 = timezone + * 4 = time + * 5 = elapsed_surface_time + * 6 = dive_type + * 7 = start_mode + * 8 = water_type + * 9 = comment + * 10 = total_dive_time + * 11 = max_depth + * 12 = firmware version + */ + + retval = sqlite3_exec(handle, get_dives, &seac_dive, &state, &err); + free_parser_state(&state); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +}
\ No newline at end of file diff --git a/core/parse.h b/core/parse.h index 8886bcd8d..350b98ed8 100644 --- a/core/parse.h +++ b/core/parse.h @@ -121,6 +121,7 @@ int atoi_n(char *ptr, unsigned int len); int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); +int parse_seac_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); int parse_shearwater_cloud_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table, struct trip_table *trips, struct dive_site_table *sites); diff --git a/core/qthelper.cpp b/core/qthelper.cpp index 0de3e6002..203234f05 100644 --- a/core/qthelper.cpp +++ b/core/qthelper.cpp @@ -5,7 +5,6 @@ #include "core/settings/qPrefUpdateManager.h" #include "core/subsurface-qt/divelistnotifier.h" #include "subsurface-string.h" -#include "subsurface-string.h" #include "gettextfromc.h" #include "statistics.h" #include "membuffer.h" @@ -955,6 +954,12 @@ QString get_dive_date_string(timestamp_t when) return loc.toString(ts.toUTC(), QString(prefs.date_format) + " " + prefs.time_format); } +// Get local seconds since Epoch from ISO formatted UTC date time + offset string +extern "C" time_t get_dive_datetime_from_isostring(char *when) { + QDateTime divetime = QDateTime::fromString(when, Qt::ISODate); + return (time_t)(divetime.toSecsSinceEpoch()); +} + QString get_short_dive_date_string(timestamp_t when) { QDateTime ts; diff --git a/core/qthelper.h b/core/qthelper.h index a18d6d04d..a3394e684 100644 --- a/core/qthelper.h +++ b/core/qthelper.h @@ -5,6 +5,7 @@ #include <libxslt/transform.h> #include <libxslt/xsltutils.h> #include "core/pref.h" +#include "subsurface-time.h" struct picture; @@ -147,6 +148,7 @@ const char *subsurface_user_agent(); enum deco_mode decoMode(); int parse_seabear_header(const char *filename, char **params, int pnr); char *get_current_date(); +time_t get_dive_datetime_from_isostring(char *when); void print_qt_versions(); void lock_planner(); void unlock_planner(); diff --git a/core/units.h b/core/units.h index 1b042513b..0ce97e21c 100644 --- a/core/units.h +++ b/core/units.h @@ -230,6 +230,11 @@ static inline unsigned long C_to_mkelvin(double c) return lrint(c * 1000 + ZERO_C_IN_MKELVIN); } +static inline unsigned long cC_to_mkelvin(double c) +{ + return lrint(c * 10 + ZERO_C_IN_MKELVIN); +} + static inline double psi_to_bar(double psi) { return psi / 14.5037738; diff --git a/dives/TestDiveSeacSync.db b/dives/TestDiveSeacSync.db Binary files differnew file mode 100644 index 000000000..a0a68246a --- /dev/null +++ b/dives/TestDiveSeacSync.db diff --git a/packaging/ios/Subsurface-mobile.pro b/packaging/ios/Subsurface-mobile.pro index 36da3e3af..1281d0377 100644 --- a/packaging/ios/Subsurface-mobile.pro +++ b/packaging/ios/Subsurface-mobile.pro @@ -67,6 +67,7 @@ SOURCES += ../../subsurface-mobile-main.cpp \ ../../core/pictureobj.cpp \ ../../core/import-suunto.c \ ../../core/import-shearwater.c \ + ../../core/import-seac.c \ ../../core/import-cobalt.c \ ../../core/import-divinglog.c \ ../../core/import-csv.c \ |