summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--Documentation/user-manual.txt15
-rw-r--r--core/CMakeLists.txt1
-rw-r--r--core/file.c10
-rw-r--r--core/import-seac.c303
-rw-r--r--core/parse.h1
-rw-r--r--core/qthelper.cpp7
-rw-r--r--core/qthelper.h2
-rw-r--r--core/units.h5
-rw-r--r--dives/TestDiveSeacSync.dbbin0 -> 73728 bytes
-rw-r--r--packaging/ios/Subsurface-mobile.pro1
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
new file mode 100644
index 000000000..a0a68246a
--- /dev/null
+++ b/dives/TestDiveSeacSync.db
Binary files differ
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 \