diff options
author | Dirk Hohndel <dirk@hohndel.org> | 2016-04-04 22:02:03 -0700 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2016-04-04 22:33:58 -0700 |
commit | 7be962bfc2879a72c32ff67518731347dcdff6de (patch) | |
tree | d05bf7ab234a448ee37a15b608e2b939f2285d07 /core | |
parent | 2d760a7bff71c46c5aeba37c40d236ea16eefea2 (diff) | |
download | subsurface-7be962bfc2879a72c32ff67518731347dcdff6de.tar.gz |
Move subsurface-core to core and qt-mobile to mobile-widgets
Having subsurface-core as a directory name really messes with
autocomplete and is obviously redundant. Simmilarly, qt-mobile caused an
autocomplete conflict and also was inconsistent with the desktop-widget
name for the directory containing the "other" UI.
And while cleaning up the resulting change in the path name for include
files, I decided to clean up those even more to make them consistent
overall.
This could have been handled in more commits, but since this requires a
make clean before the build, it seemed more sensible to do it all in one.
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'core')
118 files changed, 42338 insertions, 0 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 000000000..d9b1d3421 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,98 @@ +set(PLATFORM_SRC unknown_platform.c) +message(STATUS "system name ${CMAKE_SYSTEM_NAME}") +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(ANDROID) + set(PLATFORM_SRC android.cpp) + else() + set(PLATFORM_SRC linux.c) + endif() +elseif(CMAKE_SYSTEM_NAME STREQUAL "Android") + set(PLATFORM_SRC android.cpp) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(PLATFORM_SRC macos.c) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(PLATFORM_SRC windows.c) +endif() + +if(FTDISUPPORT) + set(SERIAL_FTDI serial_ftdi.c) +endif() + +if(BTSUPPORT) + add_definitions(-DBT_SUPPORT) + set(BT_SRC_FILES desktop-widgets/btdeviceselectiondialog.cpp) + set(BT_CORE_SRC_FILES qtserialbluetooth.cpp) +endif() + +# compile the core library, in C. +set(SUBSURFACE_CORE_LIB_SRCS + cochran.c + datatrak.c + deco.c + device.c + dive.c + divesite.c + divesite.cpp + divelist.c + equipment.c + file.c + gas-model.c + git-access.c + libdivecomputer.c + liquivision.c + load-git.c + membuffer.c + ostctools.c + parse-xml.c + planner.c + profile.c + gaspressures.c + worldmap-save.c + save-git.c + save-xml.c + save-html.c + sha1.c + statistics.c + strtod.c + subsurfacestartup.c + time.c + uemis.c + uemis-downloader.c + version.c + # gettextfrommoc should be added because we are using it on the c-code. + gettextfromc.cpp + # dirk ported some core functionality to c++. + qthelper.cpp + divecomputer.cpp + exif.cpp + subsurfacesysinfo.cpp + devicedetails.cpp + configuredivecomputer.cpp + configuredivecomputerthreads.cpp + divesitehelpers.cpp + taxonomy.c + checkcloudconnection.cpp + windowtitleupdate.cpp + divelogexportlogic.cpp + qt-init.cpp + qtserialbluetooth.cpp + metrics.cpp + color.cpp + pluginmanager.cpp + imagedownloader.cpp + isocialnetworkintegration.cpp + gpslocation.cpp + cloudstorage.cpp + + #Subsurface Qt have the Subsurface structs QObjectified for easy access via QML. + subsurface-qt/DiveObjectHelper.cpp + subsurface-qt/SettingsObjectWrapper.cpp + ${SERIAL_FTDI} + ${PLATFORM_SRC} + ${BT_CORE_SRC_FILES} +) +source_group("Subsurface Core" FILES ${SUBSURFACE_CORE_LIB_SRCS}) + +add_library(subsurface_corelib STATIC ${SUBSURFACE_CORE_LIB_SRCS} ) +target_link_libraries(subsurface_corelib ${QT_LIBRARIES}) + diff --git a/core/android.cpp b/core/android.cpp new file mode 100644 index 000000000..3631b07a1 --- /dev/null +++ b/core/android.cpp @@ -0,0 +1,199 @@ +/* implements Android specific functions */ +#include "dive.h" +#include "display.h" +#include <string.h> +#include <sys/types.h> +#include <dirent.h> +#include <fcntl.h> +#include <libusb.h> +#include <errno.h> + +#include <QtAndroidExtras/QtAndroidExtras> +#include <QtAndroidExtras/QAndroidJniObject> +#include <QtAndroid> + +#define FTDI_VID 0x0403 +#define USB_SERVICE "usb" + +extern "C" { + +const char android_system_divelist_default_font[] = "Roboto"; +const char *system_divelist_default_font = android_system_divelist_default_font; +double system_divelist_default_font_size = -1; + +int get_usb_fd(uint16_t idVendor, uint16_t idProduct); +void subsurface_OS_pref_setup(void) +{ + // Abusing this function to get a decent place where we can wire in + // our open callback into libusb +#ifdef libusb_android_open_callback_func + libusb_set_android_open_callback(get_usb_fd); +#elif __ANDROID__ +#error we need libusb_android_open_callback +#endif +} + +bool subsurface_ignore_font(const char *font) +{ + // there are no old default fonts that we would want to ignore + return false; +} + +void subsurface_user_info(struct user_info *user) +{ /* Encourage use of at least libgit2-0.20 */ } + +static const char *system_default_path_append(const char *append) +{ + // Qt appears to find a working path for us - let's just go with that + QString path = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); + + if (append) + path += QString("/%1").arg(append); + + return strdup(path.toUtf8().data()); +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static const char *filename = "subsurface.xml"; + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + /* FIXME: we need to enumerate in some other way on android */ + /* qtserialport maybee? */ + return -1; +} + +/** + * Get the file descriptor of first available matching device attached to usb in android. + * + * returns a fd to the device, or -1 and errno is set. + */ +int get_usb_fd(uint16_t idVendor, uint16_t idProduct) +{ + int i; + jint fd, vendorid, productid; + QAndroidJniObject usbName, usbDevice; + + // Get the current main activity of the application. + QAndroidJniObject activity = QtAndroid::androidActivity(); + + QAndroidJniObject usb_service = QAndroidJniObject::fromString(USB_SERVICE); + + // Get UsbManager from activity + QAndroidJniObject usbManager = activity.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", usb_service.object()); + + // Get a HashMap<Name, UsbDevice> of all USB devices attached to Android + QAndroidJniObject deviceMap = usbManager.callObjectMethod("getDeviceList", "()Ljava/util/HashMap;"); + jint num_devices = deviceMap.callMethod<jint>("size", "()I"); + if (num_devices == 0) { + // No USB device is attached. + return -1; + } + + // Iterate over all the devices and find the first available FTDI device. + QAndroidJniObject keySet = deviceMap.callObjectMethod("keySet", "()Ljava/util/Set;"); + QAndroidJniObject iterator = keySet.callObjectMethod("iterator", "()Ljava/util/Iterator;"); + + for (i = 0; i < num_devices; i++) { + usbName = iterator.callObjectMethod("next", "()Ljava/lang/Object;"); + usbDevice = deviceMap.callObjectMethod ("get", "(Ljava/lang/Object;)Ljava/lang/Object;", usbName.object()); + vendorid = usbDevice.callMethod<jint>("getVendorId", "()I"); + productid = usbDevice.callMethod<jint>("getProductId", "()I"); + if(vendorid == idVendor && productid == idProduct) // Found the requested device + break; + } + if (i == num_devices) { + // No device found. + errno = ENOENT; + return -1; + } + + jboolean hasPermission = usbManager.callMethod<jboolean>("hasPermission", "(Landroid/hardware/usb/UsbDevice;)Z", usbDevice.object()); + if (!hasPermission) { + // You do not have permission to use the usbDevice. + // Please remove and reinsert the USB device. + // Could also give an dialogbox asking for permission. + errno = EPERM; + return -1; + } + + // An device is present and we also have permission to use the device. + // Open the device and get its file descriptor. + QAndroidJniObject usbDeviceConnection = usbManager.callObjectMethod("openDevice", "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/UsbDeviceConnection;", usbDevice.object()); + if (usbDeviceConnection.object() == NULL) { + // Some error occurred while opening the device. Exit. + errno = EINVAL; + return -1; + } + + // Finally get the required file descriptor. + fd = usbDeviceConnection.callMethod<jint>("getFileDescriptor", "()I"); + if (fd == -1) { + // The device is not opened. Some error. + errno = ENODEV; + return -1; + } + return fd; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} +} diff --git a/core/checkcloudconnection.cpp b/core/checkcloudconnection.cpp new file mode 100644 index 000000000..f29d971ba --- /dev/null +++ b/core/checkcloudconnection.cpp @@ -0,0 +1,106 @@ +#include <QObject> +#include <QTimer> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QEventLoop> + +#include "pref.h" +#include "helpers.h" +#include "git-access.h" + +#include "checkcloudconnection.h" + + +CheckCloudConnection::CheckCloudConnection(QObject *parent) : + QObject(parent), + reply(0) +{ + +} + +#define TEAPOT "/make-latte?number-of-shots=3" +#define HTTP_I_AM_A_TEAPOT 418 +#define MILK "Linus does not like non-fat milk" +bool CheckCloudConnection::checkServer() +{ + if (verbose) + fprintf(stderr, "Checking cloud connection...\n"); + + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + QNetworkRequest request; + request.setRawHeader("Accept", "text/plain"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Client-Id", getUUID().toUtf8()); + request.setUrl(QString(prefs.cloud_base_url) + TEAPOT); + QNetworkAccessManager *mgr = new QNetworkAccessManager(); + reply = mgr->get(request); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::sslErrors, this, &CheckCloudConnection::sslErrors); + for (int seconds = 1; seconds <= 5; seconds++) { + timer.start(1000); // wait five seconds + loop.exec(); + if (timer.isActive()) { + // didn't time out, did we get the right response? + timer.stop(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == HTTP_I_AM_A_TEAPOT && + reply->readAll() == QByteArray(MILK)) { + reply->deleteLater(); + mgr->deleteLater(); + if (verbose > 1) + qWarning() << "Cloud storage: successfully checked connection to cloud server"; + git_storage_update_progress(last_git_storage_update_val + 1, "successfully checked cloud connection"); + return true; + } + } else if (seconds < 5) { + git_storage_update_progress(last_git_storage_update_val + 1, "waited 1 sec for cloud connection"); + } else { + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + } + git_storage_update_progress(last_git_storage_update_val + 1, "cloud connection failed"); + if (verbose) + qDebug() << "connection test to cloud server failed" << + reply->error() << reply->errorString() << + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << + reply->readAll(); + reply->deleteLater(); + mgr->deleteLater(); + if (verbose) + qWarning() << "Cloud storage: unable to connect to cloud server"; + return false; +} + +void CheckCloudConnection::sslErrors(QList<QSslError> errorList) +{ + if (verbose) { + qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; + Q_FOREACH (QSslError err, errorList) { + qDebug() << err.errorString(); + } + } + QSslConfiguration conf = reply->sslConfiguration(); + QSslCertificate cert = conf.peerCertificate(); + QByteArray hexDigest = cert.digest().toHex(); + if (reply->url().toString().contains(prefs.cloud_base_url) && + hexDigest == "13ff44c62996cfa5cd69d6810675490e") { + if (verbose) + qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; + reply->ignoreSslErrors(); + } else { + if (verbose) + qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; + } +} + +// helper to be used from C code +extern "C" bool canReachCloudServer() +{ + if (verbose) + qWarning() << "Cloud storage: checking connection to cloud server"; + CheckCloudConnection *checker = new CheckCloudConnection; + return checker->checkServer(); +} diff --git a/core/checkcloudconnection.h b/core/checkcloudconnection.h new file mode 100644 index 000000000..58a412797 --- /dev/null +++ b/core/checkcloudconnection.h @@ -0,0 +1,22 @@ +#ifndef CHECKCLOUDCONNECTION_H +#define CHECKCLOUDCONNECTION_H + +#include <QObject> +#include <QNetworkReply> +#include <QSsl> + +#include "checkcloudconnection.h" + +class CheckCloudConnection : public QObject { + Q_OBJECT +public: + CheckCloudConnection(QObject *parent = 0); + bool checkServer(); +private: + QNetworkReply *reply; +private +slots: + void sslErrors(QList<QSslError> errorList); +}; + +#endif // CHECKCLOUDCONNECTION_H diff --git a/core/cloudstorage.cpp b/core/cloudstorage.cpp new file mode 100644 index 000000000..575191891 --- /dev/null +++ b/core/cloudstorage.cpp @@ -0,0 +1,109 @@ +#include "cloudstorage.h" +#include "pref.h" +#include "dive.h" +#include "helpers.h" + +#include <QApplication> + +CloudStorageAuthenticate::CloudStorageAuthenticate(QObject *parent) : + QObject(parent), + reply(NULL) +{ + userAgent = getUserAgent(); +} + +#define CLOUDURL QString(prefs.cloud_base_url) +#define CLOUDBACKENDSTORAGE CLOUDURL + "/storage" +#define CLOUDBACKENDVERIFY CLOUDURL + "/verify" +#define CLOUDBACKENDUPDATE CLOUDURL + "/update" + +QNetworkReply* CloudStorageAuthenticate::backend(const QString& email,const QString& password,const QString& pin,const QString& newpasswd) +{ + QString payload(email + QChar(' ') + password); + QUrl requestUrl; + if (pin.isEmpty() && newpasswd.isEmpty()) { + requestUrl = QUrl(CLOUDBACKENDSTORAGE); + } else if (!newpasswd.isEmpty()) { + requestUrl = QUrl(CLOUDBACKENDUPDATE); + payload += QChar(' ') + newpasswd; + } else { + requestUrl = QUrl(CLOUDBACKENDVERIFY); + payload += QChar(' ') + pin; + } + QNetworkRequest *request = new QNetworkRequest(requestUrl); + request->setRawHeader("Accept", "text/xml, text/plain"); + request->setRawHeader("User-Agent", userAgent.toUtf8()); + request->setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + reply = manager()->post(*request, qPrintable(payload)); + connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); + connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrors(QList<QSslError>))); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(uploadError(QNetworkReply::NetworkError))); + return reply; +} + +void CloudStorageAuthenticate::uploadFinished() +{ + static QString myLastError; + + QString cloudAuthReply(reply->readAll()); + qDebug() << "Completed connection with cloud storage backend, response" << cloudAuthReply; + if (cloudAuthReply == QLatin1String("[VERIFIED]") || cloudAuthReply == QLatin1String("[OK]")) { + prefs.cloud_verification_status = CS_VERIFIED; + /* TODO: Move this to a correct place + NotificationWidget *nw = MainWindow::instance()->getNotificationWidget(); + if (nw->getNotificationText() == myLastError) + nw->hideNotification(); + */ + myLastError.clear(); + } else if (cloudAuthReply == QLatin1String("[VERIFY]")) { + prefs.cloud_verification_status = CS_NEED_TO_VERIFY; + } else if (cloudAuthReply == QLatin1String("[PASSWDCHANGED]")) { + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = prefs.cloud_storage_newpassword; + prefs.cloud_storage_newpassword = NULL; + emit passwordChangeSuccessful(); + return; + } else { + prefs.cloud_verification_status = CS_INCORRECT_USER_PASSWD; + myLastError = cloudAuthReply; + report_error("%s", qPrintable(cloudAuthReply)); + /* TODO: Emit a signal with the error + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + */ + } + emit finishedAuthenticate(); +} + +void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError) +{ + qDebug() << "Received error response from cloud storage backend:" << reply->errorString(); +} + +void CloudStorageAuthenticate::sslErrors(QList<QSslError> errorList) +{ + if (verbose) { + qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; + Q_FOREACH (QSslError err, errorList) { + qDebug() << err.errorString(); + } + } + QSslConfiguration conf = reply->sslConfiguration(); + QSslCertificate cert = conf.peerCertificate(); + QByteArray hexDigest = cert.digest().toHex(); + if (reply->url().toString().contains(prefs.cloud_base_url) && + hexDigest == "13ff44c62996cfa5cd69d6810675490e") { + if (verbose) + qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; + reply->ignoreSslErrors(); + } else { + if (verbose) + qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; + } +} + +QNetworkAccessManager *manager() +{ + static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + return manager; +} diff --git a/core/cloudstorage.h b/core/cloudstorage.h new file mode 100644 index 000000000..6addb739d --- /dev/null +++ b/core/cloudstorage.h @@ -0,0 +1,27 @@ +#ifndef CLOUD_STORAGE_H +#define CLOUD_STORAGE_H + +#include <QObject> +#include <QNetworkReply> + +class CloudStorageAuthenticate : public QObject { + Q_OBJECT +public: + QNetworkReply* backend(const QString& email,const QString& password,const QString& pin = QString(),const QString& newpasswd = QString()); + explicit CloudStorageAuthenticate(QObject *parent); +signals: + void finishedAuthenticate(); + void passwordChangeSuccessful(); +private +slots: + void uploadError(QNetworkReply::NetworkError error); + void sslErrors(QList<QSslError> errorList); + void uploadFinished(); +private: + QNetworkReply *reply; + QString userAgent; + bool verbose; +}; + +QNetworkAccessManager *manager(); +#endif
\ No newline at end of file diff --git a/core/cochran.c b/core/cochran.c new file mode 100644 index 000000000..b42ed8233 --- /dev/null +++ b/core/cochran.c @@ -0,0 +1,809 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include "dive.h" +#include "file.h" +#include "units.h" +#include "gettext.h" +#include "cochran.h" +#include "divelist.h" + +#include <libdivecomputer/parser.h> + +#define POUND 0.45359237 +#define FEET 0.3048 +#define INCH 0.0254 +#define GRAVITY 9.80665 +#define ATM 101325.0 +#define BAR 100000.0 +#define FSW (ATM / 33.0) +#define MSW (BAR / 10.0) +#define PSI ((POUND * GRAVITY) / (INCH * INCH)) + +// Some say 0x4a14 and 0x4b14 are the right number for this offset +// This works with CAN files from Analyst 4.01v and computers +// such as Commander, Gemini, EMC-16, and EMC-20H +#define LOG_ENTRY_OFFSET 0x4914 + +enum cochran_type { + TYPE_GEMINI, + TYPE_COMMANDER, + TYPE_EMC +}; + +struct config { + enum cochran_type type; + unsigned int logbook_size; + unsigned int sample_size; +} config; + + +// Convert 4 bytes into an INT +#define array_uint16_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) ) +#define array_uint32_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) + ((p)[2]<<16) \ + + ((p)[3]<<24)) + +/* + * The Cochran file format is designed to be annoying to read. It's roughly: + * + * 0x00000: room for 65534 4-byte words, giving the starting offsets + * of the dives themselves. + * + * 0x3fff8: the size of the file + 1 + * 0x3ffff: 0 (high 32 bits of filesize? Bogus: the offsets into the file + * are 32-bit, so it can't be a large file anyway) + * + * 0x40000: byte 0x46 + * 0x40001: "block 0": 256 byte encryption key + * 0x40101: the random modulus, or length of the key to use + * 0x40102: block 1: Version and date of Analyst and a feature string identifying + * the computer features and the features of the file + * 0x40138: Computer configuration page 1, 512 bytes + * 0x40338: Computer configuration page 2, 512 bytes + * 0x40538: Misc data (tissues) 1500 bytes + * 0x40b14: Ownership data 512 bytes ??? + * + * 0x4171c: Ownership data 512 bytes ??? <copy> + * + * 0x45415: Time stamp 17 bytes + * 0x45426: Computer configuration page 1, 512 bytes <copy> + * 0x45626: Computer configuration page 2, 512 bytes <copy> + * + */ +static unsigned int partial_decode(unsigned int start, unsigned int end, + const unsigned char *decode, unsigned offset, unsigned mod, + const unsigned char *buf, unsigned int size, unsigned char *dst) +{ + unsigned i, sum = 0; + + for (i = start; i < end; i++) { + unsigned char d = decode[offset++]; + if (i >= size) + break; + if (offset == mod) + offset = 0; + d += buf[i]; + if (dst) + dst[i] = d; + sum += d; + } + return sum; +} + +#ifdef COCHRAN_DEBUG + +#define hexchar(n) ("0123456789abcdef"[(n) & 15]) + +static int show_line(unsigned offset, const unsigned char *data, + unsigned size, int show_empty) +{ + unsigned char bits; + int i, off; + char buffer[120]; + + if (size > 16) + size = 16; + + bits = 0; + memset(buffer, ' ', sizeof(buffer)); + off = sprintf(buffer, "%06x ", offset); + for (i = 0; i < size; i++) { + char *hex = buffer + off + 3 * i; + char *asc = buffer + off + 50 + i; + unsigned char byte = data[i]; + + hex[0] = hexchar(byte >> 4); + hex[1] = hexchar(byte); + bits |= byte; + if (byte < 32 || byte > 126) + byte = '.'; + asc[0] = byte; + asc[1] = 0; + } + + if (bits) { + puts(buffer); + return 1; + } + if (show_empty) + puts("..."); + return 0; +} + +static void cochran_debug_write(const unsigned char *data, unsigned size) +{ + return; + + int show = 1, i; + for (i = 0; i < size; i += 16) + show = show_line(i, data + i, size - i, show); +} + +static void cochran_debug_sample(const char *s, unsigned int seconds) +{ + switch (config.type) { + case TYPE_GEMINI: + switch (seconds % 4) { + case 0: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 1: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 2: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 3: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + } + break; + case TYPE_COMMANDER: + switch (seconds % 2) { + case 0: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 1: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + } + break; + case TYPE_EMC: + switch (seconds % 2) { + case 0: + printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); + break; + case 1: + printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); + break; + } + break; + } + + printf ("%02dh %02dm %02ds: Depth: %-5.2f, ", seconds / 3660, + (seconds % 3660) / 60, seconds % 60, depth); +} + +#endif // COCHRAN_DEBUG + +static void cochran_parse_header(const unsigned char *decode, unsigned mod, + const unsigned char *in, unsigned size) +{ + unsigned char *buf = malloc(size); + + /* Do the "null decode" using a one-byte decode array of '\0' */ + /* Copies in plaintext, will be overwritten later */ + partial_decode(0, 0x0102, (const unsigned char *)"", 0, 1, in, size, buf); + + /* + * The header scrambling is different form the dive + * scrambling. Oh yay! + */ + partial_decode(0x0102, 0x010e, decode, 0, mod, in, size, buf); + partial_decode(0x010e, 0x0b14, decode, 0, mod, in, size, buf); + partial_decode(0x0b14, 0x1b14, decode, 0, mod, in, size, buf); + partial_decode(0x1b14, 0x2b14, decode, 0, mod, in, size, buf); + partial_decode(0x2b14, 0x3b14, decode, 0, mod, in, size, buf); + partial_decode(0x3b14, 0x5414, decode, 0, mod, in, size, buf); + partial_decode(0x5414, size, decode, 0, mod, in, size, buf); + + // Detect log type + switch (buf[0x133]) { + case '2': // Cochran Commander, version II log format + config.logbook_size = 256; + if (buf[0x132] == 0x10) { + config.type = TYPE_GEMINI; + config.sample_size = 2; // Gemini with tank PSI samples + } else { + config.type = TYPE_COMMANDER; + config.sample_size = 2; // Commander + } + break; + case '3': // Cochran EMC, version III log format + config.type = TYPE_EMC; + config.logbook_size = 512; + config.sample_size = 3; + break; + default: + printf ("Unknown log format v%c\n", buf[0x137]); + free(buf); + exit(1); + break; + } + +#ifdef COCHRAN_DEBUG + puts("Header\n======\n\n"); + cochran_debug_write(buf, size); +#endif + + free(buf); +} + +/* +* Bytes expected after a pre-dive event code +*/ +static int cochran_predive_event_bytes(unsigned char code) +{ + int x = 0; + int gem_event_bytes[15][2] = {{0x00, 10}, {0x02, 17}, {0x08, 18}, + {0x09, 18}, {0x0c, 18}, {0x0d, 18}, + {0x0e, 18}, + {-1, 0}}; + int cmdr_event_bytes[15][2] = {{0x00, 16}, {0x01, 20}, {0x02, 17}, + {0x03, 16}, {0x06, 18}, {0x07, 18}, + {0x08, 18}, {0x09, 18}, {0x0a, 18}, + {0x0b, 20}, {0x0c, 18}, {0x0d, 18}, + {0x0e, 18}, {0x10, 20}, + {-1, 0}}; + int emc_event_bytes[15][2] = {{0x00, 18}, {0x01, 22}, {0x02, 19}, + {0x03, 18}, {0x06, 20}, {0x07, 20}, + {0x0a, 20}, {0x0b, 20}, {0x0f, 18}, + {0x10, 20}, + {-1, 0}}; + + switch (config.type) { + case TYPE_GEMINI: + while (gem_event_bytes[x][0] != code && gem_event_bytes[x][0] != -1) + x++; + return gem_event_bytes[x][1]; + break; + case TYPE_COMMANDER: + while (cmdr_event_bytes[x][0] != code && cmdr_event_bytes[x][0] != -1) + x++; + return cmdr_event_bytes[x][1]; + break; + case TYPE_EMC: + while (emc_event_bytes[x][0] != code && emc_event_bytes[x][0] != -1) + x++; + return emc_event_bytes[x][1]; + break; + } + + return 0; +} + +int cochran_dive_event_bytes(unsigned char event) +{ + return (event == 0xAD || event == 0xAB) ? 4 : 0; +} + +static void cochran_dive_event(struct divecomputer *dc, const unsigned char *s, + unsigned int seconds, unsigned int *in_deco, + unsigned int *deco_ceiling, unsigned int *deco_time) +{ + switch (s[0]) { + case 0xC5: // Deco obligation begins + *in_deco = 1; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xDB: // Deco obligation ends + *in_deco = 0; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xAD: // Raise deco ceiling 10 ft + *deco_ceiling -= 10; // ft + *deco_time = (array_uint16_le(s + 3) + 1) * 60; + break; + case 0xAB: // Lower deco ceiling 10 ft + *deco_ceiling += 10; // ft + *deco_time = (array_uint16_le(s + 3) + 1) * 60; + break; + case 0xA8: // Entered Post Dive interval mode (surfaced) + break; + case 0xA9: // Exited PDI mode (re-submierged) + break; + case 0xBD: // Switched to normal PO2 setting + break; + case 0xC0: // Switched to FO2 21% mode (generally upon surface) + break; + case 0xC1: // "Ascent rate alarm + add_event(dc, seconds, SAMPLE_EVENT_ASCENT, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 0xC2: // Low battery warning +#ifdef SAMPLE_EVENT_BATTERY + add_event(dc, seconds, SAMPLE_EVENT_BATTERY, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "battery")); +#endif + break; + case 0xC3: // CNS warning + add_event(dc, seconds, SAMPLE_EVENT_OLF, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "OLF")); + break; + case 0xC4: // Depth alarm begin + add_event(dc, seconds, SAMPLE_EVENT_MAXDEPTH, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "maxdepth")); + break; + case 0xC8: // PPO2 alarm begin + add_event(dc, seconds, SAMPLE_EVENT_PO2, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); + break; + case 0xCC: // Low cylinder 1 pressure"; + break; + case 0xCD: // Switch to deco blend setting + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xCE: // NDL alarm begin + add_event(dc, seconds, SAMPLE_EVENT_RBT, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "rbt")); + break; + case 0xD0: // Breathing rate alarm begin + break; + case 0xD3: // Low gas 1 flow rate alarm begin"; + break; + case 0xD6: // Ceiling alarm begin + add_event(dc, seconds, SAMPLE_EVENT_CEILING, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); + break; + case 0xD8: // End decompression mode + *in_deco = 0; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xE1: // Ascent alarm end + add_event(dc, seconds, SAMPLE_EVENT_ASCENT, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 0xE2: // Low transmitter battery alarm + add_event(dc, seconds, SAMPLE_EVENT_TRANSMITTER, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "transmitter")); + break; + case 0xE3: // Switch to FO2 mode + break; + case 0xE5: // Switched to PO2 mode + break; + case 0xE8: // PO2 too low alarm + add_event(dc, seconds, SAMPLE_EVENT_PO2, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); + break; + case 0xEE: // NDL alarm end + add_event(dc, seconds, SAMPLE_EVENT_RBT, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "rbt")); + break; + case 0xEF: // Switch to blend 2 + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xF0: // Breathing rate alarm end + break; + case 0xF3: // Switch to blend 1 (often at dive start) + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xF6: // Ceiling alarm end + add_event(dc, seconds, SAMPLE_EVENT_CEILING, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); + break; + default: + break; + } +} + +/* +* Parse sample data, extract events and build a dive +*/ +static void cochran_parse_samples(struct dive *dive, const unsigned char *log, + const unsigned char *samples, unsigned int size, + unsigned int *duration, double *max_depth, + double *avg_depth, double *min_temp) +{ + const unsigned char *s; + unsigned int offset = 0, seconds = 0; + double depth = 0, temp = 0, depth_sample = 0, psi = 0, sgc_rate = 0; + int ascent_rate = 0; + unsigned int ndl = 0; + unsigned int in_deco = 0, deco_ceiling = 0, deco_time = 0; + + struct divecomputer *dc = &dive->dc; + struct sample *sample; + + // Initialize stat variables + *max_depth = 0, *avg_depth = 0, *min_temp = 0xFF; + + // Get starting depth and temp (tank PSI???) + switch (config.type) { + case TYPE_GEMINI: + depth = (float) (log[CMD_START_DEPTH] + + log[CMD_START_DEPTH + 1] * 256) / 4; + temp = log[CMD_START_TEMP]; + psi = log[CMD_START_PSI] + log[CMD_START_PSI + 1] * 256; + sgc_rate = (float)(log[CMD_START_SGC] + + log[CMD_START_SGC + 1] * 256) / 2; + break; + case TYPE_COMMANDER: + depth = (float) (log[CMD_START_DEPTH] + + log[CMD_START_DEPTH + 1] * 256) / 4; + temp = log[CMD_START_TEMP]; + break; + + case TYPE_EMC: + depth = (float) log [EMC_START_DEPTH] / 256 + + log[EMC_START_DEPTH + 1]; + temp = log[EMC_START_TEMP]; + break; + } + + // Skip past pre-dive events + unsigned int x = 0; + if (samples[x] != 0x40) { + unsigned int c; + while ((samples[x] & 0x80) == 0 && samples[x] != 0x40 && x < size) { + c = cochran_predive_event_bytes(samples[x]) + 1; +#ifdef COCHRAN_DEBUG + printf("Predive event: ", samples[x]); + for (int y = 0; y < c; y++) printf("%02x ", samples[x + y]); + putchar('\n'); +#endif + x += c; + } + } + + // Now process samples + offset = x; + while (offset < size) { + s = samples + offset; + + // Start with an empty sample + sample = prepare_sample(dc); + sample->time.seconds = seconds; + + // Check for event + if (s[0] & 0x80) { + cochran_dive_event(dc, s, seconds, &in_deco, &deco_ceiling, &deco_time); + offset += cochran_dive_event_bytes(s[0]) + 1; + continue; + } + + // Depth is in every sample + depth_sample = (float)(s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1); + depth += depth_sample; + +#ifdef COCHRAN_DEBUG + cochran_debug_sample(s, seconds); +#endif + + switch (config.type) { + case TYPE_COMMANDER: + switch (seconds % 2) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); + break; + case 1: // Temperature + temp = s[1] / 2 + 20; + break; + } + break; + case TYPE_GEMINI: + // Gemini with tank pressure and SAC rate. + switch (seconds % 4) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1); + break; + case 2: // PSI change + psi -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 4; + break; + case 1: // SGC rate + sgc_rate -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 2; + break; + case 3: // Temperature + temp = (float)s[1] / 2 + 20; + break; + } + break; + case TYPE_EMC: + switch (seconds % 2) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); + break; + case 1: // Temperature + temp = (float)s[1] / 2 + 20; + break; + } + // Get NDL and deco information + switch (seconds % 24) { + case 20: + if (in_deco) { + // Fist stop time + //first_deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds + ndl = 0; + } else { + // NDL + ndl = (s[2] + s[5] * 256 + 1) * 60; // seconds + deco_time = 0; + } + break; + case 22: + if (in_deco) { + // Total stop time + deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds + ndl = 0; + } + break; + } + } + + // Track dive stats + if (depth > *max_depth) *max_depth = depth; + if (temp < *min_temp) *min_temp = temp; + *avg_depth = (*avg_depth * seconds + depth) / (seconds + 1); + + sample->depth.mm = depth * FEET * 1000; + sample->ndl.seconds = ndl; + sample->in_deco = in_deco; + sample->stoptime.seconds = deco_time; + sample->stopdepth.mm = deco_ceiling * FEET * 1000; + sample->temperature.mkelvin = C_to_mkelvin((temp - 32) / 1.8); + sample->sensor = 0; + sample->cylinderpressure.mbar = psi * PSI / 100; + + finish_sample(dc); + + offset += config.sample_size; + seconds++; + } + (void)ascent_rate; // mark the variable as unused + + if (seconds > 0) + *duration = seconds - 1; +} + +static void cochran_parse_dive(const unsigned char *decode, unsigned mod, + const unsigned char *in, unsigned size) +{ + unsigned char *buf = malloc(size); + struct dive *dive; + struct divecomputer *dc; + struct tm tm = {0}; + uint32_t csum[5]; + + double max_depth, avg_depth, min_temp; + unsigned int duration = 0, corrupt_dive = 0; + + /* + * The scrambling has odd boundaries. I think the boundaries + * match some data structure size, but I don't know. They were + * discovered the same way we dynamically discover the decode + * size: automatically looking for least random output. + * + * The boundaries are also this confused "off-by-one" thing, + * the same way the file size is off by one. It's as if the + * cochran software forgot to write one byte at the beginning. + */ + partial_decode(0, 0x0fff, decode, 1, mod, in, size, buf); + partial_decode(0x0fff, 0x1fff, decode, 0, mod, in, size, buf); + partial_decode(0x1fff, 0x2fff, decode, 0, mod, in, size, buf); + partial_decode(0x2fff, 0x48ff, decode, 0, mod, in, size, buf); + + /* + * This is not all the descrambling you need - the above are just + * what appears to be the fixed-size blocks. The rest is also + * scrambled, but there seems to be size differences in the data, + * so this just descrambles part of it: + */ + // Decode log entry (512 bytes + random prefix) + partial_decode(0x48ff, 0x4914 + config.logbook_size, decode, + 0, mod, in, size, buf); + + unsigned int sample_size = size - 0x4914 - config.logbook_size; + int g; + + // Decode sample data + partial_decode(0x4914 + config.logbook_size, size, decode, + 0, mod, in, size, buf); + +#ifdef COCHRAN_DEBUG + // Display pre-logbook data + puts("\nPre Logbook Data\n"); + cochran_debug_write(buf, 0x4914); + + // Display log book + puts("\nLogbook Data\n"); + cochran_debug_write(buf + 0x4914, config.logbook_size + 0x400); + + // Display sample data + puts("\nSample Data\n"); +#endif + + dive = alloc_dive(); + dc = &dive->dc; + + unsigned char *log = (buf + 0x4914); + + switch (config.type) { + case TYPE_GEMINI: + case TYPE_COMMANDER: + if (config.type == TYPE_GEMINI) { + dc->model = "Gemini"; + dc->deviceid = buf[0x18c] * 256 + buf[0x18d]; // serial no + fill_default_cylinder(&dive->cylinder[0]); + dive->cylinder[0].gasmix.o2.permille = (log[CMD_O2_PERCENT] / 256 + + log[CMD_O2_PERCENT + 1]) * 10; + dive->cylinder[0].gasmix.he.permille = 0; + } else { + dc->model = "Commander"; + dc->deviceid = array_uint32_le(buf + 0x31e); // serial no + for (g = 0; g < 2; g++) { + fill_default_cylinder(&dive->cylinder[g]); + dive->cylinder[g].gasmix.o2.permille = (log[CMD_O2_PERCENT + g * 2] / 256 + + log[CMD_O2_PERCENT + g * 2 + 1]) * 10; + dive->cylinder[g].gasmix.he.permille = 0; + } + } + + tm.tm_year = log[CMD_YEAR]; + tm.tm_mon = log[CMD_MON] - 1; + tm.tm_mday = log[CMD_DAY]; + tm.tm_hour = log[CMD_HOUR]; + tm.tm_min = log[CMD_MIN]; + tm.tm_sec = log[CMD_SEC]; + tm.tm_isdst = -1; + + dive->when = dc->when = utc_mktime(&tm); + dive->number = log[CMD_NUMBER] + log[CMD_NUMBER + 1] * 256 + 1; + dc->duration.seconds = (log[CMD_BT] + log[CMD_BT + 1] * 256) * 60; + dc->surfacetime.seconds = (log[CMD_SIT] + log[CMD_SIT + 1] * 256) * 60; + dc->maxdepth.mm = (log[CMD_MAX_DEPTH] + + log[CMD_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->meandepth.mm = (log[CMD_AVG_DEPTH] + + log[CMD_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((log[CMD_MIN_TEMP] / 32) - 1.8); + dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 + * (double) log[CMD_ALTITUDE] * 250 * FEET, 5.25588) * 1000; + dc->salinity = 10000 + 150 * log[CMD_WATER_CONDUCTIVITY]; + + SHA1(log + CMD_NUMBER, 2, (unsigned char *)csum); + dc->diveid = csum[0]; + + if (log[CMD_MAX_DEPTH] == 0xff && log[CMD_MAX_DEPTH + 1] == 0xff) + corrupt_dive = 1; + + break; + case TYPE_EMC: + dc->model = "EMC"; + dc->deviceid = array_uint32_le(buf + 0x31e); // serial no + for (g = 0; g < 4; g++) { + fill_default_cylinder(&dive->cylinder[g]); + dive->cylinder[g].gasmix.o2.permille = + (log[EMC_O2_PERCENT + g * 2] / 256 + + log[EMC_O2_PERCENT + g * 2 + 1]) * 10; + dive->cylinder[g].gasmix.he.permille = + (log[EMC_HE_PERCENT + g * 2] / 256 + + log[EMC_HE_PERCENT + g * 2 + 1]) * 10; + } + + tm.tm_year = log[EMC_YEAR]; + tm.tm_mon = log[EMC_MON] - 1; + tm.tm_mday = log[EMC_DAY]; + tm.tm_hour = log[EMC_HOUR]; + tm.tm_min = log[EMC_MIN]; + tm.tm_sec = log[EMC_SEC]; + tm.tm_isdst = -1; + + dive->when = dc->when = utc_mktime(&tm); + dive->number = log[EMC_NUMBER] + log[EMC_NUMBER + 1] * 256 + 1; + dc->duration.seconds = (log[EMC_BT] + log[EMC_BT + 1] * 256) * 60; + dc->surfacetime.seconds = (log[EMC_SIT] + log[EMC_SIT + 1] * 256) * 60; + dc->maxdepth.mm = (log[EMC_MAX_DEPTH] + + log[EMC_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->meandepth.mm = (log[EMC_AVG_DEPTH] + + log[EMC_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((log[EMC_MIN_TEMP] - 32) / 1.8); + dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 + * (double) log[EMC_ALTITUDE] * 250 * FEET, 5.25588) * 1000; + dc->salinity = 10000 + 150 * (log[EMC_WATER_CONDUCTIVITY] & 0x3); + + SHA1(log + EMC_NUMBER, 2, (unsigned char *)csum); + dc->diveid = csum[0]; + + if (log[EMC_MAX_DEPTH] == 0xff && log[EMC_MAX_DEPTH + 1] == 0xff) + corrupt_dive = 1; + + break; + } + + cochran_parse_samples(dive, buf + 0x4914, buf + 0x4914 + + config.logbook_size, sample_size, + &duration, &max_depth, &avg_depth, &min_temp); + + // Check for corrupt dive + if (corrupt_dive) { + dc->maxdepth.mm = max_depth * FEET * 1000; + dc->meandepth.mm = avg_depth * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((min_temp - 32) / 1.8); + dc->duration.seconds = duration; + } + + dive->downloaded = true; + record_dive(dive); + mark_divelist_changed(true); + + free(buf); +} + +int try_to_open_cochran(const char *filename, struct memblock *mem) +{ + (void) filename; + unsigned int i; + unsigned int mod; + unsigned int *offsets, dive1, dive2; + unsigned char *decode = mem->buffer + 0x40001; + + if (mem->size < 0x40000) + return 0; + + offsets = (unsigned int *) mem->buffer; + dive1 = offsets[0]; + dive2 = offsets[1]; + + if (dive1 < 0x40000 || dive2 < dive1 || dive2 > mem->size) + return 0; + + mod = decode[0x100] + 1; + cochran_parse_header(decode, mod, mem->buffer + 0x40000, dive1 - 0x40000); + + // Decode each dive + for (i = 0; i < 65534; i++) { + dive1 = offsets[i]; + dive2 = offsets[i + 1]; + if (dive2 < dive1) + break; + if (dive2 > mem->size) + break; + + cochran_parse_dive(decode, mod, mem->buffer + dive1, + dive2 - dive1); + } + + return 1; // no further processing needed +} diff --git a/core/cochran.h b/core/cochran.h new file mode 100644 index 000000000..97d4361c8 --- /dev/null +++ b/core/cochran.h @@ -0,0 +1,44 @@ +// Commander log fields +#define CMD_SEC 1 +#define CMD_MIN 0 +#define CMD_HOUR 3 +#define CMD_DAY 2 +#define CMD_MON 5 +#define CMD_YEAR 4 +#define CME_START_OFFSET 6 // 4 bytes +#define CMD_WATER_CONDUCTIVITY 25 // 1 byte, 0=low, 2=high +#define CMD_START_SGC 42 // 2 bytes +#define CMD_START_TEMP 45 // 1 byte, F +#define CMD_START_DEPTH 56 // 2 bytes, /4=ft +#define CMD_START_PSI 62 +#define CMD_SIT 68 // 2 bytes, minutes +#define CMD_NUMBER 70 // 2 bytes +#define CMD_ALTITUDE 73 // 1 byte, /4=Kilofeet +#define CMD_END_OFFSET 128 // 4 bytes +#define CMD_MIN_TEMP 153 // 1 byte, F +#define CMD_BT 166 // 2 bytes, minutes +#define CMD_MAX_DEPTH 168 // 2 bytes, /4=ft +#define CMD_AVG_DEPTH 170 // 2 bytes, /4=ft +#define CMD_O2_PERCENT 210 // 8 bytes, 4 x 2 byte, /256=% + +// EMC log fields +#define EMC_SEC 0 +#define EMC_MIN 1 +#define EMC_HOUR 2 +#define EMC_DAY 3 +#define EMC_MON 4 +#define EMC_YEAR 5 +#define EMC_START_OFFSET 6 // 4 bytes +#define EMC_WATER_CONDUCTIVITY 24 // 1 byte bits 0:1, 0=low, 2=high +#define EMC_START_DEPTH 42 // 2 byte, /256=ft +#define EMC_START_TEMP 55 // 1 byte, F +#define EMC_SIT 84 // 2 bytes, minutes, LE +#define EMC_NUMBER 86 // 2 bytes +#define EMC_ALTITUDE 89 // 1 byte, /4=Kilofeet +#define EMC_O2_PERCENT 144 // 20 bytes, 10 x 2 bytes, /256=% +#define EMC_HE_PERCENT 164 // 20 bytes, 10 x 2 bytes, /256=% +#define EMC_END_OFFSET 256 // 4 bytes +#define EMC_MIN_TEMP 293 // 1 byte, F +#define EMC_BT 304 // 2 bytes, minutes +#define EMC_MAX_DEPTH 306 // 2 bytes, /4=ft +#define EMC_AVG_DEPTH 310 // 2 bytes, /4=ft diff --git a/core/color.cpp b/core/color.cpp new file mode 100644 index 000000000..cf6f43916 --- /dev/null +++ b/core/color.cpp @@ -0,0 +1,88 @@ +#include "color.h" + +QMap<color_indice_t, QVector<QColor> > profile_color; + +void fill_profile_color() +{ +#define COLOR(x, y, z) QVector<QColor>() << x << y << z; + profile_color[SAC_1] = COLOR(FUNGREEN1, BLACK1_LOW_TRANS, FUNGREEN1); + profile_color[SAC_2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); + profile_color[SAC_3] = COLOR(ATLANTIS1, BLACK1_LOW_TRANS, ATLANTIS1); + profile_color[SAC_4] = COLOR(ATLANTIS2, BLACK1_LOW_TRANS, ATLANTIS2); + profile_color[SAC_5] = COLOR(EARLSGREEN1, BLACK1_LOW_TRANS, EARLSGREEN1); + profile_color[SAC_6] = COLOR(HOKEYPOKEY1, BLACK1_LOW_TRANS, HOKEYPOKEY1); + profile_color[SAC_7] = COLOR(TUSCANY1, BLACK1_LOW_TRANS, TUSCANY1); + profile_color[SAC_8] = COLOR(CINNABAR1, BLACK1_LOW_TRANS, CINNABAR1); + profile_color[SAC_9] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); + + profile_color[VELO_STABLE] = COLOR(CAMARONE1, BLACK1_LOW_TRANS, CAMARONE1); + profile_color[VELO_SLOW] = COLOR(LIMENADE1, BLACK1_LOW_TRANS, LIMENADE1); + profile_color[VELO_MODERATE] = COLOR(RIOGRANDE1, BLACK1_LOW_TRANS, RIOGRANDE1); + profile_color[VELO_FAST] = COLOR(PIRATEGOLD1, BLACK1_LOW_TRANS, PIRATEGOLD1); + profile_color[VELO_CRAZY] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + + profile_color[PO2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); + profile_color[PO2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[PN2] = COLOR(BLACK1_LOW_TRANS, BLACK1_LOW_TRANS, BLACK1_LOW_TRANS); + profile_color[PN2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[PHE] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); + profile_color[PHE_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[O2SETPOINT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[CCRSENSOR1] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); + profile_color[CCRSENSOR2] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); + profile_color[CCRSENSOR3] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); + profile_color[PP_LINES] = COLOR(BLACK1_HIGH_TRANS, BLACK1_LOW_TRANS, BLACK1_HIGH_TRANS); + + profile_color[TEXT_BACKGROUND] = COLOR(CONCRETE1_LOWER_TRANS, WHITE1, CONCRETE1_LOWER_TRANS); + profile_color[ALERT_BG] = COLOR(BROOM1_LOWER_TRANS, BLACK1_LOW_TRANS, BROOM1_LOWER_TRANS); + profile_color[ALERT_FG] = COLOR(BLACK1_LOW_TRANS, WHITE1, BLACK1_LOW_TRANS); + profile_color[EVENTS] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); + profile_color[SAMPLE_DEEP] = COLOR(QColor(Qt::red).darker(), BLACK1, PERSIANRED1); + profile_color[SAMPLE_SHALLOW] = COLOR(QColor(Qt::red).lighter(), BLACK1_LOW_TRANS, PERSIANRED1); + profile_color[SMOOTHED] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_LOW_TRANS, REDORANGE1_HIGH_TRANS); + profile_color[MINUTE] = COLOR(MEDIUMREDVIOLET1_HIGHER_TRANS, BLACK1_LOW_TRANS, MEDIUMREDVIOLET1_HIGHER_TRANS); + profile_color[TIME_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); + profile_color[TIME_TEXT] = COLOR(FORESTGREEN1, BLACK1, FORESTGREEN1); + profile_color[DEPTH_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); + profile_color[MEAN_DEPTH] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_PLOT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_TEXT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_AXIS] = COLOR(MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS); + profile_color[DEPTH_BOTTOM] = COLOR(GOVERNORBAY1_MED_TRANS, BLACK1_HIGH_TRANS, GOVERNORBAY1_MED_TRANS); + profile_color[DEPTH_TOP] = COLOR(MERCURY1_MED_TRANS, WHITE1_MED_TRANS, MERCURY1_MED_TRANS); + profile_color[TEMP_TEXT] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); + profile_color[TEMP_PLOT] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); + profile_color[SAC_DEFAULT] = COLOR(WHITE1, BLACK1_LOW_TRANS, FORESTGREEN1); + profile_color[BOUNDING_BOX] = COLOR(WHITE1, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); + profile_color[PRESSURE_TEXT] = COLOR(KILLARNEY1, BLACK1_LOW_TRANS, KILLARNEY1); + profile_color[BACKGROUND] = COLOR(SPRINGWOOD1, WHITE1, SPRINGWOOD1); + profile_color[BACKGROUND_TRANS] = COLOR(SPRINGWOOD1_MED_TRANS, WHITE1_MED_TRANS, SPRINGWOOD1_MED_TRANS); + profile_color[CEILING_SHALLOW] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_HIGH_TRANS, REDORANGE1_HIGH_TRANS); + profile_color[CEILING_DEEP] = COLOR(RED1_MED_TRANS, BLACK1_HIGH_TRANS, RED1_MED_TRANS); + profile_color[CALC_CEILING_SHALLOW] = COLOR(FUNGREEN1_HIGH_TRANS, BLACK1_HIGH_TRANS, FUNGREEN1_HIGH_TRANS); + profile_color[CALC_CEILING_DEEP] = COLOR(APPLE1_HIGH_TRANS, BLACK1_HIGH_TRANS, APPLE1_HIGH_TRANS); + profile_color[TISSUE_PERCENTAGE] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); + profile_color[GF_LINE] = COLOR(BLACK1, BLACK1_LOW_TRANS, BLACK1); + profile_color[AMB_PRESSURE_LINE] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, ATLANTIS1); +#undef COLOR +} + +QColor getColor(const color_indice_t i, bool isGrayscale) +{ + if (profile_color.count() > i && i >= 0) + return profile_color[i].at((isGrayscale) ? 1 : 0); + return QColor(Qt::black); +} + +QColor getSacColor(int sac, int avg_sac) +{ + int sac_index = 0; + int delta = sac - avg_sac + 7000; + + sac_index = delta / 2000; + if (sac_index < 0) + sac_index = 0; + if (sac_index > SAC_COLORS - 1) + sac_index = SAC_COLORS - 1; + return getColor((color_indice_t)(SAC_COLORS_START_IDX + sac_index), false); +} diff --git a/core/color.h b/core/color.h new file mode 100644 index 000000000..57ad77242 --- /dev/null +++ b/core/color.h @@ -0,0 +1,152 @@ +#ifndef COLOR_H +#define COLOR_H + +/* The colors are named by picking the closest match + from http://chir.ag/projects/name-that-color */ + +#include <QColor> +#include <QMap> +#include <QVector> + +// Greens +#define CAMARONE1 QColor::fromRgbF(0.0, 0.4, 0.0, 1) +#define FUNGREEN1 QColor::fromRgbF(0.0, 0.4, 0.2, 1) +#define FUNGREEN1_HIGH_TRANS QColor::fromRgbF(0.0, 0.4, 0.2, 0.25) +#define KILLARNEY1 QColor::fromRgbF(0.2, 0.4, 0.2, 1) +#define APPLE1 QColor::fromRgbF(0.2, 0.6, 0.2, 1) +#define APPLE1_MED_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.5) +#define APPLE1_HIGH_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.25) +#define LIMENADE1 QColor::fromRgbF(0.4, 0.8, 0.0, 1) +#define ATLANTIS1 QColor::fromRgbF(0.4, 0.8, 0.2, 1) +#define ATLANTIS2 QColor::fromRgbF(0.6, 0.8, 0.2, 1) +#define RIOGRANDE1 QColor::fromRgbF(0.8, 0.8, 0.0, 1) +#define EARLSGREEN1 QColor::fromRgbF(0.8, 0.8, 0.2, 1) +#define FORESTGREEN1 QColor::fromRgbF(0.1, 0.5, 0.1, 1) +#define NITROX_GREEN QColor::fromRgbF(0, 0.54, 0.375, 1) + +// Reds +#define PERSIANRED1 QColor::fromRgbF(0.8, 0.2, 0.2, 1) +#define TUSCANY1 QColor::fromRgbF(0.8, 0.4, 0.2, 1) +#define PIRATEGOLD1 QColor::fromRgbF(0.8, 0.5, 0.0, 1) +#define HOKEYPOKEY1 QColor::fromRgbF(0.8, 0.6, 0.2, 1) +#define CINNABAR1 QColor::fromRgbF(0.9, 0.3, 0.2, 1) +#define REDORANGE1 QColor::fromRgbF(1.0, 0.2, 0.2, 1) +#define REDORANGE1_HIGH_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.25) +#define REDORANGE1_MED_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.5) +#define RED1_MED_TRANS QColor::fromRgbF(1.0, 0.0, 0.0, 0.5) +#define RED1 QColor::fromRgbF(1.0, 0.0, 0.0, 1) + +// Monochromes +#define BLACK1 QColor::fromRgbF(0.0, 0.0, 0.0, 1) +#define BLACK1_LOW_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.75) +#define BLACK1_HIGH_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.25) +#define TUNDORA1_MED_TRANS QColor::fromRgbF(0.3, 0.3, 0.3, 0.5) +#define MED_GRAY_HIGH_TRANS QColor::fromRgbF(0.5, 0.5, 0.5, 0.25) +#define MERCURY1_MED_TRANS QColor::fromRgbF(0.9, 0.9, 0.9, 0.5) +#define CONCRETE1_LOWER_TRANS QColor::fromRgbF(0.95, 0.95, 0.95, 0.9) +#define WHITE1_MED_TRANS QColor::fromRgbF(1.0, 1.0, 1.0, 0.5) +#define WHITE1 QColor::fromRgbF(1.0, 1.0, 1.0, 1) + +// Blues +#define GOVERNORBAY2 QColor::fromRgbF(0.2, 0.2, 0.7, 1) +#define GOVERNORBAY1_MED_TRANS QColor::fromRgbF(0.2, 0.2, 0.8, 0.5) +#define ROYALBLUE2 QColor::fromRgbF(0.2, 0.2, 0.9, 1) +#define ROYALBLUE2_LOW_TRANS QColor::fromRgbF(0.2, 0.2, 0.9, 0.75) +#define AIR_BLUE QColor::fromRgbF(0.25, 0.75, 1.0, 1) +#define AIR_BLUE_TRANS QColor::fromRgbF(0.25, 0.75, 1.0, 0.5) + +// Yellows / BROWNS +#define SPRINGWOOD1 QColor::fromRgbF(0.95, 0.95, 0.9, 1) +#define SPRINGWOOD1_MED_TRANS QColor::fromRgbF(0.95, 0.95, 0.9, 0.5) +#define BROOM1_LOWER_TRANS QColor::fromRgbF(1.0, 1.0, 0.1, 0.9) +#define PEANUT QColor::fromRgbF(0.5, 0.2, 0.1, 1.0) +#define PEANUT_MED_TRANS QColor::fromRgbF(0.5, 0.2, 0.1, 0.5) +#define NITROX_YELLOW QColor::fromRgbF(0.98, 0.89, 0.07, 1.0) + +// Magentas +#define MEDIUMREDVIOLET1_HIGHER_TRANS QColor::fromRgbF(0.7, 0.2, 0.7, 0.1) + +#define SAC_COLORS_START_IDX SAC_1 +#define SAC_COLORS 9 +#define VELOCITY_COLORS_START_IDX VELO_STABLE +#define VELOCITY_COLORS 5 + +typedef enum { + /* SAC colors. Order is important, the SAC_COLORS_START_IDX define above. */ + SAC_1, + SAC_2, + SAC_3, + SAC_4, + SAC_5, + SAC_6, + SAC_7, + SAC_8, + SAC_9, + + /* Velocity colors. Order is still important, ref VELOCITY_COLORS_START_IDX. */ + VELO_STABLE, + VELO_SLOW, + VELO_MODERATE, + VELO_FAST, + VELO_CRAZY, + + /* gas colors */ + PO2, + PO2_ALERT, + PN2, + PN2_ALERT, + PHE, + PHE_ALERT, + O2SETPOINT, + CCRSENSOR1, + CCRSENSOR2, + CCRSENSOR3, + PP_LINES, + + /* Other colors */ + TEXT_BACKGROUND, + ALERT_BG, + ALERT_FG, + EVENTS, + SAMPLE_DEEP, + SAMPLE_SHALLOW, + SMOOTHED, + MINUTE, + TIME_GRID, + TIME_TEXT, + DEPTH_GRID, + MEAN_DEPTH, + HR_TEXT, + HR_PLOT, + HR_AXIS, + DEPTH_TOP, + DEPTH_BOTTOM, + TEMP_TEXT, + TEMP_PLOT, + SAC_DEFAULT, + BOUNDING_BOX, + PRESSURE_TEXT, + BACKGROUND, + BACKGROUND_TRANS, + CEILING_SHALLOW, + CEILING_DEEP, + CALC_CEILING_SHALLOW, + CALC_CEILING_DEEP, + TISSUE_PERCENTAGE, + GF_LINE, + AMB_PRESSURE_LINE +} color_indice_t; + +extern QMap<color_indice_t, QVector<QColor> > profile_color; +void fill_profile_color(); +QColor getColor(const color_indice_t i, bool isGrayscale = false); +QColor getSacColor(int sac, int diveSac); +struct text_render_options { + double size; + color_indice_t color; + double hpos, vpos; +}; + +typedef text_render_options text_render_options_t; + +#endif // COLOR_H diff --git a/core/compressibility.r b/core/compressibility.r new file mode 100644 index 000000000..66310f3aa --- /dev/null +++ b/core/compressibility.r @@ -0,0 +1,115 @@ +# Compressibility data gathered by Lubomir I Ivanov: +# +# "Data obtained by finding two books online: +# +# [1] +# PERRY’S CHEMICAL ENGINEERS’ HANDBOOK SEVENTH EDITION +# pretty serious book, from which the wiki AIR values come from! +# +# http://www.unhas.ac.id/rhiza/arsip/kuliah/Sistem-dan-Tekn-Kendali-Proses/PDF_Collections/REFERENSI/Perrys_Chemical_Engineering_Handbook.pdf +# page 2-165 +# +# [*](Computed from pressure-volume-temperature tables in Vasserman monographs) +# ^ i have no idea idea what this means, but the values might not be exactly +# experimental?! +# +# the only thing this book is missing is helium, thus [2]! +# +# [2] +# VOLUMETRIC BEHAVIOR OF HELIUM-ARGON MIXTURES AT HIGH PRESSURE AND MODERATE TEMPERATURE. +# +# https://shareok.org/bitstream/handle/11244/2062/6614196.PDF?sequence=1 +# page 108 +# +# +# the book has some tables with pressure values in atmosphere units. i'm +# converting them bars. one of the relevant tables is for 323K and one for 273K +# (both almost equal distance from 300K). +# +# this again is a linear mix operation between isotherms, which is probably not +# the most accurate solution but it works. +# +# all data sets contain Z values at 300k, while the pressures are in bars in +# the 1 to 500 range +# +# + +x = c(1, 5, 10, 20, 40, 60, 80, 100, 200, 300, 400, 500) +o2 = c(0.9994, 0.9968, 0.9941, 0.9884, 0.9771, 0.9676, 0.9597, 0.9542, 0.9560, 0.9972, 1.0689, 1.1572) +n2 = c(0.9998, 0.9990, 0.9983, 0.9971, 0.9964, 0.9973, 1.0000, 1.0052, 1.0559, 1.1422, 1.2480, 1.3629) +he = c(1.0005, 1.0024, 1.0048, 1.0096, 1.0191, 1.0286, 1.0381, 1.0476, 1.0943, 1.1402, 1.1854, 1.2297) + +options(digits=15) + +# +# Get the O2 virial coefficients +# +plot(x,o2) +o2fit = nls(o2 ~ 1.0 + p1*x + p2 *x^2 + p3*x^3, start=list(p1=0,p2=0,p3=0)) +summary(o2fit) + +new = data.frame(x = seq(min(x),max(x),len=200)) +lines(new$x,predict(o2fit,newdata=new)) + +# +# Get the N2 virial coefficients +# +plot(x,n2) +n2fit = nls(n2 ~ 1.0 + p1*x + p2 *x^2 + p3*x^3, start=list(p1=0,p2=0,p3=0)) +summary(n2fit) + +new = data.frame(x = seq(min(x),max(x),len=200)) +lines(new$x,predict(n2fit,newdata=new)) + +# +# Get the He virial coefficients +# +# NOTE! This will not confirm convergence, thus the warnOnly. +# That may be a sign that the data is possibly artificial. +# +plot(x,he) +hefit = nls(he ~ 1.0 + p1*x + p2 *x^2 + p3*x^3, + start=list(p1=0,p2=0,p3=0), + control=nls.control(warnOnly=TRUE)) +summary(hefit) + +new = data.frame(x = seq(min(x),max(x),len=200)) +lines(new$x,predict(hefit,newdata=new)) + +# +# Raw data from VOLUMETRIC BEHAVIOR OF HELIUM-ARGON MIXTURES [..] +# T=323.15K (50 C) +p323atm = c(674.837, 393.223, 237.310, 146.294, 91.4027, 57.5799, 36.4620, 23.1654, 14.7478, 9.4017, 5.9987, 3.8300, + 540.204, 319.943, 195.008, 120.951, 75.8599, 47.9005, 30.3791, 19.3193, 12.3080, 7.8495, 5.0100, 3.1992) + +Hez323 = c(1.28067, 1.16782, 1.10289, 1.06407, 1.04028, 1.02548, 1.01617, 1.01029, 1.00656, 1.00418, 1.00267, 1.00171, + 1.22738, 1.13754, 1.08493, 1.05312, 1.03349, 1.02122, 1.01349, 1.00859, 1.00548, 1.00349, 1.00223, 1.00143) + + +# T=273.15 (0 C) +p273atm = c(683.599, 391.213, 233.607, 143.091, 89.0521, 55.9640, 35.3851, 22.4593, 14.2908, 9.1072, 5.8095, 3.7083, + 534.047, 312.144, 188.741, 116.508, 72.8529, 45.9194, 29.0883, 18.4851, 11.7702, 7.5040, 4.7881, 3.0570) + +Hez273 = c(1.33969, 1.19985, 1.12121, 1.07494, 1.04689, 1.02957, 1.01874, 1.01191, 1.00758, 1.00484, 1.00309, 1.00197, + 1.26914, 1.16070, 1.09837, 1.06118, 1.03843, 1.02429, 1.01541, 1.00980, 1.00625, 1.00398, 1.00254, 1.00162) + +p323 = p323atm * 1.01325 +p273 = p273atm * 1.01325 + +x2=append(p323,p273) +he2=append(Hez323,Hez273) + +plot(x2,he2) + +hefit2 = nls(he2 ~ 1.0 + p1*x2 + p2*x2^2 + p3*x2^3, + start=list(p1=0,p2=0,p3=0)) +summary(hefit2) + +he3 = function(bar) +{ + 1.0 +0.00047961098687979363 * bar -0.00000004077670019935 * bar^2 +0.00000000000077707035 * bar^3 +} + +new = data.frame(x2 = seq(min(x2),max(x2),len=200)) +lines(new$x2,predict(hefit2,newdata=new)) +curve(he3, min(x2),max(x2),add=TRUE) diff --git a/core/configuredivecomputer.cpp b/core/configuredivecomputer.cpp new file mode 100644 index 000000000..2457ffe82 --- /dev/null +++ b/core/configuredivecomputer.cpp @@ -0,0 +1,681 @@ +#include "configuredivecomputer.h" +#include "libdivecomputer/hw.h" +#include <QTextStream> +#include <QFile> +#include <libxml/parser.h> +#include <libxml/parserInternals.h> +#include <libxml/tree.h> +#include <libxslt/transform.h> +#include <QStringList> +#include <QXmlStreamWriter> + +ConfigureDiveComputer::ConfigureDiveComputer() : readThread(0), + writeThread(0), + resetThread(0), + firmwareThread(0) +{ + setState(INITIAL); +} + +void ConfigureDiveComputer::readSettings(device_data_t *data) +{ + setState(READING); + + if (readThread) + readThread->deleteLater(); + + readThread = new ReadSettingsThread(this, data); + connect(readThread, SIGNAL(finished()), + this, SLOT(readThreadFinished()), Qt::QueuedConnection); + connect(readThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(readThread, SIGNAL(devicedetails(DeviceDetails *)), this, + SIGNAL(deviceDetailsChanged(DeviceDetails *))); + connect(readThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + readThread->start(); +} + +void ConfigureDiveComputer::saveDeviceDetails(DeviceDetails *details, device_data_t *data) +{ + setState(WRITING); + + if (writeThread) + writeThread->deleteLater(); + + writeThread = new WriteSettingsThread(this, data); + connect(writeThread, SIGNAL(finished()), + this, SLOT(writeThreadFinished()), Qt::QueuedConnection); + connect(writeThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(writeThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + writeThread->setDeviceDetails(details); + writeThread->start(); +} + +bool ConfigureDiveComputer::saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data) +{ + QString xml = ""; + QString vendor = data->vendor; + QString product = data->product; + QXmlStreamWriter writer(&xml); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + writer.writeStartElement("DiveComputerSettingsBackup"); + writer.writeStartElement("DiveComputer"); + writer.writeTextElement("Vendor", vendor); + writer.writeTextElement("Product", product); + writer.writeEndElement(); + writer.writeStartElement("Settings"); + writer.writeTextElement("CustomText", details->customText); + //Add gasses + QString gas1 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas1.oxygen), + QString::number(details->gas1.helium), + QString::number(details->gas1.type), + QString::number(details->gas1.depth)); + QString gas2 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas2.oxygen), + QString::number(details->gas2.helium), + QString::number(details->gas2.type), + QString::number(details->gas2.depth)); + QString gas3 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas3.oxygen), + QString::number(details->gas3.helium), + QString::number(details->gas3.type), + QString::number(details->gas3.depth)); + QString gas4 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas4.oxygen), + QString::number(details->gas4.helium), + QString::number(details->gas4.type), + QString::number(details->gas4.depth)); + QString gas5 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas5.oxygen), + QString::number(details->gas5.helium), + QString::number(details->gas5.type), + QString::number(details->gas5.depth)); + writer.writeTextElement("Gas1", gas1); + writer.writeTextElement("Gas2", gas2); + writer.writeTextElement("Gas3", gas3); + writer.writeTextElement("Gas4", gas4); + writer.writeTextElement("Gas5", gas5); + // + //Add dil values + QString dil1 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil1.oxygen), + QString::number(details->dil1.helium), + QString::number(details->dil1.type), + QString::number(details->dil1.depth)); + QString dil2 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil2.oxygen), + QString::number(details->dil2.helium), + QString::number(details->dil2.type), + QString::number(details->dil2.depth)); + QString dil3 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil3.oxygen), + QString::number(details->dil3.helium), + QString::number(details->dil3.type), + QString::number(details->dil3.depth)); + QString dil4 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil4.oxygen), + QString::number(details->dil4.helium), + QString::number(details->dil4.type), + QString::number(details->dil4.depth)); + QString dil5 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil5.oxygen), + QString::number(details->dil5.helium), + QString::number(details->dil5.type), + QString::number(details->dil5.depth)); + writer.writeTextElement("Dil1", dil1); + writer.writeTextElement("Dil2", dil2); + writer.writeTextElement("Dil3", dil3); + writer.writeTextElement("Dil4", dil4); + writer.writeTextElement("Dil5", dil5); + // + //Add set point values + QString sp1 = QString("%1,%2") + .arg(QString::number(details->sp1.sp), + QString::number(details->sp1.depth)); + QString sp2 = QString("%1,%2") + .arg(QString::number(details->sp2.sp), + QString::number(details->sp2.depth)); + QString sp3 = QString("%1,%2") + .arg(QString::number(details->sp3.sp), + QString::number(details->sp3.depth)); + QString sp4 = QString("%1,%2") + .arg(QString::number(details->sp4.sp), + QString::number(details->sp4.depth)); + QString sp5 = QString("%1,%2") + .arg(QString::number(details->sp5.sp), + QString::number(details->sp5.depth)); + writer.writeTextElement("SetPoint1", sp1); + writer.writeTextElement("SetPoint2", sp2); + writer.writeTextElement("SetPoint3", sp3); + writer.writeTextElement("SetPoint4", sp4); + writer.writeTextElement("SetPoint5", sp5); + + //Other Settings + writer.writeTextElement("DiveMode", QString::number(details->diveMode)); + writer.writeTextElement("Saturation", QString::number(details->saturation)); + writer.writeTextElement("Desaturation", QString::number(details->desaturation)); + writer.writeTextElement("LastDeco", QString::number(details->lastDeco)); + writer.writeTextElement("Brightness", QString::number(details->brightness)); + writer.writeTextElement("Units", QString::number(details->units)); + writer.writeTextElement("SamplingRate", QString::number(details->samplingRate)); + writer.writeTextElement("Salinity", QString::number(details->salinity)); + writer.writeTextElement("DiveModeColor", QString::number(details->diveModeColor)); + writer.writeTextElement("Language", QString::number(details->language)); + writer.writeTextElement("DateFormat", QString::number(details->dateFormat)); + writer.writeTextElement("CompassGain", QString::number(details->compassGain)); + writer.writeTextElement("SafetyStop", QString::number(details->safetyStop)); + writer.writeTextElement("GfHigh", QString::number(details->gfHigh)); + writer.writeTextElement("GfLow", QString::number(details->gfLow)); + writer.writeTextElement("PressureSensorOffset", QString::number(details->pressureSensorOffset)); + writer.writeTextElement("PpO2Min", QString::number(details->ppO2Min)); + writer.writeTextElement("PpO2Max", QString::number(details->ppO2Max)); + writer.writeTextElement("FutureTTS", QString::number(details->futureTTS)); + writer.writeTextElement("CcrMode", QString::number(details->ccrMode)); + writer.writeTextElement("DecoType", QString::number(details->decoType)); + writer.writeTextElement("AGFSelectable", QString::number(details->aGFSelectable)); + writer.writeTextElement("AGFHigh", QString::number(details->aGFHigh)); + writer.writeTextElement("AGFLow", QString::number(details->aGFLow)); + writer.writeTextElement("CalibrationGas", QString::number(details->calibrationGas)); + writer.writeTextElement("FlipScreen", QString::number(details->flipScreen)); + writer.writeTextElement("SetPointFallback", QString::number(details->setPointFallback)); + writer.writeTextElement("LeftButtonSensitivity", QString::number(details->leftButtonSensitivity)); + writer.writeTextElement("RightButtonSensitivity", QString::number(details->rightButtonSensitivity)); + writer.writeTextElement("BottomGasConsumption", QString::number(details->bottomGasConsumption)); + writer.writeTextElement("DecoGasConsumption", QString::number(details->decoGasConsumption)); + writer.writeTextElement("ModWarning", QString::number(details->modWarning)); + writer.writeTextElement("DynamicAscendRate", QString::number(details->dynamicAscendRate)); + writer.writeTextElement("GraphicalSpeedIndicator", QString::number(details->graphicalSpeedIndicator)); + writer.writeTextElement("AlwaysShowppO2", QString::number(details->alwaysShowppO2)); + + // Suunto vyper settings. + writer.writeTextElement("Altitude", QString::number(details->altitude)); + writer.writeTextElement("PersonalSafety", QString::number(details->personalSafety)); + writer.writeTextElement("TimeFormat", QString::number(details->timeFormat)); + + writer.writeStartElement("Light"); + writer.writeAttribute("enabled", QString::number(details->lightEnabled)); + writer.writeCharacters(QString::number(details->light)); + writer.writeEndElement(); + + writer.writeStartElement("AlarmTime"); + writer.writeAttribute("enabled", QString::number(details->alarmTimeEnabled)); + writer.writeCharacters(QString::number(details->alarmTime)); + writer.writeEndElement(); + + writer.writeStartElement("AlarmDepth"); + writer.writeAttribute("enabled", QString::number(details->alarmDepthEnabled)); + writer.writeCharacters(QString::number(details->alarmDepth)); + writer.writeEndElement(); + + writer.writeEndElement(); + writer.writeEndElement(); + + writer.writeEndDocument(); + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + lastError = tr("Could not save the backup file %1. Error Message: %2") + .arg(fileName, file.errorString()); + return false; + } + //file open successful. write data and save. + QTextStream out(&file); + out << xml; + + file.close(); + return true; +} + +bool ConfigureDiveComputer::restoreXMLBackup(QString fileName, DeviceDetails *details) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + lastError = tr("Could not open backup file: %1").arg(file.errorString()); + return false; + } + + QString xml = file.readAll(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + if (reader.isStartElement()) { + QString settingName = reader.name().toString(); + QXmlStreamAttributes attributes = reader.attributes(); + reader.readNext(); + QString keyString = reader.text().toString(); + + if (settingName == "CustomText") + details->customText = keyString; + + if (settingName == "Gas1") { + QStringList gasData = keyString.split(","); + gas gas1; + gas1.oxygen = gasData.at(0).toInt(); + gas1.helium = gasData.at(1).toInt(); + gas1.type = gasData.at(2).toInt(); + gas1.depth = gasData.at(3).toInt(); + details->gas1 = gas1; + } + + if (settingName == "Gas2") { + QStringList gasData = keyString.split(","); + gas gas2; + gas2.oxygen = gasData.at(0).toInt(); + gas2.helium = gasData.at(1).toInt(); + gas2.type = gasData.at(2).toInt(); + gas2.depth = gasData.at(3).toInt(); + details->gas2 = gas2; + } + + if (settingName == "Gas3") { + QStringList gasData = keyString.split(","); + gas gas3; + gas3.oxygen = gasData.at(0).toInt(); + gas3.helium = gasData.at(1).toInt(); + gas3.type = gasData.at(2).toInt(); + gas3.depth = gasData.at(3).toInt(); + details->gas3 = gas3; + } + + if (settingName == "Gas4") { + QStringList gasData = keyString.split(","); + gas gas4; + gas4.oxygen = gasData.at(0).toInt(); + gas4.helium = gasData.at(1).toInt(); + gas4.type = gasData.at(2).toInt(); + gas4.depth = gasData.at(3).toInt(); + details->gas4 = gas4; + } + + if (settingName == "Gas5") { + QStringList gasData = keyString.split(","); + gas gas5; + gas5.oxygen = gasData.at(0).toInt(); + gas5.helium = gasData.at(1).toInt(); + gas5.type = gasData.at(2).toInt(); + gas5.depth = gasData.at(3).toInt(); + details->gas5 = gas5; + } + + if (settingName == "Dil1") { + QStringList dilData = keyString.split(","); + gas dil1; + dil1.oxygen = dilData.at(0).toInt(); + dil1.helium = dilData.at(1).toInt(); + dil1.type = dilData.at(2).toInt(); + dil1.depth = dilData.at(3).toInt(); + details->dil1 = dil1; + } + + if (settingName == "Dil2") { + QStringList dilData = keyString.split(","); + gas dil2; + dil2.oxygen = dilData.at(0).toInt(); + dil2.helium = dilData.at(1).toInt(); + dil2.type = dilData.at(2).toInt(); + dil2.depth = dilData.at(3).toInt(); + details->dil1 = dil2; + } + + if (settingName == "Dil3") { + QStringList dilData = keyString.split(","); + gas dil3; + dil3.oxygen = dilData.at(0).toInt(); + dil3.helium = dilData.at(1).toInt(); + dil3.type = dilData.at(2).toInt(); + dil3.depth = dilData.at(3).toInt(); + details->dil3 = dil3; + } + + if (settingName == "Dil4") { + QStringList dilData = keyString.split(","); + gas dil4; + dil4.oxygen = dilData.at(0).toInt(); + dil4.helium = dilData.at(1).toInt(); + dil4.type = dilData.at(2).toInt(); + dil4.depth = dilData.at(3).toInt(); + details->dil4 = dil4; + } + + if (settingName == "Dil5") { + QStringList dilData = keyString.split(","); + gas dil5; + dil5.oxygen = dilData.at(0).toInt(); + dil5.helium = dilData.at(1).toInt(); + dil5.type = dilData.at(2).toInt(); + dil5.depth = dilData.at(3).toInt(); + details->dil5 = dil5; + } + + if (settingName == "SetPoint1") { + QStringList spData = keyString.split(","); + setpoint sp1; + sp1.sp = spData.at(0).toInt(); + sp1.depth = spData.at(1).toInt(); + details->sp1 = sp1; + } + + if (settingName == "SetPoint2") { + QStringList spData = keyString.split(","); + setpoint sp2; + sp2.sp = spData.at(0).toInt(); + sp2.depth = spData.at(1).toInt(); + details->sp2 = sp2; + } + + if (settingName == "SetPoint3") { + QStringList spData = keyString.split(","); + setpoint sp3; + sp3.sp = spData.at(0).toInt(); + sp3.depth = spData.at(1).toInt(); + details->sp3 = sp3; + } + + if (settingName == "SetPoint4") { + QStringList spData = keyString.split(","); + setpoint sp4; + sp4.sp = spData.at(0).toInt(); + sp4.depth = spData.at(1).toInt(); + details->sp4 = sp4; + } + + if (settingName == "SetPoint5") { + QStringList spData = keyString.split(","); + setpoint sp5; + sp5.sp = spData.at(0).toInt(); + sp5.depth = spData.at(1).toInt(); + details->sp5 = sp5; + } + + if (settingName == "Saturation") + details->saturation = keyString.toInt(); + + if (settingName == "Desaturation") + details->desaturation = keyString.toInt(); + + if (settingName == "DiveMode") + details->diveMode = keyString.toInt(); + + if (settingName == "LastDeco") + details->lastDeco = keyString.toInt(); + + if (settingName == "Brightness") + details->brightness = keyString.toInt(); + + if (settingName == "Units") + details->units = keyString.toInt(); + + if (settingName == "SamplingRate") + details->samplingRate = keyString.toInt(); + + if (settingName == "Salinity") + details->salinity = keyString.toInt(); + + if (settingName == "DiveModeColour") + details->diveModeColor = keyString.toInt(); + + if (settingName == "Language") + details->language = keyString.toInt(); + + if (settingName == "DateFormat") + details->dateFormat = keyString.toInt(); + + if (settingName == "CompassGain") + details->compassGain = keyString.toInt(); + + if (settingName == "SafetyStop") + details->safetyStop = keyString.toInt(); + + if (settingName == "GfHigh") + details->gfHigh = keyString.toInt(); + + if (settingName == "GfLow") + details->gfLow = keyString.toInt(); + + if (settingName == "PressureSensorOffset") + details->pressureSensorOffset = keyString.toInt(); + + if (settingName == "PpO2Min") + details->ppO2Min = keyString.toInt(); + + if (settingName == "PpO2Max") + details->ppO2Max = keyString.toInt(); + + if (settingName == "FutureTTS") + details->futureTTS = keyString.toInt(); + + if (settingName == "CcrMode") + details->ccrMode = keyString.toInt(); + + if (settingName == "DecoType") + details->decoType = keyString.toInt(); + + if (settingName == "AGFSelectable") + details->aGFSelectable = keyString.toInt(); + + if (settingName == "AGFHigh") + details->aGFHigh = keyString.toInt(); + + if (settingName == "AGFLow") + details->aGFLow = keyString.toInt(); + + if (settingName == "CalibrationGas") + details->calibrationGas = keyString.toInt(); + + if (settingName == "FlipScreen") + details->flipScreen = keyString.toInt(); + + if (settingName == "SetPointFallback") + details->setPointFallback = keyString.toInt(); + + if (settingName == "LeftButtonSensitivity") + details->leftButtonSensitivity = keyString.toInt(); + + if (settingName == "RightButtonSensitivity") + details->rightButtonSensitivity = keyString.toInt(); + + if (settingName == "BottomGasConsumption") + details->bottomGasConsumption = keyString.toInt(); + + if (settingName == "DecoGasConsumption") + details->decoGasConsumption = keyString.toInt(); + + if (settingName == "ModWarning") + details->modWarning = keyString.toInt(); + + if (settingName == "DynamicAscendRate") + details->dynamicAscendRate = keyString.toInt(); + + if (settingName == "GraphicalSpeedIndicator") + details->graphicalSpeedIndicator = keyString.toInt(); + + if (settingName == "AlwaysShowppO2") + details->alwaysShowppO2 = keyString.toInt(); + + if (settingName == "Altitude") + details->altitude = keyString.toInt(); + + if (settingName == "PersonalSafety") + details->personalSafety = keyString.toInt(); + + if (settingName == "TimeFormat") + details->timeFormat = keyString.toInt(); + + if (settingName == "Light") { + if (attributes.hasAttribute("enabled")) + details->lightEnabled = attributes.value("enabled").toString().toInt(); + details->light = keyString.toInt(); + } + + if (settingName == "AlarmDepth") { + if (attributes.hasAttribute("enabled")) + details->alarmDepthEnabled = attributes.value("enabled").toString().toInt(); + details->alarmDepth = keyString.toInt(); + } + + if (settingName == "AlarmTime") { + if (attributes.hasAttribute("enabled")) + details->alarmTimeEnabled = attributes.value("enabled").toString().toInt(); + details->alarmTime = keyString.toInt(); + } + } + reader.readNext(); + } + + return true; +} + +void ConfigureDiveComputer::startFirmwareUpdate(QString fileName, device_data_t *data) +{ + setState(FWUPDATE); + if (firmwareThread) + firmwareThread->deleteLater(); + + firmwareThread = new FirmwareUpdateThread(this, data, fileName); + connect(firmwareThread, SIGNAL(finished()), + this, SLOT(firmwareThreadFinished()), Qt::QueuedConnection); + connect(firmwareThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(firmwareThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + firmwareThread->start(); +} + +void ConfigureDiveComputer::resetSettings(device_data_t *data) +{ + setState(RESETTING); + + if (resetThread) + resetThread->deleteLater(); + + resetThread = new ResetSettingsThread(this, data); + connect(resetThread, SIGNAL(finished()), + this, SLOT(resetThreadFinished()), Qt::QueuedConnection); + connect(resetThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(resetThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + resetThread->start(); +} + +void ConfigureDiveComputer::progressEvent(int percent) +{ + emit progress(percent); +} + +void ConfigureDiveComputer::setState(ConfigureDiveComputer::states newState) +{ + currentState = newState; + emit stateChanged(currentState); +} + +void ConfigureDiveComputer::setError(QString err) +{ + lastError = err; + emit error(err); +} + +void ConfigureDiveComputer::readThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Dive computer details read successfully")); + } +} + +void ConfigureDiveComputer::writeThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Setting successfully written to device")); + } +} + +void ConfigureDiveComputer::firmwareThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Device firmware successfully updated")); + } +} + +void ConfigureDiveComputer::resetThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Device settings successfully reset")); + } +} + +QString ConfigureDiveComputer::dc_open(device_data_t *data) +{ + FILE *fp = NULL; + dc_status_t rc; + + if (data->libdc_log) + fp = subsurface_fopen(logfile_name, "w"); + + data->libdc_logfile = fp; + + rc = dc_context_new(&data->context); + if (rc != DC_STATUS_SUCCESS) { + return tr("Unable to create libdivecomputer context"); + } + + if (fp) { + dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); + dc_context_set_logfunc(data->context, logfunc, fp); + } + +#if defined(SSRF_CUSTOM_SERIAL) + dc_serial_t *serial_device = NULL; + + if (data->bluetooth_mode) { +#ifdef BT_SUPPORT + rc = dc_serial_qt_open(&serial_device, data->context, data->devname); +#endif +#ifdef SERIAL_FTDI + } else if (!strcmp(data->devname, "ftdi")) { + rc = dc_serial_ftdi_open(&serial_device, data->context); +#endif + } + + if (rc != DC_STATUS_SUCCESS) { + return errmsg(rc); + } else if (serial_device) { + rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); + } else { +#else + { +#endif + rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); + } + + if (rc != DC_STATUS_SUCCESS) { + return tr("Could not a establish connection to the dive computer."); + } + + setState(OPEN); + + return NULL; +} + +void ConfigureDiveComputer::dc_close(device_data_t *data) +{ + if (data->device) + dc_device_close(data->device); + data->device = NULL; + if (data->context) + dc_context_free(data->context); + data->context = NULL; + + if (data->libdc_logfile) + fclose(data->libdc_logfile); + + setState(INITIAL); +} diff --git a/core/configuredivecomputer.h b/core/configuredivecomputer.h new file mode 100644 index 000000000..f14eeeca3 --- /dev/null +++ b/core/configuredivecomputer.h @@ -0,0 +1,68 @@ +#ifndef CONFIGUREDIVECOMPUTER_H +#define CONFIGUREDIVECOMPUTER_H + +#include <QObject> +#include <QThread> +#include <QVariant> +#include "libdivecomputer.h" +#include "configuredivecomputerthreads.h" +#include <QDateTime> + +#include "libxml/xmlreader.h" + +class ConfigureDiveComputer : public QObject { + Q_OBJECT +public: + explicit ConfigureDiveComputer(); + void readSettings(device_data_t *data); + + enum states { + OPEN, + INITIAL, + READING, + WRITING, + RESETTING, + FWUPDATE, + CANCELLING, + CANCELLED, + ERROR, + DONE, + }; + + QString lastError; + states currentState; + void saveDeviceDetails(DeviceDetails *details, device_data_t *data); + void fetchDeviceDetails(); + bool saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data); + bool restoreXMLBackup(QString fileName, DeviceDetails *details); + void startFirmwareUpdate(QString fileName, device_data_t *data); + void resetSettings(device_data_t *data); + + QString dc_open(device_data_t *data); +public +slots: + void dc_close(device_data_t *data); +signals: + void progress(int percent); + void message(QString msg); + void error(QString err); + void stateChanged(states newState); + void deviceDetailsChanged(DeviceDetails *newDetails); + +private: + ReadSettingsThread *readThread; + WriteSettingsThread *writeThread; + ResetSettingsThread *resetThread; + FirmwareUpdateThread *firmwareThread; + void setState(states newState); +private +slots: + void progressEvent(int percent); + void readThreadFinished(); + void writeThreadFinished(); + void resetThreadFinished(); + void firmwareThreadFinished(); + void setError(QString err); +}; + +#endif // CONFIGUREDIVECOMPUTER_H diff --git a/core/configuredivecomputerthreads.cpp b/core/configuredivecomputerthreads.cpp new file mode 100644 index 000000000..b229fc808 --- /dev/null +++ b/core/configuredivecomputerthreads.cpp @@ -0,0 +1,1778 @@ +#include "configuredivecomputerthreads.h" +#include "libdivecomputer/hw.h" +#include "libdivecomputer.h" +#include <libdivecomputer/version.h> + +#define OSTC3_GAS1 0x10 +#define OSTC3_GAS2 0x11 +#define OSTC3_GAS3 0x12 +#define OSTC3_GAS4 0x13 +#define OSTC3_GAS5 0x14 +#define OSTC3_DIL1 0x15 +#define OSTC3_DIL2 0x16 +#define OSTC3_DIL3 0x17 +#define OSTC3_DIL4 0x18 +#define OSTC3_DIL5 0x19 +#define OSTC3_SP1 0x1A +#define OSTC3_SP2 0x1B +#define OSTC3_SP3 0x1C +#define OSTC3_SP4 0x1D +#define OSTC3_SP5 0x1E +#define OSTC3_CCR_MODE 0x1F +#define OSTC3_DIVE_MODE 0x20 +#define OSTC3_DECO_TYPE 0x21 +#define OSTC3_PPO2_MAX 0x22 +#define OSTC3_PPO2_MIN 0x23 +#define OSTC3_FUTURE_TTS 0x24 +#define OSTC3_GF_LOW 0x25 +#define OSTC3_GF_HIGH 0x26 +#define OSTC3_AGF_LOW 0x27 +#define OSTC3_AGF_HIGH 0x28 +#define OSTC3_AGF_SELECTABLE 0x29 +#define OSTC3_SATURATION 0x2A +#define OSTC3_DESATURATION 0x2B +#define OSTC3_LAST_DECO 0x2C +#define OSTC3_BRIGHTNESS 0x2D +#define OSTC3_UNITS 0x2E +#define OSTC3_SAMPLING_RATE 0x2F +#define OSTC3_SALINITY 0x30 +#define OSTC3_DIVEMODE_COLOR 0x31 +#define OSTC3_LANGUAGE 0x32 +#define OSTC3_DATE_FORMAT 0x33 +#define OSTC3_COMPASS_GAIN 0x34 +#define OSTC3_PRESSURE_SENSOR_OFFSET 0x35 +#define OSTC3_SAFETY_STOP 0x36 +#define OSTC3_CALIBRATION_GAS_O2 0x37 +#define OSTC3_SETPOINT_FALLBACK 0x38 +#define OSTC3_FLIP_SCREEN 0x39 +#define OSTC3_LEFT_BUTTON_SENSIVITY 0x3A +#define OSTC3_RIGHT_BUTTON_SENSIVITY 0x3A +#define OSTC3_BOTTOM_GAS_CONSUMPTION 0x3C +#define OSTC3_DECO_GAS_CONSUMPTION 0x3D +#define OSTC3_MOD_WARNING 0x3E +#define OSTC3_DYNAMIC_ASCEND_RATE 0x3F +#define OSTC3_GRAPHICAL_SPEED_INDICATOR 0x40 +#define OSTC3_ALWAYS_SHOW_PPO2 0x41 +#define OSTC3_TEMP_SENSOR_OFFSET 0x42 +#define OSTC3_SAFETY_STOP_LENGTH 0x43 +#define OSTC3_SAFETY_STOP_START_DEPTH 0x44 +#define OSTC3_SAFETY_STOP_END_DEPTH 0x45 +#define OSTC3_SAFETY_STOP_RESET_DEPTH 0x46 + +#define OSTC3_HW_OSTC_3 0x0A +#define OSTC3_HW_OSTC_3P 0x1A +#define OSTC3_HW_OSTC_CR 0x05 +#define OSTC3_HW_OSTC_SPORT 0x12 +#define OSTC3_HW_OSTC_2 0x11 + + +#define SUUNTO_VYPER_MAXDEPTH 0x1e +#define SUUNTO_VYPER_TOTAL_TIME 0x20 +#define SUUNTO_VYPER_NUMBEROFDIVES 0x22 +#define SUUNTO_VYPER_COMPUTER_TYPE 0x24 +#define SUUNTO_VYPER_FIRMWARE 0x25 +#define SUUNTO_VYPER_SERIALNUMBER 0x26 +#define SUUNTO_VYPER_CUSTOM_TEXT 0x2c +#define SUUNTO_VYPER_SAMPLING_RATE 0x53 +#define SUUNTO_VYPER_ALTITUDE_SAFETY 0x54 +#define SUUNTO_VYPER_TIMEFORMAT 0x60 +#define SUUNTO_VYPER_UNITS 0x62 +#define SUUNTO_VYPER_MODEL 0x63 +#define SUUNTO_VYPER_LIGHT 0x64 +#define SUUNTO_VYPER_ALARM_DEPTH_TIME 0x65 +#define SUUNTO_VYPER_ALARM_TIME 0x66 +#define SUUNTO_VYPER_ALARM_DEPTH 0x68 +#define SUUNTO_VYPER_CUSTOM_TEXT_LENGHT 30 + +#ifdef DEBUG_OSTC +// Fake io to ostc memory banks +#define hw_ostc_device_eeprom_read local_hw_ostc_device_eeprom_read +#define hw_ostc_device_eeprom_write local_hw_ostc_device_eeprom_write +#define hw_ostc_device_clock local_hw_ostc_device_clock +#define OSTC_FILE "../OSTC-data-dump.bin" + +// Fake the open function. +static dc_status_t local_dc_device_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, const char *name) +{ + if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) + return DC_STATUS_SUCCESS; + else + return dc_device_open(out, context, descriptor, name); +} +#define dc_device_open local_dc_device_open + +// Fake the custom open function +static dc_status_t local_dc_device_custom_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, dc_serial_t *serial) +{ + if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) + return DC_STATUS_SUCCESS; + else + return dc_device_custom_open(out, context, descriptor, serial); +} +#define dc_device_custom_open local_dc_device_custom_open + +static dc_status_t local_hw_ostc_device_eeprom_read(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) +{ + FILE *f; + if ((f = fopen(OSTC_FILE, "r")) == NULL) + return DC_STATUS_NODEVICE; + fseek(f, bank * 256, SEEK_SET); + if (fread(data, sizeof(unsigned char), data_size, f) != data_size) { + fclose(f); + return DC_STATUS_IO; + } + fclose(f); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t local_hw_ostc_device_eeprom_write(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) +{ + FILE *f; + if ((f = fopen(OSTC_FILE, "r+")) == NULL) + f = fopen(OSTC_FILE, "w"); + fseek(f, bank * 256, SEEK_SET); + fwrite(data, sizeof(unsigned char), data_size, f); + fclose(f); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t local_hw_ostc_device_clock(void *ignored, dc_datetime_t *time) +{ + return DC_STATUS_SUCCESS; +} +#endif + +static int read_ostc_cf(unsigned char data[], unsigned char cf) +{ + return data[128 + (cf % 32) * 4 + 3] << 8 ^ data[128 + (cf % 32) * 4 + 2]; +} + +static void write_ostc_cf(unsigned char data[], unsigned char cf, unsigned char max_CF, unsigned int value) +{ + // Only write settings supported by this firmware. + if (cf > max_CF) + return; + + data[128 + (cf % 32) * 4 + 3] = (value & 0xff00) >> 8; + data[128 + (cf % 32) * 4 + 2] = (value & 0x00ff); +} + +#define EMIT_PROGRESS() do { \ + progress.current++; \ + progress_cb(device, DC_EVENT_PROGRESS, &progress, userdata); \ + } while (0) + +static dc_status_t read_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + unsigned char data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT + 1]; + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 16; + + rc = dc_device_read(device, SUUNTO_VYPER_COMPUTER_TYPE, data, 1); + if (rc == DC_STATUS_SUCCESS) { + const char *model; + // FIXME: grab this info from libdivecomputer descriptor + // instead of hard coded here + switch (data[0]) { + case 0x03: + model = "Stinger"; + break; + case 0x04: + model = "Mosquito"; + break; + case 0x05: + model = "D3"; + break; + case 0x0A: + model = "Vyper"; + break; + case 0x0B: + model = "Vytec"; + break; + case 0x0C: + model = "Cobra"; + break; + case 0x0D: + model = "Gekko"; + break; + case 0x16: + model = "Zoop"; + break; + case 20: + case 30: + case 60: + // Suunto Spyder have there sample interval at this position + // Fallthrough + default: + return DC_STATUS_UNSUPPORTED; + } + // We found a supported device + // we can safely proceed with reading/writing to this device. + m_deviceDetails->model = model; + } + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_MAXDEPTH, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + // in ft * 128.0 + int depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; + m_deviceDetails->maxDepth = depth; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_TOTAL_TIME, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int total_time = data[0] << 8 ^ data[1]; + m_deviceDetails->totalTime = total_time; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_NUMBEROFDIVES, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int number_of_dives = data[0] << 8 ^ data[1]; + m_deviceDetails->numberOfDives = number_of_dives; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_FIRMWARE, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->firmwareVersion = QString::number(data[0]) + ".0.0"; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_SERIALNUMBER, data, 4); + if (rc != DC_STATUS_SUCCESS) + return rc; + int serial_number = data[0] * 1000000 + data[1] * 10000 + data[2] * 100 + data[3]; + m_deviceDetails->serialNo = QString::number(serial_number); + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_CUSTOM_TEXT, data, SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); + if (rc != DC_STATUS_SUCCESS) + return rc; + data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT] = 0; + m_deviceDetails->customText = (const char *)data; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_SAMPLING_RATE, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->samplingRate = (int)data[0]; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALTITUDE_SAFETY, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->altitude = data[0] & 0x03; + m_deviceDetails->personalSafety = data[0] >> 2 & 0x03; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_TIMEFORMAT, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->timeFormat = data[0] & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_UNITS, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->units = data[0] & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_MODEL, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->diveMode = data[0] & 0x03; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_LIGHT, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->lightEnabled = data[0] >> 7; + m_deviceDetails->light = data[0] & 0x7F; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->alarmTimeEnabled = data[0] & 0x01; + m_deviceDetails->alarmDepthEnabled = data[0] >> 1 & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_TIME, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int time = data[0] << 8 ^ data[1]; + // The stinger stores alarm time in seconds instead of minutes. + if (m_deviceDetails->model == "Stinger") + time /= 60; + m_deviceDetails->alarmTime = time; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; + m_deviceDetails->alarmDepth = depth; + EMIT_PROGRESS(); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t write_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 10; + unsigned char data; + unsigned char data2[2]; + int time; + + // Maybee we should read the model from the device to sanity check it here too.. + // For now we just check that we actually read a device before writing to one. + if (m_deviceDetails->model == "") + return DC_STATUS_UNSUPPORTED; + + rc = dc_device_write(device, SUUNTO_VYPER_CUSTOM_TEXT, + // Convert the customText to a 30 char wide padded with " " + (const unsigned char *)QString("%1").arg(m_deviceDetails->customText, -30, QChar(' ')).toUtf8().data(), + SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->samplingRate; + rc = dc_device_write(device, SUUNTO_VYPER_SAMPLING_RATE, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->personalSafety << 2 ^ m_deviceDetails->altitude; + rc = dc_device_write(device, SUUNTO_VYPER_ALTITUDE_SAFETY, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->timeFormat; + rc = dc_device_write(device, SUUNTO_VYPER_TIMEFORMAT, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->units; + rc = dc_device_write(device, SUUNTO_VYPER_UNITS, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->diveMode; + rc = dc_device_write(device, SUUNTO_VYPER_MODEL, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->lightEnabled << 7 ^ (m_deviceDetails->light & 0x7F); + rc = dc_device_write(device, SUUNTO_VYPER_LIGHT, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->alarmDepthEnabled << 1 ^ m_deviceDetails->alarmTimeEnabled; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + // The stinger stores alarm time in seconds instead of minutes. + time = m_deviceDetails->alarmTime; + if (m_deviceDetails->model == "Stinger") + time *= 60; + data2[0] = time >> 8; + data2[1] = time & 0xFF; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_TIME, data2, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data2[0] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) >> 8; + data2[1] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) & 0x0FF; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH, data2, 2); + EMIT_PROGRESS(); + return rc; +} + +#if DC_VERSION_CHECK(0, 5, 0) +static dc_status_t read_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 57; + unsigned char hardware[1]; + + //Read hardware type + rc = hw_ostc3_device_hardware (device, hardware, sizeof (hardware)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + // FIXME: can we grab this info from libdivecomputer descriptor + // instead of hard coded here? + switch(hardware[0]) { + case OSTC3_HW_OSTC_3: + m_deviceDetails->model = "3"; + break; + case OSTC3_HW_OSTC_3P: + m_deviceDetails->model = "3+"; + break; + case OSTC3_HW_OSTC_CR: + m_deviceDetails->model = "CR"; + break; + case OSTC3_HW_OSTC_SPORT: + m_deviceDetails->model = "Sport"; + break; + case OSTC3_HW_OSTC_2: + m_deviceDetails->model = "2"; + break; + } + + //Read gas mixes + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + unsigned char gasData[4] = { 0, 0, 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS1, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas1.oxygen = gasData[0]; + gas1.helium = gasData[1]; + gas1.type = gasData[2]; + gas1.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS2, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas2.oxygen = gasData[0]; + gas2.helium = gasData[1]; + gas2.type = gasData[2]; + gas2.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS3, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas3.oxygen = gasData[0]; + gas3.helium = gasData[1]; + gas3.type = gasData[2]; + gas3.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS4, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas4.oxygen = gasData[0]; + gas4.helium = gasData[1]; + gas4.type = gasData[2]; + gas4.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS5, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas5.oxygen = gasData[0]; + gas5.helium = gasData[1]; + gas5.type = gasData[2]; + gas5.depth = gasData[3]; + EMIT_PROGRESS(); + + m_deviceDetails->gas1 = gas1; + m_deviceDetails->gas2 = gas2; + m_deviceDetails->gas3 = gas3; + m_deviceDetails->gas4 = gas4; + m_deviceDetails->gas5 = gas5; + EMIT_PROGRESS(); + + //Read Dil Values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + unsigned char dilData[4] = { 0, 0, 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL1, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil1.oxygen = dilData[0]; + dil1.helium = dilData[1]; + dil1.type = dilData[2]; + dil1.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL2, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil2.oxygen = dilData[0]; + dil2.helium = dilData[1]; + dil2.type = dilData[2]; + dil2.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL3, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil3.oxygen = dilData[0]; + dil3.helium = dilData[1]; + dil3.type = dilData[2]; + dil3.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL4, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil4.oxygen = dilData[0]; + dil4.helium = dilData[1]; + dil4.type = dilData[2]; + dil4.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL5, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil5.oxygen = dilData[0]; + dil5.helium = dilData[1]; + dil5.type = dilData[2]; + dil5.depth = dilData[3]; + EMIT_PROGRESS(); + + m_deviceDetails->dil1 = dil1; + m_deviceDetails->dil2 = dil2; + m_deviceDetails->dil3 = dil3; + m_deviceDetails->dil4 = dil4; + m_deviceDetails->dil5 = dil5; + + //Read set point Values + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + unsigned char spData[2] = { 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_SP1, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp1.sp = spData[0]; + sp1.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP2, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp2.sp = spData[0]; + sp2.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP3, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp3.sp = spData[0]; + sp3.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP4, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp4.sp = spData[0]; + sp4.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP5, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp5.sp = spData[0]; + sp5.depth = spData[1]; + EMIT_PROGRESS(); + + m_deviceDetails->sp1 = sp1; + m_deviceDetails->sp2 = sp2; + m_deviceDetails->sp3 = sp3; + m_deviceDetails->sp4 = sp4; + m_deviceDetails->sp5 = sp5; + + //Read other settings + unsigned char uData[1] = { 0 }; + +#define READ_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ + do { \ + rc = hw_ostc3_device_config_read(device, _OSTC3_SETTING, uData, sizeof(uData)); \ + if (rc != DC_STATUS_SUCCESS) \ + return rc; \ + m_deviceDetails->_DEVICE_DETAIL = uData[0]; \ + EMIT_PROGRESS(); \ + } while (0) + + READ_SETTING(OSTC3_DIVE_MODE, diveMode); + READ_SETTING(OSTC3_SATURATION, saturation); + READ_SETTING(OSTC3_DESATURATION, desaturation); + READ_SETTING(OSTC3_LAST_DECO, lastDeco); + READ_SETTING(OSTC3_BRIGHTNESS, brightness); + READ_SETTING(OSTC3_UNITS, units); + READ_SETTING(OSTC3_SAMPLING_RATE, samplingRate); + READ_SETTING(OSTC3_SALINITY, salinity); + READ_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); + READ_SETTING(OSTC3_LANGUAGE, language); + READ_SETTING(OSTC3_DATE_FORMAT, dateFormat); + READ_SETTING(OSTC3_COMPASS_GAIN, compassGain); + READ_SETTING(OSTC3_SAFETY_STOP, safetyStop); + READ_SETTING(OSTC3_GF_HIGH, gfHigh); + READ_SETTING(OSTC3_GF_LOW, gfLow); + READ_SETTING(OSTC3_PPO2_MIN, ppO2Min); + READ_SETTING(OSTC3_PPO2_MAX, ppO2Max); + READ_SETTING(OSTC3_FUTURE_TTS, futureTTS); + READ_SETTING(OSTC3_CCR_MODE, ccrMode); + READ_SETTING(OSTC3_DECO_TYPE, decoType); + READ_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); + READ_SETTING(OSTC3_AGF_HIGH, aGFHigh); + READ_SETTING(OSTC3_AGF_LOW, aGFLow); + READ_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); + READ_SETTING(OSTC3_FLIP_SCREEN, flipScreen); + READ_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); + READ_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); + READ_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); + READ_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); + READ_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); + READ_SETTING(OSTC3_MOD_WARNING, modWarning); + READ_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); + READ_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); + READ_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); + READ_SETTING(OSTC3_SAFETY_STOP_LENGTH, safetyStopLength); + READ_SETTING(OSTC3_SAFETY_STOP_START_DEPTH, safetyStopStartDepth); + READ_SETTING(OSTC3_SAFETY_STOP_END_DEPTH, safetyStopEndDepth); + READ_SETTING(OSTC3_SAFETY_STOP_RESET_DEPTH, safetyStopResetDepth); + +#undef READ_SETTING + + rc = hw_ostc3_device_config_read(device, OSTC3_PRESSURE_SENSOR_OFFSET, uData, sizeof(uData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + // OSTC3 stores the pressureSensorOffset in two-complement + m_deviceDetails->pressureSensorOffset = (signed char)uData[0]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_TEMP_SENSOR_OFFSET, uData, sizeof(uData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + // OSTC3 stores the tempSensorOffset in two-complement + m_deviceDetails->tempSensorOffset = (signed char)uData[0]; + EMIT_PROGRESS(); + + //read firmware settings + unsigned char fData[64] = { 0 }; + rc = hw_ostc3_device_version(device, fData, sizeof(fData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + int serial = fData[0] + (fData[1] << 8); + m_deviceDetails->serialNo = QString::number(serial); + m_deviceDetails->firmwareVersion = QString::number(fData[2]) + "." + QString::number(fData[3]); + QByteArray ar((char *)fData + 4, 60); + m_deviceDetails->customText = ar.trimmed(); + EMIT_PROGRESS(); + + return rc; +} + +static dc_status_t write_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 56; + + //write gas values + unsigned char gas1Data[4] = { + m_deviceDetails->gas1.oxygen, + m_deviceDetails->gas1.helium, + m_deviceDetails->gas1.type, + m_deviceDetails->gas1.depth + }; + + unsigned char gas2Data[4] = { + m_deviceDetails->gas2.oxygen, + m_deviceDetails->gas2.helium, + m_deviceDetails->gas2.type, + m_deviceDetails->gas2.depth + }; + + unsigned char gas3Data[4] = { + m_deviceDetails->gas3.oxygen, + m_deviceDetails->gas3.helium, + m_deviceDetails->gas3.type, + m_deviceDetails->gas3.depth + }; + + unsigned char gas4Data[4] = { + m_deviceDetails->gas4.oxygen, + m_deviceDetails->gas4.helium, + m_deviceDetails->gas4.type, + m_deviceDetails->gas4.depth + }; + + unsigned char gas5Data[4] = { + m_deviceDetails->gas5.oxygen, + m_deviceDetails->gas5.helium, + m_deviceDetails->gas5.type, + m_deviceDetails->gas5.depth + }; + //gas 1 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS1, gas1Data, sizeof(gas1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 2 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS2, gas2Data, sizeof(gas2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 3 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS3, gas3Data, sizeof(gas3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 4 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS4, gas4Data, sizeof(gas4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 5 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS5, gas5Data, sizeof(gas5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write set point values + unsigned char sp1Data[2] = { + m_deviceDetails->sp1.sp, + m_deviceDetails->sp1.depth + }; + + unsigned char sp2Data[2] = { + m_deviceDetails->sp2.sp, + m_deviceDetails->sp2.depth + }; + + unsigned char sp3Data[2] = { + m_deviceDetails->sp3.sp, + m_deviceDetails->sp3.depth + }; + + unsigned char sp4Data[2] = { + m_deviceDetails->sp4.sp, + m_deviceDetails->sp4.depth + }; + + unsigned char sp5Data[2] = { + m_deviceDetails->sp5.sp, + m_deviceDetails->sp5.depth + }; + + //sp 1 + rc = hw_ostc3_device_config_write(device, OSTC3_SP1, sp1Data, sizeof(sp1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 2 + rc = hw_ostc3_device_config_write(device, OSTC3_SP2, sp2Data, sizeof(sp2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 3 + rc = hw_ostc3_device_config_write(device, OSTC3_SP3, sp3Data, sizeof(sp3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 4 + rc = hw_ostc3_device_config_write(device, OSTC3_SP4, sp4Data, sizeof(sp4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 5 + rc = hw_ostc3_device_config_write(device, OSTC3_SP5, sp5Data, sizeof(sp5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write dil values + unsigned char dil1Data[4] = { + m_deviceDetails->dil1.oxygen, + m_deviceDetails->dil1.helium, + m_deviceDetails->dil1.type, + m_deviceDetails->dil1.depth + }; + + unsigned char dil2Data[4] = { + m_deviceDetails->dil2.oxygen, + m_deviceDetails->dil2.helium, + m_deviceDetails->dil2.type, + m_deviceDetails->dil2.depth + }; + + unsigned char dil3Data[4] = { + m_deviceDetails->dil3.oxygen, + m_deviceDetails->dil3.helium, + m_deviceDetails->dil3.type, + m_deviceDetails->dil3.depth + }; + + unsigned char dil4Data[4] = { + m_deviceDetails->dil4.oxygen, + m_deviceDetails->dil4.helium, + m_deviceDetails->dil4.type, + m_deviceDetails->dil4.depth + }; + + unsigned char dil5Data[4] = { + m_deviceDetails->dil5.oxygen, + m_deviceDetails->dil5.helium, + m_deviceDetails->dil5.type, + m_deviceDetails->dil5.depth + }; + //dil 1 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL1, dil1Data, sizeof(gas1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 2 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL2, dil2Data, sizeof(dil2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 3 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL3, dil3Data, sizeof(dil3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 4 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL4, dil4Data, sizeof(dil4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 5 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL5, dil5Data, sizeof(dil5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write general settings + //custom text + rc = hw_ostc3_device_customtext(device, m_deviceDetails->customText.toUtf8().data()); + if (rc != DC_STATUS_SUCCESS) + return rc; + + unsigned char data[1] = { 0 }; +#define WRITE_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ + do { \ + data[0] = m_deviceDetails->_DEVICE_DETAIL; \ + rc = hw_ostc3_device_config_write(device, _OSTC3_SETTING, data, sizeof(data)); \ + if (rc != DC_STATUS_SUCCESS) \ + return rc; \ + EMIT_PROGRESS(); \ + } while (0) + + WRITE_SETTING(OSTC3_DIVE_MODE, diveMode); + WRITE_SETTING(OSTC3_SATURATION, saturation); + WRITE_SETTING(OSTC3_DESATURATION, desaturation); + WRITE_SETTING(OSTC3_LAST_DECO, lastDeco); + WRITE_SETTING(OSTC3_BRIGHTNESS, brightness); + WRITE_SETTING(OSTC3_UNITS, units); + WRITE_SETTING(OSTC3_SAMPLING_RATE, samplingRate); + WRITE_SETTING(OSTC3_SALINITY, salinity); + WRITE_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); + WRITE_SETTING(OSTC3_LANGUAGE, language); + WRITE_SETTING(OSTC3_DATE_FORMAT, dateFormat); + WRITE_SETTING(OSTC3_COMPASS_GAIN, compassGain); + WRITE_SETTING(OSTC3_SAFETY_STOP, safetyStop); + WRITE_SETTING(OSTC3_GF_HIGH, gfHigh); + WRITE_SETTING(OSTC3_GF_LOW, gfLow); + WRITE_SETTING(OSTC3_PPO2_MIN, ppO2Min); + WRITE_SETTING(OSTC3_PPO2_MAX, ppO2Max); + WRITE_SETTING(OSTC3_FUTURE_TTS, futureTTS); + WRITE_SETTING(OSTC3_CCR_MODE, ccrMode); + WRITE_SETTING(OSTC3_DECO_TYPE, decoType); + WRITE_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); + WRITE_SETTING(OSTC3_AGF_HIGH, aGFHigh); + WRITE_SETTING(OSTC3_AGF_LOW, aGFLow); + WRITE_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); + WRITE_SETTING(OSTC3_FLIP_SCREEN, flipScreen); + WRITE_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); + WRITE_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); + WRITE_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); + WRITE_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); + WRITE_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); + WRITE_SETTING(OSTC3_MOD_WARNING, modWarning); + WRITE_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); + WRITE_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); + WRITE_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); + WRITE_SETTING(OSTC3_TEMP_SENSOR_OFFSET, tempSensorOffset); + WRITE_SETTING(OSTC3_SAFETY_STOP_LENGTH, safetyStopLength); + WRITE_SETTING(OSTC3_SAFETY_STOP_START_DEPTH, safetyStopStartDepth); + WRITE_SETTING(OSTC3_SAFETY_STOP_END_DEPTH, safetyStopEndDepth); + WRITE_SETTING(OSTC3_SAFETY_STOP_RESET_DEPTH, safetyStopResetDepth); + +#undef WRITE_SETTING + + // OSTC3 stores the pressureSensorOffset in two-complement + data[0] = (unsigned char)m_deviceDetails->pressureSensorOffset; + rc = hw_ostc3_device_config_write(device, OSTC3_PRESSURE_SENSOR_OFFSET, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + // OSTC3 stores the tempSensorOffset in two-complement + data[0] = (unsigned char)m_deviceDetails->pressureSensorOffset; + rc = hw_ostc3_device_config_write(device, OSTC3_TEMP_SENSOR_OFFSET, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //sync date and time + if (m_deviceDetails->syncTime) { + dc_datetime_t now; + dc_datetime_localtime(&now, dc_datetime_now()); + + rc = hw_ostc3_device_clock(device, &now); + } + EMIT_PROGRESS(); + + return rc; +} +#endif /* DC_VERSION_CHECK(0, 5, 0) */ + +static dc_status_t read_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 3; + + unsigned char data[256] = {}; +#ifdef DEBUG_OSTC_CF + // FIXME: how should we report settings not supported back? + unsigned char max_CF = 0; +#endif + rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + m_deviceDetails->serialNo = QString::number(data[1] << 8 ^ data[0]); + m_deviceDetails->numberOfDives = data[3] << 8 ^ data[2]; + //Byte5-6: + //Gas 1 default (%O2=21, %He=0) + gas gas1; + gas1.oxygen = data[6]; + gas1.helium = data[7]; + //Byte9-10: + //Gas 2 default (%O2=21, %He=0) + gas gas2; + gas2.oxygen = data[10]; + gas2.helium = data[11]; + //Byte13-14: + //Gas 3 default (%O2=21, %He=0) + gas gas3; + gas3.oxygen = data[14]; + gas3.helium = data[15]; + //Byte17-18: + //Gas 4 default (%O2=21, %He=0) + gas gas4; + gas4.oxygen = data[18]; + gas4.helium = data[19]; + //Byte21-22: + //Gas 5 default (%O2=21, %He=0) + gas gas5; + gas5.oxygen = data[22]; + gas5.helium = data[23]; + //Byte25-26: + //Gas 6 current (%O2, %He) + m_deviceDetails->salinity = data[26]; + // Active Gas Flag Register + gas1.type = data[27] & 0x01; + gas2.type = (data[27] & 0x02) >> 1; + gas3.type = (data[27] & 0x04) >> 2; + gas4.type = (data[27] & 0x08) >> 3; + gas5.type = (data[27] & 0x10) >> 4; + + // Gas switch depths + gas1.depth = data[28]; + gas2.depth = data[29]; + gas3.depth = data[30]; + gas4.depth = data[31]; + gas5.depth = data[32]; + // 33 which gas is Fist gas + switch (data[33]) { + case 1: + gas1.type = 2; + break; + case 2: + gas2.type = 2; + break; + case 3: + gas3.type = 2; + break; + case 4: + gas4.type = 2; + break; + case 5: + gas5.type = 2; + break; + default: + //Error? + break; + } + // Data filled up, set the gases. + m_deviceDetails->gas1 = gas1; + m_deviceDetails->gas2 = gas2; + m_deviceDetails->gas3 = gas3; + m_deviceDetails->gas4 = gas4; + m_deviceDetails->gas5 = gas5; + m_deviceDetails->decoType = data[34]; + //Byte36: + //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) + //m_deviceDetails->ccrMode = data[35]; + setpoint sp1; + sp1.sp = data[36]; + sp1.depth = 0; + setpoint sp2; + sp2.sp = data[37]; + sp2.depth = 0; + setpoint sp3; + sp3.sp = data[38]; + sp3.depth = 0; + m_deviceDetails->sp1 = sp1; + m_deviceDetails->sp2 = sp2; + m_deviceDetails->sp3 = sp3; + // Byte41-42: + // Lowest Battery voltage seen (in mV) + // Byte43: + // Lowest Battery voltage seen at (Month) + // Byte44: + // Lowest Battery voltage seen at (Day) + // Byte45: + // Lowest Battery voltage seen at (Year) + // Byte46-47: + // Lowest Battery voltage seen at (Temperature in 0.1 °C) + // Byte48: + // Last complete charge at (Month) + // Byte49: + // Last complete charge at (Day) + // Byte50: + // Last complete charge at (Year) + // Byte51-52: + // Total charge cycles + // Byte53-54: + // Total complete charge cycles + // Byte55-56: + // Temperature Extrema minimum (Temperature in 0.1 °C) + // Byte57: + // Temperature Extrema minimum at (Month) + // Byte58: + // Temperature Extrema minimum at (Day) + // Byte59: + // Temperature Extrema minimum at (Year) + // Byte60-61: + // Temperature Extrema maximum (Temperature in 0.1 °C) + // Byte62: + // Temperature Extrema maximum at (Month) + // Byte63: + // Temperature Extrema maximum at (Day) + // Byte64: + // Temperature Extrema maximum at (Year) + // Byte65: + // Custom Text active (=1), Custom Text Disabled (<>1) + // Byte66-90: + // TO FIX EDITOR SYNTAX/INDENT { + // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") + // Example: OSTC Dive Computer} (19 Characters incl. "}") Bytes 85-90 will be ignored. + if (data[64] == 1) { + // Make shure the data is null-terminated + data[89] = 0; + // Find the internal termination and replace it with 0 + char *term = strchr((char *)data + 65, (int)'}'); + if (term) + *term = 0; + m_deviceDetails->customText = (const char *)data + 65; + } + // Byte91: + // Dim OLED in Divemode (>0), Normal mode (=0) + // Byte92: + // Date format for all outputs: + // =0: MM/DD/YY + // =1: DD/MM/YY + // =2: YY/MM/DD + m_deviceDetails->dateFormat = data[91]; +// Byte93: +// Total number of CF used in installed firmware +#ifdef DEBUG_OSTC_CF + max_CF = data[92]; +#endif + // Byte94: + // Last selected view for customview area in surface mode + // Byte95: + // Last selected view for customview area in dive mode + // Byte96-97: + // Diluent 1 Default (%O2,%He) + // Byte98-99: + // Diluent 1 Current (%O2,%He) + gas dil1(data[97], data[98]); + // Byte100-101: + // Gasuent 2 Default (%O2,%He) + // Byte102-103: + // Gasuent 2 Current (%O2,%He) + gas dil2(data[101], data[102]); + // Byte104-105: + // Gasuent 3 Default (%O2,%He) + // Byte106-107: + // Gasuent 3 Current (%O2,%He) + gas dil3(data[105], data[106]); + // Byte108-109: + // Gasuent 4 Default (%O2,%He) + // Byte110-111: + // Gasuent 4 Current (%O2,%He) + gas dil4(data[109], data[110]); + // Byte112-113: + // Gasuent 5 Default (%O2,%He) + // Byte114-115: + // Gasuent 5 Current (%O2,%He) + gas dil5(data[113], data[114]); + // Byte116: + // First Diluent (1-5) + switch (data[115]) { + case 1: + dil1.type = 2; + break; + case 2: + dil2.type = 2; + break; + case 3: + dil3.type = 2; + break; + case 4: + dil4.type = 2; + break; + case 5: + dil5.type = 2; + break; + default: + //Error? + break; + } + m_deviceDetails->dil1 = dil1; + m_deviceDetails->dil2 = dil2; + m_deviceDetails->dil3 = dil3; + m_deviceDetails->dil4 = dil4; + m_deviceDetails->dil5 = dil5; + // Byte117-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF0-CF31) + + // Decode the relevant ones + // CF11: Factor for saturation processes + m_deviceDetails->saturation = read_ostc_cf(data, 11); + // CF12: Factor for desaturation processes + m_deviceDetails->desaturation = read_ostc_cf(data, 12); + // CF17: Lower threshold for ppO2 warning + m_deviceDetails->ppO2Min = read_ostc_cf(data, 17); + // CF18: Upper threshold for ppO2 warning + m_deviceDetails->ppO2Max = read_ostc_cf(data, 18); + // CF20: Depth sampling rate for Profile storage + m_deviceDetails->samplingRate = read_ostc_cf(data, 20); + // CF29: Depth of last decompression stop + m_deviceDetails->lastDeco = read_ostc_cf(data, 29); + +#ifdef DEBUG_OSTC_CF + for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1: + // Logbook version indicator (Not writable!) + // Byte2-3: + // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) + m_deviceDetails->firmwareVersion = QString::number(data[1]) + "." + QString::number(data[2]); + // Byte4: + // OLED brightness (=0: Eco, =1 High) (Not writable!) + // Byte5-11: + // Time/Date vault during firmware updates + // Byte12-128 + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 32-63) + + // Decode the relevant ones + // CF32: Gradient Factor low + m_deviceDetails->gfLow = read_ostc_cf(data, 32); + // CF33: Gradient Factor high + m_deviceDetails->gfHigh = read_ostc_cf(data, 33); + // CF56: Bottom gas consumption + m_deviceDetails->bottomGasConsumption = read_ostc_cf(data, 56); + // CF57: Ascent gas consumption + m_deviceDetails->decoGasConsumption = read_ostc_cf(data, 57); + // CF58: Future time to surface setFutureTTS + m_deviceDetails->futureTTS = read_ostc_cf(data, 58); + +#ifdef DEBUG_OSTC_CF + for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1-4: + // not used/reserved (Not writable!) + // Byte5-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 64-95) + + // Decode the relevant ones + // CF60: Graphic velocity + m_deviceDetails->graphicalSpeedIndicator = read_ostc_cf(data, 60); + // CF65: Show safety stop + m_deviceDetails->safetyStop = read_ostc_cf(data, 65); + // CF67: Alternaitve Gradient Factor low + m_deviceDetails->aGFLow = read_ostc_cf(data, 67); + // CF68: Alternative Gradient Factor high + m_deviceDetails->aGFHigh = read_ostc_cf(data, 68); + // CF69: Allow Gradient Factor change + m_deviceDetails->aGFSelectable = read_ostc_cf(data, 69); + // CF70: Safety Stop Duration [s] + m_deviceDetails->safetyStopLength = read_ostc_cf(data, 70); + // CF71: Safety Stop Start Depth [m] + m_deviceDetails->safetyStopStartDepth = read_ostc_cf(data, 71); + // CF72: Safety Stop End Depth [m] + m_deviceDetails->safetyStopEndDepth = read_ostc_cf(data, 72); + // CF73: Safety Stop Reset Depth [m] + m_deviceDetails->safetyStopResetDepth = read_ostc_cf(data, 73); + // CF74: Battery Timeout [min] + +#ifdef DEBUG_OSTC_CF + for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + return rc; +} + +static dc_status_t write_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 7; + unsigned char data[256] = {}; + unsigned char max_CF = 0; + + // Because we write whole memory blocks, we read all the current + // values out and then change then ones we should change. + rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //Byte5-6: + //Gas 1 default (%O2=21, %He=0) + gas gas1 = m_deviceDetails->gas1; + data[6] = gas1.oxygen; + data[7] = gas1.helium; + //Byte9-10: + //Gas 2 default (%O2=21, %He=0) + gas gas2 = m_deviceDetails->gas2; + data[10] = gas2.oxygen; + data[11] = gas2.helium; + //Byte13-14: + //Gas 3 default (%O2=21, %He=0) + gas gas3 = m_deviceDetails->gas3; + data[14] = gas3.oxygen; + data[15] = gas3.helium; + //Byte17-18: + //Gas 4 default (%O2=21, %He=0) + gas gas4 = m_deviceDetails->gas4; + data[18] = gas4.oxygen; + data[19] = gas4.helium; + //Byte21-22: + //Gas 5 default (%O2=21, %He=0) + gas gas5 = m_deviceDetails->gas5; + data[22] = gas5.oxygen; + data[23] = gas5.helium; + //Byte25-26: + //Gas 6 current (%O2, %He) + data[26] = m_deviceDetails->salinity; + // Gas types, 0=Disabled, 1=Active, 2=Fist + // Active Gas Flag Register + data[27] = 0; + if (gas1.type) + data[27] ^= 0x01; + if (gas2.type) + data[27] ^= 0x02; + if (gas3.type) + data[27] ^= 0x04; + if (gas4.type) + data[27] ^= 0x08; + if (gas5.type) + data[27] ^= 0x10; + + // Gas switch depths + data[28] = gas1.depth; + data[29] = gas2.depth; + data[30] = gas3.depth; + data[31] = gas4.depth; + data[32] = gas5.depth; + // 33 which gas is Fist gas + if (gas1.type == 2) + data[33] = 1; + else if (gas2.type == 2) + data[33] = 2; + else if (gas3.type == 2) + data[33] = 3; + else if (gas4.type == 2) + data[33] = 4; + else if (gas5.type == 2) + data[33] = 5; + else + // FIXME: No gas was First? + // Set gas 1 to first + data[33] = 1; + + data[34] = m_deviceDetails->decoType; + //Byte36: + //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) + //m_deviceDetails->ccrMode = data[35]; + data[36] = m_deviceDetails->sp1.sp; + data[37] = m_deviceDetails->sp2.sp; + data[38] = m_deviceDetails->sp3.sp; + // Byte41-42: + // Lowest Battery voltage seen (in mV) + // Byte43: + // Lowest Battery voltage seen at (Month) + // Byte44: + // Lowest Battery voltage seen at (Day) + // Byte45: + // Lowest Battery voltage seen at (Year) + // Byte46-47: + // Lowest Battery voltage seen at (Temperature in 0.1 °C) + // Byte48: + // Last complete charge at (Month) + // Byte49: + // Last complete charge at (Day) + // Byte50: + // Last complete charge at (Year) + // Byte51-52: + // Total charge cycles + // Byte53-54: + // Total complete charge cycles + // Byte55-56: + // Temperature Extrema minimum (Temperature in 0.1 °C) + // Byte57: + // Temperature Extrema minimum at (Month) + // Byte58: + // Temperature Extrema minimum at (Day) + // Byte59: + // Temperature Extrema minimum at (Year) + // Byte60-61: + // Temperature Extrema maximum (Temperature in 0.1 °C) + // Byte62: + // Temperature Extrema maximum at (Month) + // Byte63: + // Temperature Extrema maximum at (Day) + // Byte64: + // Temperature Extrema maximum at (Year) + // Byte65: + // Custom Text active (=1), Custom Text Disabled (<>1) + // Byte66-90: + // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") + // Example: "OSTC Dive Computer}" (19 Characters incl. "}") Bytes 85-90 will be ignored. + if (m_deviceDetails->customText == "") { + data[64] = 0; + } else { + data[64] = 1; + // Copy the string to the right place in the memory, padded with 0x20 (" ") + strncpy((char *)data + 65, QString("%1").arg(m_deviceDetails->customText, -23, QChar(' ')).toUtf8().data(), 23); + // And terminate the string. + if (m_deviceDetails->customText.length() <= 23) + data[65 + m_deviceDetails->customText.length()] = '}'; + else + data[90] = '}'; + } + // Byte91: + // Dim OLED in Divemode (>0), Normal mode (=0) + // Byte92: + // Date format for all outputs: + // =0: MM/DD/YY + // =1: DD/MM/YY + // =2: YY/MM/DD + data[91] = m_deviceDetails->dateFormat; + // Byte93: + // Total number of CF used in installed firmware + max_CF = data[92]; + // Byte94: + // Last selected view for customview area in surface mode + // Byte95: + // Last selected view for customview area in dive mode + // Byte96-97: + // Diluent 1 Default (%O2,%He) + // Byte98-99: + // Diluent 1 Current (%O2,%He) + gas dil1 = m_deviceDetails->dil1; + data[97] = dil1.oxygen; + data[98] = dil1.helium; + // Byte100-101: + // Gasuent 2 Default (%O2,%He) + // Byte102-103: + // Gasuent 2 Current (%O2,%He) + gas dil2 = m_deviceDetails->dil2; + data[101] = dil2.oxygen; + data[102] = dil2.helium; + // Byte104-105: + // Gasuent 3 Default (%O2,%He) + // Byte106-107: + // Gasuent 3 Current (%O2,%He) + gas dil3 = m_deviceDetails->dil3; + data[105] = dil3.oxygen; + data[106] = dil3.helium; + // Byte108-109: + // Gasuent 4 Default (%O2,%He) + // Byte110-111: + // Gasuent 4 Current (%O2,%He) + gas dil4 = m_deviceDetails->dil4; + data[109] = dil4.oxygen; + data[110] = dil4.helium; + // Byte112-113: + // Gasuent 5 Default (%O2,%He) + // Byte114-115: + // Gasuent 5 Current (%O2,%He) + gas dil5 = m_deviceDetails->dil5; + data[113] = dil5.oxygen; + data[114] = dil5.helium; + // Byte116: + // First Diluent (1-5) + if (dil1.type == 2) + data[115] = 1; + else if (dil2.type == 2) + data[115] = 2; + else if (dil3.type == 2) + data[115] = 3; + else if (dil4.type == 2) + data[115] = 4; + else if (dil5.type == 2) + data[115] = 5; + else + // FIXME: No first diluent? + // Set gas 1 to fist + data[115] = 1; + + // Byte117-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF0-CF31) + + // Write the relevant ones + // CF11: Factor for saturation processes + write_ostc_cf(data, 11, max_CF, m_deviceDetails->saturation); + // CF12: Factor for desaturation processes + write_ostc_cf(data, 12, max_CF, m_deviceDetails->desaturation); + // CF17: Lower threshold for ppO2 warning + write_ostc_cf(data, 17, max_CF, m_deviceDetails->ppO2Min); + // CF18: Upper threshold for ppO2 warning + write_ostc_cf(data, 18, max_CF, m_deviceDetails->ppO2Max); + // CF20: Depth sampling rate for Profile storage + write_ostc_cf(data, 20, max_CF, m_deviceDetails->samplingRate); + // CF29: Depth of last decompression stop + write_ostc_cf(data, 29, max_CF, m_deviceDetails->lastDeco); + +#ifdef DEBUG_OSTC_CF + for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1: + // Logbook version indicator (Not writable!) + // Byte2-3: + // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) + // Byte4: + // OLED brightness (=0: Eco, =1 High) (Not writable!) + // Byte5-11: + // Time/Date vault during firmware updates + // Byte12-128 + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 32-63) + + // Decode the relevant ones + // CF32: Gradient Factor low + write_ostc_cf(data, 32, max_CF, m_deviceDetails->gfLow); + // CF33: Gradient Factor high + write_ostc_cf(data, 33, max_CF, m_deviceDetails->gfHigh); + // CF56: Bottom gas consumption + write_ostc_cf(data, 56, max_CF, m_deviceDetails->bottomGasConsumption); + // CF57: Ascent gas consumption + write_ostc_cf(data, 57, max_CF, m_deviceDetails->decoGasConsumption); + // CF58: Future time to surface setFutureTTS + write_ostc_cf(data, 58, max_CF, m_deviceDetails->futureTTS); +#ifdef DEBUG_OSTC_CF + for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1-4: + // not used/reserved (Not writable!) + // Byte5-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 64-95) + + // Decode the relevant ones + // CF60: Graphic velocity + write_ostc_cf(data, 60, max_CF, m_deviceDetails->graphicalSpeedIndicator); + // CF65: Show safety stop + write_ostc_cf(data, 65, max_CF, m_deviceDetails->safetyStop); + // CF67: Alternaitve Gradient Factor low + write_ostc_cf(data, 67, max_CF, m_deviceDetails->aGFLow); + // CF68: Alternative Gradient Factor high + write_ostc_cf(data, 68, max_CF, m_deviceDetails->aGFHigh); + // CF69: Allow Gradient Factor change + write_ostc_cf(data, 69, max_CF, m_deviceDetails->aGFSelectable); + // CF70: Safety Stop Duration [s] + write_ostc_cf(data, 70, max_CF, m_deviceDetails->safetyStopLength); + // CF71: Safety Stop Start Depth [m] + write_ostc_cf(data, 71, max_CF, m_deviceDetails->safetyStopStartDepth); + // CF72: Safety Stop End Depth [m] + write_ostc_cf(data, 72, max_CF, m_deviceDetails->safetyStopEndDepth); + // CF73: Safety Stop Reset Depth [m] + write_ostc_cf(data, 73, max_CF, m_deviceDetails->safetyStopResetDepth); + // CF74: Battery Timeout [min] + +#ifdef DEBUG_OSTC_CF + for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //sync date and time + if (m_deviceDetails->syncTime) { + QDateTime timeToSet = QDateTime::currentDateTime(); + dc_datetime_t time; + time.year = timeToSet.date().year(); + time.month = timeToSet.date().month(); + time.day = timeToSet.date().day(); + time.hour = timeToSet.time().hour(); + time.minute = timeToSet.time().minute(); + time.second = timeToSet.time().second(); + rc = hw_ostc_device_clock(device, &time); + } + EMIT_PROGRESS(); + return rc; +} + +#undef EMIT_PROGRESS + +DeviceThread::DeviceThread(QObject *parent, device_data_t *data) : QThread(parent), m_data(data) +{ +} + +void DeviceThread::progressCB(int percent) +{ + emit progress(percent); +} + +void DeviceThread::event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) +{ + Q_UNUSED(device); + + const dc_event_progress_t *progress = (dc_event_progress_t *) data; + DeviceThread *dt = static_cast<DeviceThread*>(userdata); + + switch (event) { + case DC_EVENT_PROGRESS: + dt->progressCB(100.0 * (double)progress->current / (double)progress->maximum); + break; + default: + emit dt->error("Unexpected event recived"); + break; + } +} + +ReadSettingsThread::ReadSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) +{ +} + +void ReadSettingsThread::run() +{ + dc_status_t rc; + + DeviceDetails *m_deviceDetails = new DeviceDetails(0); + switch (dc_device_get_type(m_data->device)) { + case DC_FAMILY_SUUNTO_VYPER: + rc = read_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) { + emit devicedetails(m_deviceDetails); + } else if (rc == DC_STATUS_UNSUPPORTED) { + emit error(tr("This feature is not yet available for the selected dive computer.")); + } else { + emit error("Failed!"); + } + break; +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = read_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) + emit devicedetails(m_deviceDetails); + else + emit error("Failed!"); + break; +#endif // divecomputer 0.5.0 +#ifdef DEBUG_OSTC + case DC_FAMILY_NULL: +#endif + case DC_FAMILY_HW_OSTC: + rc = read_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) + emit devicedetails(m_deviceDetails); + else + emit error("Failed!"); + break; + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + break; + } +} + +WriteSettingsThread::WriteSettingsThread(QObject *parent, device_data_t *data) : + DeviceThread(parent, data), + m_deviceDetails(NULL) +{ +} + +void WriteSettingsThread::setDeviceDetails(DeviceDetails *details) +{ + m_deviceDetails = details; +} + +void WriteSettingsThread::run() +{ + dc_status_t rc; + + switch (dc_device_get_type(m_data->device)) { + case DC_FAMILY_SUUNTO_VYPER: + rc = write_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_UNSUPPORTED) { + emit error(tr("This feature is not yet available for the selected dive computer.")); + } else if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Failed!")); + } + break; +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = write_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) + emit error(tr("Failed!")); + break; +#endif // divecomputer 0.5.0 +#ifdef DEBUG_OSTC + case DC_FAMILY_NULL: +#endif + case DC_FAMILY_HW_OSTC: + rc = write_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) + emit error(tr("Failed!")); + break; + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + break; + } +} + + +FirmwareUpdateThread::FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName) : DeviceThread(parent, data), m_fileName(fileName) +{ +} + +void FirmwareUpdateThread::run() +{ + dc_status_t rc; + + rc = dc_device_set_events(m_data->device, DC_EVENT_PROGRESS, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) { + emit error("Error registering the event handler."); + return; + } + switch (dc_device_get_type(m_data->device)) { +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = hw_ostc3_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); + break; + case DC_FAMILY_HW_OSTC: + rc = hw_ostc_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); + break; +#endif // divecomputer 0.5.0 + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + return; + } + + if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Firmware update failed!")); + } +} + + +ResetSettingsThread::ResetSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) +{ +} + +void ResetSettingsThread::run() +{ + dc_status_t rc = DC_STATUS_SUCCESS; + +#if DC_VERSION_CHECK(0, 5, 0) + if (dc_device_get_type(m_data->device) == DC_FAMILY_HW_OSTC3) { + rc = hw_ostc3_device_config_reset(m_data->device); + emit progress(100); + } +#endif // divecomputer 0.5.0 + if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Reset settings failed!")); + } +} diff --git a/core/configuredivecomputerthreads.h b/core/configuredivecomputerthreads.h new file mode 100644 index 000000000..8817d848a --- /dev/null +++ b/core/configuredivecomputerthreads.h @@ -0,0 +1,60 @@ +#ifndef CONFIGUREDIVECOMPUTERTHREADS_H +#define CONFIGUREDIVECOMPUTERTHREADS_H + +#include <QObject> +#include <QThread> +#include "libdivecomputer.h" +#include "devicedetails.h" + +class DeviceThread : public QThread { + Q_OBJECT +public: + DeviceThread(QObject *parent, device_data_t *data); + virtual void run() = 0; +signals: + void error(QString err); + void progress(int value); +protected: + device_data_t *m_data; + void progressCB(int value); + static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata); +}; + +class ReadSettingsThread : public DeviceThread { + Q_OBJECT +public: + ReadSettingsThread(QObject *parent, device_data_t *data); + void run(); +signals: + void devicedetails(DeviceDetails *newDeviceDetails); +}; + +class WriteSettingsThread : public DeviceThread { + Q_OBJECT +public: + WriteSettingsThread(QObject *parent, device_data_t *data); + void setDeviceDetails(DeviceDetails *details); + void run(); + +private: + DeviceDetails *m_deviceDetails; +}; + +class FirmwareUpdateThread : public DeviceThread { + Q_OBJECT +public: + FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName); + void run(); + +private: + QString m_fileName; +}; + +class ResetSettingsThread : public DeviceThread { + Q_OBJECT +public: + ResetSettingsThread(QObject *parent, device_data_t *data); + void run(); +}; + +#endif // CONFIGUREDIVECOMPUTERTHREADS_H diff --git a/core/datatrak.c b/core/datatrak.c new file mode 100644 index 000000000..204ebd9b3 --- /dev/null +++ b/core/datatrak.c @@ -0,0 +1,698 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <time.h> + +#include "datatrak.h" +#include "dive.h" +#include "units.h" +#include "device.h" +#include "gettext.h" + +extern struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc); + +unsigned char lector_bytes[2], lector_word[4], tmp_1byte, *byte; +unsigned int tmp_2bytes; +char is_nitrox, is_O2, is_SCR; +unsigned long tmp_4bytes; + +static unsigned int two_bytes_to_int(unsigned char x, unsigned char y) +{ + return (x << 8) + y; +} + +static unsigned long four_bytes_to_long(unsigned char x, unsigned char y, unsigned char z, unsigned char t) +{ + return ((long)x << 24) + ((long)y << 16) + ((long)z << 8) + (long)t; +} + +static unsigned char *byte_to_bits(unsigned char byte) +{ + unsigned char i, *bits = (unsigned char *)malloc(8); + + for (i = 0; i < 8; i++) + bits[i] = byte & (1 << i); + return bits; +} + +/* + * Datatrak stores the date in days since 01-01-1600, while Subsurface uses + * time_t (seconds since 00:00 01-01-1970). Function subtracts + * (1970 - 1600) * 365,2425 = 135139,725 to our date variable, getting the + * days since Epoch. + */ +static time_t date_time_to_ssrfc(unsigned long date, int time) +{ + time_t tmp; + tmp = (date - 135140) * 86400 + time * 60; + return tmp; +} + +static unsigned char to_8859(unsigned char char_cp850) +{ + static const unsigned char char_8859[46] = { 0xc7, 0xfc, 0xe9, 0xe2, 0xe4, 0xe0, 0xe5, 0xe7, + 0xea, 0xeb, 0xe8, 0xef, 0xee, 0xec, 0xc4, 0xc5, + 0xc9, 0xe6, 0xc6, 0xf4, 0xf6, 0xf2, 0xfb, 0xf9, + 0xff, 0xd6, 0xdc, 0xf8, 0xa3, 0xd8, 0xd7, 0x66, + 0xe1, 0xed, 0xf3, 0xfa, 0xf1, 0xd1, 0xaa, 0xba, + 0xbf, 0xae, 0xac, 0xbd, 0xbc, 0xa1 }; + return char_8859[char_cp850 - 0x80]; +} + +static char *to_utf8(unsigned char *in_string) +{ + int outlen, inlen, i = 0, j = 0; + inlen = strlen((char *)in_string); + outlen = inlen * 2 + 1; + + char *out_string = calloc(outlen, 1); + for (i = 0; i < inlen; i++) { + if (in_string[i] < 127) + out_string[j] = in_string[i]; + else { + if (in_string[i] > 127 && in_string[i] <= 173) + in_string[i] = to_8859(in_string[i]); + out_string[j] = (in_string[i] >> 6) | 0xC0; + j++; + out_string[j] = (in_string[i] & 0x3F) | 0x80; + } + j++; + } + out_string[j + 1] = '\0'; + return out_string; +} + +/* + * Subsurface sample structure doesn't support the flags and alarms in the dt .log + * so will treat them as dc events. + */ +static struct sample *dtrak_profile(struct dive *dt_dive, FILE *archivo) +{ + int i, j = 1, interval, o2percent = dt_dive->cylinder[0].gasmix.o2.permille / 10; + struct sample *sample = dt_dive->dc.sample; + struct divecomputer *dc = &dt_dive->dc; + + for (i = 1; i <= dt_dive->dc.alloc_samples; i++) { + if (fread(&lector_bytes, 1, 2, archivo) != 2) + return sample; + interval= 20 * (i + 1); + sample = add_sample(sample, interval, dc); + sample->depth.mm = (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0xFFC0) * 1000 / 410; + byte = byte_to_bits(two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0x003F); + if (byte[0] != 0) + sample->in_deco = true; + else + sample->in_deco = false; + if (byte[1] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "rbt"); + if (byte[2] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "ascent"); + if (byte[3] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "ceiling"); + if (byte[4] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "workload"); + if (byte[5] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "transmitter"); + if (j == 3) { + read_bytes(1); + if (is_O2) { + read_bytes(1); + o2percent = tmp_1byte; + } + j = 0; + } + free(byte); + + // In commit 5f44fdd setpoint replaced po2, so although this is not necessarily CCR dive ... + if (is_O2) + sample->setpoint.mbar = calculate_depth_to_mbar(sample->depth.mm, dt_dive->surface_pressure, 0) * o2percent / 100; + j++; + } +bail: + return sample; +} + +/* + * Reads the header of a file and returns the header struct + * If it's not a DATATRAK file returns header zero initalized + */ +static dtrakheader read_file_header(FILE *archivo) +{ + dtrakheader fileheader = { 0 }; + const short headerbytes = 12; + unsigned char *lector = (unsigned char *)malloc(headerbytes); + + if (fread(lector, 1, headerbytes, archivo) != headerbytes) { + free(lector); + return fileheader; + } + if (two_bytes_to_int(lector[0], lector[1]) != 0xA100) { + report_error(translate("gettextFromC", "Error: the file does not appear to be a DATATRAK divelog")); + free(lector); + return fileheader; + } + fileheader.header = (lector[0] << 8) + lector[1]; + fileheader.dc_serial_1 = two_bytes_to_int(lector[2], lector[3]); + fileheader.dc_serial_2 = two_bytes_to_int(lector[4], lector[5]); + fileheader.divesNum = two_bytes_to_int(lector[7], lector[6]); + free(lector); + return fileheader; +} + +#define CHECK(_func, _val) if ((_func) != (_val)) goto bail + +/* + * Parses the dive extracting its data and filling a subsurface's dive structure + */ +bool dt_dive_parser(FILE *archivo, struct dive *dt_dive) +{ + unsigned char n; + int profile_length; + char *tmp_notes_str = NULL; + unsigned char *tmp_string1 = NULL, + *locality = NULL, + *dive_point = NULL; + char buffer[1024]; + struct divecomputer *dc = &dt_dive->dc; + + is_nitrox = is_O2 = is_SCR = 0; + + /* + * Parse byte to byte till next dive entry + */ + n = 0; + CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); + while (lector_bytes[n] != 0xA0) + CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); + + /* + * Found dive header 0xA000, verify second byte + */ + CHECK(fread(&lector_bytes[n+1], 1, 1, archivo), 1); + if (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) != 0xA000) { + printf("Error: byte = %4x\n", two_bytes_to_int(lector_bytes[0], lector_bytes[1])); + return false; + } + + /* + * Begin parsing + * First, Date of dive, 4 bytes + */ + read_bytes(4); + + + /* + * Next, Time in minutes since 00:00 + */ + read_bytes(2); + + dt_dive->dc.when = dt_dive->when = (timestamp_t)date_time_to_ssrfc(tmp_4bytes, tmp_2bytes); + + /* + * Now, Locality, 1st byte is long of string, rest is string + */ + read_bytes(1); + read_string(locality); + + /* + * Next, Dive point, defined as Locality + */ + read_bytes(1); + read_string(dive_point); + + /* + * Subsurface only have a location variable, so we have to merge DTrak's + * Locality and Dive points. + */ + snprintf(buffer, sizeof(buffer), "%s, %s", locality, dive_point); + dt_dive->dive_site_uuid = get_dive_site_uuid_by_name(buffer, NULL); + if (dt_dive->dive_site_uuid == 0) + dt_dive->dive_site_uuid = create_dive_site(buffer, dt_dive->when); + free(locality); + free(dive_point); + + /* + * Altitude. Don't exist in Subsurface, the equivalent would be + * surface air pressure which can, be calculated from altitude. + * As dtrak registers altitude intervals, we, arbitrarily, choose + * the lower altitude/pressure equivalence for each segment. So + * + * Datatrak table * Conversion formula: + * * + * byte = 1 0 - 700 m * P = P0 * exp(-(g * M * h ) / (R * T0)) + * byte = 2 700 - 1700m * P0 = sealevel pressure = 101325 Pa + * byte = 3 1700 - 2700 m * g = grav. acceleration = 9,80665 m/s² + * byte = 4 2700 - * m * M = molar mass (dry air) = 0,0289644 Kg/mol + * * h = altitude over sea level (m) + * * R = Universal gas constant = 8,31447 J/(mol*K) + * * T0 = sea level standard temperature = 288,15 K + */ + read_bytes(1); + switch (tmp_1byte) { + case 1: + dt_dive->dc.surface_pressure.mbar = 1013; + break; + case 2: + dt_dive->dc.surface_pressure.mbar = 932; + break; + case 3: + dt_dive->dc.surface_pressure.mbar = 828; + break; + case 4: + dt_dive->dc.surface_pressure.mbar = 735; + break; + default: + dt_dive->dc.surface_pressure.mbar = 1013; + } + + /* + * Interval (minutes) + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->dc.surfacetime.seconds = (uint32_t) tmp_2bytes * 60; + + /* + * Weather, values table, 0 to 6 + * Subsurface don't have this record but we can use tags + */ + dt_dive->tag_list = NULL; + read_bytes(1); + switch (tmp_1byte) { + case 1: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "clear"))); + break; + case 2: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "misty"))); + break; + case 3: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fog"))); + break; + case 4: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "rain"))); + break; + case 5: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "storm"))); + break; + case 6: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "snow"))); + break; + default: + // unknown, do nothing + break; + } + + /* + * Air Temperature + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->dc.airtemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); + + /* + * Dive suit, values table, 0 to 6 + */ + read_bytes(1); + switch (tmp_1byte) { + case 1: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "No suit")); + break; + case 2: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Shorty")); + break; + case 3: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Combi")); + break; + case 4: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Wet suit")); + break; + case 5: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Semidry suit")); + break; + case 6: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Dry suit")); + break; + default: + // unknown, do nothing + break; + } + + /* + * Tank, volume size in liter*100. And initialize gasmix to air (default). + * Dtrak don't record init and end pressures, but consumed bar, so let's + * init a default pressure of 200 bar. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) { + dt_dive->cylinder[0].type.size.mliter = tmp_2bytes * 10; + dt_dive->cylinder[0].type.description = strdup(""); + dt_dive->cylinder[0].start.mbar = 200000; + dt_dive->cylinder[0].gasmix.he.permille = 0; + dt_dive->cylinder[0].gasmix.o2.permille = 210; + dt_dive->cylinder[0].manually_added = true; + } + + /* + * Maximum depth, in cm. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->maxdepth.mm = dt_dive->dc.maxdepth.mm = (int32_t)tmp_2bytes * 10; + + /* + * Dive time in minutes. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->duration.seconds = dt_dive->dc.duration.seconds = (uint32_t)tmp_2bytes * 60; + + /* + * Minimum water temperature in C*100. If unknown, set it to 0K which + * is subsurface's value for "unknown" + */ + read_bytes(2); + if (tmp_2bytes != 0x7fff) + dt_dive->watertemp.mkelvin = dt_dive->dc.watertemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); + else + dt_dive->watertemp.mkelvin = 0; + + /* + * Air used in bar*100. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF && dt_dive->cylinder[0].type.size.mliter) + dt_dive->cylinder[0].gas_used.mliter = dt_dive->cylinder[0].type.size.mliter * (tmp_2bytes / 100.0); + + /* + * Dive Type 1 - Bit table. Subsurface don't have this record, but + * will use tags. Bits 0 and 1 are not used. Reuse coincident tags. + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "no stop"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "deco"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "single ascent"))); + if (byte[5] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "multiple ascent"))); + if (byte[6] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fresh"))); + if (byte[7] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "salt water"))); + free(byte); + + /* + * Dive Type 2 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) { + taglist_add_tag(&dt_dive->tag_list, strdup("nitrox")); + is_nitrox = 1; + } + if (byte[1] != 0) { + taglist_add_tag(&dt_dive->tag_list, strdup("rebreather")); + is_SCR = 1; + dt_dive->dc.divemode = PSCR; + } + free(byte); + + /* + * Dive Activity 1 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "sight seeing"))); + if (byte[1] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "club dive"))); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instructor"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instruction"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "night"))); + if (byte[5] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "cave"))); + if (byte[6] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "ice"))); + if (byte[7] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "search"))); + free(byte); + + + /* + * Dive Activity 2 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "wreck"))); + if (byte[1] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "river"))); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "drift"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "photo"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "other"))); + free(byte); + + /* + * Other activities - String 1st byte = long + * Will put this in dive notes before the true notes + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + snprintf(buffer, sizeof(buffer), "%s: %s\n", + QT_TRANSLATE_NOOP("gettextFromC", "Other activities"), + tmp_string1); + tmp_notes_str = strdup(buffer); + free(tmp_string1); + } + + /* + * Dive buddies + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + dt_dive->buddy = strdup((char *)tmp_string1); + free(tmp_string1); + } + + /* + * Dive notes + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + int len = snprintf(buffer, sizeof(buffer), "%s%s:\n%s", + tmp_notes_str ? tmp_notes_str : "", + QT_TRANSLATE_NOOP("gettextFromC", "Datatrak/Wlog notes"), + tmp_string1); + dt_dive->notes = calloc((len +1), 1); + dt_dive->notes = memcpy(dt_dive->notes, buffer, len); + free(tmp_string1); + if (tmp_notes_str != NULL) + free(tmp_notes_str); + } + + /* + * Alarms 1 - Bit table - Not in Subsurface, we use the profile + */ + read_bytes(1); + + /* + * Alarms 2 - Bit table - Not in Subsurface, we use the profile + */ + read_bytes(1); + + /* + * Dive number (in datatrak, after import user has to renumber) + */ + read_bytes(2); + dt_dive->number = tmp_2bytes; + + /* + * Computer timestamp - Useless for Subsurface + */ + read_bytes(4); + + /* + * Model - table - Not included 0x14, 0x24, 0x41, and 0x73 + * known to exist, but not its model name - To add in the future. + * Strangely 0x00 serves for manually added dives and a dc too, at + * least in EXAMPLE.LOG file, shipped with the software. + */ + read_bytes(1); + switch (tmp_1byte) { + case 0x00: + dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Manually entered dive")); + break; + case 0x1C: + dt_dive->dc.model = strdup("Aladin Air"); + break; + case 0x1D: + dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); + break; + case 0x1E: + dt_dive->dc.model = strdup("Aladin Sport"); + break; + case 0x1F: + dt_dive->dc.model = strdup("Aladin Pro"); + break; + case 0x34: + dt_dive->dc.model = strdup("Aladin Air X"); + break; + case 0x3D: + dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); + break; + case 0x3F: + dt_dive->dc.model = strdup("Mares Genius"); + break; + case 0x44: + dt_dive->dc.model = strdup("Aladin Air X"); + break; + case 0x48: + dt_dive->dc.model = strdup("Spiro Monitor 3 Air"); + break; + case 0xA4: + dt_dive->dc.model = strdup("Aladin Air X O2"); + break; + case 0xB1: + dt_dive->dc.model = strdup("Citizen Hyper Aqualand"); + break; + case 0xB2: + dt_dive->dc.model = strdup("Citizen ProMaster"); + break; + case 0xB3: + dt_dive->dc.model = strdup("Mares Guardian"); + break; + case 0xBC: + dt_dive->dc.model = strdup("Aladin Air X Nitrox"); + break; + case 0xF4: + dt_dive->dc.model = strdup("Aladin Air X Nitrox"); + break; + case 0xFF: + dt_dive->dc.model = strdup("Aladin Pro Nitrox"); + break; + default: + dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Unknown")); + break; + } + if ((tmp_1byte & 0xF0) == 0xF0) + is_nitrox = 1; + if ((tmp_1byte & 0xF0) == 0xA0) + is_O2 = 1; + + /* + * Air usage, unknown use. Probably allows or deny manually entering gas + * comsumption based on dc model - Useless for Subsurface + */ + read_bytes(1); + if (fseek(archivo, 6, 1) != 0) // jump over 6 bytes whitout known use + goto bail; + /* + * Profile data length + */ + read_bytes(2); + profile_length = tmp_2bytes; + if (profile_length != 0) { + /* + * 8 x 2 bytes for the tissues saturation useless for subsurface + * and other 6 bytes without known use + */ + if (fseek(archivo, 22, 1) != 0) + goto bail; + if (is_nitrox || is_O2) { + + /* + * CNS % (unsure) values table (only nitrox computers) + */ + read_bytes(1); + + /* + * % O2 in nitrox mix - (only nitrox and O2 computers but differents) + */ + read_bytes(1); + if (is_nitrox) { + dt_dive->cylinder[0].gasmix.o2.permille = + (tmp_1byte & 0x0F ? 20.0 + 2 * (tmp_1byte & 0x0F) : 21.0) * 10; + } else { + dt_dive->cylinder[0].gasmix.o2.permille = tmp_1byte * 10; + read_bytes(1) // Jump over one byte, unknown use + } + } + /* + * profileLength = Nº bytes, need to know how many samples are there. + * 2bytes per sample plus another one each three samples. Also includes the + * bytes jumped over (22) and the nitrox (2) or O2 (3). + */ + int samplenum = is_O2 ? (profile_length - 25) * 3 / 8 : (profile_length - 24) * 3 / 7; + + dc->events = calloc(samplenum, sizeof(struct event)); + dc->alloc_samples = samplenum; + dc->samples = 0; + dc->sample = calloc(samplenum, sizeof(struct sample)); + + dtrak_profile(dt_dive, archivo); + } + /* + * Initialize some dive data not supported by Datatrak/WLog + */ + if (!strcmp(dt_dive->dc.model, "Manually entered dive")) + dt_dive->dc.deviceid = 0; + else + dt_dive->dc.deviceid = 0xffffffff; + create_device_node(dt_dive->dc.model, dt_dive->dc.deviceid, "", "", dt_dive->dc.model); + dt_dive->dc.next = NULL; + if (!is_SCR && dt_dive->cylinder[0].type.size.mliter) { + dt_dive->cylinder[0].end.mbar = dt_dive->cylinder[0].start.mbar - + ((dt_dive->cylinder[0].gas_used.mliter / dt_dive->cylinder[0].type.size.mliter) * 1000); + } + return true; + +bail: + return false; +} + +void datatrak_import(const char *file, struct dive_table *table) +{ + FILE *archivo; + dtrakheader *fileheader = (dtrakheader *)malloc(sizeof(dtrakheader)); + int i = 0; + + if ((archivo = subsurface_fopen(file, "rb")) == NULL) { + report_error(translate("gettextFromC", "Error: couldn't open the file %s"), file); + free(fileheader); + return; + } + + /* + * Verify fileheader, get number of dives in datatrak divelog + */ + *fileheader = read_file_header(archivo); + while (i < fileheader->divesNum) { + struct dive *ptdive = alloc_dive(); + + if (!dt_dive_parser(archivo, ptdive)) { + report_error(translate("gettextFromC", "Error: no dive")); + free(ptdive); + } else { + record_dive(ptdive); + } + i++; + } + taglist_cleanup(&g_tag_list); + fclose(archivo); + sort_table(table); + free(fileheader); +} diff --git a/core/datatrak.h b/core/datatrak.h new file mode 100644 index 000000000..3a37e0465 --- /dev/null +++ b/core/datatrak.h @@ -0,0 +1,41 @@ +#ifndef DATATRAK_HEADER_H +#define DATATRAK_HEADER_H + +#include <string.h> + +typedef struct dtrakheader_ { + int header; //Must be 0xA100; + int divesNum; + int dc_serial_1; + int dc_serial_2; +} dtrakheader; + +#define read_bytes(_n) \ + switch (_n) { \ + case 1: \ + if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_1byte = lector_bytes[0]; \ + break; \ + case 2: \ + if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_2bytes = two_bytes_to_int (lector_bytes[1], lector_bytes[0]); \ + break; \ + default: \ + if (fread (&lector_word, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_4bytes = four_bytes_to_long(lector_word[3], lector_word[2], lector_word[1], lector_word[0]); \ + break; \ + } + +#define read_string(_property) \ + unsigned char *_property##tmp = (unsigned char *)calloc(tmp_1byte + 1, 1); \ + if (fread((char *)_property##tmp, 1, tmp_1byte, archivo) != tmp_1byte) { \ + free(_property##tmp); \ + goto bail; \ + } \ + _property = (unsigned char *)strcat(to_utf8(_property##tmp), ""); \ + free(_property##tmp); + +#endif // DATATRAK_HEADER_H diff --git a/core/deco.c b/core/deco.c new file mode 100644 index 000000000..af3e06126 --- /dev/null +++ b/core/deco.c @@ -0,0 +1,601 @@ +/* calculate deco values + * based on Bühlmann ZHL-16b + * based on an implemention by heinrichs weikamp for the DR5 + * the original file was given to Subsurface under the GPLv2 + * by Matthias Heinrichs + * + * The implementation below is a fairly complete rewrite since then + * (C) Robert C. Helling 2013 and released under the GPLv2 + * + * add_segment() - add <seconds> at the given pressure, breathing gasmix + * deco_allowed_depth() - ceiling based on lead tissue, surface pressure, 3m increments or smooth + * set_gf() - set Buehlmann gradient factors + * clear_deco() + * cache_deco_state() + * restore_deco_state() + * dump_tissues() + */ +#include <math.h> +#include <string.h> +#include "dive.h" +#include <assert.h> +#include "core/planner.h" + +#define cube(x) (x * x * x) + +// Subsurface appears to produce marginally less conservative plans than our benchmarks +// Introduce 1.2% additional conservatism +#define subsurface_conservatism_factor 1.012 + + +extern bool in_planner(); + +extern pressure_t first_ceiling_pressure; + +//! Option structure for Buehlmann decompression. +struct buehlmann_config { + double satmult; //! safety at inert gas accumulation as percentage of effect (more than 100). + double desatmult; //! safety at inert gas depletion as percentage of effect (less than 100). + int last_deco_stop_in_mtr; //! depth of last_deco_stop. + double gf_high; //! gradient factor high (at surface). + double gf_low; //! gradient factor low (at bottom/start of deco calculation). + double gf_low_position_min; //! gf_low_position below surface_min_shallow. + bool gf_low_at_maxdepth; //! if true, gf_low applies at max depth instead of at deepest ceiling. +}; + +struct buehlmann_config buehlmann_config = { + .satmult = 1.0, + .desatmult = 1.01, + .last_deco_stop_in_mtr = 0, + .gf_high = 0.75, + .gf_low = 0.35, + .gf_low_position_min = 1.0, + .gf_low_at_maxdepth = false +}; + +//! Option structure for VPM-B decompression. +struct vpmb_config { + double crit_radius_N2; //! Critical radius of N2 nucleon (microns). + double crit_radius_He; //! Critical radius of He nucleon (microns). + double crit_volume_lambda; //! Constant corresponding to critical gas volume (bar * min). + double gradient_of_imperm; //! Gradient after which bubbles become impermeable (bar). + double surface_tension_gamma; //! Nucleons surface tension constant (N / bar = m2). + double skin_compression_gammaC; //! Skin compression gammaC (N / bar = m2). + double regeneration_time; //! Time needed for the bubble to regenerate to the start radius (min). + double other_gases_pressure; //! Always present pressure of other gasses in tissues (bar). +}; + +struct vpmb_config vpmb_config = { + .crit_radius_N2 = 0.55, + .crit_radius_He = 0.45, + .crit_volume_lambda = 199.58, + .gradient_of_imperm = 8.30865, // = 8.2 atm + .surface_tension_gamma = 0.18137175, // = 0.0179 N/msw + .skin_compression_gammaC = 2.6040525, // = 0.257 N/msw + .regeneration_time = 20160.0, + .other_gases_pressure = 0.1359888 +}; + +const double buehlmann_N2_a[] = { 1.1696, 1.0, 0.8618, 0.7562, + 0.62, 0.5043, 0.441, 0.4, + 0.375, 0.35, 0.3295, 0.3065, + 0.2835, 0.261, 0.248, 0.2327 }; + +const double buehlmann_N2_b[] = { 0.5578, 0.6514, 0.7222, 0.7825, + 0.8126, 0.8434, 0.8693, 0.8910, + 0.9092, 0.9222, 0.9319, 0.9403, + 0.9477, 0.9544, 0.9602, 0.9653 }; + +const double buehlmann_N2_t_halflife[] = { 5.0, 8.0, 12.5, 18.5, + 27.0, 38.3, 54.3, 77.0, + 109.0, 146.0, 187.0, 239.0, + 305.0, 390.0, 498.0, 635.0 }; + +const double buehlmann_N2_factor_expositon_one_second[] = { + 2.30782347297664E-003, 1.44301447809736E-003, 9.23769302935806E-004, 6.24261986779007E-004, + 4.27777107246730E-004, 3.01585140931371E-004, 2.12729727268379E-004, 1.50020603047807E-004, + 1.05980191127841E-004, 7.91232600646508E-005, 6.17759153688224E-005, 4.83354552742732E-005, + 3.78761777920511E-005, 2.96212356654113E-005, 2.31974277413727E-005, 1.81926738960225E-005 +}; + +const double buehlmann_He_a[] = { 1.6189, 1.383, 1.1919, 1.0458, + 0.922, 0.8205, 0.7305, 0.6502, + 0.595, 0.5545, 0.5333, 0.5189, + 0.5181, 0.5176, 0.5172, 0.5119 }; + +const double buehlmann_He_b[] = { 0.4770, 0.5747, 0.6527, 0.7223, + 0.7582, 0.7957, 0.8279, 0.8553, + 0.8757, 0.8903, 0.8997, 0.9073, + 0.9122, 0.9171, 0.9217, 0.9267 }; + +const double buehlmann_He_t_halflife[] = { 1.88, 3.02, 4.72, 6.99, + 10.21, 14.48, 20.53, 29.11, + 41.20, 55.19, 70.69, 90.34, + 115.29, 147.42, 188.24, 240.03 }; + +const double buehlmann_He_factor_expositon_one_second[] = { + 6.12608039419837E-003, 3.81800836683133E-003, 2.44456078654209E-003, 1.65134647076792E-003, + 1.13084424730725E-003, 7.97503165599123E-004, 5.62552521860549E-004, 3.96776399429366E-004, + 2.80360036664540E-004, 2.09299583354805E-004, 1.63410794820518E-004, 1.27869320250551E-004, + 1.00198406028040E-004, 7.83611475491108E-005, 6.13689891868496E-005, 4.81280465299827E-005 +}; + +const double conservatism_lvls[] = { 1.0, 1.05, 1.12, 1.22, 1.35 }; + +/* Inspired gas loading equations depend on the partial pressure of inert gas in the alveolar. + * P_alv = (P_amb - P_H2O + (1 - Rq) / Rq * P_CO2) * f + * where: + * P_alv alveolar partial pressure of inert gas + * P_amb ambient pressure + * P_H2O water vapour partial pressure = ~0.0627 bar + * P_CO2 carbon dioxide partial pressure = ~0.0534 bar + * Rq respiratory quotient (O2 consumption / CO2 production) + * f fraction of inert gas + * + * In our calculations, we simplify this to use an effective water vapour pressure + * WV = P_H20 - (1 - Rq) / Rq * P_CO2 + * + * Buhlmann ignored the contribution of CO2 (i.e. Rq = 1.0), whereas Schreiner adopted Rq = 0.8. + * WV_Buhlmann = PP_H2O = 0.0627 bar + * WV_Schreiner = 0.0627 - (1 - 0.8) / Rq * 0.0534 = 0.0493 bar + + * Buhlmann calculations use the Buhlmann value, VPM-B calculations use the Schreiner value. +*/ +#define WV_PRESSURE 0.0627 // water vapor pressure in bar, based on respiratory quotient Rq = 1.0 (Buhlmann value) +#define WV_PRESSURE_SCHREINER 0.0493 // water vapor pressure in bar, based on respiratory quotient Rq = 0.8 (Schreiner value) + +#define DECO_STOPS_MULTIPLIER_MM 3000.0 +#define NITROGEN_FRACTION 0.79 + +double tissue_n2_sat[16]; +double tissue_he_sat[16]; +int ci_pointing_to_guiding_tissue; +double gf_low_pressure_this_dive; +#define TISSUE_ARRAY_SZ sizeof(tissue_n2_sat) + +double tolerated_by_tissue[16]; +double tissue_inertgas_saturation[16]; +double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; + +double max_n2_crushing_pressure[16]; +double max_he_crushing_pressure[16]; + +double crushing_onset_tension[16]; // total inert gas tension in the t* moment +double n2_regen_radius[16]; // rs +double he_regen_radius[16]; +double max_ambient_pressure; // last moment we were descending + +double bottom_n2_gradient[16]; +double bottom_he_gradient[16]; + +double initial_n2_gradient[16]; +double initial_he_gradient[16]; + +double get_crit_radius_He() +{ + if (prefs.conservatism_level <= 4) + return vpmb_config.crit_radius_He * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; + return vpmb_config.crit_radius_He; +} + +double get_crit_radius_N2() +{ + if (prefs.conservatism_level <= 4) + return vpmb_config.crit_radius_N2 * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; + return vpmb_config.crit_radius_N2; +} + +// Solve another cubic equation, this time +// x^3 - B x - C == 0 +// Use trigonometric formula for negative discriminants (see Wikipedia for details) + +double solve_cubic2(double B, double C) +{ + double discriminant = 27 * C * C - 4 * cube(B); + if (discriminant < 0.0) { + return 2.0 * sqrt(B / 3.0) * cos(acos(3.0 * C * sqrt(3.0 / B) / (2.0 * B)) / 3.0); + } + + double denominator = pow(9 * C + sqrt(3 * discriminant), 1 / 3.0); + + return pow(2.0 / 3.0, 1.0 / 3.0) * B / denominator + denominator / pow(18.0, 1.0 / 3.0); +} + +// This is a simplified formula avoiding radii. It uses the fact that Boyle's law says +// pV = (G + P_amb) / G^3 is constant to solve for the new gradient G. + +double update_gradient(double next_stop_pressure, double first_gradient) +{ + double B = cube(first_gradient) / (first_ceiling_pressure.mbar / 1000.0 + first_gradient); + double C = next_stop_pressure * B; + + double new_gradient = solve_cubic2(B, C); + + if (new_gradient < 0.0) + report_error("Negative gradient encountered!"); + return new_gradient; +} + +double vpmb_tolerated_ambient_pressure(double reference_pressure, int ci) +{ + double n2_gradient, he_gradient, total_gradient; + + if (reference_pressure >= first_ceiling_pressure.mbar / 1000.0 || !first_ceiling_pressure.mbar) { + n2_gradient = bottom_n2_gradient[ci]; + he_gradient = bottom_he_gradient[ci]; + } else { + n2_gradient = update_gradient(reference_pressure, bottom_n2_gradient[ci]); + he_gradient = update_gradient(reference_pressure, bottom_he_gradient[ci]); + } + + total_gradient = ((n2_gradient * tissue_n2_sat[ci]) + (he_gradient * tissue_he_sat[ci])) / (tissue_n2_sat[ci] + tissue_he_sat[ci]); + + return tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure - total_gradient; +} + + +double tissue_tolerance_calc(const struct dive *dive, double pressure) +{ + int ci = -1; + double ret_tolerance_limit_ambient_pressure = 0.0; + double gf_high = buehlmann_config.gf_high; + double gf_low = buehlmann_config.gf_low; + double surface = get_surface_pressure_in_mbar(dive, true) / 1000.0; + double lowest_ceiling = 0.0; + double tissue_lowest_ceiling[16]; + + if (prefs.deco_mode != VPMB) { + for (ci = 0; ci < 16; ci++) { + tissue_inertgas_saturation[ci] = tissue_n2_sat[ci] + tissue_he_sat[ci]; + buehlmann_inertgas_a[ci] = ((buehlmann_N2_a[ci] * tissue_n2_sat[ci]) + (buehlmann_He_a[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; + buehlmann_inertgas_b[ci] = ((buehlmann_N2_b[ci] * tissue_n2_sat[ci]) + (buehlmann_He_b[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; + + + /* tolerated = (tissue_inertgas_saturation - buehlmann_inertgas_a) * buehlmann_inertgas_b; */ + + tissue_lowest_ceiling[ci] = (buehlmann_inertgas_b[ci] * tissue_inertgas_saturation[ci] - gf_low * buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci]) / + ((1.0 - buehlmann_inertgas_b[ci]) * gf_low + buehlmann_inertgas_b[ci]); + if (tissue_lowest_ceiling[ci] > lowest_ceiling) + lowest_ceiling = tissue_lowest_ceiling[ci]; + if (!buehlmann_config.gf_low_at_maxdepth) { + if (lowest_ceiling > gf_low_pressure_this_dive) + gf_low_pressure_this_dive = lowest_ceiling; + } + } + for (ci = 0; ci < 16; ci++) { + double tolerated; + + if ((surface / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - surface) * gf_high + surface < + (gf_low_pressure_this_dive / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - gf_low_pressure_this_dive) * gf_low + gf_low_pressure_this_dive) + tolerated = (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high * gf_low_pressure_this_dive - gf_low * surface) - + (1.0 - buehlmann_inertgas_b[ci]) * (gf_high - gf_low) * gf_low_pressure_this_dive * surface + + buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface) * tissue_inertgas_saturation[ci]) / + (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high - gf_low) + + (1.0 - buehlmann_inertgas_b[ci]) * (gf_low * gf_low_pressure_this_dive - gf_high * surface) + + buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface)); + else + tolerated = ret_tolerance_limit_ambient_pressure; + + + tolerated_by_tissue[ci] = tolerated; + + if (tolerated >= ret_tolerance_limit_ambient_pressure) { + ci_pointing_to_guiding_tissue = ci; + ret_tolerance_limit_ambient_pressure = tolerated; + } + } + } else { + // VPM-B ceiling + double reference_pressure; + + ret_tolerance_limit_ambient_pressure = pressure; + // The Boyle compensated gradient depends on ambient pressure. For the ceiling, this should set the ambient pressure. + do { + reference_pressure = ret_tolerance_limit_ambient_pressure; + ret_tolerance_limit_ambient_pressure = 0.0; + for (ci = 0; ci < 16; ci++) { + double tolerated = vpmb_tolerated_ambient_pressure(reference_pressure, ci); + if (tolerated >= ret_tolerance_limit_ambient_pressure) { + ci_pointing_to_guiding_tissue = ci; + ret_tolerance_limit_ambient_pressure = tolerated; + } + tolerated_by_tissue[ci] = tolerated; + } + // We are doing ok if the gradient was computed within ten centimeters of the ceiling. + } while (fabs(ret_tolerance_limit_ambient_pressure - reference_pressure) > 0.01); + } + return ret_tolerance_limit_ambient_pressure; +} + +/* + * Return buelman factor for a particular period and tissue index. + * + * We cache the last factor, since we commonly call this with the + * same values... We have a special "fixed cache" for the one second + * case, although I wonder if that's even worth it considering the + * more general-purpose cache. + */ +struct factor_cache { + int last_period; + double last_factor; +}; + +double n2_factor(int period_in_seconds, int ci) +{ + static struct factor_cache cache[16]; + + if (period_in_seconds == 1) + return buehlmann_N2_factor_expositon_one_second[ci]; + + if (period_in_seconds != cache[ci].last_period) { + cache[ci].last_period = period_in_seconds; + cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_N2_t_halflife[ci] * 60)); + } + + return cache[ci].last_factor; +} + +double he_factor(int period_in_seconds, int ci) +{ + static struct factor_cache cache[16]; + + if (period_in_seconds == 1) + return buehlmann_He_factor_expositon_one_second[ci]; + + if (period_in_seconds != cache[ci].last_period) { + cache[ci].last_period = period_in_seconds; + cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_He_t_halflife[ci] * 60)); + } + + return cache[ci].last_factor; +} + +double calc_surface_phase(double surface_pressure, double he_pressure, double n2_pressure, double he_time_constant, double n2_time_constant) +{ + double inspired_n2 = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * NITROGEN_FRACTION; + + if (n2_pressure > inspired_n2) + return (he_pressure / he_time_constant + (n2_pressure - inspired_n2) / n2_time_constant) / (he_pressure + n2_pressure - inspired_n2); + + if (he_pressure + n2_pressure >= inspired_n2){ + double gradient_decay_time = 1.0 / (n2_time_constant - he_time_constant) * log ((inspired_n2 - n2_pressure) / he_pressure); + double gradients_integral = he_pressure / he_time_constant * (1.0 - exp(-he_time_constant * gradient_decay_time)) + (n2_pressure - inspired_n2) / n2_time_constant * (1.0 - exp(-n2_time_constant * gradient_decay_time)); + return gradients_integral / (he_pressure + n2_pressure - inspired_n2); + } + + return 0; +} + +void vpmb_start_gradient() +{ + int ci; + + for (ci = 0; ci < 16; ++ci) { + initial_n2_gradient[ci] = bottom_n2_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / n2_regen_radius[ci]); + initial_he_gradient[ci] = bottom_he_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / he_regen_radius[ci]); + } +} + +void vpmb_next_gradient(double deco_time, double surface_pressure) +{ + int ci; + double n2_b, n2_c; + double he_b, he_c; + double desat_time; + deco_time /= 60.0; + + for (ci = 0; ci < 16; ++ci) { + desat_time = deco_time + calc_surface_phase(surface_pressure, tissue_he_sat[ci], tissue_n2_sat[ci], log(2.0) / buehlmann_He_t_halflife[ci], log(2.0) / buehlmann_N2_t_halflife[ci]); + + n2_b = initial_n2_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); + he_b = initial_he_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); + + n2_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_n2_crushing_pressure[ci]; + n2_c = n2_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); + he_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_he_crushing_pressure[ci]; + he_c = he_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); + + bottom_n2_gradient[ci] = 0.5 * ( n2_b + sqrt(n2_b * n2_b - 4.0 * n2_c)); + bottom_he_gradient[ci] = 0.5 * ( he_b + sqrt(he_b * he_b - 4.0 * he_c)); + } +} + +// A*r^3 - B*r^2 - C == 0 +// Solved with the help of mathematica + +double solve_cubic(double A, double B, double C) +{ + double BA = B/A; + double CA = C/A; + + double discriminant = CA * (4 * cube(BA) + 27 * CA); + + // Let's make sure we have a real solution: + if (discriminant < 0.0) { + // This should better not happen + report_error("Complex solution for inner pressure encountered!\n A=%f\tB=%f\tC=%f\n", A, B, C); + return 0.0; + } + double denominator = pow(cube(BA) + 1.5 * (9 * CA + sqrt(3.0) * sqrt(discriminant)), 1/3.0); + return (BA + BA * BA / denominator + denominator) / 3.0; + +} + + +void nuclear_regeneration(double time) +{ + time /= 60.0; + int ci; + double crushing_radius_N2, crushing_radius_He; + for (ci = 0; ci < 16; ++ci) { + //rm + crushing_radius_N2 = 1.0 / (max_n2_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_N2()); + crushing_radius_He = 1.0 / (max_he_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_He()); + //rs + n2_regen_radius[ci] = crushing_radius_N2 + (get_crit_radius_N2() - crushing_radius_N2) * (1.0 - exp (-time / vpmb_config.regeneration_time)); + he_regen_radius[ci] = crushing_radius_He + (get_crit_radius_He() - crushing_radius_He) * (1.0 - exp (-time / vpmb_config.regeneration_time)); + } +} + + +// Calculates the nucleons inner pressure during the impermeable period +double calc_inner_pressure(double crit_radius, double onset_tension, double current_ambient_pressure) +{ + double onset_radius = 1.0 / (vpmb_config.gradient_of_imperm / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / crit_radius); + + + double A = current_ambient_pressure - vpmb_config.gradient_of_imperm + (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) / onset_radius; + double B = 2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma); + double C = onset_tension * pow(onset_radius, 3); + + double current_radius = solve_cubic(A, B, C); + + return onset_tension * onset_radius * onset_radius * onset_radius / (current_radius * current_radius * current_radius); +} + +// Calculates the crushing pressure in the given moment. Updates crushing_onset_tension and critical radius if needed +void calc_crushing_pressure(double pressure) +{ + int ci; + double gradient; + double gas_tension; + double n2_crushing_pressure, he_crushing_pressure; + double n2_inner_pressure, he_inner_pressure; + + for (ci = 0; ci < 16; ++ci) { + gas_tension = tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure; + gradient = pressure - gas_tension; + + if (gradient <= vpmb_config.gradient_of_imperm) { // permeable situation + n2_crushing_pressure = he_crushing_pressure = gradient; + crushing_onset_tension[ci] = gas_tension; + } + else { // impermeable + if (max_ambient_pressure >= pressure) + return; + + n2_inner_pressure = calc_inner_pressure(get_crit_radius_N2(), crushing_onset_tension[ci], pressure); + he_inner_pressure = calc_inner_pressure(get_crit_radius_He(), crushing_onset_tension[ci], pressure); + + n2_crushing_pressure = pressure - n2_inner_pressure; + he_crushing_pressure = pressure - he_inner_pressure; + } + max_n2_crushing_pressure[ci] = MAX(max_n2_crushing_pressure[ci], n2_crushing_pressure); + max_he_crushing_pressure[ci] = MAX(max_he_crushing_pressure[ci], he_crushing_pressure); + } + max_ambient_pressure = MAX(pressure, max_ambient_pressure); +} + +/* add period_in_seconds at the given pressure and gas to the deco calculation */ +void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int ccpo2, const struct dive *dive, int sac) +{ + (void) sac; + int ci; + struct gas_pressures pressures; + + fill_pressures(&pressures, pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE), + gasmix, (double) ccpo2 / 1000.0, dive->dc.divemode); + + if (buehlmann_config.gf_low_at_maxdepth && pressure > gf_low_pressure_this_dive) + gf_low_pressure_this_dive = pressure; + + for (ci = 0; ci < 16; ci++) { + double pn2_oversat = pressures.n2 - tissue_n2_sat[ci]; + double phe_oversat = pressures.he - tissue_he_sat[ci]; + double n2_f = n2_factor(period_in_seconds, ci); + double he_f = he_factor(period_in_seconds, ci); + double n2_satmult = pn2_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; + double he_satmult = phe_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; + + tissue_n2_sat[ci] += n2_satmult * pn2_oversat * n2_f; + tissue_he_sat[ci] += he_satmult * phe_oversat * he_f; + } + if(prefs.deco_mode == VPMB) + calc_crushing_pressure(pressure); + return; +} + +void dump_tissues() +{ + int ci; + printf("N2 tissues:"); + for (ci = 0; ci < 16; ci++) + printf(" %6.3e", tissue_n2_sat[ci]); + printf("\nHe tissues:"); + for (ci = 0; ci < 16; ci++) + printf(" %6.3e", tissue_he_sat[ci]); + printf("\n"); +} + +void clear_deco(double surface_pressure) +{ + int ci; + for (ci = 0; ci < 16; ci++) { + tissue_n2_sat[ci] = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * N2_IN_AIR / 1000; + tissue_he_sat[ci] = 0.0; + max_n2_crushing_pressure[ci] = 0.0; + max_he_crushing_pressure[ci] = 0.0; + n2_regen_radius[ci] = get_crit_radius_N2(); + he_regen_radius[ci] = get_crit_radius_He(); + } + gf_low_pressure_this_dive = surface_pressure; + if (!buehlmann_config.gf_low_at_maxdepth) + gf_low_pressure_this_dive += buehlmann_config.gf_low_position_min; + max_ambient_pressure = 0.0; +} + +void cache_deco_state(char **cached_datap) +{ + char *data = *cached_datap; + + if (!data) { + data = malloc(2 * TISSUE_ARRAY_SZ + sizeof(double) + sizeof(int)); + *cached_datap = data; + } + memcpy(data, tissue_n2_sat, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(data, tissue_he_sat, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(data, &gf_low_pressure_this_dive, sizeof(double)); + data += sizeof(double); + memcpy(data, &ci_pointing_to_guiding_tissue, sizeof(int)); +} + +void restore_deco_state(char *data) +{ + memcpy(tissue_n2_sat, data, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(tissue_he_sat, data, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(&gf_low_pressure_this_dive, data, sizeof(double)); + data += sizeof(double); + memcpy(&ci_pointing_to_guiding_tissue, data, sizeof(int)); +} + +int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth) +{ + int depth; + double pressure_delta; + + /* Avoid negative depths */ + pressure_delta = tissues_tolerance > surface_pressure ? tissues_tolerance - surface_pressure : 0.0; + + depth = rel_mbar_to_depth(pressure_delta * 1000, dive); + + if (!smooth) + depth = ceil(depth / DECO_STOPS_MULTIPLIER_MM) * DECO_STOPS_MULTIPLIER_MM; + + if (depth > 0 && depth < buehlmann_config.last_deco_stop_in_mtr * 1000) + depth = buehlmann_config.last_deco_stop_in_mtr * 1000; + + return depth; +} + +void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth) +{ + if (gflow != -1) + buehlmann_config.gf_low = (double)gflow / 100.0; + if (gfhigh != -1) + buehlmann_config.gf_high = (double)gfhigh / 100.0; + buehlmann_config.gf_low_at_maxdepth = gf_low_at_maxdepth; +} diff --git a/core/deco.h b/core/deco.h new file mode 100644 index 000000000..fd3b94a9f --- /dev/null +++ b/core/deco.h @@ -0,0 +1,20 @@ +#ifndef DECO_H +#define DECO_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern double tolerated_by_tissue[]; +extern double buehlmann_N2_t_halflife[]; +extern double tissue_inertgas_saturation[16]; +extern double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; +extern double gf_low_pressure_this_dive; + +extern int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth); + +#ifdef __cplusplus +} +#endif + +#endif // DECO_H diff --git a/core/device.c b/core/device.c new file mode 100644 index 000000000..6c4452f78 --- /dev/null +++ b/core/device.c @@ -0,0 +1,184 @@ +#include <string.h> +#include "dive.h" +#include "device.h" + +/* + * Good fake dive profiles are hard. + * + * "depthtime" is the integral of the dive depth over + * time ("area" of the dive profile). We want that + * area to match the average depth (avg_d*max_t). + * + * To do that, we generate a 6-point profile: + * + * (0, 0) + * (t1, max_d) + * (t2, max_d) + * (t3, d) + * (t4, d) + * (max_t, 0) + * + * with the same ascent/descent rates between the + * different depths. + * + * NOTE: avg_d, max_d and max_t are given constants. + * The rest we can/should play around with to get a + * good-looking profile. + * + * That six-point profile gives a total area of: + * + * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) + * + * And the "same ascent/descent rates" requirement + * gives us (time per depth must be same): + * + * t1 / max_d = (t3-t2) / (max_d-d) + * t1 / max_d = (max_t-t4) / d + * + * We also obviously require: + * + * 0 <= t1 <= t2 <= t3 <= t4 <= max_t + * + * Let us call 'd_frac = d / max_d', and we get: + * + * Total area must match average depth-time: + * + * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) = avg_d*max_t + * max_d*(max_t-t1-(1-d_frac)*(t4-t3)) = avg_d*max_t + * max_t-t1-(1-d_frac)*(t4-t3) = avg_d*max_t/max_d + * t1+(1-d_frac)*(t4-t3) = max_t*(1-avg_d/max_d) + * + * and descent slope must match ascent slopes: + * + * t1 / max_d = (t3-t2) / (max_d*(1-d_frac)) + * t1 = (t3-t2)/(1-d_frac) + * + * and + * + * t1 / max_d = (max_t-t4) / (max_d*d_frac) + * t1 = (max_t-t4)/d_frac + * + * In general, we have more free variables than we have constraints, + * but we can aim for certain basics, like a good ascent slope. + */ +static int fill_samples(struct sample *s, int max_d, int avg_d, int max_t, double slope, double d_frac) +{ + double t_frac = max_t * (1 - avg_d / (double)max_d); + int t1 = max_d / slope; + int t4 = max_t - t1 * d_frac; + int t3 = t4 - (t_frac - t1) / (1 - d_frac); + int t2 = t3 - t1 * (1 - d_frac); + + if (t1 < 0 || t1 > t2 || t2 > t3 || t3 > t4 || t4 > max_t) + return 0; + + s[1].time.seconds = t1; + s[1].depth.mm = max_d; + s[2].time.seconds = t2; + s[2].depth.mm = max_d; + s[3].time.seconds = t3; + s[3].depth.mm = max_d * d_frac; + s[4].time.seconds = t4; + s[4].depth.mm = max_d * d_frac; + + return 1; +} + +/* we have no average depth; instead of making up a random average depth + * we should assume either a PADI rectangular profile (for short and/or + * shallow dives) or more reasonably a six point profile with a 3 minute + * safety stop at 5m */ +static void fill_samples_no_avg(struct sample *s, int max_d, int max_t, double slope) +{ + // shallow or short dives are just trapecoids based on the given slope + if (max_d < 10000 || max_t < 600) { + s[1].time.seconds = max_d / slope; + s[1].depth.mm = max_d; + s[2].time.seconds = max_t - max_d / slope; + s[2].depth.mm = max_d; + } else { + s[1].time.seconds = max_d / slope; + s[1].depth.mm = max_d; + s[2].time.seconds = max_t - max_d / slope - 180; + s[2].depth.mm = max_d; + s[3].time.seconds = max_t - 5000 / slope - 180; + s[3].depth.mm = 5000; + s[4].time.seconds = max_t - 5000 / slope; + s[4].depth.mm = 5000; + } +} + +struct divecomputer *fake_dc(struct divecomputer *dc, bool alloc) +{ + static struct sample fake_samples[6]; + static struct divecomputer fakedc; + struct sample *fake = fake_samples; + + fakedc = (*dc); + if (alloc) + fake = malloc(sizeof(fake_samples)); + + fakedc.sample = fake; + fakedc.samples = 6; + + /* The dive has no samples, so create a few fake ones */ + int max_t = dc->duration.seconds; + int max_d = dc->maxdepth.mm; + int avg_d = dc->meandepth.mm; + + memset(fake, 0, sizeof(fake_samples)); + fake[5].time.seconds = max_t; + if (!max_t || !max_d) + return &fakedc; + + /* + * We want to fake the profile so that the average + * depth ends up correct. However, in the absence of + * a reasonable average, let's just make something + * up. Note that 'avg_d == max_d' is _not_ a reasonable + * average. + * We explicitly treat avg_d == 0 differently */ + if (avg_d == 0) { + /* we try for a sane slope, but bow to the insanity of + * the user supplied data */ + fill_samples_no_avg(fake, max_d, max_t, MAX(2.0 * max_d / max_t, 5000.0 / 60)); + if (fake[3].time.seconds == 0) { // just a 4 point profile + fakedc.samples = 4; + fake[3].time.seconds = max_t; + } + return &fakedc; + } + if (avg_d < max_d / 10 || avg_d >= max_d) { + avg_d = (max_d + 10000) / 3; + if (avg_d > max_d) + avg_d = max_d * 2 / 3; + } + if (!avg_d) + avg_d = 1; + + /* + * Ok, first we try a basic profile with a specific ascent + * rate (5 meters per minute) and d_frac (1/3). + */ + if (fill_samples(fake, max_d, avg_d, max_t, 5000.0 / 60, 0.33)) + return &fakedc; + + /* + * Ok, assume that didn't work because we cannot make the + * average come out right because it was a quick deep dive + * followed by a much shallower region + */ + if (fill_samples(fake, max_d, avg_d, max_t, 10000.0 / 60, 0.10)) + return &fakedc; + + /* + * Uhhuh. That didn't work. We'd need to find a good combination that + * satisfies our constraints. Currently, we don't, we just give insane + * slopes. + */ + if (fill_samples(fake, max_d, avg_d, max_t, 10000.0, 0.01)) + return &fakedc; + + /* Even that didn't work? Give up, there's something wrong */ + return &fakedc; +} diff --git a/core/device.h b/core/device.h new file mode 100644 index 000000000..8a00b96d3 --- /dev/null +++ b/core/device.h @@ -0,0 +1,18 @@ +#ifndef DEVICE_H +#define DEVICE_H + +#ifdef __cplusplus +#include "dive.h" +extern "C" { +#endif + +extern struct divecomputer *fake_dc(struct divecomputer *dc, bool alloc); +extern void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname); +extern void call_for_each_dc(void *f, void (*callback)(void *, const char *, uint32_t, + const char *, const char *, const char *), bool select_only); + +#ifdef __cplusplus +} +#endif + +#endif // DEVICE_H diff --git a/core/devicedetails.cpp b/core/devicedetails.cpp new file mode 100644 index 000000000..a917a4d0e --- /dev/null +++ b/core/devicedetails.cpp @@ -0,0 +1,70 @@ +#include "devicedetails.h" + +gas::gas(unsigned char oxygen, unsigned char helium, unsigned char type, unsigned char depth) : + oxygen(oxygen), helium(helium), type(type), depth(depth) +{ +} + +setpoint::setpoint(unsigned char sp, unsigned char depth) : + sp(sp), depth(depth) +{ +} + +DeviceDetails::DeviceDetails(QObject *parent) : + QObject(parent), + data(0), + syncTime(false), + setPointFallback(0), + ccrMode(0), + calibrationGas(0), + diveMode(0), + decoType(0), + ppO2Max(0), + ppO2Min(0), + futureTTS(0), + gfLow(0), + gfHigh(0), + aGFLow(0), + aGFHigh(0), + aGFSelectable(0), + saturation(0), + desaturation(0), + lastDeco(0), + brightness(0), + units(0), + samplingRate(0), + salinity(0), + diveModeColor(0), + language(0), + dateFormat(0), + compassGain(0), + pressureSensorOffset(0), + flipScreen(0), + safetyStop(0), + maxDepth(0), + totalTime(0), + numberOfDives(0), + altitude(0), + personalSafety(0), + timeFormat(0), + lightEnabled(false), + light(0), + alarmTimeEnabled(false), + alarmTime(0), + alarmDepthEnabled(false), + alarmDepth(0), + leftButtonSensitivity(0), + rightButtonSensitivity(0), + bottomGasConsumption(0), + decoGasConsumption(0), + modWarning(false), + dynamicAscendRate(false), + graphicalSpeedIndicator(false), + alwaysShowppO2(false), + tempSensorOffset(0), + safetyStopLength(0), + safetyStopStartDepth(0), + safetyStopEndDepth(0), + safetyStopResetDepth(0) +{ +} diff --git a/core/devicedetails.h b/core/devicedetails.h new file mode 100644 index 000000000..ff3009bc5 --- /dev/null +++ b/core/devicedetails.h @@ -0,0 +1,104 @@ +#ifndef DEVICEDETAILS_H +#define DEVICEDETAILS_H + +#include <QObject> +#include <QDateTime> +#include "libdivecomputer.h" + +struct gas { + unsigned char oxygen; + unsigned char helium; + unsigned char type; + unsigned char depth; + gas(unsigned char oxygen = 0, unsigned char helium = 0, unsigned char type = 0, unsigned char depth = 0); +}; + +struct setpoint { + unsigned char sp; + unsigned char depth; + setpoint(unsigned char sp = 0, unsigned char depth = 0); +}; + +class DeviceDetails : public QObject +{ + Q_OBJECT +public: + explicit DeviceDetails(QObject *parent = 0); + + device_data_t *data; + QString serialNo; + QString firmwareVersion; + QString customText; + QString model; + bool syncTime; + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + bool setPointFallback; + int ccrMode; + int calibrationGas; + int diveMode; + int decoType; + int ppO2Max; + int ppO2Min; + int futureTTS; + int gfLow; + int gfHigh; + int aGFLow; + int aGFHigh; + int aGFSelectable; + int saturation; + int desaturation; + int lastDeco; + int brightness; + int units; + int samplingRate; + int salinity; + int diveModeColor; + int language; + int dateFormat; + int compassGain; + int pressureSensorOffset; + bool flipScreen; + bool safetyStop; + int maxDepth; + int totalTime; + int numberOfDives; + int altitude; + int personalSafety; + int timeFormat; + bool lightEnabled; + int light; + bool alarmTimeEnabled; + int alarmTime; + bool alarmDepthEnabled; + int alarmDepth; + int leftButtonSensitivity; + int rightButtonSensitivity; + int bottomGasConsumption; + int decoGasConsumption; + bool modWarning; + bool dynamicAscendRate; + bool graphicalSpeedIndicator; + bool alwaysShowppO2; + int tempSensorOffset; + unsigned safetyStopLength; + unsigned safetyStopStartDepth; + unsigned safetyStopEndDepth; + unsigned safetyStopResetDepth; +}; + + +#endif // DEVICEDETAILS_H diff --git a/core/display.h b/core/display.h new file mode 100644 index 000000000..9e3e1d159 --- /dev/null +++ b/core/display.h @@ -0,0 +1,63 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct membuffer; + +#define SCALE_SCREEN 1.0 +#define SCALE_PRINT (1.0 / get_screen_dpi()) + +extern double get_screen_dpi(void); + +/* Plot info with smoothing, velocity indication + * and one-, two- and three-minute minimums and maximums */ +struct plot_info { + int nr; + int maxtime; + int meandepth, maxdepth; + int minpressure, maxpressure; + int minhr, maxhr; + int mintemp, maxtemp; + enum {AIR, NITROX, TRIMIX, FREEDIVING} dive_type; + double endtempcoord; + double maxpp; + bool has_ndl; + struct plot_data *entry; +}; + +typedef enum { + SC_SCREEN, + SC_PRINT +} scale_mode_t; + +extern struct divecomputer *select_dc(struct dive *); + +extern unsigned int dc_number; + +extern unsigned int amount_selected; + +extern int is_default_dive_computer_device(const char *); +extern int is_default_dive_computer(const char *, const char *); + +typedef void (*device_callback_t)(const char *name, void *userdata); + +#define DC_TYPE_SERIAL 1 +#define DC_TYPE_UEMIS 2 +#define DC_TYPE_OTHER 3 + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type); + +extern const char *default_dive_computer_vendor; +extern const char *default_dive_computer_product; +extern const char *default_dive_computer_device; +extern int default_dive_computer_download_mode; +#define AMB_PERCENTAGE 50.0 + +#ifdef __cplusplus +} +#endif + +#endif // DISPLAY_H diff --git a/core/dive.c b/core/dive.c new file mode 100644 index 000000000..ce730da28 --- /dev/null +++ b/core/dive.c @@ -0,0 +1,3561 @@ +/* dive.c */ +/* maintains the internal dive list structure */ +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include "gettext.h" +#include "dive.h" +#include "libdivecomputer.h" +#include "device.h" +#include "divelist.h" +#include "qthelperfromc.h" + +/* one could argue about the best place to have this variable - + * it's used in the UI, but it seems to make the most sense to have it + * here */ +struct dive displayed_dive; +struct dive_site displayed_dive_site; + +struct tag_entry *g_tag_list = NULL; + +static const char *default_tags[] = { + QT_TRANSLATE_NOOP("gettextFromC", "boat"), QT_TRANSLATE_NOOP("gettextFromC", "shore"), QT_TRANSLATE_NOOP("gettextFromC", "drift"), + QT_TRANSLATE_NOOP("gettextFromC", "deep"), QT_TRANSLATE_NOOP("gettextFromC", "cavern"), QT_TRANSLATE_NOOP("gettextFromC", "ice"), + QT_TRANSLATE_NOOP("gettextFromC", "wreck"), QT_TRANSLATE_NOOP("gettextFromC", "cave"), QT_TRANSLATE_NOOP("gettextFromC", "altitude"), + QT_TRANSLATE_NOOP("gettextFromC", "pool"), QT_TRANSLATE_NOOP("gettextFromC", "lake"), QT_TRANSLATE_NOOP("gettextFromC", "river"), + QT_TRANSLATE_NOOP("gettextFromC", "night"), QT_TRANSLATE_NOOP("gettextFromC", "fresh"), QT_TRANSLATE_NOOP("gettextFromC", "student"), + QT_TRANSLATE_NOOP("gettextFromC", "instructor"), QT_TRANSLATE_NOOP("gettextFromC", "photo"), QT_TRANSLATE_NOOP("gettextFromC", "video"), + QT_TRANSLATE_NOOP("gettextFromC", "deco") +}; + +const char *cylinderuse_text[] = { + QT_TRANSLATE_NOOP("gettextFromC", "OC-gas"), QT_TRANSLATE_NOOP("gettextFromC", "diluent"), QT_TRANSLATE_NOOP("gettextFromC", "oxygen") +}; +const char *divemode_text[] = { "OC", "CCR", "PSCR", "Freedive" }; + +int event_is_gaschange(struct event *ev) +{ + return ev->type == SAMPLE_EVENT_GASCHANGE || + ev->type == SAMPLE_EVENT_GASCHANGE2; +} + +/* + * Does the gas mix data match the legacy + * libdivecomputer event format? If so, + * we can skip saving it, in order to maintain + * the old save formats. We'll re-generate the + * gas mix when loading. + */ +int event_gasmix_redundant(struct event *ev) +{ + int value = ev->value; + int o2, he; + + o2 = (value & 0xffff) * 10; + he = (value >> 16) * 10; + return o2 == ev->gas.mix.o2.permille && + he == ev->gas.mix.he.permille; +} + +struct event *add_event(struct divecomputer *dc, unsigned int time, int type, int flags, int value, const char *name) +{ + int gas_index = -1; + struct event *ev, **p; + unsigned int size, len = strlen(name); + + size = sizeof(*ev) + len + 1; + ev = malloc(size); + if (!ev) + return NULL; + memset(ev, 0, size); + memcpy(ev->name, name, len); + ev->time.seconds = time; + ev->type = type; + ev->flags = flags; + ev->value = value; + + /* + * Expand the events into a sane format. Currently + * just gas switches + */ + switch (type) { + case SAMPLE_EVENT_GASCHANGE2: + /* High 16 bits are He percentage */ + ev->gas.mix.he.permille = (value >> 16) * 10; + + /* Extension to the GASCHANGE2 format: cylinder index in 'flags' */ + if (flags > 0 && flags <= MAX_CYLINDERS) + gas_index = flags-1; + /* Fallthrough */ + case SAMPLE_EVENT_GASCHANGE: + /* Low 16 bits are O2 percentage */ + ev->gas.mix.o2.permille = (value & 0xffff) * 10; + ev->gas.index = gas_index; + break; + } + + p = &dc->events; + + /* insert in the sorted list of events */ + while (*p && (*p)->time.seconds <= time) + p = &(*p)->next; + ev->next = *p; + *p = ev; + remember_event(name); + return ev; +} + +static int same_event(struct event *a, struct event *b) +{ + if (a->time.seconds != b->time.seconds) + return 0; + if (a->type != b->type) + return 0; + if (a->flags != b->flags) + return 0; + if (a->value != b->value) + return 0; + return !strcmp(a->name, b->name); +} + +void remove_event(struct event *event) +{ + struct event **ep = ¤t_dc->events; + while (ep && !same_event(*ep, event)) + ep = &(*ep)->next; + if (ep) { + /* we can't link directly with event->next + * because 'event' can be a copy from another + * dive (for instance the displayed_dive + * that we use on the interface to show things). */ + struct event *temp = (*ep)->next; + free(*ep); + *ep = temp; + } +} + +/* since the name is an array as part of the structure (how silly is that?) we + * have to actually remove the existing event and replace it with a new one. + * WARNING, WARNING... this may end up freeing event in case that event is indeed + * WARNING, WARNING... part of this divecomputer on this dive! */ +void update_event_name(struct dive *d, struct event *event, char *name) +{ + if (!d || !event) + return; + struct divecomputer *dc = get_dive_dc(d, dc_number); + if (!dc) + return; + struct event **removep = &dc->events; + struct event *remove; + while ((*removep)->next && !same_event(*removep, event)) + removep = &(*removep)->next; + if (!same_event(*removep, event)) + return; + remove = *removep; + *removep = (*removep)->next; + add_event(dc, event->time.seconds, event->type, event->flags, event->value, name); + free(remove); + invalidate_dive_cache(d); +} + +void add_extra_data(struct divecomputer *dc, const char *key, const char *value) +{ + struct extra_data **ed = &dc->extra_data; + + while (*ed) + ed = &(*ed)->next; + *ed = malloc(sizeof(struct extra_data)); + if (*ed) { + (*ed)->key = strdup(key); + (*ed)->value = strdup(value); + (*ed)->next = NULL; + } +} + +/* this returns a pointer to static variable - so use it right away after calling */ +struct gasmix *get_gasmix_from_event(struct event *ev) +{ + static struct gasmix dummy; + if (ev && event_is_gaschange(ev)) + return &ev->gas.mix; + + return &dummy; +} + +int get_pressure_units(int mb, const char **units) +{ + int pressure; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->pressure) { + case PASCAL: + pressure = mb * 100; + unit = translate("gettextFromC", "pascal"); + break; + case BAR: + default: + pressure = (mb + 500) / 1000; + unit = translate("gettextFromC", "bar"); + break; + case PSI: + pressure = mbar_to_PSI(mb); + unit = translate("gettextFromC", "psi"); + break; + } + if (units) + *units = unit; + return pressure; +} + +double get_temp_units(unsigned int mk, const char **units) +{ + double deg; + const char *unit; + struct units *units_p = get_units(); + + if (units_p->temperature == FAHRENHEIT) { + deg = mkelvin_to_F(mk); + unit = UTF8_DEGREE "F"; + } else { + deg = mkelvin_to_C(mk); + unit = UTF8_DEGREE "C"; + } + if (units) + *units = unit; + return deg; +} + +double get_volume_units(unsigned int ml, int *frac, const char **units) +{ + int decimals; + double vol; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->volume) { + case LITER: + default: + vol = ml / 1000.0; + unit = translate("gettextFromC", "â„“"); + decimals = 1; + break; + case CUFT: + vol = ml_to_cuft(ml); + unit = translate("gettextFromC", "cuft"); + decimals = 2; + break; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return vol; +} + +int units_to_sac(double volume) +{ + if (get_units()->volume == CUFT) + return rint(cuft_to_l(volume) * 1000.0); + else + return rint(volume * 1000); +} + +unsigned int units_to_depth(double depth) +{ + if (get_units()->length == METERS) + return rint(depth * 1000); + return feet_to_mm(depth); +} + +double get_depth_units(int mm, int *frac, const char **units) +{ + int decimals; + double d; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->length) { + case METERS: + default: + d = mm / 1000.0; + unit = translate("gettextFromC", "m"); + decimals = d < 20; + break; + case FEET: + d = mm_to_feet(mm); + unit = translate("gettextFromC", "ft"); + decimals = 0; + break; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return d; +} + +double get_vertical_speed_units(unsigned int mms, int *frac, const char **units) +{ + double d; + const char *unit; + const struct units *units_p = get_units(); + const double time_factor = units_p->vertical_speed_time == MINUTES ? 60.0 : 1.0; + + switch (units_p->length) { + case METERS: + default: + d = mms / 1000.0 * time_factor; + if (units_p->vertical_speed_time == MINUTES) + unit = translate("gettextFromC", "m/min"); + else + unit = translate("gettextFromC", "m/s"); + break; + case FEET: + d = mm_to_feet(mms) * time_factor; + if (units_p->vertical_speed_time == MINUTES) + unit = translate("gettextFromC", "ft/min"); + else + unit = translate("gettextFromC", "ft/s"); + break; + } + if (frac) + *frac = d < 10; + if (units) + *units = unit; + return d; +} + +double get_weight_units(unsigned int grams, int *frac, const char **units) +{ + int decimals; + double value; + const char *unit; + struct units *units_p = get_units(); + + if (units_p->weight == LBS) { + value = grams_to_lbs(grams); + unit = translate("gettextFromC", "lbs"); + decimals = 0; + } else { + value = grams / 1000.0; + unit = translate("gettextFromC", "kg"); + decimals = 1; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return value; +} + +bool has_hr_data(struct divecomputer *dc) +{ + int i; + struct sample *sample; + + if (!dc) + return false; + + sample = dc->sample; + for (i = 0; i < dc->samples; i++) + if (sample[i].heartbeat) + return true; + return false; +} + +struct dive *alloc_dive(void) +{ + struct dive *dive; + + dive = malloc(sizeof(*dive)); + if (!dive) + exit(1); + memset(dive, 0, sizeof(*dive)); + dive->id = dive_getUniqID(dive); + return dive; +} + +static void free_dc(struct divecomputer *dc); +static void free_pic(struct picture *picture); + +/* this is very different from the copy_divecomputer later in this file; + * this function actually makes full copies of the content */ +static void copy_dc(struct divecomputer *sdc, struct divecomputer *ddc) +{ + *ddc = *sdc; + ddc->model = copy_string(sdc->model); + copy_samples(sdc, ddc); + copy_events(sdc, ddc); +} + +/* copy an element in a list of pictures */ +static void copy_pl(struct picture *sp, struct picture *dp) +{ + *dp = *sp; + dp->filename = copy_string(sp->filename); + dp->hash = copy_string(sp->hash); +} + +/* copy an element in a list of tags */ +static void copy_tl(struct tag_entry *st, struct tag_entry *dt) +{ + dt->tag = malloc(sizeof(struct divetag)); + dt->tag->name = copy_string(st->tag->name); + dt->tag->source = copy_string(st->tag->source); +} + +/* Clear everything but the first element; + * this works for taglist, picturelist, even dive computers */ +#define STRUCTURED_LIST_FREE(_type, _start, _free) \ + { \ + _type *_ptr = _start; \ + while (_ptr) { \ + _type *_next = _ptr->next; \ + _free(_ptr); \ + _ptr = _next; \ + } \ + } + +#define STRUCTURED_LIST_COPY(_type, _first, _dest, _cpy) \ + { \ + _type *_sptr = _first; \ + _type **_dptr = &_dest; \ + while (_sptr) { \ + *_dptr = malloc(sizeof(_type)); \ + _cpy(_sptr, *_dptr); \ + _sptr = _sptr->next; \ + _dptr = &(*_dptr)->next; \ + } \ + *_dptr = 0; \ + } + +/* copy_dive makes duplicates of many components of a dive; + * in order not to leak memory, we need to free those . + * copy_dive doesn't play with the divetrip and forward/backward pointers + * so we can ignore those */ +void clear_dive(struct dive *d) +{ + if (!d) + return; + /* free the strings */ + free(d->buddy); + free(d->divemaster); + free(d->notes); + free(d->suit); + /* free tags, additional dive computers, and pictures */ + taglist_free(d->tag_list); + STRUCTURED_LIST_FREE(struct divecomputer, d->dc.next, free_dc); + STRUCTURED_LIST_FREE(struct picture, d->picture_list, free_pic); + for (int i = 0; i < MAX_CYLINDERS; i++) + free((void *)d->cylinder[i].type.description); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) + free((void *)d->weightsystem[i].description); + memset(d, 0, sizeof(struct dive)); +} + +/* make a true copy that is independent of the source dive; + * all data structures are duplicated, so the copy can be modified without + * any impact on the source */ +void copy_dive(struct dive *s, struct dive *d) +{ + clear_dive(d); + /* simply copy things over, but then make actual copies of the + * relevant components that are referenced through pointers, + * so all the strings and the structured lists */ + *d = *s; + invalidate_dive_cache(d); + d->buddy = copy_string(s->buddy); + d->divemaster = copy_string(s->divemaster); + d->notes = copy_string(s->notes); + d->suit = copy_string(s->suit); + for (int i = 0; i < MAX_CYLINDERS; i++) + d->cylinder[i].type.description = copy_string(s->cylinder[i].type.description); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) + d->weightsystem[i].description = copy_string(s->weightsystem[i].description); + STRUCTURED_LIST_COPY(struct picture, s->picture_list, d->picture_list, copy_pl); + STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); + + // Copy the first dc explicitly, then the list of subsequent dc's + copy_dc(&s->dc, &d->dc); + STRUCTURED_LIST_COPY(struct divecomputer, s->dc.next, d->dc.next, copy_dc); +} + +/* make a clone of the source dive and clean out the source dive; + * this is specifically so we can create a dive in the displayed_dive and then + * add it to the divelist. + * Note the difference to copy_dive() / clean_dive() */ +struct dive *clone_dive(struct dive *s) +{ + struct dive *dive = alloc_dive(); + *dive = *s; // so all the pointers in dive point to the things s pointed to + memset(s, 0, sizeof(struct dive)); // and now the pointers in s are gone + return dive; +} + +#define CONDITIONAL_COPY_STRING(_component) \ + if (what._component) \ + d->_component = copy_string(s->_component) + +// copy elements, depending on bits in what that are set +void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear) +{ + if (clear) + clear_dive(d); + CONDITIONAL_COPY_STRING(notes); + CONDITIONAL_COPY_STRING(divemaster); + CONDITIONAL_COPY_STRING(buddy); + CONDITIONAL_COPY_STRING(suit); + if (what.rating) + d->rating = s->rating; + if (what.visibility) + d->visibility = s->visibility; + if (what.divesite) + d->dive_site_uuid = s->dive_site_uuid; + if (what.tags) + STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); + if (what.cylinders) + copy_cylinders(s, d, false); + if (what.weights) + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + free((void *)d->weightsystem[i].description); + d->weightsystem[i] = s->weightsystem[i]; + d->weightsystem[i].description = copy_string(s->weightsystem[i].description); + } +} +#undef CONDITIONAL_COPY_STRING + +struct event *clone_event(const struct event *src_ev) +{ + struct event *ev; + if (!src_ev) + return NULL; + + size_t size = sizeof(*src_ev) + strlen(src_ev->name) + 1; + ev = (struct event*) malloc(size); + if (!ev) + exit(1); + memcpy(ev, src_ev, size); + ev->next = NULL; + + return ev; +} + +/* copies all events in this dive computer */ +void copy_events(struct divecomputer *s, struct divecomputer *d) +{ + struct event *ev, **pev; + if (!s || !d) + return; + ev = s->events; + pev = &d->events; + while (ev != NULL) { + struct event *new_ev = clone_event(ev); + *pev = new_ev; + pev = &new_ev->next; + ev = ev->next; + } + *pev = NULL; +} + +int nr_cylinders(struct dive *dive) +{ + int nr; + + for (nr = MAX_CYLINDERS; nr; --nr) { + cylinder_t *cylinder = dive->cylinder + nr - 1; + if (!cylinder_nodata(cylinder)) + break; + } + return nr; +} + +int nr_weightsystems(struct dive *dive) +{ + int nr; + + for (nr = MAX_WEIGHTSYSTEMS; nr; --nr) { + weightsystem_t *ws = dive->weightsystem + nr - 1; + if (!weightsystem_none(ws)) + break; + } + return nr; +} + +/* copy the equipment data part of the cylinders */ +void copy_cylinders(struct dive *s, struct dive *d, bool used_only) +{ + int i,j; + if (!s || !d) + return; + for (i = 0; i < MAX_CYLINDERS; i++) { + free((void *)d->cylinder[i].type.description); + memset(&d->cylinder[i], 0, sizeof(cylinder_t)); + } + for (i = j = 0; i < MAX_CYLINDERS; i++) { + if (!used_only || is_cylinder_used(s, i)) { + d->cylinder[j].type = s->cylinder[i].type; + d->cylinder[j].type.description = copy_string(s->cylinder[i].type.description); + d->cylinder[j].gasmix = s->cylinder[i].gasmix; + d->cylinder[j].depth = s->cylinder[i].depth; + d->cylinder[j].cylinder_use = s->cylinder[i].cylinder_use; + d->cylinder[j].manually_added = true; + j++; + } + } +} + +int cylinderuse_from_text(const char *text) +{ + for (enum cylinderuse i = 0; i < NUM_GAS_USE; i++) { + if (same_string(text, cylinderuse_text[i]) || same_string(text, translate("gettextFromC", cylinderuse_text[i]))) + return i; + } + return -1; +} + +void copy_samples(struct divecomputer *s, struct divecomputer *d) +{ + /* instead of carefully copying them one by one and calling add_sample + * over and over again, let's just copy the whole blob */ + if (!s || !d) + return; + int nr = s->samples; + d->samples = nr; + d->alloc_samples = nr; + // We expect to be able to read the memory in the other end of the pointer + // if its a valid pointer, so don't expect malloc() to return NULL for + // zero-sized malloc, do it ourselves. + d->sample = NULL; + + if(!nr) + return; + + d->sample = malloc(nr * sizeof(struct sample)); + if (d->sample) + memcpy(d->sample, s->sample, nr * sizeof(struct sample)); +} + +struct sample *prepare_sample(struct divecomputer *dc) +{ + if (dc) { + int nr = dc->samples; + int alloc_samples = dc->alloc_samples; + struct sample *sample; + if (nr >= alloc_samples) { + struct sample *newsamples; + + alloc_samples = (alloc_samples * 3) / 2 + 10; + newsamples = realloc(dc->sample, alloc_samples * sizeof(struct sample)); + if (!newsamples) + return NULL; + dc->alloc_samples = alloc_samples; + dc->sample = newsamples; + } + sample = dc->sample + nr; + memset(sample, 0, sizeof(*sample)); + return sample; + } + return NULL; +} + +void finish_sample(struct divecomputer *dc) +{ + dc->samples++; +} + +/* + * So when we re-calculate maxdepth and meandepth, we will + * not override the old numbers if they are close to the + * new ones. + * + * Why? Because a dive computer may well actually track the + * max depth and mean depth at finer granularity than the + * samples it stores. So it's possible that the max and mean + * have been reported more correctly originally. + * + * Only if the values calculated from the samples are clearly + * different do we override the normal depth values. + * + * This considers 1m to be "clearly different". That's + * a totally random number. + */ +static void update_depth(depth_t *depth, int new) +{ + if (new) { + int old = depth->mm; + + if (abs(old - new) > 1000) + depth->mm = new; + } +} + +static void update_temperature(temperature_t *temperature, int new) +{ + if (new) { + int old = temperature->mkelvin; + + if (abs(old - new) > 1000) + temperature->mkelvin = new; + } +} + +/* + * Calculate how long we were actually under water, and the average + * depth while under water. + * + * This ignores any surface time in the middle of the dive. + */ +void fixup_dc_duration(struct divecomputer *dc) +{ + int duration, i; + int lasttime, lastdepth, depthtime; + + duration = 0; + lasttime = 0; + lastdepth = 0; + depthtime = 0; + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int depth = sample->depth.mm; + + /* We ignore segments at the surface */ + if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { + duration += time - lasttime; + depthtime += (time - lasttime) * (depth + lastdepth) / 2; + } + lastdepth = depth; + lasttime = time; + } + if (duration) { + dc->duration.seconds = duration; + dc->meandepth.mm = (depthtime + duration / 2) / duration; + } +} + +void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration) +{ + int i; + int depthtime[MAX_CYLINDERS] = { 0, }; + uint32_t lasttime = 0; + int lastdepth = 0; + int idx = 0; + + for (i = 0; i < MAX_CYLINDERS; i++) + mean[i] = duration[i] = 0; + if (!dc) + return; + struct event *ev = get_next_event(dc->events, "gaschange"); + if (!ev || (dc && dc->sample && ev->time.seconds == dc->sample[0].time.seconds && get_next_event(ev->next, "gaschange") == NULL)) { + // we have either no gas change or only one gas change and that's setting an explicit first cylinder + mean[explicit_first_cylinder(dive, dc)] = dc->meandepth.mm; + duration[explicit_first_cylinder(dive, dc)] = dc->duration.seconds; + + if (dc->divemode == CCR) { + // Do the same for the O2 cylinder + int o2_cyl = get_cylinder_idx_by_use(dive, OXYGEN); + if (o2_cyl < 0) + return; + mean[o2_cyl] = dc->meandepth.mm; + duration[o2_cyl] = dc->duration.seconds; + } + return; + } + if (!dc->samples) + dc = fake_dc(dc, false); + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + uint32_t time = sample->time.seconds; + int depth = sample->depth.mm; + + /* Make sure to move the event past 'lasttime' */ + while (ev && lasttime >= ev->time.seconds) { + idx = get_cylinder_index(dive, ev); + ev = get_next_event(ev->next, "gaschange"); + } + + /* Do we need to fake a midway sample at an event? */ + if (ev && time > ev->time.seconds) { + int newtime = ev->time.seconds; + int newdepth = interpolate(lastdepth, depth, newtime - lasttime, time - lasttime); + + time = newtime; + depth = newdepth; + i--; + } + /* We ignore segments at the surface */ + if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { + duration[idx] += time - lasttime; + depthtime[idx] += (time - lasttime) * (depth + lastdepth) / 2; + } + lastdepth = depth; + lasttime = time; + } + for (i = 0; i < MAX_CYLINDERS; i++) { + if (duration[i]) + mean[i] = (depthtime[i] + duration[i] / 2) / duration[i]; + } +} + +static void update_min_max_temperatures(struct dive *dive, temperature_t temperature) +{ + if (temperature.mkelvin) { + if (!dive->maxtemp.mkelvin || temperature.mkelvin > dive->maxtemp.mkelvin) + dive->maxtemp = temperature; + if (!dive->mintemp.mkelvin || temperature.mkelvin < dive->mintemp.mkelvin) + dive->mintemp = temperature; + } +} + +int gas_volume(cylinder_t *cyl, pressure_t p) +{ + double bar = p.mbar / 1000.0; + double z_factor = gas_compressibility_factor(&cyl->gasmix, bar); + return cyl->type.size.mliter * bar_to_atm(bar) / z_factor; +} + +/* + * If the cylinder tank pressures are within half a bar + * (about 8 PSI) of the sample pressures, we consider it + * to be a rounding error, and throw them away as redundant. + */ +static int same_rounded_pressure(pressure_t a, pressure_t b) +{ + return abs(a.mbar - b.mbar) <= 500; +} + +/* Some dive computers (Cobalt) don't start the dive with cylinder 0 but explicitly + * tell us what the first gas is with a gas change event in the first sample. + * Sneakily we'll use a return value of 0 (or FALSE) when there is no explicit + * first cylinder - in which case cylinder 0 is indeed the first cylinder */ +int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc) +{ + if (dc) { + struct event *ev = get_next_event(dc->events, "gaschange"); + if (ev && dc->sample && ev->time.seconds == dc->sample[0].time.seconds) + return get_cylinder_index(dive, ev); + else if (dc->divemode == CCR) + return MAX(get_cylinder_idx_by_use(dive, DILUENT), 0); + } + return 0; +} + +/* this gets called when the dive mode has changed (so OC vs. CC) + * there are two places we might have setpoints... events or in the samples + */ +void update_setpoint_events(struct divecomputer *dc) +{ + struct event *ev; + int new_setpoint = 0; + + if (dc->divemode == CCR) + new_setpoint = prefs.defaultsetpoint; + + if (dc->divemode == OC && + (same_string(dc->model, "Shearwater Predator") || + same_string(dc->model, "Shearwater Petrel") || + same_string(dc->model, "Shearwater Nerd"))) { + // make sure there's no setpoint in the samples + // this is an irreversible change - so switching a dive to OC + // by mistake when it's actually CCR is _bad_ + // So we make sure, this comes from a Predator or Petrel and we only remove + // pO2 values we would have computed anyway. + struct event *ev = get_next_event(dc->events, "gaschange"); + struct gasmix *gasmix = get_gasmix_from_event(ev); + struct event *next = get_next_event(ev, "gaschange"); + + for (int i = 0; i < dc->samples; i++) { + struct gas_pressures pressures; + if (next && dc->sample[i].time.seconds >= next->time.seconds) { + ev = next; + gasmix = get_gasmix_from_event(ev); + next = get_next_event(ev, "gaschange"); + } + fill_pressures(&pressures, calculate_depth_to_mbar(dc->sample[i].depth.mm, dc->surface_pressure, 0), gasmix ,0, OC); + if (abs(dc->sample[i].setpoint.mbar - (int)(1000 * pressures.o2)) <= 50) + dc->sample[i].setpoint.mbar = 0; + } + } + + // an "SP change" event at t=0 is currently our marker for OC vs CCR + // this will need to change to a saner setup, but for now we can just + // check if such an event is there and adjust it, or add that event + ev = get_next_event(dc->events, "SP change"); + if (ev && ev->time.seconds == 0) { + ev->value = new_setpoint; + } else { + if (!add_event(dc, 0, SAMPLE_EVENT_PO2, 0, new_setpoint, "SP change")) + fprintf(stderr, "Could not add setpoint change event\n"); + } +} + +void sanitize_gasmix(struct gasmix *mix) +{ + unsigned int o2, he; + + o2 = mix->o2.permille; + he = mix->he.permille; + + /* Regular air: leave empty */ + if (!he) { + if (!o2) + return; + /* 20.8% to 21% O2 is just air */ + if (gasmix_is_air(mix)) { + mix->o2.permille = 0; + return; + } + } + + /* Sane mix? */ + if (o2 <= 1000 && he <= 1000 && o2 + he <= 1000) + return; + fprintf(stderr, "Odd gasmix: %u O2 %u He\n", o2, he); + memset(mix, 0, sizeof(*mix)); +} + +/* + * See if the size/workingpressure looks like some standard cylinder + * size, eg "AL80". + * + * NOTE! We don't take compressibility into account when naming + * cylinders. That makes a certain amount of sense, since the + * cylinder name is independent from the gasmix, and different + * gasmixes have different compressibility. + */ +static void match_standard_cylinder(cylinder_type_t *type) +{ + double cuft, bar; + int psi, len; + const char *fmt; + char buffer[40], *p; + + /* Do we already have a cylinder description? */ + if (type->description) + return; + + bar = type->workingpressure.mbar / 1000.0; + cuft = ml_to_cuft(type->size.mliter); + cuft *= bar_to_atm(bar); + psi = to_PSI(type->workingpressure); + + switch (psi) { + case 2300 ... 2500: /* 2400 psi: LP tank */ + fmt = "LP%d"; + break; + case 2600 ... 2700: /* 2640 psi: LP+10% */ + fmt = "LP%d"; + break; + case 2900 ... 3100: /* 3000 psi: ALx tank */ + fmt = "AL%d"; + break; + case 3400 ... 3500: /* 3442 psi: HP tank */ + fmt = "HP%d"; + break; + case 3700 ... 3850: /* HP+10% */ + fmt = "HP%d+"; + break; + default: + return; + } + len = snprintf(buffer, sizeof(buffer), fmt, (int)rint(cuft)); + p = malloc(len + 1); + if (!p) + return; + memcpy(p, buffer, len + 1); + type->description = p; +} + + +/* + * There are two ways to give cylinder size information: + * - total amount of gas in cuft (depends on working pressure and physical size) + * - physical size + * + * where "physical size" is the one that actually matters and is sane. + * + * We internally use physical size only. But we save the workingpressure + * so that we can do the conversion if required. + */ +static void sanitize_cylinder_type(cylinder_type_t *type) +{ + double volume_of_air, volume; + + /* If we have no working pressure, it had *better* be just a physical size! */ + if (!type->workingpressure.mbar) + return; + + /* No size either? Nothing to go on */ + if (!type->size.mliter) + return; + + if (xml_parsing_units.volume == CUFT) { + double bar = type->workingpressure.mbar / 1000.0; + /* confusing - we don't really start from ml but millicuft !*/ + volume_of_air = cuft_to_l(type->size.mliter); + /* milliliters at 1 atm: not corrected for compressibility! */ + volume = volume_of_air / bar_to_atm(bar); + type->size.mliter = rint(volume); + } + + /* Ok, we have both size and pressure: try to match a description */ + match_standard_cylinder(type); +} + +static void sanitize_cylinder_info(struct dive *dive) +{ + int i; + + for (i = 0; i < MAX_CYLINDERS; i++) { + sanitize_gasmix(&dive->cylinder[i].gasmix); + sanitize_cylinder_type(&dive->cylinder[i].type); + } +} + +/* some events should never be thrown away */ +static bool is_potentially_redundant(struct event *event) +{ + if (!strcmp(event->name, "gaschange")) + return false; + if (!strcmp(event->name, "bookmark")) + return false; + if (!strcmp(event->name, "heading")) + return false; + return true; +} + +/* match just by name - we compare the details in the code that uses this helper */ +static struct event *find_previous_event(struct divecomputer *dc, struct event *event) +{ + struct event *ev = dc->events; + struct event *previous = NULL; + + if (same_string(event->name, "")) + return NULL; + while (ev && ev != event) { + if (same_string(ev->name, event->name)) + previous = ev; + ev = ev->next; + } + return previous; +} + +static void fixup_surface_pressure(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->surface_pressure.mbar) { + sum += dc->surface_pressure.mbar; + nr++; + } + } + if (nr) + dive->surface_pressure.mbar = (sum + nr / 2) / nr; +} + +static void fixup_water_salinity(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->salinity) { + if (dc->salinity < 500) + dc->salinity += FRESHWATER_SALINITY; + sum += dc->salinity; + nr++; + } + } + if (nr) + dive->salinity = (sum + nr / 2) / nr; +} + +static void fixup_meandepth(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->meandepth.mm) { + sum += dc->meandepth.mm; + nr++; + } + } + if (nr) + dive->meandepth.mm = (sum + nr / 2) / nr; +} + +static void fixup_duration(struct dive *dive) +{ + struct divecomputer *dc; + unsigned int duration = 0; + + for_each_dc (dive, dc) + duration = MAX(duration, dc->duration.seconds); + + dive->duration.seconds = duration; +} + +/* + * What do the dive computers say the water temperature is? + * (not in the samples, but as dc property for dcs that support that) + */ +unsigned int dc_watertemp(struct divecomputer *dc) +{ + int sum = 0, nr = 0; + + do { + if (dc->watertemp.mkelvin) { + sum += dc->watertemp.mkelvin; + nr++; + } + } while ((dc = dc->next) != NULL); + if (!nr) + return 0; + return (sum + nr / 2) / nr; +} + +static void fixup_watertemp(struct dive *dive) +{ + if (!dive->watertemp.mkelvin) + dive->watertemp.mkelvin = dc_watertemp(&dive->dc); +} + +/* + * What do the dive computers say the air temperature is? + */ +unsigned int dc_airtemp(struct divecomputer *dc) +{ + int sum = 0, nr = 0; + + do { + if (dc->airtemp.mkelvin) { + sum += dc->airtemp.mkelvin; + nr++; + } + } while ((dc = dc->next) != NULL); + if (!nr) + return 0; + return (sum + nr / 2) / nr; +} + +static void fixup_cylinder_use(struct dive *dive) // for CCR dives, store the indices +{ // of the oxygen and diluent cylinders + dive->oxygen_cylinder_index = get_cylinder_idx_by_use(dive, OXYGEN); + dive->diluent_cylinder_index = get_cylinder_idx_by_use(dive, DILUENT); +} + +static void fixup_airtemp(struct dive *dive) +{ + if (!dive->airtemp.mkelvin) + dive->airtemp.mkelvin = dc_airtemp(&dive->dc); +} + +/* zero out the airtemp in the dive structure if it was just created by + * running fixup on the dive. keep it if it had been edited by hand */ +static void un_fixup_airtemp(struct dive *a) +{ + if (a->airtemp.mkelvin && a->airtemp.mkelvin == dc_airtemp(&a->dc)) + a->airtemp.mkelvin = 0; +} + +/* + * events are stored as a linked list, so the concept of + * "consecutive, identical events" is somewhat hard to + * implement correctly (especially given that on some dive + * computers events are asynchronous, so they can come in + * between what would be the non-constant sample rate). + * + * So what we do is that we throw away clearly redundant + * events that are fewer than 61 seconds apart (assuming there + * is no dive computer with a sample rate of more than 60 + * seconds... that would be pretty pointless to plot the + * profile with) + * + * We first only mark the events for deletion so that we + * still know when the previous event happened. + */ +static void fixup_dc_events(struct divecomputer *dc) +{ + struct event *event; + + event = dc->events; + while (event) { + struct event *prev; + if (is_potentially_redundant(event)) { + prev = find_previous_event(dc, event); + if (prev && prev->value == event->value && + prev->flags == event->flags && + event->time.seconds - prev->time.seconds < 61) + event->deleted = true; + } + event = event->next; + } + event = dc->events; + while (event) { + if (event->next && event->next->deleted) { + struct event *nextnext = event->next->next; + free(event->next); + event->next = nextnext; + } else { + event = event->next; + } + } +} + +static int interpolate_depth(struct divecomputer *dc, int idx, int lastdepth, int lasttime, int now) +{ + int i; + int nextdepth = lastdepth; + int nexttime = now; + + for (i = idx+1; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + if (sample->depth.mm < 0) + continue; + nextdepth = sample->depth.mm; + nexttime = sample->time.seconds; + break; + } + return interpolate(lastdepth, nextdepth, now-lasttime, nexttime-lasttime); +} + +static void fixup_dc_depths(struct dive *dive, struct divecomputer *dc) +{ + int i; + int maxdepth = dc->maxdepth.mm; + int lasttime = 0, lastdepth = 0; + + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int depth = sample->depth.mm; + + if (depth < 0) { + depth = interpolate_depth(dc, i, lastdepth, lasttime, time); + sample->depth.mm = depth; + } + + if (depth > SURFACE_THRESHOLD) { + if (depth > maxdepth) + maxdepth = depth; + } + + lastdepth = depth; + lasttime = time; + if (sample->cns > dive->maxcns) + dive->maxcns = sample->cns; + } + + update_depth(&dc->maxdepth, maxdepth); + if (maxdepth > dive->maxdepth.mm) + dive->maxdepth.mm = maxdepth; +} + +static void fixup_dc_temp(struct dive *dive, struct divecomputer *dc) +{ + int i; + int mintemp = 0, lasttemp = 0; + + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int temp = sample->temperature.mkelvin; + + if (temp) { + /* + * If we have consecutive identical + * temperature readings, throw away + * the redundant ones. + */ + if (lasttemp == temp) + sample->temperature.mkelvin = 0; + else + lasttemp = temp; + + if (!mintemp || temp < mintemp) + mintemp = temp; + } + + update_min_max_temperatures(dive, sample->temperature); + } + update_temperature(&dc->watertemp, mintemp); + update_min_max_temperatures(dive, dc->watertemp); +} + +/* + * Fix up cylinder sensor information in the samples if we have + * an explicit first cylinder + */ +static void fixup_dc_cylinder_index(struct dive *dive, struct divecomputer *dc) +{ + int i; + int first_cylinder = explicit_first_cylinder(dive, dc); + + if (!first_cylinder) + return; + + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + + if (sample->sensor == 0) + sample->sensor = first_cylinder; + } +} + +/* + * Simplify dc pressure information: + * (a) Remove redundant pressure information + * (b) Remove linearly interpolated pressure data + */ +static void simplify_dc_pressures(struct dive *dive, struct divecomputer *dc) +{ + int i, j; + int lastindex = -1; + int lastpressure = 0, lasto2pressure = 0; + int pressure_delta[MAX_CYLINDERS] = { INT_MAX, }; + + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int pressure = sample->cylinderpressure.mbar; + int o2_pressure = sample->o2cylinderpressure.mbar; + int index; + + index = sample->sensor; + + if (index == lastindex) { + /* Remove duplicate redundant pressure information */ + if (pressure == lastpressure) + sample->cylinderpressure.mbar = 0; + if (o2_pressure == lasto2pressure) + sample->o2cylinderpressure.mbar = 0; + /* check for simply linear data in the samples + +INT_MAX means uninitialized, -INT_MAX means not linear */ + if (pressure_delta[index] != -INT_MAX && lastpressure) { + if (pressure_delta[index] == INT_MAX) { + pressure_delta[index] = abs(pressure - lastpressure); + } else { + int cur_delta = abs(pressure - lastpressure); + if (cur_delta && abs(cur_delta - pressure_delta[index]) > 150) { + /* ok the samples aren't just a linearisation + * between start and end */ + pressure_delta[index] = -INT_MAX; + } + } + } + } + lastindex = index; + lastpressure = pressure; + lasto2pressure = o2_pressure; + } + + /* if all the samples for a cylinder have pressure data that + * is basically equidistant throw out the sample cylinder pressure + * information but make sure we still have a valid start and end + * pressure + * this happens when DivingLog decides to linearalize the + * pressure between beginning and end and for strange reasons + * decides to put that in the sample data as if it came from + * the dive computer; we don't want that (we'll visualize with + * constant SAC rate instead) + * WARNING WARNING - I have only seen this in single tank dives + * --- maybe I should try to create a multi tank dive and see what + * --- divinglog does there - but the code right now is only tested + * --- for the single tank case */ + for (j = 0; j < MAX_CYLINDERS; j++) { + if (abs(pressure_delta[j]) != INT_MAX) { + cylinder_t *cyl = dive->cylinder + j; + for (i = 0; i < dc->samples; i++) + if (dc->sample[i].sensor == j) + dc->sample[i].cylinderpressure.mbar = 0; + if (!cyl->start.mbar) + cyl->start.mbar = cyl->sample_start.mbar; + if (!cyl->end.mbar) + cyl->end.mbar = cyl->sample_end.mbar; + cyl->sample_start.mbar = 0; + cyl->sample_end.mbar = 0; + } + } +} + +/* FIXME! sensor -> cylinder mapping? */ +static void fixup_start_pressure(struct dive *dive, int idx, pressure_t p) +{ + if (idx >= 0 && idx < MAX_CYLINDERS) { + cylinder_t *cyl = dive->cylinder + idx; + if (p.mbar && !cyl->sample_start.mbar) + cyl->sample_start = p; + } +} + +static void fixup_end_pressure(struct dive *dive, int idx, pressure_t p) +{ + if (idx >= 0 && idx < MAX_CYLINDERS) { + cylinder_t *cyl = dive->cylinder + idx; + if (p.mbar && !cyl->sample_end.mbar) + cyl->sample_end = p; + } +} + +/* + * Check the cylinder pressure sample information and fill in the + * overall cylinder pressures from those. + * + * We ignore surface samples for tank pressure information. + * + * At the beginning of the dive, let the cylinder cool down + * if the diver starts off at the surface. And at the end + * of the dive, there may be surface pressures where the + * diver has already turned off the air supply (especially + * for computers like the Uemis Zurich that end up saving + * quite a bit of samples after the dive has ended). + */ +static void fixup_dive_pressures(struct dive *dive, struct divecomputer *dc) +{ + int i, o2index = -1; + + if (dive->dc.divemode == CCR) + o2index = get_cylinder_idx_by_use(dive, OXYGEN); + + /* Walk the samples from the beginning to find starting pressures.. */ + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + + if (sample->depth.mm < SURFACE_THRESHOLD) + continue; + + fixup_start_pressure(dive, sample->sensor, sample->cylinderpressure); + fixup_start_pressure(dive, o2index, sample->o2cylinderpressure); + } + + /* ..and from the end for ending pressures */ + for (i = dc->samples; --i >= 0; ) { + struct sample *sample = dc->sample + i; + + if (sample->depth.mm < SURFACE_THRESHOLD) + continue; + + fixup_end_pressure(dive, sample->sensor, sample->cylinderpressure); + fixup_end_pressure(dive, o2index, sample->o2cylinderpressure); + } + + simplify_dc_pressures(dive, dc); +} + +static void fixup_dive_dc(struct dive *dive, struct divecomputer *dc) +{ + int i; + + /* Add device information to table */ + if (dc->deviceid && (dc->serial || dc->fw_version)) + create_device_node(dc->model, dc->deviceid, dc->serial, dc->fw_version, ""); + + /* Fixup duration and mean depth */ + fixup_dc_duration(dc); + + /* Fix up sample depth data */ + fixup_dc_depths(dive, dc); + + /* Fix up dive temperatures based on dive computer samples */ + fixup_dc_temp(dive, dc); + + /* Fix up cylinder sensor data */ + fixup_dc_cylinder_index(dive, dc); + + /* Fix up cylinder pressures based on DC info */ + fixup_dive_pressures(dive, dc); + + fixup_dc_events(dc); +} + +struct dive *fixup_dive(struct dive *dive) +{ + int i; + struct divecomputer *dc; + + sanitize_cylinder_info(dive); + dive->maxcns = dive->cns; + + /* + * Use the dive's temperatures for minimum and maximum in case + * we do not have temperatures recorded by DC. + */ + + update_min_max_temperatures(dive, dive->watertemp); + + for_each_dc (dive, dc) + fixup_dive_dc(dive, dc); + + fixup_water_salinity(dive); + fixup_surface_pressure(dive); + fixup_meandepth(dive); + fixup_duration(dive); + fixup_watertemp(dive); + fixup_airtemp(dive); + fixup_cylinder_use(dive); // store indices for CCR oxygen and diluent cylinders + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + add_cylinder_description(&cyl->type); + if (same_rounded_pressure(cyl->sample_start, cyl->start)) + cyl->start.mbar = 0; + if (same_rounded_pressure(cyl->sample_end, cyl->end)) + cyl->end.mbar = 0; + } + update_cylinder_related_info(dive); + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + weightsystem_t *ws = dive->weightsystem + i; + add_weightsystem_description(ws); + } + /* we should always have a uniq ID as that gets assigned during alloc_dive(), + * but we want to make sure... */ + if (!dive->id) + dive->id = dive_getUniqID(dive); + + return dive; +} + +/* Don't pick a zero for MERGE_MIN() */ +#define MERGE_MAX(res, a, b, n) res->n = MAX(a->n, b->n) +#define MERGE_MIN(res, a, b, n) res->n = (a->n) ? (b->n) ? MIN(a->n, b->n) : (a->n) : (b->n) +#define MERGE_TXT(res, a, b, n) res->n = merge_text(a->n, b->n) +#define MERGE_NONZERO(res, a, b, n) res->n = a->n ? a->n : b->n + +struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc) +{ + struct sample *p = prepare_sample(dc); + + if (p) { + *p = *sample; + p->time.seconds = time; + finish_sample(dc); + } + return p; +} + +/* + * This is like add_sample(), but if the distance from the last sample + * is excessive, we add two surface samples in between. + * + * This is so that if you merge two non-overlapping dives, we make sure + * that the time in between the dives is at the surface, not some "last + * sample that happened to be at a depth of 1.2m". + */ +static void merge_one_sample(struct sample *sample, int time, struct divecomputer *dc) +{ + int last = dc->samples - 1; + if (last >= 0) { + static struct sample surface; + struct sample *prev = dc->sample + last; + int last_time = prev->time.seconds; + int last_depth = prev->depth.mm; + + /* + * Only do surface events if the samples are more than + * a minute apart, and shallower than 5m + */ + if (time > last_time + 60 && last_depth < 5000) { + add_sample(&surface, last_time + 20, dc); + add_sample(&surface, time - 20, dc); + } + } + add_sample(sample, time, dc); +} + + +/* + * Merge samples. Dive 'a' is "offset" seconds before Dive 'b' + */ +static void merge_samples(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int offset) +{ + int asamples = a->samples; + int bsamples = b->samples; + struct sample *as = a->sample; + struct sample *bs = b->sample; + + /* + * We want a positive sample offset, so that sample + * times are always positive. So if the samples for + * 'b' are before the samples for 'a' (so the offset + * is negative), we switch a and b around, and use + * the reverse offset. + */ + if (offset < 0) { + offset = -offset; + asamples = bsamples; + bsamples = a->samples; + as = bs; + bs = a->sample; + } + + for (;;) { + int at, bt; + struct sample sample; + + if (!res) + return; + + at = asamples ? as->time.seconds : -1; + bt = bsamples ? bs->time.seconds + offset : -1; + + /* No samples? All done! */ + if (at < 0 && bt < 0) + return; + + /* Only samples from a? */ + if (bt < 0) { + add_sample_a: + merge_one_sample(as, at, res); + as++; + asamples--; + continue; + } + + /* Only samples from b? */ + if (at < 0) { + add_sample_b: + merge_one_sample(bs, bt, res); + bs++; + bsamples--; + continue; + } + + if (at < bt) + goto add_sample_a; + if (at > bt) + goto add_sample_b; + + /* same-time sample: add a merged sample. Take the non-zero ones */ + sample = *bs; + if (as->depth.mm) + sample.depth = as->depth; + if (as->temperature.mkelvin) + sample.temperature = as->temperature; + if (as->cylinderpressure.mbar) + sample.cylinderpressure = as->cylinderpressure; + if (as->sensor) + sample.sensor = as->sensor; + if (as->cns) + sample.cns = as->cns; + if (as->setpoint.mbar) + sample.setpoint = as->setpoint; + if (as->ndl.seconds) + sample.ndl = as->ndl; + if (as->stoptime.seconds) + sample.stoptime = as->stoptime; + if (as->stopdepth.mm) + sample.stopdepth = as->stopdepth; + if (as->in_deco) + sample.in_deco = true; + + merge_one_sample(&sample, at, res); + + as++; + bs++; + asamples--; + bsamples--; + } +} + +static char *merge_text(const char *a, const char *b) +{ + char *res; + if (!a && !b) + return NULL; + if (!a || !*a) + return copy_string(b); + if (!b || !*b) + return strdup(a); + if (!strcmp(a, b)) + return copy_string(a); + res = malloc(strlen(a) + strlen(b) + 32); + if (!res) + return (char *)a; + sprintf(res, translate("gettextFromC", "(%s) or (%s)"), a, b); + return res; +} + +#define SORT(a, b, field) \ + if (a->field != b->field) \ + return a->field < b->field ? -1 : 1 + +static int sort_event(struct event *a, struct event *b) +{ + SORT(a, b, time.seconds); + SORT(a, b, type); + SORT(a, b, flags); + SORT(a, b, value); + return strcmp(a->name, b->name); +} + +static void merge_events(struct divecomputer *res, struct divecomputer *src1, struct divecomputer *src2, int offset) +{ + struct event *a, *b; + struct event **p = &res->events; + + /* Always use positive offsets */ + if (offset < 0) { + struct divecomputer *tmp; + + offset = -offset; + tmp = src1; + src1 = src2; + src2 = tmp; + } + + a = src1->events; + b = src2->events; + while (b) { + b->time.seconds += offset; + b = b->next; + } + b = src2->events; + + while (a || b) { + int s; + if (!b) { + *p = a; + break; + } + if (!a) { + *p = b; + break; + } + s = sort_event(a, b); + /* Pick b */ + if (s > 0) { + *p = b; + p = &b->next; + b = b->next; + continue; + } + /* Pick 'a' or neither */ + if (s < 0) { + *p = a; + p = &a->next; + } + a = a->next; + continue; + } +} + +/* Pick whichever has any info (if either). Prefer 'a' */ +static void merge_cylinder_type(cylinder_type_t *src, cylinder_type_t *dst) +{ + if (!dst->size.mliter) + dst->size.mliter = src->size.mliter; + if (!dst->workingpressure.mbar) + dst->workingpressure.mbar = src->workingpressure.mbar; + if (!dst->description) { + dst->description = src->description; + src->description = NULL; + } +} + +static void merge_cylinder_mix(struct gasmix *src, struct gasmix *dst) +{ + if (!dst->o2.permille) + *dst = *src; +} + +static void merge_cylinder_info(cylinder_t *src, cylinder_t *dst) +{ + merge_cylinder_type(&src->type, &dst->type); + merge_cylinder_mix(&src->gasmix, &dst->gasmix); + MERGE_MAX(dst, dst, src, start.mbar); + MERGE_MIN(dst, dst, src, end.mbar); +} + +static void merge_weightsystem_info(weightsystem_t *res, weightsystem_t *a, weightsystem_t *b) +{ + if (!a->weight.grams) + a = b; + *res = *a; +} + +/* get_cylinder_idx_by_use(): Find the index of the first cylinder with a particular CCR use type. + * The index returned corresponds to that of the first cylinder with a cylinder_use that + * equals the appropriate enum value [oxygen, diluent, bailout] given by cylinder_use_type. + * A negative number returned indicates that a match could not be found. + * Call parameters: dive = the dive being processed + * cylinder_use_type = an enum, one of {oxygen, diluent, bailout} */ +extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type) +{ + int cylinder_index; + for (cylinder_index = 0; cylinder_index < MAX_CYLINDERS; cylinder_index++) { + if (dive->cylinder[cylinder_index].cylinder_use == cylinder_use_type) + return cylinder_index; // return the index of the cylinder with that cylinder use type + } + return -1; // negative number means cylinder_use_type not found in list of cylinders +} + +int gasmix_distance(const struct gasmix *a, const struct gasmix *b) +{ + int a_o2 = get_o2(a), b_o2 = get_o2(b); + int a_he = get_he(a), b_he = get_he(b); + int delta_o2 = a_o2 - b_o2, delta_he = a_he - b_he; + + delta_he = delta_he * delta_he; + delta_o2 = delta_o2 * delta_o2; + return delta_he + delta_o2; +} + +/* fill_pressures(): Compute partial gas pressures in bar from gasmix and ambient pressures, possibly for OC or CCR, to be + * extended to PSCT. This function does the calculations of gas pressures applicable to a single point on the dive profile. + * The structure "pressures" is used to return calculated gas pressures to the calling software. + * Call parameters: po2 = po2 value applicable to the record in calling function + * amb_pressure = ambient pressure applicable to the record in calling function + * *pressures = structure for communicating o2 sensor values from and gas pressures to the calling function. + * *mix = structure containing cylinder gas mixture information. + * This function called by: calculate_gas_information_new() in profile.c; add_segment() in deco.c. + */ +extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type divemode) +{ + if (po2) { // This is probably a CCR dive where pressures->o2 is defined + if (po2 >= amb_pressure) { + pressures->o2 = amb_pressure; + pressures->n2 = pressures->he = 0.0; + } else { + pressures->o2 = po2; + if (get_o2(mix) == 1000) { + pressures->he = pressures->n2 = 0; + } else { + pressures->he = (amb_pressure - pressures->o2) * (double)get_he(mix) / (1000 - get_o2(mix)); + pressures->n2 = amb_pressure - pressures->o2 - pressures->he; + } + } + } else { + if (divemode == PSCR) { /* The steady state approximation should be good enough */ + pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure - (1.0 - get_o2(mix) / 1000.0) * prefs.o2consumption / (prefs.bottomsac * prefs.pscr_ratio / 1000.0); + if (pressures->o2 < 0) // He's dead, Jim. + pressures->o2 = 0; + if (get_o2(mix) != 1000) { + pressures->he = (amb_pressure - pressures->o2) * get_he(mix) / (1000.0 - get_o2(mix)); + pressures->n2 = (amb_pressure - pressures->o2) * (1000 - get_o2(mix) - get_he(mix)) / (1000.0 - get_o2(mix)); + } else { + pressures->he = pressures->n2 = 0; + } + } else { + // Open circuit dives: no gas pressure values available, they need to be calculated + pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure; // These calculations are also used if the CCR calculation above.. + pressures->he = get_he(mix) / 1000.0 * amb_pressure; // ..returned a po2 of zero (i.e. o2 sensor data not resolvable) + pressures->n2 = (1000 - get_o2(mix) - get_he(mix)) / 1000.0 * amb_pressure; + } + } +} + +static int find_cylinder_match(cylinder_t *cyl, cylinder_t array[], unsigned int used) +{ + int i; + int best = -1, score = INT_MAX; + + if (cylinder_nodata(cyl)) + return -1; + for (i = 0; i < MAX_CYLINDERS; i++) { + const cylinder_t *match; + int distance; + + if (used & (1 << i)) + continue; + match = array + i; + distance = gasmix_distance(&cyl->gasmix, &match->gasmix); + if (distance >= score) + continue; + best = i; + score = distance; + } + return best; +} + +/* Force an initial gaschange event to the (old) gas #0 */ +static void add_initial_gaschange(struct dive *dive, struct divecomputer *dc) +{ + struct event *ev = get_next_event(dc->events, "gaschange"); + + if (ev && ev->time.seconds < 30) + return; + + /* Old starting gas mix */ + add_gas_switch_event(dive, dc, 0, 0); +} + +void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]) +{ + int i; + struct event *ev; + + /* Did the first gas get remapped? Add gas switch event */ + if (mapping[0] > 0) + add_initial_gaschange(dive, dc); + + /* Remap the sensor indexes */ + for (i = 0; i < dc->samples; i++) { + struct sample *s = dc->sample + i; + int sensor; + + if (!s->cylinderpressure.mbar) + continue; + sensor = mapping[s->sensor]; + if (sensor >= 0) + s->sensor = sensor; + } + + /* Remap the gas change indexes */ + for (ev = dc->events; ev; ev = ev->next) { + if (!event_is_gaschange(ev)) + continue; + if (ev->gas.index < 0) + continue; + ev->gas.index = mapping[ev->gas.index]; + } +} + +/* + * If the cylinder indexes change (due to merging dives or deleting + * cylinders in the middle), we need to change the indexes in the + * dive computer data for this dive. + * + * Also note that we assume that the initial cylinder is cylinder 0, + * so if that got renamed, we need to create a fake gas change event + */ +static void cylinder_renumber(struct dive *dive, int mapping[]) +{ + struct divecomputer *dc; + for_each_dc (dive, dc) + dc_cylinder_renumber(dive, dc, mapping); +} + +/* + * Merging cylinder information is non-trivial, because the two dive computers + * may have different ideas of what the different cylinder indexing is. + * + * Logic: take all the cylinder information from the preferred dive ('a'), and + * then try to match each of the cylinders in the other dive by the gasmix that + * is the best match and hasn't been used yet. + */ +static void merge_cylinders(struct dive *res, struct dive *a, struct dive *b) +{ + int i, renumber = 0; + int mapping[MAX_CYLINDERS]; + unsigned int used = 0; + + /* Copy the cylinder info raw from 'a' */ + memcpy(res->cylinder, a->cylinder, sizeof(res->cylinder)); + memset(a->cylinder, 0, sizeof(a->cylinder)); + + for (i = 0; i < MAX_CYLINDERS; i++) { + int j; + cylinder_t *cyl = b->cylinder + i; + + j = find_cylinder_match(cyl, res->cylinder, used); + mapping[i] = j; + if (j < 0) + continue; + used |= 1 << j; + merge_cylinder_info(cyl, res->cylinder + j); + + /* If that renumbered the cylinders, fix it up! */ + if (i != j) + renumber = 1; + } + if (renumber) + cylinder_renumber(b, mapping); +} + +static void merge_equipment(struct dive *res, struct dive *a, struct dive *b) +{ + int i; + + merge_cylinders(res, a, b); + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + merge_weightsystem_info(res->weightsystem + i, a->weightsystem + i, b->weightsystem + i); +} + +static void merge_airtemps(struct dive *res, struct dive *a, struct dive *b) +{ + un_fixup_airtemp(a); + un_fixup_airtemp(b); + MERGE_NONZERO(res, a, b, airtemp.mkelvin); +} + +/* + * When merging two dives, this picks the trip from one, and removes it + * from the other. + * + * The 'next' dive is not involved in the dive merging, but is the dive + * that will be the next dive after the merged dive. + */ +static void pick_trip(struct dive *res, struct dive *pick) +{ + tripflag_t tripflag = pick->tripflag; + dive_trip_t *trip = pick->divetrip; + + res->tripflag = tripflag; + add_dive_to_trip(res, trip); +} + +/* + * Pick a trip for a dive + */ +static void merge_trip(struct dive *res, struct dive *a, struct dive *b) +{ + dive_trip_t *atrip, *btrip; + + /* + * The larger tripflag is more relevant: we prefer + * take manually assigned trips over auto-generated + * ones. + */ + if (a->tripflag > b->tripflag) + goto pick_a; + + if (a->tripflag < b->tripflag) + goto pick_b; + + /* Otherwise, look at the trip data and pick the "better" one */ + atrip = a->divetrip; + btrip = b->divetrip; + if (!atrip) + goto pick_b; + if (!btrip) + goto pick_a; + if (!atrip->location) + goto pick_b; + if (!btrip->location) + goto pick_a; + if (!atrip->notes) + goto pick_b; + if (!btrip->notes) + goto pick_a; + + /* + * Ok, so both have location and notes. + * Pick the earlier one. + */ + if (a->when < b->when) + goto pick_a; + goto pick_b; + +pick_a: + b = a; +pick_b: + pick_trip(res, b); +} + +#if CURRENTLY_NOT_USED +/* + * Sample 's' is between samples 'a' and 'b'. It is 'offset' seconds before 'b'. + * + * If 's' and 'a' are at the same time, offset is 0, and b is NULL. + */ +static int compare_sample(struct sample *s, struct sample *a, struct sample *b, int offset) +{ + unsigned int depth = a->depth.mm; + int diff; + + if (offset) { + unsigned int interval = b->time.seconds - a->time.seconds; + unsigned int depth_a = a->depth.mm; + unsigned int depth_b = b->depth.mm; + + if (offset > interval) + return -1; + + /* pick the average depth, scaled by the offset from 'b' */ + depth = (depth_a * offset) + (depth_b * (interval - offset)); + depth /= interval; + } + diff = s->depth.mm - depth; + if (diff < 0) + diff = -diff; + /* cut off at one meter difference */ + if (diff > 1000) + diff = 1000; + return diff * diff; +} + +/* + * Calculate a "difference" in samples between the two dives, given + * the offset in seconds between them. Use this to find the best + * match of samples between two different dive computers. + */ +static unsigned long sample_difference(struct divecomputer *a, struct divecomputer *b, int offset) +{ + int asamples = a->samples; + int bsamples = b->samples; + struct sample *as = a->sample; + struct sample *bs = b->sample; + unsigned long error = 0; + int start = -1; + + if (!asamples || !bsamples) + return 0; + + /* + * skip the first sample - this way we know can always look at + * as/bs[-1] to look at the samples around it in the loop. + */ + as++; + bs++; + asamples--; + bsamples--; + + for (;;) { + int at, bt, diff; + + + /* If we run out of samples, punt */ + if (!asamples) + return INT_MAX; + if (!bsamples) + return INT_MAX; + + at = as->time.seconds; + bt = bs->time.seconds + offset; + + /* b hasn't started yet? Ignore it */ + if (bt < 0) { + bs++; + bsamples--; + continue; + } + + if (at < bt) { + diff = compare_sample(as, bs - 1, bs, bt - at); + as++; + asamples--; + } else if (at > bt) { + diff = compare_sample(bs, as - 1, as, at - bt); + bs++; + bsamples--; + } else { + diff = compare_sample(as, bs, NULL, 0); + as++; + bs++; + asamples--; + bsamples--; + } + + /* Invalid comparison point? */ + if (diff < 0) + continue; + + if (start < 0) + start = at; + + error += diff; + + if (at - start > 120) + break; + } + return error; +} + +/* + * Dive 'a' is 'offset' seconds before dive 'b' + * + * This is *not* because the dive computers clocks aren't in sync, + * it is because the dive computers may "start" the dive at different + * points in the dive, so the sample at time X in dive 'a' is the + * same as the sample at time X+offset in dive 'b'. + * + * For example, some dive computers take longer to "wake up" when + * they sense that you are under water (ie Uemis Zurich if it was off + * when the dive started). And other dive computers have different + * depths that they activate at, etc etc. + * + * If we cannot find a shared offset, don't try to merge. + */ +static int find_sample_offset(struct divecomputer *a, struct divecomputer *b) +{ + int offset, best; + unsigned long max; + + /* No samples? Merge at any time (0 offset) */ + if (!a->samples) + return 0; + if (!b->samples) + return 0; + + /* + * Common special-case: merging a dive that came from + * the same dive computer, so the samples are identical. + * Check this first, without wasting time trying to find + * some minimal offset case. + */ + best = 0; + max = sample_difference(a, b, 0); + if (!max) + return 0; + + /* + * Otherwise, look if we can find anything better within + * a thirty second window.. + */ + for (offset = -30; offset <= 30; offset++) { + unsigned long diff; + + diff = sample_difference(a, b, offset); + if (diff > max) + continue; + best = offset; + max = diff; + } + + return best; +} +#endif + +/* + * Are a and b "similar" values, when given a reasonable lower end expected + * difference? + * + * So for example, we'd expect different dive computers to give different + * max depth readings. You might have them on different arms, and they + * have different pressure sensors and possibly different ideas about + * water salinity etc. + * + * So have an expected minimum difference, but also allow a larger relative + * error value. + */ +static int similar(unsigned long a, unsigned long b, unsigned long expected) +{ + if (!a && !b) + return 1; + + if (a && b) { + unsigned long min, max, diff; + + min = a; + max = b; + if (a > b) { + min = b; + max = a; + } + diff = max - min; + + /* Smaller than expected difference? */ + if (diff < expected) + return 1; + /* Error less than 10% or the maximum */ + if (diff * 10 < max) + return 1; + } + return 0; +} + +/* + * Match two dive computer entries against each other, and + * tell if it's the same dive. Return 0 if "don't know", + * positive for "same dive" and negative for "definitely + * not the same dive" + */ +int match_one_dc(struct divecomputer *a, struct divecomputer *b) +{ + /* Not same model? Don't know if matching.. */ + if (!a->model || !b->model) + return 0; + if (strcasecmp(a->model, b->model)) + return 0; + + /* Different device ID's? Don't know */ + if (a->deviceid != b->deviceid) + return 0; + + /* Do we have dive IDs? */ + if (!a->diveid || !b->diveid) + return 0; + + /* + * If they have different dive ID's on the same + * dive computer, that's a definite "same or not" + */ + return a->diveid == b->diveid ? 1 : -1; +} + +/* + * Match every dive computer against each other to see if + * we have a matching dive. + * + * Return values: + * -1 for "is definitely *NOT* the same dive" + * 0 for "don't know" + * 1 for "is definitely the same dive" + */ +static int match_dc_dive(struct divecomputer *a, struct divecomputer *b) +{ + do { + struct divecomputer *tmp = b; + do { + int match = match_one_dc(a, tmp); + if (match) + return match; + tmp = tmp->next; + } while (tmp); + a = a->next; + } while (a); + return 0; +} + +static bool new_without_trip(struct dive *a) +{ + return a->downloaded && !a->divetrip; +} + +/* + * Do we want to automatically try to merge two dives that + * look like they are the same dive? + * + * This happens quite commonly because you download a dive + * that you already had, or perhaps because you maintained + * multiple dive logs and want to load them all together + * (possibly one of them was imported from another dive log + * application entirely). + * + * NOTE! We mainly look at the dive time, but it can differ + * between two dives due to a few issues: + * + * - rounding the dive date to the nearest minute in other dive + * applications + * + * - dive computers with "relative datestamps" (ie the dive + * computer doesn't actually record an absolute date at all, + * but instead at download-time synchronizes its internal + * time with real-time on the downloading computer) + * + * - using multiple dive computers with different real time on + * the same dive + * + * We do not merge dives that look radically different, and if + * the dates are *too* far off the user will have to join two + * dives together manually. But this tries to handle the sane + * cases. + */ +static int likely_same_dive(struct dive *a, struct dive *b) +{ + int match, fuzz = 20 * 60; + + /* don't merge manually added dives with anything */ + if (same_string(a->dc.model, "manually added dive") || + same_string(b->dc.model, "manually added dive")) + return 0; + + /* Don't try to merge dives with different trip information */ + if (a->divetrip != b->divetrip) { + /* + * Exception: if the dive is downloaded without any + * explicit trip information, we do want to merge it + * with existing old dives even if they have trips. + */ + if (!new_without_trip(a) && !new_without_trip(b)) + return 0; + } + + /* + * Do some basic sanity testing of the values we + * have filled in during 'fixup_dive()' + */ + if (!similar(a->maxdepth.mm, b->maxdepth.mm, 1000) || + (a->meandepth.mm && b->meandepth.mm && !similar(a->meandepth.mm, b->meandepth.mm, 1000)) || + !similar(a->duration.seconds, b->duration.seconds, 5 * 60)) + return 0; + + /* See if we can get an exact match on the dive computer */ + match = match_dc_dive(&a->dc, &b->dc); + if (match) + return match > 0; + + /* + * Allow a time difference due to dive computer time + * setting etc. Check if they overlap. + */ + fuzz = MAX(a->duration.seconds, b->duration.seconds) / 2; + if (fuzz < 60) + fuzz = 60; + + return ((a->when <= b->when + fuzz) && (a->when >= b->when - fuzz)); +} + +/* + * This could do a lot more merging. Right now it really only + * merges almost exact duplicates - something that happens easily + * with overlapping dive downloads. + */ +struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded) +{ + if (likely_same_dive(a, b)) + return merge_dives(a, b, 0, prefer_downloaded); + return NULL; +} + +void free_events(struct event *ev) +{ + while (ev) { + struct event *next = ev->next; + free(ev); + ev = next; + } +} + +static void free_dc(struct divecomputer *dc) +{ + free(dc->sample); + free((void *)dc->model); + free_events(dc->events); + free(dc); +} + +static void free_pic(struct picture *picture) +{ + if (picture) { + free(picture->filename); + free(picture); + } +} + +static int same_sample(struct sample *a, struct sample *b) +{ + if (a->time.seconds != b->time.seconds) + return 0; + if (a->depth.mm != b->depth.mm) + return 0; + if (a->temperature.mkelvin != b->temperature.mkelvin) + return 0; + if (a->cylinderpressure.mbar != b->cylinderpressure.mbar) + return 0; + return a->sensor == b->sensor; +} + +static int same_dc(struct divecomputer *a, struct divecomputer *b) +{ + int i; + struct event *eva, *evb; + + i = match_one_dc(a, b); + if (i) + return i > 0; + + if (a->when && b->when && a->when != b->when) + return 0; + if (a->samples != b->samples) + return 0; + for (i = 0; i < a->samples; i++) + if (!same_sample(a->sample + i, b->sample + i)) + return 0; + eva = a->events; + evb = b->events; + while (eva && evb) { + if (!same_event(eva, evb)) + return 0; + eva = eva->next; + evb = evb->next; + } + return eva == evb; +} + +static int might_be_same_device(struct divecomputer *a, struct divecomputer *b) +{ + /* No dive computer model? That matches anything */ + if (!a->model || !b->model) + return 1; + + /* Otherwise at least the model names have to match */ + if (strcasecmp(a->model, b->model)) + return 0; + + /* No device ID? Match */ + if (!a->deviceid || !b->deviceid) + return 1; + + return a->deviceid == b->deviceid; +} + +static void remove_redundant_dc(struct divecomputer *dc, int prefer_downloaded) +{ + do { + struct divecomputer **p = &dc->next; + + /* Check this dc against all the following ones.. */ + while (*p) { + struct divecomputer *check = *p; + if (same_dc(dc, check) || (prefer_downloaded && might_be_same_device(dc, check))) { + *p = check->next; + check->next = NULL; + free_dc(check); + continue; + } + p = &check->next; + } + + /* .. and then continue down the chain, but we */ + prefer_downloaded = 0; + dc = dc->next; + } while (dc); +} + +static void clear_dc(struct divecomputer *dc) +{ + memset(dc, 0, sizeof(*dc)); +} + +static struct divecomputer *find_matching_computer(struct divecomputer *match, struct divecomputer *list) +{ + struct divecomputer *p; + + while ((p = list) != NULL) { + list = list->next; + + if (might_be_same_device(match, p)) + break; + } + return p; +} + + +static void copy_dive_computer(struct divecomputer *res, struct divecomputer *a) +{ + *res = *a; + res->model = copy_string(a->model); + res->samples = res->alloc_samples = 0; + res->sample = NULL; + res->events = NULL; + res->next = NULL; +} + +/* + * Join dive computers with a specific time offset between + * them. + * + * Use the dive computer ID's (or names, if ID's are missing) + * to match them up. If we find a matching dive computer, we + * merge them. If not, we just take the data from 'a'. + */ +static void interleave_dive_computers(struct divecomputer *res, + struct divecomputer *a, struct divecomputer *b, int offset) +{ + do { + struct divecomputer *match; + + copy_dive_computer(res, a); + + match = find_matching_computer(a, b); + if (match) { + merge_events(res, a, match, offset); + merge_samples(res, a, match, offset); + /* Use the diveid of the later dive! */ + if (offset > 0) + res->diveid = match->diveid; + } else { + res->sample = a->sample; + res->samples = a->samples; + res->events = a->events; + a->sample = NULL; + a->samples = 0; + a->events = NULL; + } + a = a->next; + if (!a) + break; + res->next = calloc(1, sizeof(struct divecomputer)); + res = res->next; + } while (res); +} + + +/* + * Join dive computer information. + * + * If we have old-style dive computer information (no model + * name etc), we will prefer a new-style one and just throw + * away the old. We're assuming it's a re-download. + * + * Otherwise, we'll just try to keep all the information, + * unless the user has specified that they prefer the + * downloaded computer, in which case we'll aggressively + * try to throw out old information that *might* be from + * that one. + */ +static void join_dive_computers(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int prefer_downloaded) +{ + struct divecomputer *tmp; + + if (a->model && !b->model) { + *res = *a; + clear_dc(a); + return; + } + if (b->model && !a->model) { + *res = *b; + clear_dc(b); + return; + } + + *res = *a; + clear_dc(a); + tmp = res; + while (tmp->next) + tmp = tmp->next; + + tmp->next = calloc(1, sizeof(*tmp)); + *tmp->next = *b; + clear_dc(b); + + remove_redundant_dc(res, prefer_downloaded); +} + +static bool tag_seen_before(struct tag_entry *start, struct tag_entry *before) +{ + while (start && start != before) { + if (same_string(start->tag->name, before->tag->name)) + return true; + start = start->next; + } + return false; +} + +/* remove duplicates and empty nodes */ +void taglist_cleanup(struct tag_entry **tag_list) +{ + struct tag_entry **tl = tag_list; + while (*tl) { + /* skip tags that are empty or that we have seen before */ + if (same_string((*tl)->tag->name, "") || tag_seen_before(*tag_list, *tl)) { + *tl = (*tl)->next; + continue; + } + tl = &(*tl)->next; + } +} + +int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len) +{ + int i = 0; + struct tag_entry *tmp; + tmp = tag_list; + memset(buffer, 0, len); + while (tmp != NULL) { + int newlength = strlen(tmp->tag->name); + if (i > 0) + newlength += 2; + if ((i + newlength) < len) { + if (i > 0) { + strcpy(buffer + i, ", "); + strcpy(buffer + i + 2, tmp->tag->name); + } else { + strcpy(buffer, tmp->tag->name); + } + } else { + return i; + } + i += newlength; + tmp = tmp->next; + } + return i; +} + +static inline void taglist_free_divetag(struct divetag *tag) +{ + if (tag->name != NULL) + free(tag->name); + if (tag->source != NULL) + free(tag->source); + free(tag); +} + +/* Add a tag to the tag_list, keep the list sorted */ +static struct divetag *taglist_add_divetag(struct tag_entry **tag_list, struct divetag *tag) +{ + struct tag_entry *next, *entry; + + while ((next = *tag_list) != NULL) { + int cmp = strcmp(next->tag->name, tag->name); + + /* Already have it? */ + if (!cmp) + return next->tag; + /* Is the entry larger? If so, insert here */ + if (cmp > 0) + break; + /* Continue traversing the list */ + tag_list = &next->next; + } + + /* Insert in front of it */ + entry = malloc(sizeof(struct tag_entry)); + entry->next = next; + entry->tag = tag; + *tag_list = entry; + return tag; +} + +struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag) +{ + size_t i = 0; + int is_default_tag = 0; + struct divetag *ret_tag, *new_tag; + const char *translation; + new_tag = malloc(sizeof(struct divetag)); + + for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) { + if (strcmp(default_tags[i], tag) == 0) { + is_default_tag = 1; + break; + } + } + /* Only translate default tags */ + if (is_default_tag) { + translation = translate("gettextFromC", tag); + new_tag->name = malloc(strlen(translation) + 1); + memcpy(new_tag->name, translation, strlen(translation) + 1); + new_tag->source = malloc(strlen(tag) + 1); + memcpy(new_tag->source, tag, strlen(tag) + 1); + } else { + new_tag->source = NULL; + new_tag->name = malloc(strlen(tag) + 1); + memcpy(new_tag->name, tag, strlen(tag) + 1); + } + /* Try to insert new_tag into g_tag_list if we are not operating on it */ + if (tag_list != &g_tag_list) { + ret_tag = taglist_add_divetag(&g_tag_list, new_tag); + /* g_tag_list already contains new_tag, free the duplicate */ + if (ret_tag != new_tag) + taglist_free_divetag(new_tag); + ret_tag = taglist_add_divetag(tag_list, ret_tag); + } else { + ret_tag = taglist_add_divetag(tag_list, new_tag); + if (ret_tag != new_tag) + taglist_free_divetag(new_tag); + } + return ret_tag; +} + +void taglist_free(struct tag_entry *entry) +{ + STRUCTURED_LIST_FREE(struct tag_entry, entry, free) +} + +/* Merge src1 and src2, write to *dst */ +static void taglist_merge(struct tag_entry **dst, struct tag_entry *src1, struct tag_entry *src2) +{ + struct tag_entry *entry; + + for (entry = src1; entry; entry = entry->next) + taglist_add_divetag(dst, entry->tag); + for (entry = src2; entry; entry = entry->next) + taglist_add_divetag(dst, entry->tag); +} + +void taglist_init_global() +{ + size_t i; + + for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) + taglist_add_tag(&g_tag_list, default_tags[i]); +} + +bool taglist_contains(struct tag_entry *tag_list, const char *tag) +{ + while (tag_list) { + if (same_string(tag_list->tag->name, tag)) + return true; + tag_list = tag_list->next; + } + return false; +} + +// check if all tags in subtl are included in supertl (so subtl is a subset of supertl) +static bool taglist_contains_all(struct tag_entry *subtl, struct tag_entry *supertl) +{ + while (subtl) { + if (!taglist_contains(supertl, subtl->tag->name)) + return false; + subtl = subtl->next; + } + return true; +} + +struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list) +{ + struct tag_entry *added_list = NULL; + while (new_list) { + if (!taglist_contains(original_list, new_list->tag->name)) + taglist_add_tag(&added_list, new_list->tag->name); + new_list = new_list->next; + } + return added_list; +} + +void dump_taglist(const char *intro, struct tag_entry *tl) +{ + char *comma = ""; + fprintf(stderr, "%s", intro); + while(tl) { + fprintf(stderr, "%s %s", comma, tl->tag->name); + comma = ","; + tl = tl->next; + } + fprintf(stderr, "\n"); +} + +// if tl1 is both a subset and superset of tl2 they must be the same +bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2) +{ + return taglist_contains_all(tl1, tl2) && taglist_contains_all(tl2, tl1); +} + +// count the dives where the tag list contains the given tag +int count_dives_with_tag(const char *tag) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(tag, "")) { + // count dives with no tags + if (d->tag_list == NULL) + counter++; + } else if (taglist_contains(d->tag_list, tag)) { + counter++; + } + } + return counter; +} + +extern bool string_sequence_contains(const char *string_sequence, const char *text); + +// count the dives where the person is included in the comma separated string sequences of buddies or divemasters +int count_dives_with_person(const char *person) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(person, "")) { + // solo dive + if (same_string(d->buddy, "") && same_string(d->divemaster, "")) + counter++; + } else if (string_sequence_contains(d->buddy, person) || string_sequence_contains(d->divemaster, person)) { + counter++; + } + } + return counter; +} + +// count the dives with exactly the location +int count_dives_with_location(const char *location) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(get_dive_location(d), location)) + counter++; + } + return counter; +} + +// count the dives with exactly the suit +int count_dives_with_suit(const char *suit) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(d->suit, suit)) + counter++; + } + return counter; +} + +/* + * Merging two dives can be subtle, because there's two different ways + * of merging: + * + * (a) two distinctly _different_ dives that have the same dive computer + * are merged into one longer dive, because the user asked for it + * in the divelist. + * + * Because this case is with the same dive computer, we *know* the + * two must have a different start time, and "offset" is the relative + * time difference between the two. + * + * (a) two different dive computers that we might want to merge into + * one single dive with multiple dive computers. + * + * This is the "try_to_merge()" case, which will have offset == 0, + * even if the dive times might be different. + */ +struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded) +{ + struct dive *res = alloc_dive(); + struct dive *dl = NULL; + + if (offset) { + /* + * If "likely_same_dive()" returns true, that means that + * it is *not* the same dive computer, and we do not want + * to try to turn it into a single longer dive. So we'd + * join them as two separate dive computers at zero offset. + */ + if (likely_same_dive(a, b)) + offset = 0; + } else { + /* Aim for newly downloaded dives to be 'b' (keep old dive data first) */ + if (a->downloaded && !b->downloaded) { + struct dive *tmp = a; + a = b; + b = tmp; + } + if (prefer_downloaded && b->downloaded) + dl = b; + } + + res->when = dl ? dl->when : a->when; + res->selected = a->selected || b->selected; + merge_trip(res, a, b); + MERGE_TXT(res, a, b, notes); + MERGE_TXT(res, a, b, buddy); + MERGE_TXT(res, a, b, divemaster); + MERGE_MAX(res, a, b, rating); + MERGE_TXT(res, a, b, suit); + MERGE_MAX(res, a, b, number); + MERGE_NONZERO(res, a, b, cns); + MERGE_NONZERO(res, a, b, visibility); + MERGE_NONZERO(res, a, b, picture_list); + taglist_merge(&res->tag_list, a->tag_list, b->tag_list); + merge_equipment(res, a, b); + merge_airtemps(res, a, b); + if (dl) { + /* If we prefer downloaded, do those first, and get rid of "might be same" computers */ + join_dive_computers(&res->dc, &dl->dc, &a->dc, 1); + } else if (offset && might_be_same_device(&a->dc, &b->dc)) + interleave_dive_computers(&res->dc, &a->dc, &b->dc, offset); + else + join_dive_computers(&res->dc, &a->dc, &b->dc, 0); + res->dive_site_uuid = a->dive_site_uuid ?: b->dive_site_uuid; + fixup_dive(res); + return res; +} + +// copy_dive(), but retaining the new ID for the copied dive +static struct dive *create_new_copy(struct dive *from) +{ + struct dive *to = alloc_dive(); + int id; + + // alloc_dive() gave us a new ID, we just need to + // make sure it's not overwritten. + id = to->id; + copy_dive(from, to); + to->id = id; + return to; +} + +static void force_fixup_dive(struct dive *d) +{ + struct divecomputer *dc = &d->dc; + int old_temp = dc->watertemp.mkelvin; + int old_mintemp = d->mintemp.mkelvin; + int old_maxtemp = d->maxtemp.mkelvin; + duration_t old_duration = d->duration; + + d->maxdepth.mm = 0; + dc->maxdepth.mm = 0; + d->watertemp.mkelvin = 0; + dc->watertemp.mkelvin = 0; + d->duration.seconds = 0; + d->maxtemp.mkelvin = 0; + d->mintemp.mkelvin = 0; + + fixup_dive(d); + + if (!d->watertemp.mkelvin) + d->watertemp.mkelvin = old_temp; + + if (!dc->watertemp.mkelvin) + dc->watertemp.mkelvin = old_temp; + + if (!d->maxtemp.mkelvin) + d->maxtemp.mkelvin = old_maxtemp; + + if (!d->mintemp.mkelvin) + d->mintemp.mkelvin = old_mintemp; + + if (!d->duration.seconds) + d->duration = old_duration; + +} + +/* + * Split a dive that has a surface interval from samples 'a' to 'b' + * into two dives. + */ +static int split_dive_at(struct dive *dive, int a, int b) +{ + int i, nr; + uint32_t t; + struct dive *d1, *d2; + struct divecomputer *dc1, *dc2; + struct event *event, **evp; + + /* if we can't find the dive in the dive list, don't bother */ + if ((nr = get_divenr(dive)) < 0) + return 0; + + /* We're not trying to be efficient here.. */ + d1 = create_new_copy(dive); + d2 = create_new_copy(dive); + + /* now unselect the first first segment so we don't keep all + * dives selected by mistake. But do keep the second one selected + * so the algorithm keeps splitting the dive further */ + d1->selected = false; + + dc1 = &d1->dc; + dc2 = &d2->dc; + /* + * Cut off the samples of d1 at the beginning + * of the interval. + */ + dc1->samples = a; + + /* And get rid of the 'b' first samples of d2 */ + dc2->samples -= b; + memmove(dc2->sample, dc2->sample+b, dc2->samples * sizeof(struct sample)); + + /* + * This is where we cut off events from d1, + * and shift everything in d2 + */ + t = dc2->sample[0].time.seconds; + d2->when += t; + for (i = 0; i < dc2->samples; i++) + dc2->sample[i].time.seconds -= t; + + /* Remove the events past 't' from d1 */ + evp = &dc1->events; + while ((event = *evp) != NULL && event->time.seconds < t) + evp = &event->next; + *evp = NULL; + while (event) { + struct event *next = event->next; + free(event); + event = next; + } + + /* Remove the events before 't' from d2, and shift the rest */ + evp = &dc2->events; + while ((event = *evp) != NULL) { + if (event->time.seconds < t) { + *evp = event->next; + free(event); + } else { + event->time.seconds -= t; + } + } + + force_fixup_dive(d1); + force_fixup_dive(d2); + + if (dive->divetrip) { + d1->divetrip = d2->divetrip = 0; + add_dive_to_trip(d1, dive->divetrip); + add_dive_to_trip(d2, dive->divetrip); + } + + delete_single_dive(nr); + add_single_dive(nr, d1); + + /* + * Was the dive numbered? If it was the last dive, then we'll + * increment the dive number for the tail part that we split off. + * Otherwise the tail is unnumbered. + */ + if (d2->number) { + if (dive_table.nr == nr + 1) + d2->number++; + else + d2->number = 0; + } + add_single_dive(nr + 1, d2); + + mark_divelist_changed(true); + + return 1; +} + +/* in freedive mode we split for as little as 10 seconds on the surface, + * otherwise we use a minute */ +static bool should_split(struct divecomputer *dc, int t1, int t2) +{ + int threshold = dc->divemode == FREEDIVE ? 10 : 60; + + return t2 - t1 >= threshold; +} + +/* + * Try to split a dive into multiple dives at a surface interval point. + * + * NOTE! We will not split dives with multiple dive computers, and + * only split when there is at least one surface event that has + * non-surface events on both sides. + * + * In other words, this is a (simplified) reversal of the dive merging. + */ +int split_dive(struct dive *dive) +{ + int i; + int at_surface, surface_start; + struct divecomputer *dc; + + if (!dive || (dc = &dive->dc)->next) + return 0; + + surface_start = 0; + at_surface = 1; + for (i = 1; i < dc->samples; i++) { + struct sample *sample = dc->sample+i; + int surface_sample = sample->depth.mm < SURFACE_THRESHOLD; + + /* + * We care about the transition from and to depth 0, + * not about the depth staying similar. + */ + if (at_surface == surface_sample) + continue; + at_surface = surface_sample; + + // Did it become surface after having been non-surface? We found the start + if (at_surface) { + surface_start = i; + continue; + } + + // Going down again? We want at least a minute from + // the surface start. + if (!surface_start) + continue; + if (!should_split(dc, dc->sample[surface_start].time.seconds, sample[i - 1].time.seconds)) + continue; + + return split_dive_at(dive, surface_start, i-1); + } + return 0; +} + +/* + * "dc_maxtime()" is how much total time this dive computer + * has for this dive. Note that it can differ from "duration" + * if there are surface events in the middle. + * + * Still, we do ignore all but the last surface samples from the + * end, because some divecomputers just generate lots of them. + */ +static inline int dc_totaltime(const struct divecomputer *dc) +{ + int time = dc->duration.seconds; + int nr = dc->samples; + + while (nr--) { + struct sample *s = dc->sample + nr; + time = s->time.seconds; + if (s->depth.mm >= SURFACE_THRESHOLD) + break; + } + return time; +} + +/* + * The end of a dive is actually not trivial, because "duration" + * is not the duration until the end, but the time we spend under + * water, which can be very different if there are surface events + * during the dive. + * + * So walk the dive computers, looking for the longest actual + * time in the samples (and just default to the dive duration if + * there are no samples). + */ +static inline int dive_totaltime(const struct dive *dive) +{ + int time = dive->duration.seconds; + const struct divecomputer *dc; + + for_each_dc(dive, dc) { + int dc_time = dc_totaltime(dc); + if (dc_time > time) + time = dc_time; + } + return time; +} + +timestamp_t dive_endtime(const struct dive *dive) +{ + return dive->when + dive_totaltime(dive); +} + +struct dive *find_dive_including(timestamp_t when) +{ + int i; + struct dive *dive; + + /* binary search, anyone? Too lazy for now; + * also we always use the duration from the first divecomputer + * could this ever be a problem? */ + for_each_dive (i, dive) { + if (dive->when <= when && when <= dive_endtime(dive)) + return dive; + } + return NULL; +} + +bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset) +{ + timestamp_t start = dive->when; + timestamp_t end = dive_endtime(dive); + return start - offset <= when && when <= end + offset; +} + +bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset) +{ + timestamp_t start = dive->when; + timestamp_t end = dive_endtime(dive); + return when - offset <= start && end <= when + offset; +} + +/* find the n-th dive that is part of a group of dives within the offset around 'when'. + * How is that for a vague definition of what this function should do... */ +struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset) +{ + int i, j = 0; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive_within_time_range(dive, when, offset)) + if (++j == n) + return dive; + } + return NULL; +} + +void shift_times(const timestamp_t amount) +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (!dive->selected) + continue; + dive->when += amount; + invalidate_dive_cache(dive); + } +} + +timestamp_t get_times() +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive->selected) + break; + } + return dive->when; +} + +void set_save_userid_local(short value) +{ + prefs.save_userid_local = value; +} + +void set_userid(char *rUserId) +{ + if (prefs.userid) + free(prefs.userid); + prefs.userid = strdup(rUserId); + if (strlen(prefs.userid) > 30) + prefs.userid[30]='\0'; +} + +/* this sets a usually unused copy of the preferences with the units + * that were active the last time the dive list was saved to git storage + * (this isn't used in XML files); storing the unit preferences in the + * data file is usually pointless (that's a setting of the software, + * not a property of the data), but it's a great hint of what the user + * might expect to see when creating a backend service that visualizes + * the dive list without Subsurface running - so this is basically a + * functionality for the core library that Subsurface itself doesn't + * use but that another consumer of the library (like an HTML exporter) + * will need */ +void set_informational_units(char *units) +{ + if (strstr(units, "METRIC")) { + informational_prefs.unit_system = METRIC; + } else if (strstr(units, "IMPERIAL")) { + informational_prefs.unit_system = IMPERIAL; + } else if (strstr(units, "PERSONALIZE")) { + informational_prefs.unit_system = PERSONALIZE; + if (strstr(units, "METERS")) + informational_prefs.units.length = METERS; + if (strstr(units, "FEET")) + informational_prefs.units.length = FEET; + if (strstr(units, "LITER")) + informational_prefs.units.volume = LITER; + if (strstr(units, "CUFT")) + informational_prefs.units.volume = CUFT; + if (strstr(units, "BAR")) + informational_prefs.units.pressure = BAR; + if (strstr(units, "PSI")) + informational_prefs.units.pressure = PSI; + if (strstr(units, "PASCAL")) + informational_prefs.units.pressure = PASCAL; + if (strstr(units, "CELSIUS")) + informational_prefs.units.temperature = CELSIUS; + if (strstr(units, "FAHRENHEIT")) + informational_prefs.units.temperature = FAHRENHEIT; + if (strstr(units, "KG")) + informational_prefs.units.weight = KG; + if (strstr(units, "LBS")) + informational_prefs.units.weight = LBS; + if (strstr(units, "SECONDS")) + informational_prefs.units.vertical_speed_time = SECONDS; + if (strstr(units, "MINUTES")) + informational_prefs.units.vertical_speed_time = MINUTES; + } +} + +void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth) +{ + int integral = 0; + int last_time = 0; + int last_depth = 0; + struct divedatapoint *dp = dive->dp; + + *max_depth = 0; + + while (dp) { + if (dp->time) { + /* Ignore gas indication samples */ + integral += (dp->depth + last_depth) * (dp->time - last_time) / 2; + last_time = dp->time; + last_depth = dp->depth; + if (dp->depth > *max_depth) + *max_depth = dp->depth; + } + dp = dp->next; + } + if (last_time) + *avg_depth = integral / last_time; + else + *avg_depth = *max_depth = 0; +} + +struct picture *alloc_picture() +{ + struct picture *pic = malloc(sizeof(struct picture)); + if (!pic) + exit(1); + memset(pic, 0, sizeof(struct picture)); + return pic; +} + +static bool new_picture_for_dive(struct dive *d, char *filename) +{ + FOR_EACH_PICTURE (d) { + if (same_string(picture->filename, filename)) + return false; + } + return true; +} + +// only add pictures that have timestamps between 30 minutes before the dive and +// 30 minutes after the dive ends +#define D30MIN (30 * 60) +bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp) +{ + offset_t offset; + if (timestamp) { + offset.seconds = timestamp - d->when + shift_time; + if (offset.seconds > -D30MIN && offset.seconds < dive_totaltime(d) + D30MIN) { + // this picture belongs to this dive + return true; + } + } + return false; +} + +bool picture_check_valid(char *filename, int shift_time) +{ + int i; + struct dive *dive; + + timestamp_t timestamp = picture_get_timestamp(filename); + for_each_dive (i, dive) + if (dive->selected && dive_check_picture_time(dive, shift_time, timestamp)) + return true; + return false; +} + +void dive_create_picture(struct dive *dive, char *filename, int shift_time, bool match_all) +{ + timestamp_t timestamp = picture_get_timestamp(filename); + if (!new_picture_for_dive(dive, filename)) + return; + if (!match_all && !dive_check_picture_time(dive, shift_time, timestamp)) + return; + + struct picture *picture = alloc_picture(); + picture->filename = strdup(filename); + picture->offset.seconds = timestamp - dive->when + shift_time; + picture_load_exif_data(picture); + + dive_add_picture(dive, picture); + dive_set_geodata_from_picture(dive, picture); + invalidate_dive_cache(dive); +} + +void dive_add_picture(struct dive *dive, struct picture *newpic) +{ + struct picture **pic_ptr = &dive->picture_list; + /* let's keep the list sorted by time */ + while (*pic_ptr && (*pic_ptr)->offset.seconds <= newpic->offset.seconds) + pic_ptr = &(*pic_ptr)->next; + newpic->next = *pic_ptr; + *pic_ptr = newpic; + cache_picture(newpic); + return; +} + +unsigned int dive_get_picture_count(struct dive *dive) +{ + unsigned int i = 0; + FOR_EACH_PICTURE (dive) + i++; + return i; +} + +void dive_set_geodata_from_picture(struct dive *dive, struct picture *picture) +{ + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (!dive_site_has_gps_location(ds) && (picture->latitude.udeg || picture->longitude.udeg)) { + if (ds) { + ds->latitude = picture->latitude; + ds->longitude = picture->longitude; + } else { + dive->dive_site_uuid = create_dive_site_with_gps("", picture->latitude, picture->longitude, dive->when); + invalidate_dive_cache(dive); + } + } +} + +void picture_free(struct picture *picture) +{ + if (!picture) + return; + free(picture->filename); + free(picture->hash); + free(picture); +} + +// When handling pictures in different threads, we need to copy them so we don't +// run into problems when the main thread frees the picture. + +struct picture *clone_picture(struct picture *src) +{ + struct picture *dst; + + dst = alloc_picture(); + copy_pl(src, dst); + return dst; +} + +void dive_remove_picture(char *filename) +{ + struct picture **picture = ¤t_dive->picture_list; + while (picture && !same_string((*picture)->filename, filename)) + picture = &(*picture)->next; + if (picture) { + struct picture *temp = (*picture)->next; + picture_free(*picture); + *picture = temp; + invalidate_dive_cache(current_dive); + } +} + +/* this always acts on the current divecomputer of the current dive */ +void make_first_dc() +{ + struct divecomputer *dc = ¤t_dive->dc; + struct divecomputer *newdc = malloc(sizeof(*newdc)); + struct divecomputer *cur_dc = current_dc; /* needs to be in a local variable so the macro isn't re-executed */ + + /* skip the current DC in the linked list */ + while (dc && dc->next != cur_dc) + dc = dc->next; + if (!dc) { + free(newdc); + fprintf(stderr, "data inconsistent: can't find the current DC"); + return; + } + dc->next = cur_dc->next; + *newdc = current_dive->dc; + current_dive->dc = *cur_dc; + current_dive->dc.next = newdc; + free(cur_dc); + invalidate_dive_cache(current_dive); +} + +/* always acts on the current dive */ +unsigned int count_divecomputers(void) +{ + int ret = 1; + struct divecomputer *dc = current_dive->dc.next; + while (dc) { + ret++; + dc = dc->next; + } + return ret; +} + +/* always acts on the current dive */ +void delete_current_divecomputer(void) +{ + struct divecomputer *dc = current_dc; + + if (dc == ¤t_dive->dc) { + /* remove the first one, so copy the second one in place of the first and free the second one + * be careful about freeing the no longer needed structures - since we copy things around we can't use free_dc()*/ + struct divecomputer *fdc = dc->next; + free(dc->sample); + free((void *)dc->model); + free_events(dc->events); + memcpy(dc, fdc, sizeof(struct divecomputer)); + free(fdc); + } else { + struct divecomputer *pdc = ¤t_dive->dc; + while (pdc->next != dc && pdc->next) + pdc = pdc->next; + if (pdc->next == dc) { + pdc->next = dc->next; + free_dc(dc); + } + } + if (dc_number == count_divecomputers()) + dc_number--; + invalidate_dive_cache(current_dive); +} + +/* helper function to make it easier to work with our structures + * we don't interpolate here, just use the value from the last sample up to that time */ +int get_depth_at_time(struct divecomputer *dc, unsigned int time) +{ + int depth = 0; + if (dc && dc->sample) + for (int i = 0; i < dc->samples; i++) { + if (dc->sample[i].time.seconds > time) + break; + depth = dc->sample[i].depth.mm; + } + return depth; +} diff --git a/core/dive.h b/core/dive.h new file mode 100644 index 000000000..ae24b7409 --- /dev/null +++ b/core/dive.h @@ -0,0 +1,913 @@ +#ifndef DIVE_H +#define DIVE_H + +#include <stdlib.h> +#include <stdint.h> +#include <time.h> +#include <math.h> +#include <zip.h> +#include <sqlite3.h> +#include <string.h> +#include "divesite.h" + +/* Windows has no MIN/MAX macros - so let's just roll our own */ +#define MIN(x, y) ({ \ + __typeof__(x) _min1 = (x); \ + __typeof__(y) _min2 = (y); \ + (void) (&_min1 == &_min2); \ + _min1 < _min2 ? _min1 : _min2; }) + +#define MAX(x, y) ({ \ + __typeof__(x) _max1 = (x); \ + __typeof__(y) _max2 = (y); \ + (void) (&_max1 == &_max2); \ + _max1 > _max2 ? _max1 : _max2; }) + +#define IS_FP_SAME(_a, _b) (fabs((_a) - (_b)) <= 0.000001 * MAX(fabs(_a), fabs(_b))) + +static inline int same_string(const char *a, const char *b) +{ + return !strcmp(a ?: "", b ?: ""); +} + +static inline char *copy_string(const char *s) +{ + return (s && *s) ? strdup(s) : NULL; +} + +#include <libxml/tree.h> +#include <libxslt/transform.h> +#include <libxslt/xsltutils.h> + +#include "sha1.h" +#include "units.h" + +#ifdef __cplusplus +extern "C" { +#else +#include <stdbool.h> +#endif + +extern int last_xml_version; + +enum dive_comp_type {OC, CCR, PSCR, FREEDIVE, NUM_DC_TYPE}; // Flags (Open-circuit and Closed-circuit-rebreather) for setting dive computer type +enum cylinderuse {OC_GAS, DILUENT, OXYGEN, NUM_GAS_USE}; // The different uses for cylinders + +extern const char *cylinderuse_text[]; +extern const char *divemode_text[]; + +struct gasmix { + fraction_t o2; + fraction_t he; +}; + +typedef struct +{ + volume_t size; + pressure_t workingpressure; + const char *description; /* "LP85", "AL72", "AL80", "HP100+" or whatever */ +} cylinder_type_t; + +typedef struct +{ + cylinder_type_t type; + struct gasmix gasmix; + pressure_t start, end, sample_start, sample_end; + depth_t depth; + bool manually_added; + volume_t gas_used; + volume_t deco_gas_used; + enum cylinderuse cylinder_use; +} cylinder_t; + +typedef struct +{ + weight_t weight; + const char *description; /* "integrated", "belt", "ankle" */ +} weightsystem_t; + +/* + * Events are currently based straight on what libdivecomputer gives us. + * We need to wrap these into our own events at some point to remove some of the limitations. + */ +struct event { + struct event *next; + duration_t time; + int type; + /* This is the annoying libdivecomputer format. */ + int flags, value; + /* .. and this is our "extended" data for some event types */ + union { + /* + * Currently only for gas switch events. + * + * NOTE! The index may be -1, which means "unknown". In that + * case, the get_cylinder_index() function will give the best + * match with the cylinders in the dive based on gasmix. + */ + struct { + int index; + struct gasmix mix; + } gas; + }; + bool deleted; + char name[]; +}; + +extern int event_is_gaschange(struct event *ev); +extern int event_gasmix_redundant(struct event *ev); + +extern int get_pressure_units(int mb, const char **units); +extern double get_depth_units(int mm, int *frac, const char **units); +extern double get_volume_units(unsigned int ml, int *frac, const char **units); +extern double get_temp_units(unsigned int mk, const char **units); +extern double get_weight_units(unsigned int grams, int *frac, const char **units); +extern double get_vertical_speed_units(unsigned int mms, int *frac, const char **units); + +extern unsigned int units_to_depth(double depth); +extern int units_to_sac(double volume); + +/* Volume in mliter of a cylinder at pressure 'p' */ +extern int gas_volume(cylinder_t *cyl, pressure_t p); +extern double gas_compressibility_factor(struct gasmix *gas, double bar); + + +static inline int get_o2(const struct gasmix *mix) +{ + return mix->o2.permille ?: O2_IN_AIR; +} + +static inline int get_he(const struct gasmix *mix) +{ + return mix->he.permille; +} + +struct gas_pressures { + double o2, n2, he; +}; + +extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type dctype); + +extern void sanitize_gasmix(struct gasmix *mix); +extern int gasmix_distance(const struct gasmix *a, const struct gasmix *b); +extern struct gasmix *get_gasmix_from_event(struct event *ev); + +static inline bool gasmix_is_air(const struct gasmix *gasmix) +{ + int o2 = gasmix->o2.permille; + int he = gasmix->he.permille; + return (he == 0) && (o2 == 0 || ((o2 >= O2_IN_AIR - 1) && (o2 <= O2_IN_AIR + 1))); +} + +/* Linear interpolation between 'a' and 'b', when we are 'part'way into the 'whole' distance from a to b */ +static inline int interpolate(int a, int b, int part, int whole) +{ + /* It is doubtful that we actually need floating point for this, but whatever */ + double x = (double)a * (whole - part) + (double)b * part; + return rint(x / whole); +} + +void get_gas_string(const struct gasmix *gasmix, char *text, int len); +const char *gasname(const struct gasmix *gasmix); + +struct sample // BASE TYPE BYTES UNITS RANGE DESCRIPTION +{ // --------- ----- ----- ----- ----------- + duration_t time; // uint32_t 4 seconds (0-68 yrs) elapsed dive time up to this sample + duration_t stoptime; // uint32_t 4 seconds (0-18 h) time duration of next deco stop + duration_t ndl; // uint32_t 4 seconds (0-18 h) time duration before no-deco limit + duration_t tts; // uint32_t 4 seconds (0-18 h) time duration to reach the surface + duration_t rbt; // uint32_t 4 seconds (0-18 h) remaining bottom time + depth_t depth; // int32_t 4 mm (0-2000 km) dive depth of this sample + depth_t stopdepth; // int32_t 4 mm (0-2000 km) depth of next deco stop + temperature_t temperature; // int32_t 4 mdegrK (0-2 MdegK) ambient temperature + pressure_t cylinderpressure; // int32_t 4 mbar (0-2 Mbar) main cylinder pressure + pressure_t o2cylinderpressure; // int32_t 4 mbar (0-2 Mbar) CCR o2 cylinder pressure (rebreather) + o2pressure_t setpoint; // uint16_t 2 mbar (0-65 bar) O2 partial pressure (will be setpoint) + o2pressure_t o2sensor[3]; // uint16_t 6 mbar (0-65 bar) Up to 3 PO2 sensor values (rebreather) + bearing_t bearing; // int16_t 2 degrees (-32k to 32k deg) compass bearing + uint8_t sensor; // uint8_t 1 sensorID (0-255) ID of cylinder pressure sensor + uint8_t cns; // uint8_t 1 % (0-255 %) cns% accumulated + uint8_t heartbeat; // uint8_t 1 beats/m (0-255) heart rate measurement + volume_t sac; // 4 ml/min predefined SAC + bool in_deco; // bool 1 y/n y/n this sample is part of deco + bool manually_entered; // bool 1 y/n y/n this sample was entered by the user, + // not calculated when planning a dive +}; // Total size of structure: 57 bytes, excluding padding at end + +struct divetag { + /* + * The name of the divetag. If a translation is available, name contains + * the translated tag + */ + char *name; + /* + * If a translation is available, we write the original tag to source. + * This enables us to write a non-localized tag to the xml file. + */ + char *source; +}; + +struct tag_entry { + struct divetag *tag; + struct tag_entry *next; +}; + +/* + * divetags are only stored once, each dive only contains + * a list of tag_entries which then point to the divetags + * in the global g_tag_list + */ + +extern struct tag_entry *g_tag_list; + +struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag); +struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list); +void dump_taglist(const char *intro, struct tag_entry *tl); + +/* + * Writes all divetags in tag_list to buffer, limited by the buffer's (len)gth. + * Returns the characters written + */ +int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len); + +/* cleans up a list: removes empty tags and duplicates */ +void taglist_cleanup(struct tag_entry **tag_list); + +void taglist_init_global(); +void taglist_free(struct tag_entry *tag_list); + +bool taglist_contains(struct tag_entry *tag_list, const char *tag); +bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2); +int count_dives_with_tag(const char *tag); +int count_dives_with_person(const char *person); +int count_dives_with_location(const char *location); +int count_dives_with_suit(const char *suit); + +struct extra_data { + const char *key; + const char *value; + struct extra_data *next; +}; + +/* + * NOTE! The deviceid and diveid are model-specific *hashes* of + * whatever device identification that model may have. Different + * dive computers will have different identifying data, it could + * be a firmware number or a serial ID (in either string or in + * numeric format), and we do not care. + * + * The only thing we care about is that subsurface will hash + * that information the same way. So then you can check the ID + * of a dive computer by comparing the hashes for equality. + * + * A deviceid or diveid of zero is assumed to be "no ID". + */ +struct divecomputer { + timestamp_t when; + duration_t duration, surfacetime; + depth_t maxdepth, meandepth; + temperature_t airtemp, watertemp; + pressure_t surface_pressure; + enum dive_comp_type divemode; // dive computer type: OC(default) or CCR + uint8_t no_o2sensors; // rebreathers: number of O2 sensors used + int salinity; // kg per 10000 l + const char *model, *serial, *fw_version; + uint32_t deviceid, diveid; + int samples, alloc_samples; + struct sample *sample; + struct event *events; + struct extra_data *extra_data; + struct divecomputer *next; +}; + +#define MAX_CYLINDERS (8) +#define MAX_WEIGHTSYSTEMS (6) +#define W_IDX_PRIMARY 0 +#define W_IDX_SECONDARY 1 + +typedef enum { + TF_NONE, + NO_TRIP, + IN_TRIP, + ASSIGNED_TRIP, + NUM_TRIPFLAGS +} tripflag_t; + +typedef struct dive_trip +{ + timestamp_t when; + char *location; + char *notes; + struct dive *dives; + int nrdives; + int index; + unsigned expanded : 1, selected : 1, autogen : 1, fixup : 1; + struct dive_trip *next; +} dive_trip_t; + +/* List of dive trips (sorted by date) */ +extern dive_trip_t *dive_trip_list; +struct picture; +struct dive { + int number; + tripflag_t tripflag; + dive_trip_t *divetrip; + struct dive *next, **pprev; + bool selected; + bool hidden_by_filter; + bool downloaded; + timestamp_t when; + uint32_t dive_site_uuid; + char *notes; + char *divemaster, *buddy; + int rating; + int visibility; /* 0 - 5 star rating */ + cylinder_t cylinder[MAX_CYLINDERS]; + weightsystem_t weightsystem[MAX_WEIGHTSYSTEMS]; + char *suit; + int sac, otu, cns, maxcns; + + /* Calculated based on dive computer data */ + temperature_t mintemp, maxtemp, watertemp, airtemp; + depth_t maxdepth, meandepth; + pressure_t surface_pressure; + duration_t duration; + int salinity; // kg per 10000 l + + struct tag_entry *tag_list; + struct divecomputer dc; + int id; // unique ID for this dive + struct picture *picture_list; + int oxygen_cylinder_index, diluent_cylinder_index; // CCR dive cylinder indices + unsigned char git_id[20]; +}; + +static inline void invalidate_dive_cache(struct dive *dive) +{ + memset(dive->git_id, 0, 20); +} + +static inline bool dive_cache_is_valid(const struct dive *dive) +{ + static const unsigned char null_id[20] = { 0, }; + return !!memcmp(dive->git_id, null_id, 20); +} + +extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type); +extern void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]); + +/* when selectively copying dive information, which parts should be copied? */ +struct dive_components { + unsigned int divesite : 1; + unsigned int notes : 1; + unsigned int divemaster : 1; + unsigned int buddy : 1; + unsigned int suit : 1; + unsigned int rating : 1; + unsigned int visibility : 1; + unsigned int tags : 1; + unsigned int cylinders : 1; + unsigned int weights : 1; +}; + +/* picture list and methods related to dive picture handling */ +struct picture { + char *filename; + char *hash; + offset_t offset; + degrees_t latitude; + degrees_t longitude; + struct picture *next; +}; + +#define FOR_EACH_PICTURE(_dive) \ + if (_dive) \ + for (struct picture *picture = (_dive)->picture_list; picture; picture = picture->next) + +#define FOR_EACH_PICTURE_NON_PTR(_divestruct) \ + for (struct picture *picture = (_divestruct).picture_list; picture; picture = picture->next) + +extern struct picture *alloc_picture(); +extern struct picture *clone_picture(struct picture *src); +extern bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp); +extern void dive_create_picture(struct dive *d, char *filename, int shift_time, bool match_all); +extern void dive_add_picture(struct dive *d, struct picture *newpic); +extern void dive_remove_picture(char *filename); +extern unsigned int dive_get_picture_count(struct dive *d); +extern bool picture_check_valid(char *filename, int shift_time); +extern void picture_load_exif_data(struct picture *p); +extern timestamp_t picture_get_timestamp(char *filename); +extern void dive_set_geodata_from_picture(struct dive *d, struct picture *pic); +extern void picture_free(struct picture *picture); + +extern int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc); +extern int get_depth_at_time(struct divecomputer *dc, unsigned int time); + +static inline int get_surface_pressure_in_mbar(const struct dive *dive, bool non_null) +{ + int mbar = dive->surface_pressure.mbar; + if (!mbar && non_null) + mbar = SURFACE_PRESSURE; + return mbar; +} + +/* Pa = N/m^2 - so we determine the weight (in N) of the mass of 10m + * of water (and use standard salt water at 1.03kg per liter if we don't know salinity) + * and add that to the surface pressure (or to 1013 if that's unknown) */ +static inline int calculate_depth_to_mbar(int depth, pressure_t surface_pressure, int salinity) +{ + double specific_weight; + int mbar = surface_pressure.mbar; + + if (!mbar) + mbar = SURFACE_PRESSURE; + if (!salinity) + salinity = SEAWATER_SALINITY; + if (salinity < 500) + salinity += FRESHWATER_SALINITY; + specific_weight = salinity / 10000.0 * 0.981; + mbar += rint(depth / 10.0 * specific_weight); + return mbar; +} + +static inline int depth_to_mbar(int depth, struct dive *dive) +{ + return calculate_depth_to_mbar(depth, dive->surface_pressure, dive->salinity); +} + +static inline double depth_to_bar(int depth, struct dive *dive) +{ + return depth_to_mbar(depth, dive) / 1000.0; +} + +static inline double depth_to_atm(int depth, struct dive *dive) +{ + return mbar_to_atm(depth_to_mbar(depth, dive)); +} + +/* for the inverse calculation we use just the relative pressure + * (that's the one that some dive computers like the Uemis Zurich + * provide - for the other models that do this libdivecomputer has to + * take care of this, but the Uemis we support natively */ +static inline int rel_mbar_to_depth(int mbar, struct dive *dive) +{ + int cm; + double specific_weight = 1.03 * 0.981; + if (dive->dc.salinity) + specific_weight = dive->dc.salinity / 10000.0 * 0.981; + /* whole mbar gives us cm precision */ + cm = rint(mbar / specific_weight); + return cm * 10; +} + +static inline int mbar_to_depth(int mbar, struct dive *dive) +{ + pressure_t surface_pressure; + if (dive->surface_pressure.mbar) + surface_pressure = dive->surface_pressure; + else + surface_pressure.mbar = SURFACE_PRESSURE; + return rel_mbar_to_depth(mbar - surface_pressure.mbar, dive); +} + +/* MOD rounded to multiples of roundto mm */ +static inline depth_t gas_mod(struct gasmix *mix, pressure_t po2_limit, struct dive *dive, int roundto) { + depth_t rounded_depth; + + double depth = (double) mbar_to_depth(po2_limit.mbar * 1000 / get_o2(mix), dive); + rounded_depth.mm = rint(depth / roundto) * roundto; + return rounded_depth; +} + +#define SURFACE_THRESHOLD 750 /* somewhat arbitrary: only below 75cm is it really diving */ + +/* this is a global spot for a temporary dive structure that we use to + * be able to edit a dive without unintended side effects */ +extern struct dive edit_dive; + +extern short autogroup; +/* random threashold: three days without diving -> new trip + * this works very well for people who usually dive as part of a trip and don't + * regularly dive at a local facility; this is why trips are an optional feature */ +#define TRIP_THRESHOLD 3600 * 24 * 3 + +#define UNGROUPED_DIVE(_dive) ((_dive)->tripflag == NO_TRIP) +#define DIVE_IN_TRIP(_dive) ((_dive)->tripflag == IN_TRIP || (_dive)->tripflag == ASSIGNED_TRIP) +#define DIVE_NEEDS_TRIP(_dive) ((_dive)->tripflag == TF_NONE) + +extern void add_dive_to_trip(struct dive *, dive_trip_t *); + +extern void delete_single_dive(int idx); +extern void add_single_dive(int idx, struct dive *dive); + +extern void insert_trip(dive_trip_t **trip); + + +extern const struct units SI_units, IMPERIAL_units; +extern struct units xml_parsing_units; + +extern struct units *get_units(void); +extern int run_survey, verbose, quit, force_root; + +struct dive_table { + int nr, allocated, preexisting; + struct dive **dives; +}; + +extern struct dive_table dive_table, downloadTable; +extern struct dive displayed_dive; +extern struct dive_site displayed_dive_site; +extern int selected_dive; +extern unsigned int dc_number; +#define current_dive (get_dive(selected_dive)) +#define current_dc (get_dive_dc(current_dive, dc_number)) + +static inline struct dive *get_dive(int nr) +{ + if (nr >= dive_table.nr || nr < 0) + return NULL; + return dive_table.dives[nr]; +} + +static inline struct dive *get_dive_from_table(int nr, struct dive_table *dt) +{ + if (nr >= dt->nr || nr < 0) + return NULL; + return dt->dives[nr]; +} + +static inline struct dive_site *get_dive_site_for_dive(struct dive *dive) +{ + if (dive) + return get_dive_site_by_uuid(dive->dive_site_uuid); + return NULL; +} + +static inline char *get_dive_location(struct dive *dive) +{ + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (ds && ds->name) + return ds->name; + return NULL; +} + +static inline unsigned int number_of_computers(struct dive *dive) +{ + unsigned int total_number = 0; + struct divecomputer *dc = &dive->dc; + + if (!dive) + return 1; + + do { + total_number++; + dc = dc->next; + } while (dc); + return total_number; +} + +static inline struct divecomputer *get_dive_dc(struct dive *dive, int nr) +{ + struct divecomputer *dc = &dive->dc; + + while (nr-- > 0) { + dc = dc->next; + if (!dc) + return &dive->dc; + } + return dc; +} + +extern timestamp_t dive_endtime(const struct dive *dive); + +extern void make_first_dc(void); +extern unsigned int count_divecomputers(void); +extern void delete_current_divecomputer(void); + +/* + * Iterate over each dive, with the first parameter being the index + * iterator variable, and the second one being the dive one. + * + * I don't think anybody really wants the index, and we could make + * it local to the for-loop, but that would make us requires C99. + */ +#define for_each_dive(_i, _x) \ + for ((_i) = 0; ((_x) = get_dive(_i)) != NULL; (_i)++) + +#define for_each_dc(_dive, _dc) \ + for (_dc = &_dive->dc; _dc; _dc = _dc->next) + +#define for_each_gps_location(_i, _x) \ + for ((_i) = 0; ((_x) = get_gps_location(_i, &gps_location_table)) != NULL; (_i)++) + +static inline struct dive *get_dive_by_uniq_id(int id) +{ + int i; + struct dive *dive = NULL; + + for_each_dive (i, dive) { + if (dive->id == id) + break; + } +#ifdef DEBUG + if (dive == NULL) { + fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); + exit(1); + } +#endif + return dive; +} + +static inline int get_idx_by_uniq_id(int id) +{ + int i; + struct dive *dive = NULL; + + for_each_dive (i, dive) { + if (dive->id == id) + break; + } +#ifdef DEBUG + if (dive == NULL) { + fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); + exit(1); + } +#endif + return i; +} + +static inline bool dive_site_has_gps_location(struct dive_site *ds) +{ + return ds && (ds->latitude.udeg || ds->longitude.udeg); +} + +static inline int dive_has_gps_location(struct dive *dive) +{ + if (!dive) + return false; + return dive_site_has_gps_location(get_dive_site_by_uuid(dive->dive_site_uuid)); +} + +#ifdef __cplusplus +extern "C" { +#endif + +extern int report_error(const char *fmt, ...); +extern void report_message(const char *msg); +extern const char *get_error_string(void); + +extern struct dive *find_dive_including(timestamp_t when); +extern bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset); +extern bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset); +struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset); + +/* Check if two dive computer entries are the exact same dive (-1=no/0=maybe/1=yes) */ +extern int match_one_dc(struct divecomputer *a, struct divecomputer *b); + +extern void parse_xml_init(void); +extern int parse_xml_buffer(const char *url, const char *buf, int size, struct dive_table *table, const char **params); +extern void parse_xml_exit(void); +extern void set_filename(const char *filename, bool force); + +extern int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_dlf_buffer(unsigned char *buffer, size_t size); + +extern int check_git_sha(const char *filename); +extern int parse_file(const char *filename); +extern int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); +extern int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); +extern int parse_txt_file(const char *filename, const char *csv); +extern int parse_manual_file(const char *filename, char **params, int pnr); +extern int save_dives(const char *filename); +extern int save_dives_logic(const char *filename, bool select_only); +extern int save_dive(FILE *f, struct dive *dive); +extern int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt); + +struct membuffer; +extern void save_one_dive_to_mb(struct membuffer *b, struct dive *dive); + +int cylinderuse_from_text(const char *text); + + +struct user_info { + const char *name; + const char *email; +}; + +extern void subsurface_user_info(struct user_info *); +extern int subsurface_rename(const char *path, const char *newpath); +extern int subsurface_dir_rename(const char *path, const char *newpath); +extern int subsurface_open(const char *path, int oflags, mode_t mode); +extern FILE *subsurface_fopen(const char *path, const char *mode); +extern void *subsurface_opendir(const char *path); +extern int subsurface_access(const char *path, int mode); +extern struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp); +extern int subsurface_zip_close(struct zip *zip); +extern void subsurface_console_init(bool dedicated); +extern void subsurface_console_exit(void); +extern bool subsurface_user_is_root(void); + +extern void shift_times(const timestamp_t amount); +extern timestamp_t get_times(); + +extern xsltStylesheetPtr get_stylesheet(const char *name); + +extern timestamp_t utc_mktime(struct tm *tm); +extern void utc_mkdate(timestamp_t, struct tm *tm); + +extern struct dive *alloc_dive(void); +extern void record_dive_to_table(struct dive *dive, struct dive_table *table); +extern void record_dive(struct dive *dive); +extern void clear_dive(struct dive *dive); +extern void copy_dive(struct dive *s, struct dive *d); +extern void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear); +extern struct dive *clone_dive(struct dive *s); + +extern void clear_table(struct dive_table *table); + +extern struct sample *prepare_sample(struct divecomputer *dc); +extern void finish_sample(struct divecomputer *dc); + +extern bool has_hr_data(struct divecomputer *dc); + +extern void sort_table(struct dive_table *table); +extern struct dive *fixup_dive(struct dive *dive); +extern void fixup_dc_duration(struct divecomputer *dc); +extern int dive_getUniqID(struct dive *d); +extern unsigned int dc_airtemp(struct divecomputer *dc); +extern unsigned int dc_watertemp(struct divecomputer *dc); +extern int split_dive(struct dive *); +extern struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded); +extern struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded); +extern struct event *clone_event(const struct event *src_ev); +extern void copy_events(struct divecomputer *s, struct divecomputer *d); +extern void free_events(struct event *ev); +extern void copy_cylinders(struct dive *s, struct dive *d, bool used_only); +extern void copy_samples(struct divecomputer *s, struct divecomputer *d); +extern bool is_cylinder_used(struct dive *dive, int idx); +extern void fill_default_cylinder(cylinder_t *cyl); +extern void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int time, int idx); +extern struct event *add_event(struct divecomputer *dc, unsigned int time, int type, int flags, int value, const char *name); +extern void remove_event(struct event *event); +extern void update_event_name(struct dive *d, struct event* event, char *name); +extern void add_extra_data(struct divecomputer *dc, const char *key, const char *value); +extern void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration); +extern int get_cylinder_index(struct dive *dive, struct event *ev); +extern int nr_cylinders(struct dive *dive); +extern int nr_weightsystems(struct dive *dive); + +/* UI related protopypes */ + +// extern void report_error(GError* error); + +extern void add_cylinder_description(cylinder_type_t *); +extern void add_weightsystem_description(weightsystem_t *); +extern void remember_event(const char *eventname); +extern void invalidate_dive_cache(struct dive *dc); + +#if WE_DONT_USE_THIS /* this is a missing feature in Qt - selecting which events to display */ +extern int evn_foreach(void (*callback)(const char *, bool *, void *), void *data); +#endif /* WE_DONT_USE_THIS */ + +extern void clear_events(void); + +extern void set_dc_nickname(struct dive *dive); +extern void set_autogroup(bool value); +extern int total_weight(struct dive *); + +#ifdef __cplusplus +} +#endif + +#define DIVE_ERROR_PARSE 1 +#define DIVE_ERROR_PLAN 2 + +const char *weekday(int wday); +const char *monthname(int mon); + +#define UTF8_DEGREE "\xc2\xb0" +#define UTF8_DELTA "\xce\x94" +#define UTF8_UPWARDS_ARROW "\xE2\x86\x91" +#define UTF8_DOWNWARDS_ARROW "\xE2\x86\x93" +#define UTF8_AVERAGE "\xc3\xb8" +#define UTF8_SUBSCRIPT_2 "\xe2\x82\x82" +#define UTF8_WHITESTAR "\xe2\x98\x86" +#define UTF8_BLACKSTAR "\xe2\x98\x85" + +extern const char *existing_filename; +extern void subsurface_command_line_init(int *, char ***); +extern void subsurface_command_line_exit(int *, char ***); + +#define FRACTION(n, x) ((unsigned)(n) / (x)), ((unsigned)(n) % (x)) + +extern void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int setpoint, const struct dive *dive, int sac); +extern void clear_deco(double surface_pressure); +extern void dump_tissues(void); +extern void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth); +extern void cache_deco_state(char **datap); +extern void restore_deco_state(char *data); +extern void nuclear_regeneration(double time); +extern void vpmb_start_gradient(); +extern void vpmb_next_gradient(double deco_time, double surface_pressure); +extern double tissue_tolerance_calc(const struct dive *dive, double pressure); + +/* this should be converted to use our types */ +struct divedatapoint { + int time; + int depth; + struct gasmix gasmix; + int setpoint; + bool entered; + struct divedatapoint *next; +}; + +struct diveplan { + timestamp_t when; + int surface_pressure; /* mbar */ + int bottomsac; /* ml/min */ + int decosac; /* ml/min */ + int salinity; + short gflow; + short gfhigh; + struct divedatapoint *dp; +}; + +struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered); +struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2); +#if DEBUG_PLAN +void dump_plan(struct diveplan *diveplan); +#endif +bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer); +void calc_crushing_pressure(double pressure); +void vpmb_start_gradient(); + +void delete_single_dive(int idx); + +struct event *get_next_event(struct event *event, const char *name); + + +/* these structs holds the information that + * describes the cylinders / weight systems. + * they are global variables initialized in equipment.c + * used to fill the combobox in the add/edit cylinder + * dialog + */ + +struct tank_info_t { + const char *name; + int cuft, ml, psi, bar; +}; +extern struct tank_info_t tank_info[100]; + +struct ws_info_t { + const char *name; + int grams; +}; +extern struct ws_info_t ws_info[100]; + +extern bool cylinder_nodata(cylinder_t *cyl); +extern bool cylinder_none(void *_data); +extern bool weightsystem_none(void *_data); +extern bool no_weightsystems(weightsystem_t *ws); +extern bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2); +extern void remove_cylinder(struct dive *dive, int idx); +extern void remove_weightsystem(struct dive *dive, int idx); +extern void reset_cylinders(struct dive *dive, bool track_gas); + +/* + * String handling. + */ +#define STRTOD_NO_SIGN 0x01 +#define STRTOD_NO_DOT 0x02 +#define STRTOD_NO_COMMA 0x04 +#define STRTOD_NO_EXPONENT 0x08 +extern double strtod_flags(const char *str, const char **ptr, unsigned int flags); + +#define STRTOD_ASCII (STRTOD_NO_COMMA) + +#define ascii_strtod(str, ptr) strtod_flags(str, ptr, STRTOD_ASCII) + +extern void set_save_userid_local(short value); +extern void set_userid(char *user_id); +extern void set_informational_units(char *units); + +extern const char *get_dive_date_c_string(timestamp_t when); +extern void update_setpoint_events(struct divecomputer *dc); +#ifdef __cplusplus +} +#endif + +extern weight_t string_to_weight(const char *str); +extern depth_t string_to_depth(const char *str); +extern pressure_t string_to_pressure(const char *str); +extern volume_t string_to_volume(const char *str, pressure_t workp); +extern fraction_t string_to_fraction(const char *str); +extern void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth); + +#include "pref.h" + +#endif // DIVE_H diff --git a/core/divecomputer.cpp b/core/divecomputer.cpp new file mode 100644 index 000000000..e4081e1cd --- /dev/null +++ b/core/divecomputer.cpp @@ -0,0 +1,228 @@ +#include "divecomputer.h" +#include "dive.h" + +#include <QSettings> + +const char *default_dive_computer_vendor; +const char *default_dive_computer_product; +const char *default_dive_computer_device; +int default_dive_computer_download_mode; +DiveComputerList dcList; + +DiveComputerList::DiveComputerList() +{ +} + +DiveComputerList::~DiveComputerList() +{ +} + +bool DiveComputerNode::operator==(const DiveComputerNode &a) const +{ + return model == a.model && + deviceId == a.deviceId && + firmware == a.firmware && + serialNumber == a.serialNumber && + nickName == a.nickName; +} + +bool DiveComputerNode::operator!=(const DiveComputerNode &a) const +{ + return !(*this == a); +} + +bool DiveComputerNode::changesValues(const DiveComputerNode &b) const +{ + if (model != b.model || deviceId != b.deviceId) { + qDebug("DiveComputerNodes were not for the same DC"); + return false; + } + return (firmware != b.firmware) || + (serialNumber != b.serialNumber) || + (nickName != b.nickName); +} + +const DiveComputerNode *DiveComputerList::getExact(const QString &m, uint32_t d) +{ + for (QMap<QString, DiveComputerNode>::iterator it = dcMap.find(m); it != dcMap.end() && it.key() == m; ++it) + if (it->deviceId == d) + return &*it; + return NULL; +} + +const DiveComputerNode *DiveComputerList::get(const QString &m) +{ + QMap<QString, DiveComputerNode>::iterator it = dcMap.find(m); + if (it != dcMap.end()) + return &*it; + return NULL; +} + +void DiveComputerNode::showchanges(const QString &n, const QString &s, const QString &f) const +{ + if (nickName != n) + qDebug("new nickname %s for DC model %s deviceId 0x%x", n.toUtf8().data(), model.toUtf8().data(), deviceId); + if (serialNumber != s) + qDebug("new serial number %s for DC model %s deviceId 0x%x", s.toUtf8().data(), model.toUtf8().data(), deviceId); + if (firmware != f) + qDebug("new firmware version %s for DC model %s deviceId 0x%x", f.toUtf8().data(), model.toUtf8().data(), deviceId); +} + +void DiveComputerList::addDC(QString m, uint32_t d, QString n, QString s, QString f) +{ + if (m.isEmpty() || d == 0) + return; + const DiveComputerNode *existNode = this->getExact(m, d); + + if (existNode) { + // Update any non-existent fields from the old entry + if (n.isEmpty()) + n = existNode->nickName; + if (s.isEmpty()) + s = existNode->serialNumber; + if (f.isEmpty()) + f = existNode->firmware; + + // Do all the old values match? + if (n == existNode->nickName && s == existNode->serialNumber && f == existNode->firmware) + return; + + // debugging: show changes + if (verbose) + existNode->showchanges(n, s, f); + dcMap.remove(m, *existNode); + } + + DiveComputerNode newNode(m, d, s, f, n); + dcMap.insert(m, newNode); +} + +extern "C" void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname) +{ + dcList.addDC(model, deviceid, nickname, serial, firmware); +} + +extern "C" bool compareDC(const DiveComputerNode &a, const DiveComputerNode &b) +{ + return a.deviceId < b.deviceId; +} + +extern "C" void call_for_each_dc (void *f, void (*callback)(void *, const char *, uint32_t, + const char *, const char *, const char *), + bool select_only) +{ + QList<DiveComputerNode> values = dcList.dcMap.values(); + qSort(values.begin(), values.end(), compareDC); + for (int i = 0; i < values.size(); i++) { + const DiveComputerNode *node = &values.at(i); + bool found = false; + if (select_only) { + int j; + struct dive *d; + for_each_dive (j, d) { + struct divecomputer *dc; + if (!d->selected) + continue; + for_each_dc(d, dc) { + if (dc->deviceid == node->deviceId) { + found = true; + break; + } + } + if (found) + break; + } + } else { + found = true; + } + if (found) + callback(f, node->model.toUtf8().data(), node->deviceId, node->nickName.toUtf8().data(), + node->serialNumber.toUtf8().data(), node->firmware.toUtf8().data()); + } +} + + +extern "C" int is_default_dive_computer(const char *vendor, const char *product) +{ + return default_dive_computer_vendor && !strcmp(vendor, default_dive_computer_vendor) && + default_dive_computer_product && !strcmp(product, default_dive_computer_product); +} + +extern "C" int is_default_dive_computer_device(const char *name) +{ + return default_dive_computer_device && !strcmp(name, default_dive_computer_device); +} + +void set_default_dive_computer(const char *vendor, const char *product) +{ + QSettings s; + + if (!vendor || !*vendor) + return; + if (!product || !*product) + return; + if (is_default_dive_computer(vendor, product)) + return; + + free((void *)default_dive_computer_vendor); + free((void *)default_dive_computer_product); + default_dive_computer_vendor = strdup(vendor); + default_dive_computer_product = strdup(product); + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_vendor", vendor); + s.setValue("dive_computer_product", product); + s.endGroup(); +} + +void set_default_dive_computer_device(const char *name) +{ + QSettings s; + + if (!name || !*name) + return; + if (is_default_dive_computer_device(name)) + return; + + free((void *)default_dive_computer_device); + default_dive_computer_device = strdup(name); + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_device", name); + s.endGroup(); +} + +void set_default_dive_computer_download_mode(int download_mode) +{ + QSettings s; + + default_dive_computer_download_mode = download_mode; + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_download_mode", download_mode); + s.endGroup(); +} + +extern "C" void set_dc_nickname(struct dive *dive) +{ + if (!dive) + return; + + struct divecomputer *dc; + + for_each_dc (dive, dc) { + if (dc->model && *dc->model && dc->deviceid && + !dcList.getExact(dc->model, dc->deviceid)) { + // we don't have this one, yet + const DiveComputerNode *existNode = dcList.get(dc->model); + if (existNode) { + // we already have this model but a different deviceid + QString simpleNick(dc->model); + if (dc->deviceid == 0) + simpleNick.append(" (unknown deviceid)"); + else + simpleNick.append(" (").append(QString::number(dc->deviceid, 16)).append(")"); + dcList.addDC(dc->model, dc->deviceid, simpleNick); + } else { + dcList.addDC(dc->model, dc->deviceid); + } + } + } +} diff --git a/core/divecomputer.h b/core/divecomputer.h new file mode 100644 index 000000000..98d12ab8b --- /dev/null +++ b/core/divecomputer.h @@ -0,0 +1,38 @@ +#ifndef DIVECOMPUTER_H +#define DIVECOMPUTER_H + +#include <QString> +#include <QMap> +#include <stdint.h> + +class DiveComputerNode { +public: + DiveComputerNode(QString m, uint32_t d, QString s, QString f, QString n) + : model(m), deviceId(d), serialNumber(s), firmware(f), nickName(n) {}; + bool operator==(const DiveComputerNode &a) const; + bool operator!=(const DiveComputerNode &a) const; + bool changesValues(const DiveComputerNode &b) const; + void showchanges(const QString &n, const QString &s, const QString &f) const; + QString model; + uint32_t deviceId; + QString serialNumber; + QString firmware; + QString nickName; +}; + +class DiveComputerList { +public: + DiveComputerList(); + ~DiveComputerList(); + const DiveComputerNode *getExact(const QString &m, uint32_t d); + const DiveComputerNode *get(const QString &m); + void addDC(QString m, uint32_t d, QString n = QString(), QString s = QString(), QString f = QString()); + DiveComputerNode matchDC(const QString &m, uint32_t d); + DiveComputerNode matchModel(const QString &m); + QMultiMap<QString, DiveComputerNode> dcMap; + QMultiMap<QString, DiveComputerNode> dcWorkingMap; +}; + +extern DiveComputerList dcList; + +#endif diff --git a/core/divelist.c b/core/divelist.c new file mode 100644 index 000000000..543d9e17b --- /dev/null +++ b/core/divelist.c @@ -0,0 +1,1207 @@ +/* divelist.c */ +/* core logic for the dive list - + * accessed through the following interfaces: + * + * dive_trip_t *dive_trip_list; + * unsigned int amount_selected; + * void dump_selection(void) + * dive_trip_t *find_trip_by_idx(int idx) + * int trip_has_selected_dives(dive_trip_t *trip) + * void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p) + * int total_weight(struct dive *dive) + * int get_divenr(struct dive *dive) + * double init_decompression(struct dive *dive) + * void update_cylinder_related_info(struct dive *dive) + * void dump_trip_list(void) + * dive_trip_t *find_matching_trip(timestamp_t when) + * void insert_trip(dive_trip_t **dive_trip_p) + * void remove_dive_from_trip(struct dive *dive) + * void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) + * dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) + * void autogroup_dives(void) + * void delete_single_dive(int idx) + * void add_single_dive(int idx, struct dive *dive) + * void merge_two_dives(struct dive *a, struct dive *b) + * void select_dive(int idx) + * void deselect_dive(int idx) + * void mark_divelist_changed(int changed) + * int unsaved_changes() + * void remove_autogen_trips() + */ +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <math.h> +#include "gettext.h" +#include <assert.h> +#include <zip.h> +#include <libxslt/transform.h> + +#include "dive.h" +#include "divelist.h" +#include "display.h" +#include "planner.h" +#include "qthelperfromc.h" +#include "git-access.h" + +static short dive_list_changed = false; + +short autogroup = false; + +dive_trip_t *dive_trip_list; + +unsigned int amount_selected; + +#if DEBUG_SELECTION_TRACKING +void dump_selection(void) +{ + int i; + struct dive *dive; + + printf("currently selected are %u dives:", amount_selected); + for_each_dive(i, dive) { + if (dive->selected) + printf(" %d", i); + } + printf("\n"); +} +#endif + +void set_autogroup(bool value) +{ + /* if we keep the UI paradigm, this needs to toggle + * the checkbox on the autogroup menu item */ + autogroup = value; +} + +dive_trip_t *find_trip_by_idx(int idx) +{ + dive_trip_t *trip = dive_trip_list; + + if (idx >= 0) + return NULL; + idx = -idx; + while (trip) { + if (trip->index == idx) + return trip; + trip = trip->next; + } + return NULL; +} + +int trip_has_selected_dives(dive_trip_t *trip) +{ + struct dive *dive; + for (dive = trip->dives; dive; dive = dive->next) { + if (dive->selected) + return 1; + } + return 0; +} + +/* + * Get "maximal" dive gas for a dive. + * Rules: + * - Trimix trumps nitrox (highest He wins, O2 breaks ties) + * - Nitrox trumps air (even if hypoxic) + * These are the same rules as the inter-dive sorting rules. + */ +void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2max_p) +{ + int i; + int maxo2 = -1, maxhe = -1, mino2 = 1000; + + + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int o2 = get_o2(&cyl->gasmix); + int he = get_he(&cyl->gasmix); + + if (!is_cylinder_used(dive, i)) + continue; + if (cylinder_none(cyl)) + continue; + if (o2 > maxo2) + maxo2 = o2; + if (he > maxhe) + goto newmax; + if (he < maxhe) + continue; + if (o2 <= maxo2) + continue; + newmax: + maxhe = he; + mino2 = o2; + } + /* All air? Show/sort as "air"/zero */ + if ((!maxhe && maxo2 == O2_IN_AIR && mino2 == maxo2) || + (maxo2 == -1 && maxhe == -1 && mino2 == 1000)) + maxo2 = mino2 = 0; + *o2_p = mino2; + *he_p = maxhe; + *o2max_p = maxo2; +} + +int total_weight(struct dive *dive) +{ + int i, total_grams = 0; + + if (dive) + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + total_grams += dive->weightsystem[i].weight.grams; + return total_grams; +} + +static int active_o2(struct dive *dive, struct divecomputer *dc, duration_t time) +{ + struct gasmix gas; + get_gas_at_time(dive, dc, time, &gas); + return get_o2(&gas); +} + +/* calculate OTU for a dive - this only takes the first divecomputer into account */ +static int calculate_otu(struct dive *dive) +{ + int i; + double otu = 0.0; + struct divecomputer *dc = &dive->dc; + + for (i = 1; i < dc->samples; i++) { + int t; + int po2; + struct sample *sample = dc->sample + i; + struct sample *psample = sample - 1; + t = sample->time.seconds - psample->time.seconds; + if (sample->setpoint.mbar) { + po2 = sample->setpoint.mbar; + } else { + int o2 = active_o2(dive, dc, sample->time); + po2 = o2 * depth_to_atm(sample->depth.mm, dive); + } + if (po2 >= 500) + otu += pow((po2 - 500) / 1000.0, 0.83) * t / 30.0; + } + return rint(otu); +} +/* calculate CNS for a dive - this only takes the first divecomputer into account */ +int const cns_table[][3] = { + /* po2, Maximum Single Exposure, Maximum 24 hour Exposure */ + { 1600, 45 * 60, 150 * 60 }, + { 1500, 120 * 60, 180 * 60 }, + { 1400, 150 * 60, 180 * 60 }, + { 1300, 180 * 60, 210 * 60 }, + { 1200, 210 * 60, 240 * 60 }, + { 1100, 240 * 60, 270 * 60 }, + { 1000, 300 * 60, 300 * 60 }, + { 900, 360 * 60, 360 * 60 }, + { 800, 450 * 60, 450 * 60 }, + { 700, 570 * 60, 570 * 60 }, + { 600, 720 * 60, 720 * 60 } +}; + +/* this only gets called if dive->maxcns == 0 which means we know that + * none of the divecomputers has tracked any CNS for us + * so we calculated it "by hand" */ +static int calculate_cns(struct dive *dive) +{ + int i, divenr; + size_t j; + double cns = 0.0; + struct divecomputer *dc = &dive->dc; + struct dive *prev_dive; + timestamp_t endtime; + + /* shortcut */ + if (dive->cns) + return dive->cns; + /* + * Do we start with a cns loading from a previous dive? + * Check if we did a dive 12 hours prior, and what cns we had from that. + * Then apply ha 90min halftime to see whats left. + */ + divenr = get_divenr(dive); + if (divenr) { + prev_dive = get_dive(divenr - 1); + if (prev_dive) { + endtime = prev_dive->when + prev_dive->duration.seconds; + if (dive->when < (endtime + 3600 * 12)) { + cns = calculate_cns(prev_dive); + cns = cns * 1 / pow(2, (dive->when - endtime) / (90.0 * 60.0)); + } + } + } + /* Caclulate the cns for each sample in this dive and sum them */ + for (i = 1; i < dc->samples; i++) { + int t; + int po2; + struct sample *sample = dc->sample + i; + struct sample *psample = sample - 1; + t = sample->time.seconds - psample->time.seconds; + if (sample->setpoint.mbar) { + po2 = sample->setpoint.mbar; + } else { + int o2 = active_o2(dive, dc, sample->time); + po2 = o2 * depth_to_atm(sample->depth.mm, dive); + } + /* CNS don't increse when below 500 matm */ + if (po2 < 500) + continue; + /* Find what table-row we should calculate % for */ + for (j = 1; j < sizeof(cns_table) / (sizeof(int) * 3); j++) + if (po2 > cns_table[j][0]) + break; + j--; + cns += ((double)t) / ((double)cns_table[j][1]) * 100; + } + /* save calculated cns in dive struct */ + dive->cns = cns; + return dive->cns; +} +/* + * Return air usage (in liters). + */ +static double calculate_airuse(struct dive *dive) +{ + int airuse = 0; + int i; + + for (i = 0; i < MAX_CYLINDERS; i++) { + pressure_t start, end; + cylinder_t *cyl = dive->cylinder + i; + + start = cyl->start.mbar ? cyl->start : cyl->sample_start; + end = cyl->end.mbar ? cyl->end : cyl->sample_end; + if (!end.mbar || start.mbar <= end.mbar) + continue; + + airuse += gas_volume(cyl, start) - gas_volume(cyl, end); + } + return airuse / 1000.0; +} + +/* this only uses the first divecomputer to calculate the SAC rate */ +static int calculate_sac(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + double airuse, pressure, sac; + int duration, meandepth; + + airuse = calculate_airuse(dive); + if (!airuse) + return 0; + + duration = dc->duration.seconds; + if (!duration) + return 0; + + meandepth = dc->meandepth.mm; + if (!meandepth) + return 0; + + /* Mean pressure in ATM (SAC calculations are in atm*l/min) */ + pressure = depth_to_atm(meandepth, dive); + sac = airuse / pressure * 60 / duration; + + /* milliliters per minute.. */ + return sac * 1000; +} + +/* for now we do this based on the first divecomputer */ +static void add_dive_to_deco(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + int i; + + if (!dc) + return; + for (i = 1; i < dc->samples; i++) { + struct sample *psample = dc->sample + i - 1; + struct sample *sample = dc->sample + i; + int t0 = psample->time.seconds; + int t1 = sample->time.seconds; + int j; + + for (j = t0; j < t1; j++) { + int depth = interpolate(psample->depth.mm, sample->depth.mm, j - t0, t1 - t0); + add_segment(depth_to_bar(depth, dive), + &dive->cylinder[sample->sensor].gasmix, 1, sample->setpoint.mbar, dive, dive->sac); + } + } +} + +int get_divenr(struct dive *dive) +{ + int i; + struct dive *d; + // tempting as it may be, don't die when called with dive=NULL + if (dive) + for_each_dive(i, d) { + if (d->id == dive->id) // don't compare pointers, we could be passing in a copy of the dive + return i; + } + return -1; +} + +int get_divesite_idx(struct dive_site *ds) +{ + int i; + struct dive_site *d; + // tempting as it may be, don't die when called with dive=NULL + if (ds) + for_each_dive_site(i, d) { + if (d->uuid == ds->uuid) // don't compare pointers, we could be passing in a copy of the dive + return i; + } + return -1; +} + +static struct gasmix air = { .o2.permille = O2_IN_AIR, .he.permille = 0 }; + +/* take into account previous dives until there is a 48h gap between dives */ +double init_decompression(struct dive *dive) +{ + int i, divenr = -1; + unsigned int surface_time; + timestamp_t when, lasttime = 0, laststart = 0; + bool deco_init = false; + double surface_pressure; + + if (!dive) + return 0.0; + + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + divenr = get_divenr(dive); + when = dive->when; + i = divenr; + if (i < 0) { + i = dive_table.nr - 1; + while (i >= 0 && get_dive(i)->when > when) + --i; + i++; + } + while (i--) { + struct dive *pdive = get_dive(i); + /* we don't want to mix dives from different trips as we keep looking + * for how far back we need to go */ + if (dive->divetrip && pdive->divetrip != dive->divetrip) + continue; + if (!pdive || pdive->when >= when || pdive->when + pdive->duration.seconds + 48 * 60 * 60 < when) + break; + /* For simultaneous dives, only consider the first */ + if (pdive->when == laststart) + continue; + when = pdive->when; + lasttime = when + pdive->duration.seconds; + } + while (++i < (divenr >= 0 ? divenr : dive_table.nr)) { + struct dive *pdive = get_dive(i); + /* again skip dives from different trips */ + if (dive->divetrip && dive->divetrip != pdive->divetrip) + continue; + surface_pressure = get_surface_pressure_in_mbar(pdive, true) / 1000.0; + if (!deco_init) { + clear_deco(surface_pressure); + deco_init = true; +#if DECO_CALC_DEBUG & 2 + dump_tissues(); +#endif + } + add_dive_to_deco(pdive); + laststart = pdive->when; +#if DECO_CALC_DEBUG & 2 + printf("added dive #%d\n", pdive->number); + dump_tissues(); +#endif + if (pdive->when > lasttime) { + surface_time = pdive->when - lasttime; + lasttime = pdive->when + pdive->duration.seconds; + add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); +#if DECO_CALC_DEBUG & 2 + printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); + dump_tissues(); +#endif + } + } + /* add the final surface time */ + if (lasttime && dive->when > lasttime) { + surface_time = dive->when - lasttime; + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); +#if DECO_CALC_DEBUG & 2 + printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); + dump_tissues(); +#endif + } + if (!deco_init) { + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + clear_deco(surface_pressure); +#if DECO_CALC_DEBUG & 2 + printf("no previous dive\n"); + dump_tissues(); +#endif + } + return tissue_tolerance_calc(dive, surface_pressure); +} + +void update_cylinder_related_info(struct dive *dive) +{ + if (dive != NULL) { + dive->sac = calculate_sac(dive); + dive->otu = calculate_otu(dive); + if (dive->maxcns == 0) + dive->maxcns = calculate_cns(dive); + } +} + +#define MAX_GAS_STRING 80 +#define UTF8_ELLIPSIS "\xE2\x80\xA6" + +/* callers needs to free the string */ +char *get_dive_gas_string(struct dive *dive) +{ + int o2, he, o2max; + char *buffer = malloc(MAX_GAS_STRING); + + if (buffer) { + get_dive_gas(dive, &o2, &he, &o2max); + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + o2max = (o2max + 5) / 10; + + if (he) + if (o2 == o2max) + snprintf(buffer, MAX_GAS_STRING, "%d/%d", o2, he); + else + snprintf(buffer, MAX_GAS_STRING, "%d/%d" UTF8_ELLIPSIS "%d%%", o2, he, o2max); + else if (o2) + if (o2 == o2max) + snprintf(buffer, MAX_GAS_STRING, "%d%%", o2); + else + snprintf(buffer, MAX_GAS_STRING, "%d" UTF8_ELLIPSIS "%d%%", o2, o2max); + else + strcpy(buffer, translate("gettextFromC", "air")); + } + return buffer; +} + +/* + * helper functions for dive_trip handling + */ +#ifdef DEBUG_TRIP +void dump_trip_list(void) +{ + dive_trip_t *trip; + int i = 0; + timestamp_t last_time = 0; + + for (trip = dive_trip_list; trip; trip = trip->next) { + struct tm tm; + utc_mkdate(trip->when, &tm); + if (trip->when < last_time) + printf("\n\ndive_trip_list OUT OF ORDER!!!\n\n\n"); + printf("%s trip %d to \"%s\" on %04u-%02u-%02u %02u:%02u:%02u (%d dives - %p)\n", + trip->autogen ? "autogen " : "", + ++i, trip->location, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, + trip->nrdives, trip); + last_time = trip->when; + } + printf("-----\n"); +} +#endif + +/* this finds the last trip that at or before the time given */ +dive_trip_t *find_matching_trip(timestamp_t when) +{ + dive_trip_t *trip = dive_trip_list; + + if (!trip || trip->when > when) { +#ifdef DEBUG_TRIP + printf("no matching trip\n"); +#endif + return NULL; + } + while (trip->next && trip->next->when <= when) + trip = trip->next; +#ifdef DEBUG_TRIP + { + struct tm tm; + utc_mkdate(trip->when, &tm); + printf("found trip %p @ %04d-%02d-%02d %02d:%02d:%02d\n", + trip, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); + } +#endif + return trip; +} + +/* insert the trip into the dive_trip_list - but ensure you don't have + * two trips for the same date; but if you have, make sure you don't + * keep the one with less information */ +void insert_trip(dive_trip_t **dive_trip_p) +{ + dive_trip_t *dive_trip = *dive_trip_p; + dive_trip_t **p = &dive_trip_list; + dive_trip_t *trip; + struct dive *divep; + + /* Walk the dive trip list looking for the right location.. */ + while ((trip = *p) != NULL && trip->when < dive_trip->when) + p = &trip->next; + + if (trip && trip->when == dive_trip->when) { + if (!trip->location) + trip->location = dive_trip->location; + if (!trip->notes) + trip->notes = dive_trip->notes; + divep = dive_trip->dives; + while (divep) { + add_dive_to_trip(divep, trip); + divep = divep->next; + } + *dive_trip_p = trip; + } else { + dive_trip->next = trip; + *p = dive_trip; + } +#ifdef DEBUG_TRIP + dump_trip_list(); +#endif +} + +static void delete_trip(dive_trip_t *trip) +{ + dive_trip_t **p, *tmp; + + assert(!trip->dives); + + /* Remove the trip from the list of trips */ + p = &dive_trip_list; + while ((tmp = *p) != NULL) { + if (tmp == trip) { + *p = trip->next; + break; + } + p = &tmp->next; + } + + /* .. and free it */ + free(trip->location); + free(trip->notes); + free(trip); +} + +void find_new_trip_start_time(dive_trip_t *trip) +{ + struct dive *dive = trip->dives; + timestamp_t when = dive->when; + + while ((dive = dive->next) != NULL) { + if (dive->when < when) + when = dive->when; + } + trip->when = when; +} + +/* check if we have a trip right before / after this dive */ +bool is_trip_before_after(struct dive *dive, bool before) +{ + int idx = get_idx_by_uniq_id(dive->id); + if (before) { + if (idx > 0 && get_dive(idx - 1)->divetrip) + return true; + } else { + if (idx < dive_table.nr - 1 && get_dive(idx + 1)->divetrip) + return true; + } + return false; +} + +struct dive *first_selected_dive() +{ + int idx; + struct dive *d; + + for_each_dive (idx, d) { + if (d->selected) + return d; + } + return NULL; +} + +struct dive *last_selected_dive() +{ + int idx; + struct dive *d, *ret = NULL; + + for_each_dive (idx, d) { + if (d->selected) + ret = d; + } + return ret; +} + +void remove_dive_from_trip(struct dive *dive, short was_autogen) +{ + struct dive *next, **pprev; + dive_trip_t *trip = dive->divetrip; + + if (!trip) + return; + + /* Remove the dive from the trip's list of dives */ + next = dive->next; + pprev = dive->pprev; + *pprev = next; + if (next) + next->pprev = pprev; + + dive->divetrip = NULL; + if (was_autogen) + dive->tripflag = TF_NONE; + else + dive->tripflag = NO_TRIP; + assert(trip->nrdives > 0); + if (!--trip->nrdives) + delete_trip(trip); + else if (trip->when == dive->when) + find_new_trip_start_time(trip); +} + +void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) +{ + if (dive->divetrip == trip) + return; + remove_dive_from_trip(dive, false); + trip->nrdives++; + dive->divetrip = trip; + dive->tripflag = ASSIGNED_TRIP; + + /* Add it to the trip's list of dives*/ + dive->next = trip->dives; + if (dive->next) + dive->next->pprev = &dive->next; + trip->dives = dive; + dive->pprev = &trip->dives; + + if (dive->when && trip->when > dive->when) + trip->when = dive->when; +} + +dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) +{ + dive_trip_t *dive_trip = calloc(1, sizeof(dive_trip_t)); + + dive_trip->when = dive->when; + dive_trip->location = copy_string(get_dive_location(dive)); + insert_trip(&dive_trip); + + dive->tripflag = IN_TRIP; + add_dive_to_trip(dive, dive_trip); + return dive_trip; +} + +/* + * Walk the dives from the oldest dive, and see if we can autogroup them + */ +void autogroup_dives(void) +{ + int i; + struct dive *dive, *lastdive = NULL; + + for_each_dive(i, dive) { + dive_trip_t *trip; + + if (dive->divetrip) { + lastdive = dive; + continue; + } + + if (!DIVE_NEEDS_TRIP(dive)) { + lastdive = NULL; + continue; + } + + /* Do we have a trip we can combine this into? */ + if (lastdive && dive->when < lastdive->when + TRIP_THRESHOLD) { + dive_trip_t *trip = lastdive->divetrip; + add_dive_to_trip(dive, trip); + if (get_dive_location(dive) && !trip->location) + trip->location = copy_string(get_dive_location(dive)); + lastdive = dive; + continue; + } + + lastdive = dive; + trip = create_and_hookup_trip_from_dive(dive); + trip->autogen = 1; + } + +#ifdef DEBUG_TRIP + dump_trip_list(); +#endif +} + +/* this implements the mechanics of removing the dive from the table, + * but doesn't deal with updating dive trips, etc */ +void delete_single_dive(int idx) +{ + int i; + struct dive *dive = get_dive(idx); + if (!dive) + return; /* this should never happen */ + remove_dive_from_trip(dive, false); + if (dive->selected) + deselect_dive(idx); + for (i = idx; i < dive_table.nr - 1; i++) + dive_table.dives[i] = dive_table.dives[i + 1]; + dive_table.dives[--dive_table.nr] = NULL; + /* free all allocations */ + free(dive->dc.sample); + free((void *)dive->notes); + free((void *)dive->divemaster); + free((void *)dive->buddy); + free((void *)dive->suit); + taglist_free(dive->tag_list); + free(dive); +} + +struct dive **grow_dive_table(struct dive_table *table) +{ + int nr = table->nr, allocated = table->allocated; + struct dive **dives = table->dives; + + if (nr >= allocated) { + allocated = (nr + 32) * 3 / 2; + dives = realloc(dives, allocated * sizeof(struct dive *)); + if (!dives) + exit(1); + table->dives = dives; + table->allocated = allocated; + } + return dives; +} + +void add_single_dive(int idx, struct dive *dive) +{ + int i; + grow_dive_table(&dive_table); + dive_table.nr++; + if (dive->selected) + amount_selected++; + + if (idx < 0) { + // convert an idx of -1 so we do insert-in-chronological-order + idx = dive_table.nr - 1; + for (int i = 0; i < dive_table.nr - 1; i++) { + if (dive->when <= dive_table.dives[i]->when) { + idx = i; + break; + } + } + } + + for (i = idx; i < dive_table.nr; i++) { + struct dive *tmp = dive_table.dives[i]; + dive_table.dives[i] = dive; + dive = tmp; + } +} + +bool consecutive_selected() +{ + struct dive *d; + int i; + bool consecutive = true; + bool firstfound = false; + bool lastfound = false; + + if (amount_selected == 0 || amount_selected == 1) + return true; + + for_each_dive(i, d) { + if (d->selected) { + if (!firstfound) + firstfound = true; + else if (lastfound) + consecutive = false; + } else if (firstfound) { + lastfound = true; + } + } + return consecutive; +} + +/* + * Merge two dives. 'a' is always before 'b' in the dive list + * (and thus in time). + */ +struct dive *merge_two_dives(struct dive *a, struct dive *b) +{ + struct dive *res; + int i, j, nr, nrdiff; + int id; + + if (!a || !b) + return NULL; + + id = a->id; + i = get_divenr(a); + j = get_divenr(b); + if (i < 0 || j < 0) + // something is wrong with those dives. Bail + return NULL; + res = merge_dives(a, b, b->when - a->when, false); + if (!res) + return NULL; + + /* + * If 'a' and 'b' were numbered, and in proper order, + * then the resulting dive will get the first number, + * and the subsequent dives will be renumbered by the + * difference. + * + * So if you had a dive list 1 3 6 7 8, and you + * merge 1 and 3, the resulting numbered list will + * be 1 4 5 6, because we assume that there were + * some missing dives (originally dives 4 and 5), + * that now will still be missing (dives 2 and 3 + * in the renumbered world). + * + * Obviously the normal case is that everything is + * consecutive, and the difference will be 1, so the + * above example is not supposed to be normal. + */ + nrdiff = 0; + nr = a->number; + if (a->number && b->number > a->number) { + res->number = nr; + nrdiff = b->number - nr; + } + + add_single_dive(i, res); + delete_single_dive(i + 1); + delete_single_dive(j); + // now make sure that we keep the id of the first dive. + // why? + // because this way one of the previously selected ids is still around + res->id = id; + + // renumber dives from merged one in advance by difference between + // merged dives numbers. Do not renumber if actual number is zero. + for (; j < dive_table.nr; j++) { + struct dive *dive = dive_table.dives[j]; + int newnr; + + if (!dive->number) + continue; + newnr = dive->number - nrdiff; + + /* + * Don't renumber stuff that isn't in order! + * + * So if the new dive number isn't larger than the + * previous dive number, just stop here. + */ + if (newnr <= nr) + break; + dive->number = newnr; + nr = newnr; + } + + mark_divelist_changed(true); + return res; +} + +void select_dive(int idx) +{ + struct dive *dive = get_dive(idx); + if (dive) { + /* never select an invalid dive that isn't displayed */ + if (!dive->selected) { + dive->selected = 1; + amount_selected++; + } + selected_dive = idx; + } +} + +void deselect_dive(int idx) +{ + struct dive *dive = get_dive(idx); + if (dive && dive->selected) { + dive->selected = 0; + if (amount_selected) + amount_selected--; + if (selected_dive == idx && amount_selected > 0) { + /* pick a different dive as selected */ + while (--selected_dive >= 0) { + dive = get_dive(selected_dive); + if (dive && dive->selected) + return; + } + selected_dive = idx; + while (++selected_dive < dive_table.nr) { + dive = get_dive(selected_dive); + if (dive && dive->selected) + return; + } + } + if (amount_selected == 0) + selected_dive = -1; + } +} + +void deselect_dives_in_trip(struct dive_trip *trip) +{ + struct dive *dive; + if (!trip) + return; + for (dive = trip->dives; dive; dive = dive->next) + deselect_dive(get_divenr(dive)); +} + +void select_dives_in_trip(struct dive_trip *trip) +{ + struct dive *dive; + if (!trip) + return; + for (dive = trip->dives; dive; dive = dive->next) + if (!dive->hidden_by_filter) + select_dive(get_divenr(dive)); +} + +void filter_dive(struct dive *d, bool shown) +{ + if (!d) + return; + d->hidden_by_filter = !shown; + if (!shown && d->selected) + deselect_dive(get_divenr(d)); +} + + +/* This only gets called with non-NULL trips. + * It does not combine notes or location, just picks the first one + * (or the second one if the first one is empty */ +void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b) +{ + if (same_string(trip_a->location, "") && trip_b->location) { + free(trip_a->location); + trip_a->location = strdup(trip_b->location); + } + if (same_string(trip_a->notes, "") && trip_b->notes) { + free(trip_a->notes); + trip_a->notes = strdup(trip_b->notes); + } + /* this also removes the dives from trip_b and eventually + * calls delete_trip(trip_b) when the last dive has been moved */ + while (trip_b->dives) + add_dive_to_trip(trip_b->dives, trip_a); +} + +void mark_divelist_changed(int changed) +{ + dive_list_changed = changed; + updateWindowTitle(); +} + +int unsaved_changes() +{ + return dive_list_changed; +} + +void remove_autogen_trips() +{ + int i; + struct dive *dive; + + for_each_dive(i, dive) { + dive_trip_t *trip = dive->divetrip; + + if (trip && trip->autogen) + remove_dive_from_trip(dive, true); + } +} + +/* + * When adding dives to the dive table, we try to renumber + * the new dives based on any old dives in the dive table. + * + * But we only do it if: + * + * - there are no dives in the dive table + * + * OR + * + * - the last dive in the old dive table was numbered + * + * - all the new dives are strictly at the end (so the + * "last dive" is at the same location in the dive table + * after re-sorting the dives. + * + * - none of the new dives have any numbers + * + * This catches the common case of importing new dives from + * a dive computer, and gives them proper numbers based on + * your old dive list. But it tries to be very conservative + * and not give numbers if there is *any* question about + * what the numbers should be - in which case you need to do + * a manual re-numbering. + */ +static void try_to_renumber(struct dive *last, int preexisting) +{ + int i, nr; + + /* + * If the new dives aren't all strictly at the end, + * we're going to expect the user to do a manual + * renumbering. + */ + if (preexisting && get_dive(preexisting - 1) != last) + return; + + /* + * If any of the new dives already had a number, + * we'll have to do a manual renumbering. + */ + for (i = preexisting; i < dive_table.nr; i++) { + struct dive *dive = get_dive(i); + if (dive->number) + return; + } + + /* + * Ok, renumber.. + */ + if (last) + nr = last->number; + else + nr = 0; + for (i = preexisting; i < dive_table.nr; i++) { + struct dive *dive = get_dive(i); + dive->number = ++nr; + } +} + +void process_dives(bool is_imported, bool prefer_imported) +{ + int i; + int preexisting = dive_table.preexisting; + bool did_merge = false; + struct dive *last; + + /* check if we need a nickname for the divecomputer for newly downloaded dives; + * since we know they all came from the same divecomputer we just check for the + * first one */ + if (preexisting < dive_table.nr && dive_table.dives[preexisting]->downloaded) + set_dc_nickname(dive_table.dives[preexisting]); + else + /* they aren't downloaded, so record / check all new ones */ + for (i = preexisting; i < dive_table.nr; i++) + set_dc_nickname(dive_table.dives[i]); + + for (i = preexisting; i < dive_table.nr; i++) + dive_table.dives[i]->downloaded = true; + + /* This does the right thing for -1: NULL */ + last = get_dive(preexisting - 1); + + sort_table(&dive_table); + + for (i = 1; i < dive_table.nr; i++) { + struct dive **pp = &dive_table.dives[i - 1]; + struct dive *prev = pp[0]; + struct dive *dive = pp[1]; + struct dive *merged; + int id; + + /* only try to merge overlapping dives - or if one of the dives has + * zero duration (that might be a gps marker from the webservice) */ + if (prev->duration.seconds && dive->duration.seconds && + prev->when + prev->duration.seconds < dive->when) + continue; + + merged = try_to_merge(prev, dive, prefer_imported); + if (!merged) + continue; + + // remember the earlier dive's id + id = prev->id; + + /* careful - we might free the dive that last points to. Oops... */ + if (last == prev || last == dive) + last = merged; + + /* Redo the new 'i'th dive */ + i--; + add_single_dive(i, merged); + delete_single_dive(i + 1); + delete_single_dive(i + 1); + // keep the id or the first dive for the merged dive + merged->id = id; + + /* this means the table was changed */ + did_merge = true; + } + /* make sure no dives are still marked as downloaded */ + for (i = 1; i < dive_table.nr; i++) + dive_table.dives[i]->downloaded = false; + + if (is_imported) { + /* If there are dives in the table, are they numbered */ + if (!last || last->number) + try_to_renumber(last, preexisting); + + /* did we add dives or divecomputers to the dive table? */ + if (did_merge || preexisting < dive_table.nr) { + mark_divelist_changed(true); + } + } +} + +void set_dive_nr_for_current_dive() +{ + if (dive_table.nr == 1) + current_dive->number = 1; + else if (selected_dive == dive_table.nr - 1 && get_dive(dive_table.nr - 2)->number) + current_dive->number = get_dive(dive_table.nr - 2)->number + 1; +} + +static int min_datafile_version; + +int get_min_datafile_version() +{ + return min_datafile_version; +} + +void reset_min_datafile_version() +{ + min_datafile_version = 0; +} + +void report_datafile_version(int version) +{ + if (min_datafile_version == 0 || min_datafile_version > version) + min_datafile_version = version; +} + +void clear_dive_file_data() +{ + while (dive_table.nr) + delete_single_dive(0); + while (dive_site_table.nr) + delete_dive_site(get_dive_site(0)->uuid); + + clear_dive(&displayed_dive); + clear_dive_site(&displayed_dive_site); + + free((void *)existing_filename); + existing_filename = NULL; + + reset_min_datafile_version(); + saved_git_id = ""; +} diff --git a/core/divelist.h b/core/divelist.h new file mode 100644 index 000000000..5bae09cff --- /dev/null +++ b/core/divelist.h @@ -0,0 +1,62 @@ +#ifndef DIVELIST_H +#define DIVELIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* this is used for both git and xml format */ +#define DATAFORMAT_VERSION 3 + +struct dive; + +extern void update_cylinder_related_info(struct dive *); +extern void mark_divelist_changed(int); +extern int unsaved_changes(void); +extern void remove_autogen_trips(void); +extern double init_decompression(struct dive *dive); + +/* divelist core logic functions */ +extern void process_dives(bool imported, bool prefer_imported); +extern char *get_dive_gas_string(struct dive *dive); + +extern dive_trip_t *find_trip_by_idx(int idx); + +struct dive **grow_dive_table(struct dive_table *table); +extern int trip_has_selected_dives(dive_trip_t *trip); +extern void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p); +extern int get_divenr(struct dive *dive); +extern int get_divesite_idx(struct dive_site *ds); +extern dive_trip_t *find_matching_trip(timestamp_t when); +extern void remove_dive_from_trip(struct dive *dive, short was_autogen); +extern dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive); +extern void autogroup_dives(void); +extern struct dive *merge_two_dives(struct dive *a, struct dive *b); +extern bool consecutive_selected(); +extern void select_dive(int idx); +extern void deselect_dive(int idx); +extern void select_dives_in_trip(struct dive_trip *trip); +extern void deselect_dives_in_trip(struct dive_trip *trip); +extern void filter_dive(struct dive *d, bool shown); +extern void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b); +extern void find_new_trip_start_time(dive_trip_t *trip); +extern struct dive *first_selected_dive(); +extern struct dive *last_selected_dive(); +extern bool is_trip_before_after(struct dive *dive, bool before); +extern void set_dive_nr_for_current_dive(); + +int get_min_datafile_version(); +void reset_min_datafile_version(); +void report_datafile_version(int version); +void clear_dive_file_data(); + +#ifdef DEBUG_TRIP +extern void dump_selection(void); +extern void dump_trip_list(void); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // DIVELIST_H diff --git a/core/divelogexportlogic.cpp b/core/divelogexportlogic.cpp new file mode 100644 index 000000000..af5157f4a --- /dev/null +++ b/core/divelogexportlogic.cpp @@ -0,0 +1,161 @@ +#include <QString> +#include <QFile> +#include <QFileInfo> +#include <QDir> +#include <QSettings> +#include <QTextStream> +#include "divelogexportlogic.h" +#include "helpers.h" +#include "units.h" +#include "statistics.h" +#include "save-html.h" + +void file_copy_and_overwrite(const QString &fileName, const QString &newName) +{ + QFile file(newName); + if (file.exists()) + file.remove(); + QFile::copy(fileName, newName); +} + +void exportHTMLsettings(const QString &filename, struct htmlExportSetting &hes) +{ + QString fontSize = hes.fontSize; + QString fontFamily = hes.fontFamily; + QFile file(filename); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + out << "settings = {\"fontSize\":\"" << fontSize << "\",\"fontFamily\":\"" << fontFamily << "\",\"listOnly\":\"" + << hes.listOnly << "\",\"subsurfaceNumbers\":\"" << hes.subsurfaceNumbers << "\","; + //save units preferences + if (prefs.unit_system == METRIC) { + out << "\"unit_system\":\"Meteric\""; + } else if (prefs.unit_system == IMPERIAL) { + out << "\"unit_system\":\"Imperial\""; + } else { + QVariant v; + QString length, pressure, volume, temperature, weight; + length = prefs.units.length == units::METERS ? "METER" : "FEET"; + pressure = prefs.units.pressure == units::BAR ? "BAR" : "PSI"; + volume = prefs.units.volume == units::LITER ? "LITER" : "CUFT"; + temperature = prefs.units.temperature == units::CELSIUS ? "CELSIUS" : "FAHRENHEIT"; + weight = prefs.units.weight == units::KG ? "KG" : "LBS"; + out << "\"unit_system\":\"Personalize\","; + out << "\"units\":{\"depth\":\"" << length << "\",\"pressure\":\"" << pressure << "\",\"volume\":\"" << volume << "\",\"temperature\":\"" << temperature << "\",\"weight\":\"" << weight << "\"}"; + } + out << "}"; + file.close(); +} + +static void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats) +{ + out << "{"; + out << "\"YEAR\":\"Total\","; + out << "\"DIVES\":\"" << total_stats->selection_size << "\","; + out << "\"TOTAL_TIME\":\"" << get_time_string(total_stats->total_time.seconds, 0) << "\","; + out << "\"AVERAGE_TIME\":\"--\","; + out << "\"SHORTEST_TIME\":\"--\","; + out << "\"LONGEST_TIME\":\"--\","; + out << "\"AVG_DEPTH\":\"--\","; + out << "\"MIN_DEPTH\":\"--\","; + out << "\"MAX_DEPTH\":\"--\","; + out << "\"AVG_SAC\":\"--\","; + out << "\"MIN_SAC\":\"--\","; + out << "\"MAX_SAC\":\"--\","; + out << "\"AVG_TEMP\":\"--\","; + out << "\"MIN_TEMP\":\"--\","; + out << "\"MAX_TEMP\":\"--\","; + out << "},"; +} + + +static void exportHTMLstatistics(const QString filename, struct htmlExportSetting &hes) +{ + QFile file(filename); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + + stats_t total_stats; + + total_stats.selection_size = 0; + total_stats.total_time.seconds = 0; + + int i = 0; + out << "divestat=["; + if (hes.yearlyStatistics) { + while (stats_yearly != NULL && stats_yearly[i].period) { + out << "{"; + out << "\"YEAR\":\"" << stats_yearly[i].period << "\","; + out << "\"DIVES\":\"" << stats_yearly[i].selection_size << "\","; + out << "\"TOTAL_TIME\":\"" << get_time_string(stats_yearly[i].total_time.seconds, 0) << "\","; + out << "\"AVERAGE_TIME\":\"" << get_minutes(stats_yearly[i].total_time.seconds / stats_yearly[i].selection_size) << "\","; + out << "\"SHORTEST_TIME\":\"" << get_minutes(stats_yearly[i].shortest_time.seconds) << "\","; + out << "\"LONGEST_TIME\":\"" << get_minutes(stats_yearly[i].longest_time.seconds) << "\","; + out << "\"AVG_DEPTH\":\"" << get_depth_string(stats_yearly[i].avg_depth) << "\","; + out << "\"MIN_DEPTH\":\"" << get_depth_string(stats_yearly[i].min_depth) << "\","; + out << "\"MAX_DEPTH\":\"" << get_depth_string(stats_yearly[i].max_depth) << "\","; + out << "\"AVG_SAC\":\"" << get_volume_string(stats_yearly[i].avg_sac) << "\","; + out << "\"MIN_SAC\":\"" << get_volume_string(stats_yearly[i].min_sac) << "\","; + out << "\"MAX_SAC\":\"" << get_volume_string(stats_yearly[i].max_sac) << "\","; + if ( stats_yearly[i].combined_count ) + out << "\"AVG_TEMP\":\"" << QString::number(stats_yearly[i].combined_temp / stats_yearly[i].combined_count, 'f', 1) << "\","; + else + out << "\"AVG_TEMP\":\"0.0\","; + out << "\"MIN_TEMP\":\"" << ( stats_yearly[i].min_temp == 0 ? 0 : get_temp_units(stats_yearly[i].min_temp, NULL)) << "\","; + out << "\"MAX_TEMP\":\"" << ( stats_yearly[i].max_temp == 0 ? 0 : get_temp_units(stats_yearly[i].max_temp, NULL)) << "\","; + out << "},"; + total_stats.selection_size += stats_yearly[i].selection_size; + total_stats.total_time.seconds += stats_yearly[i].total_time.seconds; + i++; + } + exportHTMLstatisticsTotal(out, &total_stats); + } + out << "]"; + file.close(); + +} + +void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes) +{ + QString photosDirectory; + QFile file(filename); + QFileInfo info(file); + QDir mainDir = info.absoluteDir(); + mainDir.mkdir(file.fileName() + "_files"); + QString exportFiles = file.fileName() + "_files"; + + QString json_dive_data = exportFiles + QDir::separator() + "file.js"; + QString json_settings = exportFiles + QDir::separator() + "settings.js"; + QString translation = exportFiles + QDir::separator() + "translation.js"; + QString stat_file = exportFiles + QDir::separator() + "stat.js"; + exportFiles += "/"; + + if (hes.exportPhotos) { + photosDirectory = exportFiles + QDir::separator() + "photos" + QDir::separator(); + mainDir.mkdir(photosDirectory); + } + + + exportHTMLsettings(json_settings, hes); + exportHTMLstatistics(stat_file, hes); + export_translation(translation.toUtf8().data()); + + export_HTML(qPrintable(json_dive_data), qPrintable(photosDirectory), hes.selectedOnly, hes.listOnly); + + QString searchPath = getSubsurfaceDataPath("theme"); + if (searchPath.isEmpty()) + return; + + searchPath += QDir::separator(); + + file_copy_and_overwrite(searchPath + "dive_export.html", filename); + file_copy_and_overwrite(searchPath + "list_lib.js", exportFiles + "list_lib.js"); + file_copy_and_overwrite(searchPath + "poster.png", exportFiles + "poster.png"); + file_copy_and_overwrite(searchPath + "jqplot.highlighter.min.js", exportFiles + "jqplot.highlighter.min.js"); + file_copy_and_overwrite(searchPath + "jquery.jqplot.min.js", exportFiles + "jquery.jqplot.min.js"); + file_copy_and_overwrite(searchPath + "jqplot.canvasAxisTickRenderer.min.js", exportFiles + "jqplot.canvasAxisTickRenderer.min.js"); + file_copy_and_overwrite(searchPath + "jqplot.canvasTextRenderer.min.js", exportFiles + "jqplot.canvasTextRenderer.min.js"); + file_copy_and_overwrite(searchPath + "jquery.min.js", exportFiles + "jquery.min.js"); + file_copy_and_overwrite(searchPath + "jquery.jqplot.css", exportFiles + "jquery.jqplot.css"); + file_copy_and_overwrite(searchPath + hes.themeFile, exportFiles + "theme.css"); +} diff --git a/core/divelogexportlogic.h b/core/divelogexportlogic.h new file mode 100644 index 000000000..84f09c362 --- /dev/null +++ b/core/divelogexportlogic.h @@ -0,0 +1,20 @@ +#ifndef DIVELOGEXPORTLOGIC_H +#define DIVELOGEXPORTLOGIC_H + +struct htmlExportSetting { + bool exportPhotos; + bool selectedOnly; + bool listOnly; + QString fontFamily; + QString fontSize; + int themeSelection; + bool subsurfaceNumbers; + bool yearlyStatistics; + QString themeFile; +}; + +void file_copy_and_overwrite(const QString &fileName, const QString &newName); +void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes); + +#endif // DIVELOGEXPORTLOGIC_H + diff --git a/core/divesite.c b/core/divesite.c new file mode 100644 index 000000000..e9eed2a07 --- /dev/null +++ b/core/divesite.c @@ -0,0 +1,337 @@ +/* divesite.c */ +#include "divesite.h" +#include "dive.h" +#include "divelist.h" + +#include <math.h> + +struct dive_site_table dive_site_table; + +/* there could be multiple sites of the same name - return the first one */ +uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (same_string(ds->name, name)) { + if (dsp) + *dsp = ds; + return ds->uuid; + } + } + return 0; +} + +/* there could be multiple sites at the same GPS fix - return the first one */ +uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg) { + if (dsp) + *dsp = ds; + return ds->uuid; + } + } + return 0; +} + + +/* to avoid a bug where we have two dive sites with different name and the same GPS coordinates + * and first get the gps coordinates (reading a V2 file) and happen to get back "the other" name, + * this function allows us to verify if a very specific name/GPS combination already exists */ +uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg && same_string(ds->name, name)) + return ds->uuid; + } + return 0; +} + +// Calculate the distance in meters between two coordinates. +unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2) +{ + double lat2_r = udeg_to_radians(lat2.udeg); + double lat_d_r = udeg_to_radians(lat2.udeg-lat1.udeg); + double lon_d_r = udeg_to_radians(lon2.udeg-lon1.udeg); + + double a = sin(lat_d_r/2) * sin(lat_d_r/2) + + cos(lat2_r) * cos(lat2_r) * sin(lon_d_r/2) * sin(lon_d_r/2); + double c = 2 * atan2(sqrt(a), sqrt(1.0 - a)); + + // Earth radious in metres + return 6371000 * c; +} + +/* find the closest one, no more than distance meters away - if more than one at same distance, pick the first */ +uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp) +{ + int i; + int uuid = 0; + struct dive_site *ds; + unsigned int cur_distance, min_distance = distance; + for_each_dive_site (i, ds) { + if (dive_site_has_gps_location(ds) && + (cur_distance = get_distance(ds->latitude, ds->longitude, latitude, longitude)) < min_distance) { + min_distance = cur_distance; + uuid = ds->uuid; + if (dsp) + *dsp = ds; + } + } + return uuid; +} + +/* try to create a uniqe ID - fingers crossed */ +static uint32_t dive_site_getUniqId() +{ + uint32_t id = 0; + + while (id == 0 || get_dive_site_by_uuid(id)) { + id = rand() & 0xff; + id |= (rand() & 0xff) << 8; + id |= (rand() & 0xff) << 16; + id |= (rand() & 0xff) << 24; + } + + return id; +} + +/* we never allow a second dive site with the same uuid */ +struct dive_site *alloc_or_get_dive_site(uint32_t uuid) +{ + struct dive_site *ds; + if (uuid) { + if ((ds = get_dive_site_by_uuid(uuid)) != NULL) { + fprintf(stderr, "PROBLEM: refusing to create dive site with the same uuid %08x\n", uuid); + return ds; + } + } + int nr = dive_site_table.nr; + int allocated = dive_site_table.allocated; + struct dive_site **sites = dive_site_table.dive_sites; + + if (nr >= allocated) { + allocated = (nr + 32) * 3 / 2; + sites = realloc(sites, allocated * sizeof(struct dive_site *)); + if (!sites) + exit(1); + dive_site_table.dive_sites = sites; + dive_site_table.allocated = allocated; + } + ds = calloc(1, sizeof(*ds)); + if (!ds) + exit(1); + sites[nr] = ds; + dive_site_table.nr = nr + 1; + // we should always be called with a valid uuid except in the special + // case where we want to copy a dive site into the memory we allocated + // here - then we need to pass in 0 and create a temporary uuid here + // (just so things are always consistent) + if (uuid) + ds->uuid = uuid; + else + ds->uuid = dive_site_getUniqId(); + return ds; +} + +int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only) +{ + int j; + int nr = 0; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { + nr++; + } + } + return nr; +} + +bool is_dive_site_used(uint32_t uuid, bool select_only) +{ + int j; + bool found = false; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { + found = true; + break; + } + } + return found; +} + +void delete_dive_site(uint32_t id) +{ + int nr = dive_site_table.nr; + for (int i = 0; i < nr; i++) { + struct dive_site *ds = get_dive_site(i); + if (ds->uuid == id) { + free(ds->name); + free(ds->notes); + free(ds); + if (nr - 1 > i) + memmove(&dive_site_table.dive_sites[i], + &dive_site_table.dive_sites[i+1], + (nr - 1 - i) * sizeof(dive_site_table.dive_sites[0])); + dive_site_table.nr = nr - 1; + break; + } + } +} + +uint32_t create_divesite_uuid(const char *name, timestamp_t divetime) +{ + if (name == NULL) + name =""; + unsigned char hash[20]; + SHA_CTX ctx; + SHA1_Init(&ctx); + SHA1_Update(&ctx, &divetime, sizeof(timestamp_t)); + SHA1_Update(&ctx, name, strlen(name)); + SHA1_Final(hash, &ctx); + // now return the first 32 of the 160 bit hash + return *(uint32_t *)hash; +} + +/* allocate a new site and add it to the table */ +uint32_t create_dive_site(const char *name, timestamp_t divetime) +{ + uint32_t uuid = create_divesite_uuid(name, divetime); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + ds->name = copy_string(name); + + return uuid; +} + +/* same as before, but with current time if no current_dive is present */ +uint32_t create_dive_site_from_current_dive(const char *name) +{ + if (current_dive != NULL) { + return create_dive_site(name, current_dive->when); + } else { + timestamp_t when; + time_t now = time(0); + when = utc_mktime(localtime(&now)); + return create_dive_site(name, when); + } +} + +/* same as before, but with GPS data */ +uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime) +{ + uint32_t uuid = create_divesite_uuid(name, divetime); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + ds->name = copy_string(name); + ds->latitude = latitude; + ds->longitude = longitude; + + return ds->uuid; +} + +/* a uuid is always present - but if all the other fields are empty, the dive site is pointless */ +bool dive_site_is_empty(struct dive_site *ds) +{ + return same_string(ds->name, "") && + same_string(ds->description, "") && + same_string(ds->notes, "") && + ds->latitude.udeg == 0 && + ds->longitude.udeg == 0; +} + +void copy_dive_site(struct dive_site *orig, struct dive_site *copy) +{ + free(copy->name); + free(copy->notes); + free(copy->description); + + copy->latitude = orig->latitude; + copy->longitude = orig->longitude; + copy->name = copy_string(orig->name); + copy->notes = copy_string(orig->notes); + copy->description = copy_string(orig->description); + copy->uuid = orig->uuid; + if (orig->taxonomy.category == NULL) { + free_taxonomy(©->taxonomy); + } else { + if (copy->taxonomy.category == NULL) + copy->taxonomy.category = alloc_taxonomy(); + for (int i = 0; i < TC_NR_CATEGORIES; i++) { + if (i < copy->taxonomy.nr) + free((void *)copy->taxonomy.category[i].value); + if (i < orig->taxonomy.nr) { + copy->taxonomy.category[i] = orig->taxonomy.category[i]; + copy->taxonomy.category[i].value = copy_string(orig->taxonomy.category[i].value); + } + } + copy->taxonomy.nr = orig->taxonomy.nr; + } +} + +void clear_dive_site(struct dive_site *ds) +{ + free(ds->name); + free(ds->notes); + free(ds->description); + ds->name = 0; + ds->notes = 0; + ds->description = 0; + ds->latitude.udeg = 0; + ds->longitude.udeg = 0; + ds->uuid = 0; + ds->taxonomy.nr = 0; + free_taxonomy(&ds->taxonomy); +} + +void merge_dive_sites(uint32_t ref, uint32_t* uuids, int count) +{ + int curr_dive, i; + struct dive *d; + for(i = 0; i < count; i++){ + if (uuids[i] == ref) + continue; + + for_each_dive(curr_dive, d) { + if (d->dive_site_uuid != uuids[i] ) + continue; + d->dive_site_uuid = ref; + } + } + + for(i = 0; i < count; i++) { + if (uuids[i] == ref) + continue; + delete_dive_site(uuids[i]); + } + mark_divelist_changed(true); +} + +uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime) +{ + int i; + struct dive_site *ds; + for_each_dive_site(i,ds) { + if (same_string(name, ds->name)) + break; + } + if (ds) + return ds->uuid; + return create_dive_site(name, divetime); +} + +static int compare_sites(const void *_a, const void *_b) +{ + const struct dive_site *a = (const struct dive_site *)*(void **)_a; + const struct dive_site *b = (const struct dive_site *)*(void **)_b; + return a->uuid > b->uuid ? 1 : a->uuid == b->uuid ? 0 : -1; +} + +void dive_site_table_sort() +{ + qsort(dive_site_table.dive_sites, dive_site_table.nr, sizeof(struct dive_site *), compare_sites); +} diff --git a/core/divesite.cpp b/core/divesite.cpp new file mode 100644 index 000000000..ae102a14b --- /dev/null +++ b/core/divesite.cpp @@ -0,0 +1,31 @@ +#include "divesite.h" +#include "pref.h" + +QString constructLocationTags(uint32_t ds_uuid) +{ + QString locationTag; + struct dive_site *ds = get_dive_site_by_uuid(ds_uuid); + + if (!ds || !ds->taxonomy.nr) + return locationTag; + + locationTag = "<small><small>(tags: "; + QString connector; + for (int i = 0; i < 3; i++) { + if (prefs.geocoding.category[i] == TC_NONE) + continue; + for (int j = 0; j < TC_NR_CATEGORIES; j++) { + if (ds->taxonomy.category[j].category == prefs.geocoding.category[i]) { + QString tag = ds->taxonomy.category[j].value; + if (!tag.isEmpty()) { + locationTag += connector + tag; + connector = " / "; + } + break; + } + } + } + + locationTag += ")</small></small>"; + return locationTag; +} diff --git a/core/divesite.h b/core/divesite.h new file mode 100644 index 000000000..f18b2e8e8 --- /dev/null +++ b/core/divesite.h @@ -0,0 +1,80 @@ +#ifndef DIVESITE_H +#define DIVESITE_H + +#include "units.h" +#include "taxonomy.h" +#include <stdlib.h> + +#ifdef __cplusplus +#include <QString> +extern "C" { +#else +#include <stdbool.h> +#endif + +struct dive_site +{ + uint32_t uuid; + char *name; + degrees_t latitude, longitude; + char *description; + char *notes; + struct taxonomy_data taxonomy; +}; + +struct dive_site_table { + int nr, allocated; + struct dive_site **dive_sites; +}; + +extern struct dive_site_table dive_site_table; + +static inline struct dive_site *get_dive_site(int nr) +{ + if (nr >= dive_site_table.nr || nr < 0) + return NULL; + return dive_site_table.dive_sites[nr]; +} + +/* iterate over each dive site */ +#define for_each_dive_site(_i, _x) \ + for ((_i) = 0; ((_x) = get_dive_site(_i)) != NULL; (_i)++) + +static inline struct dive_site *get_dive_site_by_uuid(uint32_t uuid) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) + if (ds->uuid == uuid) + return get_dive_site(i); + return NULL; +} + +void dive_site_table_sort(); +struct dive_site *alloc_or_get_dive_site(uint32_t uuid); +int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only); +bool is_dive_site_used(uint32_t uuid, bool select_only); +void delete_dive_site(uint32_t id); +uint32_t create_dive_site(const char *name, timestamp_t divetime); +uint32_t create_dive_site_from_current_dive(const char *name); +uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime); +uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp); +uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp); +uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude); +uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp); +bool dive_site_is_empty(struct dive_site *ds); +void copy_dive_site(struct dive_site *orig, struct dive_site *copy); +void clear_dive_site(struct dive_site *ds); +unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2); +uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime); +void merge_dive_sites(uint32_t ref, uint32_t *uuids, int count); + +#define INVALID_DIVE_SITE_NAME "development use only - not a valid dive site name" + +#ifdef __cplusplus +} +QString constructLocationTags(uint32_t ds_uuid); + +#endif + +#endif // DIVESITE_H diff --git a/core/divesitehelpers.cpp b/core/divesitehelpers.cpp new file mode 100644 index 000000000..3542f96fa --- /dev/null +++ b/core/divesitehelpers.cpp @@ -0,0 +1,208 @@ +// +// infrastructure to deal with dive sites +// + +#include "divesitehelpers.h" + +#include "divesite.h" +#include "helpers.h" +#include "membuffer.h" +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QNetworkAccessManager> +#include <QUrlQuery> +#include <QEventLoop> +#include <QTimer> + +struct GeoLookupInfo { + degrees_t lat; + degrees_t lon; + uint32_t uuid; +}; + +QVector<GeoLookupInfo> geo_lookup_data; + +ReverseGeoLookupThread* ReverseGeoLookupThread::instance() { + static ReverseGeoLookupThread* self = new ReverseGeoLookupThread(); + return self; +} + +ReverseGeoLookupThread::ReverseGeoLookupThread(QObject *obj) : QThread(obj) +{ +} + +void ReverseGeoLookupThread::run() { + if (geo_lookup_data.isEmpty()) + return; + + QNetworkRequest request; + QNetworkAccessManager *rgl = new QNetworkAccessManager(); + QEventLoop loop; + QString mapquestURL("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3"); + QString geonamesURL("http://api.geonames.org/findNearbyPlaceNameJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); + QString geonamesOceanURL("http://api.geonames.org/oceanJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); + QString divelogsURL("https://www.divelogs.de/mapsearch_divespotnames.php?lat=%1&lng=%2&radius=50"); + QTimer timer; + + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + + Q_FOREACH (const GeoLookupInfo& info, geo_lookup_data ) { + struct dive_site *ds = info.uuid ? get_dive_site_by_uuid(info.uuid) : &displayed_dive_site; + + // first check the findNearbyPlaces API from geonames - that should give us country, state, city + request.setUrl(geonamesURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); + + QNetworkReply *reply = rgl->get(request); + timer.setSingleShot(true); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(5000); // 5 secs. timeout + loop.exec(); + + if(timer.isActive()) { + timer.stop(); + if(reply->error() > 0) { + report_error("got error accessing geonames.org: %s", qPrintable(reply->errorString())); + goto clear_reply; + } + int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (v < 200 || v >= 300) + goto clear_reply; + QByteArray fullReply = reply->readAll(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); + goto clear_reply; + } + QJsonObject obj = jsonDoc.object(); + QVariant geoNamesObject = obj.value("geonames").toVariant(); + QVariantList geoNames = geoNamesObject.toList(); + if (geoNames.count() > 0) { + QVariantMap firstData = geoNames.at(0).toMap(); + int ri = 0, l3 = -1, lt = -1; + if (ds->taxonomy.category == NULL) { + ds->taxonomy.category = alloc_taxonomy(); + } else { + // clear out the data (except for the ocean data) + int ocean; + if ((ocean = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN)) > 0) { + ds->taxonomy.category[0] = ds->taxonomy.category[ocean]; + ds->taxonomy.nr = 1; + } else { + // ocean is -1 if there is no such entry, and we didn't copy above + // if ocean is 0, so the following gets us the correct count + ds->taxonomy.nr = ocean + 1; + } + } + // get all the data - OCEAN is special, so start at COUNTRY + for (int j = TC_COUNTRY; j < TC_NR_CATEGORIES; j++) { + if (firstData[taxonomy_api_names[j]].isValid()) { + ds->taxonomy.category[ri].category = j; + ds->taxonomy.category[ri].origin = taxonomy::GEOCODED; + free((void*)ds->taxonomy.category[ri].value); + ds->taxonomy.category[ri].value = copy_string(qPrintable(firstData[taxonomy_api_names[j]].toString())); + ri++; + } + } + ds->taxonomy.nr = ri; + l3 = taxonomy_index_for_category(&ds->taxonomy, TC_ADMIN_L3); + lt = taxonomy_index_for_category(&ds->taxonomy, TC_LOCALNAME); + if (l3 == -1 && lt != -1) { + // basically this means we did get a local name (what we call town), but just like most places + // we didn't get an adminName_3 - which in some regions is the actual city that town belongs to, + // then we copy the town into the city + ds->taxonomy.category[ri].value = copy_string(ds->taxonomy.category[lt].value); + ds->taxonomy.category[ri].origin = taxonomy::COPIED; + ds->taxonomy.category[ri].category = TC_ADMIN_L3; + ds->taxonomy.nr++; + } + mark_divelist_changed(true); + } else { + report_error("geonames.org did not provide reverse lookup information"); + qDebug() << "no reverse geo lookup; geonames returned\n" << fullReply; + } + } else { + report_error("timeout accessing geonames.org"); + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + // next check the oceans API to figure out the body of water + request.setUrl(geonamesOceanURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); + reply = rgl->get(request); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(5000); // 5 secs. timeout + loop.exec(); + if(timer.isActive()) { + timer.stop(); + if(reply->error() > 0) { + report_error("got error accessing oceans API of geonames.org: %s", qPrintable(reply->errorString())); + goto clear_reply; + } + int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (v < 200 || v >= 300) + goto clear_reply; + QByteArray fullReply = reply->readAll(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); + goto clear_reply; + } + QJsonObject obj = jsonDoc.object(); + QVariant oceanObject = obj.value("ocean").toVariant(); + QVariantMap oceanName = oceanObject.toMap(); + if (oceanName["name"].isValid()) { + int idx; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + idx = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN); + if (idx == -1) + idx = ds->taxonomy.nr; + if (idx < TC_NR_CATEGORIES) { + ds->taxonomy.category[idx].category = TC_OCEAN; + ds->taxonomy.category[idx].origin = taxonomy::GEOCODED; + ds->taxonomy.category[idx].value = copy_string(qPrintable(oceanName["name"].toString())); + if (idx == ds->taxonomy.nr) + ds->taxonomy.nr++; + } + mark_divelist_changed(true); + } + } else { + report_error("timeout accessing geonames.org"); + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + +clear_reply: + reply->deleteLater(); + } + rgl->deleteLater(); +} + +void ReverseGeoLookupThread::lookup(dive_site *ds) +{ + if (!ds) + return; + GeoLookupInfo info; + info.lat = ds->latitude; + info.lon = ds->longitude; + info.uuid = ds->uuid; + + geo_lookup_data.clear(); + geo_lookup_data.append(info); + run(); +} + +extern "C" void add_geo_information_for_lookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) { + GeoLookupInfo info; + info.lat = latitude; + info.lon = longitude; + info.uuid = uuid; + + geo_lookup_data.append(info); +} diff --git a/core/divesitehelpers.h b/core/divesitehelpers.h new file mode 100644 index 000000000..a08069bc0 --- /dev/null +++ b/core/divesitehelpers.h @@ -0,0 +1,18 @@ +#ifndef DIVESITEHELPERS_H +#define DIVESITEHELPERS_H + +#include "units.h" +#include <QThread> + +class ReverseGeoLookupThread : public QThread { +Q_OBJECT +public: + static ReverseGeoLookupThread *instance(); + void lookup(struct dive_site *ds); + void run() Q_DECL_OVERRIDE; + +private: + ReverseGeoLookupThread(QObject *parent = 0); +}; + +#endif // DIVESITEHELPERS_H diff --git a/core/equipment.c b/core/equipment.c new file mode 100644 index 000000000..9f3e49039 --- /dev/null +++ b/core/equipment.c @@ -0,0 +1,238 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +/* equipment.c */ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdarg.h> +#include <time.h> +#include "gettext.h" +#include "dive.h" +#include "display.h" +#include "divelist.h" + +/* placeholders for a few functions that we need to redesign for the Qt UI */ +void add_cylinder_description(cylinder_type_t *type) +{ + const char *desc; + int i; + + desc = type->description; + if (!desc) + return; + for (i = 0; i < 100 && tank_info[i].name != NULL; i++) { + if (strcmp(tank_info[i].name, desc) == 0) + return; + } + if (i < 100) { + // FIXME: leaked on exit + tank_info[i].name = strdup(desc); + tank_info[i].ml = type->size.mliter; + tank_info[i].bar = type->workingpressure.mbar / 1000; + } +} +void add_weightsystem_description(weightsystem_t *weightsystem) +{ + const char *desc; + int i; + + desc = weightsystem->description; + if (!desc) + return; + for (i = 0; i < 100 && ws_info[i].name != NULL; i++) { + if (strcmp(ws_info[i].name, desc) == 0) { + ws_info[i].grams = weightsystem->weight.grams; + return; + } + } + if (i < 100) { + // FIXME: leaked on exit + ws_info[i].name = strdup(desc); + ws_info[i].grams = weightsystem->weight.grams; + } +} + +bool cylinder_nodata(cylinder_t *cyl) +{ + return !cyl->type.size.mliter && + !cyl->type.workingpressure.mbar && + !cyl->type.description && + !cyl->gasmix.o2.permille && + !cyl->gasmix.he.permille && + !cyl->start.mbar && + !cyl->end.mbar && + !cyl->gas_used.mliter && + !cyl->deco_gas_used.mliter; +} + +static bool cylinder_nosamples(cylinder_t *cyl) +{ + return !cyl->sample_start.mbar && + !cyl->sample_end.mbar; +} + +bool cylinder_none(void *_data) +{ + cylinder_t *cyl = _data; + return cylinder_nodata(cyl) && cylinder_nosamples(cyl); +} + +void get_gas_string(const struct gasmix *gasmix, char *text, int len) +{ + if (gasmix_is_air(gasmix)) + snprintf(text, len, "%s", translate("gettextFromC", "air")); + else if (get_he(gasmix) == 0 && get_o2(gasmix) < 1000) + snprintf(text, len, translate("gettextFromC", "EAN%d"), (get_o2(gasmix) + 5) / 10); + else if (get_he(gasmix) == 0 && get_o2(gasmix) == 1000) + snprintf(text, len, "%s", translate("gettextFromC", "oxygen")); + else + snprintf(text, len, "(%d/%d)", (get_o2(gasmix) + 5) / 10, (get_he(gasmix) + 5) / 10); +} + +/* Returns a static char buffer - only good for immediate use by printf etc */ +const char *gasname(const struct gasmix *gasmix) +{ + static char gas[64]; + get_gas_string(gasmix, gas, sizeof(gas)); + return gas; +} + +bool weightsystem_none(void *_data) +{ + weightsystem_t *ws = _data; + return !ws->weight.grams && !ws->description; +} + +bool no_weightsystems(weightsystem_t *ws) +{ + int i; + + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + if (!weightsystem_none(ws + i)) + return false; + return true; +} + +static bool one_weightsystem_equal(weightsystem_t *ws1, weightsystem_t *ws2) +{ + return ws1->weight.grams == ws2->weight.grams && + same_string(ws1->description, ws2->description); +} + +bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2) +{ + int i; + + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + if (!one_weightsystem_equal(ws1 + i, ws2 + i)) + return false; + return true; +} + +/* + * We hardcode the most common standard cylinders, + * we should pick up any other names from the dive + * logs directly. + */ +struct tank_info_t tank_info[100] = { + /* Need an empty entry for the no-cylinder case */ + { "", }, + + /* Size-only metric cylinders */ + { "10.0â„“", .ml = 10000 }, + { "11.1â„“", .ml = 11100 }, + + /* Most common AL cylinders */ + { "AL40", .cuft = 40, .psi = 3000 }, + { "AL50", .cuft = 50, .psi = 3000 }, + { "AL63", .cuft = 63, .psi = 3000 }, + { "AL72", .cuft = 72, .psi = 3000 }, + { "AL80", .cuft = 80, .psi = 3000 }, + { "AL100", .cuft = 100, .psi = 3300 }, + + /* Metric AL cylinders */ + { "ALU7", .ml = 7000, .bar = 200 }, + + /* Somewhat common LP steel cylinders */ + { "LP85", .cuft = 85, .psi = 2640 }, + { "LP95", .cuft = 95, .psi = 2640 }, + { "LP108", .cuft = 108, .psi = 2640 }, + { "LP121", .cuft = 121, .psi = 2640 }, + + /* Somewhat common HP steel cylinders */ + { "HP65", .cuft = 65, .psi = 3442 }, + { "HP80", .cuft = 80, .psi = 3442 }, + { "HP100", .cuft = 100, .psi = 3442 }, + { "HP119", .cuft = 119, .psi = 3442 }, + { "HP130", .cuft = 130, .psi = 3442 }, + + /* Common European steel cylinders */ + { "3â„“ 232 bar", .ml = 3000, .bar = 232 }, + { "3â„“ 300 bar", .ml = 3000, .bar = 300 }, + { "10â„“ 300 bar", .ml = 10000, .bar = 300 }, + { "12â„“ 200 bar", .ml = 12000, .bar = 200 }, + { "12â„“ 232 bar", .ml = 12000, .bar = 232 }, + { "12â„“ 300 bar", .ml = 12000, .bar = 300 }, + { "15â„“ 200 bar", .ml = 15000, .bar = 200 }, + { "15â„“ 232 bar", .ml = 15000, .bar = 232 }, + { "D7 300 bar", .ml = 14000, .bar = 300 }, + { "D8.5 232 bar", .ml = 17000, .bar = 232 }, + { "D12 232 bar", .ml = 24000, .bar = 232 }, + { "D13 232 bar", .ml = 26000, .bar = 232 }, + { "D15 232 bar", .ml = 30000, .bar = 232 }, + { "D16 232 bar", .ml = 32000, .bar = 232 }, + { "D18 232 bar", .ml = 36000, .bar = 232 }, + { "D20 232 bar", .ml = 40000, .bar = 232 }, + + /* We'll fill in more from the dive log dynamically */ + { NULL, } +}; + +/* + * We hardcode the most common weight system types + * This is a bit odd as the weight system types don't usually encode weight + */ +struct ws_info_t ws_info[100] = { + { QT_TRANSLATE_NOOP("gettextFromC", "integrated"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "belt"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "ankle"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "backplate weight"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "clip-on"), 0 }, +}; + +void remove_cylinder(struct dive *dive, int idx) +{ + cylinder_t *cyl = dive->cylinder + idx; + int nr = MAX_CYLINDERS - idx - 1; + memmove(cyl, cyl + 1, nr * sizeof(*cyl)); + memset(cyl + nr, 0, sizeof(*cyl)); +} + +void remove_weightsystem(struct dive *dive, int idx) +{ + weightsystem_t *ws = dive->weightsystem + idx; + int nr = MAX_WEIGHTSYSTEMS - idx - 1; + memmove(ws, ws + 1, nr * sizeof(*ws)); + memset(ws + nr, 0, sizeof(*ws)); +} + +/* when planning a dive we need to make sure that all cylinders have a sane depth assigned + * and if we are tracking gas consumption the pressures need to be reset to start = end = workingpressure */ +void reset_cylinders(struct dive *dive, bool track_gas) +{ + int i; + pressure_t decopo2 = {.mbar = prefs.decopo2}; + + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = &dive->cylinder[i]; + if (cylinder_none(cyl)) + continue; + if (cyl->depth.mm == 0) /* if the gas doesn't give a mod, calculate based on prefs */ + cyl->depth = gas_mod(&cyl->gasmix, decopo2, dive, M_OR_FT(3,10)); + if (track_gas) + cyl->start.mbar = cyl->end.mbar = cyl->type.workingpressure.mbar; + cyl->gas_used.mliter = 0; + cyl->deco_gas_used.mliter = 0; + } +} diff --git a/core/exif.cpp b/core/exif.cpp new file mode 100644 index 000000000..0b1cda2bc --- /dev/null +++ b/core/exif.cpp @@ -0,0 +1,587 @@ +#include <stdio.h> +/************************************************************************** + exif.cpp -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved (BSD License). + + See exif.h for version history. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include <algorithm> +#include "dive.h" +#include "exif.h" + +using std::string; + +namespace { + // IF Entry + struct IFEntry { + // Raw fields + unsigned short tag; + unsigned short format; + unsigned data; + unsigned length; + + // Parsed fields + string val_string; + unsigned short val_16; + unsigned val_32; + double val_rational; + unsigned char val_byte; + }; + + // Helper functions + unsigned int parse32(const unsigned char *buf, bool intel) + { + if (intel) + return ((unsigned)buf[3] << 24) | + ((unsigned)buf[2] << 16) | + ((unsigned)buf[1] << 8) | + buf[0]; + + return ((unsigned)buf[0] << 24) | + ((unsigned)buf[1] << 16) | + ((unsigned)buf[2] << 8) | + buf[3]; + } + + unsigned short parse16(const unsigned char *buf, bool intel) + { + if (intel) + return ((unsigned)buf[1] << 8) | buf[0]; + return ((unsigned)buf[0] << 8) | buf[1]; + } + + string parseEXIFString(const unsigned char *buf, + const unsigned num_components, + const unsigned data, + const unsigned base, + const unsigned len) + { + string value; + if (num_components <= 4) + value.assign((const char *)&data, num_components); + else { + if (base + data + num_components <= len) + value.assign((const char *)(buf + base + data), num_components); + } + return value; + } + + double parseEXIFRational(const unsigned char *buf, bool intel) + { + double numerator = 0; + double denominator = 1; + + numerator = (double)parse32(buf, intel); + denominator = (double)parse32(buf + 4, intel); + if (denominator < 1e-20) + return 0; + return numerator / denominator; + } + + IFEntry parseIFEntry(const unsigned char *buf, + const unsigned offs, + const bool alignIntel, + const unsigned base, + const unsigned len) + { + IFEntry result; + + // Each directory entry is composed of: + // 2 bytes: tag number (data field) + // 2 bytes: data format + // 4 bytes: number of components + // 4 bytes: data value or offset to data value + result.tag = parse16(buf + offs, alignIntel); + result.format = parse16(buf + offs + 2, alignIntel); + result.length = parse32(buf + offs + 4, alignIntel); + result.data = parse32(buf + offs + 8, alignIntel); + + // Parse value in specified format + switch (result.format) { + case 1: + result.val_byte = (unsigned char)*(buf + offs + 8); + break; + case 2: + result.val_string = parseEXIFString(buf, result.length, result.data, base, len); + break; + case 3: + result.val_16 = parse16((const unsigned char *)buf + offs + 8, alignIntel); + break; + case 4: + result.val_32 = result.data; + break; + case 5: + if (base + result.data + 8 <= len) + result.val_rational = parseEXIFRational(buf + base + result.data, alignIntel); + break; + case 7: + case 9: + case 10: + break; + default: + result.tag = 0xFF; + } + return result; + } +} + +// +// Locates the EXIF segment and parses it using parseFromEXIFSegment +// +int EXIFInfo::parseFrom(const unsigned char *buf, unsigned len) +{ + // Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9 + // This check also ensures that the user has supplied a correct value for len. + if (!buf || len < 4) + return PARSE_EXIF_ERROR_NO_EXIF; + if (buf[0] != 0xFF || buf[1] != 0xD8) + return PARSE_EXIF_ERROR_NO_JPEG; + if (buf[len - 2] != 0xFF || buf[len - 1] != 0xD9) + return PARSE_EXIF_ERROR_NO_JPEG; + clear(); + + // Scan for EXIF header (bytes 0xFF 0xE1) and do a sanity check by + // looking for bytes "Exif\0\0". The marker length data is in Motorola + // byte order, which results in the 'false' parameter to parse16(). + // The marker has to contain at least the TIFF header, otherwise the + // EXIF data is corrupt. So the minimum length specified here has to be: + // 2 bytes: section size + // 6 bytes: "Exif\0\0" string + // 2 bytes: TIFF header (either "II" or "MM" string) + // 2 bytes: TIFF magic (short 0x2a00 in Motorola byte order) + // 4 bytes: Offset to first IFD + // ========= + // 16 bytes + unsigned offs = 0; // current offset into buffer + for (offs = 0; offs < len - 1; offs++) + if (buf[offs] == 0xFF && buf[offs + 1] == 0xE1) + break; + if (offs + 4 > len) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 2; + unsigned short section_length = parse16(buf + offs, false); + if (offs + section_length > len || section_length < 16) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + + return parseFromEXIFSegment(buf + offs, len - offs); +} + +int EXIFInfo::parseFrom(const string &data) +{ + return parseFrom((const unsigned char *)data.data(), data.length()); +} + +// +// Main parsing function for an EXIF segment. +// +// PARAM: 'buf' start of the EXIF TIFF, which must be the bytes "Exif\0\0". +// PARAM: 'len' length of buffer +// +int EXIFInfo::parseFromEXIFSegment(const unsigned char *buf, unsigned len) +{ + bool alignIntel = true; // byte alignment (defined in EXIF header) + unsigned offs = 0; // current offset into buffer + if (!buf || len < 6) + return PARSE_EXIF_ERROR_NO_EXIF; + + if (!std::equal(buf, buf + 6, "Exif\0\0")) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 6; + + // Now parsing the TIFF header. The first two bytes are either "II" or + // "MM" for Intel or Motorola byte alignment. Sanity check by parsing + // the unsigned short that follows, making sure it equals 0x2a. The + // last 4 bytes are an offset into the first IFD, which are added to + // the global offset counter. For this block, we expect the following + // minimum size: + // 2 bytes: 'II' or 'MM' + // 2 bytes: 0x002a + // 4 bytes: offset to first IDF + // ----------------------------- + // 8 bytes + if (offs + 8 > len) + return PARSE_EXIF_ERROR_CORRUPT; + unsigned tiff_header_start = offs; + if (buf[offs] == 'I' && buf[offs + 1] == 'I') + alignIntel = true; + else { + if (buf[offs] == 'M' && buf[offs + 1] == 'M') + alignIntel = false; + else + return PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN; + } + this->ByteAlign = alignIntel; + offs += 2; + if (0x2a != parse16(buf + offs, alignIntel)) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned first_ifd_offset = parse32(buf + offs, alignIntel); + offs += first_ifd_offset - 4; + if (offs >= len) + return PARSE_EXIF_ERROR_CORRUPT; + + // Now parsing the first Image File Directory (IFD0, for the main image). + // An IFD consists of a variable number of 12-byte directory entries. The + // first two bytes of the IFD section contain the number of directory + // entries in the section. The last 4 bytes of the IFD contain an offset + // to the next IFD, which means this IFD must contain exactly 6 + 12 * num + // bytes of data. + if (offs + 2 > len) + return PARSE_EXIF_ERROR_CORRUPT; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned exif_sub_ifd_offset = len; + unsigned gps_sub_ifd_offset = len; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + offs += 12; + switch (result.tag) { + case 0x102: + // Bits per sample + if (result.format == 3) + this->BitsPerSample = result.val_16; + break; + + case 0x10E: + // Image description + if (result.format == 2) + this->ImageDescription = result.val_string; + break; + + case 0x10F: + // Digicam make + if (result.format == 2) + this->Make = result.val_string; + break; + + case 0x110: + // Digicam model + if (result.format == 2) + this->Model = result.val_string; + break; + + case 0x112: + // Orientation of image + if (result.format == 3) + this->Orientation = result.val_16; + break; + + case 0x131: + // Software used for image + if (result.format == 2) + this->Software = result.val_string; + break; + + case 0x132: + // EXIF/TIFF date/time of image modification + if (result.format == 2) + this->DateTime = result.val_string; + break; + + case 0x8298: + // Copyright information + if (result.format == 2) + this->Copyright = result.val_string; + break; + + case 0x8825: + // GPS IFS offset + gps_sub_ifd_offset = tiff_header_start + result.data; + break; + + case 0x8769: + // EXIF SubIFD offset + exif_sub_ifd_offset = tiff_header_start + result.data; + break; + } + } + + // Jump to the EXIF SubIFD if it exists and parse all the information + // there. Note that it's possible that the EXIF SubIFD doesn't exist. + // The EXIF SubIFD contains most of the interesting information that a + // typical user might want. + if (exif_sub_ifd_offset + 4 <= len) { + offs = exif_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + switch (result.tag) { + case 0x829a: + // Exposure time in seconds + if (result.format == 5) + this->ExposureTime = result.val_rational; + break; + + case 0x829d: + // FNumber + if (result.format == 5) + this->FNumber = result.val_rational; + break; + + case 0x8827: + // ISO Speed Rating + if (result.format == 3) + this->ISOSpeedRatings = result.val_16; + break; + + case 0x9003: + // Original date and time + if (result.format == 2) + this->DateTimeOriginal = result.val_string; + break; + + case 0x9004: + // Digitization date and time + if (result.format == 2) + this->DateTimeDigitized = result.val_string; + break; + + case 0x9201: + // Shutter speed value + if (result.format == 5) + this->ShutterSpeedValue = result.val_rational; + break; + + case 0x9204: + // Exposure bias value + if (result.format == 5) + this->ExposureBiasValue = result.val_rational; + break; + + case 0x9206: + // Subject distance + if (result.format == 5) + this->SubjectDistance = result.val_rational; + break; + + case 0x9209: + // Flash used + if (result.format == 3) + this->Flash = result.data ? 1 : 0; + break; + + case 0x920a: + // Focal length + if (result.format == 5) + this->FocalLength = result.val_rational; + break; + + case 0x9207: + // Metering mode + if (result.format == 3) + this->MeteringMode = result.val_16; + break; + + case 0x9291: + // Subsecond original time + if (result.format == 2) + this->SubSecTimeOriginal = result.val_string; + break; + + case 0xa002: + // EXIF Image width + if (result.format == 4) + this->ImageWidth = result.val_32; + if (result.format == 3) + this->ImageWidth = result.val_16; + break; + + case 0xa003: + // EXIF Image height + if (result.format == 4) + this->ImageHeight = result.val_32; + if (result.format == 3) + this->ImageHeight = result.val_16; + break; + + case 0xa405: + // Focal length in 35mm film + if (result.format == 3) + this->FocalLengthIn35mm = result.val_16; + break; + } + offs += 12; + } + } + + // Jump to the GPS SubIFD if it exists and parse all the information + // there. Note that it's possible that the GPS SubIFD doesn't exist. + if (gps_sub_ifd_offset + 4 <= len) { + offs = gps_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + unsigned short tag = parse16(buf + offs, alignIntel); + unsigned short format = parse16(buf + offs + 2, alignIntel); + unsigned length = parse32(buf + offs + 4, alignIntel); + unsigned data = parse32(buf + offs + 8, alignIntel); + switch (tag) { + case 1: + // GPS north or south + this->GeoLocation.LatComponents.direction = *(buf + offs + 8); + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + break; + + case 2: + // GPS latitude + if (format == 5 && length == 3) { + this->GeoLocation.LatComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LatComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LatComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Latitude = + this->GeoLocation.LatComponents.degrees + + this->GeoLocation.LatComponents.minutes / 60 + + this->GeoLocation.LatComponents.seconds / 3600; + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + } + break; + + case 3: + // GPS east or west + this->GeoLocation.LonComponents.direction = *(buf + offs + 8); + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + break; + + case 4: + // GPS longitude + if (format == 5 && length == 3) { + this->GeoLocation.LonComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LonComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LonComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Longitude = + this->GeoLocation.LonComponents.degrees + + this->GeoLocation.LonComponents.minutes / 60 + + this->GeoLocation.LonComponents.seconds / 3600; + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + } + break; + + case 5: + // GPS altitude reference (below or above sea level) + this->GeoLocation.AltitudeRef = *(buf + offs + 8); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + break; + + case 6: + // GPS altitude reference + if (format == 5) { + this->GeoLocation.Altitude = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + } + break; + } + offs += 12; + } + } + + return PARSE_EXIF_SUCCESS; +} + +void EXIFInfo::clear() +{ + // Strings + ImageDescription.clear(); + Make.clear(); + Model.clear(); + Software.clear(); + DateTime.clear(); + DateTimeOriginal.clear(); + DateTimeDigitized.clear(); + SubSecTimeOriginal.clear(); + Copyright.clear(); + + // Shorts / unsigned / double + ByteAlign = 0; + Orientation = 0; + + BitsPerSample = 0; + ExposureTime = 0; + FNumber = 0; + ISOSpeedRatings = 0; + ShutterSpeedValue = 0; + ExposureBiasValue = 0; + SubjectDistance = 0; + FocalLength = 0; + FocalLengthIn35mm = 0; + Flash = 0; + MeteringMode = 0; + ImageWidth = 0; + ImageHeight = 0; + + // Geolocation + GeoLocation.Latitude = 0; + GeoLocation.Longitude = 0; + GeoLocation.Altitude = 0; + GeoLocation.AltitudeRef = 0; + GeoLocation.LatComponents.degrees = 0; + GeoLocation.LatComponents.minutes = 0; + GeoLocation.LatComponents.seconds = 0; + GeoLocation.LatComponents.direction = 0; + GeoLocation.LonComponents.degrees = 0; + GeoLocation.LonComponents.minutes = 0; + GeoLocation.LonComponents.seconds = 0; + GeoLocation.LonComponents.direction = 0; +} + +time_t EXIFInfo::epoch() +{ + struct tm tm; + int year, month, day, hour, min, sec; + + if (DateTimeOriginal.size()) + sscanf(DateTimeOriginal.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); + else + sscanf(DateTime.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); + tm.tm_year = year; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return (utc_mktime(&tm)); +} diff --git a/core/exif.h b/core/exif.h new file mode 100644 index 000000000..0fb3a7d4a --- /dev/null +++ b/core/exif.h @@ -0,0 +1,147 @@ +/************************************************************************** + exif.h -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Based on the description of the EXIF file format at: + -- http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html + -- http://www.media.mit.edu/pia/Research/deepview/exif.html + -- http://www.exif.org/Exif2-2.PDF + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved. + + VERSION HISTORY: + ================ + + 2.1: Released July 2013 + -- fixed a bug where JPEGs without an EXIF SubIFD would not be parsed + -- fixed a bug in parsing GPS coordinate seconds + -- fixed makefile bug + -- added two pathological test images from Matt Galloway + http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/ + -- split main parsing routine for easier integration into Firefox + + 2.0: Released February 2013 + -- complete rewrite + -- no new/delete + -- added GPS support + + 1.0: Released 2010 + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef EXIF_H +#define EXIF_H + +#include <string> + +// +// Class responsible for storing and parsing EXIF information from a JPEG blob +// +class EXIFInfo { +public: + // Parsing function for an entire JPEG image buffer. + // + // PARAM 'data': A pointer to a JPEG image. + // PARAM 'length': The length of the JPEG image. + // RETURN: PARSE_EXIF_SUCCESS (0) on succes with 'result' filled out + // error code otherwise, as defined by the PARSE_EXIF_ERROR_* macros + int parseFrom(const unsigned char *data, unsigned length); + int parseFrom(const std::string &data); + + // Parsing function for an EXIF segment. This is used internally by parseFrom() + // but can be called for special cases where only the EXIF section is + // available (i.e., a blob starting with the bytes "Exif\0\0"). + int parseFromEXIFSegment(const unsigned char *buf, unsigned len); + + // Set all data members to default values. + void clear(); + + // Data fields filled out by parseFrom() + char ByteAlign; // 0 = Motorola byte alignment, 1 = Intel + std::string ImageDescription; // Image description + std::string Make; // Camera manufacturer's name + std::string Model; // Camera model + unsigned short Orientation; // Image orientation, start of data corresponds to + // 0: unspecified in EXIF data + // 1: upper left of image + // 3: lower right of image + // 6: upper right of image + // 8: lower left of image + // 9: undefined + unsigned short BitsPerSample; // Number of bits per component + std::string Software; // Software used + std::string DateTime; // File change date and time + std::string DateTimeOriginal; // Original file date and time (may not exist) + std::string DateTimeDigitized; // Digitization date and time (may not exist) + std::string SubSecTimeOriginal; // Sub-second time that original picture was taken + std::string Copyright; // File copyright information + double ExposureTime; // Exposure time in seconds + double FNumber; // F/stop + unsigned short ISOSpeedRatings; // ISO speed + double ShutterSpeedValue; // Shutter speed (reciprocal of exposure time) + double ExposureBiasValue; // Exposure bias value in EV + double SubjectDistance; // Distance to focus point in meters + double FocalLength; // Focal length of lens in millimeters + unsigned short FocalLengthIn35mm; // Focal length in 35mm film + char Flash; // 0 = no flash, 1 = flash used + unsigned short MeteringMode; // Metering mode + // 1: average + // 2: center weighted average + // 3: spot + // 4: multi-spot + // 5: multi-segment + unsigned ImageWidth; // Image width reported in EXIF data + unsigned ImageHeight; // Image height reported in EXIF data + struct Geolocation_t + { // GPS information embedded in file + double Latitude; // Image latitude expressed as decimal + double Longitude; // Image longitude expressed as decimal + double Altitude; // Altitude in meters, relative to sea level + char AltitudeRef; // 0 = above sea level, -1 = below sea level + struct Coord_t { + double degrees; + double minutes; + double seconds; + char direction; + } LatComponents, LonComponents; // Latitude, Longitude expressed in deg/min/sec + } GeoLocation; + EXIFInfo() + { + clear(); + } + + time_t epoch(); +}; + +// Parse was successful +#define PARSE_EXIF_SUCCESS 0 +// No JPEG markers found in buffer, possibly invalid JPEG file +#define PARSE_EXIF_ERROR_NO_JPEG 1982 +// No EXIF header found in JPEG file. +#define PARSE_EXIF_ERROR_NO_EXIF 1983 +// Byte alignment specified in EXIF file was unknown (not Motorola or Intel). +#define PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN 1984 +// EXIF header was found, but data was corrupted. +#define PARSE_EXIF_ERROR_CORRUPT 1985 + +#endif // EXIF_H diff --git a/core/file.c b/core/file.c new file mode 100644 index 000000000..1337da3a2 --- /dev/null +++ b/core/file.c @@ -0,0 +1,1115 @@ +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include "gettext.h" +#include <zip.h> +#include <time.h> + +#include "dive.h" +#include "divelist.h" +#include "file.h" +#include "git-access.h" +#include "qthelperfromc.h" + +/* For SAMPLE_* */ +#include <libdivecomputer/parser.h> + +/* to check XSLT version number */ +#include <libxslt/xsltconfig.h> + +/* Crazy windows sh*t */ +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +int readfile(const char *filename, struct memblock *mem) +{ + int ret, fd; + struct stat st; + char *buf; + + mem->buffer = NULL; + mem->size = 0; + + fd = subsurface_open(filename, O_RDONLY | O_BINARY, 0); + if (fd < 0) + return fd; + ret = fstat(fd, &st); + if (ret < 0) + goto out; + ret = -EINVAL; + if (!S_ISREG(st.st_mode)) + goto out; + ret = 0; + if (!st.st_size) + goto out; + buf = malloc(st.st_size + 1); + ret = -1; + errno = ENOMEM; + if (!buf) + goto out; + mem->buffer = buf; + mem->size = st.st_size; + ret = read(fd, buf, mem->size); + if (ret < 0) + goto free; + buf[ret] = 0; + if (ret == (int)mem->size) // converting to int loses a bit but size will never be that big + goto out; + errno = EIO; + ret = -1; +free: + free(mem->buffer); + mem->buffer = NULL; + mem->size = 0; +out: + close(fd); + return ret; +} + + +static void zip_read(struct zip_file *file, const char *filename) +{ + int size = 1024, n, read = 0; + char *mem = malloc(size); + + while ((n = zip_fread(file, mem + read, size - read)) > 0) { + read += n; + size = read * 3 / 2; + mem = realloc(mem, size); + } + mem[read] = 0; + (void) parse_xml_buffer(filename, mem, read, &dive_table, NULL); + free(mem); +} + +int try_to_open_zip(const char *filename) +{ + int success = 0; + /* Grr. libzip needs to re-open the file, it can't take a buffer */ + struct zip *zip = subsurface_zip_open_readonly(filename, ZIP_CHECKCONS, NULL); + + if (zip) { + int index; + for (index = 0;; index++) { + struct zip_file *file = zip_fopen_index(zip, index, 0); + if (!file) + break; + /* skip parsing the divelogs.de pictures */ + if (strstr(zip_get_name(zip, index, 0), "pictures/")) + continue; + zip_read(file, filename); + zip_fclose(file); + success++; + } + subsurface_zip_close(zip); + + if (!success) + return report_error(translate("gettextFromC", "No dives in the input file '%s'"), filename); + } + return success; +} + +static int try_to_xslt_open_csv(const char *filename, struct memblock *mem, const char *tag) +{ + char *buf; + + if (mem->size == 0 && readfile(filename, mem) < 0) + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + + /* Surround the CSV file content with XML tags to enable XSLT + * parsing + * + * Tag markers take: strlen("<></>") = 5 + */ + buf = realloc(mem->buffer, mem->size + 7 + strlen(tag) * 2); + if (buf != NULL) { + char *starttag = NULL; + char *endtag = NULL; + + starttag = malloc(3 + strlen(tag)); + endtag = malloc(5 + strlen(tag)); + + if (starttag == NULL || endtag == NULL) { + /* this is fairly silly - so the malloc fails, but we strdup the error? + * let's complete the silliness by freeing the two pointers in case one malloc succeeded + * and the other one failed - this will make static analysis tools happy */ + free(starttag); + free(endtag); + free(buf); + return report_error("Memory allocation failed in %s", __func__); + } + + sprintf(starttag, "<%s>", tag); + sprintf(endtag, "\n</%s>", tag); + + memmove(buf + 2 + strlen(tag), buf, mem->size); + memcpy(buf, starttag, 2 + strlen(tag)); + memcpy(buf + mem->size + 2 + strlen(tag), endtag, 5 + strlen(tag)); + mem->size += (6 + 2 * strlen(tag)); + mem->buffer = buf; + + free(starttag); + free(endtag); + } else { + free(mem->buffer); + return report_error("realloc failed in %s", __func__); + } + + return 0; +} + +int db_test_func(void *param, int columns, char **data, char **column) +{ + (void) param; + (void) columns; + (void) column; + return *data[0] == '0'; +} + + +static int try_to_open_db(const char *filename, struct memblock *mem) +{ + sqlite3 *handle; + char dm4_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%ProfileBlob%'"; + char dm5_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%SampleBlob%'"; + char shearwater_test[] = "select count(*) from sqlite_master where type='table' and name='system' and sql like '%dbVersion%'"; + 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%'"; + int retval; + + retval = sqlite3_open(filename, &handle); + + if (retval) { + fprintf(stderr, "Database connection failed '%s'.\n", filename); + return 1; + } + + /* Testing if DB schema resembles Suunto DM5 database format */ + retval = sqlite3_exec(handle, dm5_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_dm5_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Suunto DM4 database format */ + retval = sqlite3_exec(handle, dm4_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_dm4_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Shearwater database format */ + retval = sqlite3_exec(handle, shearwater_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_shearwater_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Atomic Cobalt database format */ + retval = sqlite3_exec(handle, cobalt_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_cobalt_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Divinglog database format */ + retval = sqlite3_exec(handle, divinglog_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_divinglog_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + sqlite3_close(handle); + + return retval; +} + +timestamp_t parse_date(const char *date) +{ + int hour, min, sec; + struct tm tm; + char *p; + + memset(&tm, 0, sizeof(tm)); + tm.tm_mday = strtol(date, &p, 10); + if (tm.tm_mday < 1 || tm.tm_mday > 31) + return 0; + for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) { + if (!memcmp(p, monthname(tm.tm_mon), 3)) + break; + } + if (tm.tm_mon > 11) + return 0; + date = p + 3; + tm.tm_year = strtol(date, &p, 10); + if (date == p) + return 0; + if (tm.tm_year < 70) + tm.tm_year += 2000; + if (tm.tm_year < 100) + tm.tm_year += 1900; + if (sscanf(p, "%d:%d:%d", &hour, &min, &sec) != 3) + return 0; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return utc_mktime(&tm); +} + +enum csv_format { + CSV_DEPTH, + CSV_TEMP, + CSV_PRESSURE, + POSEIDON_DEPTH, + POSEIDON_TEMP, + POSEIDON_SETPOINT, + POSEIDON_SENSOR1, + POSEIDON_SENSOR2, + POSEIDON_PRESSURE, + POSEIDON_O2CYLINDER, + POSEIDON_NDL, + POSEIDON_CEILING +}; + +static void add_sample_data(struct sample *sample, enum csv_format type, double val) +{ + switch (type) { + case CSV_DEPTH: + sample->depth.mm = feet_to_mm(val); + break; + case CSV_TEMP: + sample->temperature.mkelvin = F_to_mkelvin(val); + break; + case CSV_PRESSURE: + sample->cylinderpressure.mbar = psi_to_mbar(val * 4); + break; + case POSEIDON_DEPTH: + sample->depth.mm = val * 0.5 *1000; + break; + case POSEIDON_TEMP: + sample->temperature.mkelvin = C_to_mkelvin(val * 0.2); + break; + case POSEIDON_SETPOINT: + sample->setpoint.mbar = val * 10; + break; + case POSEIDON_SENSOR1: + sample->o2sensor[0].mbar = val * 10; + break; + case POSEIDON_SENSOR2: + sample->o2sensor[1].mbar = val * 10; + break; + case POSEIDON_PRESSURE: + sample->cylinderpressure.mbar = val * 1000; + break; + case POSEIDON_O2CYLINDER: + sample->o2cylinderpressure.mbar = val * 1000; + break; + case POSEIDON_NDL: + sample->ndl.seconds = val * 60; + break; + case POSEIDON_CEILING: + sample->stopdepth.mm = val * 1000; + break; + } +} + +/* + * Cochran comma-separated values: depth in feet, temperature in F, pressure in psi. + * + * They start with eight comma-separated fields like: + * + * filename: {C:\Analyst4\can\T036785.can},{C:\Analyst4\can\K031892.can} + * divenr: %d + * datetime: {03Sep11 16:37:22},{15Dec11 18:27:02} + * ??: 1 + * serialnr??: {CCI134},{CCI207} + * computer??: {GeminiII},{CommanderIII} + * computer??: {GeminiII},{CommanderIII} + * ??: 1 + * + * Followed by the data values (all comma-separated, all one long line). + */ +static int try_to_open_csv(struct memblock *mem, enum csv_format type) +{ + char *p = mem->buffer; + char *header[8]; + int i, time; + timestamp_t date; + struct dive *dive; + struct divecomputer *dc; + + for (i = 0; i < 8; i++) { + header[i] = p; + p = strchr(p, ','); + if (!p) + return 0; + p++; + } + + date = parse_date(header[2]); + if (!date) + return 0; + + dive = alloc_dive(); + dive->when = date; + dive->number = atoi(header[1]); + dc = &dive->dc; + + time = 0; + for (;;) { + char *end; + double val; + struct sample *sample; + + errno = 0; + val = strtod(p, &end); // FIXME == localization issue + if (end == p) + break; + if (errno) + break; + + sample = prepare_sample(dc); + sample->time.seconds = time; + add_sample_data(sample, type, val); + finish_sample(dc); + + time++; + dc->duration.seconds = time; + if (*end != ',') + break; + p = end + 1; + } + record_dive(dive); + return 1; +} + +static int open_by_filename(const char *filename, const char *fmt, struct memblock *mem) +{ + // hack to be able to provide a comment for the translated string + static char *csv_warning = QT_TRANSLATE_NOOP3("gettextFromC", + "Cannot open CSV file %s; please use Import log file dialog", + "'Import log file' should be the same text as corresponding label in Import menu"); + + /* Suunto Dive Manager files: SDE, ZIP; divelogs.de files: DLD */ + if (!strcasecmp(fmt, "SDE") || !strcasecmp(fmt, "ZIP") || !strcasecmp(fmt, "DLD")) + return try_to_open_zip(filename); + + /* CSV files */ + if (!strcasecmp(fmt, "CSV")) + return report_error(translate("gettextFromC", csv_warning), filename); + /* Truly nasty intentionally obfuscated Cochran Anal software */ + if (!strcasecmp(fmt, "CAN")) + return try_to_open_cochran(filename, mem); + /* Cochran export comma-separated-value files */ + if (!strcasecmp(fmt, "DPT")) + return try_to_open_csv(mem, CSV_DEPTH); + if (!strcasecmp(fmt, "LVD")) + return try_to_open_liquivision(filename, mem); + if (!strcasecmp(fmt, "TMP")) + return try_to_open_csv(mem, CSV_TEMP); + if (!strcasecmp(fmt, "HP1")) + return try_to_open_csv(mem, CSV_PRESSURE); + + return 0; +} + +static int parse_file_buffer(const char *filename, struct memblock *mem) +{ + int ret; + char *fmt = strrchr(filename, '.'); + if (fmt && (ret = open_by_filename(filename, fmt + 1, mem)) != 0) + return ret; + + if (!mem->size || !mem->buffer) + return report_error("Out of memory parsing file %s\n", filename); + + return parse_xml_buffer(filename, mem->buffer, mem->size, &dive_table, NULL); +} + +int check_git_sha(const char *filename) +{ + struct git_repository *git; + const char *branch = NULL; + + git = is_git_repository(filename, &branch, NULL, false); + if (prefs.cloud_git_url && + strstr(filename, prefs.cloud_git_url) + && git == dummy_git_repository) + /* opening the cloud storage repository failed for some reason, + * so we don't know if there is additional data in the remote */ + return 1; + + /* if this is a git repository, do we already have this exact state loaded ? + * get the SHA and compare with what we currently have */ + if (git && git != dummy_git_repository) { + const char *sha = get_sha(git, branch); + if (!same_string(sha, "") && + same_string(sha, saved_git_id)) { + fprintf(stderr, "already have loaded SHA %s - don't load again\n", sha); + return 0; + } + } + return 1; +} + +int parse_file(const char *filename) +{ + struct git_repository *git; + const char *branch = NULL; + char *current_sha = copy_string(saved_git_id); + struct memblock mem; + char *fmt; + int ret; + + git = is_git_repository(filename, &branch, NULL, false); + if (prefs.cloud_git_url && + strstr(filename, prefs.cloud_git_url) + && git == dummy_git_repository) { + /* opening the cloud storage repository failed for some reason + * give up here and don't send errors about git repositories */ + free(current_sha); + return 0; + } + /* if this is a git repository, do we already have this exact state loaded ? + * get the SHA and compare with what we currently have */ + if (git && git != dummy_git_repository) { + const char *sha = get_sha(git, branch); + if (!same_string(sha, "") && + same_string(sha, current_sha) && + !unsaved_changes()) { + fprintf(stderr, "already have loaded SHA %s - don't load again\n", sha); + free(current_sha); + return 0; + } + } + free(current_sha); + if (git) + return git_load_dives(git, branch); + + if ((ret = readfile(filename, &mem)) < 0) { + /* we don't want to display an error if this was the default file or the cloud storage */ + if ((prefs.default_filename && !strcmp(filename, prefs.default_filename)) || + isCloudUrl(filename)) + return 0; + + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + } else if (ret == 0) { + return report_error(translate("gettextFromC", "Empty file '%s'"), filename); + } + + fmt = strrchr(filename, '.'); + if (fmt && (!strcasecmp(fmt + 1, "DB") || !strcasecmp(fmt + 1, "BAK") || !strcasecmp(fmt + 1, "SQL"))) { + if (!try_to_open_db(filename, &mem)) { + free(mem.buffer); + return 0; + } + } + + /* Divesoft Freedom */ + if (fmt && (!strcasecmp(fmt + 1, "DLF"))) { + if (!parse_dlf_buffer(mem.buffer, mem.size)) { + free(mem.buffer); + return 0; + } + return -1; + } + + /* DataTrak/Wlog */ + if (fmt && !strcasecmp(fmt + 1, "LOG")) { + datatrak_import(filename, &dive_table); + return 0; + } + + /* OSTCtools */ + if (fmt && (!strcasecmp(fmt + 1, "DIVE"))) { + ostctools_import(filename, &dive_table); + return 0; + } + + ret = parse_file_buffer(filename, &mem); + free(mem.buffer); + return ret; +} + +#define MATCH(buffer, pattern) \ + memcmp(buffer, pattern, strlen(pattern)) + +char *parse_mkvi_value(const char *haystack, const char *needle) +{ + char *lineptr, *valueptr, *endptr, *ret = NULL; + + if ((lineptr = strstr(haystack, needle)) != NULL) { + if ((valueptr = strstr(lineptr, ": ")) != NULL) { + valueptr += 2; + } + if ((endptr = strstr(lineptr, "\n")) != NULL) { + char terminator = '\n'; + if (*(endptr - 1) == '\r') { + --endptr; + terminator = '\r'; + } + *endptr = 0; + ret = copy_string(valueptr); + *endptr = terminator; + + } + } + return ret; +} + +char *next_mkvi_key(const char *haystack) +{ + char *valueptr, *endptr, *ret = NULL; + + if ((valueptr = strstr(haystack, "\n")) != NULL) { + valueptr += 1; + if ((endptr = strstr(valueptr, ": ")) != NULL) { + *endptr = 0; + ret = strdup(valueptr); + *endptr = ':'; + } + } + return ret; +} + +int parse_txt_file(const char *filename, const char *csv) +{ + struct memblock memtxt, memcsv; + + if (readfile(filename, &memtxt) < 0) { + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + } + + /* + * MkVI stores some information in .txt file but the whole profile and events are stored in .csv file. First + * make sure the input .txt looks like proper MkVI file, then start parsing the .csv. + */ + if (MATCH(memtxt.buffer, "MkVI_Config") == 0) { + int d, m, y, he; + int hh = 0, mm = 0, ss = 0; + int prev_depth = 0, cur_sampletime = 0, prev_setpoint = -1, prev_ndl = -1; + bool has_depth = false, has_setpoint = false, has_ndl = false; + char *lineptr, *key, *value; + int o2cylinder_pressure = 0, cylinder_pressure = 0, cur_cylinder_index = 0; + unsigned int prev_time = 0; + + struct dive *dive; + struct divecomputer *dc; + struct tm cur_tm; + + value = parse_mkvi_value(memtxt.buffer, "Dive started at"); + if (sscanf(value, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) != 6) { + free(value); + return -1; + } + free(value); + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hh; + cur_tm.tm_min = mm; + cur_tm.tm_sec = ss; + + dive = alloc_dive(); + dive->when = utc_mktime(&cur_tm);; + dive->dc.model = strdup("Poseidon MkVI Discovery"); + value = parse_mkvi_value(memtxt.buffer, "Rig Serial number"); + dive->dc.deviceid = atoi(value); + free(value); + dive->dc.divemode = CCR; + dive->dc.no_o2sensors = 2; + + dive->cylinder[cur_cylinder_index].cylinder_use = OXYGEN; + dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; + dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; + dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); + dive->cylinder[cur_cylinder_index].gasmix.o2.permille = 1000; + cur_cylinder_index++; + + dive->cylinder[cur_cylinder_index].cylinder_use = DILUENT; + dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; + dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; + dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); + value = parse_mkvi_value(memtxt.buffer, "Helium percentage"); + he = atoi(value); + free(value); + value = parse_mkvi_value(memtxt.buffer, "Nitrogen percentage"); + dive->cylinder[cur_cylinder_index].gasmix.o2.permille = (100 - atoi(value) - he) * 10; + free(value); + dive->cylinder[cur_cylinder_index].gasmix.he.permille = he * 10; + cur_cylinder_index++; + + lineptr = strstr(memtxt.buffer, "Dive started at"); + while (lineptr && *lineptr && (lineptr = strchr(lineptr, '\n')) && ++lineptr) { + key = next_mkvi_key(lineptr); + if (!key) + break; + value = parse_mkvi_value(lineptr, key); + if (!value) { + free(key); + break; + } + add_extra_data(&dive->dc, key, value); + free(key); + free(value); + } + dc = &dive->dc; + + /* + * Read samples from the CSV file. A sample contains all the lines with same timestamp. The CSV file has + * the following format: + * + * timestamp, type, value + * + * And following fields are of interest to us: + * + * 6 sensor1 + * 7 sensor2 + * 8 depth + * 13 o2 tank pressure + * 14 diluent tank pressure + * 20 o2 setpoint + * 39 water temp + */ + + if (readfile(csv, &memcsv) < 0) { + free(dive); + return report_error(translate("gettextFromC", "Poseidon import failed: unable to read '%s'"), csv); + } + lineptr = memcsv.buffer; + for (;;) { + struct sample *sample; + int type; + int value; + int sampletime; + int gaschange = 0; + + /* Collect all the information for one sample */ + sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); + + has_depth = false; + has_setpoint = false; + has_ndl = false; + sample = prepare_sample(dc); + + /* + * There was a bug in MKVI download tool that resulted in erroneous sample + * times. This fix should work similarly as the vendor's own. + */ + + sample->time.seconds = cur_sampletime < 0xFFFF * 3 / 4 ? cur_sampletime : prev_time; + prev_time = sample->time.seconds; + + do { + int i = sscanf(lineptr, "%d,%d,%d", &sampletime, &type, &value); + switch (i) { + case 3: + switch (type) { + case 0: + //Mouth piece position event: 0=OC, 1=CC, 2=UN, 3=NC + switch (value) { + case 0: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position OC")); + break; + case 1: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position CC")); + break; + case 2: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position unknown")); + break; + case 3: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position not connected")); + break; + } + break; + case 3: + //Power Off event + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Power off")); + break; + case 4: + //Battery State of Charge in % +#ifdef SAMPLE_EVENT_BATTERY + add_event(dc, cur_sampletime, SAMPLE_EVENT_BATTERY, 0, + value, QT_TRANSLATE_NOOP("gettextFromC", "battery")); +#endif + break; + case 6: + //PO2 Cell 1 Average + add_sample_data(sample, POSEIDON_SENSOR1, value); + break; + case 7: + //PO2 Cell 2 Average + add_sample_data(sample, POSEIDON_SENSOR2, value); + break; + case 8: + //Depth * 2 + has_depth = true; + prev_depth = value; + add_sample_data(sample, POSEIDON_DEPTH, value); + break; + //9 Max Depth * 2 + //10 Ascent/Descent Rate * 2 + case 11: + //Ascent Rate Alert >10 m/s + add_event(dc, cur_sampletime, SAMPLE_EVENT_ASCENT, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 13: + //O2 Tank Pressure + add_sample_data(sample, POSEIDON_O2CYLINDER, value); + if (!o2cylinder_pressure) { + dive->cylinder[0].sample_start.mbar = value * 1000; + o2cylinder_pressure = value; + } else + o2cylinder_pressure = value; + break; + case 14: + //Diluent Tank Pressure + add_sample_data(sample, POSEIDON_PRESSURE, value); + if (!cylinder_pressure) { + dive->cylinder[1].sample_start.mbar = value * 1000; + cylinder_pressure = value; + } else + cylinder_pressure = value; + break; + //16 Remaining dive time #1? + //17 related to O2 injection + case 20: + //PO2 Setpoint + has_setpoint = true; + prev_setpoint = value; + add_sample_data(sample, POSEIDON_SETPOINT, value); + break; + case 22: + //End of O2 calibration Event: 0 = OK, 2 = Failed, rest of dive setpoint 1.0 + if (value == 2) + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration failed")); + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); + break; + case 25: + //25 Max Ascent depth + add_sample_data(sample, POSEIDON_CEILING, value); + break; + case 31: + //Start of O2 calibration Event + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); + break; + case 37: + //Remaining dive time #2? + has_ndl = true; + prev_ndl = value; + add_sample_data(sample, POSEIDON_NDL, value); + break; + case 39: + // Water Temperature in Celcius + add_sample_data(sample, POSEIDON_TEMP, value); + break; + case 85: + //He diluent part in % + gaschange += value << 16; + break; + case 86: + //O2 diluent part in % + gaschange += value; + break; + //239 Unknown, maybe PO2 at sensor validation? + //240 Unknown, maybe PO2 at sensor validation? + //247 Unknown, maybe PO2 Cell 1 during pressure test + //248 Unknown, maybe PO2 Cell 2 during pressure test + //250 PO2 Cell 1 + //251 PO2 Cell 2 + default: + break; + } /* sample types */ + break; + case EOF: + break; + default: + printf("Unable to parse input: %s\n", lineptr); + break; + } + + lineptr = strchr(lineptr, '\n'); + if (!lineptr || !*lineptr) + break; + lineptr++; + + /* Grabbing next sample time */ + sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); + } while (sampletime == cur_sampletime); + + if (gaschange) + add_event(dc, cur_sampletime, SAMPLE_EVENT_GASCHANGE2, 0, gaschange, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + if (!has_depth) + add_sample_data(sample, POSEIDON_DEPTH, prev_depth); + if (!has_setpoint && prev_setpoint >= 0) + add_sample_data(sample, POSEIDON_SETPOINT, prev_setpoint); + if (!has_ndl && prev_ndl >= 0) + add_sample_data(sample, POSEIDON_NDL, prev_ndl); + if (cylinder_pressure) + dive->cylinder[1].sample_end.mbar = cylinder_pressure * 1000; + if (o2cylinder_pressure) + dive->cylinder[0].sample_end.mbar = o2cylinder_pressure * 1000; + finish_sample(dc); + + if (!lineptr || !*lineptr) + break; + } + record_dive(dive); + return 1; + } else { + return report_error(translate("gettextFromC", "No matching DC found for file '%s'"), csv); + } + + return 0; +} + +#define MAXCOLDIGITS 10 +#define DATESTR 9 +#define TIMESTR 6 + +int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) +{ + int ret, i; + struct memblock mem; + time_t now; + struct tm *timep = NULL; + char tmpbuf[MAXCOLDIGITS]; + + /* Increase the limits for recursion and variables on XSLT + * parsing */ + xsltMaxDepth = 30000; +#if LIBXSLT_VERSION > 10126 + xsltMaxVars = 150000; +#endif + + if (filename == NULL) + return report_error("No CSV filename"); + + time(&now); + timep = localtime(&now); + + strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); + params[pnr++] = "date"; + params[pnr++] = strdup(tmpbuf); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + + strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); + params[pnr++] = "time"; + params[pnr++] = strdup(tmpbuf); + params[pnr++] = NULL; + + mem.size = 0; + if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) + return -1; + + /* + * Lets print command line for manual testing with xsltproc if + * verbosity level is high enough. The printed line needs the + * input file added as last parameter. + */ + +#ifndef SUBSURFACE_MOBILE + if (verbose >= 2) { + fprintf(stderr, "(echo '<csv>'; cat %s;echo '</csv>') | xsltproc ", filename); + for (i=0; params[i]; i+=2) + fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); + fprintf(stderr, "%s/xslt/csv2xml.xslt -\n", SUBSURFACE_SOURCE); + } +#endif + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + + free(mem.buffer); + for (i = 0; params[i]; i += 2) + free(params[i + 1]); + + return ret; +} + +#define SBPARAMS 40 +int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) +{ + int ret, i; + struct memblock mem; + time_t now; + struct tm *timep = NULL; + char *ptr, *ptr_old = NULL; + char *NL = NULL; + char tmpbuf[MAXCOLDIGITS]; + + /* Increase the limits for recursion and variables on XSLT + * parsing */ + xsltMaxDepth = 30000; +#if LIBXSLT_VERSION > 10126 + xsltMaxVars = 150000; +#endif + + time(&now); + timep = localtime(&now); + + strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); + params[pnr++] = "date"; + params[pnr++] = strdup(tmpbuf); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); + params[pnr++] = "time"; + params[pnr++] = strdup(tmpbuf); + + + if (filename == NULL) + return report_error("No CSV filename"); + + if (readfile(filename, &mem) < 0) + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + + /* Determine NL (new line) character and the start of CSV data */ + ptr = mem.buffer; + while ((ptr = strstr(ptr, "\r\n\r\n")) != NULL) { + ptr_old = ptr; + ptr += 1; + NL = "\r\n"; + } + + if (!ptr_old) { + ptr = mem.buffer; + while ((ptr = strstr(ptr, "\n\n")) != NULL) { + ptr_old = ptr; + ptr += 1; + NL = "\n"; + } + ptr_old += 2; + } else + ptr_old += 4; + + /* + * If file does not contain empty lines, it is not a valid + * Seabear CSV file. + */ + if (NL == NULL) + return -1; + + /* + * On my current sample of Seabear DC log file, the date is + * without any identifier. Thus we must search for the previous + * line and step through from there. That is the line after + * Serial number. + */ + ptr = strstr(mem.buffer, "Serial number:"); + if (ptr) + ptr = strstr(ptr, NL); + + /* + * Write date and time values to params array, if available in + * the CSV header + */ + + if (ptr) { + ptr += strlen(NL) + 2; + /* + * pnr is the index of NULL on the params as filled by + * the init function. The two last entries should be + * date and time. Here we overwrite them with the data + * from the CSV header. + */ + + memcpy(params[pnr - 3], ptr, 4); + memcpy(params[pnr - 3] + 4, ptr + 5, 2); + memcpy(params[pnr - 3] + 6, ptr + 8, 2); + params[pnr - 3][8] = 0; + + memcpy(params[pnr - 1] + 1, ptr + 11, 2); + memcpy(params[pnr - 1] + 3, ptr + 14, 2); + params[pnr - 1][5] = 0; + } + + params[pnr++] = NULL; + + /* Move the CSV data to the start of mem buffer */ + memmove(mem.buffer, ptr_old, mem.size - (ptr_old - (char*)mem.buffer)); + mem.size = (int)mem.size - (ptr_old - (char*)mem.buffer); + + if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) + return -1; + + /* + * Lets print command line for manual testing with xsltproc if + * verbosity level is high enough. The printed line needs the + * input file added as last parameter. + */ + + if (verbose >= 2) { + fprintf(stderr, "xsltproc "); + for (i=0; params[i]; i+=2) + fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); + fprintf(stderr, "xslt/csv2xml.xslt\n"); + } + + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + free(mem.buffer); + for (i = 0; params[i]; i += 2) + free(params[i + 1]); + + return ret; +} + +int parse_manual_file(const char *filename, char **params, int pnr) +{ + struct memblock mem; + time_t now; + struct tm *timep; + char curdate[9]; + char curtime[6]; + int ret, i; + + + time(&now); + timep = localtime(&now); + strftime(curdate, DATESTR, "%Y%m%d", timep); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + strftime(curtime, TIMESTR, "1%H%M", timep); + + + params[pnr++] = strdup("date"); + params[pnr++] = strdup(curdate); + params[pnr++] = strdup("time"); + params[pnr++] = strdup(curtime); + params[pnr++] = NULL; + + if (filename == NULL) + return report_error("No manual CSV filename"); + + mem.size = 0; + if (try_to_xslt_open_csv(filename, &mem, "manualCSV")) + return -1; + + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + + free(mem.buffer); + for (i = 0; i < pnr - 2; ++i) + free(params[i]); + return ret; +} diff --git a/core/file.h b/core/file.h new file mode 100644 index 000000000..1c1dfc116 --- /dev/null +++ b/core/file.h @@ -0,0 +1,24 @@ +#ifndef FILE_H +#define FILE_H + +struct memblock { + void *buffer; + size_t size; +}; + +extern int try_to_open_cochran(const char *filename, struct memblock *mem); +extern int try_to_open_liquivision(const char *filename, struct memblock *mem); +extern void datatrak_import(const char *file, struct dive_table *table); +extern void ostctools_import(const char *file, struct dive_table *table); + +#ifdef __cplusplus +extern "C" { +#endif +extern int readfile(const char *filename, struct memblock *mem); +extern timestamp_t parse_date(const char *date); +extern int try_to_open_zip(const char *filename); +#ifdef __cplusplus +} +#endif + +#endif // FILE_H diff --git a/core/gas-model.c b/core/gas-model.c new file mode 100644 index 000000000..ad1160f3b --- /dev/null +++ b/core/gas-model.c @@ -0,0 +1,64 @@ +/* gas-model.c */ +/* gas compressibility model */ +#include <stdio.h> +#include <stdlib.h> +#include "dive.h" + +/* "Virial minus one" - the virial cubic form without the initial 1.0 */ +#define virial_m1(C, x1, x2, x3) (C[0]*x1+C[1]*x2+C[2]*x3) + +/* + * Cubic virial least-square coefficients for O2/N2/He based on data from + * + * PERRY’S CHEMICAL ENGINEERS’ HANDBOOK SEVENTH EDITION + * + * with the lookup and curve fitting by Lubomir. + * + * The "virial" form of the compression factor polynomial is + * + * Z = 1.0 + C[0]*P + C[1]*P^2 + C[2]*P^3 ... + * + * and these tables do not contain the initial 1.0 term. + * + * NOTE! Helium coefficients are a linear mix operation between the + * 323K and one for 273K isotherms, to make everything be at 300K. + */ +double gas_compressibility_factor(struct gasmix *gas, double bar) +{ + static const double o2_coefficients[3] = { + -7.18092073703e-04, + +2.81852572808e-06, + -1.50290620492e-09 + }; + static const double n2_coefficients[3] = { + -2.19260353292e-04, + +2.92844845532e-06, + -2.07613482075e-09 + }; + static const double he_coefficients[3] = { + +4.87320026468e-04, + -8.83632921053e-08, + +5.33304543646e-11 + }; + int o2, he; + double x1, x2, x3; + double Z; + + o2 = get_o2(gas); + he = get_he(gas); + + x1 = bar; x2 = x1*x1; x3 = x2*x1; + + Z = virial_m1(o2_coefficients, x1, x2, x3) * o2 + + virial_m1(he_coefficients, x1, x2, x3) * he + + virial_m1(n2_coefficients, x1, x2, x3) * (1000 - o2 - he); + + /* + * We add the 1.0 at the very end - the linear mixing of the + * three 1.0 terms is still 1.0 regardless of the gas mix. + * + * The * 0.001 is because we did the linear mixing using the + * raw permille gas values. + */ + return Z * 0.001 + 1.0; +} diff --git a/core/gaspressures.c b/core/gaspressures.c new file mode 100644 index 000000000..5d3fc9791 --- /dev/null +++ b/core/gaspressures.c @@ -0,0 +1,430 @@ +/* gaspressures.c + * --------------- + * This file contains the routines to calculate the gas pressures in the cylinders. + * The functions below support the code in profile.c. + * The high-level function is populate_pressure_information(), called by function + * create_plot_info_new() in profile.c. The other functions below are, in turn, + * called by populate_pressure_information(). The calling sequence is as follows: + * + * populate_pressure_information() -> calc_pressure_time() + * -> fill_missing_tank_pressures() -> fill_missing_segment_pressures() + * -> get_pr_interpolate_data() + * + * The pr_track_t related functions below implement a linked list that is used by + * the majority of the functions below. The linked list covers a part of the dive profile + * for which there are no cylinder pressure data. Each element in the linked list + * represents a segment between two consecutive points on the dive profile. + * pr_track_t is defined in gaspressures.h + */ + +#include "dive.h" +#include "display.h" +#include "profile.h" +#include "gaspressures.h" + +static pr_track_t *pr_track_alloc(int start, int t_start) +{ + pr_track_t *pt = malloc(sizeof(pr_track_t)); + pt->start = start; + pt->end = 0; + pt->t_start = pt->t_end = t_start; + pt->pressure_time = 0; + pt->next = NULL; + return pt; +} + +/* poor man's linked list */ +static pr_track_t *list_last(pr_track_t *list) +{ + pr_track_t *tail = list; + if (!tail) + return NULL; + while (tail->next) { + tail = tail->next; + } + return tail; +} + +static pr_track_t *list_add(pr_track_t *list, pr_track_t *element) +{ + pr_track_t *tail = list_last(list); + if (!tail) + return element; + tail->next = element; + return list; +} + +static void list_free(pr_track_t *list) +{ + if (!list) + return; + list_free(list->next); + free(list); +} + +#ifdef DEBUG_PR_TRACK +static void dump_pr_track(pr_track_t **track_pr) +{ + int cyl; + pr_track_t *list; + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + list = track_pr[cyl]; + while (list) { + printf("cyl%d: start %d end %d t_start %d t_end %d pt %d\n", cyl, + list->start, list->end, list->t_start, list->t_end, list->pressure_time); + list = list->next; + } + } +} +#endif + +/* + * This looks at the pressures for one cylinder, and + * calculates any missing beginning/end pressures for + * each segment by taking the over-all SAC-rate into + * account for that cylinder. + * + * NOTE! Many segments have full pressure information + * (both beginning and ending pressure). But if we have + * switched away from a cylinder, we will have the + * beginning pressure for the first segment with a + * missing end pressure. We may then have one or more + * segments without beginning or end pressures, until + * we finally have a segment with an end pressure. + * + * We want to spread out the pressure over these missing + * segments according to how big of a time_pressure area + * they have. + */ +static void fill_missing_segment_pressures(pr_track_t *list, enum interpolation_strategy strategy) +{ + double magic; + + while (list) { + int start = list->start, end; + pr_track_t *tmp = list; + int pt_sum = 0, pt = 0; + + for (;;) { + pt_sum += tmp->pressure_time; + end = tmp->end; + if (end) + break; + end = start; + if (!tmp->next) + break; + tmp = tmp->next; + } + + if (!start) + start = end; + + /* + * Now 'start' and 'end' contain the pressure values + * for the set of segments described by 'list'..'tmp'. + * pt_sum is the sum of all the pressure-times of the + * segments. + * + * Now dole out the pressures relative to pressure-time. + */ + list->start = start; + tmp->end = end; + switch (strategy) { + case SAC: + for (;;) { + int pressure; + pt += list->pressure_time; + pressure = start; + if (pt_sum) + pressure -= (start - end) * (double)pt / pt_sum; + list->end = pressure; + if (list == tmp) + break; + list = list->next; + list->start = pressure; + } + break; + case TIME: + if (list->t_end && (tmp->t_start - tmp->t_end)) { + magic = (list->t_start - tmp->t_end) / (tmp->t_start - tmp->t_end); + list->end = rint(start - (start - end) * magic); + } else { + list->end = start; + } + break; + case CONSTANT: + list->end = start; + } + + /* Ok, we've done that set of segments */ + list = list->next; + } +} + +#ifdef DEBUG_PR_INTERPOLATE +void dump_pr_interpolate(int i, pr_interpolate_t interpolate_pr) +{ + printf("Interpolate for entry %d: start %d - end %d - pt %d - acc_pt %d\n", i, + interpolate_pr.start, interpolate_pr.end, interpolate_pr.pressure_time, interpolate_pr.acc_pressure_time); +} +#endif + + +static struct pr_interpolate_struct get_pr_interpolate_data(pr_track_t *segment, struct plot_info *pi, int cur) +{ // cur = index to pi->entry corresponding to t_end of segment; + struct pr_interpolate_struct interpolate; + int i; + struct plot_data *entry; + + interpolate.start = segment->start; + interpolate.end = segment->end; + interpolate.acc_pressure_time = 0; + interpolate.pressure_time = 0; + + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + + if (entry->sec < segment->t_start) + continue; + interpolate.pressure_time += entry->pressure_time; + if (entry->sec >= segment->t_end) + break; + if (i <= cur) + interpolate.acc_pressure_time += entry->pressure_time; + } + return interpolate; +} + +static void fill_missing_tank_pressures(struct dive *dive, struct plot_info *pi, pr_track_t **track_pr, bool o2_flag) +{ + int cyl, i; + struct plot_data *entry; + pr_interpolate_t interpolate = { 0, 0, 0, 0 }; + pr_track_t *last_segment = NULL; + int cur_pr[MAX_CYLINDERS]; // cur_pr[MAX_CYLINDERS] is the CCR diluent cylinder + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + enum interpolation_strategy strategy; + if (!track_pr[cyl]) { + /* no segment where this cylinder is used */ + cur_pr[cyl] = -1; + continue; + } + if (dive->cylinder[cyl].cylinder_use == OC_GAS) + strategy = SAC; + else + strategy = TIME; + fill_missing_segment_pressures(track_pr[cyl], strategy); // Interpolate the missing tank pressure values .. + cur_pr[cyl] = track_pr[cyl]->start; // in the pr_track_t lists of structures + } // and keep the starting pressure for each cylinder. + +#ifdef DEBUG_PR_TRACK + /* another great debugging tool */ + dump_pr_track(track_pr); +#endif + + /* Transfer interpolated cylinder pressures from pr_track strucktures to plotdata + * Go down the list of tank pressures in plot_info. Align them with the start & + * end times of each profile segment represented by a pr_track_t structure. Get + * the accumulated pressure_depths from the pr_track_t structures and then + * interpolate the pressure where these do not exist in the plot_info pressure + * variables. Pressure values are transferred from the pr_track_t structures + * to the plot_info structure, allowing us to plot the tank pressure. + * + * The first two pi structures are "fillers", but in case we don't have a sample + * at time 0 we need to process the second of them here, therefore i=1 */ + for (i = 1; i < pi->nr; i++) { // For each point on the profile: + double magic; + pr_track_t *segment; + int pressure; + int *save_pressure, *save_interpolated; + + entry = pi->entry + i; + + if (o2_flag) { + // Find the cylinder index (cyl) and pressure + cyl = dive->oxygen_cylinder_index; + if (cyl < 0) + return; // Can we do this?!? + pressure = O2CYLINDER_PRESSURE(entry); + save_pressure = &(entry->o2cylinderpressure[SENSOR_PR]); + save_interpolated = &(entry->o2cylinderpressure[INTERPOLATED_PR]); + } else { + pressure = SENSOR_PRESSURE(entry); + save_pressure = &(entry->pressure[SENSOR_PR]); + save_interpolated = &(entry->pressure[INTERPOLATED_PR]); + cyl = entry->cylinderindex; + } + + if (pressure) { // If there is a valid pressure value, + last_segment = NULL; // get rid of interpolation data, + cur_pr[cyl] = pressure; // set current pressure + continue; // and skip to next point. + } + // If there is NO valid pressure value.. + // Find the pressure segment corresponding to this entry.. + segment = track_pr[cyl]; + while (segment && segment->t_end < entry->sec) // Find the track_pr with end time.. + segment = segment->next; // ..that matches the plot_info time (entry->sec) + + if (!segment || !segment->pressure_time) { // No (or empty) segment? + *save_pressure = cur_pr[cyl]; // Just use our current pressure + continue; // and skip to next point. + } + + // If there is a valid segment but no tank pressure .. + if (segment == last_segment) { + interpolate.acc_pressure_time += entry->pressure_time; + } else { + // Set up an interpolation structure + interpolate = get_pr_interpolate_data(segment, pi, i); + last_segment = segment; + } + + if(dive->cylinder[cyl].cylinder_use == OC_GAS) { + + /* if this segment has pressure_time, then calculate a new interpolated pressure */ + if (interpolate.pressure_time) { + /* Overall pressure change over total pressure-time for this segment*/ + magic = (interpolate.end - interpolate.start) / (double)interpolate.pressure_time; + + /* Use that overall pressure change to update the current pressure */ + cur_pr[cyl] = rint(interpolate.start + magic * interpolate.acc_pressure_time); + } + } else { + magic = (interpolate.end - interpolate.start) / (segment->t_end - segment->t_start); + cur_pr[cyl] = rint(segment->start + magic * (entry->sec - segment->t_start)); + } + *save_interpolated = cur_pr[cyl]; // and store the interpolated data in plot_info + } +} + + +/* + * What's the pressure-time between two plot data entries? + * We're calculating the integral of pressure over time by + * adding these up. + * + * The units won't matter as long as everybody agrees about + * them, since they'll cancel out - we use this to calculate + * a constant SAC-rate-equivalent, but we only use it to + * scale pressures, so it ends up being a unitless scaling + * factor. + */ +static inline int calc_pressure_time(struct dive *dive, struct plot_data *a, struct plot_data *b) +{ + int time = b->sec - a->sec; + int depth = (a->depth + b->depth) / 2; + + if (depth <= SURFACE_THRESHOLD) + return 0; + + return depth_to_mbar(depth, dive) * time; +} + +#ifdef PRINT_PRESSURES_DEBUG +// A CCR debugging tool that prints the gas pressures in cylinder 0 and in the diluent cylinder, used in populate_pressure_information(): +static void debug_print_pressures(struct plot_info *pi) +{ + int i; + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + printf("%5d |%9d | %9d || %9d | %9d |\n", i, SENSOR_PRESSURE(entry), INTERPOLATED_PRESSURE(entry), DILUENT_PRESSURE(entry), INTERPOLATED_DILUENT_PRESSURE(entry)); + } +} +#endif + +/* This function goes through the list of tank pressures, either SENSOR_PRESSURE(entry) or O2CYLINDER_PRESSURE(entry), + * of structure plot_info for the dive profile where each item in the list corresponds to one point (node) of the + * profile. It finds values for which there are no tank pressures (pressure==0). For each missing item (node) of + * tank pressure it creates a pr_track_alloc structure that represents a segment on the dive profile and that + * contains tank pressures. There is a linked list of pr_track_alloc structures for each cylinder. These pr_track_alloc + * structures ultimately allow for filling the missing tank pressure values on the dive profile using the depth_pressure + * of the dive. To do this, it calculates the summed pressure-time value for the duration of the dive and stores these + * in the pr_track_alloc structures. If diluent_flag = 1, then DILUENT_PRESSURE(entry) is used instead of SENSOR_PRESSURE. + * This function is called by create_plot_info_new() in profile.c + */ +void populate_pressure_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, int o2_flag) +{ + (void) dc; + int i, cylinderid, cylinderindex = -1; + pr_track_t *track_pr[MAX_CYLINDERS] = { NULL, }; + pr_track_t *current = NULL; + bool missing_pr = false; + bool found_any_pr_data = false; + + /* if we have no pressure data whatsoever, this is pointless, so let's just return */ + for (i = 0; i < MAX_CYLINDERS; i++) { + if (dive->cylinder[i].start.mbar || dive->cylinder[i].sample_start.mbar || + dive->cylinder[i].end.mbar || dive->cylinder[i].sample_end.mbar) { + found_any_pr_data = true; + break; + } + } + if (!found_any_pr_data) + return; + + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + unsigned pressure; + if (o2_flag) { // if this is a diluent cylinder: + pressure = O2CYLINDER_PRESSURE(entry); + cylinderid = dive->oxygen_cylinder_index; + if (cylinderid < 0) + goto GIVE_UP; + } else { + pressure = SENSOR_PRESSURE(entry); + cylinderid = entry->cylinderindex; + } + /* If track_pr structure already exists, then update it: */ + /* discrete integration of pressure over time to get the SAC rate equivalent */ + if (current) { + entry->pressure_time = calc_pressure_time(dive, entry - 1, entry); + current->pressure_time += entry->pressure_time; + current->t_end = entry->sec; + } + + /* If 1st record or different cylinder: Create a new track_pr structure: */ + /* track the segments per cylinder and their pressure/time integral */ + if (cylinderid != cylinderindex) { + if (o2_flag) // For CCR dives: + cylinderindex = dive->oxygen_cylinder_index; // indicate o2 cylinder + else + cylinderindex = entry->cylinderindex; + current = pr_track_alloc(pressure, entry->sec); + track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); + continue; + } + + if (!pressure) { + missing_pr = 1; + continue; + } + if (current) + current->end = pressure; + + /* Was it continuous? */ + if ((o2_flag) && (O2CYLINDER_PRESSURE(entry - 1))) // in the case of CCR o2 pressure + continue; + else if (SENSOR_PRESSURE(entry - 1)) // for all other cylinders + continue; + + /* transmitter stopped transmitting cylinder pressure data */ + current = pr_track_alloc(pressure, entry->sec); + if (cylinderindex >= 0) + track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); + } + + if (missing_pr) { + fill_missing_tank_pressures(dive, pi, track_pr, o2_flag); + } + +#ifdef PRINT_PRESSURES_DEBUG + debug_print_pressures(pi); +#endif + +GIVE_UP: + for (i = 0; i < MAX_CYLINDERS; i++) + list_free(track_pr[i]); +} diff --git a/core/gaspressures.h b/core/gaspressures.h new file mode 100644 index 000000000..420c117a2 --- /dev/null +++ b/core/gaspressures.h @@ -0,0 +1,35 @@ +#ifndef GASPRESSURES_H +#define GASPRESSURES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * simple structure to track the beginning and end tank pressure as + * well as the integral of depth over time spent while we have no + * pressure reading from the tank */ +typedef struct pr_track_struct pr_track_t; +struct pr_track_struct { + int start; + int end; + int t_start; + int t_end; + int pressure_time; + pr_track_t *next; +}; + +typedef struct pr_interpolate_struct pr_interpolate_t; +struct pr_interpolate_struct { + int start; + int end; + int pressure_time; + int acc_pressure_time; +}; + +enum interpolation_strategy {SAC, TIME, CONSTANT}; + +#ifdef __cplusplus +} +#endif +#endif // GASPRESSURES_H diff --git a/core/gettext.h b/core/gettext.h new file mode 100644 index 000000000..43ff023c7 --- /dev/null +++ b/core/gettext.h @@ -0,0 +1,10 @@ +#ifndef MYGETTEXT_H +#define MYGETTEXT_H + +/* this is for the Qt based translations */ +extern const char *trGettext(const char *); +#define translate(_context, arg) trGettext(arg) +#define QT_TRANSLATE_NOOP(_context, arg) arg +#define QT_TRANSLATE_NOOP3(_context, arg, _comment) arg + +#endif // MYGETTEXT_H diff --git a/core/gettextfromc.cpp b/core/gettextfromc.cpp new file mode 100644 index 000000000..43ee8da50 --- /dev/null +++ b/core/gettextfromc.cpp @@ -0,0 +1,27 @@ +#include <QCoreApplication> +#include <QString> +#include "gettextfromc.h" + +const char *gettextFromC::trGettext(const char *text) +{ + QByteArray &result = translationCache[QByteArray(text)]; + if (result.isEmpty()) + result = translationCache[QByteArray(text)] = trUtf8(text).toUtf8(); + return result.constData(); +} + +void gettextFromC::reset(void) +{ + translationCache.clear(); +} + +gettextFromC *gettextFromC::instance() +{ + static QScopedPointer<gettextFromC> self(new gettextFromC()); + return self.data(); +} + +extern "C" const char *trGettext(const char *text) +{ + return gettextFromC::instance()->trGettext(text); +} diff --git a/core/gettextfromc.h b/core/gettextfromc.h new file mode 100644 index 000000000..53df18365 --- /dev/null +++ b/core/gettextfromc.h @@ -0,0 +1,18 @@ +#ifndef GETTEXTFROMC_H +#define GETTEXTFROMC_H + +#include <QHash> +#include <QCoreApplication> + +extern "C" const char *trGettext(const char *text); + +class gettextFromC { + Q_DECLARE_TR_FUNCTIONS(gettextFromC) +public: + static gettextFromC *instance(); + const char *trGettext(const char *text); + void reset(void); + QHash<QByteArray, QByteArray> translationCache; +}; + +#endif // GETTEXTFROMC_H diff --git a/core/git-access.c b/core/git-access.c new file mode 100644 index 000000000..d10139d3d --- /dev/null +++ b/core/git-access.c @@ -0,0 +1,929 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <time.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <git2.h> + +#include "dive.h" +#include "membuffer.h" +#include "strndup.h" +#include "qthelperfromc.h" +#include "git-access.h" +#include "gettext.h" + +bool is_subsurface_cloud = false; + +int (*update_progress_cb)(int, const char *) = NULL; + +void set_git_update_cb(int(*cb)(int, const char *)) +{ + update_progress_cb = cb; +} + +// total overkill, but this allows us to get good timing in various scenarios; +// the various parts of interacting with the local and remote git repositories send +// us updates which indicate progress (and no, this is not smooth and definitely not +// proportional - some parts are based on compute performance, some on network speed) +// they also provide information where in the process we are so we can analyze the log +// to understand which parts of the process take how much time. + +// last_git_storage_update_val is used to detect when we suddenly go back to smaller +// "percentage" value because we are back to executing earlier code a second (or third +// time) in that case a negative percentage value is sent to the callback function as a +// special case to mark that situation. Overall this ensures monotonous percentage values +int last_git_storage_update_val; + +int git_storage_update_progress(int percent, const char *text) +{ + static int delta = 0; + + if (percent == 0) { + delta = 0; + } else if (percent > 0 && percent < last_git_storage_update_val) { + delta = last_git_storage_update_val + delta; + if (update_progress_cb) + (*update_progress_cb)(-delta, "DELTA"); + if (verbose) + fprintf(stderr, "set git storage percentage delta to %d\n", delta); + } + + last_git_storage_update_val = percent; + + percent += delta; + + int ret = 0; + if (update_progress_cb) + ret = (*update_progress_cb)(percent, text); + return ret; +} + +// the checkout_progress_cb doesn't allow canceling of the operation +// map the git progress to 70..90% of overall progress +static void progress_cb(const char *path, size_t completed_steps, size_t total_steps, void *payload) +{ + (void) path; + (void) payload; + + int percent = 0; + if (total_steps) + percent = 70 + 20 * completed_steps / total_steps; + (void)git_storage_update_progress(percent, "checkout_progress_cb"); +} + +// this randomly assumes that 80% of the time is spent on the objects and 20% on the deltas +// map the git progress to 70..90% of overall progress +// if the user cancels the dialog this is passed back to libgit2 +static int transfer_progress_cb(const git_transfer_progress *stats, void *payload) +{ + (void) payload; + + int percent = 0; + if (stats->total_objects) + percent = 70 + 16 * stats->received_objects / stats->total_objects; + if (stats->total_deltas) + percent += 4 * stats->indexed_deltas / stats->total_deltas; + /* for debugging this is useful + char buf[100]; + snprintf(buf, 100, "transfer cb rec_obj %d tot_obj %d idx_delta %d total_delta %d local obj %d", stats->received_objects, stats->total_objects, stats->indexed_deltas, stats->total_deltas, stats->local_objects); + return git_storage_update_progress(percent, buf); + */ + return git_storage_update_progress(percent, "transfer cb"); +} + +// the initial push to sync the repos is mapped to 10..15% of overall progress +static int push_transfer_progress_cb(unsigned int current, unsigned int total, size_t bytes, void *payload) +{ + (void) bytes; + (void) payload; + + int percent = 0; + if (total != 0) + percent = 12 + 5 * current / total; + return git_storage_update_progress(percent, "push trasfer cb"); +} + +char *get_local_dir(const char *remote, const char *branch) +{ + SHA_CTX ctx; + unsigned char hash[20]; + + // That zero-byte update is so that we don't get hash + // collisions for "repo1 branch" vs "repo 1branch". + SHA1_Init(&ctx); + SHA1_Update(&ctx, remote, strlen(remote)); + SHA1_Update(&ctx, "", 1); + SHA1_Update(&ctx, branch, strlen(branch)); + SHA1_Final(hash, &ctx); + + return format_string("%s/cloudstorage/%02x%02x%02x%02x%02x%02x%02x%02x", + system_default_directory(), + hash[0], hash[1], hash[2], hash[3], + hash[4], hash[5], hash[6], hash[7]); +} + +static char *move_local_cache(const char *remote, const char *branch) +{ + char *old_path = get_local_dir(remote, branch); + return move_away(old_path); +} + +static int check_clean(const char *path, unsigned int status, void *payload) +{ + (void) payload; + status &= ~GIT_STATUS_CURRENT | GIT_STATUS_IGNORED; + if (!status) + return 0; + if (is_subsurface_cloud) + report_error(translate("gettextFromC", "Local cache directory %s corrupted - can't sync with Subsurface cloud storage"), path); + else + report_error("WARNING: Git cache directory modified (path %s) status %0x", path, status); + return 1; +} + +/* + * The remote is strictly newer than the local branch. + */ +static int reset_to_remote(git_repository *repo, git_reference *local, const git_oid *new_id) +{ + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + opts.progress_cb = &progress_cb; + git_object *target; + + if (verbose) + fprintf(stderr, "git storage: reset to remote\n"); + + // If it's not checked out (bare or not HEAD), just update the reference */ + if (git_repository_is_bare(repo) || git_branch_is_head(local) != 1) { + git_reference *out; + + if (git_reference_set_target(&out, local, new_id, "Update to remote")) + return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); + + git_reference_free(out); + +#ifdef DEBUG + // Not really an error, just informational + report_error("Updated local branch from remote"); +#endif + return 0; + } + + if (git_object_lookup(&target, repo, new_id, GIT_OBJ_COMMIT)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Subsurface cloud storage corrupted")); + else + return report_error("Could not look up remote commit"); + } + opts.checkout_strategy = GIT_CHECKOUT_SAFE; + if (git_reset(repo, target, GIT_RESET_HARD, &opts)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); + else + return report_error("Local head checkout failed after update"); + } + // Not really an error, just informational +#ifdef DEBUG + report_error("Updated local information from remote"); +#endif + return 0; +} + +int credential_ssh_cb(git_cred **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + (void) url; + (void) allowed_types; + (void) payload; + + const char *priv_key = format_string("%s/%s", system_default_directory(), "ssrf_remote.key"); + const char *passphrase = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); + return git_cred_ssh_key_new(out, username_from_url, NULL, priv_key, passphrase); +} + +int credential_https_cb(git_cred **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + (void) url; + (void) username_from_url; + (void) payload; + (void) allowed_types; + const char *username = prefs.cloud_storage_email_encoded; + const char *password = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); + return git_cred_userpass_plaintext_new(out, username, password); +} + +#define KNOWN_CERT "\xfd\xb8\xf7\x73\x76\xe2\x75\x53\x93\x37\xdc\xfe\x1e\x55\x43\x3d\xf2\x2c\x18\x2c" +int certificate_check_cb(git_cert *cert, int valid, const char *host, void *payload) +{ + (void) payload; + if (same_string(host, "cloud.subsurface-divelog.org") && cert->cert_type == GIT_CERT_X509) { + SHA_CTX ctx; + unsigned char hash[21]; + git_cert_x509 *cert509 = (git_cert_x509 *)cert; + SHA1_Init(&ctx); + SHA1_Update(&ctx, cert509->data, cert509->len); + SHA1_Final(hash, &ctx); + hash[20] = 0; + if (verbose > 1) + if (same_string((char *)hash, KNOWN_CERT)) { + fprintf(stderr, "cloud certificate considered %s, forcing it valid\n", + valid ? "valid" : "not valid"); + return 1; + } + } + return valid; +} + +static int update_remote(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, enum remote_transport rt) +{ + (void) repo; + (void) remote; + + git_push_options opts = GIT_PUSH_OPTIONS_INIT; + git_strarray refspec; + const char *name = git_reference_name(local); + + if (verbose) + fprintf(stderr, "git storage: update remote\n"); + + refspec.count = 1; + refspec.strings = (char **)&name; + + opts.callbacks.push_transfer_progress = &push_transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; + + if (git_remote_push(origin, &refspec, &opts)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Could not update Subsurface cloud storage, try again later")); + else + return report_error("Unable to update remote with current local cache state (%s)", giterr_last()->message); + } + return 0; +} + +extern int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree); + +static int try_to_git_merge(git_repository *repo, git_reference **local_p, git_reference *remote, git_oid *base, const git_oid *local_id, const git_oid *remote_id) +{ + (void) remote; + git_tree *local_tree, *remote_tree, *base_tree; + git_commit *local_commit, *remote_commit, *base_commit; + git_index *merged_index; + git_merge_options merge_options; + + if (verbose) { + char outlocal[41], outremote[41]; + outlocal[40] = outremote[40] = 0; + git_oid_fmt(outlocal, local_id); + git_oid_fmt(outremote, remote_id); + fprintf(stderr, "trying to merge local SHA %s remote SHA %s\n", outlocal, outremote); + } + + git_merge_init_options(&merge_options, GIT_MERGE_OPTIONS_VERSION); + merge_options.tree_flags = GIT_MERGE_TREE_FIND_RENAMES; + merge_options.file_favor = GIT_MERGE_FILE_FAVOR_UNION; + merge_options.rename_threshold = 100; + if (git_commit_lookup(&local_commit, repo, local_id)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&local_tree, local_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_lookup(&remote_commit, repo, remote_id)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&remote_tree, remote_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_lookup(&base_commit, repo, base)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&base_tree, base_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed base tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_merge_trees(&merged_index, repo, base_tree, local_tree, remote_tree, &merge_options)) { + fprintf(stderr, "Remote storage and local data diverged. Error: merge failed (%s)", giterr_last()->message); + // this is the one where I want to report more detail to the user - can't quite explain why + return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: merge failed (%s)"), giterr_last()->message); + } + if (git_index_has_conflicts(merged_index)) { + int error; + const git_index_entry *ancestor = NULL, + *ours = NULL, + *theirs = NULL; + git_index_conflict_iterator *iter = NULL; + error = git_index_conflict_iterator_new(&iter, merged_index); + while (git_index_conflict_next(&ancestor, &ours, &theirs, iter) + != GIT_ITEROVER) { + /* Mark this conflict as resolved */ + fprintf(stderr, "conflict in %s / %s / %s -- ", + ours ? ours->path : "-", + theirs ? theirs->path : "-", + ancestor ? ancestor->path : "-"); + if ((!ours && theirs && ancestor) || + (ours && !theirs && ancestor)) { + // the file was removed on one side or the other - just remove it + fprintf(stderr, "looks like a delete on one side; removing the file from the index\n"); + error = git_index_remove(merged_index, ours ? ours->path : theirs->path, GIT_INDEX_STAGE_ANY); + } else if (ancestor) { + error = git_index_conflict_remove(merged_index, ours ? ours->path : theirs ? theirs->path : ancestor->path); + } + if (error) { + fprintf(stderr, "error at conflict resplution (%s)", giterr_last()->message); + } + } + git_index_conflict_cleanup(merged_index); + git_index_conflict_iterator_free(iter); + report_error(translate("gettextFromC", "Remote storage and local data diverged. Cannot combine local and remote changes")); + } + git_oid merge_oid, commit_oid; + git_tree *merged_tree; + git_signature *author; + git_commit *commit; + + if (git_index_write_tree_to(&merge_oid, merged_index, repo)) + goto write_error; + if (git_tree_lookup(&merged_tree, repo, &merge_oid)) + goto write_error; + if (git_signature_default(&author, repo) < 0) + if (git_signature_now(&author, "Subsurface", "noemail@given") < 0) + goto write_error; + if (git_commit_create_v(&commit_oid, repo, NULL, author, author, NULL, "automatic merge", merged_tree, 2, local_commit, remote_commit)) + goto write_error; + if (git_commit_lookup(&commit, repo, &commit_oid)) + goto write_error; + if (git_branch_is_head(*local_p) && !git_repository_is_bare(repo)) { + git_object *parent; + git_reference_peel(&parent, *local_p, GIT_OBJ_COMMIT); + if (update_git_checkout(repo, parent, merged_tree)) { + goto write_error; + } + } + if (git_reference_set_target(local_p, *local_p, &commit_oid, "Subsurface merge event")) + goto write_error; + set_git_id(&commit_oid); + git_signature_free(author); + if (verbose) + fprintf(stderr, "Successfully merged repositories"); + return 0; + +diverged_error: + return report_error(translate("gettextFromC", "Remote storage and local data diverged")); + +write_error: + return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: writing the data failed (%s)"), giterr_last()->message); +} + +// if accessing the local cache of Subsurface cloud storage fails, we simplify things +// for the user and simply move the cache away (in case they want to try and extract data) +// and ask them to retry the operation (which will then refresh the data from the cloud server) +static int cleanup_local_cache(const char *remote_url, const char *branch) +{ + char *backup_path = move_local_cache(remote_url, branch); + report_error(translate("gettextFromC", "Problems with local cache of Subsurface cloud data")); + report_error(translate("gettextFromC", "Moved cache data to %s. Please try the operation again."), backup_path); + free(backup_path); + return -1; +} +static int try_to_update(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, + const char *remote_url, const char *branch, enum remote_transport rt) +{ + git_oid base; + const git_oid *local_id, *remote_id; + int ret = 0; + + if (verbose) + fprintf(stderr, "git storage: try to update\n"); + git_storage_update_progress(9, "try to update"); + if (!git_reference_cmp(local, remote)) + return 0; + + // Dirty modified state in the working tree? We're not going + // to update either way + if (git_status_foreach(repo, check_clean, NULL)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("local cached copy is dirty, skipping update"); + } + local_id = git_reference_target(local); + remote_id = git_reference_target(remote); + + if (!local_id || !remote_id) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Unable to get local or remote SHA1"); + } + if (git_merge_base(&base, repo, local_id, remote_id)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Unable to find common commit of local and remote branches"); + } + /* Is the remote strictly newer? Use it */ + if (git_oid_equal(&base, local_id)) + return reset_to_remote(repo, local, remote_id); + + /* Is the local repo the more recent one? See if we can update upstream */ + if (git_oid_equal(&base, remote_id)) { + if (verbose) + fprintf(stderr, "local is newer than remote, update remote\n"); + git_storage_update_progress(10, "git_update_remote, local was newer"); + return update_remote(repo, origin, local, remote, rt); + } + /* Merging a bare repository always needs user action */ + if (git_repository_is_bare(repo)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Local and remote have diverged, merge of bare branch needed"); + } + /* Merging will definitely need the head branch too */ + if (git_branch_is_head(local) != 1) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Local and remote do not match, local branch not HEAD - cannot update"); + } + /* Ok, let's try to merge these */ + git_storage_update_progress(11, "try to merge"); + ret = try_to_git_merge(repo, &local, remote, &base, local_id, remote_id); + if (ret == 0) + return update_remote(repo, origin, local, remote, rt); + else + return ret; + +cloud_data_error: + // since we are working with Subsurface cloud storage we want to make the user interaction + // as painless as possible. So if something went wrong with the local cache, tell the user + // about it an move it away + return cleanup_local_cache(remote_url, branch); +} + +static int check_remote_status(git_repository *repo, git_remote *origin, const char *remote, const char *branch, enum remote_transport rt) +{ + int error = 0; + + git_reference *local_ref, *remote_ref; + + if (verbose) + fprintf(stderr, "git storage: check remote status\n"); + git_storage_update_progress(7, "git check remote status"); + + if (git_branch_lookup(&local_ref, repo, branch, GIT_BRANCH_LOCAL)) { + if (is_subsurface_cloud) + return cleanup_local_cache(remote, branch); + else + return report_error("Git cache branch %s no longer exists", branch); + } + if (git_branch_upstream(&remote_ref, local_ref)) { + /* so there is no upstream branch for our branch; that's a problem. + * let's push our branch */ + git_strarray refspec; + git_reference_list(&refspec, repo); + git_push_options opts = GIT_PUSH_OPTIONS_INIT; + opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; + git_storage_update_progress(8, "git remote push (no remote existed)"); + error = git_remote_push(origin, &refspec, &opts); + } else { + error = try_to_update(repo, origin, local_ref, remote_ref, remote, branch, rt); + git_reference_free(remote_ref); + } + git_reference_free(local_ref); + return error; +} + +int sync_with_remote(git_repository *repo, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_remote *origin; + char *proxy_string; + git_config *conf; + + if (prefs.git_local_only) { + if (verbose) + fprintf(stderr, "don't sync with remote - read from cache only\n"); + return 0; + } + if (verbose) + fprintf(stderr, "sync with remote %s[%s]\n", remote, branch); + git_storage_update_progress(2, "sync with remote"); + git_repository_config(&conf, repo); + if (rt == RT_HTTPS && getProxyString(&proxy_string)) { + if (verbose) + fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); + git_config_set_string(conf, "http.proxy", proxy_string); + free(proxy_string); + } else { + if (verbose) + fprintf(stderr, "delete proxy setting\n"); + git_config_delete_entry(conf, "http.proxy"); + } + + /* + * NOTE! Remote errors are reported, but are nonfatal: + * we still successfully return the local repository. + */ + error = git_remote_lookup(&origin, repo, "origin"); + if (error) { + if (!is_subsurface_cloud) + report_error("Repository '%s' origin lookup failed (%s)", remote, giterr_last()->message); + return 0; + } + + if (rt == RT_HTTPS && !canReachCloudServer()) { + // this is not an error, just a warning message, so return 0 + report_error("Cannot connect to cloud server, working with local copy"); + git_storage_update_progress(18, "can't reach cloud server, working with local copy"); + return 0; + } + if (verbose) + fprintf(stderr, "git storage: fetch remote\n"); + git_fetch_options opts = GIT_FETCH_OPTIONS_INIT; + opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; + git_storage_update_progress(6, "git fetch remote"); + error = git_remote_fetch(origin, NULL, &opts, NULL); + // NOTE! A fetch error is not fatal, we just report it + if (error) { + if (is_subsurface_cloud) + report_error("Cannot sync with cloud server, working with offline copy"); + else + report_error("Unable to fetch remote '%s'", remote); + if (verbose) + fprintf(stderr, "remote fetch failed (%s)\n", giterr_last()->message); + error = 0; + } else { + error = check_remote_status(repo, origin, remote, branch, rt); + } + git_remote_free(origin); + git_storage_update_progress(18, "done with sync with remote"); + return error; +} + +static git_repository *update_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_repository *repo = NULL; + + if (verbose) + fprintf(stderr, "git storage: update local repo\n"); + + error = git_repository_open(&repo, localdir); + if (error) { + if (is_subsurface_cloud) + (void)cleanup_local_cache(remote, branch); + else + report_error("Unable to open git cache repository at %s: %s", localdir, giterr_last()->message); + return NULL; + } + sync_with_remote(repo, remote, branch, rt); + return repo; +} + +static int repository_create_cb(git_repository **out, const char *path, int bare, void *payload) +{ + (void) payload; + char *proxy_string; + git_config *conf; + + int ret = git_repository_init(out, path, bare); + + git_repository_config(&conf, *out); + if (getProxyString(&proxy_string)) { + if (verbose) + fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); + git_config_set_string(conf, "http.proxy", proxy_string); + free(proxy_string); + } else { + if (verbose) + fprintf(stderr, "delete proxy setting\n"); + git_config_delete_entry(conf, "http.proxy"); + } + return ret; +} + +/* this should correctly initialize both the local and remote + * repository for the Subsurface cloud storage */ +static git_repository *create_and_push_remote(const char *localdir, const char *remote, const char *branch) +{ + git_repository *repo; + git_config *conf; + int len; + char *variable_name, *merge_head; + + if (verbose) + fprintf(stderr, "git storage: create and push remote\n"); + + /* first make sure the directory for the local cache exists */ + subsurface_mkdir(localdir); + + /* set up the origin to point to our remote */ + git_repository_init_options init_opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; + init_opts.origin_url = remote; + + /* now initialize the repository with */ + git_repository_init_ext(&repo, localdir, &init_opts); + + /* create a config so we can set the remote tracking branch */ + git_repository_config(&conf, repo); + len = sizeof("branch..remote") + strlen(branch); + variable_name = malloc(len); + snprintf(variable_name, len, "branch.%s.remote", branch); + git_config_set_string(conf, variable_name, "origin"); + /* we know this is shorter than the previous one, so we reuse the variable*/ + snprintf(variable_name, len, "branch.%s.merge", branch); + len = sizeof("refs/heads/") + strlen(branch); + merge_head = malloc(len); + snprintf(merge_head, len, "refs/heads/%s", branch); + git_config_set_string(conf, variable_name, merge_head); + + /* finally create an empty commit and push it to the remote */ + if (do_git_save(repo, branch, remote, false, true)) + return NULL; + return(repo); +} + +static git_repository *create_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_repository *cloned_repo = NULL; + git_clone_options opts = GIT_CLONE_OPTIONS_INIT; + + if (verbose) + fprintf(stderr, "git storage: create_local_repo\n"); + + opts.fetch_opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.fetch_opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.fetch_opts.callbacks.credentials = credential_https_cb; + opts.repository_cb = repository_create_cb; + opts.fetch_opts.callbacks.certificate_check = certificate_check_cb; + + opts.checkout_branch = branch; + if (rt == RT_HTTPS && !canReachCloudServer()) + return 0; + if (verbose > 1) + fprintf(stderr, "git storage: calling git_clone()\n"); + error = git_clone(&cloned_repo, remote, localdir, &opts); + if (verbose > 1) + fprintf(stderr, "git storage: returned from git_clone() with error %d\n", error); + if (error) { + char *msg = giterr_last()->message; + int len = sizeof("Reference 'refs/remotes/origin/' not found") + strlen(branch); + char *pattern = malloc(len); + snprintf(pattern, len, "Reference 'refs/remotes/origin/%s' not found", branch); + if (strstr(remote, prefs.cloud_git_url) && strstr(msg, pattern)) { + /* we're trying to open the remote branch that corresponds + * to our cloud storage and the branch doesn't exist. + * So we need to create the branch and push it to the remote */ + cloned_repo = create_and_push_remote(localdir, remote, branch); +#if !defined(DEBUG) && !defined(SUBSURFACE_MOBILE) + } else if (is_subsurface_cloud) { + report_error(translate("gettextFromC", "Error connecting to Subsurface cloud storage")); +#endif + } else { + report_error(translate("gettextFromC", "git clone of %s failed (%s)"), remote, msg); + } + free(pattern); + } + return cloned_repo; +} + +static struct git_repository *get_remote_repo(const char *localdir, const char *remote, const char *branch) +{ + struct stat st; + enum remote_transport rt; + + /* figure out the remote transport */ + if (strncmp(remote, "ssh://", 6) == 0) + rt = RT_SSH; + else if (strncmp(remote, "https://", 8) == 0) + rt = RT_HTTPS; + else + rt = RT_OTHER; + + if (verbose > 1) { + fprintf(stderr, "git_remote_repo: accessing %s\n", remote); + } + git_storage_update_progress(1, "start git interaction"); + /* Do we already have a local cache? */ + if (!stat(localdir, &st)) { + if (!S_ISDIR(st.st_mode)) { + if (is_subsurface_cloud) + (void)cleanup_local_cache(remote, branch); + else + report_error("local git cache at '%s' is corrupt"); + return NULL; + } + return update_local_repo(localdir, remote, branch, rt); + } + if (!prefs.git_local_only) + return create_local_repo(localdir, remote, branch, rt); + else + return 0; + +} + +/* + * This turns a remote repository into a local one if possible. + * + * The recognized formats are + * git://host/repo[branch] + * ssh://host/repo[branch] + * http://host/repo[branch] + * https://host/repo[branch] + * file://repo[branch] + */ +static struct git_repository *is_remote_git_repository(char *remote, const char *branch) +{ + char c, *localdir; + const char *p = remote; + + while ((c = *p++) >= 'a' && c <= 'z') + /* nothing */; + if (c != ':') + return NULL; + if (*p++ != '/' || *p++ != '/') + return NULL; + + /* Special-case "file://", since it's already local */ + if (!strncmp(remote, "file://", 7)) + remote += 7; + + /* + * Ok, we found "[a-z]*://", we've simplified the + * local repo case (because libgit2 is insanely slow + * for that), and we think we have a real "remote + * git" format. + * + * We now create the SHA1 hash of the whole thing, + * including the branch name. That will be our unique + * unique local repository name. + * + * NOTE! We will create a local repository per branch, + * because + * + * (a) libgit2 remote tracking branch support seems to + * be a bit lacking + * (b) we'll actually check the branch out so that we + * can do merges etc too. + * + * so even if you have a single remote git repo with + * multiple branches for different people, the local + * caches will sadly force that to split into multiple + * individual repositories. + */ + + /* + * next we need to make sure that any encoded username + * has been extracted from an https:// based URL + */ + if (!strncmp(remote, "https://", 8)) { + char *at = strchr(remote, '@'); + if (at) { + /* was this the @ that denotes an account? that means it was before the + * first '/' after the https:// - so let's find a '/' after that and compare */ + char *slash = strchr(remote + 8, '/'); + if (slash && slash > at) { + /* grab the part between "https://" and "@" as encoded email address + * (that's our username) and move the rest of the URL forward, remembering + * to copy the closing NUL as well */ + prefs.cloud_storage_email_encoded = strndup(remote + 8, at - remote - 8); + memmove(remote + 8, at + 1, strlen(at + 1) + 1); + } + } + } + localdir = get_local_dir(remote, branch); + if (!localdir) + return NULL; + + /* remember if the current git storage we are working on is our cloud storage + * this is used to create more user friendly error message and warnings */ + is_subsurface_cloud = strstr(remote, prefs.cloud_git_url) != NULL; + + return get_remote_repo(localdir, remote, branch); +} + +/* + * If it's not a git repo, return NULL. Be very conservative. + */ +struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run) +{ + int flen, blen, ret; + int offset = 1; + struct stat st; + git_repository *repo; + char *loc, *branch; + + flen = strlen(filename); + if (!flen || filename[--flen] != ']') + return NULL; + + /* Find the matching '[' */ + blen = 0; + while (flen && filename[--flen] != '[') + blen++; + + /* Ignore slashes at the end of the repo name */ + while (flen && filename[flen-1] == '/') { + flen--; + offset++; + } + + if (!flen) + return NULL; + + /* + * This is the "point of no return": the name matches + * the git repository name rules, and we will no longer + * return NULL. + * + * We will either return "dummy_git_repository" and the + * branch pointer will have the _whole_ filename in it, + * or we will return a real git repository with the + * branch pointer being filled in with just the branch + * name. + * + * The actual git reading/writing routines can use this + * to generate proper error messages. + */ + *branchp = filename; + loc = format_string("%.*s", flen, filename); + if (!loc) + return dummy_git_repository; + + branch = format_string("%.*s", blen, filename + flen + offset); + if (!branch) { + free(loc); + return dummy_git_repository; + } + + if (dry_run) { + *branchp = branch; + *remote = loc; + return dummy_git_repository; + } + repo = is_remote_git_repository(loc, branch); + if (repo) { + if (remote) + *remote = loc; + else + free(loc); + *branchp = branch; + return repo; + } + + if (stat(loc, &st) < 0 || !S_ISDIR(st.st_mode)) { + free(loc); + free(branch); + return dummy_git_repository; + } + + ret = git_repository_open(&repo, loc); + free(loc); + if (ret < 0) { + free(branch); + return dummy_git_repository; + } + if (remote) + *remote = NULL; + *branchp = branch; + return repo; +} diff --git a/core/git-access.h b/core/git-access.h new file mode 100644 index 000000000..3a0c5160a --- /dev/null +++ b/core/git-access.h @@ -0,0 +1,36 @@ +#ifndef GITACCESS_H +#define GITACCESS_H + +#include "git2.h" + +#ifdef __cplusplus +extern "C" { +#else +#include <stdbool.h> +#endif + +enum remote_transport { RT_OTHER, RT_HTTPS, RT_SSH }; + +struct git_oid; +struct git_repository; +#define dummy_git_repository ((git_repository *)3ul) /* Random bogus pointer, not NULL */ +extern struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run); +extern int sync_with_remote(struct git_repository *repo, const char *remote, const char *branch, enum remote_transport rt); +extern int git_save_dives(struct git_repository *, const char *, const char *remote, bool select_only); +extern int git_load_dives(struct git_repository *, const char *); +extern const char *get_sha(git_repository *repo, const char *branch); +extern int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty); +extern const char *saved_git_id; +extern void clear_git_id(void); +extern void set_git_id(const struct git_oid *); +void set_git_update_cb(int (*)(int, const char *)); +int git_storage_update_progress(int percent, const char *text); +char *get_local_dir(const char *remote, const char *branch); + +extern int last_git_storage_update_val; + +#ifdef __cplusplus +} +#endif +#endif // GITACCESS_H + diff --git a/core/gpslocation.cpp b/core/gpslocation.cpp new file mode 100644 index 000000000..8595cc45d --- /dev/null +++ b/core/gpslocation.cpp @@ -0,0 +1,606 @@ +#include "core/gpslocation.h" +#include "qt-models/gpslistmodel.h" +#include "core/pref.h" +#include "core/dive.h" +#include "core/helpers.h" +#include <time.h> +#include <unistd.h> +#include <QDebug> +#include <QVariant> +#include <QUrlQuery> +#include <QApplication> +#include <QTimer> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> + +#define GPS_FIX_ADD_URL "http://api.subsurface-divelog.org/api/dive/add/" +#define GPS_FIX_DELETE_URL "http://api.subsurface-divelog.org/api/dive/delete/" +#define GPS_FIX_DOWNLOAD_URL "http://api.subsurface-divelog.org/api/dive/get/" +#define GET_WEBSERVICE_UID_URL "https://cloud.subsurface-divelog.org/webuserid/" + +GpsLocation *GpsLocation::m_Instance = NULL; + +GpsLocation::GpsLocation(void (*showMsgCB)(const char *), QObject *parent) : QObject(parent) +{ + Q_ASSERT_X(m_Instance == NULL, "GpsLocation", "GpsLocation recreated"); + m_Instance = this; + m_GpsSource = 0; + waitingForPosition = false; + showMessageCB = showMsgCB; + // create a QSettings object that's separate from the main application settings + geoSettings = new QSettings(QSettings::NativeFormat, QSettings::UserScope, + QString("org.subsurfacedivelog"), QString("subsurfacelocation"), this); +#ifdef SUBSURFACE_MOBILE + if (hasLocationsSource()) + status(QString("Found GPS with positioning methods %1").arg(QString::number(m_GpsSource->supportedPositioningMethods(), 16))); +#endif + userAgent = getUserAgent(); + loadFromStorage(); +} + +GpsLocation *GpsLocation::instance() +{ + Q_ASSERT(m_Instance != NULL); + + return m_Instance; +} + +GpsLocation::~GpsLocation() +{ + m_Instance = NULL; +} + +QGeoPositionInfoSource *GpsLocation::getGpsSource() +{ + if (!m_GpsSource) { + m_GpsSource = QGeoPositionInfoSource::createDefaultSource(this); + if (m_GpsSource != 0) { +#ifndef SUBSURFACE_MOBILE + if (verbose) +#endif + status(QString("Created position source %1").arg(m_GpsSource->sourceName())); + connect(m_GpsSource, SIGNAL(positionUpdated(QGeoPositionInfo)), this, SLOT(newPosition(QGeoPositionInfo))); + connect(m_GpsSource, SIGNAL(updateTimeout()), this, SLOT(updateTimeout())); + m_GpsSource->setUpdateInterval(5 * 60 * 1000); // 5 minutes so the device doesn't drain the battery + } else { +#ifdef SUBSURFACE_MOBILE + status("don't have GPS source"); +#endif + } + } + return m_GpsSource; +} + +bool GpsLocation::hasLocationsSource() +{ + QGeoPositionInfoSource *gpsSource = getGpsSource(); + return gpsSource != 0 && (gpsSource->supportedPositioningMethods() & QGeoPositionInfoSource::SatellitePositioningMethods); +} + +void GpsLocation::serviceEnable(bool toggle) +{ + QGeoPositionInfoSource *gpsSource = getGpsSource(); + if (!gpsSource) { + if (toggle) + status("Can't start location service, no location source available"); + return; + } + if (toggle) { + gpsSource->startUpdates(); + status(QString("Starting Subsurface GPS service with update interval %1").arg(gpsSource->updateInterval())); + } else { + gpsSource->stopUpdates(); + status("Stopping Subsurface GPS service"); + } +} + +QString GpsLocation::currentPosition() +{ + if (!hasLocationsSource()) + return tr("Unknown GPS location"); + if (m_trackers.count() && m_trackers.lastKey() + 300 >= QDateTime::currentMSecsSinceEpoch() / 1000) { + // we can simply use the last position that we tracked + gpsTracker gt = m_trackers.last(); + QString gpsString = printGPSCoords(gt.latitude.udeg, gt.longitude.udeg); + qDebug() << "returning last position" << gpsString; + return gpsString; + } + qDebug() << "requesting new GPS position"; + m_GpsSource->requestUpdate(); + // ok, we need to get the current position and somehow in the callback update the location in the QML UI + // punting right now + waitingForPosition = true; + return QString("waiting for the next gps location"); +} + +void GpsLocation::newPosition(QGeoPositionInfo pos) +{ + int64_t lastTime = 0; + QGeoCoordinate lastCoord; + int nr = m_trackers.count(); + if (nr) { + gpsTracker gt = m_trackers.last(); + lastCoord.setLatitude(gt.latitude.udeg / 1000000.0); + lastCoord.setLongitude(gt.longitude.udeg / 1000000.0); + lastTime = gt.when; + } + // if we are waiting for a position update or + // if we have no record stored or if at least the configured minimum + // time has passed or we moved at least the configured minimum distance + int64_t delta = (int64_t)pos.timestamp().toTime_t() + gettimezoneoffset() - lastTime; + if (!nr || waitingForPosition || delta > prefs.time_threshold || + lastCoord.distanceTo(pos.coordinate()) > prefs.distance_threshold) { + QString msg("received new position %1 after delta %2 threshold %3"); + status(qPrintable(msg.arg(pos.coordinate().toString()).arg(delta).arg(prefs.time_threshold))); + waitingForPosition = false; + gpsTracker gt; + gt.when = pos.timestamp().toTime_t(); + gt.when += gettimezoneoffset(gt.when); + gt.latitude.udeg = rint(pos.coordinate().latitude() * 1000000); + gt.longitude.udeg = rint(pos.coordinate().longitude() * 1000000); + addFixToStorage(gt); + } +} + +void GpsLocation::updateTimeout() +{ + status("request to get new position timed out"); +} + +void GpsLocation::status(QString msg) +{ + qDebug() << msg; + if (showMessageCB) + (*showMessageCB)(qPrintable(msg)); +} + +QString GpsLocation::getUserid(QString user, QString passwd) +{ + qDebug() << "called getUserid"; + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + + QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + QUrl url(GET_WEBSERVICE_UID_URL); + QString data; + data = user + " " + passwd; + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Accept", "text/html"); + request.setRawHeader("Content-type", "application/x-www-form-urlencoded"); + reply = manager->post(request, data.toUtf8()); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(getUseridError(QNetworkReply::NetworkError))); + timer.start(10000); + loop.exec(); + if (timer.isActive()) { + timer.stop(); + if (reply->error() == QNetworkReply::NoError) { + QString result = reply->readAll(); + status(QString("received ") + result); + result.remove("WebserviceID:"); + reply->deleteLater(); + return result; + } + } else { + status("Getting Web service ID timed out"); + } + reply->deleteLater(); + return QString(); +} + +int GpsLocation::getGpsNum() const +{ + return m_trackers.count(); +} + +static void copy_gps_location(struct gpsTracker &gps, struct dive *d) +{ + struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid); + if (!ds) { + d->dive_site_uuid = create_dive_site(qPrintable(gps.name), gps.when); + ds = get_dive_site_by_uuid(d->dive_site_uuid); + } + ds->latitude = gps.latitude; + ds->longitude = gps.longitude; +} + +#define SAME_GROUP 6 * 3600 /* six hours */ +bool GpsLocation::applyLocations() +{ + int i; + bool changed = false; + int last = 0; + int cnt = m_trackers.count(); + if (cnt == 0) + return false; + + // create a table with the GPS information + QList<struct gpsTracker> gpsTable = m_trackers.values(); + + // now walk the dive table and see if we can fill in missing gps data + struct dive *d; + for_each_dive(i, d) { + if (dive_has_gps_location(d)) + continue; + for (int j = last; j < cnt; j++) { + if (time_during_dive_with_offset(d, gpsTable[j].when, SAME_GROUP)) { + if (verbose) + qDebug() << "processing gpsFix @" << get_dive_date_string(gpsTable[j].when) << + "which is withing six hours of dive from" << + get_dive_date_string(d->when) << "until" << + get_dive_date_string(d->when + d->duration.seconds); + /* + * If position is fixed during dive. This is the good one. + * Asign and mark position, and end gps_location loop + */ + if (time_during_dive_with_offset(d, gpsTable[j].when, 0)) { + if (verbose) + qDebug() << "gpsFix is during the dive, pick that one"; + copy_gps_location(gpsTable[j], d); + changed = true; + last = j; + break; + } else { + /* + * If it is not, check if there are more position fixes in SAME_GROUP range + */ + if (j + 1 < cnt && time_during_dive_with_offset(d, gpsTable[j+1].when, SAME_GROUP)) { + if (verbose) + qDebug() << "look at the next gps fix @" << get_dive_date_string(gpsTable[j+1].when); + /* first let's test if this one is during the dive */ + if (time_during_dive_with_offset(d, gpsTable[j+1].when, 0)) { + if (verbose) + qDebug() << "which is during the dive, pick that one"; + copy_gps_location(gpsTable[j+1], d); + changed = true; + last = j + 1; + break; + } + /* we know the gps fixes are sorted; if they are both before the dive, ignore the first, + * if theay are both after the dive, take the first, + * if the first is before and the second is after, take the closer one */ + if (gpsTable[j+1].when < d->when) { + if (verbose) + qDebug() << "which is closer to the start of the dive, do continue with that"; + continue; + } else if (gpsTable[j].when > d->when + d->duration.seconds) { + if (verbose) + qDebug() << "which is even later after the end of the dive, so pick the previous one"; + copy_gps_location(gpsTable[j], d); + changed = true; + last = j; + break; + } else { + /* ok, gpsFix is before, nextgpsFix is after */ + if (d->when - gpsTable[j].when <= gpsTable[j+1].when - (d->when + d->duration.seconds)) { + if (verbose) + qDebug() << "pick the one before as it's closer to the start"; + copy_gps_location(gpsTable[j], d); + changed = true; + last = j; + break; + } else { + if (verbose) + qDebug() << "pick the one after as it's closer to the start"; + copy_gps_location(gpsTable[j+1], d); + changed = true; + last = j + 1; + break; + } + } + /* + * If no more positions in range, the actual is the one. Asign, mark and end loop. + */ + } else { + if (verbose) + qDebug() << "which seems to be the best one for this dive, so pick it"; + copy_gps_location(gpsTable[j], d); + changed = true; + last = j; + break; + } + } + } else { + /* If position is out of SAME_GROUP range and in the future, mark position for + * next dive iteration and end the gps_location loop + */ + if (gpsTable[j].when >= d->when + d->duration.seconds + SAME_GROUP) { + last = j; + break; + } + } + + } + } + if (changed) + mark_divelist_changed(true); + return changed; +} + +QMap<qint64, gpsTracker> GpsLocation::currentGPSInfo() const +{ + return m_trackers; +} + +void GpsLocation::loadFromStorage() +{ + int nr = geoSettings->value(QString("count")).toInt(); + for (int i = 0; i < nr; i++) { + struct gpsTracker gt; + gt.when = geoSettings->value(QString("gpsFix%1_time").arg(i)).toLongLong(); + gt.latitude.udeg = geoSettings->value(QString("gpsFix%1_lat").arg(i)).toInt(); + gt.longitude.udeg = geoSettings->value(QString("gpsFix%1_lon").arg(i)).toInt(); + gt.name = geoSettings->value(QString("gpsFix%1_name").arg(i)).toString(); + gt.idx = i; + m_trackers.insert(gt.when, gt); + } +} + +void GpsLocation::replaceFixToStorage(gpsTracker >) +{ + if (!m_trackers.keys().contains(gt.when)) { + addFixToStorage(gt); + return; + } + gpsTracker replacedTracker = m_trackers.value(gt.when); + geoSettings->setValue(QString("gpsFix%1_time").arg(replacedTracker.idx), gt.when); + geoSettings->setValue(QString("gpsFix%1_lat").arg(replacedTracker.idx), gt.latitude.udeg); + geoSettings->setValue(QString("gpsFix%1_lon").arg(replacedTracker.idx), gt.longitude.udeg); + geoSettings->setValue(QString("gpsFix%1_name").arg(replacedTracker.idx), gt.name); + replacedTracker.latitude = gt.latitude; + replacedTracker.longitude = gt.longitude; + replacedTracker.name = gt.name; +} + +void GpsLocation::addFixToStorage(gpsTracker >) +{ + int nr = m_trackers.count(); + geoSettings->setValue("count", nr + 1); + geoSettings->setValue(QString("gpsFix%1_time").arg(nr), gt.when); + geoSettings->setValue(QString("gpsFix%1_lat").arg(nr), gt.latitude.udeg); + geoSettings->setValue(QString("gpsFix%1_lon").arg(nr), gt.longitude.udeg); + geoSettings->setValue(QString("gpsFix%1_name").arg(nr), gt.name); + gt.idx = nr; + geoSettings->sync(); + m_trackers.insert(gt.when, gt); +} + +void GpsLocation::deleteFixFromStorage(gpsTracker >) +{ + qint64 when = gt.when; + int cnt = m_trackers.count(); + if (cnt == 0 || !m_trackers.keys().contains(when)) { + qDebug() << "no gps fix with timestamp" << when; + return; + } + if (geoSettings->value(QString("gpsFix%1_time").arg(gt.idx)).toLongLong() != when) { + qDebug() << "uh oh - index for tracker has gotten out of sync..."; + return; + } + if (cnt > 1) { + // now we move the last tracker into that spot (so our settings stay consecutive) + // and delete the last settings entry + when = geoSettings->value(QString("gpsFix%1_time").arg(cnt - 1)).toLongLong(); + struct gpsTracker movedTracker = m_trackers.value(when); + movedTracker.idx = gt.idx; + m_trackers.remove(movedTracker.when); + m_trackers.insert(movedTracker.when, movedTracker); + geoSettings->setValue(QString("gpsFix%1_time").arg(movedTracker.idx), when); + geoSettings->setValue(QString("gpsFix%1_lat").arg(movedTracker.idx), movedTracker.latitude.udeg); + geoSettings->setValue(QString("gpsFix%1_lon").arg(movedTracker.idx), movedTracker.longitude.udeg); + geoSettings->setValue(QString("gpsFix%1_name").arg(movedTracker.idx), movedTracker.name); + geoSettings->remove(QString("gpsFix%1_lat").arg(cnt - 1)); + geoSettings->remove(QString("gpsFix%1_lon").arg(cnt - 1)); + geoSettings->remove(QString("gpsFix%1_time").arg(cnt - 1)); + geoSettings->remove(QString("gpsFix%1_name").arg(cnt - 1)); + } + geoSettings->setValue("count", cnt - 1); + geoSettings->sync(); + m_trackers.remove(gt.when); +} + +void GpsLocation::deleteGpsFix(qint64 when) +{ + struct gpsTracker defaultTracker; + defaultTracker.when = 0; + struct gpsTracker deletedTracker = m_trackers.value(when, defaultTracker); + if (deletedTracker.when != when) { + qDebug() << "can't find tracker for timestamp" << when; + return; + } + deleteFixFromStorage(deletedTracker); + m_deletedTrackers.append(deletedTracker); +} + +void GpsLocation::clearGpsData() +{ + m_trackers.clear(); + geoSettings->clear(); + geoSettings->sync(); +} + +void GpsLocation::postError(QNetworkReply::NetworkError error) +{ + Q_UNUSED(error); + status(QString("error when sending a GPS fix: %1").arg(reply->errorString())); +} + +void GpsLocation::getUseridError(QNetworkReply::NetworkError error) +{ + Q_UNUSED(error); + status(QString("error when retrieving Subsurface webservice user id: %1").arg(reply->errorString())); +} + +void GpsLocation::deleteFixesFromServer() +{ + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + + QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + QUrl url(GPS_FIX_DELETE_URL); + QList<qint64> keys = m_trackers.keys(); + while (!m_deletedTrackers.isEmpty()) { + gpsTracker gt = m_deletedTrackers.takeFirst(); + QDateTime dt; + QUrlQuery data; + dt.setTime_t(gt.when - gettimezoneoffset(gt.when)); + data.addQueryItem("login", prefs.userid); + data.addQueryItem("dive_date", dt.toString("yyyy-MM-dd")); + data.addQueryItem("dive_time", dt.toString("hh:mm")); + status(data.toString(QUrl::FullyEncoded).toUtf8()); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("Content-type", "application/x-www-form-urlencoded"); + reply = manager->post(request, data.toString(QUrl::FullyEncoded).toUtf8()); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(postError(QNetworkReply::NetworkError))); + timer.start(10000); + loop.exec(); + if (timer.isActive()) { + timer.stop(); + if (reply->error() != QNetworkReply::NoError) { + QString response = reply->readAll(); + status(QString("Server response:") + reply->readAll()); + } + } else { + status("Deleting on the server timed out - try again later"); + m_deletedTrackers.prepend(gt); + break; + } + reply->deleteLater(); + status(QString("completed deleting gps fix %1 - response: ").arg(gt.idx) + reply->readAll()); + } +} + +void GpsLocation::uploadToServer() +{ + // we want to do this one at a time (the server prefers that) + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + + QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + QUrl url(GPS_FIX_ADD_URL); + Q_FOREACH(qint64 key, m_trackers.keys()) { + struct gpsTracker gt = m_trackers.value(key); + QDateTime dt; + QUrlQuery data; + dt.setTime_t(gt.when - gettimezoneoffset(gt.when)); + data.addQueryItem("login", prefs.userid); + data.addQueryItem("dive_date", dt.toString("yyyy-MM-dd")); + data.addQueryItem("dive_time", dt.toString("hh:mm")); + data.addQueryItem("dive_latitude", QString::number(gt.latitude.udeg / 1000000.0, 'f', 9)); + data.addQueryItem("dive_longitude", QString::number(gt.longitude.udeg / 1000000.0, 'f', 9)); + if (gt.name.isEmpty()) + gt.name = "Auto-created dive"; + data.addQueryItem("dive_name", gt.name); + status(data.toString(QUrl::FullyEncoded).toUtf8()); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("Content-type", "application/x-www-form-urlencoded"); + reply = manager->post(request, data.toString(QUrl::FullyEncoded).toUtf8()); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + // somehoe I cannot get this to work with the new connect syntax: + // connect(reply, &QNetworkReply::error, this, &GpsLocation::postError); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(postError(QNetworkReply::NetworkError))); + timer.start(10000); + loop.exec(); + if (timer.isActive()) { + timer.stop(); + if (reply->error() != QNetworkReply::NoError) { + QString response = reply->readAll(); + if (!response.contains("Duplicate entry")) { + status(QString("Server response:") + reply->readAll()); + break; + } + } + } else { + status("Uploading to server timed out"); + break; + } + reply->deleteLater(); + status(QString("completed sending gps fix %1 - response: ").arg(gt.idx) + reply->readAll()); + } + // and now remove the ones that were locally deleted + deleteFixesFromServer(); +} + +void GpsLocation::downloadFromServer() +{ + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + QUrl url(QString(GPS_FIX_DOWNLOAD_URL "?login=%1").arg(prefs.userid)); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("Content-type", "text/html"); + qDebug() << "downloadFromServer accessing" << url; + reply = manager->get(request); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(getUseridError(QNetworkReply::NetworkError))); + timer.start(10000); + loop.exec(); + if (timer.isActive()) { + timer.stop(); + if (!reply->error()) { + QString response = reply->readAll(); + QJsonDocument json = QJsonDocument::fromJson(response.toLocal8Bit()); + QJsonObject object = json.object(); + if (object.value("download").toString() != "ok") { + qDebug() << "problems downloading GPS fixes"; + return; + } + qDebug() << "already have" << m_trackers.count() << "GPS fixes"; + QJsonArray downloadedFixes = object.value("dives").toArray(); + qDebug() << downloadedFixes.count() << "GPS fixes downloaded"; + for (int i = 0; i < downloadedFixes.count(); i++) { + QJsonObject fix = downloadedFixes[i].toObject(); + QString date = fix.value("date").toString(); + QString time = fix.value("time").toString(); + QString name = fix.value("name").toString(); + QString latitude = fix.value("latitude").toString(); + QString longitude = fix.value("longitude").toString(); + QDateTime timestamp = QDateTime::fromString(date + " " + time, "yyyy-M-d hh:m:s"); + + struct gpsTracker gt; + gt.when = timestamp.toMSecsSinceEpoch() / 1000 + gettimezoneoffset(timestamp.toMSecsSinceEpoch() / 1000); + gt.latitude.udeg = latitude.toDouble() * 1000000; + gt.longitude.udeg = longitude.toDouble() * 1000000; + gt.name = name; + // add this GPS fix to the QMap and the settings (remove existing fix at the same timestamp first) + if (m_trackers.keys().contains(gt.when)) { + qDebug() << "already have a fix at time stamp" << gt.when; + replaceFixToStorage(gt); + } else { + addFixToStorage(gt); + } + } + } else { + qDebug() << "network error" << reply->error() << reply->errorString() << reply->readAll(); + } + } else { + qDebug() << "download timed out"; + status("Download from server timed out"); + } + reply->deleteLater(); +} diff --git a/core/gpslocation.h b/core/gpslocation.h new file mode 100644 index 000000000..82b51a291 --- /dev/null +++ b/core/gpslocation.h @@ -0,0 +1,66 @@ +#ifndef GPSLOCATION_H +#define GPSLOCATION_H + +#include "units.h" +#include <QObject> +#include <QGeoCoordinate> +#include <QGeoPositionInfoSource> +#include <QGeoPositionInfo> +#include <QSettings> +#include <QNetworkReply> +#include <QMap> + +struct gpsTracker { + degrees_t latitude; + degrees_t longitude; + qint64 when; + QString name; + int idx; +}; + +class GpsLocation : QObject { + Q_OBJECT +public: + GpsLocation(void (*showMsgCB)(const char *msg), QObject *parent); + ~GpsLocation(); + static GpsLocation *instance(); + bool applyLocations(); + int getGpsNum() const; + QString getUserid(QString user, QString passwd); + bool hasLocationsSource(); + QString currentPosition(); + + QMap<qint64, gpsTracker> currentGPSInfo() const; + +private: + QGeoPositionInfo lastPos; + QGeoPositionInfoSource *getGpsSource(); + QGeoPositionInfoSource *m_GpsSource; + void status(QString msg); + QSettings *geoSettings; + QNetworkReply *reply; + QString userAgent; + void (*showMessageCB)(const char *msg); + static GpsLocation *m_Instance; + bool waitingForPosition; + QMap<qint64, gpsTracker> m_trackers; + QList<gpsTracker> m_deletedTrackers; + void loadFromStorage(); + void addFixToStorage(gpsTracker >); + void replaceFixToStorage(gpsTracker >); + void deleteFixFromStorage(gpsTracker >); + void deleteFixesFromServer(); + +public slots: + void serviceEnable(bool toggle); + void newPosition(QGeoPositionInfo pos); + void updateTimeout(); + void uploadToServer(); + void downloadFromServer(); + void postError(QNetworkReply::NetworkError error); + void getUseridError(QNetworkReply::NetworkError error); + void clearGpsData(); + void deleteGpsFix(qint64 when); +}; + +#endif // GPSLOCATION_H diff --git a/core/helpers.h b/core/helpers.h new file mode 100644 index 000000000..f88da015c --- /dev/null +++ b/core/helpers.h @@ -0,0 +1,56 @@ +/* + * helpers.h + * + * header file for random helper functions of Subsurface + * + */ +#ifndef HELPERS_H +#define HELPERS_H + +#include <QString> +#include "dive.h" +#include "qthelper.h" + +QString get_depth_string(depth_t depth, bool showunit = false, bool showdecimal = true); +QString get_depth_string(int mm, bool showunit = false, bool showdecimal = true); +QString get_depth_unit(); +QString get_weight_string(weight_t weight, bool showunit = false); +QString get_weight_unit(); +QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit = false); +QString get_temperature_string(temperature_t temp, bool showunit = false); +QString get_temp_unit(); +QString get_volume_string(volume_t volume, bool showunit = false); +QString get_volume_unit(); +QString get_pressure_string(pressure_t pressure, bool showunit = false); +QString get_pressure_unit(); +void set_default_dive_computer(const char *vendor, const char *product); +void set_default_dive_computer_device(const char *name); +void set_default_dive_computer_download_mode(int downloadMode); +QString getSubsurfaceDataPath(QString folderToFind); +QString getPrintingTemplatePathUser(); +QString getPrintingTemplatePathBundle(); +void copyPath(QString src, QString dst); +extern const QString get_dc_nickname(const char *model, uint32_t deviceid); +int gettimezoneoffset(timestamp_t when = 0); +int parseLengthToMm(const QString &text); +int parseTemperatureToMkelvin(const QString &text); +int parseWeightToGrams(const QString &text); +int parsePressureToMbar(const QString &text); +int parseGasMixO2(const QString &text); +int parseGasMixHE(const QString &text); +QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText); +QString get_dive_date_string(timestamp_t when); +QString get_short_dive_date_string(timestamp_t when); +bool is_same_day (timestamp_t trip_when, timestamp_t dive_when); +QString get_trip_date_string(timestamp_t when, int nr, bool getday); +QString uiLanguage(QLocale *callerLoc); +QLocale getLocale(); +void selectedDivesGasUsed(QVector<QPair<QString, int> > &gasUsed); +QString getUserAgent(); + +#if defined __APPLE__ +#define TITLE_OR_TEXT(_t, _m) "", _t + "\n" + _m +#else +#define TITLE_OR_TEXT(_t, _m) _t, _m +#endif +#endif // HELPERS_H diff --git a/core/imagedownloader.cpp b/core/imagedownloader.cpp new file mode 100644 index 000000000..f406ee45a --- /dev/null +++ b/core/imagedownloader.cpp @@ -0,0 +1,113 @@ +#include "dive.h" +#include "metrics.h" +#include "divelist.h" +#include "qthelper.h" +#include "imagedownloader.h" +#include <unistd.h> + +#include <QtConcurrent> + +QUrl cloudImageURL(const char *hash) +{ + return QUrl::fromUserInput(QString("https://cloud.subsurface-divelog.org/images/").append(hash)); +} + +ImageDownloader::ImageDownloader(struct picture *pic) +{ + picture = pic; +} + +ImageDownloader::~ImageDownloader() +{ + picture_free(picture); +} + +void ImageDownloader::load(bool fromHash){ + QUrl url; + loadFromHash = fromHash; + if(fromHash) + url = cloudImageURL(picture->hash); + else + url = QUrl::fromUserInput(QString(picture->filename)); + if (url.isValid()) { + QEventLoop loop; + QNetworkRequest request(url); + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveImage(QNetworkReply *))); + QNetworkReply *reply = manager.get(request); + while (reply->isRunning()) { + loop.processEvents(); + sleep(1); + } + } +} + +void ImageDownloader::saveImage(QNetworkReply *reply) +{ + QByteArray imageData = reply->readAll(); + QImage image = QImage(); + image.loadFromData(imageData); + if (image.isNull()) { + if (loadFromHash) + load(false); + return; + } + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(imageData); + QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); + QDir dir(path); + if (!dir.exists()) + dir.mkpath(path); + QFile imageFile(path.append("/").append(hash.result().toHex())); + if (imageFile.open(QIODevice::WriteOnly)) { + QDataStream stream(&imageFile); + stream.writeRawData(imageData.data(), imageData.length()); + imageFile.waitForBytesWritten(-1); + imageFile.close(); + add_hash(imageFile.fileName(), hash.result()); + learnHash(picture, hash.result()); + } + reply->manager()->deleteLater(); + reply->deleteLater(); + // This should be called to make the picture actually show. + // Problem is DivePictureModel is not in core. + // Nevertheless, the image shows when the dive is selected the next time. + // DivePictureModel::instance()->updateDivePictures(); + +} + +void loadPicture(struct picture *picture, bool fromHash) +{ + ImageDownloader download(picture); + download.load(fromHash); +} + +SHashedImage::SHashedImage(struct picture *picture) : QImage() +{ + QUrl url = QUrl::fromUserInput(localFilePath(QString(picture->filename))); + if(url.isLocalFile()) + load(url.toLocalFile()); + if (isNull()) { + // This did not load anything. Let's try to get the image from other sources + // Let's try to load it locally via its hash + QString filename = fileFromHash(picture->hash); + if (filename.isNull()) { + // That didn't produce a local filename. + // Try the cloud server + QtConcurrent::run(loadPicture, clone_picture(picture), true); + } else { + // Load locally from translated file name + load(filename); + if (!isNull()) { + // Make sure the hash still matches the image file + QtConcurrent::run(updateHash, clone_picture(picture)); + } else { + // Interpret filename as URL + QtConcurrent::run(loadPicture, clone_picture(picture), false); + } + } + } else { + // We loaded successfully. Now, make sure hash is up to date. + QtConcurrent::run(hashPicture, clone_picture(picture)); + } +} + diff --git a/core/imagedownloader.h b/core/imagedownloader.h new file mode 100644 index 000000000..f4e3df875 --- /dev/null +++ b/core/imagedownloader.h @@ -0,0 +1,34 @@ +#ifndef IMAGEDOWNLOADER_H +#define IMAGEDOWNLOADER_H + +#include <QImage> +#include <QFuture> +#include <QNetworkReply> + +typedef QPair<QString, QByteArray> SHashedFilename; + +extern QUrl cloudImageURL(const char *hash); + + +class ImageDownloader : public QObject { + Q_OBJECT; +public: + ImageDownloader(struct picture *picture); + ~ImageDownloader(); + void load(bool fromHash); + +private: + struct picture *picture; + QNetworkAccessManager manager; + bool loadFromHash; + +private slots: + void saveImage(QNetworkReply *reply); +}; + +class SHashedImage : public QImage { +public: + SHashedImage(struct picture *picture); +}; + +#endif // IMAGEDOWNLOADER_H diff --git a/core/isocialnetworkintegration.cpp b/core/isocialnetworkintegration.cpp new file mode 100644 index 000000000..eb1e82a49 --- /dev/null +++ b/core/isocialnetworkintegration.cpp @@ -0,0 +1,6 @@ +#include "isocialnetworkintegration.h" + +//Hack for moc. +ISocialNetworkIntegration::ISocialNetworkIntegration(QObject* parent) : QObject(parent) +{ +} diff --git a/core/isocialnetworkintegration.h b/core/isocialnetworkintegration.h new file mode 100644 index 000000000..70ea3d9ab --- /dev/null +++ b/core/isocialnetworkintegration.h @@ -0,0 +1,73 @@ +#ifndef ISOCIALNETWORKINTEGRATION_H +#define ISOCIALNETWORKINTEGRATION_H + +#include <QtPlugin> + +/* This Interface represents a Plugin for Social Network integration, + * with it you may be able to create plugins for facebook, instagram, + * twitpic, google plus and any other thing you may imagine. + * + * We bundle facebook integration as an example. + */ + +class ISocialNetworkIntegration : public QObject { + Q_OBJECT +public: + ISocialNetworkIntegration(QObject* parent = 0); + + /*! + * @name socialNetworkName + * @brief The name of this social network + * @return The name of this social network + * + * The name of this social network will be used to populate the Menu to toggle states + * between connected/disconnected, and also submit stuff to it. + */ + virtual QString socialNetworkName() const = 0; + + /*! + * @name socialNetworkIcon + * @brief The icon of this social network + * @return The icon of this social network + * + * The icon of this social network will be used to populate the menu, and can also be + * used on a toolbar if requested. + */ + virtual QString socialNetworkIcon() const = 0; + + /*! + * @name isConnected + * @brief returns true if connected to this social network, false otherwise + * @return true if connected to this social network, false otherwise + */ + virtual bool isConnected() = 0; + + /*! + * @name requestLogin + * @brief try to login on this social network. + * + * Try to login on this social network. All widget implementation that + * manages login should be done inside this function. + */ + virtual void requestLogin() = 0; + + /*! + * @name requestLogoff + * @brief tries to logoff from this social network + * + * Try to logoff from this social network. + */ + virtual void requestLogoff() = 0; + + /*! + * @name uploadCurrentDive + * @brief send the current dive info to the Social Network + * + * Should format all the options and pixmaps from the current dive + * to update to the social network. All widget stuff related to sendint + * dive information should be executed inside this function. + */ + virtual void requestUpload() = 0; +}; + +#endif
\ No newline at end of file diff --git a/core/libdivecomputer.c b/core/libdivecomputer.c new file mode 100644 index 000000000..549b894ce --- /dev/null +++ b/core/libdivecomputer.c @@ -0,0 +1,1081 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <unistd.h> +#include <inttypes.h> +#include <string.h> +#include "gettext.h" +#include "dive.h" +#include "device.h" +#include "divelist.h" +#include "display.h" + +#include <libdivecomputer/uwatec.h> +#include <libdivecomputer/hw.h> +#include <libdivecomputer/version.h> +#include "libdivecomputer.h" + +/* Christ. Libdivecomputer has the worst configuration system ever. */ +#ifdef HW_FROG_H +#define NOT_FROG , 0 +#define LIBDIVECOMPUTER_SUPPORTS_FROG +#else +#define NOT_FROG +#endif + +char *dumpfile_name; +char *logfile_name; +const char *progress_bar_text = ""; +double progress_bar_fraction = 0.0; + +static int stoptime, stopdepth, ndl, po2, cns; +static bool in_deco, first_temp_is_air; + +/* + * Directly taken from libdivecomputer's examples/common.c to improve + * the error messages resulting from libdc's return codes + */ +const char *errmsg (dc_status_t rc) +{ + switch (rc) { + case DC_STATUS_SUCCESS: + return "Success"; + case DC_STATUS_UNSUPPORTED: + return "Unsupported operation"; + case DC_STATUS_INVALIDARGS: + return "Invalid arguments"; + case DC_STATUS_NOMEMORY: + return "Out of memory"; + case DC_STATUS_NODEVICE: + return "No device found"; + case DC_STATUS_NOACCESS: + return "Access denied"; + case DC_STATUS_IO: + return "Input/output error"; + case DC_STATUS_TIMEOUT: + return "Timeout"; + case DC_STATUS_PROTOCOL: + return "Protocol error"; + case DC_STATUS_DATAFORMAT: + return "Data format error"; + case DC_STATUS_CANCELLED: + return "Cancelled"; + default: + return "Unknown error"; + } +} + +static dc_status_t create_parser(device_data_t *devdata, dc_parser_t **parser) +{ + return dc_parser_new(parser, devdata->device); +} + +static int parse_gasmixes(device_data_t *devdata, struct dive *dive, dc_parser_t *parser, unsigned int ngases) +{ + static bool shown_warning = false; + unsigned int i; + int rc; + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + unsigned int ntanks = 0; + rc = dc_parser_get_field(parser, DC_FIELD_TANK_COUNT, 0, &ntanks); + if (rc == DC_STATUS_SUCCESS) { + if (ntanks && ntanks != ngases) { + shown_warning = true; + report_error("different number of gases (%d) and tanks (%d)", ngases, ntanks); + } + } + dc_tank_t tank = { 0 }; +#endif + + for (i = 0; i < ngases; i++) { + dc_gasmix_t gasmix = { 0 }; + int o2, he; + bool no_volume = true; + + rc = dc_parser_get_field(parser, DC_FIELD_GASMIX, i, &gasmix); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) + return rc; + + if (i >= MAX_CYLINDERS) + continue; + + o2 = rint(gasmix.oxygen * 1000); + he = rint(gasmix.helium * 1000); + + /* Ignore bogus data - libdivecomputer does some crazy stuff */ + if (o2 + he <= O2_IN_AIR || o2 > 1000) { + if (!shown_warning) { + shown_warning = true; + report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); + } + o2 = 0; + } + if (he < 0 || o2 + he > 1000) { + if (!shown_warning) { + shown_warning = true; + report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); + } + he = 0; + } + dive->cylinder[i].gasmix.o2.permille = o2; + dive->cylinder[i].gasmix.he.permille = he; + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + tank.volume = 0.0; + if (i < ntanks) { + rc = dc_parser_get_field(parser, DC_FIELD_TANK, i, &tank); + if (rc == DC_STATUS_SUCCESS) { + if (tank.type == DC_TANKVOLUME_IMPERIAL) { + dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); + dive->cylinder[i].type.workingpressure.mbar = rint(tank.workpressure * 1000); + if (same_string(devdata->model, "Suunto EON Steel")) { + /* Suunto EON Steele gets this wrong. Badly. + * but on the plus side it only supports a few imperial sizes, + * so let's try and guess at least the most common ones. + * First, the pressures are off by a constant factor. WTF? + * Then we can round the wet sizes so we get to multiples of 10 + * for cuft sizes (as that's all that you can enter) */ + dive->cylinder[i].type.workingpressure.mbar *= 206.843 / 206.7; + char name_buffer[9]; + int rounded_size = ml_to_cuft(gas_volume(&dive->cylinder[i], + dive->cylinder[i].type.workingpressure)); + rounded_size = (int)((rounded_size + 5) / 10) * 10; + switch (dive->cylinder[i].type.workingpressure.mbar) { + case 206843: + snprintf(name_buffer, 9, "AL%d", rounded_size); + break; + case 234422: /* this is wrong - HP tanks tend to be 3440, but Suunto only allows 3400 */ + snprintf(name_buffer, 9, "HP%d", rounded_size); + break; + case 179263: + snprintf(name_buffer, 9, "LP+%d", rounded_size); + break; + case 165474: + snprintf(name_buffer, 9, "LP%d", rounded_size); + break; + default: + snprintf(name_buffer, 9, "%d cuft", rounded_size); + break; + } + dive->cylinder[i].type.description = copy_string(name_buffer); + dive->cylinder[i].type.size.mliter = cuft_to_l(rounded_size) * 1000 / + mbar_to_atm(dive->cylinder[i].type.workingpressure.mbar); + } + } else if (tank.type == DC_TANKVOLUME_METRIC) { + dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); + } + if (tank.gasmix != i) { // we don't handle this, yet + shown_warning = true; + report_error("gasmix %d for tank %d doesn't match", tank.gasmix, i); + } + } + } + if (!IS_FP_SAME(tank.volume, 0.0)) + no_volume = false; + + // this new API also gives us the beginning and end pressure for the tank + if (!IS_FP_SAME(tank.beginpressure, 0.0) && !IS_FP_SAME(tank.endpressure, 0.0)) { + dive->cylinder[i].start.mbar = tank.beginpressure * 1000; + dive->cylinder[i].end.mbar = tank.endpressure * 1000; + } +#endif + if (no_volume) { + /* for the first tank, if there is no tanksize available from the + * dive computer, fill in the default tank information (if set) */ + fill_default_cylinder(&dive->cylinder[i]); + } + /* whatever happens, make sure there is a name for the cylinder */ + if (same_string(dive->cylinder[i].type.description, "")) + dive->cylinder[i].type.description = strdup(translate("gettextFromC", "unknown")); + } + return DC_STATUS_SUCCESS; +} + +static void handle_event(struct divecomputer *dc, struct sample *sample, dc_sample_value_t value) +{ + int type, time; + /* we mark these for translation here, but we store the untranslated strings + * and only translate them when they are displayed on screen */ + static const char *events[] = { + QT_TRANSLATE_NOOP("gettextFromC", "none"), QT_TRANSLATE_NOOP("gettextFromC", "deco stop"), QT_TRANSLATE_NOOP("gettextFromC", "rbt"), QT_TRANSLATE_NOOP("gettextFromC", "ascent"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling"), QT_TRANSLATE_NOOP("gettextFromC", "workload"), + QT_TRANSLATE_NOOP("gettextFromC", "transmitter"), QT_TRANSLATE_NOOP("gettextFromC", "violation"), QT_TRANSLATE_NOOP("gettextFromC", "bookmark"), QT_TRANSLATE_NOOP("gettextFromC", "surface"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop"), + QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (voluntary)"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (mandatory)"), + QT_TRANSLATE_NOOP("gettextFromC", "deepstop"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling (safety stop)"), QT_TRANSLATE_NOOP3("gettextFromC", "below floor", "event showing dive is below deco floor and adding deco time"), QT_TRANSLATE_NOOP("gettextFromC", "divetime"), + QT_TRANSLATE_NOOP("gettextFromC", "maxdepth"), QT_TRANSLATE_NOOP("gettextFromC", "OLF"), QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚"), QT_TRANSLATE_NOOP("gettextFromC", "airtime"), QT_TRANSLATE_NOOP("gettextFromC", "rgbm"), QT_TRANSLATE_NOOP("gettextFromC", "heading"), + QT_TRANSLATE_NOOP("gettextFromC", "tissue level warning"), QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "non stop time") + }; + const int nr_events = sizeof(events) / sizeof(const char *); + const char *name; + /* + * Just ignore surface events. They are pointless. What "surface" + * means depends on the dive computer (and possibly even settings + * in the dive computer). It does *not* necessarily mean "depth 0", + * so don't even turn it into that. + */ + if (value.event.type == SAMPLE_EVENT_SURFACE) + return; + + /* + * Other evens might be more interesting, but for now we just print them out. + */ + type = value.event.type; + name = QT_TRANSLATE_NOOP("gettextFromC", "invalid event number"); + if (type < nr_events) + name = events[type]; + + time = value.event.time; + if (sample) + time += sample->time.seconds; + + add_event(dc, time, type, value.event.flags, value.event.value, name); +} + +void +sample_cb(dc_sample_type_t type, dc_sample_value_t value, void *userdata) +{ + static unsigned int nsensor = 0; + struct divecomputer *dc = userdata; + struct sample *sample; + + /* + * We fill in the "previous" sample - except for DC_SAMPLE_TIME, + * which creates a new one. + */ + sample = dc->samples ? dc->sample + dc->samples - 1 : NULL; + + /* + * Ok, sanity check. + * If first sample is not a DC_SAMPLE_TIME, Allocate a sample for us + */ + if (sample == NULL && type != DC_SAMPLE_TIME) + sample = prepare_sample(dc); + + switch (type) { + case DC_SAMPLE_TIME: + nsensor = 0; + + // The previous sample gets some sticky values + // that may have been around from before, even + // if there was no new data + if (sample) { + sample->in_deco = in_deco; + sample->ndl.seconds = ndl; + sample->stoptime.seconds = stoptime; + sample->stopdepth.mm = stopdepth; + sample->setpoint.mbar = po2; + sample->cns = cns; + } + // Create a new sample. + // Mark depth as negative + sample = prepare_sample(dc); + sample->time.seconds = value.time; + sample->depth.mm = -1; + finish_sample(dc); + break; + case DC_SAMPLE_DEPTH: + sample->depth.mm = rint(value.depth * 1000); + break; + case DC_SAMPLE_PRESSURE: + sample->sensor = value.pressure.tank; + sample->cylinderpressure.mbar = rint(value.pressure.value * 1000); + break; + case DC_SAMPLE_TEMPERATURE: + sample->temperature.mkelvin = C_to_mkelvin(value.temperature); + break; + case DC_SAMPLE_EVENT: + handle_event(dc, sample, value); + break; + case DC_SAMPLE_RBT: + sample->rbt.seconds = (!strncasecmp(dc->model, "suunto", 6)) ? value.rbt : value.rbt * 60; + break; + case DC_SAMPLE_HEARTBEAT: + sample->heartbeat = value.heartbeat; + break; + case DC_SAMPLE_BEARING: + sample->bearing.degrees = value.bearing; + break; +#ifdef DEBUG_DC_VENDOR + case DC_SAMPLE_VENDOR: + printf(" <vendor time='%u:%02u' type=\"%u\" size=\"%u\">", FRACTION(sample->time.seconds, 60), + value.vendor.type, value.vendor.size); + for (int i = 0; i < value.vendor.size; ++i) + printf("%02X", ((unsigned char *)value.vendor.data)[i]); + printf("</vendor>\n"); + break; +#endif +#if DC_VERSION_CHECK(0, 3, 0) + case DC_SAMPLE_SETPOINT: + /* for us a setpoint means constant pO2 from here */ + sample->setpoint.mbar = po2 = rint(value.setpoint * 1000); + break; + case DC_SAMPLE_PPO2: + if (nsensor < 3) + sample->o2sensor[nsensor].mbar = rint(value.ppo2 * 1000); + else + report_error("%d is more o2 sensors than we can handle", nsensor); + nsensor++; + // Set the amount of detected o2 sensors + if (nsensor > dc->no_o2sensors) + dc->no_o2sensors = nsensor; + break; + case DC_SAMPLE_CNS: + sample->cns = cns = rint(value.cns * 100); + break; + case DC_SAMPLE_DECO: + if (value.deco.type == DC_DECO_NDL) { + sample->ndl.seconds = ndl = value.deco.time; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->in_deco = in_deco = false; + } else if (value.deco.type == DC_DECO_DECOSTOP || + value.deco.type == DC_DECO_DEEPSTOP) { + sample->in_deco = in_deco = true; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->stoptime.seconds = stoptime = value.deco.time; + ndl = 0; + } else if (value.deco.type == DC_DECO_SAFETYSTOP) { + sample->in_deco = in_deco = false; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->stoptime.seconds = stoptime = value.deco.time; + } +#endif + default: + break; + } +} + +static void dev_info(device_data_t *devdata, const char *fmt, ...) +{ + (void) devdata; + static char buffer[1024]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + progress_bar_text = buffer; +} + +static int import_dive_number = 0; + +static int parse_samples(device_data_t *devdata, struct divecomputer *dc, dc_parser_t *parser) +{ + (void) devdata; + // Parse the sample data. + return dc_parser_samples_foreach(parser, sample_cb, dc); +} + +static int might_be_same_dc(struct divecomputer *a, struct divecomputer *b) +{ + if (!a->model || !b->model) + return 1; + if (strcasecmp(a->model, b->model)) + return 0; + if (!a->deviceid || !b->deviceid) + return 1; + return a->deviceid == b->deviceid; +} + +static int match_one_dive(struct divecomputer *a, struct dive *dive) +{ + struct divecomputer *b = &dive->dc; + + /* + * Walk the existing dive computer data, + * see if we have a match (or an anti-match: + * the same dive computer but a different + * dive ID). + */ + do { + int match = match_one_dc(a, b); + if (match) + return match > 0; + b = b->next; + } while (b); + + /* Ok, no exact dive computer match. Does the date match? */ + b = &dive->dc; + do { + if (a->when == b->when && might_be_same_dc(a, b)) + return 1; + b = b->next; + } while (b); + + return 0; +} + +/* + * Check if this dive already existed before the import + */ +static int find_dive(struct divecomputer *match) +{ + int i; + + for (i = 0; i < dive_table.preexisting; i++) { + struct dive *old = dive_table.dives[i]; + + if (match_one_dive(match, old)) + return 1; + } + return 0; +} + +/* + * Like g_strdup_printf(), but without the stupid g_malloc/g_free confusion. + * And we limit the string to some arbitrary size. + */ +static char *str_printf(const char *fmt, ...) +{ + va_list args; + char buf[1024]; + + va_start(args, fmt); + vsnprintf(buf, sizeof(buf) - 1, fmt, args); + va_end(args); + buf[sizeof(buf) - 1] = 0; + return strdup(buf); +} + +/* + * The dive ID for libdivecomputer dives is the first word of the + * SHA1 of the fingerprint, if it exists. + * + * NOTE! This is byte-order dependent, and I don't care. + */ +static uint32_t calculate_diveid(const unsigned char *fingerprint, unsigned int fsize) +{ + uint32_t csum[5]; + + if (!fingerprint || !fsize) + return 0; + + SHA1(fingerprint, fsize, (unsigned char *)csum); + return csum[0]; +} + +#ifdef DC_FIELD_STRING +static uint32_t calculate_string_hash(const char *str) +{ + return calculate_diveid((const unsigned char *)str, strlen(str)); +} + +static void parse_string_field(struct dive *dive, dc_field_string_t *str) +{ + // Our dive ID is the string hash of the "Dive ID" string + if (!strcmp(str->desc, "Dive ID")) { + if (!dive->dc.diveid) + dive->dc.diveid = calculate_string_hash(str->value); + return; + } + add_extra_data(&dive->dc, str->desc, str->value); + if (!strcmp(str->desc, "Serial")) { + dive->dc.serial = strdup(str->value); + /* should we just overwrite this whenever we have the "Serial" field? + * It's a much better deviceid then what we have so far... for now I'm leaving it as is */ + if (!dive->dc.deviceid) + dive->dc.deviceid = calculate_string_hash(str->value); + return; + } + if (!strcmp(str->desc, "FW Version")) { + dive->dc.fw_version = strdup(str->value); + return; + } +} +#endif + +static dc_status_t libdc_header_parser(dc_parser_t *parser, struct device_data_t *devdata, struct dive *dive) +{ + dc_status_t rc = 0; + dc_datetime_t dt = { 0 }; + struct tm tm; + + rc = dc_parser_get_datetime(parser, &dt); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the datetime")); + return rc; + } + + dive->dc.deviceid = devdata->deviceid; + + if (rc == DC_STATUS_SUCCESS) { + tm.tm_year = dt.year; + tm.tm_mon = dt.month - 1; + tm.tm_mday = dt.day; + tm.tm_hour = dt.hour; + tm.tm_min = dt.minute; + tm.tm_sec = dt.second; + dive->when = dive->dc.when = utc_mktime(&tm); + } + + // Parse the divetime. + const char *date_string = get_dive_date_c_string(dive->when); + dev_info(devdata, translate("gettextFromC", "Dive %d: %s"), import_dive_number, date_string); + free((void *)date_string); + + unsigned int divetime = 0; + rc = dc_parser_get_field(parser, DC_FIELD_DIVETIME, 0, &divetime); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the divetime")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.duration.seconds = divetime; + + // Parse the maxdepth. + double maxdepth = 0.0; + rc = dc_parser_get_field(parser, DC_FIELD_MAXDEPTH, 0, &maxdepth); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the maxdepth")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.maxdepth.mm = rint(maxdepth * 1000); + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + // if this is defined then we have a fairly late version of libdivecomputer + // from the 0.5 development cylcle - most likely temperatures and tank sizes + // are supported + + // Parse temperatures + double temperature; + dc_field_type_t temp_fields[] = {DC_FIELD_TEMPERATURE_SURFACE, + DC_FIELD_TEMPERATURE_MAXIMUM, + DC_FIELD_TEMPERATURE_MINIMUM}; + for (int i = 0; i < 3; i++) { + rc = dc_parser_get_field(parser, temp_fields[i], 0, &temperature); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing temperature")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + switch(i) { + case 0: + dive->dc.airtemp.mkelvin = C_to_mkelvin(temperature); + break; + case 1: // we don't distinguish min and max water temp here, so take min if given, max otherwise + case 2: + dive->dc.watertemp.mkelvin = C_to_mkelvin(temperature); + break; + } + } +#endif + + // Parse the gas mixes. + unsigned int ngases = 0; + rc = dc_parser_get_field(parser, DC_FIELD_GASMIX_COUNT, 0, &ngases); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix count")); + return rc; + } + +#if DC_VERSION_CHECK(0, 3, 0) + // Check if the libdivecomputer version already supports salinity & atmospheric + dc_salinity_t salinity = { + .type = DC_WATER_SALT, + .density = SEAWATER_SALINITY / 10.0 + }; + rc = dc_parser_get_field(parser, DC_FIELD_SALINITY, 0, &salinity); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining water salinity")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.salinity = rint(salinity.density * 10.0); + + double surface_pressure = 0; + rc = dc_parser_get_field(parser, DC_FIELD_ATMOSPHERIC, 0, &surface_pressure); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining surface pressure")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.surface_pressure.mbar = rint(surface_pressure * 1000.0); +#endif + +#ifdef DC_FIELD_STRING + // The dive parsing may give us more device information + int idx; + for (idx = 0; idx < 100; idx++) { + dc_field_string_t str = { NULL }; + rc = dc_parser_get_field(parser, DC_FIELD_STRING, idx, &str); + if (rc != DC_STATUS_SUCCESS) + break; + if (!str.desc || !str.value) + break; + parse_string_field(dive, &str); + } +#endif + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + dc_divemode_t divemode; + rc = dc_parser_get_field(parser, DC_FIELD_DIVEMODE, 0, &divemode); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining divemode")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + switch(divemode) { + case DC_DIVEMODE_FREEDIVE: + dive->dc.divemode = FREEDIVE; + break; + case DC_DIVEMODE_GAUGE: + case DC_DIVEMODE_OC: /* Open circuit */ + dive->dc.divemode = OC; + break; + case DC_DIVEMODE_CC: /* Closed circuit */ + dive->dc.divemode = CCR; + break; + } +#endif + + rc = parse_gasmixes(devdata, dive, parser, ngases); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix")); + return rc; + } + + return DC_STATUS_SUCCESS; +} + +/* returns true if we want libdivecomputer's dc_device_foreach() to continue, + * false otherwise */ +static int dive_cb(const unsigned char *data, unsigned int size, + const unsigned char *fingerprint, unsigned int fsize, + void *userdata) +{ + int rc; + dc_parser_t *parser = NULL; + device_data_t *devdata = userdata; + struct dive *dive = NULL; + + /* reset the deco / ndl data */ + ndl = stoptime = stopdepth = 0; + in_deco = false; + + rc = create_parser(devdata, &parser); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Unable to create parser for %s %s"), devdata->vendor, devdata->product); + return false; + } + + rc = dc_parser_set_data(parser, data, size); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Error registering the data")); + goto error_exit; + } + + import_dive_number++; + dive = alloc_dive(); + + // Parse the dive's header data + rc = libdc_header_parser (parser, devdata, dive); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("getextFromC", "Error parsing the header")); + goto error_exit; + } + + dive->dc.model = strdup(devdata->model); + dive->dc.diveid = calculate_diveid(fingerprint, fsize); + + // Initialize the sample data. + rc = parse_samples(devdata, &dive->dc, parser); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Error parsing the samples")); + goto error_exit; + } + + /* If we already saw this dive, abort. */ + if (!devdata->force_download && find_dive(&dive->dc)) + goto error_exit; + + dc_parser_destroy(parser); + + /* Various libdivecomputer interface fixups */ + if (dive->dc.airtemp.mkelvin == 0 && first_temp_is_air && dive->dc.samples) { + dive->dc.airtemp = dive->dc.sample[0].temperature; + dive->dc.sample[0].temperature.mkelvin = 0; + } + + if (devdata->create_new_trip) { + if (!devdata->trip) + devdata->trip = create_and_hookup_trip_from_dive(dive); + else + add_dive_to_trip(dive, devdata->trip); + } + + dive->downloaded = true; + record_dive_to_table(dive, devdata->download_table); + mark_divelist_changed(true); + return true; + +error_exit: + dc_parser_destroy(parser); + free(dive); + return false; + +} + +/* + * The device ID for libdivecomputer devices is the first 32-bit word + * of the SHA1 hash of the model/firmware/serial numbers. + * + * NOTE! This is byte-order-dependent. And I can't find it in myself to + * care. + */ +static uint32_t calculate_sha1(unsigned int model, unsigned int firmware, unsigned int serial) +{ + SHA_CTX ctx; + uint32_t csum[5]; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, &model, sizeof(model)); + SHA1_Update(&ctx, &firmware, sizeof(firmware)); + SHA1_Update(&ctx, &serial, sizeof(serial)); + SHA1_Final((unsigned char *)csum, &ctx); + return csum[0]; +} + +/* + * libdivecomputer has returned two different serial numbers for the + * same device in different versions. First it used to just do the four + * bytes as one 32-bit number, then it turned it into a decimal number + * with each byte giving two digits (0-99). + * + * The only way we can tell is by looking at the format of the number, + * so we'll just fix it to the first format. + */ +static unsigned int undo_libdivecomputer_suunto_nr_changes(unsigned int serial) +{ + unsigned char b0, b1, b2, b3; + + /* + * The second format will never have more than 8 decimal + * digits, so do a cheap check first + */ + if (serial >= 100000000) + return serial; + + /* The original format seems to be four bytes of values 00-99 */ + b0 = (serial >> 0) & 0xff; + b1 = (serial >> 8) & 0xff; + b2 = (serial >> 16) & 0xff; + b3 = (serial >> 24) & 0xff; + + /* Looks like an old-style libdivecomputer serial number */ + if ((b0 < 100) && (b1 < 100) && (b2 < 100) && (b3 < 100)) + return serial; + + /* Nope, it was converted. */ + b0 = serial % 100; + serial /= 100; + b1 = serial % 100; + serial /= 100; + b2 = serial % 100; + serial /= 100; + b3 = serial % 100; + + serial = b0 + (b1 << 8) + (b2 << 16) + (b3 << 24); + return serial; +} + +static unsigned int fixup_suunto_versions(device_data_t *devdata, const dc_event_devinfo_t *devinfo) +{ + unsigned int serial = devinfo->serial; + char serial_nr[13] = ""; + char firmware[13] = ""; + + first_temp_is_air = 1; + + serial = undo_libdivecomputer_suunto_nr_changes(serial); + + if (serial) { + snprintf(serial_nr, sizeof(serial_nr), "%02d%02d%02d%02d", + (devinfo->serial >> 24) & 0xff, + (devinfo->serial >> 16) & 0xff, + (devinfo->serial >> 8) & 0xff, + (devinfo->serial >> 0) & 0xff); + } + if (devinfo->firmware) { + snprintf(firmware, sizeof(firmware), "%d.%d.%d", + (devinfo->firmware >> 16) & 0xff, + (devinfo->firmware >> 8) & 0xff, + (devinfo->firmware >> 0) & 0xff); + } + create_device_node(devdata->model, devdata->deviceid, serial_nr, firmware, ""); + + return serial; +} + +static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) +{ + (void) device; + const dc_event_progress_t *progress = data; + const dc_event_devinfo_t *devinfo = data; + const dc_event_clock_t *clock = data; + const dc_event_vendor_t *vendor = data; + device_data_t *devdata = userdata; + unsigned int serial; + + switch (event) { + case DC_EVENT_WAITING: + dev_info(devdata, translate("gettextFromC", "Event: waiting for user action")); + break; + case DC_EVENT_PROGRESS: + if (!progress->maximum) + break; + progress_bar_fraction = (double)progress->current / (double)progress->maximum; + break; + case DC_EVENT_DEVINFO: + dev_info(devdata, translate("gettextFromC", "model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)"), + devinfo->model, devinfo->model, + devinfo->firmware, devinfo->firmware, + devinfo->serial, devinfo->serial); + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)\n", + devinfo->model, devinfo->model, + devinfo->firmware, devinfo->firmware, + devinfo->serial, devinfo->serial); + } + /* + * libdivecomputer doesn't give serial numbers in the proper string form, + * so we have to see if we can do some vendor-specific munging. + */ + serial = devinfo->serial; + if (!strcmp(devdata->vendor, "Suunto")) + serial = fixup_suunto_versions(devdata, devinfo); + devdata->deviceid = calculate_sha1(devinfo->model, devinfo->firmware, serial); + /* really, serial and firmware version are NOT numbers. We'll try to save them here + * in something that might work, but this really needs to be handled with the + * DC_FIELD_STRING interface instead */ + devdata->libdc_serial = devinfo->serial; + devdata->libdc_firmware = devinfo->firmware; + break; + case DC_EVENT_CLOCK: + dev_info(devdata, translate("gettextFromC", "Event: systime=%" PRId64 ", devtime=%u\n"), + (uint64_t)clock->systime, clock->devtime); + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: systime=%" PRId64 ", devtime=%u\n", + (uint64_t)clock->systime, clock->devtime); + } + break; + case DC_EVENT_VENDOR: + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: vendor="); + for (unsigned int i = 0; i < vendor->size; ++i) + fprintf(devdata->libdc_logfile, "%02X", vendor->data[i]); + fprintf(devdata->libdc_logfile, "\n"); + } + break; + default: + break; + } +} + +int import_thread_cancelled; + +static int cancel_cb(void *userdata) +{ + (void) userdata; + return import_thread_cancelled; +} + +static const char *do_device_import(device_data_t *data) +{ + dc_status_t rc; + dc_device_t *device = data->device; + + data->model = str_printf("%s %s", data->vendor, data->product); + + // Register the event handler. + int events = DC_EVENT_WAITING | DC_EVENT_PROGRESS | DC_EVENT_DEVINFO | DC_EVENT_CLOCK | DC_EVENT_VENDOR; + rc = dc_device_set_events(device, events, event_cb, data); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Error registering the event handler."); + + // Register the cancellation handler. + rc = dc_device_set_cancel(device, cancel_cb, data); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Error registering the cancellation handler."); + + if (data->libdc_dump) { + dc_buffer_t *buffer = dc_buffer_new(0); + + rc = dc_device_dump(device, buffer); + if (rc == DC_STATUS_SUCCESS && dumpfile_name) { + FILE *fp = subsurface_fopen(dumpfile_name, "wb"); + if (fp != NULL) { + fwrite(dc_buffer_get_data(buffer), 1, dc_buffer_get_size(buffer), fp); + fclose(fp); + } + } + + dc_buffer_free(buffer); + } else { + rc = dc_device_foreach(device, dive_cb, data); + } + + if (rc != DC_STATUS_SUCCESS) { + progress_bar_fraction = 0.0; + return translate("gettextFromC", "Dive data import error"); + } + + /* All good */ + return NULL; +} + +void logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata) +{ + (void) context; + const char *loglevels[] = { "NONE", "ERROR", "WARNING", "INFO", "DEBUG", "ALL" }; + + FILE *fp = (FILE *)userdata; + + if (loglevel == DC_LOGLEVEL_ERROR || loglevel == DC_LOGLEVEL_WARNING) { + fprintf(fp, "%s: %s [in %s:%d (%s)]\n", loglevels[loglevel], msg, file, line, function); + } else { + fprintf(fp, "%s: %s\n", loglevels[loglevel], msg); + } +} + +const char *do_libdivecomputer_import(device_data_t *data) +{ + dc_status_t rc; + const char *err; + FILE *fp = NULL; + + import_dive_number = 0; + first_temp_is_air = 0; + data->device = NULL; + data->context = NULL; + + if (data->libdc_log && logfile_name) + fp = subsurface_fopen(logfile_name, "w"); + + data->libdc_logfile = fp; + + rc = dc_context_new(&data->context); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Unable to create libdivecomputer context"); + + if (fp) { + dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); + dc_context_set_logfunc(data->context, logfunc, fp); + } + + err = translate("gettextFromC", "Unable to open %s %s (%s)"); + +#if defined(SSRF_CUSTOM_SERIAL) + dc_serial_t *serial_device = NULL; + + if (data->bluetooth_mode) { +#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) + rc = dc_serial_qt_open(&serial_device, data->context, data->devname); +#endif +#ifdef SERIAL_FTDI + } else if (!strcmp(data->devname, "ftdi")) { + rc = dc_serial_ftdi_open(&serial_device, data->context); +#endif + } + + if (rc != DC_STATUS_SUCCESS) { + report_error(errmsg(rc)); + } else if (serial_device) { + rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); + } else { +#else + { +#endif + rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); + + if (rc != DC_STATUS_SUCCESS && subsurface_access(data->devname, R_OK | W_OK) != 0) + err = translate("gettextFromC", "Insufficient privileges to open the device %s %s (%s)"); + } + + if (rc == DC_STATUS_SUCCESS) { + err = do_device_import(data); + /* TODO: Show the logfile to the user on error. */ + dc_device_close(data->device); + data->device = NULL; + } + + dc_context_free(data->context); + data->context = NULL; + + if (fp) { + fclose(fp); + } + + return err; +} + +/* + * Parse data buffers instead of dc devices downloaded data. + * Intended to be used to parse profile data from binary files during import tasks. + * Actually included Uwatec families because of works on datatrak and smartrak logs + * and OSTC families for OSTCTools logs import. + * For others, simply include them in the switch (check parameters). + * Note that dc_descriptor_t in data *must* have been filled using dc_descriptor_iterator() + * calls. + */ +dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size) +{ + dc_status_t rc; + dc_parser_t *parser = NULL; + + switch (data->descriptor->type) { + case DC_FAMILY_UWATEC_ALADIN: + case DC_FAMILY_UWATEC_MEMOMOUSE: + rc = uwatec_memomouse_parser_create(&parser, data->context, 0, 0); + break; + case DC_FAMILY_UWATEC_SMART: + case DC_FAMILY_UWATEC_MERIDIAN: + rc = uwatec_smart_parser_create (&parser, data->context, data->descriptor->model, 0, 0); + break; + case DC_FAMILY_HW_OSTC: +#if defined(SSRF_CUSTOM_SERIAL) + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 0); +#else + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); +#endif + break; + case DC_FAMILY_HW_FROG: + case DC_FAMILY_HW_OSTC3: +#if defined(SSRF_CUSTOM_SERIAL) + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 1); +#else + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); +#endif + break; + default: + report_error("Device type not handled!"); + return DC_STATUS_UNSUPPORTED; + } + if (rc != DC_STATUS_SUCCESS) { + report_error("Error creating parser."); + dc_parser_destroy (parser); + return rc; + } + rc = dc_parser_set_data(parser, buffer, size); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error registering the data."); + dc_parser_destroy (parser); + return rc; + } + // Do not parse Aladin/Memomouse headers as they are fakes + // Do not return on error, we can still parse the samples + if (data->descriptor->type != DC_FAMILY_UWATEC_ALADIN && data->descriptor->type != DC_FAMILY_UWATEC_MEMOMOUSE) { + rc = libdc_header_parser (parser, data, dive); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error parsing the dive header data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); + } + } + rc = dc_parser_samples_foreach (parser, sample_cb, &dive->dc); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error parsing the sample data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); + dc_parser_destroy (parser); + return rc; + } + dc_parser_destroy(parser); + return(DC_STATUS_SUCCESS); +} diff --git a/core/libdivecomputer.h b/core/libdivecomputer.h new file mode 100644 index 000000000..99f1c2490 --- /dev/null +++ b/core/libdivecomputer.h @@ -0,0 +1,72 @@ +#ifndef LIBDIVECOMPUTER_H +#define LIBDIVECOMPUTER_H + + +/* libdivecomputer */ + +#ifdef DC_VERSION /* prevent a warning with wingdi.h */ +#undef DC_VERSION +#endif +#ifdef HAVE_LIBDIVECOMPUTER +#include <libdivecomputer/version.h> +#endif +#include <libdivecomputer/device.h> +#include <libdivecomputer/parser.h> + +#include "dive.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct dc_descriptor_t { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +/* don't forget to include the UI toolkit specific display-XXX.h first + to get the definition of progressbar_t */ +typedef struct device_data_t +{ + dc_descriptor_t *descriptor; + const char *vendor, *product, *devname; + const char *model; + uint32_t libdc_firmware, libdc_serial; + uint32_t deviceid, diveid; + dc_device_t *device; + dc_context_t *context; + struct dive_trip *trip; + int preexisting; + bool force_download; + bool create_new_trip; + bool libdc_log; + bool libdc_dump; + bool bluetooth_mode; + FILE *libdc_logfile; + struct dive_table *download_table; +} device_data_t; + +const char *errmsg (dc_status_t rc); +const char *do_libdivecomputer_import(device_data_t *data); +const char *do_uemis_import(device_data_t *data); +dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size); +void logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata); + +extern int import_thread_cancelled; +extern const char *progress_bar_text; +extern double progress_bar_fraction; +extern char *logfile_name; +extern char *dumpfile_name; + +#if SSRF_CUSTOM_SERIAL +extern dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr); +extern dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // LIBDIVECOMPUTER_H diff --git a/core/linux.c b/core/linux.c new file mode 100644 index 000000000..b81f6bf53 --- /dev/null +++ b/core/linux.c @@ -0,0 +1,232 @@ +/* linux.c */ +/* implements Linux specific functions */ +#include "dive.h" +#include "display.h" +#include "membuffer.h" +#include <string.h> +#include <sys/types.h> +#include <dirent.h> +#include <fnmatch.h> +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <pwd.h> + +// the DE should provide us with a default font and font size... +const char linux_system_divelist_default_font[] = "Sans"; +const char *system_divelist_default_font = linux_system_divelist_default_font; +double system_divelist_default_font_size = -1.0; + +void subsurface_OS_pref_setup(void) +{ + // nothing +} + +bool subsurface_ignore_font(const char *font) +{ + // there are no old default fonts to ignore + (void)font; + return false; +} + +void subsurface_user_info(struct user_info *user) +{ + struct passwd *pwd = getpwuid(getuid()); + const char *username = getenv("USER"); + + if (pwd) { + if (pwd->pw_gecos && *pwd->pw_gecos) + user->name = pwd->pw_gecos; + if (!username) + username = pwd->pw_name; + } + if (username && *username) { + char hostname[64]; + struct membuffer mb = {}; + gethostname(hostname, sizeof(hostname)); + put_format(&mb, "%s@%s", username, hostname); + user->email = mb_cstring(&mb); + } +} + +static const char *system_default_path_append(const char *append) +{ + const char *home = getenv("HOME"); + if (!home) + home = "~"; + const char *path = "/.subsurface"; + + int len = strlen(home) + strlen(path) + 1; + if (append) + len += strlen(append) + 1; + + char *buffer = (char *)malloc(len); + memset(buffer, 0, len); + strcat(buffer, home); + strcat(buffer, path); + if (append) { + strcat(buffer, "/"); + strcat(buffer, append); + } + + return buffer; +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static char *filename = NULL; + if (!filename) { + const char *user = getenv("LOGNAME"); + if (same_string(user, "")) + user = "username"; + filename = calloc(strlen(user) + 5, 1); + strcat(filename, user); + strcat(filename, ".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1, entries = 0; + DIR *dp = NULL; + struct dirent *ep = NULL; + size_t i; + FILE *file; + char *line = NULL; + char *fname; + size_t len; + if (dc_type != DC_TYPE_UEMIS) { + const char *dirname = "/dev"; + const char *patterns[] = { + "ttyUSB*", + "ttyS*", + "ttyACM*", + "rfcomm*", + NULL + }; + + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + for (i = 0; patterns[i] != NULL; ++i) { + if (fnmatch(patterns[i], ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= (int)sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + break; + } + } + } + closedir(dp); + } + if (dc_type != DC_TYPE_SERIAL) { + int num_uemis = 0; + file = fopen("/proc/mounts", "r"); + if (file == NULL) + return index; + + while ((getline(&line, &len, file)) != -1) { + char *ptr = strstr(line, "UEMISSDA"); + if (ptr) { + char *end = ptr, *start = ptr; + while (start > line && *start != ' ') + start--; + if (*start == ' ') + start++; + while (*end != ' ' && *end != '\0') + end++; + + *end = '\0'; + fname = strdup(start); + + callback(fname, userdata); + + if (is_default_dive_computer_device(fname)) + index = entries; + entries++; + num_uemis++; + free((void *)fname); + } + } + free(line); + fclose(file); + if (num_uemis == 1 && entries == 1) /* if we found only one and it's a mounted Uemis, pick it */ + index = 0; + } + return index; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + (void)dedicated; + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} + +bool subsurface_user_is_root() +{ + return (geteuid() == 0); +} diff --git a/core/liquivision.c b/core/liquivision.c new file mode 100644 index 000000000..9347a724a --- /dev/null +++ b/core/liquivision.c @@ -0,0 +1,420 @@ +#include <string.h> + +#include "dive.h" +#include "divelist.h" +#include "file.h" +#include "strndup.h" + +// Convert bytes into an INT +#define array_uint16_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) ) +#define array_uint32_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) + ((p)[2]<<16) \ + + ((p)[3]<<24)) + +struct lv_event { + time_t time; + struct pressure { + int sensor; + int mbar; + } pressure; +}; + +uint16_t primary_sensor; + +static int handle_event_ver2(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) +{ + (void) code; + (void) ps; + (void) ps_ptr; + (void) event; + + // Skip 4 bytes + return 4; +} + + +static int handle_event_ver3(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) +{ + int skip = 4; + uint16_t current_sensor; + + switch (code) { + case 0x0002: // Unknown + case 0x0004: // Unknown + skip = 4; + break; + case 0x0005: // Unknown + skip = 6; + break; + case 0x0007: // Gas + // 4 byte time + // 1 byte O2, 1 bye He + skip = 6; + break; + case 0x0008: + // 4 byte time + // 2 byte gas set point 2 + skip = 6; + break; + case 0x000f: + // Tank pressure + event->time = array_uint32_le(ps + ps_ptr); + + /* As far as I know, Liquivision supports 2 sensors, own and buddie's. This is my + * best guess how it is represented. */ + + current_sensor = array_uint16_le(ps + ps_ptr + 4); + if (primary_sensor == 0) { + primary_sensor = current_sensor; + } + if (current_sensor == primary_sensor) { + event->pressure.sensor = 0; + event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb + } else { + /* Ignoring the buddy sensor for no as we cannot draw it on the profile. + event->pressure.sensor = 1; + event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb + */ + } + // 1 byte PSR + // 1 byte ST + skip = 10; + break; + case 0x0010: + skip = 26; + break; + case 0x0015: // Unknown + skip = 2; + break; + default: + skip = 4; + break; + } + + return skip; +} + +static void parse_dives (int log_version, const unsigned char *buf, unsigned int buf_size) +{ + unsigned int ptr = 0; + unsigned char model; + + struct dive *dive; + struct divecomputer *dc; + struct sample *sample; + + while (ptr < buf_size) { + int i; + bool found_divesite = false; + dive = alloc_dive(); + primary_sensor = 0; + dc = &dive->dc; + + /* Just the main cylinder until we can handle the buddy cylinder porperly */ + for (i = 0; i < 1; i++) + fill_default_cylinder(&dive->cylinder[i]); + + // Model 0=Xen, 1,2=Xeo, 4=Lynx, other=Liquivision + model = *(buf + ptr); + switch (model) { + case 0: + dc->model = strdup("Xen"); + break; + case 1: + case 2: + dc->model = strdup("Xeo"); + break; + case 4: + dc->model = strdup("Lynx"); + break; + default: + dc->model = strdup("Liquivision"); + break; + } + ptr++; + + // Dive location, assemble Location and Place + unsigned int len, place_len; + char *location; + len = array_uint32_le(buf + ptr); + ptr += 4; + place_len = array_uint32_le(buf + ptr + len); + + if (len && place_len) { + location = malloc(len + place_len + 4); + memset(location, 0, len + place_len + 4); + memcpy(location, buf + ptr, len); + memcpy(location + len, ", ", 2); + memcpy(location + len + 2, buf + ptr + len + 4, place_len); + } else if (len) { + location = strndup((char *)buf + ptr, len); + } else if (place_len) { + location = strndup((char *)buf + ptr + len + 4, place_len); + } + + /* Store the location only if we have one */ + if (len || place_len) + found_divesite = true; + + ptr += len + 4 + place_len; + + // Dive comment + len = array_uint32_le(buf + ptr); + ptr += 4; + + // Blank notes are better than the default text + if (len && strncmp((char *)buf + ptr, "Comment ...", 11)) { + dive->notes = strndup((char *)buf + ptr, len); + } + ptr += len; + + dive->id = array_uint32_le(buf + ptr); + ptr += 4; + + dive->number = array_uint16_le(buf + ptr) + 1; + ptr += 2; + + dive->duration.seconds = array_uint32_le(buf + ptr); // seconds + ptr += 4; + + dive->maxdepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm + ptr += 2; + + dive->meandepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm + ptr += 2; + + dive->when = array_uint32_le(buf + ptr); + ptr += 4; + + // now that we have the dive time we can store the divesite + // (we need the dive time to create deterministic uuids) + if (found_divesite) { + dive->dive_site_uuid = find_or_create_dive_site_with_name(location, dive->when); + free(location); + } + //unsigned int end_time = array_uint32_le(buf + ptr); + ptr += 4; + + //unsigned int sit = array_uint32_le(buf + ptr); + ptr += 4; + //if (sit == 0xffffffff) { + //} + + dive->surface_pressure.mbar = array_uint16_le(buf + ptr); // ??? + ptr += 2; + + //unsigned int rep_dive = array_uint16_le(buf + ptr); + ptr += 2; + + dive->mintemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK + ptr += 2; + + dive->maxtemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK + ptr += 2; + + dive->salinity = *(buf + ptr); // ??? + ptr += 1; + + unsigned int sample_count = array_uint32_le(buf + ptr); + ptr += 4; + + // Sample interval + unsigned char sample_interval; + sample_interval = 1; + + unsigned char intervals[6] = {1,2,5,10,30,60}; + if (*(buf + ptr) < 6) + sample_interval = intervals[*(buf + ptr)]; + ptr += 1; + + float start_cns = 0; + unsigned char dive_mode = 0, algorithm = 0; + if (array_uint32_le(buf + ptr) != sample_count) { + // Xeo, with CNS and OTU + start_cns = *(float *) (buf + ptr); + ptr += 4; + dive->cns = *(float *) (buf + ptr); // end cns + ptr += 4; + dive->otu = *(float *) (buf + ptr); + ptr += 4; + dive_mode = *(buf + ptr++); // 0=Deco, 1=Gauge, 2=None + algorithm = *(buf + ptr++); // 0=ZH-L16C+GF + sample_count = array_uint32_le(buf + ptr); + } + // we aren't using the start_cns, dive_mode, and algorithm, yet + (void)start_cns; + (void)dive_mode; + (void)algorithm; + + ptr += 4; + + // Parse dive samples + const unsigned char *ds = buf + ptr; + const unsigned char *ts = buf + ptr + sample_count * 2 + 4; + const unsigned char *ps = buf + ptr + sample_count * 4 + 4; + unsigned int ps_count = array_uint32_le(ps); + ps += 4; + + // Bump ptr + ptr += sample_count * 4 + 4; + + // Handle events + unsigned int ps_ptr; + ps_ptr = 0; + + unsigned int event_code, d = 0, e; + struct lv_event event; + + // Loop through events + for (e = 0; e < ps_count; e++) { + // Get event + event_code = array_uint16_le(ps + ps_ptr); + ps_ptr += 2; + + if (log_version == 3) { + ps_ptr += handle_event_ver3(event_code, ps, ps_ptr, &event); + if (event_code != 0xf) + continue; // ignore all by pressure sensor event + } else { // version 2 + ps_ptr += handle_event_ver2(event_code, ps, ps_ptr, &event); + continue; // ignore all events + } + int sample_time, last_time; + int depth_mm, last_depth, temp_mk, last_temp; + + while (true) { + sample = prepare_sample(dc); + + // Get sample times + sample_time = d * sample_interval; + depth_mm = array_uint16_le(ds + d * 2) * 10; // cm->mm + temp_mk = C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); // dC->mK + last_time = (d ? (d - 1) * sample_interval : 0); + + if (d == sample_count) { + // We still have events to record + sample->time.seconds = event.time; + sample->depth.mm = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm + sample->temperature.mkelvin = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + finish_sample(dc); + + break; + } else if (event.time > sample_time) { + // Record sample and loop + sample->time.seconds = sample_time; + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + finish_sample(dc); + d++; + + continue; + } else if (event.time == sample_time) { + sample->time.seconds = sample_time; + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + finish_sample(dc); + + break; + } else { // Event is prior to sample + sample->time.seconds = event.time; + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + if (last_time == sample_time) { + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + } else { + // Extrapolate + last_depth = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm + last_temp = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK + sample->depth.mm = last_depth + (depth_mm - last_depth) + * (event.time - last_time) / sample_interval; + sample->temperature.mkelvin = last_temp + (temp_mk - last_temp) + * (event.time - last_time) / sample_interval; + } + finish_sample(dc); + + break; + } + } // while (true); + } // for each event sample + + // record trailing depth samples + for ( ;d < sample_count; d++) { + sample = prepare_sample(dc); + sample->time.seconds = d * sample_interval; + + sample->depth.mm = array_uint16_le(ds + d * 2) * 10; // cm->mm + sample->temperature.mkelvin = + C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); + finish_sample(dc); + } + + if (log_version == 3 && model == 4) { + // Advance to begin of next dive + switch (array_uint16_le(ps + ps_ptr)) { + case 0x0000: + ps_ptr += 5; + break; + case 0x0100: + ps_ptr += 7; + break; + case 0x0200: + ps_ptr += 9; + break; + case 0x0300: + ps_ptr += 11; + break; + case 0x0b0b: + ps_ptr += 27; + break; + } + + while (*(ps + ps_ptr) != 0x04) + ps_ptr++; + } + + // End dive + dive->downloaded = true; + record_dive(dive); + mark_divelist_changed(true); + + // Advance ptr for next dive + ptr += ps_ptr + 4; + } // while + + //DEBUG save_dives("/tmp/test.xml"); +} + +int try_to_open_liquivision(const char *filename, struct memblock *mem) +{ + (void) filename; + const unsigned char *buf = mem->buffer; + unsigned int buf_size = mem->size; + unsigned int ptr; + int log_version; + + // Get name length + unsigned int len = array_uint32_le(buf); + // Ignore length field and the name + ptr = 4 + len; + + unsigned int dive_count = array_uint32_le(buf + ptr); + if (dive_count == 0xffffffff) { + // File version 3.0 + log_version = 3; + ptr += 6; + dive_count = array_uint32_le(buf + ptr); + } else { + log_version = 2; + } + ptr += 4; + + parse_dives(log_version, buf + ptr, buf_size - ptr); + + return 1; +} diff --git a/core/load-git.c b/core/load-git.c new file mode 100644 index 000000000..a1f2d2031 --- /dev/null +++ b/core/load-git.c @@ -0,0 +1,1709 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <time.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <git2.h> +#include <libdivecomputer/parser.h> + +#include "gettext.h" + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "git-access.h" +#include "qthelperfromc.h" + +const char *saved_git_id = NULL; + +struct picture_entry_list { + void *data; + int len; + const char *hash; + struct picture_entry_list *next; +}; +struct picture_entry_list *pel = NULL; + +struct keyword_action { + const char *keyword; + void (*fn)(char *, struct membuffer *, void *); +}; +#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0])) + +extern degrees_t parse_degrees(char *buf, char **end); +git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry); + +static void save_picture_from_git(struct picture *picture) +{ + struct picture_entry_list *pic_entry = pel; + + while (pic_entry) { + if (same_string(pic_entry->hash, picture->hash)) { + savePictureLocal(picture, pic_entry->data, pic_entry->len); + return; + } + pic_entry = pic_entry->next; + } + fprintf(stderr, "didn't find picture entry for %s\n", picture->filename); +} + +static char *get_utf8(struct membuffer *b) +{ + int len = b->len; + char *res; + + if (!len) + return NULL; + res = malloc(len+1); + if (res) { + memcpy(res, b->buffer, len); + res[len] = 0; + } + return res; +} + +static temperature_t get_temperature(const char *line) +{ + temperature_t t; + t.mkelvin = C_to_mkelvin(ascii_strtod(line, NULL)); + return t; +} + +static depth_t get_depth(const char *line) +{ + depth_t d; + d.mm = rint(1000*ascii_strtod(line, NULL)); + return d; +} + +static volume_t get_volume(const char *line) +{ + volume_t v; + v.mliter = rint(1000*ascii_strtod(line, NULL)); + return v; +} + +static weight_t get_weight(const char *line) +{ + weight_t w; + w.grams = rint(1000*ascii_strtod(line, NULL)); + return w; +} + +static pressure_t get_pressure(const char *line) +{ + pressure_t p; + p.mbar = rint(1000*ascii_strtod(line, NULL)); + return p; +} + +static int get_salinity(const char *line) +{ + return rint(10*ascii_strtod(line, NULL)); +} + +static fraction_t get_fraction(const char *line) +{ + fraction_t f; + f.permille = rint(10*ascii_strtod(line, NULL)); + return f; +} + +static void update_date(timestamp_t *when, const char *line) +{ + unsigned yyyy, mm, dd; + struct tm tm; + + if (sscanf(line, "%04u-%02u-%02u", &yyyy, &mm, &dd) != 3) + return; + utc_mkdate(*when, &tm); + tm.tm_year = yyyy - 1900; + tm.tm_mon = mm - 1; + tm.tm_mday = dd; + *when = utc_mktime(&tm); +} + +static void update_time(timestamp_t *when, const char *line) +{ + unsigned h, m, s = 0; + struct tm tm; + + if (sscanf(line, "%02u:%02u:%02u", &h, &m, &s) < 2) + return; + utc_mkdate(*when, &tm); + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = s; + *when = utc_mktime(&tm); +} + +static duration_t get_duration(const char *line) +{ + int m = 0, s = 0; + duration_t d; + sscanf(line, "%d:%d", &m, &s); + d.seconds = m*60+s; + return d; +} + +static enum dive_comp_type get_dctype(const char *line) +{ + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { + if (strcmp(line, divemode_text[i]) == 0) + return i; + } + return 0; +} + +static int get_index(const char *line) +{ return atoi(line); } + +static int get_hex(const char *line) +{ return strtoul(line, NULL, 16); } + +/* this is in qthelper.cpp, so including the .h file is a pain */ +extern const char *printGPSCoords(int lat, int lon); + +static void parse_dive_gps(char *line, struct membuffer *str, void *_dive) +{ + (void) str; + uint32_t uuid; + degrees_t latitude = parse_degrees(line, &line); + degrees_t longitude = parse_degrees(line, &line); + struct dive *dive = _dive; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + uuid = get_dive_site_uuid_by_gps(latitude, longitude, NULL); + if (!uuid) + uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); + dive->dive_site_uuid = uuid; + } else { + if (dive_site_has_gps_location(ds) && + (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { + const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); + // we have a dive site that already has GPS coordinates + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); + free((void *)coords); + } + ds->latitude = latitude; + ds->longitude = longitude; + } + +} + +static void parse_dive_location(char *line, struct membuffer *str, void *_dive) +{ + (void) line; + uint32_t uuid; + char *name = get_utf8(str); + struct dive *dive = _dive; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + uuid = get_dive_site_uuid_by_name(name, NULL); + if (!uuid) + uuid = create_dive_site(name, dive->when); + dive->dive_site_uuid = uuid; + } else { + // we already had a dive site linked to the dive + if (same_string(ds->name, "")) { + ds->name = strdup(name); + } else { + // and that dive site had a name. that's weird - if our name is different, add it to the notes + if (!same_string(ds->name, name)) + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "additional name for site: %s\n"), name); + } + } + free(name); +} + +static void parse_dive_divemaster(char *line, struct membuffer *str, void *_dive) +{ (void) line; struct dive *dive = _dive; dive->divemaster = get_utf8(str); } + +static void parse_dive_buddy(char *line, struct membuffer *str, void *_dive) +{ (void) line; struct dive *dive = _dive; dive->buddy = get_utf8(str); } + +static void parse_dive_suit(char *line, struct membuffer *str, void *_dive) +{ (void) line; struct dive *dive = _dive; dive->suit = get_utf8(str); } + +static void parse_dive_notes(char *line, struct membuffer *str, void *_dive) +{ (void) line; struct dive *dive = _dive; dive->notes = get_utf8(str); } + +static void parse_dive_divesiteid(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->dive_site_uuid = get_hex(line); } + +/* + * We can have multiple tags in the membuffer. They are separated by + * NUL bytes. + */ +static void parse_dive_tags(char *line, struct membuffer *str, void *_dive) +{ + (void) line; + struct dive *dive = _dive; + const char *tag; + int len = str->len; + + if (!len) + return; + + /* Make sure there is a NUL at the end too */ + tag = mb_cstring(str); + for (;;) { + int taglen = strlen(tag); + if (taglen) + taglist_add_tag(&dive->tag_list, tag); + len -= taglen; + if (!len) + return; + tag += taglen+1; + len--; + } +} + +static void parse_dive_airtemp(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->airtemp = get_temperature(line); } + +static void parse_dive_watertemp(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->watertemp = get_temperature(line); } + +static void parse_dive_duration(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->duration = get_duration(line); } + +static void parse_dive_rating(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->rating = get_index(line); } + +static void parse_dive_visibility(char *line, struct membuffer *str, void *_dive) +{ (void) str; struct dive *dive = _dive; dive->visibility = get_index(line); } + +static void parse_dive_notrip(char *line, struct membuffer *str, void *_dive) +{ + (void) str; + (void) line; + struct dive *dive = _dive; dive->tripflag = NO_TRIP; +} + +static void parse_site_description(char *line, struct membuffer *str, void *_ds) +{ (void) line; struct dive_site *ds = _ds; ds->description = strdup(mb_cstring(str)); } + +static void parse_site_name(char *line, struct membuffer *str, void *_ds) +{ (void) line; struct dive_site *ds = _ds; ds->name = strdup(mb_cstring(str)); } + +static void parse_site_notes(char *line, struct membuffer *str, void *_ds) +{ (void) line; struct dive_site *ds = _ds; ds->notes = strdup(mb_cstring(str)); } + +extern degrees_t parse_degrees(char *buf, char **end); +static void parse_site_gps(char *line, struct membuffer *str, void *_ds) +{ + (void) str; + struct dive_site *ds = _ds; + + ds->latitude = parse_degrees(line, &line); + ds->longitude = parse_degrees(line, &line); +} + +static void parse_site_geo(char *line, struct membuffer *str, void *_ds) +{ + struct dive_site *ds = _ds; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + int nr = ds->taxonomy.nr; + if (nr < TC_NR_CATEGORIES) { + struct taxonomy *t = &ds->taxonomy.category[nr]; + t->value = strdup(mb_cstring(str)); + sscanf(line, "cat %d origin %d \"", &t->category, (int *)&t->origin); + ds->taxonomy.nr++; + } +} + +/* Parse key=val parts of samples and cylinders etc */ +static char *parse_keyvalue_entry(void (*fn)(void *, const char *, const char *), void *fndata, char *line) +{ + char *key = line, *val, c; + + while ((c = *line) != 0) { + if (isspace(c) || c == '=') + break; + line++; + } + + if (c == '=') + *line++ = 0; + val = line; + + while ((c = *line) != 0) { + if (isspace(c)) + break; + line++; + } + if (c) + *line++ = 0; + + fn(fndata, key, val); + return line; +} + +static int cylinder_index, weightsystem_index; + +static void parse_cylinder_keyvalue(void *_cylinder, const char *key, const char *value) +{ + cylinder_t *cylinder = _cylinder; + if (!strcmp(key, "vol")) { + cylinder->type.size = get_volume(value); + return; + } + if (!strcmp(key, "workpressure")) { + cylinder->type.workingpressure = get_pressure(value); + return; + } + /* This is handled by the "get_utf8()" */ + if (!strcmp(key, "description")) + return; + if (!strcmp(key, "o2")) { + cylinder->gasmix.o2 = get_fraction(value); + return; + } + if (!strcmp(key, "he")) { + cylinder->gasmix.he = get_fraction(value); + return; + } + if (!strcmp(key, "start")) { + cylinder->start = get_pressure(value); + return; + } + if (!strcmp(key, "end")) { + cylinder->end = get_pressure(value); + return; + } + if (!strcmp(key, "use")) { + cylinder->cylinder_use = cylinderuse_from_text(value); + return; + } + report_error("Unknown cylinder key/value pair (%s/%s)", key, value); +} + +static void parse_dive_cylinder(char *line, struct membuffer *str, void *_dive) +{ + struct dive *dive = _dive; + cylinder_t *cylinder = dive->cylinder + cylinder_index; + + cylinder_index++; + cylinder->type.description = get_utf8(str); + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_cylinder_keyvalue, cylinder, line); + } +} + +static void parse_weightsystem_keyvalue(void *_ws, const char *key, const char *value) +{ + weightsystem_t *ws = _ws; + if (!strcmp(key, "weight")) { + ws->weight = get_weight(value); + return; + } + /* This is handled by the "get_utf8()" */ + if (!strcmp(key, "description")) + return; + report_error("Unknown weightsystem key/value pair (%s/%s)", key, value); +} + +static void parse_dive_weightsystem(char *line, struct membuffer *str, void *_dive) +{ + struct dive *dive = _dive; + weightsystem_t *ws = dive->weightsystem + weightsystem_index; + + weightsystem_index++; + ws->description = get_utf8(str); + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_weightsystem_keyvalue, ws, line); + } +} + +static int match_action(char *line, struct membuffer *str, void *data, + struct keyword_action *action, unsigned nr_action) +{ + char *p = line, c; + unsigned low, high; + + while ((c = *p) >= 'a' && c <= 'z') // skip over 1st word + p++; // Extract the second word from the line: + if (p == line) + return -1; + switch (c) { + case 0: // if 2nd word is C-terminated + break; + case ' ': // =end of 2nd word? + *p++ = 0; // then C-terminate that word + break; + default: + return -1; + } + + /* Standard binary search in a table */ + low = 0; + high = nr_action; + while (low < high) { + unsigned mid = (low+high)/2; + struct keyword_action *a = action + mid; + int cmp = strcmp(line, a->keyword); + if (!cmp) { // attribute found: + a->fn(p, str, data); // Execute appropriate function, + return 0; // .. passing 2n word from above + } // (p) as a function argument. + if (cmp < 0) + high = mid; + else + low = mid+1; + } +report_error("Unmatched action '%s'", line); + return -1; +} + +/* FIXME! We should do the array thing here too. */ +static void parse_sample_keyvalue(void *_sample, const char *key, const char *value) +{ + struct sample *sample = _sample; + + if (!strcmp(key, "sensor")) { + sample->sensor = atoi(value); + return; + } + if (!strcmp(key, "ndl")) { + sample->ndl = get_duration(value); + return; + } + if (!strcmp(key, "tts")) { + sample->tts = get_duration(value); + return; + } + if (!strcmp(key, "in_deco")) { + sample->in_deco = atoi(value); + return; + } + if (!strcmp(key, "stoptime")) { + sample->stoptime = get_duration(value); + return; + } + if (!strcmp(key, "stopdepth")) { + sample->stopdepth = get_depth(value); + return; + } + if (!strcmp(key, "cns")) { + sample->cns = atoi(value); + return; + } + + if (!strcmp(key, "rbt")) { + sample->rbt = get_duration(value); + return; + } + + if (!strcmp(key, "po2")) { + pressure_t p = get_pressure(value); + sample->setpoint.mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor1")) { + pressure_t p = get_pressure(value); + sample->o2sensor[0].mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor2")) { + pressure_t p = get_pressure(value); + sample->o2sensor[1].mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor3")) { + pressure_t p = get_pressure(value); + sample->o2sensor[2].mbar = p.mbar; + return; + } + if (!strcmp(key, "o2pressure")) { + pressure_t p = get_pressure(value); + sample->o2cylinderpressure.mbar = p.mbar; + return; + } + if (!strcmp(key, "heartbeat")) { + sample->heartbeat = atoi(value); + return; + } + if (!strcmp(key, "bearing")) { + sample->bearing.degrees = atoi(value); + return; + } + + report_error("Unexpected sample key/value pair (%s/%s)", key, value); +} + +static char *parse_sample_unit(struct sample *sample, double val, char *unit) +{ + char *end = unit, c; + + /* Skip over the unit */ + while ((c = *end) != 0) { + if (isspace(c)) { + *end++ = 0; + break; + } + end++; + } + + /* The units are "°C", "m" or "bar", so let's just look at the first character */ + switch (*unit) { + case 'm': + sample->depth.mm = rint(1000*val); + break; + case 'b': + sample->cylinderpressure.mbar = rint(1000*val); + break; + default: + sample->temperature.mkelvin = C_to_mkelvin(val); + break; + } + + return end; +} + +/* + * By default the sample data does not change unless the + * save-file gives an explicit new value. So we copy the + * data from the previous sample if one exists, and then + * the parsing will update it as necessary. + * + * There are a few exceptions, like the sample pressure: + * missing sample pressure doesn't mean "same as last + * time", but "interpolate". We clear those ones + * explicitly. + */ +static struct sample *new_sample(struct divecomputer *dc) +{ + struct sample *sample = prepare_sample(dc); + if (sample != dc->sample) { + memcpy(sample, sample-1, sizeof(struct sample)); + sample->cylinderpressure.mbar = 0; + } + return sample; +} + +static void sample_parser(char *line, struct divecomputer *dc) +{ + int m, s = 0; + struct sample *sample = new_sample(dc); + + m = strtol(line, &line, 10); + if (*line == ':') + s = strtol(line+1, &line, 10); + sample->time.seconds = m*60+s; + + for (;;) { + char c; + + while (isspace(c = *line)) + line++; + if (!c) + break; + /* Less common sample entries have a name */ + if (c >= 'a' && c <= 'z') { + line = parse_keyvalue_entry(parse_sample_keyvalue, sample, line); + } else { + const char *end; + double val = ascii_strtod(line, &end); + if (end == line) { + report_error("Odd sample data: %s", line); + break; + } + line = (char *)end; + line = parse_sample_unit(sample, val, line); + } + } + finish_sample(dc); +} + +static void parse_dc_airtemp(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->airtemp = get_temperature(line); } + +static void parse_dc_date(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; update_date(&dc->when, line); } + +static void parse_dc_deviceid(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->deviceid = get_hex(line); } + +static void parse_dc_diveid(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->diveid = get_hex(line); } + +static void parse_dc_duration(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->duration = get_duration(line); } + +static void parse_dc_dctype(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->divemode = get_dctype(line); } + +static void parse_dc_maxdepth(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->maxdepth = get_depth(line); } + +static void parse_dc_meandepth(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->meandepth = get_depth(line); } + +static void parse_dc_model(char *line, struct membuffer *str, void *_dc) +{ (void) line; struct divecomputer *dc = _dc; dc->model = get_utf8(str); } + +static void parse_dc_numberofoxygensensors(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->no_o2sensors = get_index(line); } + +static void parse_dc_surfacepressure(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->surface_pressure = get_pressure(line); } + +static void parse_dc_salinity(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->salinity = get_salinity(line); } + +static void parse_dc_surfacetime(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->surfacetime = get_duration(line); } + +static void parse_dc_time(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; update_time(&dc->when, line); } + +static void parse_dc_watertemp(char *line, struct membuffer *str, void *_dc) +{ (void) str; struct divecomputer *dc = _dc; dc->watertemp = get_temperature(line); } + +static void parse_event_keyvalue(void *_event, const char *key, const char *value) +{ + struct event *event = _event; + int val = atoi(value); + + if (!strcmp(key, "type")) { + event->type = val; + } else if (!strcmp(key, "flags")) { + event->flags = val; + } else if (!strcmp(key, "value")) { + event->value = val; + } else if (!strcmp(key, "name")) { + /* We get the name from the string handling */ + } else if (!strcmp(key, "cylinder")) { + /* NOTE! We add one here as a marker that "yes, we got a cylinder index" */ + event->gas.index = 1+get_index(value); + } else if (!strcmp(key, "o2")) { + event->gas.mix.o2 = get_fraction(value); + } else if (!strcmp(key, "he")) { + event->gas.mix.he = get_fraction(value); + } else + report_error("Unexpected event key/value pair (%s/%s)", key, value); +} + +/* keyvalue "key" "value" + * so we have two strings (possibly empty) in the membuffer, separated by a '\0' */ +static void parse_dc_keyvalue(char *line, struct membuffer *str, void *_dc) +{ + const char *key, *value; + struct divecomputer *dc = _dc; + + // Let's make sure we have two strings... + int string_counter = 0; + while(*line) { + if (*line == '"') + string_counter++; + line++; + } + if (string_counter != 2) + return; + + // stupidly the second string in the membuffer isn't NUL terminated; + // asking for a cstring fixes that; interestingly enough, given that there are two + // strings in the mb, the next command at the same time assigns a pointer to the + // first string to 'key' and NUL terminates the second string (which then goes to 'value') + key = mb_cstring(str); + value = key + strlen(key) + 1; + add_extra_data(dc, key, value); +} + +static void parse_dc_event(char *line, struct membuffer *str, void *_dc) +{ + int m, s = 0; + const char *name; + struct divecomputer *dc = _dc; + struct event event = { 0 }, *ev; + + m = strtol(line, &line, 10); + if (*line == ':') + s = strtol(line+1, &line, 10); + event.time.seconds = m*60+s; + + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_event_keyvalue, &event, line); + } + + name = ""; + if (str->len) + name = mb_cstring(str); + ev = add_event(dc, event.time.seconds, event.type, event.flags, event.value, name); + + /* + * Older logs might mark the dive to be CCR by having an "SP change" event at time 0:00. + * Better to mark them being CCR on import so no need for special treatments elsewhere on + * the code. + */ + if (ev && event.time.seconds == 0 && event.type == SAMPLE_EVENT_PO2 && dc->divemode==OC) { + dc->divemode = CCR; + } + + if (ev && event_is_gaschange(ev)) { + /* + * We subtract one here because "0" is "no index", + * and the parsing will add one for actual cylinder + * index data (see parse_event_keyvalue) + */ + ev->gas.index = event.gas.index-1; + if (event.gas.mix.o2.permille || event.gas.mix.he.permille) + ev->gas.mix = event.gas.mix; + } +} + +static void parse_trip_date(char *line, struct membuffer *str, void *_trip) +{ (void) str; dive_trip_t *trip = _trip; update_date(&trip->when, line); } + +static void parse_trip_time(char *line, struct membuffer *str, void *_trip) +{ (void) str; dive_trip_t *trip = _trip; update_time(&trip->when, line); } + +static void parse_trip_location(char *line, struct membuffer *str, void *_trip) +{ (void) line; dive_trip_t *trip = _trip; trip->location = get_utf8(str); } + +static void parse_trip_notes(char *line, struct membuffer *str, void *_trip) +{ (void) line; dive_trip_t *trip = _trip; trip->notes = get_utf8(str); } + +static void parse_settings_autogroup(char *line, struct membuffer *str, void *_unused) +{ + (void) line; + (void) str; + (void) _unused; + set_autogroup(1); +} + +static void parse_settings_units(char *line, struct membuffer *str, void *unused) +{ + (void) str; + (void) unused; + if (line) + set_informational_units(line); +} + +static void parse_settings_userid(char *line, struct membuffer *str, void *_unused) +{ + (void) str; + (void) _unused; + if (line) { + set_save_userid_local(true); + set_userid(line); + } +} + +/* + * Our versioning is a joke right now, but this is more of an example of what we + * *can* do some day. And if we do change the version, this warning will show if + * you read with a version of subsurface that doesn't know about it. + * We MUST keep this in sync with the XML version (so we can report a consistent + * minimum datafile version) + */ +static void parse_settings_version(char *line, struct membuffer *str, void *_unused) +{ + (void) str; + (void) _unused; + int version = atoi(line); + report_datafile_version(version); + if (version > DATAFORMAT_VERSION) + report_error("Git save file version %d is newer than version %d I know about", version, DATAFORMAT_VERSION); +} + +/* The string in the membuffer is the version string of subsurface that saved things, just FYI */ +static void parse_settings_subsurface(char *line, struct membuffer *str, void *_unused) +{ + (void) line; + (void) str; + (void) _unused; +} + +struct divecomputerid { + const char *model; + const char *nickname; + const char *firmware; + const char *serial; + const char *cstr; + unsigned int deviceid; +}; + +static void parse_divecomputerid_keyvalue(void *_cid, const char *key, const char *value) +{ + struct divecomputerid *cid = _cid; + + if (*value == '"') { + value = cid->cstr; + cid->cstr += strlen(cid->cstr)+1; + } + if (!strcmp(key, "deviceid")) { + cid->deviceid = get_hex(value); + return; + } + if (!strcmp(key, "serial")) { + cid->serial = value; + return; + } + if (!strcmp(key, "firmware")) { + cid->firmware = value; + return; + } + if (!strcmp(key, "nickname")) { + cid->nickname = value; + return; + } + report_error("Unknow divecomputerid key/value pair (%s/%s)", key, value); +} + +/* + * The 'divecomputerid' is a bit harder to parse than some other things, because + * it can have multiple strings (but see the tag parsing for another example of + * that) in addition to the non-string entries. + * + * We keep the "next" string in "id.cstr" and update it as we use it. + */ +static void parse_settings_divecomputerid(char *line, struct membuffer *str, void *_unused) +{ + (void) _unused; + struct divecomputerid id = { mb_cstring(str) }; + + id.cstr = id.model + strlen(id.model) + 1; + + /* Skip the '"' that stood for the model string */ + line++; + + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_divecomputerid_keyvalue, &id, line); + } + create_device_node(id.model, id.deviceid, id.serial, id.firmware, id.nickname); +} + +static void parse_picture_filename(char *line, struct membuffer *str, void *_pic) +{ + (void) line; + struct picture *pic = _pic; + pic->filename = get_utf8(str); +} + +static void parse_picture_gps(char *line, struct membuffer *str, void *_pic) +{ + (void) str; + struct picture *pic = _pic; + + pic->latitude = parse_degrees(line, &line); + pic->longitude = parse_degrees(line, &line); +} + +static void parse_picture_hash(char *line, struct membuffer *str, void *_pic) +{ + (void) line; + struct picture *pic = _pic; + pic->hash = get_utf8(str); +} + +/* These need to be sorted! */ +struct keyword_action dc_action[] = { +#undef D +#define D(x) { #x, parse_dc_ ## x } + D(airtemp), D(date), D(dctype), D(deviceid), D(diveid), D(duration), + D(event), D(keyvalue), D(maxdepth), D(meandepth), D(model), D(numberofoxygensensors), + D(salinity), D(surfacepressure), D(surfacetime), D(time), D(watertemp) +}; + +/* Sample lines start with a space or a number */ +static void divecomputer_parser(char *line, struct membuffer *str, void *_dc) +{ + char c = *line; + if (c < 'a' || c > 'z') + sample_parser(line, _dc); + match_action(line, str, _dc, dc_action, ARRAY_SIZE(dc_action)); +} + +/* These need to be sorted! */ +struct keyword_action dive_action[] = { +#undef D +#define D(x) { #x, parse_dive_ ## x } + D(airtemp), D(buddy), D(cylinder), D(divemaster), D(divesiteid), D(duration), + D(gps), D(location), D(notes), D(notrip), D(rating), D(suit), + D(tags), D(visibility), D(watertemp), D(weightsystem) +}; + +static void dive_parser(char *line, struct membuffer *str, void *_dive) +{ + match_action(line, str, _dive, dive_action, ARRAY_SIZE(dive_action)); +} + +/* These need to be sorted! */ +struct keyword_action site_action[] = { +#undef D +#define D(x) { #x, parse_site_ ## x } + D(description), D(geo), D(gps), D(name), D(notes) +}; + +static void site_parser(char *line, struct membuffer *str, void *_ds) +{ + match_action(line, str, _ds, site_action, ARRAY_SIZE(site_action)); +} + +/* These need to be sorted! */ +struct keyword_action trip_action[] = { +#undef D +#define D(x) { #x, parse_trip_ ## x } + D(date), D(location), D(notes), D(time), +}; + +static void trip_parser(char *line, struct membuffer *str, void *_trip) +{ + match_action(line, str, _trip, trip_action, ARRAY_SIZE(trip_action)); +} + +/* These need to be sorted! */ +static struct keyword_action settings_action[] = { +#undef D +#define D(x) { #x, parse_settings_ ## x } + D(autogroup), D(divecomputerid), D(subsurface), D(units), D(userid), D(version), +}; + +static void settings_parser(char *line, struct membuffer *str, void *_unused) +{ + (void) _unused; + match_action(line, str, NULL, settings_action, ARRAY_SIZE(settings_action)); +} + +/* These need to be sorted! */ +static struct keyword_action picture_action[] = { +#undef D +#define D(x) { #x, parse_picture_ ## x } + D(filename), D(gps), D(hash) +}; + +static void picture_parser(char *line, struct membuffer *str, void *_pic) +{ + match_action(line, str, _pic, picture_action, ARRAY_SIZE(picture_action)); +} + +/* + * We have a very simple line-based interface, with the small + * complication that lines can have strings in the middle, and + * a string can be multiple lines. + * + * The UTF-8 string escaping is *very* simple, though: + * + * - a string starts and ends with double quotes (") + * + * - inside the string we escape: + * (a) double quotes with '\"' + * (b) backslash (\) with '\\' + * + * - additionally, for human readability, we escape + * newlines with '\n\t', with the exception that + * consecutive newlines are left unescaped (so an + * empty line doesn't become a line with just a tab + * on it). + * + * Also, while the UTF-8 string can have arbitrarily + * long lines, the non-string parts of the lines are + * never long, so we can use a small temporary buffer + * on stack for that part. + * + * Also, note that if a line has one or more strings + * in it: + * + * - each string will be represented as a single '"' + * character in the output. + * + * - all string will exist in the same 'membuffer', + * separated by NUL characters (that cannot exist + * in a string, not even quoted). + */ +static const char *parse_one_string(const char *buf, const char *end, struct membuffer *b) +{ + const char *p = buf; + + /* + * We turn multiple strings one one line (think dive tags) into one + * membuffer that has NUL characters in between strings. + */ + if (b->len) + put_bytes(b, "", 1); + + while (p < end) { + char replace; + + switch (*p++) { + default: + continue; + case '\n': + if (p < end && *p == '\t') { + replace = '\n'; + break; + } + continue; + case '\\': + if (p < end) { + replace = *p; + break; + } + continue; + case '"': + replace = 0; + break; + } + put_bytes(b, buf, p - buf - 1); + if (!replace) + break; + put_bytes(b, &replace, 1); + buf = ++p; + } + return p; +} + +typedef void (line_fn_t)(char *, struct membuffer *, void *); +#define MAXLINE 500 +static unsigned parse_one_line(const char *buf, unsigned size, line_fn_t *fn, void *fndata, struct membuffer *b) +{ + const char *end = buf + size; + const char *p = buf; + char line[MAXLINE+1]; + int off = 0; + + while (p < end) { + char c = *p++; + if (c == '\n') + break; + line[off] = c; + off++; + if (off > MAXLINE) + off = MAXLINE; + if (c == '"') + p = parse_one_string(p, end, b); + } + line[off] = 0; + fn(line, b, fndata); + return p - buf; +} + +/* + * We keep on re-using the membuffer that we use for + * strings, but the callback function can "steal" it by + * saving its value and just clear the original. + */ +static void for_each_line(git_blob *blob, line_fn_t *fn, void *fndata) +{ + const char *content = git_blob_rawcontent(blob); + unsigned int size = git_blob_rawsize(blob); + struct membuffer str = { 0 }; + + while (size) { + unsigned int n = parse_one_line(content, size, fn, fndata, &str); + content += n; + size -= n; + + /* Re-use the allocation, but forget the data */ + str.len = 0; + } + free_buffer(&str); +} + +#define GIT_WALK_OK 0 +#define GIT_WALK_SKIP 1 + +static struct divecomputer *active_dc; +static struct dive *active_dive; +static dive_trip_t *active_trip; + +static void finish_active_trip(void) +{ + dive_trip_t *trip = active_trip; + + if (trip) { + active_trip = NULL; + insert_trip(&trip); + } +} + +static void finish_active_dive(void) +{ + struct dive *dive = active_dive; + + if (dive) { + /* check if we need to save pictures */ + FOR_EACH_PICTURE(dive) { + if (!picture_exists(picture)) + save_picture_from_git(picture); + } + /* free any memory we allocated to track pictures */ + while (pel) { + free(pel->data); + void *lastone = pel; + pel = pel->next; + free(lastone); + } + active_dive = NULL; + record_dive(dive); + } +} + +static struct dive *create_new_dive(timestamp_t when) +{ + struct dive *dive = alloc_dive(); + + /* We'll fill in more data from the dive file */ + dive->when = when; + + if (active_trip) + add_dive_to_trip(dive, active_trip); + return dive; +} + +static dive_trip_t *create_new_trip(int yyyy, int mm, int dd) +{ + dive_trip_t *trip = calloc(1, sizeof(dive_trip_t)); + struct tm tm = { 0 }; + + /* We'll fill in the real data from the trip descriptor file */ + tm.tm_year = yyyy; + tm.tm_mon = mm-1; + tm.tm_mday = dd; + trip->when = utc_mktime(&tm); + + return trip; +} + +static bool validate_date(int yyyy, int mm, int dd) +{ + return yyyy > 1970 && yyyy < 3000 && + mm > 0 && mm < 13 && + dd > 0 && dd < 32; +} + +static bool validate_time(int h, int m, int s) +{ + return h >= 0 && h < 24 && + m >= 0 && m < 60 && + s >=0 && s <= 60; +} + +/* + * Dive trip directory, name is 'nn-alphabetic[~hex]' + */ +static int dive_trip_directory(const char *root, const char *name) +{ + int yyyy = -1, mm = -1, dd = -1; + + if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) + return GIT_WALK_SKIP; + dd = atoi(name); + if (!validate_date(yyyy, mm, dd)) + return GIT_WALK_SKIP; + finish_active_trip(); + active_trip = create_new_trip(yyyy, mm, dd); + return GIT_WALK_OK; +} + +/* + * Dive directory, name is [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] in older git repositories + * but [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] in newer repos as ':' is an illegal character for Windows files + * and 'timeoff' points to what should be the time part of + * the name (the first digit of the hour). + * + * The root path will be of the form yyyy/mm[/tripdir], + */ +static int dive_directory(const char *root, const git_tree_entry *entry, const char *name, int timeoff) +{ + int yyyy = -1, mm = -1, dd = -1; + int h, m, s; + int mday_off, month_off, year_off; + struct tm tm; + + /* Skip the '-' before the time */ + mday_off = timeoff; + if (!mday_off || name[--mday_off] != '-') + return GIT_WALK_SKIP; + /* Skip the day name */ + while (mday_off > 0 && name[--mday_off] != '-') + /* nothing */; + + mday_off = mday_off - 2; + month_off = mday_off - 3; + year_off = month_off - 5; + if (mday_off < 0) + return GIT_WALK_SKIP; + + /* Get the time of day -- parse both time formats so we can read old repos when not on Windows */ + if (sscanf(name+timeoff, "%d:%d:%d", &h, &m, &s) != 3 && sscanf(name+timeoff, "%d=%d=%d", &h, &m, &s) != 3) + return GIT_WALK_SKIP; + if (!validate_time(h, m, s)) + return GIT_WALK_SKIP; + + /* + * Using the "git_tree_walk()" interface is simple, but + * it kind of sucks as an interface because there is + * no sane way to pass the hierarchy to the callbacks. + * The "payload" is a fixed one-time thing: we'd like + * the "current trip" to be passed down to the dives + * that get parsed under that trip, but we can't. + * + * So "active_trip" is not the trip that is in the hierarchy + * _above_ us, it's just the trip that was _before_ us. But + * if a dive is not in a trip at all, we can't tell. + * + * We could just do a better walker that passes the + * return value around, but we hack around this by + * instead looking at the one hierarchical piece of + * data we have: the pathname to the current entry. + * + * This is pretty hacky. The magic '8' is the length + * of a pathname of the form 'yyyy/mm/'. + */ + if (strlen(root) == 8) + finish_active_trip(); + + /* + * Get the date. The day of the month is in the dive directory + * name, the year and month might be in the path leading up + * to it. + */ + dd = atoi(name + mday_off); + if (year_off < 0) { + if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) + return GIT_WALK_SKIP; + } else + yyyy = atoi(name + year_off); + if (month_off >= 0) + mm = atoi(name + month_off); + + if (!validate_date(yyyy, mm, dd)) + return GIT_WALK_SKIP; + + /* Ok, close enough. We've gotten sufficient information */ + memset(&tm, 0, sizeof(tm)); + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = s; + tm.tm_year = yyyy - 1900; + tm.tm_mon = mm-1; + tm.tm_mday = dd; + + finish_active_dive(); + active_dive = create_new_dive(utc_mktime(&tm)); + memcpy(active_dive->git_id, git_tree_entry_id(entry)->id, 20); + return GIT_WALK_OK; +} + +static int picture_directory(const char *root, const char *name) +{ + (void) root; + (void) name; + if (!active_dive) + return GIT_WALK_SKIP; + return GIT_WALK_OK; +} + +/* + * Return the length of the string without the unique part. + */ +static int nonunique_length(const char *str) +{ + int len = 0; + + for (;;) { + char c = *str++; + if (!c || c == '~') + return len; + len++; + } +} + +/* + * When hitting a directory node, we have a couple of cases: + * + * - It's just a date entry - all numeric (either year or month): + * + * [yyyy|mm] + * + * We don't do anything with these, we just traverse into them. + * The numeric data will show up as part of the full path when + * we hit more interesting entries. + * + * - It's a trip directory. The name will be of the form + * + * nn-alphabetic[~hex] + * + * where 'nn' is the day of the month (year and month will be + * encoded in the path leading up to this). + * + * - It's a dive directory. The name will be of the form + * + * [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] + * + * (older versions had this as [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] + * but that faile on Windows) + * + * which describes the date and time of a dive (yyyy and mm + * are optional, and may be encoded in the path leading up to + * the dive). + * + * - It is a per-dive picture directory ("Pictures") + * + * - It's some random non-dive-data directory. + * + * If it doesn't match the above patterns, we'll ignore them + * for dive loading purposes, and not even recurse into them. + */ +static int walk_tree_directory(const char *root, const git_tree_entry *entry) +{ + const char *name = git_tree_entry_name(entry); + int digits = 0, len; + char c; + + if (!strcmp(name, "Pictures")) + return picture_directory(root, name); + + if (!strcmp(name, "01-Divesites")) + return GIT_WALK_OK; + + while (isdigit(c = name[digits])) + digits++; + + /* Doesn't start with two or four digits? Skip */ + if (digits != 4 && digits != 2) + return GIT_WALK_SKIP; + + /* Only digits? Do nothing, but recurse into it */ + if (!c) + return GIT_WALK_OK; + + /* All valid cases need to have a slash following */ + if (c != '-') + return GIT_WALK_SKIP; + + /* Do a quick check for a common dive case */ + len = nonunique_length(name); + + /* + * We know the len is at least 3, because we had at least + * two digits and a dash + */ + if (name[len-3] == ':' || name[len-3] == '=') + return dive_directory(root, entry, name, len-8); + + if (digits != 2) + return GIT_WALK_SKIP; + + return dive_trip_directory(root, name); +} + +git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry) +{ + const git_oid *id = git_tree_entry_id(entry); + git_blob *blob; + + if (git_blob_lookup(&blob, repo, id)) + return NULL; + return blob; +} + +static struct divecomputer *create_new_dc(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + + while (dc->next) + dc = dc->next; + /* Did we already fill that in? */ + if (dc->samples || dc->model || dc->when) { + struct divecomputer *newdc = calloc(1, sizeof(*newdc)); + if (!newdc) + return NULL; + dc->next = newdc; + dc = newdc; + } + dc->when = dive->when; + dc->duration = dive->duration; + return dc; +} + +/* + * We should *really* try to delay the dive computer data parsing + * until necessary, in order to reduce load-time. The parsing is + * cheap, but the loading of the git blob into memory can be pretty + * costly. + */ +static int parse_divecomputer_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + (void) suffix; + git_blob *blob = git_tree_entry_blob(repo, entry); + + if (!blob) + return report_error("Unable to read divecomputer file"); + + active_dc = create_new_dc(active_dive); + for_each_line(blob, divecomputer_parser, active_dc); + git_blob_free(blob); + active_dc = NULL; + return 0; +} + +/* + * NOTE! The "git_id" for the dive is the hash for the whole dive directory. + * As such, it covers not just the dive, but the divecomputers and the + * pictures too. So if any of the dive computers change, the dive cache + * has to be invalidated too. + */ +static int parse_dive_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + struct dive *dive = active_dive; + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read dive file"); + if (*suffix) + dive->number = atoi(suffix+1); + cylinder_index = weightsystem_index = 0; + for_each_line(blob, dive_parser, active_dive); + git_blob_free(blob); + return 0; +} + +static int parse_site_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + if (*suffix == '\0') + return report_error("Dive site without uuid"); + uint32_t uuid = strtoul(suffix, NULL, 16); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read dive site file"); + for_each_line(blob, site_parser, ds); + git_blob_free(blob); + return 0; +} + +static int parse_trip_entry(git_repository *repo, const git_tree_entry *entry) +{ + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read trip file"); + for_each_line(blob, trip_parser, active_trip); + git_blob_free(blob); + return 0; +} + +static int parse_settings_entry(git_repository *repo, const git_tree_entry *entry) +{ + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read settings file"); + set_save_userid_local(false); + for_each_line(blob, settings_parser, NULL); + git_blob_free(blob); + return 0; +} + +static int parse_picture_file(git_repository *repo, const git_tree_entry *entry, const char *name) +{ + /* remember the picture data so we can handle it when all dive data has been loaded + * the name of the git file is PIC-<hash> */ + git_blob *blob = git_tree_entry_blob(repo, entry); + const void *rawdata = git_blob_rawcontent(blob); + int len = git_blob_rawsize(blob); + struct picture_entry_list *new_pel = malloc(sizeof(struct picture_entry_list)); + new_pel->next = pel; + pel = new_pel; + pel->data = malloc(len); + memcpy(pel->data, rawdata, len); + pel->len = len; + pel->hash = strdup(name + 4); + git_blob_free(blob); + return 0; +} + +static int parse_picture_entry(git_repository *repo, const git_tree_entry *entry, const char *name) +{ + git_blob *blob; + struct picture *pic; + int hh, mm, ss, offset; + char sign; + + /* + * The format of the picture name files is just the offset within + * the dive in form [[+-]hh=mm=ss (previously [[+-]hh:mm:ss, but + * that didn't work on Windows), possibly followed by a hash to + * make the filename unique (which we can just ignore). + */ + if (sscanf(name, "%c%d:%d:%d", &sign, &hh, &mm, &ss) != 4 && + sscanf(name, "%c%d=%d=%d", &sign, &hh, &mm, &ss) != 4) + return report_error("Unknown file name %s", name); + offset = ss + 60*(mm + 60*hh); + if (sign == '-') + offset = -offset; + + blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read picture file"); + + pic = alloc_picture(); + pic->offset.seconds = offset; + + for_each_line(blob, picture_parser, pic); + dive_add_picture(active_dive, pic); + git_blob_free(blob); + return 0; +} + +static int walk_tree_file(const char *root, const git_tree_entry *entry, git_repository *repo) +{ + struct dive *dive = active_dive; + dive_trip_t *trip = active_trip; + const char *name = git_tree_entry_name(entry); + if (verbose > 1) + fprintf(stderr, "git load handling file %s\n", name); + switch (*name) { + /* Picture file? They are saved as time offsets in the dive */ + case '-': case '+': + if (dive) + return parse_picture_entry(repo, entry, name); + break; + case 'D': + if (dive && !strncmp(name, "Divecomputer", 12)) + return parse_divecomputer_entry(repo, entry, name+12); + if (dive && !strncmp(name, "Dive", 4)) + return parse_dive_entry(repo, entry, name+4); + break; + case 'S': + if (!strncmp(name, "Site", 4)) + return parse_site_entry(repo, entry, name + 5); + break; + case '0': + if (trip && !strcmp(name, "00-Trip")) + return parse_trip_entry(repo, entry); + if (!strcmp(name, "00-Subsurface")) + return parse_settings_entry(repo, entry); + break; + case 'P': + if (dive && !strncmp(name, "PIC-", 4)) + return parse_picture_file(repo, entry, name); + break; + } + report_error("Unknown file %s%s (%p %p)", root, name, dive, trip); + return GIT_WALK_SKIP; +} + +static int walk_tree_cb(const char *root, const git_tree_entry *entry, void *payload) +{ + git_repository *repo = payload; + git_filemode_t mode = git_tree_entry_filemode(entry); + + if (mode == GIT_FILEMODE_TREE) + return walk_tree_directory(root, entry); + + walk_tree_file(root, entry, repo); + /* Ignore failed blob loads */ + return GIT_WALK_OK; +} + +static int load_dives_from_tree(git_repository *repo, git_tree *tree) +{ + git_tree_walk(tree, GIT_TREEWALK_PRE, walk_tree_cb, repo); + return 0; +} + +void clear_git_id(void) +{ + saved_git_id = NULL; +} + +void set_git_id(const struct git_oid * id) +{ + static char git_id_buffer[GIT_OID_HEXSZ+1]; + + git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), id); + saved_git_id = git_id_buffer; +} + +static int find_commit(git_repository *repo, const char *branch, git_commit **commit_p) +{ + git_object *object; + + if (git_revparse_single(&object, repo, branch)) + return report_error("Unable to look up revision '%s'", branch); + if (git_object_peel((git_object **)commit_p, object, GIT_OBJ_COMMIT)) + return report_error("Revision '%s' is not a valid commit", branch); + return 0; +} + +static int do_git_load(git_repository *repo, const char *branch) +{ + int ret; + git_commit *commit; + git_tree *tree; + + ret = find_commit(repo, branch, &commit); + if (ret) + return ret; + if (git_commit_tree(&tree, commit)) + return report_error("Could not look up tree of commit in branch '%s'", branch); + ret = load_dives_from_tree(repo, tree); + if (!ret) + set_git_id(git_commit_id(commit)); + git_object_free((git_object *)tree); + return ret; +} + +const char *get_sha(git_repository *repo, const char *branch) +{ + static char git_id_buffer[GIT_OID_HEXSZ+1]; + git_commit *commit; + if (find_commit(repo, branch, &commit)) + return NULL; + git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), (const git_oid *)commit); + return git_id_buffer; +} + +/* + * Like git_save_dives(), this silently returns a negative + * value if it's not a git repository at all (so that you + * can try to load it some other way. + * + * If it is a git repository, we return zero for success, + * or report an error and return 1 if the load failed. + */ +int git_load_dives(struct git_repository *repo, const char *branch) +{ + int ret; + + if (repo == dummy_git_repository) + return report_error("Unable to open git repository at '%s'", branch); + ret = do_git_load(repo, branch); + git_repository_free(repo); + free((void *)branch); + finish_active_dive(); + finish_active_trip(); + return ret; +} diff --git a/core/macos.c b/core/macos.c new file mode 100644 index 000000000..500412cd8 --- /dev/null +++ b/core/macos.c @@ -0,0 +1,218 @@ +/* macos.c */ +/* implements Mac OS X specific functions */ +#include <stdlib.h> +#include <dirent.h> +#include <fnmatch.h> +#include "dive.h" +#include "display.h" +#include <CoreFoundation/CoreFoundation.h> +#if !defined(__IPHONE_5_0) +#include <CoreServices/CoreServices.h> +#endif +#include <mach-o/dyld.h> +#include <sys/syslimits.h> +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> + +void subsurface_user_info(struct user_info *info) +{ + (void) info; + /* Nothing, let's use libgit2-20 on MacOS */ +} + +/* macos defines CFSTR to create a CFString object from a constant, + * but no similar macros if a C string variable is supposed to be + * the argument. We add this here (hardcoding the default allocator + * and MacRoman encoding */ +#define CFSTR_VAR(_var) CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, \ + (_var), kCFStringEncodingMacRoman, \ + kCFAllocatorNull) + +#define SUBSURFACE_PREFERENCES CFSTR("org.hohndel.subsurface") +#define ICON_NAME "Subsurface.icns" +#define UI_FONT "Arial 12" + +const char mac_system_divelist_default_font[] = "Arial"; +const char *system_divelist_default_font = mac_system_divelist_default_font; +double system_divelist_default_font_size = -1.0; + +void subsurface_OS_pref_setup(void) +{ + // nothing +} + +bool subsurface_ignore_font(const char *font) +{ + (void) font; + // there are no old default fonts to ignore + return false; +} + +static const char *system_default_path_append(const char *append) +{ + const char *home = getenv("HOME"); + const char *path = "/Library/Application Support/Subsurface"; + + int len = strlen(home) + strlen(path) + 1; + if (append) + len += strlen(append) + 1; + + char *buffer = (char *)malloc(len); + memset(buffer, 0, len); + strcat(buffer, home); + strcat(buffer, path); + if (append) { + strcat(buffer, "/"); + strcat(buffer, append); + } + + return buffer; +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static char *filename = NULL; + if (!filename) { + const char *user = getenv("LOGNAME"); + if (same_string(user, "")) + user = "username"; + filename = calloc(strlen(user) + 5, 1); + strcat(filename, user); + strcat(filename, ".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1, entries = 0; + DIR *dp = NULL; + struct dirent *ep = NULL; + size_t i; + if (dc_type != DC_TYPE_UEMIS) { + const char *dirname = "/dev"; + const char *patterns[] = { + "tty.*", + "usbserial", + NULL + }; + + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + for (i = 0; patterns[i] != NULL; ++i) { + if (fnmatch(patterns[i], ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= (int)sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + break; + } + } + } + closedir(dp); + } + if (dc_type != DC_TYPE_SERIAL) { + const char *dirname = "/Volumes"; + int num_uemis = 0; + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + if (fnmatch("UEMISSDA", ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= (int)sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + num_uemis++; + break; + } + } + closedir(dp); + if (num_uemis == 1 && entries == 1) /* if we find exactly one entry and that's a Uemis, select it */ + index = 0; + } + return index; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + (void) dedicated; + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} + +bool subsurface_user_is_root() +{ + return (geteuid() == 0); +} diff --git a/core/membuffer.c b/core/membuffer.c new file mode 100644 index 000000000..053edb8f0 --- /dev/null +++ b/core/membuffer.c @@ -0,0 +1,288 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + +#include "dive.h" +#include "membuffer.h" + +char *detach_buffer(struct membuffer *b) +{ + char *result = b->buffer; + b->buffer = NULL; + b->len = 0; + b->alloc = 0; + return result; +} + +void free_buffer(struct membuffer *b) +{ + free(detach_buffer(b)); +} + +void flush_buffer(struct membuffer *b, FILE *f) +{ + if (b->len) { + fwrite(b->buffer, 1, b->len, f); + free_buffer(b); + } +} + +void strip_mb(struct membuffer *b) +{ + while (b->len && isspace(b->buffer[b->len - 1])) + b->len--; +} + +/* + * Running out of memory isn't really an issue these days. + * So rather than do insane error handling and making the + * interface very complex, we'll just die. It won't happen + * unless you're running on a potato. + */ +static void oom(void) +{ + fprintf(stderr, "Out of memory\n"); + exit(1); +} + +static void make_room(struct membuffer *b, unsigned int size) +{ + unsigned int needed = b->len + size; + if (needed > b->alloc) { + char *n; + /* round it up to not reallocate all the time.. */ + needed = needed * 9 / 8 + 1024; + n = realloc(b->buffer, needed); + if (!n) + oom(); + b->buffer = n; + b->alloc = needed; + } +} + +const char *mb_cstring(struct membuffer *b) +{ + make_room(b, 1); + b->buffer[b->len] = 0; + return b->buffer; +} + +void put_bytes(struct membuffer *b, const char *str, int len) +{ + make_room(b, len); + memcpy(b->buffer + b->len, str, len); + b->len += len; +} + +void put_string(struct membuffer *b, const char *str) +{ + put_bytes(b, str, strlen(str)); +} + +void put_vformat(struct membuffer *b, const char *fmt, va_list args) +{ + int room = 128; + + for (;;) { + int len; + va_list copy; + char *target; + + make_room(b, room); + room = b->alloc - b->len; + target = b->buffer + b->len; + + va_copy(copy, args); + len = vsnprintf(target, room, fmt, copy); + va_end(copy); + + if (len < room) { + b->len += len; + return; + } + + room = len + 1; + } +} + +/* Silly helper using membuffer */ +char *vformat_string(const char *fmt, va_list args) +{ + struct membuffer mb = { 0 }; + put_vformat(&mb, fmt, args); + mb_cstring(&mb); + return detach_buffer(&mb); +} + +char *format_string(const char *fmt, ...) +{ + va_list args; + char *result; + + va_start(args, fmt); + result = vformat_string(fmt, args); + va_end(args); + return result; +} + +void put_format(struct membuffer *b, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + put_vformat(b, fmt, args); + va_end(args); +} + +void put_milli(struct membuffer *b, const char *pre, int value, const char *post) +{ + int i; + char buf[4]; + const char *sign = ""; + unsigned v; + + v = value; + if (value < 0) { + sign = "-"; + v = -value; + } + for (i = 2; i >= 0; i--) { + buf[i] = (v % 10) + '0'; + v /= 10; + } + buf[3] = 0; + if (buf[2] == '0') { + buf[2] = 0; + if (buf[1] == '0') + buf[1] = 0; + } + + put_format(b, "%s%s%u.%s%s", pre, sign, v, buf, post); +} + +void put_temperature(struct membuffer *b, temperature_t temp, const char *pre, const char *post) +{ + if (temp.mkelvin) + put_milli(b, pre, temp.mkelvin - ZERO_C_IN_MKELVIN, post); +} + +void put_depth(struct membuffer *b, depth_t depth, const char *pre, const char *post) +{ + if (depth.mm) + put_milli(b, pre, depth.mm, post); +} + +void put_duration(struct membuffer *b, duration_t duration, const char *pre, const char *post) +{ + if (duration.seconds) + put_format(b, "%s%u:%02u%s", pre, FRACTION(duration.seconds, 60), post); +} + +void put_pressure(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) +{ + if (pressure.mbar) + put_milli(b, pre, pressure.mbar, post); +} + +void put_salinity(struct membuffer *b, int salinity, const char *pre, const char *post) +{ + if (salinity) + put_format(b, "%s%d%s", pre, salinity / 10, post); +} + +void put_degrees(struct membuffer *b, degrees_t value, const char *pre, const char *post) +{ + int udeg = value.udeg; + const char *sign = ""; + + if (udeg < 0) { + udeg = -udeg; + sign = "-"; + } + put_format(b, "%s%s%u.%06u%s", pre, sign, FRACTION(udeg, 1000000), post); +} + +void put_quoted(struct membuffer *b, const char *text, int is_attribute, int is_html) +{ + const char *p = text; + + for (;;) { + const char *escape; + + switch (*p++) { + default: + continue; + case 0: + escape = NULL; + break; + case 1 ... 8: + case 11: + case 12: + case 14 ... 31: + escape = "?"; + break; + case '<': + escape = "<"; + break; + case '>': + escape = ">"; + break; + case '&': + escape = "&"; + break; + case '\'': + if (!is_attribute) + continue; + escape = "'"; + break; + case '\"': + if (!is_attribute) + continue; + escape = """; + break; + case '\n': + if (!is_html) + continue; + else + escape = "<br>"; + } + put_bytes(b, text, (p - text - 1)); + if (!escape) + break; + put_string(b, escape); + text = p; + } +} + +char *add_to_string_va(const char *old, const char *fmt, va_list args) +{ + char *res; + struct membuffer o = { 0 }, n = { 0 }; + put_vformat(&n, fmt, args); + put_format(&o, "%s\n%s", old ?: "", mb_cstring(&n)); + res = strdup(mb_cstring(&o)); + free_buffer(&o); + free_buffer(&n); + free((void *)old); + return res; +} + +/* this is a convenience function that cleverly adds text to a string, using our membuffer + * infrastructure. + * WARNING - this will free(old), the intended pattern is + * string = add_to_string(string, fmt, ...) + */ +char *add_to_string(const char *old, const char *fmt, ...) +{ + char *res; + va_list args; + + va_start(args, fmt); + res = add_to_string_va(old, fmt, args); + va_end(args); + return res; +} diff --git a/core/membuffer.h b/core/membuffer.h new file mode 100644 index 000000000..434b34c71 --- /dev/null +++ b/core/membuffer.h @@ -0,0 +1,74 @@ +#ifndef MEMBUFFER_H +#define MEMBUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <ctype.h> + +struct membuffer { + unsigned int len, alloc; + char *buffer; +}; + +#ifdef __GNUC__ +#define __printf(x, y) __attribute__((__format__(__printf__, x, y))) +#else +#define __printf(x, y) +#endif + +extern char *detach_buffer(struct membuffer *b); +extern void free_buffer(struct membuffer *); +extern void flush_buffer(struct membuffer *, FILE *); +extern void put_bytes(struct membuffer *, const char *, int); +extern void put_string(struct membuffer *, const char *); +extern void put_quoted(struct membuffer *, const char *, int, int); +extern void strip_mb(struct membuffer *); +extern const char *mb_cstring(struct membuffer *); +extern __printf(2, 0) void put_vformat(struct membuffer *, const char *, va_list); +extern __printf(2, 3) void put_format(struct membuffer *, const char *fmt, ...); +extern __printf(2, 0) char *add_to_string_va(const char *old, const char *fmt, va_list args); +extern __printf(2, 3) char *add_to_string(const char *old, const char *fmt, ...); + +/* Helpers that use membuffers internally */ +extern __printf(1, 0) char *vformat_string(const char *, va_list); +extern __printf(1, 2) char *format_string(const char *, ...); + + +/* Output one of our "milli" values with type and pre/post data */ +extern void put_milli(struct membuffer *, const char *, int, const char *); + +/* + * Helper functions for showing particular types. If the type + * is empty, nothing is done, and the function returns false. + * Otherwise, it returns true. + * + * The two "const char *" at the end are pre/post data. + * + * The reason for the pre/post data is so that you can easily + * prepend and append a string without having to test whether the + * type is empty. So + * + * put_temperature(b, temp, "Temp=", " C\n"); + * + * writes nothing to the buffer if there is no temperature data, + * but otherwise would a string that looks something like + * + * "Temp=28.1 C\n" + * + * to the memory buffer (typically the post/pre will be some XML + * pattern and unit string or whatever). + */ +extern void put_temperature(struct membuffer *, temperature_t, const char *, const char *); +extern void put_depth(struct membuffer *, depth_t, const char *, const char *); +extern void put_duration(struct membuffer *, duration_t, const char *, const char *); +extern void put_pressure(struct membuffer *, pressure_t, const char *, const char *); +extern void put_salinity(struct membuffer *, int, const char *, const char *); +extern void put_degrees(struct membuffer *b, degrees_t value, const char *, const char *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/metrics.cpp b/core/metrics.cpp new file mode 100644 index 000000000..3c66528b8 --- /dev/null +++ b/core/metrics.cpp @@ -0,0 +1,65 @@ +/* + * metrics.cpp + * + * methods to find/compute essential UI metrics + * (font properties, icon sizes, etc) + * + */ + +#include "metrics.h" + +static IconMetrics dfltIconMetrics; + +IconMetrics::IconMetrics() : + sz_small(-1), + sz_med(-1), + sz_big(-1), + sz_pic(-1), + spacing(-1), + dpr(1.0) +{ +} + +QFont defaultModelFont() +{ + QFont font; +// font.setPointSizeF(font.pointSizeF() * 0.8); + return font; +} + +QFontMetrics defaultModelFontMetrics() +{ + return QFontMetrics(defaultModelFont()); +} + +// return the default icon size, computed as the multiple of 16 closest to +// the given height +static int defaultIconSize(int height) +{ + int ret = (height + 8)/16; + ret *= 16; + if (ret < 16) + ret = 16; + return ret; +} + +const IconMetrics & defaultIconMetrics() +{ + if (dfltIconMetrics.sz_small == -1) { + int small = defaultIconSize(defaultModelFontMetrics().height()); + dfltIconMetrics.sz_small = small; + dfltIconMetrics.sz_med = small + small/2; + dfltIconMetrics.sz_big = 2*small; + + dfltIconMetrics.sz_pic = 8*small; + + dfltIconMetrics.spacing = small/8; + } + + return dfltIconMetrics; +} + +void updateDevicePixelRatio(double dpr) +{ + dfltIconMetrics.dpr = dpr; +} diff --git a/core/metrics.h b/core/metrics.h new file mode 100644 index 000000000..ca281b3b1 --- /dev/null +++ b/core/metrics.h @@ -0,0 +1,36 @@ +/* + * metrics.h + * + * header file for common function to find/compute essential UI metrics + * (font properties, icon sizes, etc) + * + */ +#ifndef METRICS_H +#define METRICS_H + +#include <QFont> +#include <QFontMetrics> +#include <QSize> + +QFont defaultModelFont(); +QFontMetrics defaultModelFontMetrics(); + +// Collection of icon/picture sizes and other metrics, resolution independent +struct IconMetrics { + // icon sizes + int sz_small; // ex 16px + int sz_med; // ex 24px + int sz_big; // ex 32px + // picture size + int sz_pic; // ex 128px + // icon spacing + int spacing; // ex 2px + // devicePixelRatio + double dpr; // 1.0 for traditional screens, HiDPI screens up to 3.0 + IconMetrics(); +}; + +const IconMetrics & defaultIconMetrics(); +void updateDevicePixelRatio(double dpr); + +#endif // METRICS_H diff --git a/core/ostctools.c b/core/ostctools.c new file mode 100644 index 000000000..9be591b0e --- /dev/null +++ b/core/ostctools.c @@ -0,0 +1,193 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include "dive.h" +#include "gettext.h" +#include "divelist.h" +#include "libdivecomputer.h" + +/* + * Returns a dc_descriptor_t structure based on dc model's number and family. + */ + +static dc_descriptor_t *ostc_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) { + fprintf(stderr, "Error creating 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 with known dc data and a descriptor. + */ +static int ostc_prepare_data(int data_model, dc_family_t dc_fam, device_data_t *dev_data) +{ + dc_descriptor_t *data_descriptor; + + dev_data->device = NULL; + dev_data->context = NULL; + + data_descriptor = ostc_get_data_descriptor(data_model, dc_fam); + if (data_descriptor) { + dev_data->descriptor = data_descriptor; + dev_data->vendor = copy_string(data_descriptor->vendor); + dev_data->model = copy_string(data_descriptor->product); + } else { + return 0; + } + return 1; +} + +/* + * OSTCTools stores the raw dive data in heavily padded files, one dive + * each file. So it's not necessary to iterate once and again on a parsing + * function. Actually there's only one kind of archive for every DC model. + */ +void ostctools_import(const char *file, struct dive_table *divetable) +{ + FILE *archive; + device_data_t *devdata = calloc(1, sizeof(device_data_t)); + dc_family_t dc_fam; + unsigned char *buffer = calloc(65536, 1), *uc_tmp; + char *tmp; + struct dive *ostcdive = alloc_dive(); + dc_status_t rc = 0; + int model, ret, i = 0; + unsigned int serial; + struct extra_data *ptr; + + // Open the archive + if ((archive = subsurface_fopen(file, "rb")) == NULL) { + report_error(translate("gettextFromC", "Failed to read '%s'"), file); + free(ostcdive); + goto out; + } + + // Read dive number from the log + uc_tmp = calloc(2, 1); + fseek(archive, 258, 0); + fread(uc_tmp, 1, 2, archive); + ostcdive->number = uc_tmp[0] + (uc_tmp[1] << 8); + free(uc_tmp); + + // Read device's serial number + uc_tmp = calloc(2, 1); + fseek(archive, 265, 0); + fread(uc_tmp, 1, 2, archive); + serial = uc_tmp[0] + (uc_tmp[1] << 8); + free(uc_tmp); + + // Read dive's raw data, header + profile + fseek(archive, 456, 0); + while (!feof(archive)) { + fread(buffer + i, 1, 1, archive); + if (buffer[i] == 0xFD && buffer[i - 1] == 0xFD) + break; + i++; + } + + // Try to determine the dc family based on the header type + if (buffer[2] == 0x20 || buffer[2] == 0x21) { + dc_fam = DC_FAMILY_HW_OSTC; + } else { + switch (buffer[8]) { + case 0x22: + dc_fam = DC_FAMILY_HW_FROG; + break; + case 0x23: + dc_fam = DC_FAMILY_HW_OSTC3; + break; + default: + report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); + free(ostcdive); + fclose(archive); + goto out; + } + } + + // Try to determine the model based on serial number + switch (dc_fam) { + case DC_FAMILY_HW_OSTC: + if (serial > 7000) + model = 3; //2C + else if (serial > 2048) + model = 2; //2N + else if (serial > 300) + model = 1; //MK2 + else + model = 0; //OSTC + break; + case DC_FAMILY_HW_FROG: + model = 0; + break; + default: + if (serial > 10000) + model = 0x12; //Sport + else + model = 0x0A; //OSTC3 + } + + // Prepare data to pass to libdivecomputer. + ret = ostc_prepare_data(model, dc_fam, devdata); + if (ret == 0) { + report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); + free(ostcdive); + fclose(archive); + goto out; + } + tmp = calloc(strlen(devdata->vendor) + strlen(devdata->model) + 28, 1); + sprintf(tmp, "%s %s (Imported from OSTCTools)", devdata->vendor, devdata->model); + ostcdive->dc.model = copy_string(tmp); + free(tmp); + + // Parse the dive data + rc = libdc_buffer_parser(ostcdive, devdata, buffer, i + 1); + if (rc != DC_STATUS_SUCCESS) + report_error(translate("gettextFromC", "Error - %s - parsing dive %d"), errmsg(rc), ostcdive->number); + + // Serial number is not part of the header nor the profile, so libdc won't + // catch it. If Serial is part of the extra_data, and set to zero, remove + // it from the list and add again. + tmp = calloc(12, 1); + sprintf(tmp, "%d", serial); + ostcdive->dc.serial = copy_string(tmp); + free(tmp); + + if (ostcdive->dc.extra_data) { + ptr = ostcdive->dc.extra_data; + while (strcmp(ptr->key, "Serial")) + ptr = ptr->next; + if (!strcmp(ptr->value, "0")) { + add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); + *ptr = *(ptr)->next; + } + } else { + add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); + } + record_dive_to_table(ostcdive, divetable); + mark_divelist_changed(true); + sort_table(divetable); + fclose(archive); +out: + free(devdata); + free(buffer); +} diff --git a/core/parse-xml.c b/core/parse-xml.c new file mode 100644 index 000000000..e8782251e --- /dev/null +++ b/core/parse-xml.c @@ -0,0 +1,3751 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <assert.h> +#define __USE_XOPEN +#include <time.h> +#include <libxml/parser.h> +#include <libxml/parserInternals.h> +#include <libxml/tree.h> +#include <libxslt/transform.h> +#include <libdivecomputer/parser.h> + +#include "gettext.h" + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" + +int verbose, quit, force_root; +int metric = 1; +int last_xml_version = -1; +int diveid = -1; + +static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params); + +/* the dive table holds the overall dive list; target table points at + * the table we are currently filling */ +struct dive_table dive_table; +struct dive_table *target_table = NULL; + +/* Trim a character string by removing leading and trailing white space characters. + * Parameter: a pointer to a null-terminated character string (buffer); + * Return value: length of the trimmed string, excluding the terminal 0x0 byte + * The original pointer (buffer) remains valid after this function has been called + * and points to the trimmed string */ +int trimspace(char *buffer) { + int i, size, start, end; + size = strlen(buffer); + for(start = 0; isspace(buffer[start]); start++) + if (start >= size) return 0; // Find 1st character following leading whitespace + for(end = size - 1; isspace(buffer[end]); end--) // Find last character before trailing whitespace + if (end <= 0) return 0; + for(i = start; i <= end; i++) // Move the nonspace characters to the start of the string + buffer[i-start] = buffer[i]; + size = end - start + 1; + buffer[size] = 0x0; // then terminate the string + return size; // return string length +} + +/* + * Clear a dive_table + */ +void clear_table(struct dive_table *table) +{ + for (int i = 0; i < table->nr; i++) + free(table->dives[i]); + table->nr = 0; +} + +/* + * Add a dive into the dive_table array + */ +void record_dive_to_table(struct dive *dive, struct dive_table *table) +{ + assert(table != NULL); + struct dive **dives = grow_dive_table(table); + int nr = table->nr; + + dives[nr] = fixup_dive(dive); + table->nr = nr + 1; +} + +void record_dive(struct dive *dive) +{ + record_dive_to_table(dive, &dive_table); +} + +static void start_match(const char *type, const char *name, char *buffer) +{ + if (verbose > 2) + printf("Matching %s '%s' (%s)\n", + type, name, buffer); +} + +static void nonmatch(const char *type, const char *name, char *buffer) +{ + if (verbose > 1) + printf("Unable to match %s '%s' (%s)\n", + type, name, buffer); +} + +typedef void (*matchfn_t)(char *buffer, void *); + +static int match(const char *pattern, int plen, + const char *name, + matchfn_t fn, char *buf, void *data) +{ + switch (name[plen]) { + case '\0': + case '.': + break; + default: + return 0; + } + if (memcmp(pattern, name, plen)) + return 0; + fn(buf, data); + return 1; +} + + +struct units xml_parsing_units; +const struct units SI_units = SI_UNITS; +const struct units IMPERIAL_units = IMPERIAL_UNITS; + +/* + * Dive info as it is being built up.. + */ +#define MAX_EVENT_NAME 128 +static struct divecomputer *cur_dc; +static struct dive *cur_dive; +static struct dive_site *cur_dive_site; +degrees_t cur_latitude, cur_longitude; +static dive_trip_t *cur_trip = NULL; +static struct sample *cur_sample; +static struct picture *cur_picture; +static union { + struct event event; + char allocation[sizeof(struct event)+MAX_EVENT_NAME]; +} event_allocation = { .event.deleted = 1 }; +#define cur_event event_allocation.event +static struct { + struct { + const char *model; + uint32_t deviceid; + const char *nickname, *serial_nr, *firmware; + } dc; +} cur_settings; +static bool in_settings = false; +static bool in_userid = false; +static struct tm cur_tm; +static int cur_cylinder_index, cur_ws_index; +static int lastndl, laststoptime, laststopdepth, lastcns, lastpo2, lastindeco; +static int lastcylinderindex, lastsensor, next_o2_sensor; +static struct extra_data cur_extra_data; + +/* + * If we don't have an explicit dive computer, + * we use the implicit one that every dive has.. + */ +static struct divecomputer *get_dc(void) +{ + return cur_dc ?: &cur_dive->dc; +} + +static enum import_source { + UNKNOWN, + LIBDIVECOMPUTER, + DIVINGLOG, + UDDF, + SSRF_WS, +} import_source; + +static void divedate(const char *buffer, timestamp_t *when) +{ + int d, m, y; + int hh, mm, ss; + + hh = 0; + mm = 0; + ss = 0; + if (sscanf(buffer, "%d.%d.%d %d:%d:%d", &d, &m, &y, &hh, &mm, &ss) >= 3) { + /* This is ok, and we got at least the date */ + } else if (sscanf(buffer, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) >= 3) { + /* This is also ok */ + } else { + fprintf(stderr, "Unable to parse date '%s'\n", buffer); + return; + } + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hh; + cur_tm.tm_min = mm; + cur_tm.tm_sec = ss; + + *when = utc_mktime(&cur_tm); +} + +static void divetime(const char *buffer, timestamp_t *when) +{ + int h, m, s = 0; + + if (sscanf(buffer, "%d:%d:%d", &h, &m, &s) >= 2) { + cur_tm.tm_hour = h; + cur_tm.tm_min = m; + cur_tm.tm_sec = s; + *when = utc_mktime(&cur_tm); + } +} + +/* Libdivecomputer: "2011-03-20 10:22:38" */ +static void divedatetime(char *buffer, timestamp_t *when) +{ + int y, m, d; + int hr, min, sec; + + if (sscanf(buffer, "%d-%d-%d %d:%d:%d", + &y, &m, &d, &hr, &min, &sec) == 6) { + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hr; + cur_tm.tm_min = min; + cur_tm.tm_sec = sec; + *when = utc_mktime(&cur_tm); + } +} + +enum ParseState { + FINDSTART, + FINDEND +}; +static void divetags(char *buffer, struct tag_entry **tags) +{ + int i = 0, start = 0, end = 0; + enum ParseState state = FINDEND; + int len = buffer ? strlen(buffer) : 0; + + while (i < len) { + if (buffer[i] == ',') { + if (state == FINDSTART) { + /* Detect empty tags */ + } else if (state == FINDEND) { + /* Found end of tag */ + if (i > 0 && buffer[i - 1] != '\\') { + buffer[i] = '\0'; + state = FINDSTART; + taglist_add_tag(tags, buffer + start); + } else { + state = FINDSTART; + } + } + } else if (buffer[i] == ' ') { + /* Handled */ + } else { + /* Found start of tag */ + if (state == FINDSTART) { + state = FINDEND; + start = i; + } else if (state == FINDEND) { + end = i; + } + } + i++; + } + if (state == FINDEND) { + if (end < start) + end = len - 1; + if (len > 0) { + buffer[end + 1] = '\0'; + taglist_add_tag(tags, buffer + start); + } + } +} + +enum number_type { + NEITHER, + FLOAT +}; + +static enum number_type parse_float(const char *buffer, double *res, const char **endp) +{ + double val; + static bool first_time = true; + + errno = 0; + val = ascii_strtod(buffer, endp); + if (errno || *endp == buffer) + return NEITHER; + if (**endp == ',') { + if (IS_FP_SAME(val, rint(val))) { + /* we really want to send an error if this is a Subsurface native file + * as this is likely indication of a bug - but right now we don't have + * that information available */ + if (first_time) { + fprintf(stderr, "Floating point value with decimal comma (%s)?\n", buffer); + first_time = false; + } + /* Try again in permissive mode*/ + val = strtod_flags(buffer, endp, 0); + } + } + + *res = val; + return FLOAT; +} + +union int_or_float { + double fp; +}; + +static enum number_type integer_or_float(char *buffer, union int_or_float *res) +{ + const char *end; + return parse_float(buffer, &res->fp, &end); +} + +static void pressure(char *buffer, pressure_t *pressure) +{ + double mbar = 0.0; + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + /* Just ignore zero values */ + if (!val.fp) + break; + switch (xml_parsing_units.pressure) { + case PASCAL: + mbar = val.fp / 100; + break; + case BAR: + /* Assume mbar, but if it's really small, it's bar */ + mbar = val.fp; + if (fabs(mbar) < 5000) + mbar = mbar * 1000; + break; + case PSI: + mbar = psi_to_mbar(val.fp); + break; + } + if (fabs(mbar) > 5 && fabs(mbar) < 5000000) { + pressure->mbar = rint(mbar); + break; + } + /* fallthrough */ + default: + printf("Strange pressure reading %s\n", buffer); + } +} + +static void cylinder_use(char *buffer, enum cylinderuse *cyl_use) +{ + if (trimspace(buffer)) + *cyl_use = cylinderuse_from_text(buffer); +} + +static void salinity(char *buffer, int *salinity) +{ + union int_or_float val; + switch (integer_or_float(buffer, &val)) { + case FLOAT: + *salinity = rint(val.fp * 10.0); + break; + default: + printf("Strange salinity reading %s\n", buffer); + } +} + +static void depth(char *buffer, depth_t *depth) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.length) { + case METERS: + depth->mm = rint(val.fp * 1000); + break; + case FEET: + depth->mm = feet_to_mm(val.fp); + break; + } + break; + default: + printf("Strange depth reading %s\n", buffer); + } +} + +static void extra_data_start(void) +{ + memset(&cur_extra_data, 0, sizeof(struct extra_data)); +} + +static void extra_data_end(void) +{ + // don't save partial structures - we must have both key and value + if (cur_extra_data.key && cur_extra_data.value) + add_extra_data(cur_dc, cur_extra_data.key, cur_extra_data.value); +} + +static void weight(char *buffer, weight_t *weight) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.weight) { + case KG: + weight->grams = rint(val.fp * 1000); + break; + case LBS: + weight->grams = lbs_to_grams(val.fp); + break; + } + break; + default: + printf("Strange weight reading %s\n", buffer); + } +} + +static void temperature(char *buffer, temperature_t *temperature) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.temperature) { + case KELVIN: + temperature->mkelvin = val.fp * 1000; + break; + case CELSIUS: + temperature->mkelvin = C_to_mkelvin(val.fp); + break; + case FAHRENHEIT: + temperature->mkelvin = F_to_mkelvin(val.fp); + break; + } + break; + default: + printf("Strange temperature reading %s\n", buffer); + } + /* temperatures outside -40C .. +70C should be ignored */ + if (temperature->mkelvin < ZERO_C_IN_MKELVIN - 40000 || + temperature->mkelvin > ZERO_C_IN_MKELVIN + 70000) + temperature->mkelvin = 0; +} + +static void sampletime(char *buffer, duration_t *time) +{ + int i; + int min, sec; + + i = sscanf(buffer, "%d:%d", &min, &sec); + switch (i) { + case 1: + sec = min; + min = 0; + /* fallthrough */ + case 2: + time->seconds = sec + min * 60; + break; + default: + printf("Strange sample time reading %s\n", buffer); + } +} + +static void offsettime(char *buffer, offset_t *time) +{ + duration_t uoffset; + int sign = 1; + if (*buffer == '-') { + sign = -1; + buffer++; + } + /* yes, this could indeed fail if we have an offset > 34yrs + * - too bad */ + sampletime(buffer, &uoffset); + time->seconds = sign * uoffset.seconds; +} + +static void duration(char *buffer, duration_t *time) +{ + /* DivingLog 5.08 (and maybe other versions) appear to sometimes + * store the dive time as 44.00 instead of 44:00; + * This attempts to parse this in a fairly robust way */ + if (!strchr(buffer, ':') && strchr(buffer, '.')) { + char *mybuffer = strdup(buffer); + char *dot = strchr(mybuffer, '.'); + *dot = ':'; + sampletime(mybuffer, time); + free(mybuffer); + } else { + sampletime(buffer, time); + } +} + +static void percent(char *buffer, fraction_t *fraction) +{ + double val; + const char *end; + + switch (parse_float(buffer, &val, &end)) { + case FLOAT: + /* Turn fractions into percent unless explicit.. */ + if (val <= 1.0) { + while (isspace(*end)) + end++; + if (*end != '%') + val *= 100; + } + + /* Then turn percent into our integer permille format */ + if (val >= 0 && val <= 100.0) { + fraction->permille = rint(val * 10); + break; + } + default: + printf(translate("gettextFromC", "Strange percentage reading %s\n"), buffer); + break; + } +} + +static void gasmix(char *buffer, fraction_t *fraction) +{ + /* libdivecomputer does negative percentages. */ + if (*buffer == '-') + return; + if (cur_cylinder_index < MAX_CYLINDERS) + percent(buffer, fraction); +} + +static void gasmix_nitrogen(char *buffer, struct gasmix *gasmix) +{ + (void) buffer; + (void) gasmix; + /* Ignore n2 percentages. There's no value in them. */ +} + +static void cylindersize(char *buffer, volume_t *volume) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + volume->mliter = rint(val.fp * 1000); + break; + + default: + printf("Strange volume reading %s\n", buffer); + break; + } +} + +static void utf8_string(char *buffer, void *_res) +{ + char **res = _res; + int size; + size = trimspace(buffer); + if(size) + *res = strdup(buffer); +} + +static void event_name(char *buffer, char *name) +{ + int size = trimspace(buffer); + if (size >= MAX_EVENT_NAME) + size = MAX_EVENT_NAME-1; + memcpy(name, buffer, size); + name[size] = 0; +} + +// We don't use gauge as a mode, and pscr doesn't exist as a libdc divemode +const char *libdc_divemode_text[] = { "oc", "cc", "pscr", "freedive", "gauge"}; + +/* Extract the dive computer type from the xml text buffer */ +static void get_dc_type(char *buffer, enum dive_comp_type *dct) +{ + if (trimspace(buffer)) { + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { + if (strcmp(buffer, divemode_text[i]) == 0) + *dct = i; + else if (strcmp(buffer, libdc_divemode_text[i]) == 0) + *dct = i; + } + } +} + +#define MATCH(pattern, fn, dest) ({ \ + /* Silly type compatibility test */ \ + if (0) (fn)("test", dest); \ + match(pattern, strlen(pattern), name, (matchfn_t) (fn), buf, dest); }) + +static void get_index(char *buffer, int *i) +{ + *i = atoi(buffer); +} + +static void get_uint8(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); +} + +static void get_bearing(char *buffer, bearing_t *bearing) +{ + bearing->degrees = atoi(buffer); +} + +static void get_rating(char *buffer, int *i) +{ + int j = atoi(buffer); + if (j >= 0 && j <= 5) { + *i = j; + } +} + +static void double_to_o2pressure(char *buffer, o2pressure_t *i) +{ + i->mbar = rint(ascii_strtod(buffer, NULL) * 1000.0); +} + +static void hex_value(char *buffer, uint32_t *i) +{ + *i = strtoul(buffer, NULL, 16); +} + +static void get_tripflag(char *buffer, tripflag_t *tf) +{ + *tf = strcmp(buffer, "NOTRIP") ? TF_NONE : NO_TRIP; +} + +/* + * Divinglog is crazy. The temperatures are in celsius. EXCEPT + * for the sample temperatures, that are in Fahrenheit. + * WTF? + * + * Oh, and I think Diving Log *internally* probably kept them + * in celsius, because I'm seeing entries like + * + * <Temp>32.0</Temp> + * + * in there. Which is freezing, aka 0 degC. I bet the "0" is + * what Diving Log uses for "no temperature". + * + * So throw away crap like that. + * + * It gets worse. Sometimes the sample temperatures are in + * Celsius, which apparently happens if you are in a SI + * locale. So we now do: + * + * - temperatures < 32.0 == Celsius + * - temperature == 32.0 -> garbage, it's a missing temperature (zero converted from C to F) + * - temperatures > 32.0 == Fahrenheit + */ +static void fahrenheit(char *buffer, temperature_t *temperature) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + if (IS_FP_SAME(val.fp, 32.0)) + break; + if (val.fp < 32.0) + temperature->mkelvin = C_to_mkelvin(val.fp); + else + temperature->mkelvin = F_to_mkelvin(val.fp); + break; + default: + fprintf(stderr, "Crazy Diving Log temperature reading %s\n", buffer); + } +} + +/* + * Did I mention how bat-shit crazy divinglog is? The sample + * pressures are in PSI. But the tank working pressure is in + * bar. WTF^2? + * + * Crazy stuff like this is why subsurface has everything in + * these inconvenient typed structures, and you have to say + * "pressure->mbar" to get the actual value. Exactly so that + * you can never have unit confusion. + * + * It gets worse: sometimes apparently the pressures are in + * bar, sometimes in psi. Dirk suspects that this may be a + * DivingLog Uemis importer bug, and that they are always + * supposed to be in bar, but that the importer got the + * sample importing wrong. + * + * Sadly, there's no way to really tell. So I think we just + * have to have some arbitrary cut-off point where we assume + * that smaller values mean bar.. Not good. + */ +static void psi_or_bar(char *buffer, pressure_t *pressure) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + if (val.fp > 400) + pressure->mbar = psi_to_mbar(val.fp); + else + pressure->mbar = rint(val.fp * 1000); + break; + default: + fprintf(stderr, "Crazy Diving Log PSI reading %s\n", buffer); + } +} + +static int divinglog_fill_sample(struct sample *sample, const char *name, char *buf) +{ + return MATCH("time.p", sampletime, &sample->time) || + MATCH("depth.p", depth, &sample->depth) || + MATCH("temp.p", fahrenheit, &sample->temperature) || + MATCH("press1.p", psi_or_bar, &sample->cylinderpressure) || + 0; +} + +static void uddf_gasswitch(char *buffer, struct sample *sample) +{ + int idx = atoi(buffer); + int seconds = sample->time.seconds; + struct dive *dive = cur_dive; + struct divecomputer *dc = get_dc(); + + add_gas_switch_event(dive, dc, seconds, idx); +} + +static int uddf_fill_sample(struct sample *sample, const char *name, char *buf) +{ + return MATCH("divetime", sampletime, &sample->time) || + MATCH("depth", depth, &sample->depth) || + MATCH("temperature", temperature, &sample->temperature) || + MATCH("tankpressure", pressure, &sample->cylinderpressure) || + MATCH("ref.switchmix", uddf_gasswitch, sample) || + 0; +} + +static void eventtime(char *buffer, duration_t *duration) +{ + sampletime(buffer, duration); + if (cur_sample) + duration->seconds += cur_sample->time.seconds; +} + +static void try_to_match_autogroup(const char *name, char *buf) +{ + int autogroupvalue; + + start_match("autogroup", name, buf); + if (MATCH("state.autogroup", get_index, &autogroupvalue)) { + set_autogroup(autogroupvalue); + return; + } + nonmatch("autogroup", name, buf); +} + +void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int seconds, int idx) +{ + /* sanity check so we don't crash */ + if (idx < 0 || idx >= MAX_CYLINDERS) + return; + /* The gas switch event format is insane for historical reasons */ + struct gasmix *mix = &dive->cylinder[idx].gasmix; + int o2 = get_o2(mix); + int he = get_he(mix); + struct event *ev; + int value; + + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + value = o2 + (he << 16); + + ev = add_event(dc, seconds, he ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE, 0, value, "gaschange"); + if (ev) { + ev->gas.index = idx; + ev->gas.mix = *mix; + } +} + +static void get_cylinderindex(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); + if (lastcylinderindex != *i) { + add_gas_switch_event(cur_dive, get_dc(), cur_sample->time.seconds, *i); + lastcylinderindex = *i; + } +} + +static void get_sensor(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); + lastsensor = *i; +} + +static void parse_libdc_deco(char *buffer, struct sample *s) +{ + if (strcmp(buffer, "deco") == 0) { + s->in_deco = true; + } else if (strcmp(buffer, "ndl") == 0) { + s->in_deco = false; + // The time wasn't stoptime, it was ndl + s->ndl = s->stoptime; + s->stoptime.seconds = 0; + } +} + +static void try_to_fill_dc_settings(const char *name, char *buf) +{ + start_match("divecomputerid", name, buf); + if (MATCH("model.divecomputerid", utf8_string, &cur_settings.dc.model)) + return; + if (MATCH("deviceid.divecomputerid", hex_value, &cur_settings.dc.deviceid)) + return; + if (MATCH("nickname.divecomputerid", utf8_string, &cur_settings.dc.nickname)) + return; + if (MATCH("serial.divecomputerid", utf8_string, &cur_settings.dc.serial_nr)) + return; + if (MATCH("firmware.divecomputerid", utf8_string, &cur_settings.dc.firmware)) + return; + + nonmatch("divecomputerid", name, buf); +} + +static void try_to_fill_event(const char *name, char *buf) +{ + start_match("event", name, buf); + if (MATCH("event", event_name, cur_event.name)) + return; + if (MATCH("name", event_name, cur_event.name)) + return; + if (MATCH("time", eventtime, &cur_event.time)) + return; + if (MATCH("type", get_index, &cur_event.type)) + return; + if (MATCH("flags", get_index, &cur_event.flags)) + return; + if (MATCH("value", get_index, &cur_event.value)) + return; + if (MATCH("cylinder", get_index, &cur_event.gas.index)) { + /* We add one to indicate that we got an actual cylinder index value */ + cur_event.gas.index++; + return; + } + if (MATCH("o2", percent, &cur_event.gas.mix.o2)) + return; + if (MATCH("he", percent, &cur_event.gas.mix.he)) + return; + nonmatch("event", name, buf); +} + +static int match_dc_data_fields(struct divecomputer *dc, const char *name, char *buf) +{ + if (MATCH("maxdepth", depth, &dc->maxdepth)) + return 1; + if (MATCH("meandepth", depth, &dc->meandepth)) + return 1; + if (MATCH("max.depth", depth, &dc->maxdepth)) + return 1; + if (MATCH("mean.depth", depth, &dc->meandepth)) + return 1; + if (MATCH("duration", duration, &dc->duration)) + return 1; + if (MATCH("divetime", duration, &dc->duration)) + return 1; + if (MATCH("divetimesec", duration, &dc->duration)) + return 1; + if (MATCH("surfacetime", duration, &dc->surfacetime)) + return 1; + if (MATCH("airtemp", temperature, &dc->airtemp)) + return 1; + if (MATCH("watertemp", temperature, &dc->watertemp)) + return 1; + if (MATCH("air.temperature", temperature, &dc->airtemp)) + return 1; + if (MATCH("water.temperature", temperature, &dc->watertemp)) + return 1; + if (MATCH("pressure.surface", pressure, &dc->surface_pressure)) + return 1; + if (MATCH("salinity.water", salinity, &dc->salinity)) + return 1; + if (MATCH("key.extradata", utf8_string, &cur_extra_data.key)) + return 1; + if (MATCH("value.extradata", utf8_string, &cur_extra_data.value)) + return 1; + if (MATCH("divemode", get_dc_type, &dc->divemode)) + return 1; + if (MATCH("salinity", salinity, &dc->salinity)) + return 1; + if (MATCH("atmospheric", pressure, &dc->surface_pressure)) + return 1; + return 0; +} + +/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ +static void try_to_fill_dc(struct divecomputer *dc, const char *name, char *buf) +{ + start_match("divecomputer", name, buf); + + if (MATCH("date", divedate, &dc->when)) + return; + if (MATCH("time", divetime, &dc->when)) + return; + if (MATCH("model", utf8_string, &dc->model)) + return; + if (MATCH("deviceid", hex_value, &dc->deviceid)) + return; + if (MATCH("diveid", hex_value, &dc->diveid)) + return; + if (MATCH("dctype", get_dc_type, &dc->divemode)) + return; + if (MATCH("no_o2sensors", get_sensor, &dc->no_o2sensors)) + return; + if (match_dc_data_fields(dc, name, buf)) + return; + + nonmatch("divecomputer", name, buf); +} + +/* We're in samples - try to convert the random xml value to something useful */ +static void try_to_fill_sample(struct sample *sample, const char *name, char *buf) +{ + int in_deco; + + start_match("sample", name, buf); + if (MATCH("pressure.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("cylpress.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("pdiluent.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("o2pressure.sample", pressure, &sample->o2cylinderpressure)) + return; + if (MATCH("cylinderindex.sample", get_cylinderindex, &sample->sensor)) + return; + if (MATCH("sensor.sample", get_sensor, &sample->sensor)) + return; + if (MATCH("depth.sample", depth, &sample->depth)) + return; + if (MATCH("temp.sample", temperature, &sample->temperature)) + return; + if (MATCH("temperature.sample", temperature, &sample->temperature)) + return; + if (MATCH("sampletime.sample", sampletime, &sample->time)) + return; + if (MATCH("time.sample", sampletime, &sample->time)) + return; + if (MATCH("ndl.sample", sampletime, &sample->ndl)) + return; + if (MATCH("tts.sample", sampletime, &sample->tts)) + return; + if (MATCH("in_deco.sample", get_index, &in_deco)) { + sample->in_deco = (in_deco == 1); + return; + } + if (MATCH("stoptime.sample", sampletime, &sample->stoptime)) + return; + if (MATCH("stopdepth.sample", depth, &sample->stopdepth)) + return; + if (MATCH("cns.sample", get_uint8, &sample->cns)) + return; + if (MATCH("rbt.sample", sampletime, &sample->rbt)) + return; + if (MATCH("sensor1.sample", double_to_o2pressure, &sample->o2sensor[0])) // CCR O2 sensor data + return; + if (MATCH("sensor2.sample", double_to_o2pressure, &sample->o2sensor[1])) + return; + if (MATCH("sensor3.sample", double_to_o2pressure, &sample->o2sensor[2])) // up to 3 CCR sensors + return; + if (MATCH("po2.sample", double_to_o2pressure, &sample->setpoint)) + return; + if (MATCH("heartbeat", get_uint8, &sample->heartbeat)) + return; + if (MATCH("bearing", get_bearing, &sample->bearing)) + return; + if (MATCH("setpoint.sample", double_to_o2pressure, &sample->setpoint)) + return; + if (MATCH("ppo2.sample", double_to_o2pressure, &sample->o2sensor[next_o2_sensor])) { + next_o2_sensor++; + return; + } + if (MATCH("deco.sample", parse_libdc_deco, sample)) + return; + if (MATCH("time.deco", sampletime, &sample->stoptime)) + return; + if (MATCH("depth.deco", depth, &sample->stopdepth)) + return; + + switch (import_source) { + case DIVINGLOG: + if (divinglog_fill_sample(sample, name, buf)) + return; + break; + + case UDDF: + if (uddf_fill_sample(sample, name, buf)) + return; + break; + + default: + break; + } + + nonmatch("sample", name, buf); +} + +void try_to_fill_userid(const char *name, char *buf) +{ + (void) name; + if (prefs.save_userid_local) + set_userid(buf); +} + +static const char *country, *city; + +static void divinglog_place(char *place, uint32_t *uuid) +{ + char buffer[1024]; + + snprintf(buffer, sizeof(buffer), + "%s%s%s%s%s", + place, + city ? ", " : "", + city ? city : "", + country ? ", " : "", + country ? country : ""); + *uuid = get_dive_site_uuid_by_name(buffer, NULL); + if (*uuid == 0) + *uuid = create_dive_site(buffer, cur_dive->when); + + city = NULL; + country = NULL; +} + +static int divinglog_dive_match(struct dive *dive, const char *name, char *buf) +{ + return MATCH("divedate", divedate, &dive->when) || + MATCH("entrytime", divetime, &dive->when) || + MATCH("divetime", duration, &dive->dc.duration) || + MATCH("depth", depth, &dive->dc.maxdepth) || + MATCH("depthavg", depth, &dive->dc.meandepth) || + MATCH("tanktype", utf8_string, &dive->cylinder[0].type.description) || + MATCH("tanksize", cylindersize, &dive->cylinder[0].type.size) || + MATCH("presw", pressure, &dive->cylinder[0].type.workingpressure) || + MATCH("press", pressure, &dive->cylinder[0].start) || + MATCH("prese", pressure, &dive->cylinder[0].end) || + MATCH("comments", utf8_string, &dive->notes) || + MATCH("names.buddy", utf8_string, &dive->buddy) || + MATCH("name.country", utf8_string, &country) || + MATCH("name.city", utf8_string, &city) || + MATCH("name.place", divinglog_place, &dive->dive_site_uuid) || + 0; +} + +/* + * Uddf specifies ISO 8601 time format. + * + * There are many variations on that. This handles the useful cases. + */ +static void uddf_datetime(char *buffer, timestamp_t *when) +{ + char c; + int y, m, d, hh, mm, ss; + struct tm tm = { 0 }; + int i; + + i = sscanf(buffer, "%d-%d-%d%c%d:%d:%d", &y, &m, &d, &c, &hh, &mm, &ss); + if (i == 7) + goto success; + ss = 0; + if (i == 6) + goto success; + + i = sscanf(buffer, "%04d%02d%02d%c%02d%02d%02d", &y, &m, &d, &c, &hh, &mm, &ss); + if (i == 7) + goto success; + ss = 0; + if (i == 6) + goto success; +bad_date: + printf("Bad date time %s\n", buffer); + return; + +success: + if (c != 'T' && c != ' ') + goto bad_date; + tm.tm_year = y; + tm.tm_mon = m - 1; + tm.tm_mday = d; + tm.tm_hour = hh; + tm.tm_min = mm; + tm.tm_sec = ss; + *when = utc_mktime(&tm); +} + +#define uddf_datedata(name, offset) \ + static void uddf_##name(char *buffer, timestamp_t *when) \ + { \ + cur_tm.tm_##name = atoi(buffer) + offset; \ + *when = utc_mktime(&cur_tm); \ + } + +uddf_datedata(year, 0) +uddf_datedata(mon, -1) +uddf_datedata(mday, 0) +uddf_datedata(hour, 0) +uddf_datedata(min, 0) + +static int uddf_dive_match(struct dive *dive, const char *name, char *buf) +{ + return MATCH("datetime", uddf_datetime, &dive->when) || + MATCH("diveduration", duration, &dive->dc.duration) || + MATCH("greatestdepth", depth, &dive->dc.maxdepth) || + MATCH("year.date", uddf_year, &dive->when) || + MATCH("month.date", uddf_mon, &dive->when) || + MATCH("day.date", uddf_mday, &dive->when) || + MATCH("hour.time", uddf_hour, &dive->when) || + MATCH("minute.time", uddf_min, &dive->when) || + 0; +} + +/* + * This parses "floating point" into micro-degrees. + * We don't do exponentials etc, if somebody does + * GPS locations in that format, they are insane. + */ +degrees_t parse_degrees(char *buf, char **end) +{ + int sign = 1, decimals = 6, value = 0; + degrees_t ret; + + while (isspace(*buf)) + buf++; + switch (*buf) { + case '-': + sign = -1; + /* fallthrough */ + case '+': + buf++; + } + while (isdigit(*buf)) { + value = 10 * value + *buf - '0'; + buf++; + } + + /* Get the first six decimals if they exist */ + if (*buf == '.') + buf++; + do { + value *= 10; + if (isdigit(*buf)) { + value += *buf - '0'; + buf++; + } + } while (--decimals); + + /* Rounding */ + switch (*buf) { + case '5' ... '9': + value++; + } + while (isdigit(*buf)) + buf++; + + *end = buf; + ret.udeg = value * sign; + return ret; +} + +static void gps_lat(char *buffer, struct dive *dive) +{ + char *end; + degrees_t latitude = parse_degrees(buffer, &end); + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + dive->dive_site_uuid = create_dive_site_with_gps(NULL, latitude, (degrees_t){0}, dive->when); + } else { + if (ds->latitude.udeg && ds->latitude.udeg != latitude.udeg) + fprintf(stderr, "Oops, changing the latitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); + ds->latitude = latitude; + } +} + +static void gps_long(char *buffer, struct dive *dive) +{ + char *end; + degrees_t longitude = parse_degrees(buffer, &end); + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + dive->dive_site_uuid = create_dive_site_with_gps(NULL, (degrees_t){0}, longitude, dive->when); + } else { + if (ds->longitude.udeg && ds->longitude.udeg != longitude.udeg) + fprintf(stderr, "Oops, changing the longitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); + ds->longitude = longitude; + } + +} + +static void gps_location(char *buffer, struct dive_site *ds) +{ + char *end; + + ds->latitude = parse_degrees(buffer, &end); + ds->longitude = parse_degrees(end, &end); +} + +/* this is in qthelper.cpp, so including the .h file is a pain */ +extern const char *printGPSCoords(int lat, int lon); + +static void gps_in_dive(char *buffer, struct dive *dive) +{ + char *end; + struct dive_site *ds = NULL; + degrees_t latitude = parse_degrees(buffer, &end); + degrees_t longitude = parse_degrees(end, &end); + uint32_t uuid = dive->dive_site_uuid; + if (uuid == 0) { + // check if we have a dive site within 20 meters of that gps fix + uuid = get_dive_site_uuid_by_gps_proximity(latitude, longitude, 20, &ds); + + if (ds) { + // found a site nearby; in case it turns out this one had a different name let's + // remember the original coordinates so we can create the correct dive site later + cur_latitude = latitude; + cur_longitude = longitude; + dive->dive_site_uuid = uuid; + } else { + dive->dive_site_uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); + ds = get_dive_site_by_uuid(dive->dive_site_uuid); + } + } else { + ds = get_dive_site_by_uuid(uuid); + if (dive_site_has_gps_location(ds) && + (latitude.udeg != 0 || longitude.udeg != 0) && + (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { + // Houston, we have a problem + fprintf(stderr, "dive site uuid in dive, but gps location (%10.6f/%10.6f) different from dive location (%10.6f/%10.6f)\n", + ds->latitude.udeg / 1000000.0, ds->longitude.udeg / 1000000.0, + latitude.udeg / 1000000.0, longitude.udeg / 1000000.0); + const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); + free((void *)coords); + } else { + ds->latitude = latitude; + ds->longitude = longitude; + } + } +} + +static void add_dive_site(char *ds_name, struct dive *dive) +{ + static int suffix = 1; + char *buffer = ds_name; + char *to_free = NULL; + int size = trimspace(buffer); + if(size) { + uint32_t uuid = dive->dive_site_uuid; + struct dive_site *ds = get_dive_site_by_uuid(uuid); + if (uuid && !ds) { + // that's strange - we have a uuid but it doesn't exist - let's just ignore it + fprintf(stderr, "dive contains a non-existing dive site uuid %x\n", dive->dive_site_uuid); + uuid = 0; + } + if (!uuid) { + // if the dive doesn't have a uuid, check if there's already a dive site by this name + uuid = get_dive_site_uuid_by_name(buffer, &ds); + if (uuid && import_source == SSRF_WS) { + // when downloading GPS fixes from the Subsurface webservice we will often + // get a lot of dives with identical names (the autogenerated fixes). + // So in this case modify the name to make it unique + int name_size = strlen(buffer) + 10; // 8 digits - enough for 100 million sites + to_free = buffer = malloc(name_size); + do { + suffix++; + snprintf(buffer, name_size, "%s %8d", ds_name, suffix); + } while (get_dive_site_uuid_by_name(buffer, NULL) != 0); + ds = NULL; + } + } + if (ds) { + // we have a uuid, let's hope there isn't a different name + if (same_string(ds->name, "")) { + ds->name = copy_string(buffer); + } else if (!same_string(ds->name, buffer)) { + // if it's not the same name, it's not the same dive site + // but wait, we could have gotten this one based on GPS coords and could + // have had two different names for the same site... so let's search the other + // way around + uint32_t exact_match_uuid = get_dive_site_uuid_by_gps_and_name(buffer, ds->latitude, ds->longitude); + if (exact_match_uuid) { + dive->dive_site_uuid = exact_match_uuid; + } else { + dive->dive_site_uuid = create_dive_site(buffer, dive->when); + struct dive_site *newds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (cur_latitude.udeg || cur_longitude.udeg) { + // we started this uuid with GPS data, so lets use those + newds->latitude = cur_latitude; + newds->longitude = cur_longitude; + } else { + newds->latitude = ds->latitude; + newds->longitude = ds->longitude; + } + newds->notes = add_to_string(newds->notes, translate("gettextFromC", "additional name for site: %s\n"), ds->name); + } + } else { + // add the existing dive site to the current dive + dive->dive_site_uuid = uuid; + } + } else { + dive->dive_site_uuid = create_dive_site(buffer, dive->when); + } + } + free(to_free); +} + +static void gps_picture_location(char *buffer, struct picture *pic) +{ + char *end; + + pic->latitude = parse_degrees(buffer, &end); + pic->longitude = parse_degrees(end, &end); +} + +/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ +static void try_to_fill_dive(struct dive *dive, const char *name, char *buf) +{ + start_match("dive", name, buf); + + switch (import_source) { + case DIVINGLOG: + if (divinglog_dive_match(dive, name, buf)) + return; + break; + + case UDDF: + if (uddf_dive_match(dive, name, buf)) + return; + break; + + default: + break; + } + if (MATCH("divesiteid", hex_value, &dive->dive_site_uuid)) + return; + if (MATCH("number", get_index, &dive->number)) + return; + if (MATCH("tags", divetags, &dive->tag_list)) + return; + if (MATCH("tripflag", get_tripflag, &dive->tripflag)) + return; + if (MATCH("date", divedate, &dive->when)) + return; + if (MATCH("time", divetime, &dive->when)) + return; + if (MATCH("datetime", divedatetime, &dive->when)) + return; + /* + * Legacy format note: per-dive depths and duration get saved + * in the first dive computer entry + */ + if (match_dc_data_fields(&dive->dc, name, buf)) + return; + + if (MATCH("filename.picture", utf8_string, &cur_picture->filename)) + return; + if (MATCH("offset.picture", offsettime, &cur_picture->offset)) + return; + if (MATCH("gps.picture", gps_picture_location, cur_picture)) + return; + if (MATCH("hash.picture", utf8_string, &cur_picture->hash)) + return; + if (MATCH("cylinderstartpressure", pressure, &dive->cylinder[0].start)) + return; + if (MATCH("cylinderendpressure", pressure, &dive->cylinder[0].end)) + return; + if (MATCH("gps", gps_in_dive, dive)) + return; + if (MATCH("Place", gps_in_dive, dive)) + return; + if (MATCH("latitude", gps_lat, dive)) + return; + if (MATCH("sitelat", gps_lat, dive)) + return; + if (MATCH("lat", gps_lat, dive)) + return; + if (MATCH("longitude", gps_long, dive)) + return; + if (MATCH("sitelon", gps_long, dive)) + return; + if (MATCH("lon", gps_long, dive)) + return; + if (MATCH("location", add_dive_site, dive)) + return; + if (MATCH("name.dive", add_dive_site, dive)) + return; + if (MATCH("suit", utf8_string, &dive->suit)) + return; + if (MATCH("divesuit", utf8_string, &dive->suit)) + return; + if (MATCH("notes", utf8_string, &dive->notes)) + return; + if (MATCH("divemaster", utf8_string, &dive->divemaster)) + return; + if (MATCH("buddy", utf8_string, &dive->buddy)) + return; + if (MATCH("rating.dive", get_rating, &dive->rating)) + return; + if (MATCH("visibility.dive", get_rating, &dive->visibility)) + return; + if (cur_ws_index < MAX_WEIGHTSYSTEMS) { + if (MATCH("description.weightsystem", utf8_string, &dive->weightsystem[cur_ws_index].description)) + return; + if (MATCH("weight.weightsystem", weight, &dive->weightsystem[cur_ws_index].weight)) + return; + if (MATCH("weight", weight, &dive->weightsystem[cur_ws_index].weight)) + return; + } + if (cur_cylinder_index < MAX_CYLINDERS) { + if (MATCH("size.cylinder", cylindersize, &dive->cylinder[cur_cylinder_index].type.size)) + return; + if (MATCH("workpressure.cylinder", pressure, &dive->cylinder[cur_cylinder_index].type.workingpressure)) + return; + if (MATCH("description.cylinder", utf8_string, &dive->cylinder[cur_cylinder_index].type.description)) + return; + if (MATCH("start.cylinder", pressure, &dive->cylinder[cur_cylinder_index].start)) + return; + if (MATCH("end.cylinder", pressure, &dive->cylinder[cur_cylinder_index].end)) + return; + if (MATCH("use.cylinder", cylinder_use, &dive->cylinder[cur_cylinder_index].cylinder_use)) + return; + if (MATCH("o2", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) + return; + if (MATCH("o2percent", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) + return; + if (MATCH("n2", gasmix_nitrogen, &dive->cylinder[cur_cylinder_index].gasmix)) + return; + if (MATCH("he", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.he)) + return; + } + if (MATCH("air.divetemperature", temperature, &dive->airtemp)) + return; + if (MATCH("water.divetemperature", temperature, &dive->watertemp)) + return; + + nonmatch("dive", name, buf); +} + +/* We're in the top-level trip xml. Try to convert whatever value to a trip value */ +static void try_to_fill_trip(dive_trip_t **dive_trip_p, const char *name, char *buf) +{ + start_match("trip", name, buf); + + dive_trip_t *dive_trip = *dive_trip_p; + + if (MATCH("date", divedate, &dive_trip->when)) + return; + if (MATCH("time", divetime, &dive_trip->when)) + return; + if (MATCH("location", utf8_string, &dive_trip->location)) + return; + if (MATCH("notes", utf8_string, &dive_trip->notes)) + return; + + nonmatch("trip", name, buf); +} + +/* We're processing a divesite entry - try to fill the components */ +static void try_to_fill_dive_site(struct dive_site **ds_p, const char *name, char *buf) +{ + start_match("divesite", name, buf); + + struct dive_site *ds = *ds_p; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + + if (MATCH("uuid", hex_value, &ds->uuid)) + return; + if (MATCH("name", utf8_string, &ds->name)) + return; + if (MATCH("description", utf8_string, &ds->description)) + return; + if (MATCH("notes", utf8_string, &ds->notes)) + return; + if (MATCH("gps", gps_location, ds)) + return; + if (MATCH("cat.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].category)) + return; + if (MATCH("origin.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].origin)) + return; + if (MATCH("value.geo", utf8_string, &ds->taxonomy.category[ds->taxonomy.nr].value)) { + if (ds->taxonomy.nr < TC_NR_CATEGORIES) + ds->taxonomy.nr++; + return; + } + + nonmatch("divesite", name, buf); +} + +/* + * While in some formats file boundaries are dive boundaries, in many + * others (as for example in our native format) there are + * multiple dives per file, so there can be other events too that + * trigger a "new dive" marker and you may get some nesting due + * to that. Just ignore nesting levels. + * On the flipside it is possible that we start an XML file that ends + * up having no dives in it at all - don't create a bogus empty dive + * for those. It's not entirely clear what is the minimum set of data + * to make a dive valid, but if it has no location, no date and no + * samples I'm pretty sure it's useless. + */ +static bool is_dive(void) +{ + return (cur_dive && + (cur_dive->dive_site_uuid || cur_dive->when || cur_dive->dc.samples)); +} + +static void reset_dc_info(struct divecomputer *dc) +{ + /* WARN: reset dc info does't touch the dc? */ + (void) dc; + lastcns = lastpo2 = lastndl = laststoptime = laststopdepth = lastindeco = 0; + lastsensor = lastcylinderindex = 0; +} + +static void reset_dc_settings(void) +{ + free((void *)cur_settings.dc.model); + free((void *)cur_settings.dc.nickname); + free((void *)cur_settings.dc.serial_nr); + free((void *)cur_settings.dc.firmware); + cur_settings.dc.model = NULL; + cur_settings.dc.nickname = NULL; + cur_settings.dc.serial_nr = NULL; + cur_settings.dc.firmware = NULL; + cur_settings.dc.deviceid = 0; +} + +static void settings_start(void) +{ + in_settings = true; +} + +static void settings_end(void) +{ + in_settings = false; +} + +static void dc_settings_start(void) +{ + reset_dc_settings(); +} + +static void dc_settings_end(void) +{ + create_device_node(cur_settings.dc.model, cur_settings.dc.deviceid, cur_settings.dc.serial_nr, + cur_settings.dc.firmware, cur_settings.dc.nickname); + reset_dc_settings(); +} + +static void dive_site_start(void) +{ + if (cur_dive_site) + return; + cur_dive_site = calloc(1, sizeof(struct dive_site)); +} + +static void dive_site_end(void) +{ + if (!cur_dive_site) + return; + if (cur_dive_site->uuid) { + // we intentionally call this with '0' to ensure we get + // a new structure and then copy things into that new + // structure a few lines below (which sets the correct + // uuid) + struct dive_site *ds = alloc_or_get_dive_site(0); + if (cur_dive_site->taxonomy.nr == 0) { + free(cur_dive_site->taxonomy.category); + cur_dive_site->taxonomy.category = NULL; + } + copy_dive_site(cur_dive_site, ds); + + if (verbose > 3) + printf("completed dive site uuid %x8 name {%s}\n", ds->uuid, ds->name); + } + free_taxonomy(&cur_dive_site->taxonomy); + free(cur_dive_site); + cur_dive_site = NULL; +} + +// now we need to add the code to parse the parts of the divesite enry + +static void dive_start(void) +{ + if (cur_dive) + return; + cur_dive = alloc_dive(); + reset_dc_info(&cur_dive->dc); + memset(&cur_tm, 0, sizeof(cur_tm)); + if (cur_trip) { + add_dive_to_trip(cur_dive, cur_trip); + cur_dive->tripflag = IN_TRIP; + } +} + +static void dive_end(void) +{ + if (!cur_dive) + return; + if (!is_dive()) + free(cur_dive); + else + record_dive_to_table(cur_dive, target_table); + cur_dive = NULL; + cur_dc = NULL; + cur_latitude.udeg = 0; + cur_longitude.udeg = 0; + cur_cylinder_index = 0; + cur_ws_index = 0; +} + +static void trip_start(void) +{ + if (cur_trip) + return; + dive_end(); + cur_trip = calloc(1, sizeof(dive_trip_t)); + memset(&cur_tm, 0, sizeof(cur_tm)); +} + +static void trip_end(void) +{ + if (!cur_trip) + return; + insert_trip(&cur_trip); + cur_trip = NULL; +} + +static void event_start(void) +{ + memset(&cur_event, 0, sizeof(cur_event)); + cur_event.deleted = 0; /* Active */ +} + +static void event_end(void) +{ + struct divecomputer *dc = get_dc(); + if (strcmp(cur_event.name, "surface") != 0) { /* 123 is a magic event that we used for a while to encode images in dives */ + if (cur_event.type == 123) { + struct picture *pic = alloc_picture(); + pic->filename = strdup(cur_event.name); + /* theoretically this could fail - but we didn't support multi year offsets */ + pic->offset.seconds = cur_event.time.seconds; + dive_add_picture(cur_dive, pic); + } else { + struct event *ev; + /* At some point gas change events did not have any type. Thus we need to add + * one on import, if we encounter the type one missing. + */ + if (cur_event.type == 0 && strcmp(cur_event.name, "gaschange") == 0) + cur_event.type = cur_event.value >> 16 > 0 ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE; + ev = add_event(dc, cur_event.time.seconds, + cur_event.type, cur_event.flags, + cur_event.value, cur_event.name); + + /* + * Older logs might mark the dive to be CCR by having an "SP change" event at time 0:00. Better + * to mark them being CCR on import so no need for special treatments elsewhere on the code. + */ + if (ev && cur_event.time.seconds == 0 && cur_event.type == SAMPLE_EVENT_PO2 && dc->divemode==OC) { + dc->divemode = CCR; + } + + if (ev && event_is_gaschange(ev)) { + /* See try_to_fill_event() on why the filled-in index is one too big */ + ev->gas.index = cur_event.gas.index-1; + if (cur_event.gas.mix.o2.permille || cur_event.gas.mix.he.permille) + ev->gas.mix = cur_event.gas.mix; + } + } + } + cur_event.deleted = 1; /* No longer active */ +} + +static void picture_start(void) +{ + cur_picture = alloc_picture(); +} + +static void picture_end(void) +{ + dive_add_picture(cur_dive, cur_picture); + cur_picture = NULL; +} + +static void cylinder_start(void) +{ +} + +static void cylinder_end(void) +{ + cur_cylinder_index++; +} + +static void ws_start(void) +{ +} + +static void ws_end(void) +{ + cur_ws_index++; +} + +static void sample_start(void) +{ + cur_sample = prepare_sample(get_dc()); + cur_sample->ndl.seconds = lastndl; + cur_sample->in_deco = lastindeco; + cur_sample->stoptime.seconds = laststoptime; + cur_sample->stopdepth.mm = laststopdepth; + cur_sample->cns = lastcns; + cur_sample->setpoint.mbar = lastpo2; + cur_sample->sensor = lastsensor; + next_o2_sensor = 0; +} + +static void sample_end(void) +{ + if (!cur_dive) + return; + + finish_sample(get_dc()); + lastndl = cur_sample->ndl.seconds; + lastindeco = cur_sample->in_deco; + laststoptime = cur_sample->stoptime.seconds; + laststopdepth = cur_sample->stopdepth.mm; + lastcns = cur_sample->cns; + lastpo2 = cur_sample->setpoint.mbar; + cur_sample = NULL; +} + +static void divecomputer_start(void) +{ + struct divecomputer *dc; + + /* Start from the previous dive computer */ + dc = &cur_dive->dc; + while (dc->next) + dc = dc->next; + + /* Did we already fill that in? */ + if (dc->samples || dc->model || dc->when) { + struct divecomputer *newdc = calloc(1, sizeof(*newdc)); + if (newdc) { + dc->next = newdc; + dc = newdc; + } + } + + /* .. this is the one we'll use */ + cur_dc = dc; + reset_dc_info(dc); +} + +static void divecomputer_end(void) +{ + if (!cur_dc->when) + cur_dc->when = cur_dive->when; + cur_dc = NULL; +} + +static void userid_start(void) +{ + in_userid = true; + set_save_userid_local(true); //if the xml contains userid, keep saving it. +} + +static void userid_stop(void) +{ + in_userid = false; +} + +static bool entry(const char *name, char *buf) +{ + if (!strncmp(name, "version.program", sizeof("version.program") - 1) || + !strncmp(name, "version.divelog", sizeof("version.divelog") - 1)) { + last_xml_version = atoi(buf); + report_datafile_version(last_xml_version); + } + if (in_userid) { + try_to_fill_userid(name, buf); + return true; + } + if (in_settings) { + try_to_fill_dc_settings(name, buf); + try_to_match_autogroup(name, buf); + return true; + } + if (cur_dive_site) { + try_to_fill_dive_site(&cur_dive_site, name, buf); + return true; + } + if (!cur_event.deleted) { + try_to_fill_event(name, buf); + return true; + } + if (cur_sample) { + try_to_fill_sample(cur_sample, name, buf); + return true; + } + if (cur_dc) { + try_to_fill_dc(cur_dc, name, buf); + return true; + } + if (cur_dive) { + try_to_fill_dive(cur_dive, name, buf); + return true; + } + if (cur_trip) { + try_to_fill_trip(&cur_trip, name, buf); + return true; + } + return true; +} + +static const char *nodename(xmlNode *node, char *buf, int len) +{ + int levels = 2; + char *p = buf; + + if (!node || (node->type != XML_CDATA_SECTION_NODE && !node->name)) { + return "root"; + } + + if (node->type == XML_CDATA_SECTION_NODE || (node->parent && !strcmp((const char *)node->name, "text"))) + node = node->parent; + + /* Make sure it's always NUL-terminated */ + p[--len] = 0; + + for (;;) { + const char *name = (const char *)node->name; + char c; + while ((c = *name++) != 0) { + /* Cheaper 'tolower()' for ASCII */ + c = (c >= 'A' && c <= 'Z') ? c - 'A' + 'a' : c; + *p++ = c; + if (!--len) + return buf; + } + *p = 0; + node = node->parent; + if (!node || !node->name) + return buf; + *p++ = '.'; + if (!--len) + return buf; + if (!--levels) + return buf; + } +} + +#define MAXNAME 32 + +static bool visit_one_node(xmlNode *node) +{ + xmlChar *content; + static char buffer[MAXNAME]; + const char *name; + + content = node->content; + if (!content || xmlIsBlankNode(node)) + return true; + + name = nodename(node, buffer, sizeof(buffer)); + + return entry(name, (char *)content); +} + +static bool traverse(xmlNode *root); + +static bool traverse_properties(xmlNode *node) +{ + xmlAttr *p; + bool ret = true; + + for (p = node->properties; p; p = p->next) + if ((ret = traverse(p->children)) == false) + break; + return ret; +} + +static bool visit(xmlNode *n) +{ + return visit_one_node(n) && traverse_properties(n) && traverse(n->children); +} + +static void DivingLog_importer(void) +{ + import_source = DIVINGLOG; + + /* + * Diving Log units are really strange. + * + * Temperatures are in C, except in samples, + * when they are in Fahrenheit. Depths are in + * meters, an dpressure is in PSI in the samples, + * but in bar when it comes to working pressure. + * + * Crazy f*%^ morons. + */ + xml_parsing_units = SI_units; +} + +static void uddf_importer(void) +{ + import_source = UDDF; + xml_parsing_units = SI_units; + xml_parsing_units.pressure = PASCAL; + xml_parsing_units.temperature = KELVIN; +} + +static void subsurface_webservice(void) +{ + import_source = SSRF_WS; +} + +/* + * I'm sure this could be done as some fancy DTD rules. + * It's just not worth the headache. + */ +static struct nesting { + const char *name; + void (*start)(void), (*end)(void); +} nesting[] = { + { "divecomputerid", dc_settings_start, dc_settings_end }, + { "settings", settings_start, settings_end }, + { "site", dive_site_start, dive_site_end }, + { "dive", dive_start, dive_end }, + { "Dive", dive_start, dive_end }, + { "trip", trip_start, trip_end }, + { "sample", sample_start, sample_end }, + { "waypoint", sample_start, sample_end }, + { "SAMPLE", sample_start, sample_end }, + { "reading", sample_start, sample_end }, + { "event", event_start, event_end }, + { "mix", cylinder_start, cylinder_end }, + { "gasmix", cylinder_start, cylinder_end }, + { "cylinder", cylinder_start, cylinder_end }, + { "weightsystem", ws_start, ws_end }, + { "divecomputer", divecomputer_start, divecomputer_end }, + { "P", sample_start, sample_end }, + { "userid", userid_start, userid_stop}, + { "picture", picture_start, picture_end }, + { "extradata", extra_data_start, extra_data_end }, + + /* Import type recognition */ + { "Divinglog", DivingLog_importer }, + { "uddf", uddf_importer }, + { "output", subsurface_webservice }, + { NULL, } + }; + +static bool traverse(xmlNode *root) +{ + xmlNode *n; + bool ret = true; + + for (n = root; n; n = n->next) { + struct nesting *rule = nesting; + + if (!n->name) { + if ((ret = visit(n)) == false) + break; + continue; + } + + do { + if (!strcmp(rule->name, (const char *)n->name)) + break; + rule++; + } while (rule->name); + + if (rule->start) + rule->start(); + if ((ret = visit(n)) == false) + break; + if (rule->end) + rule->end(); + } + return ret; +} + +/* Per-file reset */ +static void reset_all(void) +{ + /* + * We reset the units for each file. You'd think it was + * a per-dive property, but I'm not going to trust people + * to do per-dive setup. If the xml does have per-dive + * data within one file, we might have to reset it per + * dive for that format. + */ + xml_parsing_units = SI_units; + import_source = UNKNOWN; +} + +/* divelog.de sends us xml files that claim to be iso-8859-1 + * but once we decode the HTML encoded characters they turn + * into UTF-8 instead. So skip the incorrect encoding + * declaration and decode the HTML encoded characters */ +const char *preprocess_divelog_de(const char *buffer) +{ + char *ret = strstr(buffer, "<DIVELOGSDATA>"); + + if (ret) { + xmlParserCtxtPtr ctx; + char buf[] = ""; + size_t i; + + for (i = 0; i < strlen(ret); ++i) + if (!isascii(ret[i])) + return buffer; + + ctx = xmlCreateMemoryParserCtxt(buf, sizeof(buf)); + ret = (char *)xmlStringLenDecodeEntities(ctx, (xmlChar *)ret, strlen(ret), XML_SUBSTITUTE_REF, 0, 0, 0); + + return ret; + } + return buffer; +} + +int parse_xml_buffer(const char *url, const char *buffer, int size, + struct dive_table *table, const char **params) +{ + (void) size; + xmlDoc *doc; + const char *res = preprocess_divelog_de(buffer); + int ret = 0; + + target_table = table; + doc = xmlReadMemory(res, strlen(res), url, NULL, 0); + if (res != buffer) + free((char *)res); + + if (!doc) + return report_error(translate("gettextFromC", "Failed to parse '%s'"), url); + + set_save_userid_local(false); + reset_all(); + dive_start(); + doc = test_xslt_transforms(doc, params); + if (!traverse(xmlDocGetRootElement(doc))) { + // we decided to give up on parsing... why? + ret = -1; + } + dive_end(); + xmlFreeDoc(doc); + return ret; +} + +void parse_mkvi_buffer(struct membuffer *txt, struct membuffer *csv, const char *starttime) +{ + (void) csv; + (void) txt; + dive_start(); + divedate(starttime, &cur_dive->when); + dive_end(); +} + +extern int dm4_events(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + event_start(); + if (data[1]) + cur_event.time.seconds = atoi(data[1]); + + if (data[2]) { + switch (atoi(data[2])) { + case 1: + /* 1 Mandatory Safety Stop */ + strcpy(cur_event.name, "safety stop (mandatory)"); + break; + case 3: + /* 3 Deco */ + /* What is Subsurface's term for going to + * deco? */ + strcpy(cur_event.name, "deco"); + break; + case 4: + /* 4 Ascent warning */ + strcpy(cur_event.name, "ascent"); + break; + case 5: + /* 5 Ceiling broken */ + strcpy(cur_event.name, "violation"); + break; + case 6: + /* 6 Mandatory safety stop ceiling error */ + strcpy(cur_event.name, "violation"); + break; + case 7: + /* 7 Below deco floor */ + strcpy(cur_event.name, "below floor"); + break; + case 8: + /* 8 Dive time alarm */ + strcpy(cur_event.name, "divetime"); + break; + case 9: + /* 9 Depth alarm */ + strcpy(cur_event.name, "maxdepth"); + break; + case 10: + /* 10 OLF 80% */ + case 11: + /* 11 OLF 100% */ + strcpy(cur_event.name, "OLF"); + break; + case 12: + /* 12 High pOâ‚‚ */ + strcpy(cur_event.name, "PO2"); + break; + case 13: + /* 13 Air time */ + strcpy(cur_event.name, "airtime"); + break; + case 17: + /* 17 Ascent warning */ + strcpy(cur_event.name, "ascent"); + break; + case 18: + /* 18 Ceiling error */ + strcpy(cur_event.name, "ceiling"); + break; + case 19: + /* 19 Surfaced */ + strcpy(cur_event.name, "surface"); + break; + case 20: + /* 20 Deco */ + strcpy(cur_event.name, "deco"); + break; + case 22: + case 32: + /* 22 Mandatory safety stop violation */ + /* 32 Deep stop violation */ + strcpy(cur_event.name, "violation"); + break; + case 30: + /* Tissue level warning */ + strcpy(cur_event.name, "tissue warning"); + break; + case 37: + /* Tank pressure alarm */ + strcpy(cur_event.name, "tank pressure"); + break; + case 257: + /* 257 Dive active */ + /* This seems to be given after surface when + * descending again. */ + strcpy(cur_event.name, "surface"); + break; + case 258: + /* 258 Bookmark */ + if (data[3]) { + strcpy(cur_event.name, "heading"); + cur_event.value = atoi(data[3]); + } else { + strcpy(cur_event.name, "bookmark"); + } + break; + case 259: + /* Deep stop */ + strcpy(cur_event.name, "Deep stop"); + break; + case 260: + /* Deep stop */ + strcpy(cur_event.name, "Deep stop cleared"); + break; + case 266: + /* Mandatory safety stop activated */ + strcpy(cur_event.name, "safety stop (mandatory)"); + break; + case 267: + /* Mandatory safety stop deactivated */ + /* DM5 shows this only on event list, not on the + * profile so skipping as well for now */ + break; + default: + strcpy(cur_event.name, "unknown"); + cur_event.value = atoi(data[2]); + break; + } + } + event_end(); + + return 0; +} + +extern int dm5_cylinders(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + cylinder_start(); + if (data[7] && atoi(data[7]) > 0 && atoi(data[7]) < 350000) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[7]); + if (data[8] && atoi(data[8]) > 0 && atoi(data[8]) < 350000) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[8])); + if (data[6]) { + /* DM5 shows tank size of 12 liters when the actual + * value is 0 (and using metric units). So we just use + * the same 12 liters when size is not available */ + if (atof(data[6]) == 0.0 && cur_dive->cylinder[cur_cylinder_index].start.mbar) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = 12000; + else + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[6])) * 1000; + } + if (data[2]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[2]) * 10; + if (data[3]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[3]) * 10; + cylinder_end(); + return 0; +} + +extern int dm5_gaschange(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + event_start(); + if (data[0]) + cur_event.time.seconds = atoi(data[0]); + if (data[1]) { + strcpy(cur_event.name, "gaschange"); + cur_event.value = atof(data[1]); + } + event_end(); + + return 0; +} + +extern int dm4_tags(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + if (data[0]) + taglist_add_tag(&cur_dive->tag_list, data[0]); + + return 0; +} + +extern int dm4_dive(void *param, int columns, char **data, char **column) +{ + (void) columns; + (void) column; + unsigned int i; + int interval, retval = 0; + sqlite3 *handle = (sqlite3 *)param; + float *profileBlob; + unsigned char *tempBlob; + int *pressureBlob; + char *err = NULL; + char get_events_template[] = "select * from Mark where DiveId = %d"; + char get_tags_template[] = "select Text from DiveTag where DiveId = %d"; + char get_events[64]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + if (data[2]) + utf8_string(data[2], &cur_dive->notes); + + /* + * DM4 stores Duration and DiveTime. It looks like DiveTime is + * 10 to 60 seconds shorter than Duration. However, I have no + * idea what is the difference and which one should be used. + * Duration = data[3] + * DiveTime = data[15] + */ + if (data[3]) + cur_dive->duration.seconds = atoi(data[3]); + if (data[15]) + cur_dive->dc.duration.seconds = atoi(data[15]); + + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[4]) + utf8_string(data[4], &cur_settings.dc.serial_nr); + if (data[5]) + utf8_string(data[5], &cur_settings.dc.model); + + cur_settings.dc.deviceid = 0xffffffff; + dc_settings_end(); + settings_end(); + + if (data[6]) + cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; + if (data[8]) + cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); + if (data[9]) + cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); + + /* + * TODO: handle multiple cylinders + */ + cylinder_start(); + if (data[22] && atoi(data[22]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[22]); + else if (data[10] && atoi(data[10]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[10]); + if (data[23] && atoi(data[23]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[23])); + if (data[11] && atoi(data[11]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[11])); + if (data[12]) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[12])) * 1000; + if (data[13]) + cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = (atoi(data[13])); + if (data[20]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[20]) * 10; + if (data[21]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[21]) * 10; + cylinder_end(); + + if (data[14]) + cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) * 1000); + + interval = data[16] ? atoi(data[16]) : 0; + profileBlob = (float *)data[17]; + tempBlob = (unsigned char *)data[18]; + pressureBlob = (int *)data[19]; + for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { + sample_start(); + cur_sample->time.seconds = i * interval; + if (profileBlob) + cur_sample->depth.mm = profileBlob[i] * 1000; + else + cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; + + if (data[18] && data[18][0]) + cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); + if (data[19] && data[19][0]) + cur_sample->cylinderpressure.mbar = pressureBlob[i]; + sample_end(); + } + + snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_events failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); + return 1; + } + + dive_end(); + + /* + for (i=0; i<columns;++i) { + fprintf(stderr, "%s\t", column[i]); + } + fprintf(stderr, "\n"); + for (i=0; i<columns;++i) { + fprintf(stderr, "%s\t", data[i]); + } + fprintf(stderr, "\n"); + //exit(0); + */ + return SQLITE_OK; +} + +extern int dm5_dive(void *param, int columns, char **data, char **column) +{ + (void) columns; + (void) column; + unsigned int i; + int interval, retval = 0, block_size; + sqlite3 *handle = (sqlite3 *)param; + unsigned const char *sampleBlob; + char *err = NULL; + char get_events_template[] = "select * from Mark where DiveId = %d"; + char get_tags_template[] = "select Text from DiveTag where DiveId = %d"; + char get_cylinders_template[] = "select * from DiveMixture where DiveId = %d"; + char get_gaschange_template[] = "select GasChangeTime,Oxygen,Helium from DiveGasChange join DiveMixture on DiveGasChange.DiveMixtureId=DiveMixture.DiveMixtureId where DiveId = %d"; + char get_events[512]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + if (data[2]) + utf8_string(data[2], &cur_dive->notes); + + if (data[3]) + cur_dive->duration.seconds = atoi(data[3]); + if (data[15]) + cur_dive->dc.duration.seconds = atoi(data[15]); + + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[4]) { + utf8_string(data[4], &cur_settings.dc.serial_nr); + cur_settings.dc.deviceid = atoi(data[4]); + } + if (data[5]) + utf8_string(data[5], &cur_settings.dc.model); + + dc_settings_end(); + settings_end(); + + if (data[6]) + cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; + if (data[8]) + cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); + if (data[9]) + cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); + + if (data[4]) { + cur_dive->dc.deviceid = atoi(data[4]); + } + if (data[5]) + utf8_string(data[5], &cur_dive->dc.model); + + snprintf(get_events, sizeof(get_events) - 1, get_cylinders_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm5_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm5_cylinders failed.\n"); + return 1; + } + + if (data[14]) + cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) / 100); + + interval = data[16] ? atoi(data[16]) : 0; + sampleBlob = (unsigned const char *)data[24]; + + if (sampleBlob) { + switch (sampleBlob[0]) { + case 2: + block_size = 19; + break; + case 3: + block_size = 23; + break; + default: + block_size = 16; + break; + } + } + + for (i = 0; interval && sampleBlob && i * interval < cur_dive->duration.seconds; i++) { + float *depth = (float *)&sampleBlob[i * block_size + 3]; + int32_t temp = (sampleBlob[i * block_size + 10] << 8) + sampleBlob[i * block_size + 11]; + int32_t pressure = (sampleBlob[i * block_size + 9] << 16) + (sampleBlob[i * block_size + 8] << 8) + sampleBlob[i * block_size + 7]; + + sample_start(); + cur_sample->time.seconds = i * interval; + cur_sample->depth.mm = depth[0] * 1000; + /* + * Limit temperatures and cylinder pressures to somewhat + * sensible values + */ + if (temp >= -10 && temp < 50) + cur_sample->temperature.mkelvin = C_to_mkelvin(temp); + if (pressure >= 0 && pressure < 350000) + cur_sample->cylinderpressure.mbar = pressure; + sample_end(); + } + + /* + * Log was converted from DM4, thus we need to parse the profile + * from DM4 format + */ + + if (i == 0) { + float *profileBlob; + unsigned char *tempBlob; + int *pressureBlob; + + profileBlob = (float *)data[17]; + tempBlob = (unsigned char *)data[18]; + pressureBlob = (int *)data[19]; + for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { + sample_start(); + cur_sample->time.seconds = i * interval; + if (profileBlob) + cur_sample->depth.mm = profileBlob[i] * 1000; + else + cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; + + if (data[18] && data[18][0]) + cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); + if (data[19] && data[19][0]) + cur_sample->cylinderpressure.mbar = pressureBlob[i]; + sample_end(); + } + } + + snprintf(get_events, sizeof(get_events) - 1, get_gaschange_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm5_gaschange, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm5_gaschange failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_events failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + (void) buffer; + (void) size; + + int retval; + char *err = NULL; + target_table = table; + + /* StartTime is converted from Suunto's nano seconds to standard + * time. We also need epoch, not seconds since year 1. */ + char get_dives[] = "select D.DiveId,StartTime/10000000-62135596800,Note,Duration,SourceSerialNumber,Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,D.StartPressure,D.EndPressure,Size,CylinderWorkPressure,SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,Oxygen,Helium,MIX.StartPressure,MIX.EndPressure FROM Dive AS D JOIN DiveMixture AS MIX ON D.DiveId=MIX.DiveId"; + + retval = sqlite3_exec(handle, get_dives, &dm4_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + (void) buffer; + (void) size; + + int retval; + char *err = NULL; + target_table = table; + + /* StartTime is converted from Suunto's nano seconds to standard + * time. We also need epoch, not seconds since year 1. */ + char get_dives[] = "select DiveId,StartTime/10000000-62135596800,Note,Duration,coalesce(SourceSerialNumber,SerialNumber),Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,StartPressure,EndPressure,'','',SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,'','','','',SampleBlob FROM Dive where Deleted is null"; + + retval = sqlite3_exec(handle, get_dives, &dm5_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +extern int shearwater_cylinders(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + cylinder_start(); + if (data[0]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atof(data[0]) * 1000; + if (data[1]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atof(data[1]) * 1000; + cylinder_end(); + + return 0; +} + +extern int shearwater_changes(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + event_start(); + if (data[0]) + cur_event.time.seconds = atoi(data[0]); + if (data[1]) { + strcpy(cur_event.name, "gaschange"); + cur_event.value = atof(data[1]) * 100; + } + event_end(); + + return 0; +} + + +extern int cobalt_profile_sample(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + sample_start(); + if (data[0]) + cur_sample->time.seconds = atoi(data[0]); + if (data[1]) + cur_sample->depth.mm = atoi(data[1]); + if (data[2]) + cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); + sample_end(); + + return 0; +} + + +extern int shearwater_profile_sample(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + sample_start(); + if (data[0]) + cur_sample->time.seconds = atoi(data[0]); + if (data[1]) + cur_sample->depth.mm = metric ? atof(data[1]) * 1000 : feet_to_mm(atof(data[1])); + if (data[2]) + cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); + if (data[3]) { + cur_sample->setpoint.mbar = atof(data[3]) * 1000; + cur_dive->dc.divemode = CCR; + } + if (data[4]) + cur_sample->ndl.seconds = atoi(data[4]) * 60; + if (data[5]) + cur_sample->cns = atoi(data[5]); + if (data[6]) + cur_sample->stopdepth.mm = metric ? atoi(data[6]) * 1000 : feet_to_mm(atoi(data[6])); + + /* We don't actually have data[3], but it should appear in the + * SQL query at some point. + if (data[3]) + cur_sample->cylinderpressure.mbar = metric ? atoi(data[3]) * 1000 : psi_to_mbar(atoi(data[3])); + */ + sample_end(); + + return 0; +} + +extern int shearwater_dive(void *param, int columns, char **data, char **column) +{ + (void) columns; + (void) column; + + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select currentTime,currentDepth,waterTemp,averagePPO2,currentNdl,CNSPercent,decoCeiling from dive_log_records where diveLogId = %d"; + char get_cylinder_template[] = "select fractionO2,fractionHe from dive_log_records where diveLogId = %d group by fractionO2,fractionHe"; + char get_changes_template[] = "select a.currentTime,a.fractionO2,a.fractionHe from dive_log_records as a,dive_log_records as b where a.diveLogId = %d and b.diveLogId = %d and (a.id - 1) = b.id and (a.fractionO2 != b.fractionO2 or a.fractionHe != b.fractionHe) union select min(currentTime),fractionO2,fractionHe from dive_log_records"; + char get_buffer[1024]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[2]) + add_dive_site(data[2], cur_dive); + if (data[3]) + utf8_string(data[3], &cur_dive->buddy); + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + metric = atoi(data[5]) == 1 ? 0 : 1; + + /* TODO: verify that metric calculation is correct */ + if (data[6]) + cur_dive->dc.maxdepth.mm = metric ? atof(data[6]) * 1000 : feet_to_mm(atof(data[6])); + + if (data[7]) + cur_dive->dc.duration.seconds = atoi(data[7]) * 60; + + if (data[8]) + cur_dive->dc.surface_pressure.mbar = atoi(data[8]); + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[9]) + utf8_string(data[9], &cur_settings.dc.serial_nr); + if (data[10]) + utf8_string(data[10], &cur_settings.dc.model); + + cur_settings.dc.deviceid = 0xffffffff; + dc_settings_end(); + settings_end(); + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_cylinders failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_changes_template, cur_dive->number, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_changes, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_changes failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_profile_sample, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_profile_sample failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + +extern int cobalt_cylinders(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + cylinder_start(); + if (data[0]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[0]) * 10; + if (data[1]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[1]) * 10; + if (data[2]) + cur_dive->cylinder[cur_cylinder_index].start.mbar = psi_to_mbar(atoi(data[2])); + if (data[3]) + cur_dive->cylinder[cur_cylinder_index].end.mbar = psi_to_mbar(atoi(data[3])); + if (data[4]) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atoi(data[4]) * 100; + if (data[5]) + cur_dive->cylinder[cur_cylinder_index].gas_used.mliter = atoi(data[5]) * 1000; + cylinder_end(); + + return 0; +} + +extern int cobalt_buddies(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + if (data[0]) + utf8_string(data[0], &cur_dive->buddy); + + return 0; +} + +/* + * We still need to figure out how to map free text visibility to + * Subsurface star rating. + */ + +extern int cobalt_visibility(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + (void) data; + return 0; +} + +extern int cobalt_location(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + static char *location = NULL; + if (data[0]) { + if (location) { + char *tmp = malloc(strlen(location) + strlen(data[0]) + 4); + if (!tmp) + return -1; + sprintf(tmp, "%s / %s", location, data[0]); + free(location); + location = NULL; + cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(tmp, cur_dive->when); + free(tmp); + } else { + location = strdup(data[0]); + } + } + return 0; +} + + +extern int cobalt_dive(void *param, int columns, char **data, char **column) +{ + (void) columns; + (void) column; + + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select runtime*60,(DepthPressure*10000/SurfacePressure)-10000,p.Temperature from Dive AS d JOIN TrackPoints AS p ON d.Id=p.DiveId where d.Id=%d"; + char get_cylinder_template[] = "select FO2,FHe,StartingPressure,EndingPressure,TankSize,TankPressure,TotalConsumption from GasMixes where DiveID=%d and StartingPressure>0 group by FO2,FHe"; + char get_buddy_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=4"; + char get_visibility_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=3"; + char get_location_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=0"; + char get_site_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=1"; + char get_buffer[1024]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + /* data[5] should have information on Units used, but I cannot + * parse it at all based on the sample log I have received. The + * temperatures in the samples are all Imperial, so let's go by + * that. + */ + + metric = 0; + + /* Cobalt stores the pressures, not the depth */ + if (data[6]) + cur_dive->dc.maxdepth.mm = atoi(data[6]); + + if (data[7]) + cur_dive->dc.duration.seconds = atoi(data[7]); + + if (data[8]) + cur_dive->dc.surface_pressure.mbar = atoi(data[8]); + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[9]) { + utf8_string(data[9], &cur_settings.dc.serial_nr); + cur_settings.dc.deviceid = atoi(data[9]); + cur_settings.dc.model = strdup("Cobalt import"); + } + + dc_settings_end(); + settings_end(); + + if (data[9]) { + cur_dive->dc.deviceid = atoi(data[9]); + cur_dive->dc.model = strdup("Cobalt import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_cylinders failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_buddy_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_buddies, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_buddies failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_visibility_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_visibility, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_visibility failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_location_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_location failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_site_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_location (site) failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_profile_sample, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_profile_sample failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + (void) buffer; + (void) size; + + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select i.diveId,timestamp,location||' / '||site,buddy,notes,imperialUnits,maxDepth,maxTime,startSurfacePressure,computerSerial,computerModel FROM dive_info AS i JOIN dive_logs AS l ON i.diveId=l.diveId"; + + retval = sqlite3_exec(handle, get_dives, &shearwater_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + (void) buffer; + (void) size; + + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select Id,strftime('%s',DiveStartTime),LocationId,'buddy','notes',Units,(MaxDepthPressure*10000/SurfacePressure)-10000,DiveMinutes,SurfacePressure,SerialNumber,'model' from Dive where IsViewDeleted = 0"; + + retval = sqlite3_exec(handle, get_dives, &cobalt_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +extern int divinglog_cylinder(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + short dbl = 1; + //char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d"; + + /* + * Divinglog might have more cylinders than what we support. So + * better to ignore those. + */ + + if (cur_cylinder_index >= MAX_CYLINDERS) + return 0; + + if (data[7] && atoi(data[7]) > 0) + dbl = 2; + + cylinder_start(); + + /* + * Assuming that we have to double the cylinder size, if double + * is set + */ + + if (data[1] && atoi(data[1]) > 0) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atol(data[1]) * 1000 * dbl; + + if (data[2] && atoi(data[2]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atol(data[2]) * 1000; + if (data[3] && atoi(data[3]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = atol(data[3]) * 1000; + if (data[4] && atoi(data[4]) > 0) + cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = atol(data[4]) * 1000; + if (data[5] && atoi(data[5]) > 0) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atol(data[5]) * 10; + if (data[6] && atoi(data[6]) > 0) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atol(data[6]) * 10; + + cylinder_end(); + + return 0; +} + +extern int divinglog_profile(void *handle, int columns, char **data, char **column) +{ + (void) handle; + (void) columns; + (void) column; + + int sinterval = 0; + unsigned long i, len, lenprofile2 = 0; + char *ptr, temp[4], pres[5], hbeat[4], stop[4], stime[4], ndl[4], ppo2_1[4], ppo2_2[4], ppo2_3[4], cns[5], setpoint[3]; + short oldcyl = -1; + + /* We do not have samples */ + if (!data[1]) + return 0; + + if (data[0]) + sinterval = atoi(data[0]); + + /* + * Profile + * + * DDDDDCRASWEE + * D: Depth (in meter with two decimals) + * C: Deco (1 = yes, 0 = no) + * R: RBT (Remaining Bottom Time warning) + * A: Ascent warning + * S: Decostop ignored + * W: Work warning + * E: Extra info (different for every computer) + * + * Example: 004500010000 + * 4.5 m, no deco, no RBT warning, ascanding too fast, no decostop ignored, no work, no extra info + * + * + * Profile2 + * + * TTTFFFFIRRR + * + * T: Temperature (in °C with one decimal) + * F: Tank pressure 1 (in bar with one decimal) + * I: Tank ID (0, 1, 2 ... 9) + * R: RBT (in min) + * + * Example: 25518051099 + * 25.5 °C, 180.5 bar, Tank 1, 99 min RBT + * + */ + + len = strlen(data[1]); + + if (data[2]) + lenprofile2 = strlen(data[2]); + + for (i = 0, ptr = data[1]; i * 12 < len; ++i) { + sample_start(); + + cur_sample->time.seconds = sinterval * i; + cur_sample->in_deco = ptr[5] - '0' ? true : false; + ptr[5] = 0; + cur_sample->depth.mm = atoi(ptr) * 10; + + if (i * 11 < lenprofile2) { + memcpy(temp, &data[2][i * 11], 3); + cur_sample->temperature.mkelvin = C_to_mkelvin(atoi(temp) / 10); + } + + if (data[2]) { + memcpy(pres, &data[2][i * 11 + 3], 4); + cur_sample->cylinderpressure.mbar = atoi(pres) * 100; + } + + if (data[3] && strlen(data[3])) { + memcpy(hbeat, &data[3][i * 14 + 8], 3); + cur_sample->heartbeat = atoi(hbeat); + } + + if (data[4] && strlen(data[4])) { + memcpy(stop, &data[4][i * 9 + 6], 3); + cur_sample->stopdepth.mm = atoi(stop) * 1000; + + memcpy(stime, &data[4][i * 9 + 3], 3); + cur_sample->stoptime.seconds = atoi(stime) * 60; + + /* + * Following value is NDL when not in deco, and + * either 0 or TTS when in deco. + */ + + memcpy(ndl, &data[4][i * 9 + 0], 3); + if (cur_sample->in_deco == false) + cur_sample->ndl.seconds = atoi(ndl) * 60; + else if (atoi(ndl)) + cur_sample->tts.seconds = atoi(ndl) * 60; + + if (cur_sample->in_deco == true) + cur_sample->ndl.seconds = 0; + } + + /* + * AAABBBCCCOOOONNNNSS + * + * A = ppO2 cell 1 (measured) + * B = ppO2 cell 2 (measured) + * C = ppO2 cell 3 (measured) + * O = OTU + * N = CNS + * S = Setpoint + * + * Example: 1121131141548026411 + * 1.12 bar, 1.13 bar, 1.14 bar, OTU = 154.8, CNS = 26.4, Setpoint = 1.1 + */ + + if (data[5] && strlen(data[5])) { + memcpy(ppo2_1, &data[5][i * 19 + 0], 3); + memcpy(ppo2_2, &data[5][i * 19 + 3], 3); + memcpy(ppo2_3, &data[5][i * 19 + 6], 3); + memcpy(cns, &data[5][i * 19 + 13], 4); + memcpy(setpoint, &data[5][i * 19 + 17], 2); + + if (atoi(ppo2_1) > 0) + cur_sample->o2sensor[0].mbar = atoi(ppo2_1) * 100; + if (atoi(ppo2_2) > 0) + cur_sample->o2sensor[1].mbar = atoi(ppo2_2) * 100; + if (atoi(ppo2_3) > 0) + cur_sample->o2sensor[2].mbar = atoi(ppo2_3) * 100; + if (atoi(cns) > 0) + cur_sample->cns = rint(atoi(cns) / 10); + if (atoi(setpoint) > 0) + cur_sample->setpoint.mbar = atoi(setpoint) * 100; + + } + + /* + * My best guess is that if we have o2sensors, then it + * is either CCR or PSCR dive. And the first time we + * have O2 sensor readings, we can count them to get + * the amount O2 sensors. + */ + + if (!cur_dive->dc.no_o2sensors) { + cur_dive->dc.no_o2sensors = cur_sample->o2sensor[0].mbar ? 1 : 0 + + cur_sample->o2sensor[1].mbar ? 1 : 0 + + cur_sample->o2sensor[2].mbar ? 1 : 0; + cur_dive->dc.divemode = CCR; + } + + ptr += 12; + sample_end(); + } + + for (i = 0, ptr = data[1]; i * 12 < len; ++i) { + /* Remaining bottom time warning */ + if (ptr[6] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "rbt"); + event_end(); + } + + /* Ascent warning */ + if (ptr[7] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "ascent"); + event_end(); + } + + /* Deco stop ignored */ + if (ptr[8] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "violation"); + event_end(); + } + + /* Workload warning */ + if (ptr[9] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "workload"); + event_end(); + } + ptr += 12; + } + + for (i = 0; i * 11 < lenprofile2; ++i) { + short tank = data[2][i * 11 + 7] - '0'; + if (oldcyl != tank) { + struct gasmix *mix = &cur_dive->cylinder[tank].gasmix; + int o2 = get_o2(mix); + int he = get_he(mix); + + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "gaschange"); + + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + cur_event.value = o2 + (he << 16); + + event_end(); + oldcyl = tank; + } + } + + return 0; +} + + +extern int divinglog_dive(void *param, int columns, char **data, char **column) +{ + (void) columns; + (void) column; + + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select ProfileInt,Profile,Profile2,Profile3,Profile4,Profile5 from Logbook where ID = %d"; + char get_cylinder0_template[] = "select 0,TankSize,PresS,PresE,PresW,O2,He,DblTank from Logbook where ID = %d"; + char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d order by TankID"; + char get_buffer[1024]; + + dive_start(); + diveid = atoi(data[13]); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[2]) + cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(data[2], cur_dive->when); + + if (data[3]) + utf8_string(data[3], &cur_dive->buddy); + + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + if (data[5]) + cur_dive->dc.maxdepth.mm = atof(data[5]) * 1000; + + if (data[6]) + cur_dive->dc.duration.seconds = atoi(data[6]) * 60; + + if (data[7]) + utf8_string(data[7], &cur_dive->divemaster); + + if (data[8]) + cur_dive->airtemp.mkelvin = C_to_mkelvin(atol(data[8])); + + if (data[9]) + cur_dive->watertemp.mkelvin = C_to_mkelvin(atol(data[9])); + + if (data[10]) { + cur_dive->weightsystem[0].weight.grams = atol(data[10]) * 1000; + cur_dive->weightsystem[0].description = strdup(translate("gettextFromC", "unknown")); + } + + if (data[11]) + cur_dive->suit = strdup(data[11]); + + settings_start(); + dc_settings_start(); + + if (data[12]) { + cur_dive->dc.model = strdup(data[12]); + } else { + cur_settings.dc.model = strdup("Divinglog import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder0_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_cylinder0 failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_cylinder failed.\n"); + return 1; + } + + + dc_settings_end(); + settings_end(); + + if (data[12]) { + cur_dive->dc.model = strdup(data[12]); + } else { + cur_dive->dc.model = strdup("Divinglog import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_profile, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_profile failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + (void) buffer; + (void) size; + + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select Number,strftime('%s',Divedate || ' ' || ifnull(Entrytime,'00:00')),Country || ' - ' || City || ' - ' || Place,Buddy,Comments,Depth,Divetime,Divemaster,Airtemp,Watertemp,Weight,Divesuit,Computer,ID from Logbook where UUID not in (select UUID from DeletedRecords)"; + + retval = sqlite3_exec(handle, get_dives, &divinglog_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +/* + * Parse a unsigned 32-bit integer in little-endian mode, + * that is seconds since Jan 1, 2000. + */ +static timestamp_t parse_dlf_timestamp(unsigned char *buffer) +{ + timestamp_t offset; + + offset = buffer[3]; + offset = (offset << 8) + buffer[2]; + offset = (offset << 8) + buffer[1]; + offset = (offset << 8) + buffer[0]; + + // Jan 1, 2000 is 946684800 seconds after Jan 1, 1970, which is + // the Unix epoch date that "timestamp_t" uses. + return offset + 946684800; +} + +int parse_dlf_buffer(unsigned char *buffer, size_t size) +{ + unsigned char *ptr = buffer; + unsigned char event; + bool found; + unsigned int time = 0; + int i; + char serial[6]; + + target_table = &dive_table; + + // Check for the correct file magic + if (ptr[0] != 'D' || ptr[1] != 'i' || ptr[2] != 'v' || ptr[3] != 'E') + return -1; + + dive_start(); + divecomputer_start(); + + cur_dc->model = strdup("DLF import"); + // (ptr[7] << 8) + ptr[6] Is "Serial" + snprintf(serial, sizeof(serial), "%d", (ptr[7] << 8) + ptr[6]); + cur_dc->serial = strdup(serial); + cur_dc->when = parse_dlf_timestamp(ptr + 8); + cur_dive->when = cur_dc->when; + + cur_dc->duration.seconds = ((ptr[14] & 0xFE) << 16) + (ptr[13] << 8) + ptr[12]; + + // ptr[14] >> 1 is scrubber used in % + + // 3 bit dive type + switch((ptr[15] & 0x30) >> 3) { + case 0: // unknown + case 1: + cur_dc->divemode = OC; + break; + case 2: + cur_dc->divemode = CCR; + break; + case 3: + cur_dc->divemode = CCR; // mCCR + break; + case 4: + cur_dc->divemode = FREEDIVE; + break; + case 5: + cur_dc->divemode = OC; // Gauge + break; + case 6: + cur_dc->divemode = PSCR; // ASCR + break; + case 7: + cur_dc->divemode = PSCR; + break; + } + + cur_dc->maxdepth.mm = ((ptr[21] << 8) + ptr[20]) * 10; + cur_dc->surface_pressure.mbar = ((ptr[25] << 8) + ptr[24]) / 10; + + /* Done with parsing what we know about the dive header */ + ptr += 32; + + // We're going to interpret ppO2 saved as a sensor value in these modes. + if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) + cur_dc->no_o2sensors = 1; + + while (ptr < buffer + size) { + time = ((ptr[0] >> 4) & 0x0f) + + ((ptr[1] << 4) & 0xff0) + + (ptr[2] & 0x0f) * 3600; /* hours */ + event = ptr[0] & 0x0f; + switch (event) { + case 0: + /* Regular sample */ + sample_start(); + cur_sample->time.seconds = time; + cur_sample->depth.mm = ((ptr[5] << 8) + ptr[4]) * 10; + // Crazy precision on these stored values... + // Only store value if we're in CCR/PSCR mode, + // because we rather calculate ppo2 our selfs. + if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) + cur_sample->o2sensor[0].mbar = ((ptr[7] << 8) + ptr[6]) / 10; + // NDL in minutes, 10 bit + cur_sample->ndl.seconds = (((ptr[9] & 0x03) << 8) + ptr[8]) * 60; + // TTS in minutes, 10 bit + cur_sample->tts.seconds = (((ptr[10] & 0x0F) << 6) + (ptr[9] >> 2)) * 60; + // Temperature in 1/10 C, 10 bit signed + cur_sample->temperature.mkelvin = ((ptr[11] & 0x20) ? -1 : 1) * (((ptr[11] & 0x1F) << 4) + (ptr[10] >> 4)) * 100 + ZERO_C_IN_MKELVIN; + // ptr[11] & 0xF0 is unknown, and always 0xC in all checked files + cur_sample->stopdepth.mm = ((ptr[13] << 8) + ptr[12]) * 10; + if (cur_sample->stopdepth.mm) + cur_sample->in_deco = true; + //ptr[14] is helium content, always zero? + //ptr[15] is setpoint, always zero? + sample_end(); + break; + case 1: /* dive event */ + case 2: /* automatic parameter change */ + case 3: /* diver error */ + case 4: /* internal error */ + case 5: /* device activity log */ + event_start(); + cur_event.time.seconds = time; + switch (ptr[4]) { + case 1: + strcpy(cur_event.name, "Setpoint Manual"); + // There is a setpoint value somewhere... + break; + case 2: + strcpy(cur_event.name, "Setpoint Auto"); + // There is a setpoint value somewhere... + switch (ptr[7]) { + case 0: + strcat(cur_event.name, " Manual"); + break; + case 1: + strcat(cur_event.name, " Auto Start"); + break; + case 2: + strcat(cur_event.name, " Auto Hypox"); + break; + case 3: + strcat(cur_event.name, " Auto Timeout"); + break; + case 4: + strcat(cur_event.name, " Auto Ascent"); + break; + case 5: + strcat(cur_event.name, " Auto Stall"); + break; + case 6: + strcat(cur_event.name, " Auto SP Low"); + break; + default: + break; + } + break; + case 3: + // obsolete + strcpy(cur_event.name, "OC"); + break; + case 4: + // obsolete + strcpy(cur_event.name, "CCR"); + break; + case 5: + strcpy(cur_event.name, "gaschange"); + cur_event.type = SAMPLE_EVENT_GASCHANGE2; + cur_event.value = ptr[7] << 8 ^ ptr[6]; + + found = false; + for (i = 0; i < cur_cylinder_index; ++i) { + if (cur_dive->cylinder[i].gasmix.o2.permille == ptr[6] * 10 && cur_dive->cylinder[i].gasmix.he.permille == ptr[7] * 10) { + found = true; + break; + } + } + if (!found) { + cylinder_start(); + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = ptr[6] * 10; + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = ptr[7] * 10; + cylinder_end(); + cur_event.gas.index = cur_cylinder_index; + } else { + cur_event.gas.index = i; + } + break; + case 6: + strcpy(cur_event.name, "Start"); + break; + case 7: + strcpy(cur_event.name, "Too Fast"); + break; + case 8: + strcpy(cur_event.name, "Above Ceiling"); + break; + case 9: + strcpy(cur_event.name, "Toxic"); + break; + case 10: + strcpy(cur_event.name, "Hypox"); + break; + case 11: + strcpy(cur_event.name, "Critical"); + break; + case 12: + strcpy(cur_event.name, "Sensor Disabled"); + break; + case 13: + strcpy(cur_event.name, "Sensor Enabled"); + break; + case 14: + strcpy(cur_event.name, "O2 Backup"); + break; + case 15: + strcpy(cur_event.name, "Peer Down"); + break; + case 16: + strcpy(cur_event.name, "HS Down"); + break; + case 17: + strcpy(cur_event.name, "Inconsistent"); + break; + case 18: + // key pressed - probably not + // interesting to view on profile + break; + case 19: + // obsolete + strcpy(cur_event.name, "SCR"); + break; + case 20: + strcpy(cur_event.name, "Above Stop"); + break; + case 21: + strcpy(cur_event.name, "Safety Miss"); + break; + case 22: + strcpy(cur_event.name, "Fatal"); + break; + case 23: + strcpy(cur_event.name, "Diluent"); + break; + case 24: + strcpy(cur_event.name, "gaschange"); + cur_event.type = SAMPLE_EVENT_GASCHANGE2; + cur_event.value = ptr[7] << 8 ^ ptr[6]; + event_end(); + // This is both a mode change and a gas change event + // so we encode it as two separate events. + event_start(); + strcpy(cur_event.name, "Change Mode"); + switch (ptr[8]) { + case 1: + strcat(cur_event.name, ": OC"); + break; + case 2: + strcat(cur_event.name, ": CCR"); + break; + case 3: + strcat(cur_event.name, ": mCCR"); + break; + case 4: + strcat(cur_event.name, ": Free"); + break; + case 5: + strcat(cur_event.name, ": Gauge"); + break; + case 6: + strcat(cur_event.name, ": ASCR"); + break; + case 7: + strcat(cur_event.name, ": PSCR"); + break; + default: + break; + } + event_end(); + break; + case 25: + strcpy(cur_event.name, "CCR O2 solenoid opened/closed"); + break; + case 26: + strcpy(cur_event.name, "User mark"); + break; + case 27: + snprintf(cur_event.name, MAX_EVENT_NAME, "%sGF Switch (%d/%d)", ptr[6] ? "Bailout, ": "", ptr[7], ptr[8]); + break; + case 28: + strcpy(cur_event.name, "Peer Up"); + break; + case 29: + strcpy(cur_event.name, "HS Up"); + break; + case 30: + snprintf(cur_event.name, MAX_EVENT_NAME, "CNS %d%%", ptr[6]); + break; + default: + // No values above 30 had any description + break; + } + event_end(); + break; + case 6: + /* device configuration */ + break; + case 7: + /* measure record */ + /* Po2 sample? Solenoid inject? */ + //fprintf(stderr, "%02X %02X%02X %02X%02X\n", ptr[5], ptr[6], ptr[7], ptr[8], ptr[9]); + break; + default: + /* Unknown... */ + break; + } + ptr += 16; + } + divecomputer_end(); + dive_end(); + return 0; +} + + +void parse_xml_init(void) +{ + LIBXML_TEST_VERSION +} + +void parse_xml_exit(void) +{ + xmlCleanupParser(); +} + +static struct xslt_files { + const char *root; + const char *file; + const char *attribute; +} xslt_files[] = { + { "SUUNTO", "SuuntoSDM.xslt", NULL }, + { "Dive", "SuuntoDM4.xslt", "xmlns" }, + { "Dive", "shearwater.xslt", "version" }, + { "JDiveLog", "jdivelog2subsurface.xslt", NULL }, + { "dives", "MacDive.xslt", NULL }, + { "DIVELOGSDATA", "divelogs.xslt", NULL }, + { "uddf", "uddf.xslt", NULL }, + { "UDDF", "uddf.xslt", NULL }, + { "profile", "udcf.xslt", NULL }, + { "Divinglog", "DivingLog.xslt", NULL }, + { "csv", "csv2xml.xslt", NULL }, + { "sensuscsv", "sensuscsv.xslt", NULL }, + { "SubsurfaceCSV", "subsurfacecsv.xslt", NULL }, + { "manualcsv", "manualcsv2xml.xslt", NULL }, + { "logbook", "DiveLog.xslt", NULL }, + { NULL, } + }; + +static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params) +{ + struct xslt_files *info = xslt_files; + xmlDoc *transformed; + xsltStylesheetPtr xslt = NULL; + xmlNode *root_element = xmlDocGetRootElement(doc); + char *attribute; + + while (info->root) { + if ((strcasecmp((const char *)root_element->name, info->root) == 0)) { + if (info->attribute == NULL) + break; + else if (xmlGetProp(root_element, (const xmlChar *)info->attribute) != NULL) + break; + } + info++; + } + + if (info->root) { + attribute = (char *)xmlGetProp(xmlFirstElementChild(root_element), (const xmlChar *)"name"); + if (attribute) { + if (strcasecmp(attribute, "subsurface") == 0) { + free((void *)attribute); + return doc; + } + free((void *)attribute); + } + xmlSubstituteEntitiesDefault(1); + xslt = get_stylesheet(info->file); + if (xslt == NULL) { + report_error(translate("gettextFromC", "Can't open stylesheet %s"), info->file); + return doc; + } + transformed = xsltApplyStylesheet(xslt, doc, params); + xmlFreeDoc(doc); + xsltFreeStylesheet(xslt); + + return transformed; + } + return doc; +} diff --git a/core/planner.c b/core/planner.c new file mode 100644 index 000000000..705aad1cb --- /dev/null +++ b/core/planner.c @@ -0,0 +1,1471 @@ +/* planner.c + * + * code that allows us to plan future dives + * + * (c) Dirk Hohndel 2013 + */ +#include <assert.h> +#include <unistd.h> +#include <ctype.h> +#include <string.h> +#include "dive.h" +#include "deco.h" +#include "divelist.h" +#include "planner.h" +#include "gettext.h" +#include "libdivecomputer/parser.h" + +#define TIMESTEP 2 /* second */ +#define DECOTIMESTEP 60 /* seconds. Unit of deco stop times */ + +int decostoplevels_metric[] = { 0, 3000, 6000, 9000, 12000, 15000, 18000, 21000, 24000, 27000, + 30000, 33000, 36000, 39000, 42000, 45000, 48000, 51000, 54000, 57000, + 60000, 63000, 66000, 69000, 72000, 75000, 78000, 81000, 84000, 87000, + 90000, 100000, 110000, 120000, 130000, 140000, 150000, 160000, 170000, + 180000, 190000, 200000, 220000, 240000, 260000, 280000, 300000, + 320000, 340000, 360000, 380000 }; +int decostoplevels_imperial[] = { 0, 3048, 6096, 9144, 12192, 15240, 18288, 21336, 24384, 27432, + 30480, 33528, 36576, 39624, 42672, 45720, 48768, 51816, 54864, 57912, + 60960, 64008, 67056, 70104, 73152, 76200, 79248, 82296, 85344, 88392, + 91440, 101600, 111760, 121920, 132080, 142240, 152400, 162560, 172720, + 182880, 193040, 203200, 223520, 243840, 264160, 284480, 304800, + 325120, 345440, 365760, 386080 }; + +double plangflow, plangfhigh; +bool plan_verbatim, plan_display_runtime, plan_display_duration, plan_display_transitions; + +pressure_t first_ceiling_pressure, max_bottom_ceiling_pressure = {}; + +const char *disclaimer; + +#if DEBUG_PLAN +void dump_plan(struct diveplan *diveplan) +{ + struct divedatapoint *dp; + struct tm tm; + + if (!diveplan) { + printf("Diveplan NULL\n"); + return; + } + utc_mkdate(diveplan->when, &tm); + + printf("\nDiveplan @ %04d-%02d-%02d %02d:%02d:%02d (surfpres %dmbar):\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + diveplan->surface_pressure); + dp = diveplan->dp; + while (dp) { + printf("\t%3u:%02u: %dmm gas: %d o2 %d h2\n", FRACTION(dp->time, 60), dp->depth, get_o2(&dp->gasmix), get_he(&dp->gasmix)); + dp = dp->next; + } +} +#endif + +bool diveplan_empty(struct diveplan *diveplan) +{ + struct divedatapoint *dp; + if (!diveplan || !diveplan->dp) + return true; + dp = diveplan->dp; + while (dp) { + if (dp->time) + return false; + dp = dp->next; + } + return true; +} + +/* get the gas at a certain time during the dive */ +void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas) +{ + // we always start with the first gas, so that's our gas + // unless an event tells us otherwise + struct event *event = dc->events; + *gas = dive->cylinder[0].gasmix; + while (event && event->time.seconds <= time.seconds) { + if (!strcmp(event->name, "gaschange")) { + int cylinder_idx = get_cylinder_index(dive, event); + *gas = dive->cylinder[cylinder_idx].gasmix; + } + event = event->next; + } +} + +int get_gasidx(struct dive *dive, struct gasmix *mix) +{ + int gasidx = -1; + + while (++gasidx < MAX_CYLINDERS) + if (gasmix_distance(&dive->cylinder[gasidx].gasmix, mix) < 100) + return gasidx; + return -1; +} + +void interpolate_transition(struct dive *dive, duration_t t0, duration_t t1, depth_t d0, depth_t d1, const struct gasmix *gasmix, o2pressure_t po2) +{ + uint32_t j; + + for (j = t0.seconds; j < t1.seconds; j++) { + int depth = interpolate(d0.mm, d1.mm, j - t0.seconds, t1.seconds - t0.seconds); + add_segment(depth_to_bar(depth, dive), gasmix, 1, po2.mbar, dive, prefs.bottomsac); + } + if (d1.mm > d0.mm) + calc_crushing_pressure(depth_to_bar(d1.mm, &displayed_dive)); +} + +/* returns the tissue tolerance at the end of this (partial) dive */ +void tissue_at_end(struct dive *dive, char **cached_datap) +{ + struct divecomputer *dc; + struct sample *sample, *psample; + int i; + depth_t lastdepth = {}; + duration_t t0 = {}, t1 = {}; + struct gasmix gas; + + if (!dive) + return; + if (*cached_datap) { + restore_deco_state(*cached_datap); + } else { + init_decompression(dive); + cache_deco_state(cached_datap); + } + dc = &dive->dc; + if (!dc->samples) + return; + psample = sample = dc->sample; + + for (i = 0; i < dc->samples; i++, sample++) { + o2pressure_t setpoint; + + if (i) + setpoint = sample[-1].setpoint; + else + setpoint = sample[0].setpoint; + + t1 = sample->time; + get_gas_at_time(dive, dc, t0, &gas); + if (i > 0) + lastdepth = psample->depth; + + /* The ceiling in the deeper portion of a multilevel dive is sometimes critical for the VPM-B + * Boyle's law compensation. We should check the ceiling prior to ascending during the bottom + * portion of the dive. The maximum ceiling might be reached while ascending, but testing indicates + * that it is only marginally deeper than the ceiling at the start of ascent. + * Do not set the first_ceiling_pressure variable (used for the Boyle's law compensation calculation) + * at this stage, because it would interfere with calculating the ceiling at the end of the bottom + * portion of the dive. + * Remember the value for later. + */ + if ((prefs.deco_mode == VPMB) && (lastdepth.mm > sample->depth.mm)) { + pressure_t ceiling_pressure; + nuclear_regeneration(t0.seconds); + vpmb_start_gradient(); + ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(dive, + depth_to_bar(lastdepth.mm, dive)), + dive->surface_pressure.mbar / 1000.0, + dive, + 1), + dive); + if (ceiling_pressure.mbar > max_bottom_ceiling_pressure.mbar) + max_bottom_ceiling_pressure.mbar = ceiling_pressure.mbar; + } + + interpolate_transition(dive, t0, t1, lastdepth, sample->depth, &gas, setpoint); + psample = sample; + t0 = t1; + } +} + + +/* if a default cylinder is set, use that */ +void fill_default_cylinder(cylinder_t *cyl) +{ + const char *cyl_name = prefs.default_cylinder; + struct tank_info_t *ti = tank_info; + pressure_t pO2 = {.mbar = 1600}; + + if (!cyl_name) + return; + while (ti->name != NULL) { + if (strcmp(ti->name, cyl_name) == 0) + break; + ti++; + } + if (ti->name == NULL) + /* didn't find it */ + return; + cyl->type.description = strdup(ti->name); + if (ti->ml) { + cyl->type.size.mliter = ti->ml; + cyl->type.workingpressure.mbar = ti->bar * 1000; + } else { + cyl->type.workingpressure.mbar = psi_to_mbar(ti->psi); + if (ti->psi) + cyl->type.size.mliter = cuft_to_l(ti->cuft) * 1000 / bar_to_atm(psi_to_bar(ti->psi)); + } + // MOD of air + cyl->depth = gas_mod(&cyl->gasmix, pO2, &displayed_dive, 1); +} + +/* make sure that the gas we are switching to is represented in our + * list of cylinders */ +static int verify_gas_exists(struct gasmix mix_in) +{ + int i; + cylinder_t *cyl; + + for (i = 0; i < MAX_CYLINDERS; i++) { + cyl = displayed_dive.cylinder + i; + if (cylinder_nodata(cyl)) + continue; + if (gasmix_distance(&cyl->gasmix, &mix_in) < 100) + return i; + } + fprintf(stderr, "this gas %s should have been on the cylinder list\nThings will fail now\n", gasname(&mix_in)); + return -1; +} + +/* calculate the new end pressure of the cylinder, based on its current end pressure and the + * latest segment. */ +static void update_cylinder_pressure(struct dive *d, int old_depth, int new_depth, int duration, int sac, cylinder_t *cyl, bool in_deco) +{ + volume_t gas_used; + pressure_t delta_p; + depth_t mean_depth; + int factor = 1000; + + if (d->dc.divemode == PSCR) + factor = prefs.pscr_ratio; + + if (!cyl) + return; + mean_depth.mm = (old_depth + new_depth) / 2; + gas_used.mliter = depth_to_atm(mean_depth.mm, d) * sac / 60 * duration * factor / 1000; + cyl->gas_used.mliter += gas_used.mliter; + if (in_deco) + cyl->deco_gas_used.mliter += gas_used.mliter; + if (cyl->type.size.mliter) { + delta_p.mbar = gas_used.mliter * 1000.0 / cyl->type.size.mliter; + cyl->end.mbar -= delta_p.mbar; + } +} + +/* simply overwrite the data in the displayed_dive + * return false if something goes wrong */ +static void create_dive_from_plan(struct diveplan *diveplan, bool track_gas) +{ + struct divedatapoint *dp; + struct divecomputer *dc; + struct sample *sample; + struct gasmix oldgasmix; + struct event *ev; + cylinder_t *cyl; + int oldpo2 = 0; + int lasttime = 0; + int lastdepth = 0; + enum dive_comp_type type = displayed_dive.dc.divemode; + + if (!diveplan || !diveplan->dp) + return; +#if DEBUG_PLAN & 4 + printf("in create_dive_from_plan\n"); + dump_plan(diveplan); +#endif + displayed_dive.salinity = diveplan->salinity; + // reset the cylinders and clear out the samples and events of the + // displayed dive so we can restart + reset_cylinders(&displayed_dive, track_gas); + dc = &displayed_dive.dc; + dc->when = displayed_dive.when = diveplan->when; + free(dc->sample); + dc->sample = NULL; + dc->samples = 0; + dc->alloc_samples = 0; + while ((ev = dc->events)) { + dc->events = dc->events->next; + free(ev); + } + dp = diveplan->dp; + cyl = &displayed_dive.cylinder[0]; + oldgasmix = cyl->gasmix; + sample = prepare_sample(dc); + sample->setpoint.mbar = dp->setpoint; + sample->sac.mliter = prefs.bottomsac; + oldpo2 = dp->setpoint; + if (track_gas && cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->end.mbar; + sample->manually_entered = true; + finish_sample(dc); + while (dp) { + struct gasmix gasmix = dp->gasmix; + int po2 = dp->setpoint; + if (dp->setpoint) + type = CCR; + int time = dp->time; + int depth = dp->depth; + + if (time == 0) { + /* special entries that just inform the algorithm about + * additional gases that are available */ + if (verify_gas_exists(gasmix) < 0) + goto gas_error_exit; + dp = dp->next; + continue; + } + + /* Check for SetPoint change */ + if (oldpo2 != po2) { + /* this is a bad idea - we should get a different SAMPLE_EVENT type + * reserved for this in libdivecomputer... overloading SMAPLE_EVENT_PO2 + * with a different meaning will only cause confusion elsewhere in the code */ + add_event(dc, lasttime, SAMPLE_EVENT_PO2, 0, po2, "SP change"); + oldpo2 = po2; + } + + /* Make sure we have the new gas, and create a gas change event */ + if (gasmix_distance(&gasmix, &oldgasmix) > 0) { + int idx; + if ((idx = verify_gas_exists(gasmix)) < 0) + goto gas_error_exit; + /* need to insert a first sample for the new gas */ + add_gas_switch_event(&displayed_dive, dc, lasttime + 1, idx); + cyl = &displayed_dive.cylinder[idx]; + sample = prepare_sample(dc); + sample[-1].setpoint.mbar = po2; + sample->time.seconds = lasttime + 1; + sample->depth.mm = lastdepth; + sample->manually_entered = dp->entered; + sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; + if (track_gas && cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->sample_end.mbar; + finish_sample(dc); + oldgasmix = gasmix; + } + /* Create sample */ + sample = prepare_sample(dc); + /* set po2 at beginning of this segment */ + /* and keep it valid for last sample - where it likely doesn't matter */ + sample[-1].setpoint.mbar = po2; + sample->setpoint.mbar = po2; + sample->time.seconds = lasttime = time; + sample->depth.mm = lastdepth = depth; + sample->manually_entered = dp->entered; + sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; + if (track_gas && !sample[-1].setpoint.mbar) { /* Don't track gas usage for CCR legs of dive */ + update_cylinder_pressure(&displayed_dive, sample[-1].depth.mm, depth, time - sample[-1].time.seconds, + dp->entered ? diveplan->bottomsac : diveplan->decosac, cyl, !dp->entered); + if (cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->end.mbar; + } + finish_sample(dc); + dp = dp->next; + } + dc->divemode = type; +#if DEBUG_PLAN & 32 + save_dive(stdout, &displayed_dive); +#endif + return; + +gas_error_exit: + report_error(translate("gettextFromC", "Too many gas mixes")); + return; +} + +void free_dps(struct diveplan *diveplan) +{ + if (!diveplan) + return; + struct divedatapoint *dp = diveplan->dp; + while (dp) { + struct divedatapoint *ndp = dp->next; + free(dp); + dp = ndp; + } + diveplan->dp = NULL; +} + +struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2) +{ + struct divedatapoint *dp; + + dp = malloc(sizeof(struct divedatapoint)); + dp->time = time_incr; + dp->depth = depth; + dp->gasmix = gasmix; + dp->setpoint = po2; + dp->entered = false; + dp->next = NULL; + return dp; +} + +void add_to_end_of_diveplan(struct diveplan *diveplan, struct divedatapoint *dp) +{ + struct divedatapoint **lastdp = &diveplan->dp; + struct divedatapoint *ldp = *lastdp; + int lasttime = 0; + while (*lastdp) { + ldp = *lastdp; + if (ldp->time > lasttime) + lasttime = ldp->time; + lastdp = &(*lastdp)->next; + } + *lastdp = dp; + if (ldp && dp->time != 0) + dp->time += lasttime; +} + +struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered) +{ + struct divedatapoint *dp = create_dp(duration, depth, gasmix, po2); + dp->entered = entered; + add_to_end_of_diveplan(diveplan, dp); + return (dp); +} + +struct gaschanges { + int depth; + int gasidx; +}; + + +static struct gaschanges *analyze_gaslist(struct diveplan *diveplan, int *gaschangenr, int depth, int *asc_cylinder) +{ + struct gasmix gas; + int nr = 0; + struct gaschanges *gaschanges = NULL; + struct divedatapoint *dp = diveplan->dp; + int best_depth = displayed_dive.cylinder[*asc_cylinder].depth.mm; + while (dp) { + if (dp->time == 0) { + gas = dp->gasmix; + if (dp->depth <= depth) { + int i = 0; + nr++; + gaschanges = realloc(gaschanges, nr * sizeof(struct gaschanges)); + while (i < nr - 1) { + if (dp->depth < gaschanges[i].depth) { + memmove(gaschanges + i + 1, gaschanges + i, (nr - i - 1) * sizeof(struct gaschanges)); + break; + } + i++; + } + gaschanges[i].depth = dp->depth; + gaschanges[i].gasidx = get_gasidx(&displayed_dive, &gas); + assert(gaschanges[i].gasidx != -1); + } else { + /* is there a better mix to start deco? */ + if (dp->depth < best_depth) { + best_depth = dp->depth; + *asc_cylinder = get_gasidx(&displayed_dive, &gas); + } + } + } + dp = dp->next; + } + *gaschangenr = nr; +#if DEBUG_PLAN & 16 + for (nr = 0; nr < *gaschangenr; nr++) { + int idx = gaschanges[nr].gasidx; + printf("gaschange nr %d: @ %5.2lfm gasidx %d (%s)\n", nr, gaschanges[nr].depth / 1000.0, + idx, gasname(&displayed_dive.cylinder[idx].gasmix)); + } +#endif + return gaschanges; +} + +/* sort all the stops into one ordered list */ +static int *sort_stops(int *dstops, int dnr, struct gaschanges *gstops, int gnr) +{ + int i, gi, di; + int total = dnr + gnr; + int *stoplevels = malloc(total * sizeof(int)); + + /* no gaschanges */ + if (gnr == 0) { + memcpy(stoplevels, dstops, dnr * sizeof(int)); + return stoplevels; + } + i = total - 1; + gi = gnr - 1; + di = dnr - 1; + while (i >= 0) { + if (dstops[di] > gstops[gi].depth) { + stoplevels[i] = dstops[di]; + di--; + } else if (dstops[di] == gstops[gi].depth) { + stoplevels[i] = dstops[di]; + di--; + gi--; + } else { + stoplevels[i] = gstops[gi].depth; + gi--; + } + i--; + if (di < 0) { + while (gi >= 0) + stoplevels[i--] = gstops[gi--].depth; + break; + } + if (gi < 0) { + while (di >= 0) + stoplevels[i--] = dstops[di--]; + break; + } + } + while (i >= 0) + stoplevels[i--] = 0; + +#if DEBUG_PLAN & 16 + int k; + for (k = gnr + dnr - 1; k >= 0; k--) { + printf("stoplevel[%d]: %5.2lfm\n", k, stoplevels[k] / 1000.0); + if (stoplevels[k] == 0) + break; + } +#endif + return stoplevels; +} + +static void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, int error) +{ + const unsigned int sz_buffer = 2000000; + const unsigned int sz_temp = 100000; + char *buffer = (char *)malloc(sz_buffer); + char *temp = (char *)malloc(sz_temp); + char *deco, *segmentsymbol; + static char buf[1000]; + int len, lastdepth = 0, lasttime = 0, lastsetpoint = -1, newdepth = 0, lastprintdepth = 0, lastprintsetpoint = -1; + struct gasmix lastprintgasmix = {{ -1 }, { -1 }}; + struct divedatapoint *dp = diveplan->dp; + bool gaschange_after = !plan_verbatim; + bool gaschange_before; + bool lastentered = true; + struct divedatapoint *nextdp = NULL; + + plan_verbatim = prefs.verbatim_plan; + plan_display_runtime = prefs.display_runtime; + plan_display_duration = prefs.display_duration; + plan_display_transitions = prefs.display_transitions; + + if (prefs.deco_mode == VPMB) { + deco = "VPM-B"; + } else { + deco = "BUHLMANN"; + } + + snprintf(buf, sizeof(buf), translate("gettextFromC", "DISCLAIMER / WARNING: THIS IS A NEW IMPLEMENTATION OF THE %s " + "ALGORITHM AND A DIVE PLANNER IMPLEMENTATION BASED ON THAT WHICH HAS " + "RECEIVED ONLY A LIMITED AMOUNT OF TESTING. WE STRONGLY RECOMMEND NOT TO " + "PLAN DIVES SIMPLY BASED ON THE RESULTS GIVEN HERE."), deco); + disclaimer = buf; + + if (!dp) { + free((void *)buffer); + free((void *)temp); + return; + } + + if (error) { + snprintf(temp, sz_temp, "%s", + translate("gettextFromC", "Decompression calculation aborted due to excessive time")); + snprintf(buffer, sz_buffer, "<span style='color: red;'>%s </span> %s<br>", + translate("gettextFromC", "Warning:"), temp); + dive->notes = strdup(buffer); + + free((void *)buffer); + free((void *)temp); + return; + } + + len = show_disclaimer ? snprintf(buffer, sz_buffer, "<div><b>%s<b></div><br>", disclaimer) : 0; + if (prefs.deco_mode == BUEHLMANN){ + snprintf(temp, sz_temp, translate("gettextFromC", "based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), + diveplan->gflow, diveplan->gfhigh); + } else if (prefs.deco_mode == VPMB){ + if (prefs.conservatism_level == 0) + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "based on VPM-B at nominal conservatism")); + else + snprintf(temp, sz_temp, translate("gettextFromC", "based on VPM-B at +%d conservatism"), prefs.conservatism_level); + } else if (prefs.deco_mode == RECREATIONAL){ + snprintf(temp, sz_temp, translate("gettextFromC", "recreational mode based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), + diveplan->gflow, diveplan->gfhigh); + } + len += snprintf(buffer + len, sz_buffer - len, "<div><b>%s</b><br>%s</div><br>", + translate("gettextFromC", "Subsurface dive plan"), temp); + + if (!plan_verbatim) { + len += snprintf(buffer + len, sz_buffer - len, "<div><table><thead><tr><th></th><th>%s</th>", + translate("gettextFromC", "depth")); + if (plan_display_duration) + len += snprintf(buffer + len, sz_buffer - len, "<th style='padding-left: 10px;'>%s</th>", + translate("gettextFromC", "duration")); + if (plan_display_runtime) + len += snprintf(buffer + len, sz_buffer - len, "<th style='padding-left: 10px;'>%s</th>", + translate("gettextFromC", "runtime")); + len += snprintf(buffer + len, sz_buffer - len, + "<th style='padding-left: 10px; float: left;'>%s</th></tr></thead><tbody style='float: left;'>", + translate("gettextFromC", "gas")); + } + do { + struct gasmix gasmix, newgasmix = {}; + const char *depth_unit; + double depthvalue; + int decimals; + bool isascent = (dp->depth < lastdepth); + + nextdp = dp->next; + if (dp->time == 0) + continue; + gasmix = dp->gasmix; + depthvalue = get_depth_units(dp->depth, &decimals, &depth_unit); + /* analyze the dive points ahead */ + while (nextdp && nextdp->time == 0) + nextdp = nextdp->next; + if (nextdp) + newgasmix = nextdp->gasmix; + gaschange_after = (nextdp && (gasmix_distance(&gasmix, &newgasmix) || dp->setpoint != nextdp->setpoint)); + gaschange_before = (gasmix_distance(&lastprintgasmix, &gasmix) || lastprintsetpoint != dp->setpoint); + /* do we want to skip this leg as it is devoid of anything useful? */ + if (!dp->entered && + nextdp && + dp->depth != lastdepth && + nextdp->depth != dp->depth && + !gaschange_before && + !gaschange_after) + continue; + if (dp->time - lasttime < 10 && !(gaschange_after && dp->next && dp->depth != dp->next->depth)) + continue; + + len = strlen(buffer); + if (plan_verbatim) { + /* When displaying a verbatim plan, we output a waypoint for every gas change. + * Therefore, we do not need to test for difficult cases that mean we need to + * print a segment just so we don't miss a gas change. This makes the logic + * to determine whether or not to print a segment much simpler than with the + * non-verbatim plan. + */ + if (dp->depth != lastprintdepth) { + if (plan_display_transitions || dp->entered || !dp->next || (gaschange_after && dp->next && dp->depth != nextdp->depth)) { + if (dp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix), + (double) dp->setpoint / 1000.0); + + else + snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s<br>", temp); + } + newdepth = dp->depth; + lasttime = dp->time; + } else { + if ((nextdp && dp->depth != nextdp->depth) || gaschange_after) { + if (dp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix), + (double) dp->setpoint / 1000.0); + else + snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s<br>", temp); + newdepth = dp->depth; + lasttime = dp->time; + } + } + } else { + /* When not displaying the verbatim dive plan, we typically ignore ascents between deco stops, + * unless the display transitions option has been selected. We output a segment if any of the + * following conditions are met. + * 1) Display transitions is selected + * 2) The segment was manually entered + * 3) It is the last segment of the dive + * 4) The segment is not an ascent, there was a gas change at the start of the segment and the next segment + * is a change in depth (typical deco stop) + * 5) There is a gas change at the end of the segment and the last segment was entered (first calculated + * segment if it ends in a gas change) + * 6) There is a gaschange after but no ascent. This should only occur when backgas breaks option is selected + * 7) It is an ascent ending with a gas change, but is not followed by a stop. As case 5 already matches + * the first calculated ascent if it ends with a gas change, this should only occur if a travel gas is + * used for a calculated ascent, there is a subsequent gas change before the first deco stop, and zero + * time has been allowed for a gas switch. + */ + if (plan_display_transitions || dp->entered || !dp->next || + (nextdp && dp->depth != nextdp->depth) || + (!isascent && gaschange_before && nextdp && dp->depth != nextdp->depth) || + (gaschange_after && lastentered) || (gaschange_after && !isascent) || + (isascent && gaschange_after && nextdp && dp->depth != nextdp->depth )) { + // Print a symbol to indicate whether segment is an ascent, descent, constant depth (user entered) or deco stop + if (isascent) + segmentsymbol = "➚"; // up-right arrow for ascent + else if (dp->depth > lastdepth) + segmentsymbol = "➘"; // down-right arrow for descent + else if (dp->entered) + segmentsymbol = "➙"; // right arrow for entered entered segment at constant depth + else + segmentsymbol = "➖"; // heavey minus sign for deco stop + + len += snprintf(buffer + len, sz_buffer - len, "<tr><td style='padding-left: 10px; float: right;'>%s</td>", segmentsymbol); + + snprintf(temp, sz_temp, translate("gettextFromC", "%3.0f%s"), depthvalue, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", temp); + if (plan_display_duration) { + snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time - lasttime + 30) / 60); + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", temp); + } + if (plan_display_runtime) { + snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time + 30) / 60); + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", temp); + } + + /* Normally a gas change is displayed on the stopping segment, so only display a gas change at the end of + * an ascent segment if it is not followed by a stop + */ + if (isascent && gaschange_after && dp->next && nextdp && dp->depth != nextdp->depth) { + if (dp->setpoint) { + snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) nextdp->setpoint / 1000.0); + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s %s</b></td>", gasname(&newgasmix), + temp); + } else { + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s</b></td>", gasname(&newgasmix)); + } + lastprintsetpoint = nextdp->setpoint; + lastprintgasmix = newgasmix; + gaschange_after = false; + } else if (gaschange_before) { + // If a new gas has been used for this segment, now is the time to show it + if (dp->setpoint) { + snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) dp->setpoint / 1000.0); + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s %s</b></td>", gasname(&gasmix), + temp); + } else { + len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s</b></td>", gasname(&gasmix)); + } + // Set variables so subsequent iterations can test against the last gas printed + lastprintsetpoint = dp->setpoint; + lastprintgasmix = gasmix; + gaschange_after = false; + } else { + len += snprintf(buffer + len, sz_buffer - len, "<td> </td>"); + } + len += snprintf(buffer + len, sz_buffer - len, "</tr>"); + newdepth = dp->depth; + lasttime = dp->time; + } + } + if (gaschange_after) { + // gas switch at this waypoint + if (plan_verbatim) { + if (lastsetpoint >= 0) { + if (nextdp && nextdp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s (SP = %.1fbar)"), gasname(&newgasmix), (double) nextdp->setpoint / 1000.0); + else + snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s"), gasname(&newgasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s<br>", temp); + } + gaschange_after = false; + gasmix = newgasmix; + } + } + lastprintdepth = newdepth; + lastdepth = dp->depth; + lastsetpoint = dp->setpoint; + lastentered = dp->entered; + } while ((dp = nextdp) != NULL); + if (!plan_verbatim) + len += snprintf(buffer + len, sz_buffer - len, "</tbody></table></div>"); + + dive->cns = 0; + dive->maxcns = 0; + update_cylinder_related_info(dive); + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "CNS")); + len += snprintf(buffer + len, sz_buffer - len, "<div><br>%s: %i%%", temp, dive->cns); + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "OTU")); + len += snprintf(buffer + len, sz_buffer - len, "<br>%s: %i</div>", temp, dive->otu); + + if (dive->dc.divemode == CCR) + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption (CCR legs excluded):")); + else + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption:")); + len += snprintf(buffer + len, sz_buffer - len, "<div><br>%s<br>", temp); + for (int gasidx = 0; gasidx < MAX_CYLINDERS; gasidx++) { + double volume, pressure, deco_volume, deco_pressure; + const char *unit, *pressure_unit; + char warning[1000] = ""; + cylinder_t *cyl = &dive->cylinder[gasidx]; + if (cylinder_none(cyl)) + break; + + volume = get_volume_units(cyl->gas_used.mliter, NULL, &unit); + deco_volume = get_volume_units(cyl->deco_gas_used.mliter, NULL, &unit); + if (cyl->type.size.mliter) { + deco_pressure = get_pressure_units(1000.0 * cyl->deco_gas_used.mliter / cyl->type.size.mliter, &pressure_unit); + pressure = get_pressure_units(1000.0 * cyl->gas_used.mliter / cyl->type.size.mliter, &pressure_unit); + /* Warn if the plan uses more gas than is available in a cylinder + * This only works if we have working pressure for the cylinder + * 10bar is a made up number - but it seemed silly to pretend you could breathe cylinder down to 0 */ + if (cyl->end.mbar < 10000) + snprintf(warning, sizeof(warning), " — <span style='color: red;'>%s </span> %s", + translate("gettextFromC", "Warning:"), + translate("gettextFromC", "this is more gas than available in the specified cylinder!")); + else + if ((float) cyl->end.mbar * cyl->type.size.mliter / 1000.0 < (float) cyl->deco_gas_used.mliter) + snprintf(warning, sizeof(warning), " — <span style='color: red;'>%s </span> %s", + translate("gettextFromC", "Warning:"), + translate("gettextFromC", "not enough reserve for gas sharing on ascent!")); + + snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s/%.0f%s of %s (%.0f%s/%.0f%s in planned ascent)"), volume, unit, pressure, pressure_unit, gasname(&cyl->gasmix), deco_volume, unit, deco_pressure, pressure_unit); + } else { + snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s (%.0f%s during planned ascent) of %s"), volume, unit, deco_volume, unit, gasname(&cyl->gasmix)); + } + len += snprintf(buffer + len, sz_buffer - len, "%s%s<br>", temp, warning); + } + dp = diveplan->dp; + if (dive->dc.divemode != CCR) { + while (dp) { + if (dp->time != 0) { + struct gas_pressures pressures; + fill_pressures(&pressures, depth_to_atm(dp->depth, dive), &dp->gasmix, 0.0, dive->dc.divemode); + + if (pressures.o2 > (dp->entered ? prefs.bottompo2 : prefs.decopo2) / 1000.0) { + const char *depth_unit; + int decimals; + double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); + len = strlen(buffer); + snprintf(temp, sz_temp, + translate("gettextFromC", "high pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), + pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "<span style='color: red;'>%s </span> %s<br>", + translate("gettextFromC", "Warning:"), temp); + } else if (pressures.o2 < 0.16) { + const char *depth_unit; + int decimals; + double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); + len = strlen(buffer); + snprintf(temp, sz_temp, + translate("gettextFromC", "low pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), + pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "<span style='color: red;'>%s </span> %s<br>", + translate("gettextFromC", "Warning:"), temp); + + } + } + dp = dp->next; + } + } + snprintf(buffer + len, sz_buffer - len, "</div>"); + dive->notes = strdup(buffer); + + free((void *)buffer); + free((void *)temp); +} + +int ascent_velocity(int depth, int avg_depth, int bottom_time) +{ + (void) bottom_time; + /* We need to make this configurable */ + + /* As an example (and possibly reasonable default) this is the Tech 1 provedure according + * to http://www.globalunderwaterexplorers.org/files/Standards_and_Procedures/SOP_Manual_Ver2.0.2.pdf */ + + if (depth * 4 > avg_depth * 3) { + return prefs.ascrate75; + } else { + if (depth * 2 > avg_depth) { + return prefs.ascrate50; + } else { + if (depth > 6000) + return prefs.ascratestops; + else + return prefs.ascratelast6m; + } + } +} + +void track_ascent_gas(int depth, cylinder_t *cylinder, int avg_depth, int bottom_time, bool safety_stop) +{ + while (depth > 0) { + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (deltad > depth) + deltad = depth; + update_cylinder_pressure(&displayed_dive, depth, depth - deltad, TIMESTEP, prefs.decosac, cylinder, true); + if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { + update_cylinder_pressure(&displayed_dive, 5000, 5000, 180, prefs.decosac, cylinder, true); + safety_stop = false; + } + depth -= deltad; + } +} + +// Determine whether ascending to the next stop will break the ceiling. Return true if the ascent is ok, false if it isn't. +bool trial_ascent(int trial_depth, int stoplevel, int avg_depth, int bottom_time, struct gasmix *gasmix, int po2, double surface_pressure) +{ + + bool clear_to_ascend = true; + char *trial_cache = NULL; + + // For consistency with other VPM-B implementations, we should not start the ascent while the ceiling is + // deeper than the next stop (thus the offgasing during the ascent is ignored). + // However, we still need to make sure we don't break the ceiling due to on-gassing during ascent. + if (prefs.deco_mode == VPMB && (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, + depth_to_bar(stoplevel, &displayed_dive)), + surface_pressure, &displayed_dive, 1) > stoplevel)) + return false; + + cache_deco_state(&trial_cache); + while (trial_depth > stoplevel) { + int deltad = ascent_velocity(trial_depth, avg_depth, bottom_time) * TIMESTEP; + if (deltad > trial_depth) /* don't test against depth above surface */ + deltad = trial_depth; + add_segment(depth_to_bar(trial_depth, &displayed_dive), + gasmix, + TIMESTEP, po2, &displayed_dive, prefs.decosac); + if (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, depth_to_bar(trial_depth, &displayed_dive)), + surface_pressure, &displayed_dive, 1) > trial_depth - deltad) { + /* We should have stopped */ + clear_to_ascend = false; + break; + } + trial_depth -= deltad; + } + restore_deco_state(trial_cache); + free(trial_cache); + return clear_to_ascend; +} + +/* Determine if there is enough gas for the dive. Return true if there is enough. + * Also return true if this cannot be calculated because the cylinder doesn't have + * size or a starting pressure. + */ +bool enough_gas(int current_cylinder) +{ + cylinder_t *cyl; + cyl = &displayed_dive.cylinder[current_cylinder]; + + if (!cyl->start.mbar) + return true; + if (cyl->type.size.mliter) + return (float) (cyl->end.mbar - prefs.reserve_gas) * cyl->type.size.mliter / 1000.0 > (float) cyl->deco_gas_used.mliter; + else + return true; +} + +// Work out the stops. Return value is if there were any mandatory stops. + +bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer) +{ + int bottom_depth; + int bottom_gi; + int bottom_stopidx; + bool is_final_plan = true; + int deco_time; + int previous_deco_time; + char *bottom_cache = NULL; + struct sample *sample; + int po2; + int transitiontime, gi; + int current_cylinder; + int stopidx; + int depth; + struct gaschanges *gaschanges = NULL; + int gaschangenr; + int *decostoplevels; + int decostoplevelcount; + int *stoplevels = NULL; + bool stopping = false; + bool pendinggaschange = false; + int clock, previous_point_time; + int avg_depth, max_depth, bottom_time = 0; + int last_ascend_rate; + int best_first_ascend_cylinder; + struct gasmix gas, bottom_gas; + int o2time = 0; + int breaktime = -1; + int breakcylinder = 0; + int error = 0; + bool decodive = false; + + set_gf(diveplan->gflow, diveplan->gfhigh, prefs.gf_low_at_maxdepth); + if (!diveplan->surface_pressure) + diveplan->surface_pressure = SURFACE_PRESSURE; + displayed_dive.surface_pressure.mbar = diveplan->surface_pressure; + clear_deco(displayed_dive.surface_pressure.mbar / 1000.0); + max_bottom_ceiling_pressure.mbar = first_ceiling_pressure.mbar = 0; + create_dive_from_plan(diveplan, is_planner); + + // Do we want deco stop array in metres or feet? + if (prefs.units.length == METERS ) { + decostoplevels = decostoplevels_metric; + decostoplevelcount = sizeof(decostoplevels_metric) / sizeof(int); + } else { + decostoplevels = decostoplevels_imperial; + decostoplevelcount = sizeof(decostoplevels_imperial) / sizeof(int); + } + + /* If the user has selected last stop to be at 6m/20', we need to get rid of the 3m/10' stop. + * Otherwise reinstate the last stop 3m/10' stop. + */ + if (prefs.last_stop) + *(decostoplevels + 1) = 0; + else + *(decostoplevels + 1) = M_OR_FT(3,10); + + /* Let's start at the last 'sample', i.e. the last manually entered waypoint. */ + sample = &displayed_dive.dc.sample[displayed_dive.dc.samples - 1]; + + get_gas_at_time(&displayed_dive, &displayed_dive.dc, sample->time, &gas); + + po2 = sample->setpoint.mbar; + if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { + report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); + current_cylinder = 0; + } + depth = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].depth.mm; + average_max_depth(diveplan, &avg_depth, &max_depth); + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + + /* if all we wanted was the dive just get us back to the surface */ + if (!is_planner) { + transitiontime = depth / 75; /* this still needs to be made configurable */ + plan_add_segment(diveplan, transitiontime, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + return(false); + } + +#if DEBUG_PLAN & 4 + printf("gas %s\n", gasname(&gas)); + printf("depth %5.2lfm \n", depth / 1000.0); +#endif + + best_first_ascend_cylinder = current_cylinder; + /* Find the gases available for deco */ + + if (po2) { // Don't change gas in CCR mode + gaschanges = NULL; + gaschangenr = 0; + } else { + gaschanges = analyze_gaslist(diveplan, &gaschangenr, depth, &best_first_ascend_cylinder); + } + /* Find the first potential decostopdepth above current depth */ + for (stopidx = 0; stopidx < decostoplevelcount; stopidx++) + if (*(decostoplevels + stopidx) >= depth) + break; + if (stopidx > 0) + stopidx--; + /* Stoplevels are either depths of gas changes or potential deco stop depths. */ + stoplevels = sort_stops(decostoplevels, stopidx + 1, gaschanges, gaschangenr); + stopidx += gaschangenr; + + /* Keep time during the ascend */ + bottom_time = clock = previous_point_time = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].time.seconds; + gi = gaschangenr - 1; + + /* Set tissue tolerance and initial vpmb gradient at start of ascent phase */ + tissue_at_end(&displayed_dive, cached_datap); + nuclear_regeneration(clock); + vpmb_start_gradient(); + + if(prefs.deco_mode == RECREATIONAL) { + bool safety_stop = prefs.safetystop && max_depth >= 10000; + track_ascent_gas(depth, &displayed_dive.cylinder[current_cylinder], avg_depth, bottom_time, safety_stop); + // How long can we stay at the current depth and still directly ascent to the surface? + do { + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + DECOTIMESTEP, po2, &displayed_dive, prefs.bottomsac); + update_cylinder_pressure(&displayed_dive, depth, depth, DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); + clock += DECOTIMESTEP; + } while (trial_ascent(depth, 0, avg_depth, bottom_time, &displayed_dive.cylinder[current_cylinder].gasmix, + po2, diveplan->surface_pressure / 1000.0) && + enough_gas(current_cylinder)); + + // We did stay one DECOTIMESTEP too many. + // In the best of all worlds, we would roll back also the last add_segment in terms of caching deco state, but + // let's ignore that since for the eventual ascent in recreational mode, nobody looks at the ceiling anymore, + // so we don't really have to compute the deco state. + update_cylinder_pressure(&displayed_dive, depth, depth, -DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); + clock -= DECOTIMESTEP; + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, true); + previous_point_time = clock; + do { + /* Ascend to surface */ + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + } + if (depth - deltad < 0) + deltad = depth; + + clock += TIMESTEP; + depth -= deltad; + if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { + plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); + previous_point_time = clock; + clock += 180; + plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); + previous_point_time = clock; + safety_stop = false; + } + } while (depth > 0); + plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); + fixup_dc_duration(&displayed_dive.dc); + + free(stoplevels); + free(gaschanges); + return(false); + } + + if (best_first_ascend_cylinder != current_cylinder) { + current_cylinder = best_first_ascend_cylinder; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", best_first_ascend_cylinder, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[best_first_ascend_cylinder].depth / 1000.0); +#endif + } + + // VPM-B or Buehlmann Deco + tissue_at_end(&displayed_dive, cached_datap); + previous_deco_time = 100000000; + deco_time = 10000000; + cache_deco_state(&bottom_cache); // Lets us make several iterations + bottom_depth = depth; + bottom_gi = gi; + bottom_gas = gas; + bottom_stopidx = stopidx; + + //CVA + do { + is_final_plan = (prefs.deco_mode == BUEHLMANN) || (previous_deco_time - deco_time < 10); // CVA time converges + if (deco_time != 10000000) + vpmb_next_gradient(deco_time, diveplan->surface_pressure / 1000.0); + + previous_deco_time = deco_time; + restore_deco_state(bottom_cache); + + depth = bottom_depth; + gi = bottom_gi; + clock = previous_point_time = bottom_time; + gas = bottom_gas; + stopping = false; + decodive = false; + stopidx = bottom_stopidx; + breaktime = -1; + breakcylinder = 0; + o2time = 0; + first_ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, + depth_to_bar(depth, &displayed_dive)), + diveplan->surface_pressure / 1000.0, + &displayed_dive, + 1), + &displayed_dive); + if (max_bottom_ceiling_pressure.mbar > first_ceiling_pressure.mbar) + first_ceiling_pressure.mbar = max_bottom_ceiling_pressure.mbar; + + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { + report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); + current_cylinder = 0; + } + + while (1) { + /* We will break out when we hit the surface */ + do { + /* Ascend to next stop depth */ + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = false; + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + } + if (depth - deltad < stoplevels[stopidx]) + deltad = depth - stoplevels[stopidx]; + + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + TIMESTEP, po2, &displayed_dive, prefs.decosac); + clock += TIMESTEP; + depth -= deltad; + } while (depth > 0 && depth > stoplevels[stopidx]); + + if (depth <= 0) + break; /* We are at the surface */ + + if (gi >= 0 && stoplevels[stopidx] <= gaschanges[gi].depth) { + /* We have reached a gas change. + * Record this in the dive plan */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = true; + + /* Check we need to change cylinder. + * We might not if the cylinder was chosen by the user + * or user has selected only to switch only at required stops. + * If current gas is hypoxic, we want to switch asap */ + + if (current_cylinder != gaschanges[gi].gasidx) { + if (!prefs.switch_at_req_stop || + !trial_ascent(depth, stoplevels[stopidx - 1], avg_depth, bottom_time, + &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0) || get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) < 160) { + current_cylinder = gaschanges[gi].gasidx; + gas = displayed_dive.cylinder[current_cylinder].gasmix; +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi].gasidx, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi].depth / 1000.0); +#endif + /* Stop for the minimum duration to switch gas */ + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); + clock += prefs.min_switch_duration; + if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) + o2time += prefs.min_switch_duration; + } else { + /* The user has selected the option to switch gas only at required stops. + * Remember that we are waiting to switch gas + */ + pendinggaschange = true; + } + } + gi--; + } + --stopidx; + + /* Save the current state and try to ascend to the next stopdepth */ + while (1) { + /* Check if ascending to next stop is clear, go back and wait if we hit the ceiling on the way */ + if (trial_ascent(depth, stoplevels[stopidx], avg_depth, bottom_time, + &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0)) + break; /* We did not hit the ceiling */ + + /* Add a minute of deco time and then try again */ + decodive = true; + if (!stopping) { + /* The last segment was an ascend segment. + * Add a waypoint for start of this deco stop */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = true; + } + + /* Are we waiting to switch gas? + * Occurs when the user has selected the option to switch only at required stops + */ + if (pendinggaschange) { + current_cylinder = gaschanges[gi + 1].gasidx; + gas = displayed_dive.cylinder[current_cylinder].gasmix; +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi + 1].gasidx, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi + 1].depth / 1000.0); +#endif + /* Stop for the minimum duration to switch gas */ + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); + clock += prefs.min_switch_duration; + if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) + o2time += prefs.min_switch_duration; + pendinggaschange = false; + } + + /* Deco stop should end when runtime is at a whole minute */ + int this_decotimestep; + this_decotimestep = DECOTIMESTEP - clock % DECOTIMESTEP; + + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + this_decotimestep, po2, &displayed_dive, prefs.decosac); + clock += this_decotimestep; + /* Finish infinite deco */ + if(clock >= 48 * 3600 && depth >= 6000) { + error = LONGDECO; + break; + } + if (prefs.doo2breaks) { + /* The backgas breaks option limits time on oxygen to 12 minutes, followed by 6 minutes on + * backgas (first defined gas). This could be customized if there were demand. + */ + if (get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) { + o2time += DECOTIMESTEP; + if (o2time >= 12 * 60) { + breaktime = 0; + breakcylinder = current_cylinder; + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + current_cylinder = 0; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + } + } else { + if (breaktime >= 0) { + breaktime += DECOTIMESTEP; + if (breaktime >= 6 * 60) { + o2time = 0; + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + current_cylinder = breakcylinder; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + breaktime = -1; + } + } + } + } + } + if (stopping) { + /* Next we will ascend again. Add a waypoint if we have spend deco time */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = false; + } + } + + deco_time = clock - bottom_time; + } while (!is_final_plan); + + plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); + fixup_dc_duration(&displayed_dive.dc); + + free(stoplevels); + free(gaschanges); + free(bottom_cache); + return decodive; +} + +/* + * Get a value in tenths (so "10.2" == 102, "9" = 90) + * + * Return negative for errors. + */ +static int get_tenths(const char *begin, const char **endp) +{ + char *end; + int value = strtol(begin, &end, 10); + + if (begin == end) + return -1; + value *= 10; + + /* Fraction? We only look at the first digit */ + if (*end == '.') { + end++; + if (!isdigit(*end)) + return -1; + value += *end - '0'; + do { + end++; + } while (isdigit(*end)); + } + *endp = end; + return value; +} + +static int get_permille(const char *begin, const char **end) +{ + int value = get_tenths(begin, end); + if (value >= 0) { + /* Allow a percentage sign */ + if (**end == '%') + ++*end; + } + return value; +} + +int validate_gas(const char *text, struct gasmix *gas) +{ + int o2, he; + + if (!text) + return 0; + + while (isspace(*text)) + text++; + + if (!*text) + return 0; + + if (!strcasecmp(text, translate("gettextFromC", "air"))) { + o2 = O2_IN_AIR; + he = 0; + text += strlen(translate("gettextFromC", "air")); + } else if (!strcasecmp(text, translate("gettextFromC", "oxygen"))) { + o2 = 1000; + he = 0; + text += strlen(translate("gettextFromC", "oxygen")); + } else if (!strncasecmp(text, translate("gettextFromC", "ean"), 3)) { + o2 = get_permille(text + 3, &text); + he = 0; + } else { + o2 = get_permille(text, &text); + he = 0; + if (*text == '/') + he = get_permille(text + 1, &text); + } + + /* We don't want any extra crud */ + while (isspace(*text)) + text++; + if (*text) + return 0; + + /* Validate the gas mix */ + if (*text || o2 < 1 || o2 > 1000 || he < 0 || o2 + he > 1000) + return 0; + + /* Let it rip */ + gas->o2.permille = o2; + gas->he.permille = he; + return 1; +} + +int validate_po2(const char *text, int *mbar_po2) +{ + int po2; + + if (!text) + return 0; + + po2 = get_tenths(text, &text); + if (po2 < 0) + return 0; + + while (isspace(*text)) + text++; + + while (isspace(*text)) + text++; + if (*text) + return 0; + + *mbar_po2 = po2 * 100; + return 1; +} diff --git a/core/planner.h b/core/planner.h new file mode 100644 index 000000000..a675989e0 --- /dev/null +++ b/core/planner.h @@ -0,0 +1,32 @@ +#ifndef PLANNER_H +#define PLANNER_H + +#define LONGDECO 1 +#define NOT_RECREATIONAL 2 + +#ifdef __cplusplus +extern "C" { +#endif + +extern int validate_gas(const char *text, struct gasmix *gas); +extern int validate_po2(const char *text, int *mbar_po2); +extern timestamp_t current_time_notz(void); +extern void set_last_stop(bool last_stop_6m); +extern void set_verbatim(bool verbatim); +extern void set_display_runtime(bool display); +extern void set_display_duration(bool display); +extern void set_display_transitions(bool display); +extern void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas); +extern int get_gasidx(struct dive *dive, struct gasmix *mix); +extern bool diveplan_empty(struct diveplan *diveplan); + +extern void free_dps(struct diveplan *diveplan); +extern struct dive *planned_dive; +extern char *cache_data; +extern const char *disclaimer; +extern double plangflow, plangfhigh; + +#ifdef __cplusplus +} +#endif +#endif // PLANNER_H diff --git a/core/pluginmanager.cpp b/core/pluginmanager.cpp new file mode 100644 index 000000000..28c978280 --- /dev/null +++ b/core/pluginmanager.cpp @@ -0,0 +1,53 @@ +#include "pluginmanager.h" + +#include <QApplication> +#include <QDir> +#include <QPluginLoader> +#include <QDebug> + +static QList<ISocialNetworkIntegration*> _socialNetworks; + +PluginManager& PluginManager::instance() +{ + static PluginManager self; + return self; +} + +PluginManager::PluginManager() +{ +} + +void PluginManager::loadPlugins() +{ + QDir pluginsDir(qApp->applicationDirPath()); + +#if defined(Q_OS_WIN) + if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release") + pluginsDir.cdUp(); +#elif defined(Q_OS_MAC) + if (pluginsDir.dirName() == "MacOS") { + pluginsDir.cdUp(); + pluginsDir.cdUp(); + pluginsDir.cdUp(); + } +#endif + pluginsDir.cd("plugins"); + + qDebug() << "Plugins Directory: " << pluginsDir; + foreach (const QString& fileName, pluginsDir.entryList(QDir::Files)) { + QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = loader.instance(); + if(!plugin) + continue; + + if (ISocialNetworkIntegration *social = qobject_cast<ISocialNetworkIntegration*>(plugin)) { + qDebug() << "Adding the plugin: " << social->socialNetworkName(); + _socialNetworks.push_back(social); + } + } +} + +QList<ISocialNetworkIntegration*> PluginManager::socialNetworkIntegrationPlugins() const +{ + return _socialNetworks; +} diff --git a/core/pluginmanager.h b/core/pluginmanager.h new file mode 100644 index 000000000..3f43b5db1 --- /dev/null +++ b/core/pluginmanager.h @@ -0,0 +1,19 @@ +#ifndef PLUGINMANAGER_H +#define PLUGINMANAGER_H + +#include <QObject> + +#include "isocialnetworkintegration.h" + +class PluginManager { +public: + static PluginManager& instance(); + void loadPlugins(); + QList<ISocialNetworkIntegration*> socialNetworkIntegrationPlugins() const; +private: + PluginManager(); + PluginManager(const PluginManager&); + PluginManager& operator=(const PluginManager&); +}; + +#endif diff --git a/core/pref.h b/core/pref.h new file mode 100644 index 000000000..be684fd90 --- /dev/null +++ b/core/pref.h @@ -0,0 +1,172 @@ +#ifndef PREF_H +#define PREF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "units.h" +#include "taxonomy.h" + +/* can't use 'bool' for the boolean values - different size in C and C++ */ +typedef struct +{ + short po2; + short pn2; + short phe; + double po2_threshold; + double pn2_threshold; + double phe_threshold; +} partial_pressure_graphs_t; + +typedef struct { + char *access_token; + char *user_id; + char *album_id; +} facebook_prefs_t; + +typedef struct { + bool enable_geocoding; + bool parse_dive_without_gps; + bool tag_existing_dives; + enum taxonomy_category category[3]; +} geocoding_prefs_t; + +typedef struct { + const char *language; + bool use_system_language; +} locale_prefs_t; + +enum deco_mode { + BUEHLMANN, + RECREATIONAL, + VPMB +}; + +struct preferences { + const char *divelist_font; + const char *default_filename; + const char *default_cylinder; + const char *cloud_base_url; + const char *cloud_git_url; + const char *time_format; + const char *date_format; + const char *date_format_short; + bool time_format_override; + bool date_format_override; + double font_size; + partial_pressure_graphs_t pp_graphs; + short mod; + double modpO2; + short ead; + short dcceiling; + short redceiling; + short calcceiling; + short calcceiling3m; + short calcalltissues; + short calcndltts; + short gflow; + short gfhigh; + int animation_speed; + bool gf_low_at_maxdepth; + bool show_ccr_setpoint; + bool show_ccr_sensors; + short display_invalid_dives; + short unit_system; + struct units units; + bool coordinates_traditional; + short show_sac; + short display_unused_tanks; + short show_average_depth; + short zoomed_plot; + short hrgraph; + short percentagegraph; + short rulergraph; + short tankbar; + short save_userid_local; + char *userid; + int ascrate75; // All rates in mm / sec + int ascrate50; + int ascratestops; + int ascratelast6m; + int descrate; + int bottompo2; + int decopo2; + int proxy_type; + char *proxy_host; + int proxy_port; + short proxy_auth; + char *proxy_user; + char *proxy_pass; + bool doo2breaks; + bool drop_stone_mode; + bool last_stop; // At 6m? + bool verbatim_plan; + bool display_runtime; + bool display_duration; + bool display_transitions; + bool safetystop; + bool switch_at_req_stop; + int reserve_gas; + int min_switch_duration; // seconds + int bottomsac; + int decosac; + int o2consumption; // ml per min + int pscr_ratio; // dump ratio times 1000 + int defaultsetpoint; // default setpoint in mbar + bool show_pictures_in_profile; + bool use_default_file; + short default_file_behavior; + facebook_prefs_t facebook; + char *cloud_storage_password; + char *cloud_storage_newpassword; + char *cloud_storage_email; + char *cloud_storage_email_encoded; + bool save_password_local; + short cloud_verification_status; + bool cloud_background_sync; + geocoding_prefs_t geocoding; + enum deco_mode deco_mode; + short conservatism_level; + int time_threshold; + int distance_threshold; + bool git_local_only; + locale_prefs_t locale; //: TODO: move the rest of locale based info here. +}; +enum unit_system_values { + METRIC, + IMPERIAL, + PERSONALIZE +}; + +enum def_file_behavior { + UNDEFINED_DEFAULT_FILE, + LOCAL_DEFAULT_FILE, + NO_DEFAULT_FILE, + CLOUD_DEFAULT_FILE +}; + +enum cloud_status { + CS_UNKNOWN, + CS_INCORRECT_USER_PASSWD, + CS_NEED_TO_VERIFY, + CS_VERIFIED +}; + +extern struct preferences prefs, default_prefs, informational_prefs; + +#define PP_GRAPHS_ENABLED (prefs.pp_graphs.po2 || prefs.pp_graphs.pn2 || prefs.pp_graphs.phe) + +extern const char *system_divelist_default_font; +extern double system_divelist_default_font_size; + +extern const char *system_default_directory(void); +extern const char *system_default_filename(); +extern bool subsurface_ignore_font(const char *font); +extern void subsurface_OS_pref_setup(); + +#ifdef __cplusplus +} +#endif + +#endif // PREF_H diff --git a/core/prefs-macros.h b/core/prefs-macros.h new file mode 100644 index 000000000..fe459d3da --- /dev/null +++ b/core/prefs-macros.h @@ -0,0 +1,68 @@ +#ifndef PREFSMACROS_H +#define PREFSMACROS_H + +#define SB(V, B) s.setValue(V, (int)(B->isChecked() ? 1 : 0)) + +#define GET_UNIT(name, field, f, t) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.units.field = (v.toInt() == (t)) ? (t) : (f); \ + else \ + prefs.units.field = default_prefs.units.field + +#define GET_BOOL(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toBool(); \ + else \ + prefs.field = default_prefs.field + +#define GET_DOUBLE(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toDouble(); \ + else \ + prefs.field = default_prefs.field + +#define GET_INT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toInt(); \ + else \ + prefs.field = default_prefs.field + +#define GET_ENUM(name, type, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = (enum type)v.toInt(); \ + else \ + prefs.field = default_prefs.field + +#define GET_INT_DEF(name, field, defval) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toInt(); \ + else \ + prefs.field = defval + +#define GET_TXT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = strdup(v.toString().toUtf8().constData()); \ + else \ + prefs.field = default_prefs.field + +#define SAVE_OR_REMOVE_SPECIAL(_setting, _default, _compare, _value) \ + if (_compare != _default) \ + s.setValue(_setting, _value); \ + else \ + s.remove(_setting) + +#define SAVE_OR_REMOVE(_setting, _default, _value) \ + if (_value != _default) \ + s.setValue(_setting, _value); \ + else \ + s.remove(_setting) + +#endif // PREFSMACROS_H + diff --git a/core/profile.c b/core/profile.c new file mode 100644 index 000000000..6576f6453 --- /dev/null +++ b/core/profile.c @@ -0,0 +1,1544 @@ +/* profile.c */ +/* creates all the necessary data for drawing the dive profile + */ +#include "gettext.h" +#include <limits.h> +#include <string.h> +#include <assert.h> + +#include "dive.h" +#include "display.h" +#include "divelist.h" + +#include "profile.h" +#include "gaspressures.h" +#include "deco.h" +#include "libdivecomputer/parser.h" +#include "libdivecomputer/version.h" +#include "membuffer.h" + +//#define DEBUG_GAS 1 + +#define MAX_PROFILE_DECO 7200 + + +int selected_dive = -1; /* careful: 0 is a valid value */ +unsigned int dc_number = 0; + +static struct plot_data *last_pi_entry_new = NULL; +void populate_pressure_information(struct dive *, struct divecomputer *, struct plot_info *, int); + +extern bool in_planner(); +extern pressure_t first_ceiling_pressure; + +#ifdef DEBUG_PI +/* debugging tool - not normally used */ +static void dump_pi(struct plot_info *pi) +{ + int i; + + printf("pi:{nr:%d maxtime:%d meandepth:%d maxdepth:%d \n" + " maxpressure:%d mintemp:%d maxtemp:%d\n", + pi->nr, pi->maxtime, pi->meandepth, pi->maxdepth, + pi->maxpressure, pi->mintemp, pi->maxtemp); + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = &pi->entry[i]; + printf(" entry[%d]:{cylinderindex:%d sec:%d pressure:{%d,%d}\n" + " time:%d:%02d temperature:%d depth:%d stopdepth:%d stoptime:%d ndl:%d smoothed:%d po2:%lf phe:%lf pn2:%lf sum-pp %lf}\n", + i, entry->cylinderindex, entry->sec, + entry->pressure[0], entry->pressure[1], + entry->sec / 60, entry->sec % 60, + entry->temperature, entry->depth, entry->stopdepth, entry->stoptime, entry->ndl, entry->smoothed, + entry->pressures.o2, entry->pressures.he, entry->pressures.n2, + entry->pressures.o2 + entry->pressures.he + entry->pressures.n2); + } + printf(" }\n"); +} +#endif + +#define ROUND_UP(x, y) ((((x) + (y) - 1) / (y)) * (y)) +#define DIV_UP(x, y) (((x) + (y) - 1) / (y)) + +/* + * When showing dive profiles, we scale things to the + * current dive. However, we don't scale past less than + * 30 minutes or 90 ft, just so that small dives show + * up as such unless zoom is enabled. + * We also need to add 180 seconds at the end so the min/max + * plots correctly + */ +int get_maxtime(struct plot_info *pi) +{ + int seconds = pi->maxtime; + + int DURATION_THR = (pi->dive_type == FREEDIVING ? 60 : 600); + int CEILING = (pi->dive_type == FREEDIVING ? 30 : 60); + + if (prefs.zoomed_plot) { + /* Rounded up to one minute, with at least 2.5 minutes to + * spare. + * For dive times shorter than 10 minutes, we use seconds/4 to + * calculate the space dynamically. + * This is seamless since 600/4 = 150. + */ + if (seconds < DURATION_THR) + return ROUND_UP(seconds + seconds / 4, CEILING); + else + return ROUND_UP(seconds + DURATION_THR/4, CEILING); + } else { +#ifndef SUBSURFACE_MOBILE + /* min 30 minutes, rounded up to 5 minutes, with at least 2.5 minutes to spare */ + return MAX(30 * 60, ROUND_UP(seconds + DURATION_THR/4, CEILING * 5)); +#else + /* just add 2.5 minutes so we have a consistant right margin */ + return seconds + DURATION_THR / 4; +#endif + } +} + +/* get the maximum depth to which we want to plot + * take into account the additional vertical space needed to plot + * partial pressure graphs */ +int get_maxdepth(struct plot_info *pi) +{ + unsigned mm = pi->maxdepth; + int md; + + if (prefs.zoomed_plot) { + /* Rounded up to 10m, with at least 3m to spare */ + md = ROUND_UP(mm + 3000, 10000); + } else { + /* Minimum 30m, rounded up to 10m, with at least 3m to spare */ + md = MAX((unsigned)30000, ROUND_UP(mm + 3000, 10000)); + } + md += pi->maxpp * 9000; + return md; +} + +/* collect all event names and whether we display them */ +struct ev_select *ev_namelist; +int evn_allocated; +int evn_used; + +#if WE_DONT_USE_THIS /* we need to implement event filters in Qt */ +int evn_foreach (void (*callback)(const char *, bool *, void *), void *data) { + int i; + + for (i = 0; i < evn_used; i++) { + /* here we display an event name on screen - so translate */ + callback(translate("gettextFromC", ev_namelist[i].ev_name), &ev_namelist[i].plot_ev, data); + } + return i; +} +#endif /* WE_DONT_USE_THIS */ + +void clear_events(void) +{ + for (int i = 0; i < evn_used; i++) + free(ev_namelist[i].ev_name); + evn_used = 0; +} + +void remember_event(const char *eventname) +{ + int i = 0, len; + + if (!eventname || (len = strlen(eventname)) == 0) + return; + while (i < evn_used) { + if (!strncmp(eventname, ev_namelist[i].ev_name, len)) + return; + i++; + } + if (evn_used == evn_allocated) { + evn_allocated += 10; + ev_namelist = realloc(ev_namelist, evn_allocated * sizeof(struct ev_select)); + if (!ev_namelist) + /* we are screwed, but let's just bail out */ + return; + } + ev_namelist[evn_used].ev_name = strdup(eventname); + ev_namelist[evn_used].plot_ev = true; + evn_used++; +} + +/* UNUSED! */ +static int get_local_sac(struct plot_data *entry1, struct plot_data *entry2, struct dive *dive) __attribute__((unused)); + +/* Get local sac-rate (in ml/min) between entry1 and entry2 */ +static int get_local_sac(struct plot_data *entry1, struct plot_data *entry2, struct dive *dive) +{ + int index = entry1->cylinderindex; + cylinder_t *cyl; + int duration = entry2->sec - entry1->sec; + int depth, airuse; + pressure_t a, b; + double atm; + + if (entry2->cylinderindex != index) + return 0; + if (duration <= 0) + return 0; + a.mbar = GET_PRESSURE(entry1); + b.mbar = GET_PRESSURE(entry2); + if (!b.mbar || a.mbar <= b.mbar) + return 0; + + /* Mean pressure in ATM */ + depth = (entry1->depth + entry2->depth) / 2; + atm = depth_to_atm(depth, dive); + + cyl = dive->cylinder + index; + + airuse = gas_volume(cyl, a) - gas_volume(cyl, b); + + /* milliliters per minute */ + return airuse / atm * 60 / duration; +} + +static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index) +{ + struct plot_data *p = entry; + int time = entry->sec; + int seconds = 90 * (index + 1); + struct plot_data *min, *max; + int avg, nr; + + /* Go back 'seconds' in time */ + while (p > first) { + if (p[-1].sec < time - seconds) + break; + p--; + } + + /* Then go forward until we hit an entry past the time */ + min = max = p; + avg = p->depth; + nr = 1; + while (++p < last) { + int depth = p->depth; + if (p->sec > time + seconds) + break; + avg += depth; + nr++; + if (depth < min->depth) + min = p; + if (depth > max->depth) + max = p; + } + entry->min[index] = min; + entry->max[index] = max; + entry->avg[index] = (avg + nr / 2) / nr; +} + +static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last) +{ + analyze_plot_info_minmax_minute(entry, first, last, 0); + analyze_plot_info_minmax_minute(entry, first, last, 1); + analyze_plot_info_minmax_minute(entry, first, last, 2); +} + +static velocity_t velocity(int speed) +{ + velocity_t v; + + if (speed < -304) /* ascent faster than -60ft/min */ + v = CRAZY; + else if (speed < -152) /* above -30ft/min */ + v = FAST; + else if (speed < -76) /* -15ft/min */ + v = MODERATE; + else if (speed < -25) /* -5ft/min */ + v = SLOW; + else if (speed < 25) /* very hard to find data, but it appears that the recommendations + for descent are usually about 2x ascent rate; still, we want + stable to mean stable */ + v = STABLE; + else if (speed < 152) /* between 5 and 30ft/min is considered slow */ + v = SLOW; + else if (speed < 304) /* up to 60ft/min is moderate */ + v = MODERATE; + else if (speed < 507) /* up to 100ft/min is fast */ + v = FAST; + else /* more than that is just crazy - you'll blow your ears out */ + v = CRAZY; + + return v; +} + +struct plot_info *analyze_plot_info(struct plot_info *pi) +{ + int i; + int nr = pi->nr; + + /* Smoothing function: 5-point triangular smooth */ + for (i = 2; i < nr; i++) { + struct plot_data *entry = pi->entry + i; + int depth; + + if (i < nr - 2) { + depth = entry[-2].depth + 2 * entry[-1].depth + 3 * entry[0].depth + 2 * entry[1].depth + entry[2].depth; + entry->smoothed = (depth + 4) / 9; + } + /* vertical velocity in mm/sec */ + /* Linus wants to smooth this - let's at least look at the samples that aren't FAST or CRAZY */ + if (entry[0].sec - entry[-1].sec) { + entry->speed = (entry[0].depth - entry[-1].depth) / (entry[0].sec - entry[-1].sec); + entry->velocity = velocity(entry->speed); + /* if our samples are short and we aren't too FAST*/ + if (entry[0].sec - entry[-1].sec < 15 && entry->velocity < FAST) { + int past = -2; + while (i + past > 0 && entry[0].sec - entry[past].sec < 15) + past--; + entry->velocity = velocity((entry[0].depth - entry[past].depth) / + (entry[0].sec - entry[past].sec)); + } + } else { + entry->velocity = STABLE; + entry->speed = 0; + } + } + + /* One-, two- and three-minute minmax data */ + for (i = 0; i < nr; i++) { + struct plot_data *entry = pi->entry + i; + analyze_plot_info_minmax(entry, pi->entry, pi->entry + nr); + } + + return pi; +} + +/* + * If the event has an explicit cylinder index, + * we return that. If it doesn't, we return the best + * match based on the gasmix. + * + * Some dive computers give cylinder indexes, some + * give just the gas mix. + */ +int get_cylinder_index(struct dive *dive, struct event *ev) +{ + int i; + int best = 0, score = INT_MAX; + int target_o2, target_he; + struct gasmix *g; + + if (ev->gas.index >= 0) + return ev->gas.index; + + g = get_gasmix_from_event(ev); + target_o2 = get_o2(g); + target_he = get_he(g); + + /* + * Try to find a cylinder that best matches the target gas + * mix. + */ + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int delta_o2, delta_he, distance; + + if (cylinder_nodata(cyl)) + continue; + + delta_o2 = get_o2(&cyl->gasmix) - target_o2; + delta_he = get_he(&cyl->gasmix) - target_he; + distance = delta_o2 * delta_o2; + distance += delta_he * delta_he; + + if (distance >= score) + continue; + score = distance; + best = i; + } + return best; +} + +struct event *get_next_event(struct event *event, const char *name) +{ + if (!name || !*name) + return NULL; + while (event) { + if (!strcmp(event->name, name)) + return event; + event = event->next; + } + return event; +} + +static int count_events(struct divecomputer *dc) +{ + int result = 0; + struct event *ev = dc->events; + while (ev != NULL) { + result++; + ev = ev->next; + } + return result; +} + +static int set_cylinder_index(struct plot_info *pi, int i, int cylinderindex, int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + if (entry->cylinderindex != cylinderindex) { + entry->cylinderindex = cylinderindex; + entry->pressure[0] = 0; + } + i++; + } + return i; +} + +static int set_setpoint(struct plot_info *pi, int i, int setpoint, int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + entry->o2pressure.mbar = setpoint; + i++; + } + return i; +} + +/* normally the first cylinder has index 0... if not, we need to fix this up here */ +static int set_first_cylinder_index(struct plot_info *pi, int i, int cylinderindex, int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + entry->cylinderindex = cylinderindex; + i++; + } + return i; +} + +static void check_gas_change_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i = 0, cylinderindex = 0; + struct event *ev = get_next_event(dc->events, "gaschange"); + + // for dive computers that tell us their first gas as an event on the first sample + // we need to make sure things are setup correctly + cylinderindex = explicit_first_cylinder(dive, dc); + set_first_cylinder_index(pi, 0, cylinderindex, INT_MAX); + + if (!ev) + return; + + do { + i = set_cylinder_index(pi, i, cylinderindex, ev->time.seconds); + cylinderindex = get_cylinder_index(dive, ev); + ev = get_next_event(ev->next, "gaschange"); + } while (ev); + set_cylinder_index(pi, i, cylinderindex, INT_MAX); +} + +static void check_setpoint_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i = 0; + pressure_t setpoint; + (void) dive; + setpoint.mbar = 0; + struct event *ev = get_next_event(dc->events, "SP change"); + + if (!ev) + return; + + do { + i = set_setpoint(pi, i, setpoint.mbar, ev->time.seconds); + setpoint.mbar = ev->value; + if (setpoint.mbar) + dc->divemode = CCR; + ev = get_next_event(ev->next, "SP change"); + } while (ev); + set_setpoint(pi, i, setpoint.mbar, INT_MAX); +} + + +struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc) +{ + struct divecomputer *dc = &(dive->dc); + bool seen = false; + static struct plot_info pi; + int maxdepth = dive->maxdepth.mm; + unsigned int maxtime = 0; + int maxpressure = 0, minpressure = INT_MAX; + int maxhr = 0, minhr = INT_MAX; + int mintemp = dive->mintemp.mkelvin; + int maxtemp = dive->maxtemp.mkelvin; + int cyl; + + /* Get the per-cylinder maximum pressure if they are manual */ + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + int mbar = dive->cylinder[cyl].start.mbar; + if (mbar > maxpressure) + maxpressure = mbar; + if (mbar < minpressure) + minpressure = mbar; + } + + /* Then do all the samples from all the dive computers */ + do { + if (dc == given_dc) + seen = true; + int i = dc->samples; + int lastdepth = 0; + struct sample *s = dc->sample; + + while (--i >= 0) { + int depth = s->depth.mm; + int pressure = s->cylinderpressure.mbar; + int temperature = s->temperature.mkelvin; + int heartbeat = s->heartbeat; + + if (!mintemp && temperature < mintemp) + mintemp = temperature; + if (temperature > maxtemp) + maxtemp = temperature; + + if (pressure && pressure < minpressure) + minpressure = pressure; + if (pressure > maxpressure) + maxpressure = pressure; + if (heartbeat > maxhr) + maxhr = heartbeat; + if (heartbeat < minhr) + minhr = heartbeat; + + if (depth > maxdepth) + maxdepth = s->depth.mm; + if ((depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) && + s->time.seconds > maxtime) + maxtime = s->time.seconds; + lastdepth = depth; + s++; + } + dc = dc->next; + if (dc == NULL && !seen) { + dc = given_dc; + seen = true; + } + } while (dc != NULL); + + if (minpressure > maxpressure) + minpressure = 0; + if (minhr > maxhr) + minhr = 0; + + memset(&pi, 0, sizeof(pi)); + pi.maxdepth = maxdepth; + pi.maxtime = maxtime; + pi.maxpressure = maxpressure; + pi.minpressure = minpressure; + pi.minhr = minhr; + pi.maxhr = maxhr; + pi.mintemp = mintemp; + pi.maxtemp = maxtemp; + return pi; +} + +/* copy the previous entry (we know this exists), update time and depth + * and zero out the sensor pressure (since this is a synthetic entry) + * increment the entry pointer and the count of synthetic entries. */ +#define INSERT_ENTRY(_time, _depth, _sac) \ + *entry = entry[-1]; \ + entry->sec = _time; \ + entry->depth = _depth; \ + entry->running_sum = (entry - 1)->running_sum + (_time - (entry - 1)->sec) * (_depth + (entry - 1)->depth) / 2; \ + SENSOR_PRESSURE(entry) = 0; \ + entry->sac = _sac; \ + entry++; \ + idx++ + +struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + + int idx, maxtime, nr, i; + int lastdepth, lasttime, lasttemp = 0; + struct plot_data *plot_data; + struct event *ev = dc->events; + (void) dive; + maxtime = pi->maxtime; + + /* + * We want to have a plot_info event at least every 10s (so "maxtime/10+1"), + * but samples could be more dense than that (so add in dc->samples). We also + * need to have one for every event (so count events and add that) and + * additionally we want two surface events around the whole thing (thus the + * additional 4). There is also one extra space for a final entry + * that has time > maxtime (because there can be surface samples + * past "maxtime" in the original sample data) + */ + nr = dc->samples + 6 + maxtime / 10 + count_events(dc); + plot_data = calloc(nr, sizeof(struct plot_data)); + pi->entry = plot_data; + if (!plot_data) + return NULL; + pi->nr = nr; + idx = 2; /* the two extra events at the start */ + + lastdepth = 0; + lasttime = 0; + /* skip events at time = 0 */ + while (ev && ev->time.seconds == 0) + ev = ev->next; + for (i = 0; i < dc->samples; i++) { + struct plot_data *entry = plot_data + idx; + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int offset, delta; + int depth = sample->depth.mm; + int sac = sample->sac.mliter; + + /* Add intermediate plot entries if required */ + delta = time - lasttime; + if (delta <= 0) { + time = lasttime; + delta = 1; // avoid divide by 0 + } + for (offset = 10; offset < delta; offset += 10) { + if (lasttime + offset > maxtime) + break; + + /* Add events if they are between plot entries */ + while (ev && (int)ev->time.seconds < lasttime + offset) { + INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); + ev = ev->next; + } + + /* now insert the time interpolated entry */ + INSERT_ENTRY(lasttime + offset, interpolate(lastdepth, depth, offset, delta), sac); + + /* skip events that happened at this time */ + while (ev && (int)ev->time.seconds == lasttime + offset) + ev = ev->next; + } + + /* Add events if they are between plot entries */ + while (ev && (int)ev->time.seconds < time) { + INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); + ev = ev->next; + } + + + entry->sec = time; + entry->depth = depth; + + entry->running_sum = (entry - 1)->running_sum + (time - (entry - 1)->sec) * (depth + (entry - 1)->depth) / 2; + entry->stopdepth = sample->stopdepth.mm; + entry->stoptime = sample->stoptime.seconds; + entry->ndl = sample->ndl.seconds; + entry->tts = sample->tts.seconds; + pi->has_ndl |= sample->ndl.seconds; + entry->in_deco = sample->in_deco; + entry->cns = sample->cns; + if (dc->divemode == CCR) { + entry->o2pressure.mbar = entry->o2setpoint.mbar = sample->setpoint.mbar; // for rebreathers + entry->o2sensor[0].mbar = sample->o2sensor[0].mbar; // for up to three rebreather O2 sensors + entry->o2sensor[1].mbar = sample->o2sensor[1].mbar; + entry->o2sensor[2].mbar = sample->o2sensor[2].mbar; + } else { + entry->pressures.o2 = sample->setpoint.mbar / 1000.0; + } + /* FIXME! sensor index -> cylinder index translation! */ + // entry->cylinderindex = sample->sensor; + SENSOR_PRESSURE(entry) = sample->cylinderpressure.mbar; + O2CYLINDER_PRESSURE(entry) = sample->o2cylinderpressure.mbar; + if (sample->temperature.mkelvin) + entry->temperature = lasttemp = sample->temperature.mkelvin; + else + entry->temperature = lasttemp; + entry->heartbeat = sample->heartbeat; + entry->bearing = sample->bearing.degrees; + entry->sac = sample->sac.mliter; + if (sample->rbt.seconds) + entry->rbt = sample->rbt.seconds; + /* skip events that happened at this time */ + while (ev && (int)ev->time.seconds == time) + ev = ev->next; + lasttime = time; + lastdepth = depth; + idx++; + + if (time > maxtime) + break; + } + + /* Add two final surface events */ + plot_data[idx++].sec = lasttime + 1; + plot_data[idx++].sec = lasttime + 2; + pi->nr = idx; + + return plot_data; +} + +#undef INSERT_ENTRY + +static void populate_cylinder_pressure_data(int idx, int start, int end, struct plot_info *pi, bool o2flag) +{ + int i; + + /* First: check that none of the entries has sensor pressure for this cylinder index */ + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (CYLINDER_PRESSURE(o2flag, entry)) + return; + } + + /* Then: populate the first entry with the beginning cylinder pressure */ + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (o2flag) + O2CYLINDER_PRESSURE(entry) = start; + else + SENSOR_PRESSURE(entry) = start; + break; + } + + /* .. and the last entry with the ending cylinder pressure */ + for (i = pi->nr; --i >= 0; /* nothing */) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (o2flag) + O2CYLINDER_PRESSURE(entry) = end; + else + SENSOR_PRESSURE(entry) = end; + break; + } +} + +/* + * Calculate the sac rate between the two plot entries 'first' and 'last'. + * + * Everything in between has a cylinder pressure, and it's all the same + * cylinder. + */ +static int sac_between(struct dive *dive, struct plot_data *first, struct plot_data *last) +{ + int airuse; + double pressuretime; + pressure_t a, b; + cylinder_t *cyl; + + if (first == last) + return 0; + + /* Calculate air use - trivial */ + a.mbar = GET_PRESSURE(first); + b.mbar = GET_PRESSURE(last); + cyl = dive->cylinder + first->cylinderindex; + airuse = gas_volume(cyl, a) - gas_volume(cyl, b); + if (airuse <= 0) + return 0; + + /* Calculate depthpressure integrated over time */ + pressuretime = 0.0; + do { + int depth = (first[0].depth + first[1].depth) / 2; + int time = first[1].sec - first[0].sec; + double atm = depth_to_atm(depth, dive); + + pressuretime += atm * time; + } while (++first < last); + + /* Turn "atmseconds" into "atmminutes" */ + pressuretime /= 60; + + /* SAC = mliter per minute */ + return rint(airuse / pressuretime); +} + +/* + * Try to do the momentary sac rate for this entry, averaging over one + * minute. + */ +static void fill_sac(struct dive *dive, struct plot_info *pi, int idx) +{ + struct plot_data *entry = pi->entry + idx; + struct plot_data *first, *last; + int time; + + if (entry->sac) + return; + + if (!GET_PRESSURE(entry)) + return; + + /* + * Try to go back 30 seconds to get 'first'. + * Stop if the sensor changed, or if we went back too far. + */ + first = entry; + time = entry->sec - 30; + while (idx > 0) { + struct plot_data *prev = first-1; + if (prev->cylinderindex != first->cylinderindex) + break; + if (prev->depth < SURFACE_THRESHOLD && first->depth < SURFACE_THRESHOLD) + break; + if (prev->sec < time) + break; + if (!GET_PRESSURE(prev)) + break; + idx--; + first = prev; + } + + /* Now find an entry a minute after the first one */ + last = first; + time = first->sec + 60; + while (++idx < pi->nr) { + struct plot_data *next = last+1; + if (next->cylinderindex != last->cylinderindex) + break; + if (next->depth < SURFACE_THRESHOLD && last->depth < SURFACE_THRESHOLD) + break; + if (next->sec > time) + break; + if (!GET_PRESSURE(next)) + break; + last = next; + } + + /* Ok, now calculate the SAC between 'first' and 'last' */ + entry->sac = sac_between(dive, first, last); +} + +static void calculate_sac(struct dive *dive, struct plot_info *pi) +{ + for (int i = 0; i < pi->nr; i++) + fill_sac(dive, pi, i); +} + +static void populate_secondary_sensor_data(struct divecomputer *dc, struct plot_info *pi) +{ + (void) dc; + (void) pi; + /* We should try to see if it has interesting pressure data here */ +} + +static void setup_gas_sensor_pressure(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i; + struct divecomputer *secondary; + + /* First, populate the pressures with the manual cylinder data.. */ + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int start = cyl->start.mbar ?: cyl->sample_start.mbar; + int end = cyl->end.mbar ?: cyl->sample_end.mbar; + + if (!start || !end) + continue; + + populate_cylinder_pressure_data(i, start, end, pi, dive->cylinder[i].cylinder_use == OXYGEN); + } + + /* + * Here, we should try to walk through all the dive computers, + * and try to see if they have sensor data different from the + * primary dive computer (dc). + */ + secondary = &dive->dc; + do { + if (secondary == dc) + continue; + populate_secondary_sensor_data(dc, pi); + } while ((secondary = secondary->next) != NULL); +} + +#ifndef SUBSURFACE_MOBILE +/* calculate DECO STOP / TTS / NDL */ +static void calculate_ndl_tts(struct plot_data *entry, struct dive *dive, double surface_pressure) +{ + /* FIXME: This should be configurable */ + /* ascent speed up to first deco stop */ + const int ascent_s_per_step = 1; + const int ascent_mm_per_step = 200; /* 12 m/min */ + /* ascent speed between deco stops */ + const int ascent_s_per_deco_step = 1; + const int ascent_mm_per_deco_step = 16; /* 1 m/min */ + /* how long time steps in deco calculations? */ + const int time_stepsize = 60; + const int deco_stepsize = 3000; + /* at what depth is the current deco-step? */ + int next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), + surface_pressure, dive, 1), deco_stepsize); + int ascent_depth = entry->depth; + /* at what time should we give up and say that we got enuff NDL? */ + int cylinderindex = entry->cylinderindex; + /* If iterating through a dive, entry->tts_calc needs to be reset */ + entry->tts_calc = 0; + + /* If we don't have a ceiling yet, calculate ndl. Don't try to calculate + * a ndl for lower values than 3m it would take forever */ + if (next_stop == 0) { + if (entry->depth < 3000) { + entry->ndl = MAX_PROFILE_DECO; + return; + } + /* stop if the ndl is above max_ndl seconds, and call it plenty of time */ + while (entry->ndl_calc < MAX_PROFILE_DECO && deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, 1) <= 0) { + entry->ndl_calc += time_stepsize; + add_segment(depth_to_bar(entry->depth, dive), + &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.bottomsac); + } + /* we don't need to calculate anything else */ + return; + } + + /* We are in deco */ + entry->in_deco_calc = true; + + /* Add segments for movement to stopdepth */ + for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_step, entry->tts_calc += ascent_s_per_step) { + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, ascent_s_per_step, entry->o2pressure.mbar, dive, prefs.decosac); + next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth, dive)), surface_pressure, dive, 1), deco_stepsize); + } + ascent_depth = next_stop; + + /* And how long is the current deco-step? */ + entry->stoptime_calc = 0; + entry->stopdepth_calc = next_stop; + next_stop -= deco_stepsize; + + /* And how long is the total TTS */ + while (next_stop >= 0) { + /* save the time for the first stop to show in the graph */ + if (ascent_depth == entry->stopdepth_calc) + entry->stoptime_calc += time_stepsize; + + entry->tts_calc += time_stepsize; + if (entry->tts_calc > MAX_PROFILE_DECO) + break; + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.decosac); + + if (deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth,dive)), surface_pressure, dive, 1) <= next_stop) { + /* move to the next stop and add the travel between stops */ + for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_deco_step, entry->tts_calc += ascent_s_per_deco_step) + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, ascent_s_per_deco_step, entry->o2pressure.mbar, dive, prefs.decosac); + ascent_depth = next_stop; + next_stop -= deco_stepsize; + } + } +} + +/* Let's try to do some deco calculations. + */ +void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode) +{ + int i, count_iteration = 0; + double surface_pressure = (dc->surface_pressure.mbar ? dc->surface_pressure.mbar : get_surface_pressure_in_mbar(dive, true)) / 1000.0; + int last_ndl_tts_calc_time = 0; + int first_ceiling = 0; + bool first_iteration = true; + int final_tts = 0 , time_clear_ceiling = 0, time_deep_ceiling = 0, deco_time = 0, prev_deco_time = 10000000; + char *cache_data_initial = NULL; + /* For VPM-B outside the planner, cache the initial deco state for CVA iterations */ + if (prefs.deco_mode == VPMB && !in_planner()) + cache_deco_state(&cache_data_initial); + /* For VPM-B outside the planner, iterate until deco time converges (usually one or two iterations after the initial) + * Set maximum number of iterations to 10 just in case */ + while ((abs(prev_deco_time - deco_time) >= 30) && (count_iteration < 10)) { + for (i = 1; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + int j, t0 = (entry - 1)->sec, t1 = entry->sec; + int time_stepsize = 20; + + entry->ambpressure = depth_to_bar(entry->depth, dive); + entry->gfline = MAX((double)prefs.gflow, (entry->ambpressure - surface_pressure) / (gf_low_pressure_this_dive - surface_pressure) * + (prefs.gflow - prefs.gfhigh) + + prefs.gfhigh) * + (100.0 - AMB_PERCENTAGE) / 100.0 + AMB_PERCENTAGE; + if (t0 > t1) { + fprintf(stderr, "non-monotonous dive stamps %d %d\n", t0, t1); + int xchg = t1; + t1 = t0; + t0 = xchg; + } + if (t0 != t1 && t1 - t0 < time_stepsize) + time_stepsize = t1 - t0; + for (j = t0 + time_stepsize; j <= t1; j += time_stepsize) { + int depth = interpolate(entry[-1].depth, entry[0].depth, j - t0, t1 - t0); + add_segment(depth_to_bar(depth, dive), + &dive->cylinder[entry->cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, entry->sac); + if ((t1 - j < time_stepsize) && (j < t1)) + time_stepsize = t1 - j; + } + if (t0 == t1) { + entry->ceiling = (entry - 1)->ceiling; + } else { + /* Keep updating the VPM-B gradients until the start of the ascent phase of the dive. */ + if (prefs.deco_mode == VPMB && !in_planner() && (entry - 1)->ceiling >= first_ceiling && first_iteration == true) { + nuclear_regeneration(t1); + vpmb_start_gradient(); + /* For CVA calculations, start by guessing deco time = dive time remaining */ + deco_time = pi->maxtime - t1; + vpmb_next_gradient(deco_time, surface_pressure / 1000.0); + } + entry->ceiling = deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, !prefs.calcceiling3m); + /* If using VPM-B outside the planner, take first_ceiling_pressure as the deepest ceiling */ + if (prefs.deco_mode == VPMB && !in_planner()) { + if (entry->ceiling >= first_ceiling) { + time_deep_ceiling = t1; + first_ceiling = entry->ceiling; + first_ceiling_pressure.mbar = depth_to_mbar(first_ceiling, dive); + if (first_iteration) { + nuclear_regeneration(t1); + vpmb_start_gradient(); + /* For CVA calculations, start by guessing deco time = dive time remaining */ + deco_time = pi->maxtime - t1; + vpmb_next_gradient(deco_time, surface_pressure / 1000.0); + } + } + // Use the point where the ceiling clears as the end of deco phase for CVA calculations + if (entry->ceiling > 0) + time_clear_ceiling = 0; + else if (time_clear_ceiling == 0) + time_clear_ceiling = t1; + } + } + for (j = 0; j < 16; j++) { + double m_value = buehlmann_inertgas_a[j] + entry->ambpressure / buehlmann_inertgas_b[j]; + entry->ceilings[j] = deco_allowed_depth(tolerated_by_tissue[j], surface_pressure, dive, 1); + entry->percentages[j] = tissue_inertgas_saturation[j] < entry->ambpressure ? + tissue_inertgas_saturation[j] / entry->ambpressure * AMB_PERCENTAGE : + AMB_PERCENTAGE + (tissue_inertgas_saturation[j] - entry->ambpressure) / (m_value - entry->ambpressure) * (100.0 - AMB_PERCENTAGE); + } + + /* should we do more calculations? + * We don't for print-mode because this info doesn't show up there + * If the ceiling hasn't cleared by the last data point, we need tts for VPM-B CVA calculation + * It is not necessary to do these calculation on the first VPMB iteration, except for the last data point */ + if ((prefs.calcndltts && !print_mode && (prefs.deco_mode != VPMB || in_planner() || !first_iteration)) || + (prefs.deco_mode == VPMB && !in_planner() && i == pi->nr - 1)) { + /* only calculate ndl/tts on every 30 seconds */ + if ((entry->sec - last_ndl_tts_calc_time) < 30 && i != pi->nr - 1) { + struct plot_data *prev_entry = (entry - 1); + entry->stoptime_calc = prev_entry->stoptime_calc; + entry->stopdepth_calc = prev_entry->stopdepth_calc; + entry->tts_calc = prev_entry->tts_calc; + entry->ndl_calc = prev_entry->ndl_calc; + continue; + } + last_ndl_tts_calc_time = entry->sec; + + /* We are going to mess up deco state, so store it for later restore */ + char *cache_data = NULL; + cache_deco_state(&cache_data); + calculate_ndl_tts(entry, dive, surface_pressure); + if (prefs.deco_mode == VPMB && !in_planner() && i == pi->nr - 1) + final_tts = entry->tts_calc; + /* Restore "real" deco state for next real time step */ + restore_deco_state(cache_data); + free(cache_data); + } + } + if (prefs.deco_mode == VPMB && !in_planner()) { + prev_deco_time = deco_time; + // Do we need to update deco_time? + if (final_tts > 0) + deco_time = pi->maxtime + final_tts - time_deep_ceiling; + else if (time_clear_ceiling > 0) + deco_time = time_clear_ceiling - time_deep_ceiling; + vpmb_next_gradient(deco_time, surface_pressure / 1000.0); + final_tts = 0; + last_ndl_tts_calc_time = 0; + first_ceiling = 0; + first_iteration = false; + count_iteration ++; + restore_deco_state(cache_data_initial); + } else { + // With Buhlmann, or not in planner, iterating isn't needed. This makes the while condition false. + prev_deco_time = deco_time = 0; + } + } + free(cache_data_initial); +#if DECO_CALC_DEBUG & 1 + dump_tissues(); +#endif +} +#endif + +/* Function calculate_ccr_po2: This function takes information from one plot_data structure (i.e. one point on + * the dive profile), containing the oxygen sensor values of a CCR system and, for that plot_data structure, + * calculates the po2 value from the sensor data. Several rules are applied, depending on how many o2 sensors + * there are and the differences among the readings from these sensors. + */ +static int calculate_ccr_po2(struct plot_data *entry, struct divecomputer *dc) +{ + int sump = 0, minp = 999999, maxp = -999999; + int diff_limit = 100; // The limit beyond which O2 sensor differences are considered significant (default = 100 mbar) + int i, np = 0; + + for (i = 0; i < dc->no_o2sensors; i++) + if (entry->o2sensor[i].mbar) { // Valid reading + ++np; + sump += entry->o2sensor[i].mbar; + minp = MIN(minp, entry->o2sensor[i].mbar); + maxp = MAX(maxp, entry->o2sensor[i].mbar); + } + switch (np) { + case 0: // Uhoh + return entry->o2pressure.mbar; + case 1: // Return what we have + return sump; + case 2: // Take the average + return sump / 2; + case 3: // Voting logic + if (2 * maxp - sump + minp < diff_limit) { // Upper difference acceptable... + if (2 * minp - sump + maxp) // ...and lower difference acceptable + return sump / 3; + else + return (sump - minp) / 2; + } else { + if (2 * minp - sump + maxp) // ...but lower difference acceptable + return (sump - maxp) / 2; + else + return sump / 3; + } + default: // This should not happen + assert(np <= 3); + return 0; + } +} + +static void calculate_gas_information_new(struct dive *dive, struct plot_info *pi) +{ + int i; + double amb_pressure; + + for (i = 1; i < pi->nr; i++) { + int fn2, fhe; + struct plot_data *entry = pi->entry + i; + int cylinderindex = entry->cylinderindex; + + amb_pressure = depth_to_bar(entry->depth, dive); + + fill_pressures(&entry->pressures, amb_pressure, &dive->cylinder[cylinderindex].gasmix, entry->o2pressure.mbar / 1000.0, dive->dc.divemode); + fn2 = (int)(1000.0 * entry->pressures.n2 / amb_pressure); + fhe = (int)(1000.0 * entry->pressures.he / amb_pressure); + + /* Calculate MOD, EAD, END and EADD based on partial pressures calculated before + * so there is no difference in calculating between OC and CC + * END takes Oâ‚‚ + Nâ‚‚ (air) into account ("Narcotic" for trimix dives) + * EAD just uses Nâ‚‚ ("Air" for nitrox dives) */ + pressure_t modpO2 = { .mbar = (int)(prefs.modpO2 * 1000) }; + entry->mod = (double)gas_mod(&dive->cylinder[cylinderindex].gasmix, modpO2, dive, 1).mm; + entry->end = (entry->depth + 10000) * (1000 - fhe) / 1000.0 - 10000; + entry->ead = (entry->depth + 10000) * fn2 / (double)N2_IN_AIR - 10000; + entry->eadd = (entry->depth + 10000) * + (entry->pressures.o2 / amb_pressure * O2_DENSITY + + entry->pressures.n2 / amb_pressure * N2_DENSITY + + entry->pressures.he / amb_pressure * HE_DENSITY) / + (O2_IN_AIR * O2_DENSITY + N2_IN_AIR * N2_DENSITY) * 1000 - 10000; + if (entry->mod < 0) + entry->mod = 0; + if (entry->ead < 0) + entry->ead = 0; + if (entry->end < 0) + entry->end = 0; + if (entry->eadd < 0) + entry->eadd = 0; + } +} + +void fill_o2_values(struct divecomputer *dc, struct plot_info *pi, struct dive *dive) +/* In the samples from each dive computer, there may be uninitialised oxygen + * sensor or setpoint values, e.g. when events were inserted into the dive log + * or if the dive computer does not report o2 values with every sample. But + * for drawing the profile a complete series of valid o2 pressure values is + * required. This function takes the oxygen sensor data and setpoint values + * from the structures of plotinfo and replaces the zero values with their + * last known values so that the oxygen sensor data are complete and ready + * for plotting. This function called by: create_plot_info_new() */ +{ + int i, j; + pressure_t last_sensor[3], o2pressure; + pressure_t amb_pressure; + + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + + if (dc->divemode == CCR) { + if (i == 0) { // For 1st iteration, initialise the last_sensor values + for (j = 0; j < dc->no_o2sensors; j++) + last_sensor[j].mbar = pi->entry->o2sensor[j].mbar; + } else { // Now re-insert the missing oxygen pressure values + for (j = 0; j < dc->no_o2sensors; j++) + if (entry->o2sensor[j].mbar) + last_sensor[j].mbar = entry->o2sensor[j].mbar; + else + entry->o2sensor[j].mbar = last_sensor[j].mbar; + } // having initialised the empty o2 sensor values for this point on the profile, + amb_pressure.mbar = depth_to_mbar(entry->depth, dive); + o2pressure.mbar = calculate_ccr_po2(entry, dc); // ...calculate the po2 based on the sensor data + entry->o2pressure.mbar = MIN(o2pressure.mbar, amb_pressure.mbar); + } else { + entry->o2pressure.mbar = 0; // initialise po2 to zero for dctype = OC + } + } +} + +#ifdef DEBUG_GAS +/* A CCR debug function that writes the cylinder pressure and the oxygen values to the file debug_print_profiledata.dat: + * Called in create_plot_info_new() + */ +static void debug_print_profiledata(struct plot_info *pi) +{ + FILE *f1; + struct plot_data *entry; + int i; + if (!(f1 = fopen("debug_print_profiledata.dat", "w"))) { + printf("File open error for: debug_print_profiledata.dat\n"); + } else { + fprintf(f1, "id t1 gas gasint t2 t3 dil dilint t4 t5 setpoint sensor1 sensor2 sensor3 t6 po2 fo2\n"); + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + fprintf(f1, "%d gas=%8d %8d ; dil=%8d %8d ; o2_sp= %d %d %d %d PO2= %f\n", i, SENSOR_PRESSURE(entry), + INTERPOLATED_PRESSURE(entry), O2CYLINDER_PRESSURE(entry), INTERPOLATED_O2CYLINDER_PRESSURE(entry), + entry->o2pressure.mbar, entry->o2sensor[0].mbar, entry->o2sensor[1].mbar, entry->o2sensor[2].mbar, entry->pressures.o2); + } + fclose(f1); + } +} +#endif + +/* + * Create a plot-info with smoothing and ranged min/max + * + * This also makes sure that we have extra empty events on both + * sides, so that you can do end-points without having to worry + * about it. + */ +void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast) +{ + int o2, he, o2max; +#ifndef SUBSURFACE_MOBILE + init_decompression(dive); +#endif + /* Create the new plot data */ + free((void *)last_pi_entry_new); + + get_dive_gas(dive, &o2, &he, &o2max); + if (dc->divemode == FREEDIVE){ + pi->dive_type = FREEDIVE; + } else if (he > 0) { + pi->dive_type = TRIMIX; + } else { + if (o2) + pi->dive_type = NITROX; + else + pi->dive_type = AIR; + } + + last_pi_entry_new = populate_plot_entries(dive, dc, pi); + + check_gas_change_events(dive, dc, pi); /* Populate the gas index from the gas change events */ + check_setpoint_events(dive, dc, pi); /* Populate setpoints */ + setup_gas_sensor_pressure(dive, dc, pi); /* Try to populate our gas pressure knowledge */ + if (!fast) { + populate_pressure_information(dive, dc, pi, false); /* .. calculate missing pressure entries for all gasses except o2 */ + if (dc->divemode == CCR) /* For CCR dives.. */ + populate_pressure_information(dive, dc, pi, true); /* .. calculate missing o2 gas pressure entries */ + } + fill_o2_values(dc, pi, dive); /* .. and insert the O2 sensor data having 0 values. */ + calculate_sac(dive, pi); /* Calculate sac */ +#ifndef SUBSURFACE_MOBILE + calculate_deco_information(dive, dc, pi, false); /* and ceiling information, using gradient factor values in Preferences) */ +#endif + calculate_gas_information_new(dive, pi); /* Calculate gas partial pressures */ + +#ifdef DEBUG_GAS + debug_print_profiledata(pi); +#endif + + pi->meandepth = dive->dc.meandepth.mm; + analyze_plot_info(pi); +} + +struct divecomputer *select_dc(struct dive *dive) +{ + unsigned int max = number_of_computers(dive); + unsigned int i = dc_number; + + /* Reset 'dc_number' if we've switched dives and it is now out of range */ + if (i >= max) + dc_number = i = 0; + + return get_dive_dc(dive, i); +} + +static void plot_string(struct plot_info *pi, struct plot_data *entry, struct membuffer *b, bool has_ndl) +{ + int pressurevalue, mod, ead, end, eadd; + const char *depth_unit, *pressure_unit, *temp_unit, *vertical_speed_unit; + double depthvalue, tempvalue, speedvalue, sacvalue; + int decimals; + const char *unit; + + depthvalue = get_depth_units(entry->depth, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "@: %d:%02d\nD: %.1f%s\n"), FRACTION(entry->sec, 60), depthvalue, depth_unit); + if (GET_PRESSURE(entry)) { + pressurevalue = get_pressure_units(GET_PRESSURE(entry), &pressure_unit); + put_format(b, translate("gettextFromC", "P: %d%s\n"), pressurevalue, pressure_unit); + } + if (entry->temperature) { + tempvalue = get_temp_units(entry->temperature, &temp_unit); + put_format(b, translate("gettextFromC", "T: %.1f%s\n"), tempvalue, temp_unit); + } + speedvalue = get_vertical_speed_units(abs(entry->speed), NULL, &vertical_speed_unit); + /* Ascending speeds are positive, descending are negative */ + if (entry->speed > 0) + speedvalue *= -1; + put_format(b, translate("gettextFromC", "V: %.1f%s\n"), speedvalue, vertical_speed_unit); + sacvalue = get_volume_units(entry->sac, &decimals, &unit); + if (entry->sac && prefs.show_sac) + put_format(b, translate("gettextFromC", "SAC: %.*f%s/min\n"), decimals, sacvalue, unit); + if (entry->cns) + put_format(b, translate("gettextFromC", "CNS: %u%%\n"), entry->cns); + if (prefs.pp_graphs.po2) + put_format(b, translate("gettextFromC", "pO%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.o2); + if (prefs.pp_graphs.pn2) + put_format(b, translate("gettextFromC", "pN%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.n2); + if (prefs.pp_graphs.phe) + put_format(b, translate("gettextFromC", "pHe: %.2fbar\n"), entry->pressures.he); + if (prefs.mod) { + mod = (int)get_depth_units(entry->mod, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "MOD: %d%s\n"), mod, depth_unit); + } + eadd = (int)get_depth_units(entry->eadd, NULL, &depth_unit); + if (prefs.ead) { + switch (pi->dive_type) { + case NITROX: + ead = (int)get_depth_units(entry->ead, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "EAD: %d%s\nEADD: %d%s\n"), ead, depth_unit, eadd, depth_unit); + break; + case TRIMIX: + end = (int)get_depth_units(entry->end, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "END: %d%s\nEADD: %d%s\n"), end, depth_unit, eadd, depth_unit); + break; + case AIR: + case FREEDIVING: + /* nothing */ + break; + } + } + if (entry->stopdepth) { + depthvalue = get_depth_units(entry->stopdepth, NULL, &depth_unit); + if (entry->ndl) { + /* this is a safety stop as we still have ndl */ + if (entry->stoptime) + put_format(b, translate("gettextFromC", "Safetystop: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), + depthvalue, depth_unit); + else + put_format(b, translate("gettextFromC", "Safetystop: unkn time @ %.0f%s\n"), + depthvalue, depth_unit); + } else { + /* actual deco stop */ + if (entry->stoptime) + put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), + depthvalue, depth_unit); + else + put_format(b, translate("gettextFromC", "Deco: unkn time @ %.0f%s\n"), + depthvalue, depth_unit); + } + } else if (entry->in_deco) { + put_string(b, translate("gettextFromC", "In deco\n")); + } else if (has_ndl) { + put_format(b, translate("gettextFromC", "NDL: %umin\n"), DIV_UP(entry->ndl, 60)); + } + if (entry->tts) + put_format(b, translate("gettextFromC", "TTS: %umin\n"), DIV_UP(entry->tts, 60)); + if (entry->stopdepth_calc && entry->stoptime_calc) { + depthvalue = get_depth_units(entry->stopdepth_calc, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s (calc)\n"), DIV_UP(entry->stoptime_calc, 60), + depthvalue, depth_unit); + } else if (entry->in_deco_calc) { + /* This means that we have no NDL left, + * and we have no deco stop, + * so if we just accend to the surface slowly + * (ascent_mm_per_step / ascent_s_per_step) + * everything will be ok. */ + put_string(b, translate("gettextFromC", "In deco (calc)\n")); + } else if (prefs.calcndltts && entry->ndl_calc != 0) { + if(entry->ndl_calc < MAX_PROFILE_DECO) + put_format(b, translate("gettextFromC", "NDL: %umin (calc)\n"), DIV_UP(entry->ndl_calc, 60)); + else + put_format(b, "%s", translate("gettextFromC", "NDL: >2h (calc)\n")); + } + if (entry->tts_calc) { + if (entry->tts_calc < MAX_PROFILE_DECO) + put_format(b, translate("gettextFromC", "TTS: %umin (calc)\n"), DIV_UP(entry->tts_calc, 60)); + else + put_format(b, "%s", translate("gettextFromC", "TTS: >2h (calc)\n")); + } + if (entry->rbt) + put_format(b, translate("gettextFromC", "RBT: %umin\n"), DIV_UP(entry->rbt, 60)); + if (entry->ceiling) { + depthvalue = get_depth_units(entry->ceiling, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Calculated ceiling %.0f%s\n"), depthvalue, depth_unit); + if (prefs.calcalltissues) { + int k; + for (k = 0; k < 16; k++) { + if (entry->ceilings[k]) { + depthvalue = get_depth_units(entry->ceilings[k], NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Tissue %.0fmin: %.1f%s\n"), buehlmann_N2_t_halflife[k], depthvalue, depth_unit); + } + } + } + } + if (entry->heartbeat && prefs.hrgraph) + put_format(b, translate("gettextFromC", "heartbeat: %d\n"), entry->heartbeat); + if (entry->bearing) + put_format(b, translate("gettextFromC", "bearing: %d\n"), entry->bearing); + if (entry->running_sum) { + depthvalue = get_depth_units(entry->running_sum / entry->sec, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "mean depth to here %.1f%s\n"), depthvalue, depth_unit); + } + + strip_mb(b); +} + +struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *mb) +{ + struct plot_data *entry = NULL; + int i; + + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + if (entry->sec >= time) + break; + } + if (entry) + plot_string(pi, entry, mb, pi->has_ndl); + return (entry); +} + +/* Compare two plot_data entries and writes the results into a string */ +void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum) +{ + struct plot_data *start, *stop, *data; + const char *depth_unit, *pressure_unit, *vertical_speed_unit; + char *buf2 = malloc(bufsize); + int avg_speed, max_asc_speed, max_desc_speed; + int delta_depth, avg_depth, max_depth, min_depth; + int bar_used, last_pressure, pressurevalue; + int count, last_sec, delta_time; + + double depthvalue, speedvalue; + + if (bufsize > 0) + buf[0] = '\0'; + if (e1 == NULL || e2 == NULL) { + free(buf2); + return; + } + + if (e1->sec < e2->sec) { + start = e1; + stop = e2; + } else if (e1->sec > e2->sec) { + start = e2; + stop = e1; + } else { + free(buf2); + return; + } + count = 0; + avg_speed = 0; + max_asc_speed = 0; + max_desc_speed = 0; + + delta_depth = abs(start->depth - stop->depth); + delta_time = abs(start->sec - stop->sec); + avg_depth = 0; + max_depth = 0; + min_depth = INT_MAX; + bar_used = 0; + + last_sec = start->sec; + last_pressure = GET_PRESSURE(start); + + data = start; + while (data != stop) { + data = start + count; + if (sum) + avg_speed += abs(data->speed) * (data->sec - last_sec); + else + avg_speed += data->speed * (data->sec - last_sec); + avg_depth += data->depth * (data->sec - last_sec); + + if (data->speed > max_desc_speed) + max_desc_speed = data->speed; + if (data->speed < max_asc_speed) + max_asc_speed = data->speed; + + if (data->depth < min_depth) + min_depth = data->depth; + if (data->depth > max_depth) + max_depth = data->depth; + /* Try to detect gas changes */ + if (GET_PRESSURE(data) < last_pressure + 2000) + bar_used += last_pressure - GET_PRESSURE(data); + + count += 1; + last_sec = data->sec; + last_pressure = GET_PRESSURE(data); + } + avg_depth /= stop->sec - start->sec; + avg_speed /= stop->sec - start->sec; + + snprintf(buf, bufsize, translate("gettextFromC", "%sT: %d:%02d min"), UTF8_DELTA, delta_time / 60, delta_time % 60); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(delta_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DELTA, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(min_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DOWNWARDS_ARROW, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(max_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_UPWARDS_ARROW, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(avg_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s\n"), buf2, UTF8_AVERAGE, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(max_desc_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s%sV:%.2f%s"), buf2, UTF8_DOWNWARDS_ARROW, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(max_asc_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_UPWARDS_ARROW, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(avg_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_AVERAGE, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + /* Only print if gas has been used */ + if (bar_used) { + pressurevalue = get_pressure_units(bar_used, &pressure_unit); + memcpy(buf2, buf, bufsize); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sP:%d %s"), buf2, UTF8_DELTA, pressurevalue, pressure_unit); + } + + free(buf2); +} diff --git a/core/profile.h b/core/profile.h new file mode 100644 index 000000000..abac9dd49 --- /dev/null +++ b/core/profile.h @@ -0,0 +1,111 @@ +#ifndef PROFILE_H +#define PROFILE_H + +#include "dive.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + STABLE, + SLOW, + MODERATE, + FAST, + CRAZY +} velocity_t; + +struct membuffer; +struct divecomputer; +struct plot_info; +struct plot_data { + unsigned int in_deco : 1; + int cylinderindex; + int sec; + /* pressure[0] is sensor cylinder pressure [when CCR, the pressure of the diluent cylinder] + * pressure[1] is interpolated cylinder pressure */ + int pressure[2]; + /* o2pressure[0] is o2 cylinder pressure [CCR] + * o2pressure[1] is interpolated o2 cylinder pressure [CCR] */ + int o2cylinderpressure[2]; + int temperature; + /* Depth info */ + int depth; + int ceiling; + int ceilings[16]; + int percentages[16]; + int ndl; + int tts; + int rbt; + int stoptime; + int stopdepth; + int cns; + int smoothed; + int sac; + int running_sum; + struct gas_pressures pressures; + pressure_t o2pressure; // for rebreathers, this is consensus measured po2, or setpoint otherwise. 0 for OC. + pressure_t o2sensor[3]; //for rebreathers with up to 3 PO2 sensors + pressure_t o2setpoint; + double mod, ead, end, eadd; + velocity_t velocity; + int speed; + struct plot_data *min[3]; + struct plot_data *max[3]; + int avg[3]; + /* values calculated by us */ + unsigned int in_deco_calc : 1; + int ndl_calc; + int tts_calc; + int stoptime_calc; + int stopdepth_calc; + int pressure_time; + int heartbeat; + int bearing; + double ambpressure; + double gfline; +}; + +struct ev_select { + char *ev_name; + bool plot_ev; +}; + +struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc); +void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum); +struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi); +struct plot_info *analyze_plot_info(struct plot_info *pi); +void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast); +void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode); +struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *); + +/* + * When showing dive profiles, we scale things to the + * current dive. However, we don't scale past less than + * 30 minutes or 90 ft, just so that small dives show + * up as such unless zoom is enabled. + * We also need to add 180 seconds at the end so the min/max + * plots correctly + */ +int get_maxtime(struct plot_info *pi); + +/* get the maximum depth to which we want to plot + * take into account the additional verical space needed to plot + * partial pressure graphs */ +int get_maxdepth(struct plot_info *pi); + +#define SENSOR_PR 0 +#define INTERPOLATED_PR 1 +#define SENSOR_PRESSURE(_entry) (_entry)->pressure[SENSOR_PR] +#define O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[SENSOR_PR] +#define CYLINDER_PRESSURE(_o2, _entry) (_o2 ? O2CYLINDER_PRESSURE(_entry) : SENSOR_PRESSURE(_entry)) +#define INTERPOLATED_PRESSURE(_entry) (_entry)->pressure[INTERPOLATED_PR] +#define INTERPOLATED_O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[INTERPOLATED_PR] +#define GET_PRESSURE(_entry) (SENSOR_PRESSURE(_entry) ? SENSOR_PRESSURE(_entry) : INTERPOLATED_PRESSURE(_entry)) +#define GET_O2CYLINDER_PRESSURE(_entry) (O2CYLINDER_PRESSURE(_entry) ? O2CYLINDER_PRESSURE(_entry) : INTERPOLATED_O2CYLINDER_PRESSURE(_entry)) +#define SAC_WINDOW 45 /* sliding window in seconds for current SAC calculation */ + +#ifdef __cplusplus +} +#endif +#endif // PROFILE_H diff --git a/core/qt-gui.h b/core/qt-gui.h new file mode 100644 index 000000000..92532d22f --- /dev/null +++ b/core/qt-gui.h @@ -0,0 +1,15 @@ +#ifndef QT_GUI_H +#define QT_GUI_H + +void init_qt_late(); +void init_ui(); + +void run_ui(); +void exit_ui(); + +#if defined(SUBSURFACE_MOBILE) +#include <QQuickWindow> +extern QObject *qqWindowObject; +#endif + +#endif // QT_GUI_H diff --git a/core/qt-init.cpp b/core/qt-init.cpp new file mode 100644 index 000000000..b52dfd970 --- /dev/null +++ b/core/qt-init.cpp @@ -0,0 +1,48 @@ +#include <QApplication> +#include <QNetworkProxy> +#include <QLibraryInfo> +#include <QTextCodec> +#include "helpers.h" + +void init_qt_late() +{ + QApplication *application = qApp; + // tell Qt to use system proxies + // note: on Linux, "system" == "environment variables" + QNetworkProxyFactory::setUseSystemConfiguration(true); + + // for Win32 and Qt5 we try to set the locale codec to UTF-8. + // this makes QFile::encodeName() work. +#ifdef Q_OS_WIN + QTextCodec::setCodecForLocale(QTextCodec::codecForMib(106)); +#endif + + QCoreApplication::setOrganizationName("Subsurface"); + QCoreApplication::setOrganizationDomain("subsurface.hohndel.org"); + QCoreApplication::setApplicationName("Subsurface"); + // find plugins installed in the application directory (without this SVGs don't work on Windows) + QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); + QLocale loc; + QString uiLang = uiLanguage(&loc); + QLocale::setDefault(loc); + + // we don't have translations for English - if we don't check for this + // Qt will proceed to load the second language in preference order - not what we want + // on Linux this tends to be en-US, but on the Mac it's just en + if (!uiLang.startsWith("en") || uiLang.startsWith("en-GB")) { + qtTranslator = new QTranslator; + if (qtTranslator->load(loc, "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + application->installTranslator(qtTranslator); + } else { + qDebug() << "can't find Qt localization for locale" << uiLang << "searching in" << QLibraryInfo::location(QLibraryInfo::TranslationsPath); + } + ssrfTranslator = new QTranslator; + if (ssrfTranslator->load(loc, "subsurface", "_") || + ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("translations")) || + ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("../translations"))) { + application->installTranslator(ssrfTranslator); + } else { + qDebug() << "can't find Subsurface localization for locale" << uiLang; + } + } +} diff --git a/core/qthelper.cpp b/core/qthelper.cpp new file mode 100644 index 000000000..1d3951088 --- /dev/null +++ b/core/qthelper.cpp @@ -0,0 +1,1615 @@ +#include "qthelper.h" +#include "helpers.h" +#include "gettextfromc.h" +#include "statistics.h" +#include "membuffer.h" +#include "subsurfacesysinfo.h" +#include "version.h" +#include "divecomputer.h" +#include "time.h" +#include "gettextfromc.h" +#include <sys/time.h> +#include "exif.h" +#include "file.h" +#include "prefs-macros.h" +#include <QFile> +#include <QRegExp> +#include <QDir> +#include <QDebug> +#include <QSettings> +#include <QStandardPaths> +#include <QJsonDocument> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QNetworkAccessManager> +#include <QNetworkProxy> +#include <QDateTime> +#include <QImageReader> +#include <QtConcurrent> +#include <QFont> +#include <QApplication> +#include <QTextDocument> + +#include <libxslt/documents.h> + +const char *existing_filename; +static QLocale loc; + +#define translate(_context, arg) trGettext(arg) +static const QString DEGREE_SIGNS("dD" UTF8_DEGREE); + +QString weight_string(int weight_in_grams) +{ + QString str; + if (get_units()->weight == units::KG) { + int gr = weight_in_grams % 1000; + int kg = weight_in_grams / 1000; + if (kg >= 20.0) + str = QString("%1").arg(kg + (gr >= 500 ? 1 : 0)); + else + str = QString("%1.%2").arg(kg).arg((unsigned)(gr + 50) / 100); + } else { + double lbs = grams_to_lbs(weight_in_grams); + if (lbs >= 40.0) + lbs = rint(lbs + 0.5); + else + lbs = rint(lbs + 0.05); + str = QString("%1").arg(lbs, 0, 'f', lbs >= 40.0 ? 0 : 1); + } + return (str); +} + +QString distance_string(int distanceInMeters) +{ + QString str; + if(get_units()->length == units::METERS) { + if (distanceInMeters >= 1000) + str = QString(translate("gettextFromC", "%1km")).arg(distanceInMeters / 1000); + else + str = QString(translate("gettextFromC", "%1m")).arg(distanceInMeters); + } else { + double miles = m_to_mile(distanceInMeters); + if (miles >= 1.0) + str = QString(translate("gettextFromC", "%1mi")).arg((int)miles); + else + str = QString(translate("gettextFromC", "%1yd")).arg((int)(miles * 1760)); + } + return str; +} + +extern "C" const char *printGPSCoords(int lat, int lon) +{ + unsigned int latdeg, londeg; + unsigned int latmin, lonmin; + double latsec, lonsec; + QString lath, lonh, result; + + if (!lat && !lon) + return strdup(""); + + if (prefs.coordinates_traditional) { + lath = lat >= 0 ? translate("gettextFromC", "N") : translate("gettextFromC", "S"); + lonh = lon >= 0 ? translate("gettextFromC", "E") : translate("gettextFromC", "W"); + lat = abs(lat); + lon = abs(lon); + latdeg = lat / 1000000U; + londeg = lon / 1000000U; + latmin = (lat % 1000000U) * 60U; + lonmin = (lon % 1000000U) * 60U; + latsec = (latmin % 1000000) * 60; + lonsec = (lonmin % 1000000) * 60; + result.sprintf("%u%s%02d\'%06.3f\"%s %u%s%02d\'%06.3f\"%s", + latdeg, UTF8_DEGREE, latmin / 1000000, latsec / 1000000, lath.toUtf8().data(), + londeg, UTF8_DEGREE, lonmin / 1000000, lonsec / 1000000, lonh.toUtf8().data()); + } else { + result.sprintf("%f %f", (double) lat / 1000000.0, (double) lon / 1000000.0); + } + return strdup(result.toUtf8().data()); +} + +/** +* Try to parse in a generic manner a coordinate. +*/ +static bool parseCoord(const QString& txt, int& pos, const QString& positives, + const QString& negatives, const QString& others, + double& value) +{ + bool numberDefined = false, degreesDefined = false, + minutesDefined = false, secondsDefined = false; + double number = 0.0; + int posBeforeNumber = pos; + int sign = 0; + value = 0.0; + while (pos < txt.size()) { + if (txt[pos].isDigit()) { + if (numberDefined) + return false; + QRegExp numberRe("(\\d+(?:[\\.,]\\d+)?).*"); + if (!numberRe.exactMatch(txt.mid(pos))) + return false; + number = numberRe.cap(1).toDouble(); + numberDefined = true; + posBeforeNumber = pos; + pos += numberRe.cap(1).size() - 1; + } else if (positives.indexOf(txt[pos]) >= 0) { + if (sign != 0) + return false; + sign = 1; + if (degreesDefined || numberDefined) { + //sign after the degrees => + //at the end of the coordinate + ++pos; + break; + } + } else if (negatives.indexOf(txt[pos]) >= 0) { + if (sign != 0) { + if (others.indexOf(txt[pos]) >= 0) + //special case for the '-' sign => next coordinate + break; + return false; + } + sign = -1; + if (degreesDefined || numberDefined) { + //sign after the degrees => + //at the end of the coordinate + ++pos; + break; + } + } else if (others.indexOf(txt[pos]) >= 0) { + //we are at the next coordinate. + break; + } else if (DEGREE_SIGNS.indexOf(txt[pos]) >= 0 || + (txt[pos].isSpace() && !degreesDefined && numberDefined)) { + if (!numberDefined) + return false; + if (degreesDefined) { + //next coordinate => need to put back the number + pos = posBeforeNumber; + numberDefined = false; + break; + } + value += number; + numberDefined = false; + degreesDefined = true; + } else if (txt[pos] == '\'' || (txt[pos].isSpace() && !minutesDefined && numberDefined)) { + if (!numberDefined || minutesDefined) + return false; + value += number / 60.0; + numberDefined = false; + minutesDefined = true; + } else if (txt[pos] == '"' || (txt[pos].isSpace() && !secondsDefined && numberDefined)) { + if (!numberDefined || secondsDefined) + return false; + value += number / 3600.0; + numberDefined = false; + secondsDefined = true; + } else if ((numberDefined || minutesDefined || secondsDefined) && + (txt[pos] == ',' || txt[pos] == ';')) { + // next coordinate coming up + // eat the ',' and any subsequent white space + while (txt[++pos].isSpace()) + /* nothing */ ; + break; + } else { + return false; + } + ++pos; + } + if (!degreesDefined && numberDefined) { + value = number; //just a single number => degrees + } else if (!minutesDefined && numberDefined) { + value += number / 60.0; + } else if (!secondsDefined && numberDefined) { + value += number / 3600.0; + } else if (numberDefined) { + return false; + } + if (sign == -1) value *= -1.0; + return true; +} + +/** +* Parse special coordinate formats that cannot be handled by parseCoord. +*/ +static bool parseSpecialCoords(const QString& txt, double& latitude, double& longitude) { + QRegExp xmlFormat("(-?\\d+(?:\\.\\d+)?),?\\s+(-?\\d+(?:\\.\\d+)?)"); + if (xmlFormat.exactMatch(txt)) { + latitude = xmlFormat.cap(1).toDouble(); + longitude = xmlFormat.cap(2).toDouble(); + return true; + } + return false; +} + +bool parseGpsText(const QString &gps_text, double *latitude, double *longitude) +{ + static const QString POS_LAT = QString("+N") + translate("gettextFromC", "N"); + static const QString NEG_LAT = QString("-S") + translate("gettextFromC", "S"); + static const QString POS_LON = QString("+E") + translate("gettextFromC", "E"); + static const QString NEG_LON = QString("-W") + translate("gettextFromC", "W"); + + //remove the useless spaces (but keep the ones separating numbers) + static const QRegExp SPACE_CLEANER("\\s*([" + POS_LAT + NEG_LAT + POS_LON + + NEG_LON + DEGREE_SIGNS + "'\"\\s])\\s*"); + const QString normalized = gps_text.trimmed().toUpper().replace(SPACE_CLEANER, "\\1"); + + if (normalized.isEmpty()) { + *latitude = 0.0; + *longitude = 0.0; + return true; + } + if (parseSpecialCoords(normalized, *latitude, *longitude)) + return true; + int pos = 0; + return parseCoord(normalized, pos, POS_LAT, NEG_LAT, POS_LON + NEG_LON, *latitude) && + parseCoord(normalized, pos, POS_LON, NEG_LON, "", *longitude) && + pos == normalized.size(); +} + +#if 0 // we'll need something like this for the dive site management, eventually +bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out) +{ + double latitude, longitude; + int latudeg, longudeg; + bool ignore; + bool *parsed = parsed_out ?: &ignore; + *parsed = true; + + /* if we have a master and the dive's gps address is different from it, + * don't change the dive */ + if (master && (master->latitude.udeg != dive->latitude.udeg || + master->longitude.udeg != dive->longitude.udeg)) + return false; + + if (!(*parsed = parseGpsText(gps_text, &latitude, &longitude))) + return false; + + latudeg = rint(1000000 * latitude); + longudeg = rint(1000000 * longitude); + + /* if dive gps didn't change, nothing changed */ + if (dive->latitude.udeg == latudeg && dive->longitude.udeg == longudeg) + return false; + /* ok, update the dive and mark things changed */ + dive->latitude.udeg = latudeg; + dive->longitude.udeg = longudeg; + return true; +} +#endif + +QList<int> getDivesInTrip(dive_trip_t *trip) +{ + QList<int> ret; + int i; + struct dive *d; + for_each_dive (i, d) { + if (d->divetrip == trip) { + ret.push_back(get_divenr(d)); + } + } + return ret; +} + +// we need this to be uniq, but also make sure +// it doesn't change during the life time of a Subsurface session +// oh, and it has no meaning whatsoever - that's why we have the +// silly initial number and increment by 3 :-) +int dive_getUniqID(struct dive *d) +{ + static QSet<int> ids; + static int maxId = 83529; + + int id = d->id; + if (id) { + if (!ids.contains(id)) { + qDebug() << "WTF - only I am allowed to create IDs"; + ids.insert(id); + } + return id; + } + maxId += 3; + id = maxId; + Q_ASSERT(!ids.contains(id)); + ids.insert(id); + return id; +} + + +static xmlDocPtr get_stylesheet_doc(const xmlChar *uri, xmlDictPtr, int, void *, xsltLoadType) +{ + QFile f(QLatin1String(":/xslt/") + (const char *)uri); + if (!f.open(QIODevice::ReadOnly)) { + if (verbose > 0) { + qDebug() << "cannot open stylesheet" << QLatin1String(":/xslt/") + (const char *)uri; + return NULL; + } + } + /* Load and parse the data */ + QByteArray source = f.readAll(); + + xmlDocPtr doc = xmlParseMemory(source, source.size()); + return doc; +} + +extern "C" xsltStylesheetPtr get_stylesheet(const char *name) +{ + // this needs to be done only once, but doesn't hurt to run every time + xsltSetLoaderFunc(get_stylesheet_doc); + + // get main document: + xmlDocPtr doc = get_stylesheet_doc((const xmlChar *)name, NULL, 0, NULL, XSLT_LOAD_START); + if (!doc) + return NULL; + + // xsltSetGenericErrorFunc(stderr, NULL); + xsltStylesheetPtr xslt = xsltParseStylesheetDoc(doc); + if (!xslt) { + xmlFreeDoc(doc); + return NULL; + } + + return xslt; +} + + +extern "C" timestamp_t picture_get_timestamp(char *filename) +{ + EXIFInfo exif; + memblock mem; + int retval; + + // filename might not be the actual filename, so let's go via the hash. + if (readfile(localFilePath(QString(filename)).toUtf8().data(), &mem) <= 0) + return 0; + retval = exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size); + free(mem.buffer); + if (retval != PARSE_EXIF_SUCCESS) + return 0; + return exif.epoch(); +} + +extern "C" char *move_away(const char *old_path) +{ + if (verbose > 1) + qDebug() << "move away" << old_path; + QDir oldDir(old_path); + QDir newDir; + QString newPath; + int i = 0; + do { + newPath = QString(old_path) + QString(".%1").arg(++i); + newDir.setPath(newPath); + } while(newDir.exists()); + if (verbose > 1) + qDebug() << "renaming to" << newPath; + if (!oldDir.rename(old_path, newPath)) { + if (verbose) + qDebug() << "rename of" << old_path << "to" << newPath << "failed"; + // this next one we only try on Windows... if we are on a different platform + // we simply give up and return an empty string +#ifdef WIN32 + if (subsurface_dir_rename(old_path, qPrintable(newPath)) == 0) +#endif + return strdup(""); + } + return strdup(qPrintable(newPath)); +} + +extern "C" char *get_file_name(const char *fileName) +{ + QFileInfo fileInfo(fileName); + return strdup(fileInfo.fileName().toUtf8()); +} + +extern "C" void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName) +{ + QString fileName(cfileName); + QString newName(path); + newName += cnewName; + QFile file(newName); + if (file.exists()) + file.remove(); + if (!QFile::copy(fileName, newName)) + qDebug() << "copy of" << fileName << "to" << newName << "failed"; +} + +extern "C" bool string_sequence_contains(const char *string_sequence, const char *text) +{ + if (same_string(text, "") || same_string(string_sequence, "")) + return false; + + QString stringSequence(string_sequence); + QStringList strings = stringSequence.split(",", QString::SkipEmptyParts); + Q_FOREACH (const QString& string, strings) { + if (string.trimmed().compare(QString(text).trimmed(), Qt::CaseInsensitive) == 0) + return true; + } + return false; +} + +static bool lessThan(const QPair<QString, int> &a, const QPair<QString, int> &b) +{ + return a.second < b.second; +} + +void selectedDivesGasUsed(QVector<QPair<QString, int> > &gasUsedOrdered) +{ + int i, j; + struct dive *d; + QMap<QString, int> gasUsed; + for_each_dive (i, d) { + if (!d->selected) + continue; + volume_t diveGases[MAX_CYLINDERS] = {}; + get_gas_used(d, diveGases); + for (j = 0; j < MAX_CYLINDERS; j++) + if (diveGases[j].mliter) { + QString gasName = gasname(&d->cylinder[j].gasmix); + gasUsed[gasName] += diveGases[j].mliter; + } + } + Q_FOREACH(const QString& gas, gasUsed.keys()) { + gasUsedOrdered.append(qMakePair(gas, gasUsed[gas])); + } + qSort(gasUsedOrdered.begin(), gasUsedOrdered.end(), lessThan); +} + +QString getUserAgent() +{ + QString arch; + // fill in the system data - use ':' as separator + // replace all other ':' with ' ' so that this is easy to parse +#ifdef SUBSURFACE_MOBILE + QString userAgent = QString("Subsurface-mobile:%1(%2):").arg(subsurface_mobile_version()).arg(subsurface_canonical_version()); +#else + QString userAgent = QString("Subsurface:%1:").arg(subsurface_canonical_version()); +#endif + userAgent.append(SubsurfaceSysInfo::prettyOsName().replace(':', ' ') + ":"); + arch = SubsurfaceSysInfo::buildCpuArchitecture().replace(':', ' '); + userAgent.append(arch); + if (arch == "i386") + userAgent.append("/" + SubsurfaceSysInfo::currentCpuArchitecture()); + userAgent.append(":" + uiLanguage(NULL)); + return userAgent; + +} + +extern "C" const char *subsurface_user_agent() +{ + static QString uA = getUserAgent(); + + return strdup(qPrintable(uA)); +} + +QString uiLanguage(QLocale *callerLoc) +{ + QString shortDateFormat; + QString dateFormat; + QString timeFormat; + QSettings s; + QVariant v; + s.beginGroup("Language"); + + if (!s.value("UseSystemLanguage", true).toBool()) { + loc = QLocale(s.value("UiLanguage", QLocale().uiLanguages().first()).toString()); + } else { + loc = QLocale(QLocale().uiLanguages().first()); + } + QStringList languages = loc.uiLanguages(); + QString uiLang; + if (languages[0].contains('-')) + uiLang = languages[0]; + else if (languages.count() > 1 && languages[1].contains('-')) + uiLang = languages[1]; + else if (languages.count() > 2 && languages[2].contains('-')) + uiLang = languages[2]; + else + uiLang = languages[0]; + GET_BOOL("time_format_override", time_format_override); + GET_BOOL("date_format_override", date_format_override); + GET_TXT("time_format", time_format); + GET_TXT("date_format", date_format); + GET_TXT("date_format_short", date_format_short); + s.endGroup(); + + // there's a stupid Qt bug on MacOS where uiLanguages doesn't give us the country info + if (!uiLang.contains('-') && uiLang != loc.bcp47Name()) { + QLocale loc2(loc.bcp47Name()); + loc = loc2; + QStringList languages = loc2.uiLanguages(); + if (languages[0].contains('-')) + uiLang = languages[0]; + else if (languages.count() > 1 && languages[1].contains('-')) + uiLang = languages[1]; + else if (languages.count() > 2 && languages[2].contains('-')) + uiLang = languages[2]; + } + if (callerLoc) + *callerLoc = loc; + + if (!prefs.date_format_override || same_string(prefs.date_format_short, "") || same_string(prefs.date_format, "")) { + // derive our standard date format from what the locale gives us + // the short format is fine + // the long format uses long weekday and month names, so replace those with the short ones + // for time we don't want the time zone designator and don't want leading zeroes on the hours + shortDateFormat = loc.dateFormat(QLocale::ShortFormat); + dateFormat = loc.dateFormat(QLocale::LongFormat); + dateFormat.replace("dddd,", "ddd").replace("dddd", "ddd").replace("MMMM", "MMM"); + // special hack for Swedish as our switching from long weekday names to short weekday names + // messes things up there + dateFormat.replace("'en' 'den' d:'e'", " d"); + if (!prefs.date_format_override || same_string(prefs.date_format, "")) { + free((void*)prefs.date_format); + prefs.date_format = strdup(qPrintable(dateFormat)); + } + if (!prefs.date_format_override || same_string(prefs.date_format_short, "")) { + free((void*)prefs.date_format_short); + prefs.date_format_short = strdup(qPrintable(shortDateFormat)); + } + } + if (!prefs.time_format_override || same_string(prefs.time_format, "")) { + timeFormat = loc.timeFormat(); + timeFormat.replace("(t)", "").replace(" t", "").replace("t", "").replace("hh", "h").replace("HH", "H").replace("'kl'.", ""); + timeFormat.replace(".ss", "").replace(":ss", "").replace("ss", ""); + free((void*)prefs.time_format); + prefs.time_format = strdup(qPrintable(timeFormat)); + } + return uiLang; +} + +QLocale getLocale() +{ + return loc; +} + +void set_filename(const char *filename, bool force) +{ + if (!force && existing_filename) + return; + free((void *)existing_filename); + if (filename) + existing_filename = strdup(filename); + else + existing_filename = NULL; +} + +const QString get_dc_nickname(const char *model, uint32_t deviceid) +{ + const DiveComputerNode *existNode = dcList.getExact(model, deviceid); + + if (existNode && !existNode->nickName.isEmpty()) + return existNode->nickName; + else + return model; +} + +QString get_depth_string(int mm, bool showunit, bool showdecimal) +{ + if (prefs.units.length == units::METERS) { + double meters = mm / 1000.0; + return QString("%1%2").arg(meters, 0, 'f', (showdecimal && meters < 20.0) ? 1 : 0).arg(showunit ? translate("gettextFromC", "m") : ""); + } else { + double feet = mm_to_feet(mm); + return QString("%1%2").arg(feet, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "ft") : ""); + } +} + +QString get_depth_string(depth_t depth, bool showunit, bool showdecimal) +{ + return get_depth_string(depth.mm, showunit, showdecimal); +} + +QString get_depth_unit() +{ + if (prefs.units.length == units::METERS) + return QString("%1").arg(translate("gettextFromC", "m")); + else + return QString("%1").arg(translate("gettextFromC", "ft")); +} + +QString get_weight_string(weight_t weight, bool showunit) +{ + QString str = weight_string(weight.grams); + if (get_units()->weight == units::KG) { + str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "kg") : ""); + } else { + str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "lbs") : ""); + } + return (str); +} + +QString get_weight_unit() +{ + if (prefs.units.weight == units::KG) + return QString("%1").arg(translate("gettextFromC", "kg")); + else + return QString("%1").arg(translate("gettextFromC", "lbs")); +} + +/* these methods retrieve used gas per cylinder */ +static unsigned start_pressure(cylinder_t *cyl) +{ + return cyl->start.mbar ?: cyl->sample_start.mbar; +} + +static unsigned end_pressure(cylinder_t *cyl) +{ + return cyl->end.mbar ?: cyl->sample_end.mbar; +} + +QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit) +{ + int decimals; + const char *unit; + double gas_usage; + /* Get the cylinder gas use in mbar */ + gas_usage = start_pressure(cyl) - end_pressure(cyl); + /* Can we turn it into a volume? */ + if (cyl->type.size.mliter) { + gas_usage = bar_to_atm(gas_usage / 1000); + gas_usage *= cyl->type.size.mliter; + gas_usage = get_volume_units(gas_usage, &decimals, &unit); + } else { + gas_usage = get_pressure_units(gas_usage, &unit); + decimals = 0; + } + // translate("gettextFromC","%.*f %s" + return QString("%1 %2").arg(gas_usage, 0, 'f', decimals).arg(showunit ? unit : ""); +} + +QString get_temperature_string(temperature_t temp, bool showunit) +{ + if (temp.mkelvin == 0) { + return ""; //temperature not defined + } else if (prefs.units.temperature == units::CELSIUS) { + double celsius = mkelvin_to_C(temp.mkelvin); + return QString("%1%2%3").arg(celsius, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "C") : ""); + } else { + double fahrenheit = mkelvin_to_F(temp.mkelvin); + return QString("%1%2%3").arg(fahrenheit, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "F") : ""); + } +} + +QString get_temp_unit() +{ + if (prefs.units.temperature == units::CELSIUS) + return QString(UTF8_DEGREE "C"); + else + return QString(UTF8_DEGREE "F"); +} + +QString get_volume_string(volume_t volume, bool showunit) +{ + const char *unit; + int decimals; + double value = get_volume_units(volume.mliter, &decimals, &unit); + return QString("%1%2").arg(value, 0, 'f', decimals).arg(showunit ? unit : ""); +} + +QString get_volume_unit() +{ + const char *unit; + (void) get_volume_units(0, NULL, &unit); + return QString(unit); +} + +QString get_pressure_string(pressure_t pressure, bool showunit) +{ + if (prefs.units.pressure == units::BAR) { + double bar = pressure.mbar / 1000.0; + return QString("%1%2").arg(bar, 0, 'f', 1).arg(showunit ? translate("gettextFromC", "bar") : ""); + } else { + double psi = mbar_to_PSI(pressure.mbar); + return QString("%1%2").arg(psi, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "psi") : ""); + } +} + +QString getSubsurfaceDataPath(QString folderToFind) +{ + QString execdir; + QDir folder; + + // first check if we are running in the build dir, so the path that we + // are looking for is just a subdirectory of the execution path; + // this also works on Windows as there we install the dirs + // under the application path + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append(QDir::separator()).append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + + // next check for the Linux typical $(prefix)/share/subsurface + execdir = QCoreApplication::applicationDirPath(); + if (execdir.contains("bin")) { + folder = QDir(execdir.replace("bin", "share/subsurface/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + } + // then look for the usual locations on a Mac + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append("/../Resources/share/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append("/../Resources/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + return QString(""); +} + +static const char *printing_templates = "printing_templates"; + +QString getPrintingTemplatePathUser() +{ + static QString path = QString(); + if (path.isEmpty()) + path = QString(system_default_directory()) + QDir::separator() + QString(printing_templates); + return path; +} + +QString getPrintingTemplatePathBundle() +{ + static QString path = QString(); + if (path.isEmpty()) + path = getSubsurfaceDataPath(printing_templates); + return path; +} + +void copyPath(QString src, QString dst) +{ + QDir dir(src); + if (!dir.exists()) + return; + foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QString dst_path = dst + QDir::separator() + d; + dir.mkpath(dst_path); + copyPath(src + QDir::separator() + d, dst_path); + } + foreach (QString f, dir.entryList(QDir::Files)) + QFile::copy(src + QDir::separator() + f, dst + QDir::separator() + f); +} + +int gettimezoneoffset(timestamp_t when) +{ + QDateTime dt1, dt2; + if (when == 0) + dt1 = QDateTime::currentDateTime(); + else + dt1 = QDateTime::fromMSecsSinceEpoch(when * 1000); + dt2 = dt1.toUTC(); + dt1.setTimeSpec(Qt::UTC); + return dt2.secsTo(dt1); +} + +int parseLengthToMm(const QString &text) +{ + int mm; + QString numOnly = text; + numOnly.replace(",", ".").remove(QRegExp("[^-0-9.]")); + if (numOnly.isEmpty()) + return 0; + double number = numOnly.toDouble(); + if (text.contains(QObject::tr("m"), Qt::CaseInsensitive)) { + mm = number * 1000; + } else if (text.contains(QObject::tr("ft"), Qt::CaseInsensitive)) { + mm = feet_to_mm(number); + } else { + switch (prefs.units.length) { + case units::FEET: + mm = feet_to_mm(number); + break; + case units::METERS: + mm = number * 1000; + break; + default: + mm = 0; + } + } + return mm; + +} + +int parseTemperatureToMkelvin(const QString &text) +{ + int mkelvin; + QString numOnly = text; + numOnly.replace(",", ".").remove(QRegExp("[^-0-9.]")); + if (numOnly.isEmpty()) + return 0; + double number = numOnly.toDouble(); + if (text.contains(QObject::tr("C"), Qt::CaseInsensitive)) { + mkelvin = C_to_mkelvin(number); + } else if (text.contains(QObject::tr("F"), Qt::CaseInsensitive)) { + mkelvin = F_to_mkelvin(number); + } else { + switch (prefs.units.temperature) { + case units::CELSIUS: + mkelvin = C_to_mkelvin(number); + break; + case units::FAHRENHEIT: + mkelvin = F_to_mkelvin(number); + break; + default: + mkelvin = 0; + } + } + return mkelvin; +} + +int parseWeightToGrams(const QString &text) +{ + int grams; + QString numOnly = text; + numOnly.replace(",", ".").remove(QRegExp("[^0-9.]")); + if (numOnly.isEmpty()) + return 0; + double number = numOnly.toDouble(); + if (text.contains(QObject::tr("kg"), Qt::CaseInsensitive)) { + grams = rint(number * 1000); + } else if (text.contains(QObject::tr("lbs"), Qt::CaseInsensitive)) { + grams = lbs_to_grams(number); + } else { + switch (prefs.units.weight) { + case units::KG: + grams = rint(number * 1000); + break; + case units::LBS: + grams = lbs_to_grams(number); + break; + default: + grams = 0; + } + } + return grams; +} + +int parsePressureToMbar(const QString &text) +{ + int mbar; + QString numOnly = text; + numOnly.replace(",", ".").remove(QRegExp("[^0-9.]")); + if (numOnly.isEmpty()) + return 0; + double number = numOnly.toDouble(); + if (text.contains(QObject::tr("bar"), Qt::CaseInsensitive)) { + mbar = rint(number * 1000); + } else if (text.contains(QObject::tr("psi"), Qt::CaseInsensitive)) { + mbar = psi_to_mbar(number); + } else { + switch (prefs.units.pressure) { + case units::BAR: + mbar = rint(number * 1000); + break; + case units::PSI: + mbar = psi_to_mbar(number); + break; + default: + mbar = 0; + } + } + return mbar; +} + +int parseGasMixO2(const QString &text) +{ + QString gasString = text; + int o2, number; + if (gasString.contains(QObject::tr("AIR"), Qt::CaseInsensitive)) { + o2 = O2_IN_AIR; + } else if (gasString.contains(QObject::tr("EAN"), Qt::CaseInsensitive)) { + gasString.remove(QRegExp("[^0-9]")); + number = gasString.toInt(); + o2 = number * 10; + } else if (gasString.contains("/")) { + QStringList gasSplit = gasString.split("/"); + number = gasSplit[0].toInt(); + o2 = number * 10; + } else { + number = gasString.toInt(); + o2 = number * 10; + } + return o2; +} + +int parseGasMixHE(const QString &text) +{ + QString gasString = text; + int he, number; + if (gasString.contains("/")) { + QStringList gasSplit = gasString.split("/"); + number = gasSplit[1].toInt(); + he = number * 10; + } else { + he = 0; + } + return he; +} + +QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText) +{ + int hrs, mins; + mins = (when + 59) / 60; + hrs = mins / 60; + mins -= hrs * 60; + + QString displayTime; + if (hrs) + displayTime = QString("%1%2%3%4").arg(hrs).arg(hourText).arg(mins, 2, 10, QChar('0')).arg(minutesText); + else + displayTime = QString("%1%2").arg(mins).arg(minutesText); + + return displayTime; +} + +QString get_dive_date_string(timestamp_t when) +{ + QDateTime ts; + ts.setMSecsSinceEpoch(when * 1000L); + return loc.toString(ts.toUTC(), QString(prefs.date_format) + " " + prefs.time_format); +} + +QString get_short_dive_date_string(timestamp_t when) +{ + QDateTime ts; + ts.setMSecsSinceEpoch(when * 1000L); + return loc.toString(ts.toUTC(), QString(prefs.date_format_short) + " " + prefs.time_format); +} + +const char *get_dive_date_c_string(timestamp_t when) +{ + QString text = get_dive_date_string(when); + return strdup(text.toUtf8().data()); +} + +bool is_same_day(timestamp_t trip_when, timestamp_t dive_when) +{ + static timestamp_t twhen = (timestamp_t) 0; + static struct tm tmt; + struct tm tmd; + + utc_mkdate(dive_when, &tmd); + + if (twhen != trip_when) { + twhen = trip_when; + utc_mkdate(twhen, &tmt); + } + + return ((tmd.tm_mday == tmt.tm_mday) && (tmd.tm_mon == tmt.tm_mon) && (tmd.tm_year == tmt.tm_year)); +} + +QString get_trip_date_string(timestamp_t when, int nr, bool getday) +{ + struct tm tm; + utc_mkdate(when, &tm); + QDateTime localTime = QDateTime::fromTime_t(when); + localTime.setTimeSpec(Qt::UTC); + QString ret ; + + QString suffix = " " + QObject::tr("(%n dive(s))", "", nr); + if (getday) { + ret = localTime.date().toString(prefs.date_format) + suffix; + } else { + ret = localTime.date().toString("MMM yy") + suffix; + } + return ret; + +} + +extern "C" void reverseGeoLookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) +{ + QNetworkRequest request; + QNetworkAccessManager *rgl = new QNetworkAccessManager(); + request.setUrl(QString("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3") + .arg(uiLanguage(NULL)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0)); + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + QNetworkReply *reply = rgl->get(request); + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + qDebug() << errorObject.errorString(); + } else { + QJsonObject obj = jsonDoc.object(); + QJsonObject address = obj.value("address").toObject(); + qDebug() << "found country:" << address.value("country").toString(); + struct dive_site *ds = get_dive_site_by_uuid(uuid); + ds->notes = add_to_string(ds->notes, "countrytag: %s", address.value("country").toString().toUtf8().data()); + } +} + +QHash<QString, QByteArray> hashOf; +QMutex hashOfMutex; +QHash<QByteArray, QString> localFilenameOf; +QHash <QString, QImage > thumbnailCache; + +extern "C" char * hashstring(char * filename) +{ + return hashOf[QString(filename)].toHex().data(); +} + +const QString hashfile_name() +{ + return QString(system_default_directory()).append("/hashes"); +} + +extern "C" char *hashfile_name_string() +{ + return strdup(hashfile_name().toUtf8().data()); +} + +void read_hashes() +{ + QFile hashfile(hashfile_name()); + if (hashfile.open(QIODevice::ReadOnly)) { + QDataStream stream(&hashfile); + stream >> localFilenameOf; + stream >> hashOf; + stream >> thumbnailCache; + hashfile.close(); + } +} + +void write_hashes() +{ + QSaveFile hashfile(hashfile_name()); + if (hashfile.open(QIODevice::WriteOnly)) { + QDataStream stream(&hashfile); + stream << localFilenameOf; + stream << hashOf; + stream << thumbnailCache; + hashfile.commit(); + } else { + qDebug() << "cannot open" << hashfile.fileName(); + } +} + +void add_hash(const QString filename, QByteArray hash) +{ + QMutexLocker locker(&hashOfMutex); + hashOf[filename] = hash; + localFilenameOf[hash] = filename; +} + +QByteArray hashFile(const QString filename) +{ + QCryptographicHash hash(QCryptographicHash::Sha1); + QFile imagefile(filename); + if (imagefile.exists() && imagefile.open(QIODevice::ReadOnly)) { + hash.addData(&imagefile); + add_hash(filename, hash.result()); + return hash.result(); + } else { + return QByteArray(); + } +} + +void learnHash(struct picture *picture, QByteArray hash) +{ + if (picture->hash) + free(picture->hash); + QMutexLocker locker(&hashOfMutex); + hashOf[QString(picture->filename)] = hash; + picture->hash = strdup(hash.toHex()); +} + +QString localFilePath(const QString originalFilename) +{ + if (hashOf.contains(originalFilename) && localFilenameOf.contains(hashOf[originalFilename])) + return localFilenameOf[hashOf[originalFilename]]; + else + return originalFilename; +} + +QString fileFromHash(char *hash) +{ + return localFilenameOf[QByteArray::fromHex(hash)]; +} + +// This needs to operate on a copy of picture as it frees it after finishing! +void updateHash(struct picture *picture) { + QByteArray hash = hashFile(fileFromHash(picture->hash)); + learnHash(picture, hash); + picture_free(picture); +} + +// This needs to operate on a copy of picture as it frees it after finishing! +void hashPicture(struct picture *picture) +{ + char *oldHash = copy_string(picture->hash); + learnHash(picture, hashFile(QString(picture->filename))); + if (!same_string(picture->hash, "") && !same_string(picture->hash, oldHash)) + mark_divelist_changed((true)); + free(oldHash); + picture_free(picture); +} + +extern "C" void cache_picture(struct picture *picture) +{ + QString filename = picture->filename; + if (!hashOf.contains(filename)) + QtConcurrent::run(hashPicture, clone_picture(picture)); +} + +void learnImages(const QDir dir, int max_recursions) +{ + QStringList filters, files; + + if (max_recursions) { + foreach (QString dirname, dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs)) { + learnImages(QDir(dir.filePath(dirname)), max_recursions - 1); + } + } + + foreach (QString format, QImageReader::supportedImageFormats()) { + filters.append(QString("*.").append(format)); + } + + foreach (QString file, dir.entryList(filters, QDir::Files)) { + files.append(dir.absoluteFilePath(file)); + } + + QtConcurrent::blockingMap(files, hashFile); +} + +extern "C" const char *local_file_path(struct picture *picture) +{ + QString hashString = picture->hash; + if (hashString.isEmpty()) { + QByteArray hash = hashFile(picture->filename); + free(picture->hash); + picture->hash = strdup(hash.toHex().data()); + } + QString localFileName = fileFromHash(picture->hash); + if (localFileName.isEmpty()) + localFileName = picture->filename; + return strdup(qPrintable(localFileName)); +} + +extern "C" bool picture_exists(struct picture *picture) +{ + QString localFilename = fileFromHash(picture->hash); + QByteArray hash = hashFile(localFilename); + return same_string(hash.toHex().data(), picture->hash); +} + +const QString picturedir() +{ + return QString(system_default_directory()).append("/picturedata/"); +} + +extern "C" char *picturedir_string() +{ + return strdup(picturedir().toUtf8().data()); +} + +/* when we get a picture from git storage (local or remote) and can't find the picture + * based on its hash, we create a local copy with the hash as filename and the appropriate + * suffix */ +extern "C" void savePictureLocal(struct picture *picture, const char *data, int len) +{ + QString dirname = picturedir(); + QDir localPictureDir(dirname); + localPictureDir.mkpath(dirname); + QString suffix(picture->filename); + suffix.replace(QRegularExpression(".*\\."), ""); + QString filename(dirname + picture->hash + "." + suffix); + QSaveFile out(filename); + if (out.open(QIODevice::WriteOnly)) { + out.write(data, len); + out.commit(); + add_hash(filename, QByteArray::fromHex(picture->hash)); + } +} + +extern "C" void picture_load_exif_data(struct picture *p) +{ + EXIFInfo exif; + memblock mem; + + if (readfile(localFilePath(QString(p->filename)).toUtf8().data(), &mem) <= 0) + goto picture_load_exit; + if (exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size) != PARSE_EXIF_SUCCESS) + goto picture_load_exit; + p->longitude.udeg= lrint(1000000.0 * exif.GeoLocation.Longitude); + p->latitude.udeg = lrint(1000000.0 * exif.GeoLocation.Latitude); + +picture_load_exit: + free(mem.buffer); + return; +} + +QString get_gas_string(struct gasmix gas) +{ + uint o2 = (get_o2(&gas) + 5) / 10, he = (get_he(&gas) + 5) / 10; + QString result = gasmix_is_air(&gas) ? QObject::tr("AIR") : he == 0 ? (o2 == 100 ? QObject::tr("OXYGEN") : QString("EAN%1").arg(o2, 2, 10, QChar('0'))) : QString("%1/%2").arg(o2).arg(he); + return result; +} + +QString get_divepoint_gas_string(const divedatapoint &p) +{ + return get_gas_string(p.gasmix); +} + +weight_t string_to_weight(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_kg = QObject::tr("kg"); + QString local_lbs = QObject::tr("lbs"); + weight_t weight; + + if (rest.startsWith("kg") || rest.startsWith(local_kg)) + goto kg; + // using just "lb" instead of "lbs" is intentional - some people might enter the singular + if (rest.startsWith("lb") || rest.startsWith(local_lbs)) + goto lbs; + if (prefs.units.weight == prefs.units.LBS) + goto lbs; +kg: + weight.grams = rint(value * 1000); + return weight; +lbs: + weight.grams = lbs_to_grams(value); + return weight; +} + +depth_t string_to_depth(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_ft = QObject::tr("ft"); + QString local_m = QObject::tr("m"); + depth_t depth; + + if (rest.startsWith("m") || rest.startsWith(local_m)) + goto m; + if (rest.startsWith("ft") || rest.startsWith(local_ft)) + goto ft; + if (prefs.units.length == prefs.units.FEET) + goto ft; +m: + depth.mm = rint(value * 1000); + return depth; +ft: + depth.mm = feet_to_mm(value); + return depth; +} + +pressure_t string_to_pressure(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_psi = QObject::tr("psi"); + QString local_bar = QObject::tr("bar"); + pressure_t pressure; + + if (rest.startsWith("bar") || rest.startsWith(local_bar)) + goto bar; + if (rest.startsWith("psi") || rest.startsWith(local_psi)) + goto psi; + if (prefs.units.pressure == prefs.units.PSI) + goto psi; +bar: + pressure.mbar = rint(value * 1000); + return pressure; +psi: + pressure.mbar = psi_to_mbar(value); + return pressure; +} + +volume_t string_to_volume(const char *str, pressure_t workp) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_l = QObject::tr("l"); + QString local_cuft = QObject::tr("cuft"); + volume_t volume; + + if (rest.startsWith("l") || rest.startsWith("â„“") || rest.startsWith(local_l)) + goto l; + if (rest.startsWith("cuft") || rest.startsWith(local_cuft)) + goto cuft; + /* + * If we don't have explicit units, and there is no working + * pressure, we're going to assume "liter" even in imperial + * measurements. + */ + if (!workp.mbar) + goto l; + if (prefs.units.volume == prefs.units.LITER) + goto l; +cuft: + if (workp.mbar) + value /= bar_to_atm(workp.mbar / 1000.0); + value = cuft_to_l(value); +l: + volume.mliter = rint(value * 1000); + return volume; +} + +fraction_t string_to_fraction(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + fraction_t fraction; + + fraction.permille = rint(value * 10); + return fraction; +} + +int getCloudURL(QString &filename) +{ + QString email = QString(prefs.cloud_storage_email); + email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); + if (email.isEmpty() || same_string(prefs.cloud_storage_password, "")) + return report_error("Please configure Cloud storage email and password in the preferences"); + if (email != prefs.cloud_storage_email_encoded) { + free(prefs.cloud_storage_email_encoded); + prefs.cloud_storage_email_encoded = strdup(qPrintable(email)); + } + filename = QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email); + if (verbose) + qDebug() << "cloud URL set as" << filename; + return 0; +} + +extern "C" char *cloud_url() +{ + QString filename; + getCloudURL(filename); + return strdup(filename.toUtf8().data()); +} + +void loadPreferences() +{ + QSettings s; + QVariant v; + + uiLanguage(NULL); + s.beginGroup("Units"); + if (s.value("unit_system").toString() == "metric") { + prefs.unit_system = METRIC; + prefs.units = SI_units; + } else if (s.value("unit_system").toString() == "imperial") { + prefs.unit_system = IMPERIAL; + prefs.units = IMPERIAL_units; + } else { + prefs.unit_system = PERSONALIZE; + GET_UNIT("length", length, units::FEET, units::METERS); + GET_UNIT("pressure", pressure, units::PSI, units::BAR); + GET_UNIT("volume", volume, units::CUFT, units::LITER); + GET_UNIT("temperature", temperature, units::FAHRENHEIT, units::CELSIUS); + GET_UNIT("weight", weight, units::LBS, units::KG); + } + GET_UNIT("vertical_speed_time", vertical_speed_time, units::MINUTES, units::SECONDS); + GET_BOOL("coordinates", coordinates_traditional); + s.endGroup(); + s.beginGroup("TecDetails"); + GET_BOOL("po2graph", pp_graphs.po2); + GET_BOOL("pn2graph", pp_graphs.pn2); + GET_BOOL("phegraph", pp_graphs.phe); + GET_DOUBLE("po2threshold", pp_graphs.po2_threshold); + GET_DOUBLE("pn2threshold", pp_graphs.pn2_threshold); + GET_DOUBLE("phethreshold", pp_graphs.phe_threshold); + GET_BOOL("mod", mod); + GET_DOUBLE("modpO2", modpO2); + GET_BOOL("ead", ead); + GET_BOOL("redceiling", redceiling); + GET_BOOL("dcceiling", dcceiling); + GET_BOOL("calcceiling", calcceiling); + GET_BOOL("calcceiling3m", calcceiling3m); + GET_BOOL("calcndltts", calcndltts); + GET_BOOL("calcalltissues", calcalltissues); + GET_BOOL("hrgraph", hrgraph); + GET_BOOL("tankbar", tankbar); + GET_BOOL("RulerBar", rulergraph); + GET_BOOL("percentagegraph", percentagegraph); + GET_INT("gflow", gflow); + GET_INT("gfhigh", gfhigh); + GET_BOOL("gf_low_at_maxdepth", gf_low_at_maxdepth); + GET_BOOL("show_ccr_setpoint",show_ccr_setpoint); + GET_BOOL("show_ccr_sensors",show_ccr_sensors); + GET_BOOL("zoomed_plot", zoomed_plot); + set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); + GET_BOOL("show_sac", show_sac); + GET_BOOL("display_unused_tanks", display_unused_tanks); + GET_BOOL("show_average_depth", show_average_depth); + s.endGroup(); + + s.beginGroup("GeneralSettings"); + GET_TXT("default_filename", default_filename); + GET_INT("default_file_behavior", default_file_behavior); + if (prefs.default_file_behavior == UNDEFINED_DEFAULT_FILE) { + // undefined, so check if there's a filename set and + // use that, otherwise go with no default file + if (QString(prefs.default_filename).isEmpty()) + prefs.default_file_behavior = NO_DEFAULT_FILE; + else + prefs.default_file_behavior = LOCAL_DEFAULT_FILE; + } + GET_TXT("default_cylinder", default_cylinder); + GET_BOOL("use_default_file", use_default_file); + GET_INT("defaultsetpoint", defaultsetpoint); + GET_INT("o2consumption", o2consumption); + GET_INT("pscr_ratio", pscr_ratio); + s.endGroup(); + + s.beginGroup("Display"); + // get the font from the settings or our defaults + // respect the system default font size if none is explicitly set + QFont defaultFont = s.value("divelist_font", prefs.divelist_font).value<QFont>(); + if (IS_FP_SAME(system_divelist_default_font_size, -1.0)) { + prefs.font_size = qApp->font().pointSizeF(); + system_divelist_default_font_size = prefs.font_size; // this way we don't save it on exit + } + prefs.font_size = s.value("font_size", prefs.font_size).toFloat(); + // painful effort to ignore previous default fonts on Windows - ridiculous + QString fontName = defaultFont.toString(); + if (fontName.contains(",")) + fontName = fontName.left(fontName.indexOf(",")); + if (subsurface_ignore_font(fontName.toUtf8().constData())) { + defaultFont = QFont(prefs.divelist_font); + } else { + free((void *)prefs.divelist_font); + prefs.divelist_font = strdup(fontName.toUtf8().constData()); + } + defaultFont.setPointSizeF(prefs.font_size); + qApp->setFont(defaultFont); + GET_INT("displayinvalid", display_invalid_dives); + s.endGroup(); + + s.beginGroup("Animations"); + GET_INT("animation_speed", animation_speed); + s.endGroup(); + + s.beginGroup("Network"); + GET_INT_DEF("proxy_type", proxy_type, QNetworkProxy::DefaultProxy); + GET_TXT("proxy_host", proxy_host); + GET_INT("proxy_port", proxy_port); + GET_BOOL("proxy_auth", proxy_auth); + GET_TXT("proxy_user", proxy_user); + GET_TXT("proxy_pass", proxy_pass); + s.endGroup(); + + s.beginGroup("CloudStorage"); + GET_TXT("email", cloud_storage_email); +#ifndef SUBSURFACE_MOBILE + GET_BOOL("save_password_local", save_password_local); +#else + // always save the password in Subsurface-mobile + prefs.save_password_local = true; +#endif + if (prefs.save_password_local) { // GET_TEXT macro is not a single statement + GET_TXT("password", cloud_storage_password); + } + GET_INT("cloud_verification_status", cloud_verification_status); + GET_BOOL("cloud_background_sync", cloud_background_sync); + GET_BOOL("git_local_only", git_local_only); + + // creating the git url here is simply a convenience when C code wants + // to compare against that git URL - it's always derived from the base URL + GET_TXT("cloud_base_url", cloud_base_url); + prefs.cloud_git_url = strdup(qPrintable(QString(prefs.cloud_base_url) + "/git")); + s.endGroup(); + + // Subsurface webservice id is stored outside of the groups + GET_TXT("subsurface_webservice_uid", userid); + + // but the related time / distance threshold (only used in the mobile app) + // are in their own group + s.beginGroup("locationService"); + GET_INT("distance_threshold", distance_threshold); + GET_INT("time_threshold", time_threshold); + s.endGroup(); + + // GeoManagement + s.beginGroup("geocoding"); +#ifdef DISABLED + GET_BOOL("enable_geocoding", geocoding.enable_geocoding); + GET_BOOL("parse_dive_without_gps", geocoding.parse_dive_without_gps); + GET_BOOL("tag_existing_dives", geocoding.tag_existing_dives); +#else + prefs.geocoding.enable_geocoding = true; +#endif + GET_ENUM("cat0", taxonomy_category, geocoding.category[0]); + GET_ENUM("cat1", taxonomy_category, geocoding.category[1]); + GET_ENUM("cat2", taxonomy_category, geocoding.category[2]); + s.endGroup(); + + // GPS service time and distance thresholds + s.beginGroup("LocationService"); + GET_INT("time_threshold", time_threshold); + GET_INT("distance_threshold", distance_threshold); + s.endGroup(); +} + +extern "C" bool isCloudUrl(const char *filename) +{ + QString email = QString(prefs.cloud_storage_email); + email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); + if (!email.isEmpty() && + QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email) == filename) + return true; + return false; +} + +extern "C" bool getProxyString(char **buffer) +{ + if (prefs.proxy_type == QNetworkProxy::HttpProxy) { + QString proxy; + if (prefs.proxy_auth) + proxy = QString("http://%1:%2@%3:%4").arg(prefs.proxy_user).arg(prefs.proxy_pass) + .arg(prefs.proxy_host).arg(prefs.proxy_port); + else + proxy = QString("http://%1:%2").arg(prefs.proxy_host).arg(prefs.proxy_port); + if (buffer) + *buffer = strdup(qPrintable(proxy)); + return true; + } + return false; +} + +extern "C" void subsurface_mkdir(const char *dir) +{ + QDir directory; + if (!directory.mkpath(QString(dir))) + qDebug() << "failed to create path" << dir; +} + +extern "C" void parse_display_units(char *line) +{ + qDebug() << line; +} + +static QByteArray currentApplicationState; + +QByteArray getCurrentAppState() +{ + return currentApplicationState; +} + +void setCurrentAppState(QByteArray state) +{ + currentApplicationState = state; +} + +extern "C" bool in_planner() +{ + return (currentApplicationState == "PlanDive" || currentApplicationState == "EditPlannedDive"); +} + +void init_proxy() +{ + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::ProxyType(prefs.proxy_type)); + proxy.setHostName(prefs.proxy_host); + proxy.setPort(prefs.proxy_port); + if (prefs.proxy_auth) { + proxy.setUser(prefs.proxy_user); + proxy.setPassword(prefs.proxy_pass); + } + QNetworkProxy::setApplicationProxy(proxy); +} + +QString getUUID() +{ + QString uuidString; + QSettings settings; + settings.beginGroup("UpdateManager"); + if (settings.contains("UUID")) { + uuidString = settings.value("UUID").toString(); + } else { + QUuid uuid = QUuid::createUuid(); + uuidString = uuid.toString(); + settings.setValue("UUID", uuidString); + } + uuidString.replace("{", "").replace("}", ""); + return uuidString; +} diff --git a/core/qthelper.h b/core/qthelper.h new file mode 100644 index 000000000..3a5ef60e4 --- /dev/null +++ b/core/qthelper.h @@ -0,0 +1,48 @@ +#ifndef QTHELPER_H +#define QTHELPER_H + +#include <QMultiMap> +#include <QString> +#include <stdint.h> +#include "dive.h" +#include "divelist.h" +#include <QTranslator> +#include <QDir> + +// global pointers for our translation +extern QTranslator *qtTranslator, *ssrfTranslator; + +QString weight_string(int weight_in_grams); +QString distance_string(int distanceInMeters); +bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out = 0); +extern "C" const char *printGPSCoords(int lat, int lon); +QList<int> getDivesInTrip(dive_trip_t *trip); +QString get_gas_string(struct gasmix gas); +QString get_divepoint_gas_string(const divedatapoint& dp); +void read_hashes(); +void write_hashes(); +void updateHash(struct picture *picture); +QByteArray hashFile(const QString filename); +void learnImages(const QDir dir, int max_recursions); +void add_hash(const QString filename, QByteArray hash); +void hashPicture(struct picture *picture); +QString localFilePath(const QString originalFilename); +QString fileFromHash(char *hash); +void learnHash(struct picture *picture, QByteArray hash); +extern "C" void cache_picture(struct picture *picture); +weight_t string_to_weight(const char *str); +depth_t string_to_depth(const char *str); +pressure_t string_to_pressure(const char *str); +volume_t string_to_volume(const char *str, pressure_t workp); +fraction_t string_to_fraction(const char *str); +int getCloudURL(QString &filename); +void loadPreferences(); +bool parseGpsText(const QString &gps_text, double *latitude, double *longitude); +QByteArray getCurrentAppState(); +void setCurrentAppState(QByteArray state); +extern "C" bool in_planner(); +extern "C" void subsurface_mkdir(const char *dir); +void init_proxy(); +QString getUUID(); + +#endif // QTHELPER_H diff --git a/core/qthelperfromc.h b/core/qthelperfromc.h new file mode 100644 index 000000000..32aed8949 --- /dev/null +++ b/core/qthelperfromc.h @@ -0,0 +1,22 @@ +#ifndef QTHELPERFROMC_H +#define QTHELPERFROMC_H + +bool getProxyString(char **buffer); +bool canReachCloudServer(); +void updateWindowTitle(); +bool isCloudUrl(const char *filename); +void subsurface_mkdir(const char *dir); +char *get_file_name(const char *fileName); +void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName); +char *hashstring(char *filename); +bool picture_exists(struct picture *picture); +char *move_away(const char *path); +const char *local_file_path(struct picture *picture); +void savePictureLocal(struct picture *picture, const char *data, int len); +void cache_picture(struct picture *picture); +char *cloud_url(); +char *hashfile_name_string(); +char *picturedir_string(); +const char *subsurface_user_agent(); + +#endif // QTHELPERFROMC_H diff --git a/core/qtserialbluetooth.cpp b/core/qtserialbluetooth.cpp new file mode 100644 index 000000000..6b104157a --- /dev/null +++ b/core/qtserialbluetooth.cpp @@ -0,0 +1,416 @@ +#include <errno.h> + +#include <QtBluetooth/QBluetoothAddress> +#include <QtBluetooth/QBluetoothSocket> +#include <QEventLoop> +#include <QTimer> +#include <QDebug> + +#include <libdivecomputer/version.h> + +#if defined(SSRF_CUSTOM_SERIAL) + +#if defined(Q_OS_WIN) + #include <winsock2.h> + #include <windows.h> + #include <ws2bth.h> +#endif + +#include <libdivecomputer/custom_serial.h> + +extern "C" { +typedef struct serial_t { + /* Library context. */ + dc_context_t *context; + /* + * RFCOMM socket used for Bluetooth Serial communication. + */ +#if defined(Q_OS_WIN) + SOCKET socket; +#else + QBluetoothSocket *socket; +#endif + long timeout; +} serial_t; + +static int qt_serial_open(serial_t **out, dc_context_t *context, const char* devaddr) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + serial_t *serial_port = (serial_t *) malloc (sizeof (serial_t)); + if (serial_port == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Library context. + serial_port->context = context; + + // Default to blocking reads. + serial_port->timeout = -1; + +#if defined(Q_OS_WIN) + // Create a RFCOMM socket + serial_port->socket = ::socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); + + if (serial_port->socket == INVALID_SOCKET) { + free(serial_port); + return DC_STATUS_IO; + } + + SOCKADDR_BTH socketBthAddress; + int socketBthAddressBth = sizeof (socketBthAddress); + char *address = strdup(devaddr); + + ZeroMemory(&socketBthAddress, socketBthAddressBth); + qDebug() << "Trying to connect to address " << devaddr; + + if (WSAStringToAddressA(address, + AF_BTH, + NULL, + (LPSOCKADDR) &socketBthAddress, + &socketBthAddressBth + ) != 0) { + qDebug() << "FAiled to convert the address " << address; + free(address); + + return DC_STATUS_IO; + } + + free(address); + + socketBthAddress.addressFamily = AF_BTH; + socketBthAddress.port = BT_PORT_ANY; + memset(&socketBthAddress.serviceClassId, 0, sizeof(socketBthAddress.serviceClassId)); + socketBthAddress.serviceClassId = SerialPortServiceClass_UUID; + + // Try to connect to the device + if (::connect(serial_port->socket, + (struct sockaddr *) &socketBthAddress, + socketBthAddressBth + ) != 0) { + qDebug() << "Failed to connect to device"; + + return DC_STATUS_NODEVICE; + } + + qDebug() << "Succesfully connected to device"; +#else + // Create a RFCOMM socket + serial_port->socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); + + // Wait until the connection succeeds or until an error occurs + QEventLoop loop; + loop.connect(serial_port->socket, SIGNAL(connected()), SLOT(quit())); + loop.connect(serial_port->socket, SIGNAL(error(QBluetoothSocket::SocketError)), SLOT(quit())); + + // Create a timer. If the connection doesn't succeed after five seconds or no error occurs then stop the opening step + QTimer timer; + int msec = 5000; + timer.setSingleShot(true); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + // First try to connect on RFCOMM channel 1. This is the default channel for most devices + QBluetoothAddress remoteDeviceAddress(devaddr); + serial_port->socket->connectToService(remoteDeviceAddress, 1, QIODevice::ReadWrite | QIODevice::Unbuffered); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { + // It seems that the connection on channel 1 took more than expected. Wait another 15 seconds + qDebug() << "The connection on RFCOMM channel number 1 took more than expected. Wait another 15 seconds."; + timer.start(3 * msec); + loop.exec(); + } else if (serial_port->socket->state() == QBluetoothSocket::UnconnectedState) { + // Try to connect on channel number 5. Maybe this is a Shearwater Petrel2 device. + qDebug() << "Connection on channel 1 failed. Trying on channel number 5."; + serial_port->socket->connectToService(remoteDeviceAddress, 5, QIODevice::ReadWrite | QIODevice::Unbuffered); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { + // It seems that the connection on channel 5 took more than expected. Wait another 15 seconds + qDebug() << "The connection on RFCOMM channel number 5 took more than expected. Wait another 15 seconds."; + timer.start(3 * msec); + loop.exec(); + } + } +#elif defined(Q_OS_ANDROID) || (QT_VERSION >= 0x050500 && defined(Q_OS_MAC)) + // Try to connect to the device using the uuid of the Serial Port Profile service + QBluetoothAddress remoteDeviceAddress(devaddr); + serial_port->socket->connectToService(remoteDeviceAddress, QBluetoothUuid(QBluetoothUuid::SerialPort)); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState || + serial_port->socket->state() == QBluetoothSocket::ServiceLookupState) { + // It seems that the connection step took more than expected. Wait another 20 seconds. + qDebug() << "The connection step took more than expected. Wait another 20 seconds"; + timer.start(4 * msec); + loop.exec(); + } +#endif + if (serial_port->socket->state() != QBluetoothSocket::ConnectedState) { + + // Get the latest error and try to match it with one from libdivecomputer + QBluetoothSocket::SocketError err = serial_port->socket->error(); + qDebug() << "Failed to connect to device " << devaddr << ". Device state " << serial_port->socket->state() << ". Error: " << err; + + free (serial_port); + switch(err) { + case QBluetoothSocket::HostNotFoundError: + case QBluetoothSocket::ServiceNotFoundError: + return DC_STATUS_NODEVICE; + case QBluetoothSocket::UnsupportedProtocolError: + return DC_STATUS_PROTOCOL; +#if QT_VERSION >= 0x050400 + case QBluetoothSocket::OperationError: + return DC_STATUS_UNSUPPORTED; +#endif + case QBluetoothSocket::NetworkError: + return DC_STATUS_IO; + default: + return QBluetoothSocket::UnknownSocketError; + } + } +#endif + *out = serial_port; + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_close(serial_t *device) +{ + if (device == NULL) + return DC_STATUS_SUCCESS; + +#if defined(Q_OS_WIN) + // Cleanup + closesocket(device->socket); + free(device); +#else + if (device->socket == NULL) { + free(device); + return DC_STATUS_SUCCESS; + } + + device->socket->close(); + + delete device->socket; + free(device); +#endif + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_read(serial_t *device, void* data, unsigned int size) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while (nbytes < size) { + rc = recv (device->socket, (char *) data + nbytes, size - nbytes, 0); + + if (rc < 0) { + return -1; // Error during recv call. + } else if (rc == 0) { + break; // EOF reached. + } + + nbytes += rc; + } + + return nbytes; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) + { + rc = device->socket->read((char *) data + nbytes, size - nbytes); + + if (rc < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; // Retry. + + return -1; // Something really bad happened :-( + } else if (rc == 0) { + // Wait until the device is available for read operations + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + loop.connect(device->socket, SIGNAL(readyRead()), SLOT(quit())); + timer.start(device->timeout); + loop.exec(); + + if (!timer.isActive()) + return nbytes; + } + + nbytes += rc; + } + + return nbytes; +#endif +} + +static int qt_serial_write(serial_t *device, const void* data, unsigned int size) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while (nbytes < size) { + rc = send(device->socket, (char *) data + nbytes, size - nbytes, 0); + + if (rc < 0) { + return -1; // Error during send call. + } + + nbytes += rc; + } + + return nbytes; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) + { + rc = device->socket->write((char *) data + nbytes, size - nbytes); + + if (rc < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; // Retry. + + return -1; // Something really bad happened :-( + } else if (rc == 0) { + break; + } + + nbytes += rc; + } + + return nbytes; +#endif +} + +static int qt_serial_flush(serial_t *device, int queue) +{ + (void)queue; + if (device == NULL) + return DC_STATUS_INVALIDARGS; +#if !defined(Q_OS_WIN) + if (device->socket == NULL) + return DC_STATUS_INVALIDARGS; +#endif + // TODO: add implementation + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_get_received(serial_t *device) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + // TODO use WSAIoctl to get the information + + return 0; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + return device->socket->bytesAvailable(); +#endif +} + +static int qt_serial_get_transmitted(serial_t *device) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + // TODO add implementation + + return 0; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + return device->socket->bytesToWrite(); +#endif +} + +static int qt_serial_set_timeout(serial_t *device, long timeout) +{ + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + device->timeout = timeout; + + return DC_STATUS_SUCCESS; +} + + +const dc_serial_operations_t qt_serial_ops = { + .open = qt_serial_open, + .close = qt_serial_close, + .read = qt_serial_read, + .write = qt_serial_write, + .flush = qt_serial_flush, + .get_received = qt_serial_get_received, + .get_transmitted = qt_serial_get_transmitted, + .set_timeout = qt_serial_set_timeout +}; + +extern void dc_serial_init (dc_serial_t *serial, void *data, const dc_serial_operations_t *ops); + +dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); + + if (serial_device == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Initialize data and function pointers + dc_serial_init(serial_device, NULL, &qt_serial_ops); + + // Open the serial device. + dc_status_t rc = (dc_status_t)qt_serial_open (&serial_device->port, context, devaddr); + if (rc != DC_STATUS_SUCCESS) { + free (serial_device); + return rc; + } + + // Set the type of the device + serial_device->type = DC_TRANSPORT_BLUETOOTH; + + *out = serial_device; + + return DC_STATUS_SUCCESS; +} +} +#endif diff --git a/core/save-git.c b/core/save-git.c new file mode 100644 index 000000000..e22019ab0 --- /dev/null +++ b/core/save-git.c @@ -0,0 +1,1249 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <time.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <git2.h> + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "git-access.h" +#include "version.h" +#include "qthelperfromc.h" + +#define VA_BUF(b, fmt) do { va_list args; va_start(args, fmt); put_vformat(b, fmt, args); va_end(args); } while (0) + +static void cond_put_format(int cond, struct membuffer *b, const char *fmt, ...) +{ + if (cond) { + VA_BUF(b, fmt); + } +} + +#define SAVE(str, x) cond_put_format(dive->x, b, str " %d\n", dive->x) + +static void show_gps(struct membuffer *b, degrees_t latitude, degrees_t longitude) +{ + if (latitude.udeg || longitude.udeg) { + put_degrees(b, latitude, "gps ", " "); + put_degrees(b, longitude, "", "\n"); + } +} + +static void quote(struct membuffer *b, const char *text) +{ + const char *p = text; + + for (;;) { + const char *escape; + + switch (*p++) { + default: + continue; + case 0: + escape = NULL; + break; + case 1 ... 8: + case 11: + case 12: + case 14 ... 31: + escape = "?"; + break; + case '\\': + escape = "\\\\"; + break; + case '"': + escape = "\\\""; + break; + case '\n': + escape = "\n\t"; + if (*p == '\n') + escape = "\n"; + break; + } + put_bytes(b, text, (p - text - 1)); + if (!escape) + break; + put_string(b, escape); + text = p; + } +} + +static void show_utf8(struct membuffer *b, const char *prefix, const char *value, const char *postfix) +{ + if (value) { + put_format(b, "%s\"", prefix); + quote(b, value); + put_format(b, "\"%s", postfix); + } +} + +static void save_overview(struct membuffer *b, struct dive *dive) +{ + show_utf8(b, "divemaster ", dive->divemaster, "\n"); + show_utf8(b, "buddy ", dive->buddy, "\n"); + show_utf8(b, "suit ", dive->suit, "\n"); + show_utf8(b, "notes ", dive->notes, "\n"); +} + +static void save_tags(struct membuffer *b, struct tag_entry *tags) +{ + const char *sep = " "; + + if (!tags) + return; + put_string(b, "tags"); + while (tags) { + show_utf8(b, sep, tags->tag->source ? : tags->tag->name, ""); + sep = ", "; + tags = tags->next; + } + put_string(b, "\n"); +} + +static void save_extra_data(struct membuffer *b, struct extra_data *ed) +{ + while (ed) { + if (ed->key && ed->value) + put_format(b, "keyvalue \"%s\" \"%s\"\n", ed->key ? : "", ed->value ? : ""); + ed = ed->next; + } +} + +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2=%u.%u%%", FRACTION(o2, 10)); + if (he) + put_format(b, " he=%u.%u%%", FRACTION(he, 10)); + } +} + +static void save_cylinder_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_cylinders(dive); + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + int volume = cylinder->type.size.mliter; + const char *description = cylinder->type.description; + + put_string(b, "cylinder"); + if (volume) + put_milli(b, " vol=", volume, "l"); + put_pressure(b, cylinder->type.workingpressure, " workpressure=", "bar"); + show_utf8(b, " description=", description, ""); + strip_mb(b); + put_gasmix(b, &cylinder->gasmix); + put_pressure(b, cylinder->start, " start=", "bar"); + put_pressure(b, cylinder->end, " end=", "bar"); + if (cylinder->cylinder_use != OC_GAS) + put_format(b, " use=%s", cylinderuse_text[cylinder->cylinder_use]); + + put_string(b, "\n"); + } +} + +static void save_weightsystem_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_string(b, "weightsystem"); + put_milli(b, " weight=", grams, "kg"); + show_utf8(b, " description=", description, ""); + put_string(b, "\n"); + } +} + +static void save_dive_temperature(struct membuffer *b, struct dive *dive) +{ + if (dive->airtemp.mkelvin != dc_airtemp(&dive->dc)) + put_temperature(b, dive->airtemp, "airtemp ", "°C\n"); + if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) + put_temperature(b, dive->watertemp, "watertemp ", "°C\n"); +} + +static void save_depths(struct membuffer *b, struct divecomputer *dc) +{ + put_depth(b, dc->maxdepth, "maxdepth ", "m\n"); + put_depth(b, dc->meandepth, "meandepth ", "m\n"); +} + +static void save_temperatures(struct membuffer *b, struct divecomputer *dc) +{ + put_temperature(b, dc->airtemp, "airtemp ", "°C\n"); + put_temperature(b, dc->watertemp, "watertemp ", "°C\n"); +} + +static void save_airpressure(struct membuffer *b, struct divecomputer *dc) +{ + put_pressure(b, dc->surface_pressure, "surfacepressure ", "bar\n"); +} + +static void save_salinity(struct membuffer *b, struct divecomputer *dc) +{ + /* only save if we have a value that isn't the default of sea water */ + if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) + return; + put_salinity(b, dc->salinity, "salinity ", "g/l\n"); +} + +static void show_date(struct membuffer *b, timestamp_t when) +{ + struct tm tm; + + utc_mkdate(when, &tm); + + put_format(b, "date %04u-%02u-%02u\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); + put_format(b, "time %02u:%02u:%02u\n", + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void show_index(struct membuffer *b, int value, const char *pre, const char *post) +{ + if (value) + put_format(b, " %s%d%s", pre, value, post); +} + +/* + * Samples are saved as densely as possible while still being readable, + * since they are the bulk of the data. + * + * For parsing, look at the units to figure out what the numbers are. + */ +static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) +{ + put_format(b, "%3u:%02u", FRACTION(sample->time.seconds, 60)); + put_milli(b, " ", sample->depth.mm, "m"); + put_temperature(b, sample->temperature, " ", "°C"); + put_pressure(b, sample->cylinderpressure, " ", "bar"); + put_pressure(b, sample->o2cylinderpressure," o2pressure=","bar"); + + /* + * We only show sensor information for samples with pressure, and only if it + * changed from the previous sensor we showed. + */ + if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { + put_format(b, " sensor=%d", sample->sensor); + old->sensor = sample->sensor; + } + + /* the deco/ndl values are stored whenever they change */ + if (sample->ndl.seconds != old->ndl.seconds) { + put_format(b, " ndl=%u:%02u", FRACTION(sample->ndl.seconds, 60)); + old->ndl = sample->ndl; + } + if (sample->tts.seconds != old->tts.seconds) { + put_format(b, " tts=%u:%02u", FRACTION(sample->tts.seconds, 60)); + old->tts = sample->tts; + } + if (sample->in_deco != old->in_deco) { + put_format(b, " in_deco=%d", sample->in_deco ? 1 : 0); + old->in_deco = sample->in_deco; + } + if (sample->stoptime.seconds != old->stoptime.seconds) { + put_format(b, " stoptime=%u:%02u", FRACTION(sample->stoptime.seconds, 60)); + old->stoptime = sample->stoptime; + } + + if (sample->stopdepth.mm != old->stopdepth.mm) { + put_milli(b, " stopdepth=", sample->stopdepth.mm, "m"); + old->stopdepth = sample->stopdepth; + } + + if (sample->cns != old->cns) { + put_format(b, " cns=%u%%", sample->cns); + old->cns = sample->cns; + } + + if (sample->rbt.seconds) + put_format(b, " rbt=%u:%02u", FRACTION(sample->rbt.seconds, 60)); + + if (sample->o2sensor[0].mbar != old->o2sensor[0].mbar) { + put_milli(b, " sensor1=", sample->o2sensor[0].mbar, "bar"); + old->o2sensor[0] = sample->o2sensor[0]; + } + + if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { + put_milli(b, " sensor2=", sample->o2sensor[1].mbar, "bar"); + old->o2sensor[1] = sample->o2sensor[1]; + } + + if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { + put_milli(b, " sensor3=", sample->o2sensor[2].mbar, "bar"); + old->o2sensor[2] = sample->o2sensor[2]; + } + + if (sample->setpoint.mbar != old->setpoint.mbar) { + put_milli(b, " po2=", sample->setpoint.mbar, "bar"); + old->setpoint = sample->setpoint; + } + show_index(b, sample->heartbeat, "heartbeat=", ""); + show_index(b, sample->bearing.degrees, "bearing=", "°"); + put_format(b, "\n"); +} + +static void save_samples(struct membuffer *b, int nr, struct sample *s) +{ + struct sample dummy = {}; + + while (--nr >= 0) { + save_sample(b, s, &dummy); + s++; + } +} + +static void save_one_event(struct membuffer *b, struct event *ev) +{ + put_format(b, "event %d:%02d", FRACTION(ev->time.seconds, 60)); + show_index(b, ev->type, "type=", ""); + show_index(b, ev->flags, "flags=", ""); + show_index(b, ev->value, "value=", ""); + show_utf8(b, " name=", ev->name, ""); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder=", ""); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } + put_string(b, "\n"); +} + +static void save_events(struct membuffer *b, struct event *ev) +{ + while (ev) { + save_one_event(b, ev); + ev = ev->next; + } +} + +static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) +{ + show_utf8(b, "model ", dc->model, "\n"); + if (dc->deviceid) + put_format(b, "deviceid %08x\n", dc->deviceid); + if (dc->diveid) + put_format(b, "diveid %08x\n", dc->diveid); + if (dc->when && dc->when != dive->when) + show_date(b, dc->when); + if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) + put_duration(b, dc->duration, "duration ", "min\n"); + if (dc->divemode != OC) { + put_format(b, "dctype %s\n", divemode_text[dc->divemode]); + put_format(b, "numberofoxygensensors %d\n",dc->no_o2sensors); + } + + save_depths(b, dc); + save_temperatures(b, dc); + save_airpressure(b, dc); + save_salinity(b, dc); + put_duration(b, dc->surfacetime, "surfacetime ", "min\n"); + + save_extra_data(b, dc->extra_data); + save_events(b, dc->events); + save_samples(b, dc->samples, dc->sample); +} + +/* + * Note that we don't save the date and time or dive + * number: they are encoded in the filename. + */ +static void create_dive_buffer(struct dive *dive, struct membuffer *b) +{ + put_format(b, "duration %u:%02u min\n", FRACTION(dive->dc.duration.seconds, 60)); + SAVE("rating", rating); + SAVE("visibility", visibility); + cond_put_format(dive->tripflag == NO_TRIP, b, "notrip\n"); + save_tags(b, dive->tag_list); + cond_put_format(dive->dive_site_uuid && get_dive_site_by_uuid(dive->dive_site_uuid), + b, "divesiteid %08x\n", dive->dive_site_uuid); + if (verbose && dive->dive_site_uuid && !get_dive_site_by_uuid(dive->dive_site_uuid)) + fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); + save_overview(b, dive); + save_cylinder_info(b, dive); + save_weightsystem_info(b, dive); + save_dive_temperature(b, dive); +} + +static struct membuffer error_string_buffer = { 0 }; + +/* + * Note that the act of "getting" the error string + * buffer doesn't de-allocate the buffer, but it does + * set the buffer length to zero, so that any future + * error reports will overwrite the string rather than + * append to it. + */ +const char *get_error_string(void) +{ + const char *str; + + if (!error_string_buffer.len) + return ""; + str = mb_cstring(&error_string_buffer); + error_string_buffer.len = 0; + return str; +} + +int report_error(const char *fmt, ...) +{ + struct membuffer *buf = &error_string_buffer; + + /* Previous unprinted errors? Add a newline in between */ + if (buf->len) + put_bytes(buf, "\n", 1); + VA_BUF(buf, fmt); + mb_cstring(buf); + return -1; +} + +void report_message(const char *msg) +{ + (void)report_error("%s", msg); +} + +/* + * libgit2 has a "git_treebuilder" concept, but it's broken, and can not + * be used to do a flat tree (like the git "index") nor a recursive tree. + * Stupid. + * + * So we have to do that "keep track of recursive treebuilder entries" + * ourselves. We use 'git_treebuilder' for any regular files, and our own + * data structures for recursive trees. + * + * When finally writing it out, we traverse the subdirectories depth- + * first, writing them out, and then adding the written-out trees to + * the git_treebuilder they existed in. + */ +struct dir { + git_treebuilder *files; + struct dir *subdirs, *sibling; + char unique, name[1]; +}; + +static int tree_insert(git_treebuilder *dir, const char *name, int mkunique, git_oid *id, unsigned mode) +{ + int ret; + struct membuffer uniquename = { 0 }; + + if (mkunique && git_treebuilder_get(dir, name)) { + char hex[8]; + git_oid_nfmt(hex, 7, id); + hex[7] = 0; + put_format(&uniquename, "%s~%s", name, hex); + name = mb_cstring(&uniquename); + } + ret = git_treebuilder_insert(NULL, dir, name, id, mode); + free_buffer(&uniquename); + return ret; +} + +/* + * This does *not* make sure the new subdirectory doesn't + * alias some existing name. That is actually useful: you + * can create multiple directories with the same name, and + * set the "unique" flag, which will then append the SHA1 + * of the directory to the name when it is written. + */ +static struct dir *new_directory(git_repository *repo, struct dir *parent, struct membuffer *namebuf) +{ + struct dir *subdir; + const char *name = mb_cstring(namebuf); + int len = namebuf->len; + + subdir = malloc(sizeof(*subdir)+len); + + /* + * It starts out empty: no subdirectories of its own, + * and an empty treebuilder list of files. + */ + subdir->subdirs = NULL; + git_treebuilder_new(&subdir->files, repo, NULL); + memcpy(subdir->name, name, len); + subdir->unique = 0; + subdir->name[len] = 0; + + /* Add it to the list of subdirs of the parent */ + subdir->sibling = parent->subdirs; + parent->subdirs = subdir; + + return subdir; +} + +static struct dir *mktree(git_repository *repo, struct dir *dir, const char *fmt, ...) +{ + struct membuffer buf = { 0 }; + struct dir *subdir; + + VA_BUF(&buf, fmt); + for (subdir = dir->subdirs; subdir; subdir = subdir->sibling) { + if (subdir->unique) + continue; + if (strncmp(subdir->name, buf.buffer, buf.len)) + continue; + if (!subdir->name[buf.len]) + break; + } + if (!subdir) + subdir = new_directory(repo, dir, &buf); + free_buffer(&buf); + return subdir; +} + +/* + * The name of a dive is the date and the dive number (and possibly + * the uniqueness suffix). + * + * Note that the time of the dive may not be the same as the + * time of the directory structure it is created in: the dive + * might be part of a trip that straddles a month (or even a + * year). + * + * We do *not* want to use localized weekdays and cause peoples save + * formats to depend on their locale. + */ +static void create_dive_name(struct dive *dive, struct membuffer *name, struct tm *dirtm) +{ + struct tm tm; + static const char weekday[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + utc_mkdate(dive->when, &tm); + if (tm.tm_year != dirtm->tm_year) + put_format(name, "%04u-", tm.tm_year + 1900); + if (tm.tm_mon != dirtm->tm_mon) + put_format(name, "%02u-", tm.tm_mon+1); + + /* a colon is an illegal char in a file name on Windows - use an '=' instead */ + put_format(name, "%02u-%s-%02u=%02u=%02u", + tm.tm_mday, weekday[tm.tm_wday], + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +/* + * Write a membuffer to the git repo, and free it + */ +static int blob_insert(git_repository *repo, struct dir *tree, struct membuffer *b, const char *fmt, ...) +{ + int ret; + git_oid blob_id; + struct membuffer name = { 0 }; + + ret = git_blob_create_frombuffer(&blob_id, repo, b->buffer, b->len); + free_buffer(b); + if (ret) + return ret; + + VA_BUF(&name, fmt); + ret = tree_insert(tree->files, mb_cstring(&name), 1, &blob_id, GIT_FILEMODE_BLOB); + free_buffer(&name); + return ret; +} + +static int save_one_divecomputer(git_repository *repo, struct dir *tree, struct dive *dive, struct divecomputer *dc, int idx) +{ + int ret; + struct membuffer buf = { 0 }; + + save_dc(&buf, dive, dc); + ret = blob_insert(repo, tree, &buf, "Divecomputer%c%03u", idx ? '-' : 0, idx); + if (ret) + report_error("divecomputer tree insert failed"); + return ret; +} + +static int save_one_picture(git_repository *repo, struct dir *dir, struct picture *pic) +{ + int offset = pic->offset.seconds; + struct membuffer buf = { 0 }; + char sign = '+'; + unsigned h; + int error; + + show_utf8(&buf, "filename ", pic->filename, "\n"); + show_gps(&buf, pic->latitude, pic->longitude); + show_utf8(&buf, "hash ", pic->hash, "\n"); + + /* Picture loading will load even negative offsets.. */ + if (offset < 0) { + offset = -offset; + sign = '-'; + } + + /* Use full hh:mm:ss format to make it all sort nicely */ + h = offset / 3600; + offset -= h *3600; + error = blob_insert(repo, dir, &buf, "%c%02u=%02u=%02u", + sign, h, FRACTION(offset, 60)); +#if 0 + /* storing pictures into git was a mistake. This makes for HUGE git repositories */ + if (!error) { + /* next store the actual picture; we prefix all picture names + * with "PIC-" to make things easier on the parsing side */ + struct membuffer namebuf = { 0 }; + const char *localfn = local_file_path(pic); + put_format(&namebuf, "PIC-%s", pic->hash); + error = blob_insert_fromdisk(repo, dir, localfn, mb_cstring(&namebuf)); + free((void *)localfn); + } +#endif + return error; +} + +static int save_pictures(git_repository *repo, struct dir *dir, struct dive *dive) +{ + if (dive->picture_list) { + dir = mktree(repo, dir, "Pictures"); + FOR_EACH_PICTURE(dive) { + save_one_picture(repo, dir, picture); + } + } + return 0; +} + +static int save_one_dive(git_repository *repo, struct dir *tree, struct dive *dive, struct tm *tm, bool cached_ok) +{ + struct divecomputer *dc; + struct membuffer buf = { 0 }, name = { 0 }; + struct dir *subdir; + int ret, nr; + + /* Create dive directory */ + create_dive_name(dive, &name, tm); + + /* + * If the dive git ID is valid, we just create the whole directory + * with that ID + */ + if (cached_ok && dive_cache_is_valid(dive)) { + git_oid oid; + git_oid_fromraw(&oid, dive->git_id); + ret = tree_insert(tree->files, mb_cstring(&name), 1, + &oid, GIT_FILEMODE_TREE); + free_buffer(&name); + if (ret) + return report_error("cached dive tree insert failed"); + return 0; + } + + subdir = new_directory(repo, tree, &name); + subdir->unique = 1; + free_buffer(&name); + + create_dive_buffer(dive, &buf); + nr = dive->number; + ret = blob_insert(repo, subdir, &buf, + "Dive%c%d", nr ? '-' : 0, nr); + if (ret) + return report_error("dive save-file tree insert failed"); + + /* + * Save the dive computer data. If there is only one dive + * computer, use index 0 for that (which disables the index + * generation when naming it). + */ + dc = &dive->dc; + nr = dc->next ? 1 : 0; + do { + save_one_divecomputer(repo, subdir, dive, dc, nr++); + dc = dc->next; + } while (dc); + + /* Save the picture data, if any */ + save_pictures(repo, subdir, dive); + return 0; +} + +/* + * We'll mark the trip directories unique, so this does not + * need to be unique per se. It could be just "trip". But + * to make things a bit more user-friendly, we try to take + * the trip location into account. + * + * But no special characters, and no numbers (numbers in the + * name could be construed as a date). + * + * So we might end up with "02-Maui", and then the unique + * flag will make us write it out as "02-Maui~54b4" or + * similar. + */ +#define MAXTRIPNAME 15 +static void create_trip_name(dive_trip_t *trip, struct membuffer *name, struct tm *tm) +{ + put_format(name, "%02u-", tm->tm_mday); + if (trip->location) { + char ascii_loc[MAXTRIPNAME+1], *p = trip->location; + int i; + + for (i = 0; i < MAXTRIPNAME; ) { + char c = *p++; + switch (c) { + case 0: + case ',': + case '.': + break; + + case 'a' ... 'z': + case 'A' ... 'Z': + ascii_loc[i++] = c; + continue; + default: + continue; + } + break; + } + if (i > 1) { + put_bytes(name, ascii_loc, i); + return; + } + } + + /* No useful name? */ + put_string(name, "trip"); +} + +static int save_trip_description(git_repository *repo, struct dir *dir, dive_trip_t *trip, struct tm *tm) +{ + int ret; + git_oid blob_id; + struct membuffer desc = { 0 }; + + put_format(&desc, "date %04u-%02u-%02u\n", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + put_format(&desc, "time %02u:%02u:%02u\n", + tm->tm_hour, tm->tm_min, tm->tm_sec); + + show_utf8(&desc, "location ", trip->location, "\n"); + show_utf8(&desc, "notes ", trip->notes, "\n"); + + ret = git_blob_create_frombuffer(&blob_id, repo, desc.buffer, desc.len); + free_buffer(&desc); + if (ret) + return report_error("trip blob creation failed"); + ret = tree_insert(dir->files, "00-Trip", 0, &blob_id, GIT_FILEMODE_BLOB); + if (ret) + return report_error("trip description tree insert failed"); + return 0; +} + +static void verify_shared_date(timestamp_t when, struct tm *tm) +{ + struct tm tmp_tm; + + utc_mkdate(when, &tmp_tm); + if (tmp_tm.tm_year != tm->tm_year) { + tm->tm_year = -1; + tm->tm_mon = -1; + } + if (tmp_tm.tm_mon != tm->tm_mon) + tm->tm_mon = -1; +} + +#define MIN_TIMESTAMP (0) +#define MAX_TIMESTAMP (0x7fffffffffffffff) + +static int save_one_trip(git_repository *repo, struct dir *tree, dive_trip_t *trip, struct tm *tm, bool cached_ok) +{ + int i; + struct dive *dive; + struct dir *subdir; + struct membuffer name = { 0 }; + timestamp_t first, last; + + /* Create trip directory */ + create_trip_name(trip, &name, tm); + subdir = new_directory(repo, tree, &name); + subdir->unique = 1; + free_buffer(&name); + + /* Trip description file */ + save_trip_description(repo, subdir, trip, tm); + + /* Make sure we write out the dates to the dives consistently */ + first = MAX_TIMESTAMP; + last = MIN_TIMESTAMP; + for_each_dive(i, dive) { + if (dive->divetrip != trip) + continue; + if (dive->when < first) + first = dive->when; + if (dive->when > last) + last = dive->when; + } + verify_shared_date(first, tm); + verify_shared_date(last, tm); + + /* Save each dive in the directory */ + for_each_dive(i, dive) { + if (dive->divetrip == trip) + save_one_dive(repo, subdir, dive, tm, cached_ok); + } + + return 0; +} + +static void save_units(void *_b) +{ + struct membuffer *b =_b; + if (prefs.unit_system == METRIC) + put_string(b, "units METRIC\n"); + else if (prefs.unit_system == IMPERIAL) + put_string(b, "units IMPERIAL\n"); + else + put_format(b, "units PERSONALIZE %s %s %s %s %s %s", + prefs.units.length == METERS ? "METERS" : "FEET", + prefs.units.volume == LITER ? "LITER" : "CUFT", + prefs.units.pressure == BAR ? "BAR" : prefs.units.pressure == PSI ? "PSI" : "PASCAL", + prefs.units.temperature == CELSIUS ? "CELSIUS" : prefs.units.temperature == FAHRENHEIT ? "FAHRENHEIT" : "KELVIN", + prefs.units.weight == KG ? "KG" : "LBS", + prefs.units.vertical_speed_time == SECONDS ? "SECONDS" : "MINUTES"); +} + +static void save_userid(void *_b) +{ + struct membuffer *b = _b; + if (prefs.save_userid_local) + put_format(b, "userid %30s\n", prefs.userid); +} + +static void save_one_device(void *_b, const char *model, uint32_t deviceid, + const char *nickname, const char *serial, const char *firmware) +{ + struct membuffer *b = _b; + + if (nickname && !strcmp(model, nickname)) + nickname = NULL; + if (serial && !*serial) serial = NULL; + if (firmware && !*firmware) firmware = NULL; + if (nickname && !*nickname) nickname = NULL; + if (!nickname && !serial && !firmware) + return; + + show_utf8(b, "divecomputerid ", model, ""); + put_format(b, " deviceid=%08x", deviceid); + show_utf8(b, " serial=", serial, ""); + show_utf8(b, " firmware=", firmware, ""); + show_utf8(b, " nickname=", nickname, ""); + put_string(b, "\n"); +} + +static void save_settings(git_repository *repo, struct dir *tree) +{ + struct membuffer b = { 0 }; + + put_format(&b, "version %d\n", DATAFORMAT_VERSION); + save_userid(&b); + call_for_each_dc(&b, save_one_device, false); + cond_put_format(autogroup, &b, "autogroup\n"); + save_units(&b); + + blob_insert(repo, tree, &b, "00-Subsurface"); +} + +static void save_divesites(git_repository *repo, struct dir *tree) +{ + struct dir *subdir; + struct membuffer dirname = { 0 }; + put_format(&dirname, "01-Divesites"); + subdir = new_directory(repo, tree, &dirname); + + for (int i = 0; i < dive_site_table.nr; i++) { + struct membuffer b = { 0 }; + struct dive_site *ds = get_dive_site(i); + if (dive_site_is_empty(ds)) { + int j; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == ds->uuid) + d->dive_site_uuid = 0; + } + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } else if (ds->name && + (strncmp(ds->name, "Auto-created dive", 17) == 0 || + strncmp(ds->name, "New Dive", 8) == 0)) { + fprintf(stderr, "found an auto divesite %s\n", ds->name); + // these are the two default names for sites from + // the web service; if the site isn't used in any + // dive (really? you didn't rename it?), delete it + if (!is_dive_site_used(ds->uuid, false)) { + if (verbose) + fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } + } + struct membuffer site_file_name = { 0 }; + put_format(&site_file_name, "Site-%08x", ds->uuid); + show_utf8(&b, "name ", ds->name, "\n"); + show_utf8(&b, "description ", ds->description, "\n"); + show_utf8(&b, "notes ", ds->notes, "\n"); + show_gps(&b, ds->latitude, ds->longitude); + for (int j = 0; j < ds->taxonomy.nr; j++) { + struct taxonomy *t = &ds->taxonomy.category[j]; + if (t->category != TC_NONE && t->value) { + put_format(&b, "geo cat %d origin %d ", t->category, t->origin); + show_utf8(&b, "", t->value, "\n" ); + } + } + blob_insert(repo, subdir, &b, mb_cstring(&site_file_name)); + } +} + +static int create_git_tree(git_repository *repo, struct dir *root, bool select_only, bool cached_ok) +{ + int i; + struct dive *dive; + dive_trip_t *trip; + + save_settings(repo, root); + + save_divesites(repo, root); + + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + /* save the dives */ + int notify_increment = dive_table.nr > 10 ? dive_table.nr / 10 : 1; + int last_threshold = 0; + for_each_dive(i, dive) { + struct tm tm; + struct dir *tree; + char buf[] = "save dives x0%"; + + if (i / notify_increment > last_threshold) { + // notify of progress - we cover the range of 20..50 + last_threshold = i / notify_increment; + buf[11] = last_threshold + '0'; + git_storage_update_progress(20 + 3 * last_threshold, buf); + } + + trip = dive->divetrip; + + if (select_only) { + if (!dive->selected) + continue; + /* We don't save trips when doing selected dive saves */ + trip = NULL; + } + + /* Create the date-based hierarchy */ + utc_mkdate(trip ? trip->when : dive->when, &tm); + tree = mktree(repo, root, "%04d", tm.tm_year + 1900); + tree = mktree(repo, tree, "%02d", tm.tm_mon + 1); + + if (trip) { + /* Did we already save this trip? */ + if (trip->index) + continue; + trip->index = 1; + + /* Pass that new subdirectory in for save-trip */ + save_one_trip(repo, tree, trip, &tm, cached_ok); + continue; + } + + save_one_dive(repo, tree, dive, &tm, cached_ok); + } + return 0; +} + +/* + * See if we can find the parent ID that the git data came from + */ +static git_object *try_to_find_parent(const char *hex_id, git_repository *repo) +{ + git_oid object_id; + git_commit *commit; + + if (!hex_id) + return NULL; + if (git_oid_fromstr(&object_id, hex_id)) + return NULL; + if (git_commit_lookup(&commit, repo, &object_id)) + return NULL; + return (git_object *)commit; +} + +static int notify_cb(git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload) +{ + (void) baseline; + (void) target; + (void) workdir; + (void) payload; + (void) why; + report_error("File '%s' does not match in working tree", path); + return 0; /* Continue with checkout */ +} + +static git_tree *get_git_tree(git_repository *repo, git_object *parent) +{ + git_tree *tree; + if (!parent) + return NULL; + if (git_tree_lookup(&tree, repo, git_commit_tree_id((const git_commit *) parent))) + return NULL; + return tree; +} + +int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree) +{ + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + opts.checkout_strategy = GIT_CHECKOUT_SAFE; + opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_DIRTY; + opts.notify_cb = notify_cb; + opts.baseline = get_git_tree(repo, parent); + return git_checkout_tree(repo, (git_object *) tree, &opts); +} + +static int get_authorship(git_repository *repo, git_signature **authorp) +{ +#if LIBGIT2_VER_MAJOR || LIBGIT2_VER_MINOR >= 20 + if (git_signature_default(authorp, repo) == 0) + return 0; +#endif + /* Default name information, with potential OS overrides */ + struct user_info user = { + .name = "Subsurface", + .email = "subsurace@subsurface-divelog.org" + }; + + subsurface_user_info(&user); + + /* git_signature_default() is too recent */ + return git_signature_now(authorp, user.name, user.email); +} + +static void create_commit_message(struct membuffer *msg) +{ + int nr = dive_table.nr; + struct dive *dive = get_dive(nr-1); + + if (dive) { + dive_trip_t *trip = dive->divetrip; + const char *location = get_dive_location(dive) ? : "no location"; + struct divecomputer *dc = &dive->dc; + const char *sep = "\n"; + + if (dive->number) + nr = dive->number; + + put_format(msg, "dive %d: %s", nr, location); + if (trip && trip->location && *trip->location && strcmp(trip->location, location)) + put_format(msg, " (%s)", trip->location); + put_format(msg, "\n"); + do { + if (dc->model && *dc->model) { + put_format(msg, "%s%s", sep, dc->model); + sep = ", "; + } + } while ((dc = dc->next) != NULL); + put_format(msg, "\n"); + } + put_format(msg, "Created by subsurface %s\n", subsurface_user_agent()); +} + +static int create_new_commit(git_repository *repo, const char *remote, const char *branch, git_oid *tree_id) +{ + int ret; + git_reference *ref; + git_object *parent; + git_oid commit_id; + git_signature *author; + git_commit *commit; + git_tree *tree; + + ret = git_branch_lookup(&ref, repo, branch, GIT_BRANCH_LOCAL); + switch (ret) { + default: + return report_error("Bad branch '%s' (%s)", branch, strerror(errno)); + case GIT_EINVALIDSPEC: + return report_error("Invalid branch name '%s'", branch); + case GIT_ENOTFOUND: /* We'll happily create it */ + ref = NULL; + parent = try_to_find_parent(saved_git_id, repo); + break; + case 0: + if (git_reference_peel(&parent, ref, GIT_OBJ_COMMIT)) + return report_error("Unable to look up parent in branch '%s'", branch); + + if (saved_git_id) { + if (existing_filename) + fprintf(stderr, "existing filename %s\n", existing_filename); + const git_oid *id = git_commit_id((const git_commit *) parent); + /* if we are saving to the same git tree we got this from, let's make + * sure there is no confusion */ + if (same_string(existing_filename, remote) && git_oid_strcmp(id, saved_git_id)) + return report_error("The git branch does not match the git parent of the source"); + } + + /* all good */ + break; + } + + if (git_tree_lookup(&tree, repo, tree_id)) + return report_error("Could not look up newly created tree"); + + if (get_authorship(repo, &author)) + return report_error("No user name configuration in git repo"); + + /* If the parent commit has the same tree ID, do not create a new commit */ + if (parent && git_oid_equal(tree_id, git_commit_tree_id((const git_commit *) parent))) { + /* If the parent already came from the ref, the commit is already there */ + if (ref) + return 0; + /* Else we do want to create the new branch, but with the old commit */ + commit = (git_commit *) parent; + } else { + struct membuffer commit_msg = { 0 }; + + create_commit_message(&commit_msg); + if (git_commit_create_v(&commit_id, repo, NULL, author, author, NULL, mb_cstring(&commit_msg), tree, parent != NULL, parent)) + return report_error("Git commit create failed (%s)", strerror(errno)); + free_buffer(&commit_msg); + + if (git_commit_lookup(&commit, repo, &commit_id)) + return report_error("Could not look up newly created commit"); + } + + if (!ref) { + if (git_branch_create(&ref, repo, branch, commit, 0)) + return report_error("Failed to create branch '%s'", branch); + } + /* + * If it's a checked-out branch, try to also update the working + * tree and index. If that fails (dirty working tree or whatever), + * this is not technically a save error (we did save things in + * the object database), but it can cause extreme confusion, so + * warn about it. + */ + if (git_branch_is_head(ref) && !git_repository_is_bare(repo)) { + if (update_git_checkout(repo, parent, tree)) { + const git_error *err = giterr_last(); + const char *errstr = err ? err->message : strerror(errno); + report_error("Git branch '%s' is checked out, but worktree is dirty (%s)", + branch, errstr); + } + } + + if (git_reference_set_target(&ref, ref, &commit_id, "Subsurface save event")) + return report_error("Failed to update branch '%s'", branch); + set_git_id(&commit_id); + + git_signature_free(author); + + return 0; +} + +static int write_git_tree(git_repository *repo, struct dir *tree, git_oid *result) +{ + int ret; + struct dir *subdir; + + /* Write out our subdirectories, add them to the treebuilder, and free them */ + while ((subdir = tree->subdirs) != NULL) { + git_oid id; + + if (!write_git_tree(repo, subdir, &id)) + tree_insert(tree->files, subdir->name, subdir->unique, &id, GIT_FILEMODE_TREE); + tree->subdirs = subdir->sibling; + free(subdir); + }; + + /* .. write out the resulting treebuilder */ + ret = git_treebuilder_write(result, tree->files); + + /* .. and free the now useless treebuilder */ + git_treebuilder_free(tree->files); + + return ret; +} + +int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty) +{ + struct dir tree; + git_oid id; + bool cached_ok; + + if (verbose) + fprintf(stderr, "git storage: do git save\n"); + + if (!create_empty) // so we are actually saving the dives + git_storage_update_progress(19, "start git save"); + + /* + * Check if we can do the cached writes - we need to + * have the original git commit we loaded in the repo + */ + cached_ok = try_to_find_parent(saved_git_id, repo); + + /* Start with an empty tree: no subdirectories, no files */ + tree.name[0] = 0; + tree.subdirs = NULL; + if (git_treebuilder_new(&tree.files, repo, NULL)) + return report_error("git treebuilder failed"); + + if (!create_empty) + /* Populate our tree data structure */ + if (create_git_tree(repo, &tree, select_only, cached_ok)) + return -1; + + if (verbose) + fprintf(stderr, "git storage, write git tree\n"); + + if (write_git_tree(repo, &tree, &id)) + return report_error("git tree write failed"); + + /* And save the tree! */ + if (create_new_commit(repo, remote, branch, &id)) + return report_error("creating commit failed"); + + if (remote && prefs.cloud_background_sync) { + /* now sync the tree with the cloud server */ + if (strstr(remote, prefs.cloud_git_url)) { + return sync_with_remote(repo, remote, branch, RT_HTTPS); + } + } + return 0; +} + +int git_save_dives(struct git_repository *repo, const char *branch, const char *remote, bool select_only) +{ + int ret; + + if (repo == dummy_git_repository) + return report_error("Unable to open git repository '%s'", branch); + ret = do_git_save(repo, branch, remote, select_only, false); + git_repository_free(repo); + free((void *)branch); + return ret; +} diff --git a/core/save-html.c b/core/save-html.c new file mode 100644 index 000000000..2d0ea9cf3 --- /dev/null +++ b/core/save-html.c @@ -0,0 +1,559 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include "save-html.h" +#include "qthelperfromc.h" +#include "gettext.h" +#include "stdio.h" + +void write_attribute(struct membuffer *b, const char *att_name, const char *value, const char *separator) +{ + if (!value) + value = "--"; + put_format(b, "\"%s\":\"", att_name); + put_HTML_quoted(b, value); + put_format(b, "\"%s", separator); +} + +void save_photos(struct membuffer *b, const char *photos_dir, struct dive *dive) +{ + struct picture *pic = dive->picture_list; + + if (!pic) + return; + + char *separator = "\"photos\":["; + do { + put_string(b, separator); + separator = ", "; + char *fname = get_file_name(local_file_path(pic)); + put_format(b, "{\"filename\":\"%s\"}", fname); + copy_image_and_overwrite(local_file_path(pic), photos_dir, fname); + free(fname); + pic = pic->next; + } while (pic); + put_string(b, "],"); +} + +void write_divecomputers(struct membuffer *b, struct dive *dive) +{ + put_string(b, "\"divecomputers\":["); + struct divecomputer *dc; + char *separator = ""; + for_each_dc (dive, dc) { + put_string(b, separator); + separator = ", "; + put_format(b, "{"); + write_attribute(b, "model", dc->model, ", "); + if (dc->deviceid) + put_format(b, "\"deviceid\":\"%08x\", ", dc->deviceid); + else + put_string(b, "\"deviceid\":\"--\", "); + if (dc->diveid) + put_format(b, "\"diveid\":\"%08x\" ", dc->diveid); + else + put_string(b, "\"diveid\":\"--\" "); + put_format(b, "}"); + } + put_string(b, "],"); +} + +void write_dive_status(struct membuffer *b, struct dive *dive) +{ + put_format(b, "\"sac\":\"%d\",", dive->sac); + put_format(b, "\"otu\":\"%d\",", dive->otu); + put_format(b, "\"cns\":\"%d\",", dive->cns); +} + +void put_HTML_bookmarks(struct membuffer *b, struct dive *dive) +{ + struct event *ev = dive->dc.events; + + if (!ev) + return; + + char *separator = "\"events\":["; + do { + put_string(b, separator); + separator = ", "; + put_format(b, "{\"name\":\"%s\",", ev->name); + put_format(b, "\"value\":\"%d\",", ev->value); + put_format(b, "\"type\":\"%d\",", ev->type); + put_format(b, "\"time\":\"%d\"}", ev->time.seconds); + ev = ev->next; + } while (ev); + put_string(b, "],"); +} + +static void put_weightsystem_HTML(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + + put_string(b, "\"Weights\":["); + + char *separator = ""; + + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_string(b, separator); + separator = ", "; + put_string(b, "{"); + put_HTML_weight_units(b, grams, "\"weight\":\"", "\","); + write_attribute(b, "description", description, " "); + put_string(b, "}"); + } + put_string(b, "],"); +} + +static void put_cylinder_HTML(struct membuffer *b, struct dive *dive) +{ + int i, nr; + char *separator = "\"Cylinders\":["; + nr = nr_cylinders(dive); + + if (!nr) + put_string(b, separator); + + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + put_format(b, "%s{", separator); + separator = ", "; + write_attribute(b, "Type", cylinder->type.description, ", "); + if (cylinder->type.size.mliter) { + int volume = cylinder->type.size.mliter; + if (prefs.units.volume == CUFT && cylinder->type.workingpressure.mbar) + volume *= bar_to_atm(cylinder->type.workingpressure.mbar / 1000.0); + put_HTML_volume_units(b, volume, "\"Size\":\"", " \", "); + } else { + write_attribute(b, "Size", "--", ", "); + } + put_HTML_pressure_units(b, cylinder->type.workingpressure, "\"WPressure\":\"", " \", "); + + if (cylinder->start.mbar) { + put_HTML_pressure_units(b, cylinder->start, "\"SPressure\":\"", " \", "); + } else { + write_attribute(b, "SPressure", "--", ", "); + } + + if (cylinder->end.mbar) { + put_HTML_pressure_units(b, cylinder->end, "\"EPressure\":\"", " \", "); + } else { + write_attribute(b, "EPressure", "--", ", "); + } + + if (cylinder->gasmix.o2.permille) { + put_format(b, "\"O2\":\"%u.%u%%\",", FRACTION(cylinder->gasmix.o2.permille, 10)); + put_format(b, "\"He\":\"%u.%u%%\"", FRACTION(cylinder->gasmix.he.permille, 10)); + } else { + write_attribute(b, "O2", "Air", ""); + } + + put_string(b, "}"); + } + + put_string(b, "],"); +} + + +void put_HTML_samples(struct membuffer *b, struct dive *dive) +{ + int i; + put_format(b, "\"maxdepth\":%d,", dive->dc.maxdepth.mm); + put_format(b, "\"duration\":%d,", dive->dc.duration.seconds); + struct sample *s = dive->dc.sample; + + if (!dive->dc.samples) + return; + + char *separator = "\"samples\":["; + for (i = 0; i < dive->dc.samples; i++) { + put_format(b, "%s[%d,%d,%d,%d]", separator, s->time.seconds, s->depth.mm, s->cylinderpressure.mbar, s->temperature.mkelvin); + separator = ", "; + s++; + } + put_string(b, "],"); +} + +void put_HTML_coordinates(struct membuffer *b, struct dive *dive) +{ + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) + return; + degrees_t latitude = ds->latitude; + degrees_t longitude = ds->longitude; + + //don't put coordinates if in (0,0) + if (!latitude.udeg && !longitude.udeg) + return; + + put_string(b, "\"coordinates\":{"); + put_degrees(b, latitude, "\"lat\":\"", "\","); + put_degrees(b, longitude, "\"lon\":\"", "\""); + put_string(b, "},"); +} + +void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + struct tm tm; + utc_mkdate(dive->when, &tm); + put_format(b, "%s%04u-%02u-%02u%s", pre, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, post); +} + +void put_HTML_quoted(struct membuffer *b, const char *text) +{ + int is_html = 1, is_attribute = 1; + put_quoted(b, text, is_attribute, is_html); +} + +void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + put_string(b, pre); + if (dive->notes) { + put_HTML_quoted(b, dive->notes); + } else { + put_string(b, "--"); + } + put_string(b, post); +} + +void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!pressure.mbar) { + put_format(b, "%s%s", pre, post); + return; + } + + value = get_pressure_units(pressure.mbar, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post) +{ + const char *unit; + double value; + int frac; + + value = get_volume_units(ml, &frac, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post) +{ + const char *unit; + double value; + int frac; + + value = get_weight_units(grams, &frac, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + struct tm tm; + utc_mkdate(dive->when, &tm); + put_format(b, "%s%02u:%02u:%02u%s", pre, tm.tm_hour, tm.tm_min, tm.tm_sec, post); +} + +void put_HTML_depth(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + const char *unit; + double value; + struct units *units_p = get_units(); + + if (!dive->maxdepth.mm) { + put_format(b, "%s--%s", pre, post); + return; + } + value = get_depth_units(dive->maxdepth.mm, NULL, &unit); + + switch (units_p->length) { + case METERS: + default: + put_format(b, "%s%.1f %s%s", pre, value, unit, post); + break; + case FEET: + put_format(b, "%s%.0f %s%s", pre, value, unit, post); + break; + } +} + +void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!dive->airtemp.mkelvin) { + put_format(b, "%s--%s", pre, post); + return; + } + value = get_temp_units(dive->airtemp.mkelvin, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!dive->watertemp.mkelvin) { + put_format(b, "%s--%s", pre, post); + return; + } + value = get_temp_units(dive->watertemp.mkelvin, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_tags(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + put_string(b, pre); + struct tag_entry *tag = dive->tag_list; + + if (!tag) + put_string(b, "[\"--\""); + + char *separator = "["; + while (tag) { + put_format(b, "%s\"", separator); + separator = ", "; + put_HTML_quoted(b, tag->tag->name); + put_string(b, "\""); + tag = tag->next; + } + put_string(b, "]"); + put_string(b, post); +} + +/* if exporting list_only mode, we neglect exporting the samples, bookmarks and cylinders */ +void write_one_dive(struct membuffer *b, struct dive *dive, const char *photos_dir, int *dive_no, const bool list_only) +{ + put_string(b, "{"); + put_format(b, "\"number\":%d,", *dive_no); + put_format(b, "\"subsurface_number\":%d,", dive->number); + put_HTML_date(b, dive, "\"date\":\"", "\","); + put_HTML_time(b, dive, "\"time\":\"", "\","); + write_attribute(b, "location", get_dive_location(dive), ", "); + put_HTML_coordinates(b, dive); + put_format(b, "\"rating\":%d,", dive->rating); + put_format(b, "\"visibility\":%d,", dive->visibility); + put_format(b, "\"dive_duration\":\"%u:%02u min\",", + FRACTION(dive->duration.seconds, 60)); + put_string(b, "\"temperature\":{"); + put_HTML_airtemp(b, dive, "\"air\":\"", "\","); + put_HTML_watertemp(b, dive, "\"water\":\"", "\""); + put_string(b, " },"); + write_attribute(b, "buddy", dive->buddy, ", "); + write_attribute(b, "divemaster", dive->divemaster, ", "); + write_attribute(b, "suit", dive->suit, ", "); + put_HTML_tags(b, dive, "\"tags\":", ","); + if (!list_only) { + put_cylinder_HTML(b, dive); + put_weightsystem_HTML(b, dive); + put_HTML_samples(b, dive); + put_HTML_bookmarks(b, dive); + write_dive_status(b, dive); + if (photos_dir && strcmp(photos_dir, "")) + save_photos(b, photos_dir, dive); + write_divecomputers(b, dive); + } + put_HTML_notes(b, dive, "\"notes\":\"", "\""); + put_string(b, "}\n"); + (*dive_no)++; +} + +void write_no_trip(struct membuffer *b, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) +{ + int i; + struct dive *dive; + char *separator = ""; + bool found_sel_dive = 0; + + for_each_dive (i, dive) { + // write dive if it doesn't belong to any trip and the dive is selected + // or we are in exporting all dives mode. + if (!dive->divetrip && (dive->selected || !selected_only)) { + if (!found_sel_dive) { + put_format(b, "%c{", *sep); + (*sep) = ','; + put_format(b, "\"name\":\"Other\","); + put_format(b, "\"dives\":["); + found_sel_dive = 1; + } + put_string(b, separator); + separator = ", "; + write_one_dive(b, dive, photos_dir, dive_no, list_only); + } + } + if (found_sel_dive) + put_format(b, "]}\n\n"); +} + +void write_trip(struct membuffer *b, dive_trip_t *trip, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) +{ + struct dive *dive; + char *separator = ""; + bool found_sel_dive = 0; + + for (dive = trip->dives; dive != NULL; dive = dive->next) { + if (!dive->selected && selected_only) + continue; + + // save trip if found at least one selected dive. + if (!found_sel_dive) { + found_sel_dive = 1; + put_format(b, "%c {", *sep); + (*sep) = ','; + put_format(b, "\"name\":\"%s\",", trip->location); + put_format(b, "\"dives\":["); + } + put_string(b, separator); + separator = ", "; + write_one_dive(b, dive, photos_dir, dive_no, list_only); + } + + // close the trip object if contain dives. + if (found_sel_dive) + put_format(b, "]}\n\n"); +} + +void write_trips(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) +{ + int i, dive_no = 0; + struct dive *dive; + dive_trip_t *trip; + char sep_ = ' '; + char *sep = &sep_; + + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + for_each_dive (i, dive) { + trip = dive->divetrip; + + /*Continue if the dive have no trips or we have seen this trip before*/ + if (!trip || trip->index) + continue; + + /* We haven't seen this trip before - save it and all dives */ + trip->index = 1; + write_trip(b, trip, &dive_no, selected_only, photos_dir, list_only, sep); + } + + /*Save all remaining trips into Others*/ + write_no_trip(b, &dive_no, selected_only, photos_dir, list_only, sep); +} + +void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) +{ + put_string(b, "trips=["); + write_trips(b, photos_dir, selected_only, list_only); + put_string(b, "]"); +} + +void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only) +{ + FILE *f; + + struct membuffer buf = { 0 }; + export_list(&buf, photos_dir, selected_only, list_only); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} + +void export_translation(const char *file_name) +{ + FILE *f; + + struct membuffer buf = { 0 }; + struct membuffer *b = &buf; + + //export translated words here + put_format(b, "translate={"); + + //Dive list view + write_attribute(b, "Number", translate("gettextFromC", "Number"), ", "); + write_attribute(b, "Date", translate("gettextFromC", "Date"), ", "); + write_attribute(b, "Time", translate("gettextFromC", "Time"), ", "); + write_attribute(b, "Location", translate("gettextFromC", "Location"), ", "); + write_attribute(b, "Air_Temp", translate("gettextFromC", "Air temp."), ", "); + write_attribute(b, "Water_Temp", translate("gettextFromC", "Water temp."), ", "); + write_attribute(b, "dives", translate("gettextFromC", "Dives"), ", "); + write_attribute(b, "Expand_All", translate("gettextFromC", "Expand all"), ", "); + write_attribute(b, "Collapse_All", translate("gettextFromC", "Collapse all"), ", "); + write_attribute(b, "trips", translate("gettextFromC", "Trips"), ", "); + write_attribute(b, "Statistics", translate("gettextFromC", "Statistics"), ", "); + write_attribute(b, "Advanced_Search", translate("gettextFromC", "Advanced search"), ", "); + + //Dive expanded view + write_attribute(b, "Rating", translate("gettextFromC", "Rating"), ", "); + write_attribute(b, "Visibility", translate("gettextFromC", "Visibility"), ", "); + write_attribute(b, "Duration", translate("gettextFromC", "Duration"), ", "); + write_attribute(b, "DiveMaster", translate("gettextFromC", "Divemaster"), ", "); + write_attribute(b, "Buddy", translate("gettextFromC", "Buddy"), ", "); + write_attribute(b, "Suit", translate("gettextFromC", "Suit"), ", "); + write_attribute(b, "Tags", translate("gettextFromC", "Tags"), ", "); + write_attribute(b, "Notes", translate("gettextFromC", "Notes"), ", "); + write_attribute(b, "Show_more_details", translate("gettextFromC", "Show more details"), ", "); + + //Yearly statistics view + write_attribute(b, "Yearly_statistics", translate("gettextFromC", "Yearly statistics"), ", "); + write_attribute(b, "Year", translate("gettextFromC", "Year"), ", "); + write_attribute(b, "Total_Time", translate("gettextFromC", "Total time"), ", "); + write_attribute(b, "Average_Time", translate("gettextFromC", "Average time"), ", "); + write_attribute(b, "Shortest_Time", translate("gettextFromC", "Shortest time"), ", "); + write_attribute(b, "Longest_Time", translate("gettextFromC", "Longest time"), ", "); + write_attribute(b, "Average_Depth", translate("gettextFromC", "Average depth"), ", "); + write_attribute(b, "Min_Depth", translate("gettextFromC", "Min. depth"), ", "); + write_attribute(b, "Max_Depth", translate("gettextFromC", "Max. depth"), ", "); + write_attribute(b, "Average_SAC", translate("gettextFromC", "Average SAC"), ", "); + write_attribute(b, "Min_SAC", translate("gettextFromC", "Min. SAC"), ", "); + write_attribute(b, "Max_SAC", translate("gettextFromC", "Max. SAC"), ", "); + write_attribute(b, "Average_Temp", translate("gettextFromC", "Average temp."), ", "); + write_attribute(b, "Min_Temp", translate("gettextFromC", "Min. temp."), ", "); + write_attribute(b, "Max_Temp", translate("gettextFromC", "Max. temp."), ", "); + write_attribute(b, "Back_to_List", translate("gettextFromC", "Back to list"), ", "); + + //dive detailed view + write_attribute(b, "Dive_No", translate("gettextFromC", "Dive No."), ", "); + write_attribute(b, "Dive_profile", translate("gettextFromC", "Dive profile"), ", "); + write_attribute(b, "Dive_information", translate("gettextFromC", "Dive information"), ", "); + write_attribute(b, "Dive_equipment", translate("gettextFromC", "Dive equipment"), ", "); + write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); + write_attribute(b, "Size", translate("gettextFromC", "Size"), ", "); + write_attribute(b, "Work_Pressure", translate("gettextFromC", "Work pressure"), ", "); + write_attribute(b, "Start_Pressure", translate("gettextFromC", "Start pressure"), ", "); + write_attribute(b, "End_Pressure", translate("gettextFromC", "End pressure"), ", "); + write_attribute(b, "Gas", translate("gettextFromC", "Gas"), ", "); + write_attribute(b, "Weight", translate("gettextFromC", "Weight"), ", "); + write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); + write_attribute(b, "Events", translate("gettextFromC", "Events"), ", "); + write_attribute(b, "Name", translate("gettextFromC", "Name"), ", "); + write_attribute(b, "Value", translate("gettextFromC", "Value"), ", "); + write_attribute(b, "Coordinates", translate("gettextFromC", "Coordinates"), ", "); + write_attribute(b, "Dive_Status", translate("gettextFromC", "Dive status"), " "); + + put_format(b, "}"); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/core/save-html.h b/core/save-html.h new file mode 100644 index 000000000..13bb102b1 --- /dev/null +++ b/core/save-html.h @@ -0,0 +1,31 @@ +#ifndef HTML_SAVE_H +#define HTML_SAVE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "dive.h" +#include "membuffer.h" + +void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_depth(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_quoted(struct membuffer *b, const char *text); +void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post); +void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post); +void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post); + +void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only); +void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only); + +void export_translation(const char *file_name); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/save-xml.c b/core/save-xml.c new file mode 100644 index 000000000..2335637e8 --- /dev/null +++ b/core/save-xml.c @@ -0,0 +1,749 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <time.h> +#include <unistd.h> +#include <fcntl.h> + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "strndup.h" +#include "git-access.h" +#include "qthelperfromc.h" + +/* + * We're outputting utf8 in xml. + * We need to quote the characters <, >, &. + * + * Technically I don't think we'd necessarily need to quote the control + * characters, but at least libxml2 doesn't like them. It doesn't even + * allow them quoted. So we just skip them and replace them with '?'. + * + * If we do this for attributes, we need to quote the quotes we use too. + */ +static void quote(struct membuffer *b, const char *text, int is_attribute) +{ + int is_html = 0; + put_quoted(b, text, is_attribute, is_html); +} + +static void show_utf8(struct membuffer *b, const char *text, const char *pre, const char *post, int is_attribute) +{ + int len; + char *cleaned; + + if (!text) + return; + /* remove leading and trailing space */ + /* We need to combine isascii() with isspace(), + * because we can only trust isspace() with 7-bit ascii, + * on windows for example */ + while (isascii(*text) && isspace(*text)) + text++; + len = strlen(text); + if (!len) + return; + while (len && isascii(text[len - 1]) && isspace(text[len - 1])) + len--; + cleaned = strndup(text, len); + put_string(b, pre); + quote(b, cleaned, is_attribute); + put_string(b, post); + free(cleaned); +} + +static void save_depths(struct membuffer *b, struct divecomputer *dc) +{ + /* What's the point of this dive entry again? */ + if (!dc->maxdepth.mm && !dc->meandepth.mm) + return; + + put_string(b, " <depth"); + put_depth(b, dc->maxdepth, " max='", " m'"); + put_depth(b, dc->meandepth, " mean='", " m'"); + put_string(b, " />\n"); +} + +static void save_dive_temperature(struct membuffer *b, struct dive *dive) +{ + if (!dive->airtemp.mkelvin && !dive->watertemp.mkelvin) + return; + if (dive->airtemp.mkelvin == dc_airtemp(&dive->dc) && dive->watertemp.mkelvin == dc_watertemp(&dive->dc)) + return; + + put_string(b, " <divetemperature"); + if (dive->airtemp.mkelvin != dc_airtemp(&dive->dc)) + put_temperature(b, dive->airtemp, " air='", " C'"); + if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) + put_temperature(b, dive->watertemp, " water='", " C'"); + put_string(b, "/>\n"); +} + +static void save_temperatures(struct membuffer *b, struct divecomputer *dc) +{ + if (!dc->airtemp.mkelvin && !dc->watertemp.mkelvin) + return; + put_string(b, " <temperature"); + put_temperature(b, dc->airtemp, " air='", " C'"); + put_temperature(b, dc->watertemp, " water='", " C'"); + put_string(b, " />\n"); +} + +static void save_airpressure(struct membuffer *b, struct divecomputer *dc) +{ + if (!dc->surface_pressure.mbar) + return; + put_string(b, " <surface"); + put_pressure(b, dc->surface_pressure, " pressure='", " bar'"); + put_string(b, " />\n"); +} + +static void save_salinity(struct membuffer *b, struct divecomputer *dc) +{ + /* only save if we have a value that isn't the default of sea water */ + if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) + return; + put_string(b, " <water"); + put_salinity(b, dc->salinity, " salinity='", " g/l'"); + put_string(b, " />\n"); +} + +static void save_overview(struct membuffer *b, struct dive *dive) +{ + show_utf8(b, dive->divemaster, " <divemaster>", "</divemaster>\n", 0); + show_utf8(b, dive->buddy, " <buddy>", "</buddy>\n", 0); + show_utf8(b, dive->notes, " <notes>", "</notes>\n", 0); + show_utf8(b, dive->suit, " <suit>", "</suit>\n", 0); +} + +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2='%u.%u%%'", FRACTION(o2, 10)); + if (he) + put_format(b, " he='%u.%u%%'", FRACTION(he, 10)); + } +} + +static void save_cylinder_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_cylinders(dive); + + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + int volume = cylinder->type.size.mliter; + const char *description = cylinder->type.description; + + put_format(b, " <cylinder"); + if (volume) + put_milli(b, " size='", volume, " l'"); + put_pressure(b, cylinder->type.workingpressure, " workpressure='", " bar'"); + show_utf8(b, description, " description='", "'", 1); + put_gasmix(b, &cylinder->gasmix); + put_pressure(b, cylinder->start, " start='", " bar'"); + put_pressure(b, cylinder->end, " end='", " bar'"); + if (cylinder->cylinder_use != OC_GAS) + show_utf8(b, cylinderuse_text[cylinder->cylinder_use], " use='", "'", 1); + put_format(b, " />\n"); + } +} + +static void save_weightsystem_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_format(b, " <weightsystem"); + put_milli(b, " weight='", grams, " kg'"); + show_utf8(b, description, " description='", "'", 1); + put_format(b, " />\n"); + } +} + +static void show_index(struct membuffer *b, int value, const char *pre, const char *post) +{ + if (value) + put_format(b, " %s%d%s", pre, value, post); +} + +static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) +{ + put_format(b, " <sample time='%u:%02u min'", FRACTION(sample->time.seconds, 60)); + put_milli(b, " depth='", sample->depth.mm, " m'"); + if (sample->temperature.mkelvin && sample->temperature.mkelvin != old->temperature.mkelvin) { + put_temperature(b, sample->temperature, " temp='", " C'"); + old->temperature = sample->temperature; + } + put_pressure(b, sample->cylinderpressure, " pressure='", " bar'"); + put_pressure(b, sample->o2cylinderpressure, " o2pressure='", " bar'"); + + /* + * We only show sensor information for samples with pressure, and only if it + * changed from the previous sensor we showed. + */ + if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { + put_format(b, " sensor='%d'", sample->sensor); + old->sensor = sample->sensor; + } + + /* the deco/ndl values are stored whenever they change */ + if (sample->ndl.seconds != old->ndl.seconds) { + put_format(b, " ndl='%u:%02u min'", FRACTION(sample->ndl.seconds, 60)); + old->ndl = sample->ndl; + } + if (sample->tts.seconds != old->tts.seconds) { + put_format(b, " tts='%u:%02u min'", FRACTION(sample->tts.seconds, 60)); + old->tts = sample->tts; + } + if (sample->rbt.seconds) + put_format(b, " rbt='%u:%02u min'", FRACTION(sample->rbt.seconds, 60)); + if (sample->in_deco != old->in_deco) { + put_format(b, " in_deco='%d'", sample->in_deco ? 1 : 0); + old->in_deco = sample->in_deco; + } + if (sample->stoptime.seconds != old->stoptime.seconds) { + put_format(b, " stoptime='%u:%02u min'", FRACTION(sample->stoptime.seconds, 60)); + old->stoptime = sample->stoptime; + } + + if (sample->stopdepth.mm != old->stopdepth.mm) { + put_milli(b, " stopdepth='", sample->stopdepth.mm, " m'"); + old->stopdepth = sample->stopdepth; + } + + if (sample->cns != old->cns) { + put_format(b, " cns='%u%%'", sample->cns); + old->cns = sample->cns; + } + + if ((sample->o2sensor[0].mbar) && (sample->o2sensor[0].mbar != old->o2sensor[0].mbar)) { + put_milli(b, " sensor1='", sample->o2sensor[0].mbar, " bar'"); + old->o2sensor[0] = sample->o2sensor[0]; + } + + if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { + put_milli(b, " sensor2='", sample->o2sensor[1].mbar, " bar'"); + old->o2sensor[1] = sample->o2sensor[1]; + } + + if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { + put_milli(b, " sensor3='", sample->o2sensor[2].mbar, " bar'"); + old->o2sensor[2] = sample->o2sensor[2]; + } + + if (sample->setpoint.mbar != old->setpoint.mbar) { + put_milli(b, " po2='", sample->setpoint.mbar, " bar'"); + old->setpoint = sample->setpoint; + } + show_index(b, sample->heartbeat, "heartbeat='", "'"); + show_index(b, sample->bearing.degrees, "bearing='", "'"); + put_format(b, " />\n"); +} + +static void save_one_event(struct membuffer *b, struct event *ev) +{ + put_format(b, " <event time='%d:%02d min'", FRACTION(ev->time.seconds, 60)); + show_index(b, ev->type, "type='", "'"); + show_index(b, ev->flags, "flags='", "'"); + show_index(b, ev->value, "value='", "'"); + show_utf8(b, ev->name, " name='", "'", 1); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder='", "'"); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } + put_format(b, " />\n"); +} + + +static void save_events(struct membuffer *b, struct event *ev) +{ + while (ev) { + save_one_event(b, ev); + ev = ev->next; + } +} + +static void save_tags(struct membuffer *b, struct tag_entry *entry) +{ + if (entry) { + const char *sep = " tags='"; + do { + struct divetag *tag = entry->tag; + put_string(b, sep); + /* If the tag has been translated, write the source to the xml file */ + quote(b, tag->source ?: tag->name, 1); + sep = ", "; + } while ((entry = entry->next) != NULL); + put_string(b, "'"); + } +} + +static void save_extra_data(struct membuffer *b, struct extra_data *ed) +{ + while (ed) { + if (ed->key && ed->value) { + put_string(b, " <extradata"); + show_utf8(b, ed->key, " key='", "'", 1); + show_utf8(b, ed->value, " value='", "'", 1); + put_string(b, " />\n"); + } + ed = ed->next; + } +} + +static void show_date(struct membuffer *b, timestamp_t when) +{ + struct tm tm; + + utc_mkdate(when, &tm); + + put_format(b, " date='%04u-%02u-%02u'", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); + put_format(b, " time='%02u:%02u:%02u'", + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void save_samples(struct membuffer *b, int nr, struct sample *s) +{ + struct sample dummy = {}; + + while (--nr >= 0) { + save_sample(b, s, &dummy); + s++; + } +} + +static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) +{ + put_format(b, " <divecomputer"); + show_utf8(b, dc->model, " model='", "'", 1); + if (dc->deviceid) + put_format(b, " deviceid='%08x'", dc->deviceid); + if (dc->diveid) + put_format(b, " diveid='%08x'", dc->diveid); + if (dc->when && dc->when != dive->when) + show_date(b, dc->when); + if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) + put_duration(b, dc->duration, " duration='", " min'"); + if (dc->divemode != OC) { + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) + if (dc->divemode == i) + show_utf8(b, divemode_text[i], " dctype='", "'", 1); + if (dc->no_o2sensors) + put_format(b," no_o2sensors='%d'", dc->no_o2sensors); + } + put_format(b, ">\n"); + save_depths(b, dc); + save_temperatures(b, dc); + save_airpressure(b, dc); + save_salinity(b, dc); + put_duration(b, dc->surfacetime, " <surfacetime>", " min</surfacetime>\n"); + save_extra_data(b, dc->extra_data); + save_events(b, dc->events); + save_samples(b, dc->samples, dc->sample); + + put_format(b, " </divecomputer>\n"); +} + +static void save_picture(struct membuffer *b, struct picture *pic) +{ + put_string(b, " <picture filename='"); + put_quoted(b, pic->filename, true, false); + put_string(b, "'"); + if (pic->offset.seconds) { + int offset = pic->offset.seconds; + char sign = '+'; + if (offset < 0) { + sign = '-'; + offset = -offset; + } + put_format(b, " offset='%c%u:%02u min'", sign, FRACTION(offset, 60)); + } + if (pic->latitude.udeg || pic->longitude.udeg) { + put_degrees(b, pic->latitude, " gps='", " "); + put_degrees(b, pic->longitude, "", "'"); + } + if (hashstring(pic->filename)) + put_format(b, " hash='%s'", hashstring(pic->filename)); + + put_string(b, "/>\n"); +} + +void save_one_dive_to_mb(struct membuffer *b, struct dive *dive) +{ + struct divecomputer *dc; + + put_string(b, "<dive"); + if (dive->number) + put_format(b, " number='%d'", dive->number); + if (dive->tripflag == NO_TRIP) + put_format(b, " tripflag='NOTRIP'"); + if (dive->rating) + put_format(b, " rating='%d'", dive->rating); + if (dive->visibility) + put_format(b, " visibility='%d'", dive->visibility); + save_tags(b, dive->tag_list); + if (dive->dive_site_uuid) { + if (get_dive_site_by_uuid(dive->dive_site_uuid) != NULL) + put_format(b, " divesiteid='%8x'", dive->dive_site_uuid); + else if (verbose) + fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); + } + show_date(b, dive->when); + put_format(b, " duration='%u:%02u min'>\n", + FRACTION(dive->dc.duration.seconds, 60)); + save_overview(b, dive); + save_cylinder_info(b, dive); + save_weightsystem_info(b, dive); + save_dive_temperature(b, dive); + /* Save the dive computer data */ + for_each_dc(dive, dc) + save_dc(b, dive, dc); + FOR_EACH_PICTURE(dive) + save_picture(b, picture); + put_format(b, "</dive>\n"); +} + +int save_dive(FILE *f, struct dive *dive) +{ + struct membuffer buf = { 0 }; + + save_one_dive_to_mb(&buf, dive); + flush_buffer(&buf, f); + /* Error handling? */ + return 0; +} + +static void save_trip(struct membuffer *b, dive_trip_t *trip) +{ + int i; + struct dive *dive; + + put_format(b, "<trip"); + show_date(b, trip->when); + show_utf8(b, trip->location, " location=\'", "\'", 1); + put_format(b, ">\n"); + show_utf8(b, trip->notes, "<notes>", "</notes>\n", 0); + + /* + * Incredibly cheesy: we want to save the dives sorted, and they + * are sorted in the dive array.. So instead of using the dive + * list in the trip, we just traverse the global dive array and + * check the divetrip pointer.. + */ + for_each_dive(i, dive) { + if (dive->divetrip == trip) + save_one_dive_to_mb(b, dive); + } + + put_format(b, "</trip>\n"); +} + +static void save_one_device(void *_f, const char *model, uint32_t deviceid, + const char *nickname, const char *serial_nr, const char *firmware) +{ + struct membuffer *b = _f; + + /* Nicknames that are empty or the same as the device model are not interesting */ + if (nickname) { + if (!*nickname || !strcmp(model, nickname)) + nickname = NULL; + } + + /* Serial numbers that are empty are not interesting */ + if (serial_nr && !*serial_nr) + serial_nr = NULL; + + /* Firmware strings that are empty are not interesting */ + if (firmware && !*firmware) + firmware = NULL; + + /* Do we have anything interesting about this dive computer to save? */ + if (!serial_nr && !nickname && !firmware) + return; + + put_format(b, "<divecomputerid"); + show_utf8(b, model, " model='", "'", 1); + put_format(b, " deviceid='%08x'", deviceid); + show_utf8(b, serial_nr, " serial='", "'", 1); + show_utf8(b, firmware, " firmware='", "'", 1); + show_utf8(b, nickname, " nickname='", "'", 1); + put_format(b, "/>\n"); +} + +int save_dives(const char *filename) +{ + return save_dives_logic(filename, false); +} + +void save_dives_buffer(struct membuffer *b, const bool select_only) +{ + int i; + struct dive *dive; + dive_trip_t *trip; + + put_format(b, "<divelog program='subsurface' version='%d'>\n<settings>\n", DATAFORMAT_VERSION); + + if (prefs.save_userid_local) + put_format(b, " <userid>%30s</userid>\n", prefs.userid); + + /* save the dive computer nicknames, if any */ + call_for_each_dc(b, save_one_device, select_only); + if (autogroup) + put_format(b, " <autogroup state='1' />\n"); + put_format(b, "</settings>\n"); + + /* save the dive sites - to make the output consistent let's sort the table, first */ + dive_site_table_sort(); + put_format(b, "<divesites>\n"); + for (i = 0; i < dive_site_table.nr; i++) { + int j; + struct dive *d; + struct dive_site *ds = get_dive_site(i); + if (dive_site_is_empty(ds)) { + for_each_dive(j, d) { + if (d->dive_site_uuid == ds->uuid) + d->dive_site_uuid = 0; + } + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } else if (ds->name && + (strncmp(ds->name, "Auto-created dive", 17) == 0 || + strncmp(ds->name, "New Dive", 8) == 0)) { + // these are the two default names for sites from + // the web service; if the site isn't used in any + // dive (really? you didn't rename it?), delete it + if (!is_dive_site_used(ds->uuid, false)) { + if (verbose) + fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } + } + if (select_only && !is_dive_site_used(ds->uuid, true)) + continue; + + put_format(b, "<site uuid='%8x'", ds->uuid); + show_utf8(b, ds->name, " name='", "'", 1); + if (ds->latitude.udeg || ds->longitude.udeg) { + put_degrees(b, ds->latitude, " gps='", " "); + put_degrees(b, ds->longitude, "", "'"); + } + show_utf8(b, ds->description, " description='", "'", 1); + put_format(b, ">\n"); + show_utf8(b, ds->notes, " <notes>", " </notes>\n", 0); + if (ds->taxonomy.nr) { + for (int j = 0; j < ds->taxonomy.nr; j++) { + struct taxonomy *t = &ds->taxonomy.category[j]; + if (t->category != TC_NONE && t->value) { + put_format(b, " <geo cat='%d'", t->category); + put_format(b, " origin='%d'", t->origin); + show_utf8(b, t->value, " value='", "'/>\n", 1); + } + } + } + put_format(b, "</site>\n"); + } + put_format(b, "</divesites>\n<dives>\n"); + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + /* save the dives */ + for_each_dive(i, dive) { + if (select_only) { + + if (!dive->selected) + continue; + save_one_dive_to_mb(b, dive); + + } else { + trip = dive->divetrip; + + /* Bare dive without a trip? */ + if (!trip) { + save_one_dive_to_mb(b, dive); + continue; + } + + /* Have we already seen this trip (and thus saved this dive?) */ + if (trip->index) + continue; + + /* We haven't seen this trip before - save it and all dives */ + trip->index = 1; + save_trip(b, trip); + } + } + put_format(b, "</dives>\n</divelog>\n"); +} + +static void save_backup(const char *name, const char *ext, const char *new_ext) +{ + int len = strlen(name); + int a = strlen(ext), b = strlen(new_ext); + char *newname; + + /* len up to and including the final '.' */ + len -= a; + if (len <= 1) + return; + if (name[len - 1] != '.') + return; + /* msvc doesn't have strncasecmp, has _strnicmp instead - crazy */ + if (strncasecmp(name + len, ext, a)) + return; + + newname = malloc(len + b + 1); + if (!newname) + return; + + memcpy(newname, name, len); + memcpy(newname + len, new_ext, b + 1); + + /* + * Ignore errors. Maybe we can't create the backup file, + * maybe no old file existed. Regardless, we'll write the + * new file. + */ + (void) subsurface_rename(name, newname); + free(newname); +} + +static void try_to_backup(const char *filename) +{ + char extension[][5] = { "xml", "ssrf", "" }; + int i = 0; + int flen = strlen(filename); + + /* Maybe we might want to make this configurable? */ + while (extension[i][0] != '\0') { + int elen = strlen(extension[i]); + if (strcasecmp(filename + flen - elen, extension[i]) == 0) { + if (last_xml_version < DATAFORMAT_VERSION) { + int se_len = strlen(extension[i]) + 5; + char *special_ext = malloc(se_len); + snprintf(special_ext, se_len, "%s.v%d", extension[i], last_xml_version); + save_backup(filename, extension[i], special_ext); + free(special_ext); + } else { + save_backup(filename, extension[i], "bak"); + } + break; + } + i++; + } +} + +int save_dives_logic(const char *filename, const bool select_only) +{ + struct membuffer buf = { 0 }; + FILE *f; + void *git; + const char *branch, *remote; + int error; + + git = is_git_repository(filename, &branch, &remote, false); + if (git) + return git_save_dives(git, branch, remote, select_only); + + save_dives_buffer(&buf, select_only); + + if (same_string(filename, "-")) { + f = stdout; + } else { + try_to_backup(filename); + error = -1; + f = subsurface_fopen(filename, "w"); + } + if (f) { + flush_buffer(&buf, f); + error = fclose(f); + } + if (error) + report_error("Save failed (%s)", strerror(errno)); + + free_buffer(&buf); + return error; +} + +int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt) +{ + FILE *f; + struct membuffer buf = { 0 }; + xmlDoc *doc; + xsltStylesheetPtr xslt = NULL; + xmlDoc *transformed; + int res = 0; + char *params[3]; + int pnr = 0; + char unitstr[3]; + + if (verbose) + fprintf(stderr, "export_dives_xslt with stylesheet %s\n", export_xslt); + + if (!filename) + return report_error("No filename for export"); + + /* Save XML to file and convert it into a memory buffer */ + save_dives_buffer(&buf, selected); + + /* + * Parse the memory buffer into XML document and + * transform it to selected export format, finally dumping + * the XML into a character buffer. + */ + doc = xmlReadMemory(buf.buffer, buf.len, "divelog", NULL, 0); + free_buffer(&buf); + if (!doc) + return report_error("Failed to read XML memory"); + + /* Convert to export format */ + xslt = get_stylesheet(export_xslt); + if (!xslt) + return report_error("Failed to open export conversion stylesheet"); + + snprintf(unitstr, 3, "%d", units); + params[pnr++] = "units"; + params[pnr++] = unitstr; + params[pnr++] = NULL; + + transformed = xsltApplyStylesheet(xslt, doc, (const char **)params); + xmlFreeDoc(doc); + + /* Write the transformed export to file */ + f = subsurface_fopen(filename, "w"); + if (f) { + xsltSaveResultToFile(f, transformed, xslt); + fclose(f); + /* Check write errors? */ + } else { + res = report_error("Failed to open %s for writing (%s)", filename, strerror(errno)); + } + xsltFreeStylesheet(xslt); + xmlFreeDoc(transformed); + + return res; +} diff --git a/core/serial_ftdi.c b/core/serial_ftdi.c new file mode 100644 index 000000000..ff1335171 --- /dev/null +++ b/core/serial_ftdi.c @@ -0,0 +1,665 @@ +/* + * libdivecomputer + * + * Copyright (C) 2008 Jef Driesen + * Copyright (C) 2014 Venkatesh Shukla + * Copyright (C) 2015 Anton Lundin + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include <stdlib.h> // malloc, free +#include <string.h> // strerror +#include <errno.h> // errno +#include <sys/time.h> // gettimeofday +#include <time.h> // nanosleep +#include <stdio.h> + +#include <libusb.h> +#include <ftdi.h> + +#ifndef __ANDROID__ +#define INFO(context, fmt, ...) fprintf(stderr, "INFO: " fmt "\n", ##__VA_ARGS__) +#define ERROR(context, fmt, ...) fprintf(stderr, "ERROR: " fmt "\n", ##__VA_ARGS__) +#else +#include <android/log.h> +#define INFO(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "INFO: " fmt "\n", ##__VA_ARGS__) +#define ERROR(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "ERROR: " fmt "\n", ##__VA_ARGS__) +#endif +//#define SYSERROR(context, errcode) ERROR(__FILE__ ":" __LINE__ ": %s", strerror(errcode)) +#define SYSERROR(context, errcode) ; + +#include <libdivecomputer/custom_serial.h> + +/* Verbatim copied libdivecomputer enums to support configure */ +typedef enum serial_parity_t { + SERIAL_PARITY_NONE, + SERIAL_PARITY_EVEN, + SERIAL_PARITY_ODD +} serial_parity_t; + +typedef enum serial_flowcontrol_t { + SERIAL_FLOWCONTROL_NONE, + SERIAL_FLOWCONTROL_HARDWARE, + SERIAL_FLOWCONTROL_SOFTWARE +} serial_flowcontrol_t; + +typedef enum serial_queue_t { + SERIAL_QUEUE_INPUT = 0x01, + SERIAL_QUEUE_OUTPUT = 0x02, + SERIAL_QUEUE_BOTH = SERIAL_QUEUE_INPUT | SERIAL_QUEUE_OUTPUT +} serial_queue_t; + +typedef enum serial_line_t { + SERIAL_LINE_DCD, // Data carrier detect + SERIAL_LINE_CTS, // Clear to send + SERIAL_LINE_DSR, // Data set ready + SERIAL_LINE_RNG, // Ring indicator +} serial_line_t; + +#define VID 0x0403 // Vendor ID of FTDI + +#define MAX_BACKOFF 500 // Max milliseconds to wait before timing out. + +typedef struct serial_t { + /* Library context. */ + dc_context_t *context; + /* + * The file descriptor corresponding to the serial port. + * Also a libftdi_ftdi_ctx could be used? + */ + struct ftdi_context *ftdi_ctx; + long timeout; + /* + * Serial port settings are saved into this variable immediately + * after the port is opened. These settings are restored when the + * serial port is closed. + * Saving this using libftdi context or libusb. Search further. + * Custom implementation using libftdi functions could be done. + */ + + /* Half-duplex settings */ + int halfduplex; + unsigned int baudrate; + unsigned int nbits; +} serial_t; + +static int serial_ftdi_get_received (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // Direct access is not encouraged. But function implementation + // is not available. The return quantity might be anything. + // Find out further about its possible values and correct way of + // access. + int bytes = device->ftdi_ctx->readbuffer_remaining; + + return bytes; +} + +static int serial_ftdi_get_transmitted (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // This is not possible using libftdi. Look further into it. + return -1; +} + +static int serial_ftdi_sleep (serial_t *device, unsigned long timeout) +{ + if (device == NULL) + return -1; + + INFO (device->context, "Sleep: value=%lu", timeout); + + struct timespec ts; + ts.tv_sec = (timeout / 1000); + ts.tv_nsec = (timeout % 1000) * 1000000; + + while (nanosleep (&ts, &ts) != 0) { + if (errno != EINTR ) { + SYSERROR (device->context, errno); + return -1; + } + } + + return 0; +} + + +// Used internally for opening ftdi devices +static int serial_ftdi_open_device (struct ftdi_context *ftdi_ctx) +{ + int accepted_pids[] = { 0x6001, 0x6010, 0x6011, // Suunto (Smart Interface), Heinrichs Weikamp + 0xF460, // Oceanic + 0xF680, // Suunto + 0x87D0, // Cressi (Leonardo) + }; + int num_accepted_pids = 6; + int i, pid, ret; + for (i = 0; i < num_accepted_pids; i++) { + pid = accepted_pids[i]; + ret = ftdi_usb_open (ftdi_ctx, VID, pid); + if (ret == -3) // Device not found + continue; + else + return ret; + } + // No supported devices are attached. + return ret; +} + +// +// Open the serial port. +// Initialise ftdi_context and use it to open the device +// +//FIXME: ugly forward declaration of serial_ftdi_configure, util we support configure for real... +static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol); +static dc_status_t serial_ftdi_open (serial_t **out, dc_context_t *context, const char* name) +{ + if (out == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (context, "Open: name=%s", name ? name : ""); + + // Allocate memory. + serial_t *device = (serial_t *) malloc (sizeof (serial_t)); + if (device == NULL) { + SYSERROR (context, errno); + return DC_STATUS_NOMEMORY; + } + + struct ftdi_context *ftdi_ctx = ftdi_new(); + if (ftdi_ctx == NULL) { + free(device); + SYSERROR (context, errno); + return DC_STATUS_NOMEMORY; + } + + // Library context. + device->context = context; + + // Default to blocking reads. + device->timeout = -1; + + // Default to full-duplex. + device->halfduplex = 0; + device->baudrate = 0; + device->nbits = 0; + + // Initialize device ftdi context + ftdi_init(ftdi_ctx); + + if (ftdi_set_interface(ftdi_ctx,INTERFACE_ANY)) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (serial_ftdi_open_device(ftdi_ctx) < 0) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (ftdi_usb_reset(ftdi_ctx)) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (ftdi_usb_purge_buffers(ftdi_ctx)) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + device->ftdi_ctx = ftdi_ctx; + + //FIXME: remove this when custom-serial have support for configure calls + serial_ftdi_configure (device, 115200, 8, 0, 1, 0); + + *out = device; + + return DC_STATUS_SUCCESS; +} + +// +// Close the serial port. +// +static int serial_ftdi_close (serial_t *device) +{ + if (device == NULL) + return 0; + + // Restore the initial terminal attributes. + // See if it is possible using libusb or libftdi + + int ret = ftdi_usb_close(device->ftdi_ctx); + if (ret < 0) { + ERROR (device->context, "Unable to close the ftdi device : %d (%s)\n", + ret, ftdi_get_error_string(device->ftdi_ctx)); + return ret; + } + + ftdi_free(device->ftdi_ctx); + + // Free memory. + free (device); + + return 0; +} + +// +// Configure the serial port (baudrate, databits, parity, stopbits and flowcontrol). +// +static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Configure: baudrate=%i, databits=%i, parity=%i, stopbits=%i, flowcontrol=%i", + baudrate, databits, parity, stopbits, flowcontrol); + + enum ftdi_bits_type ft_bits; + enum ftdi_stopbits_type ft_stopbits; + enum ftdi_parity_type ft_parity; + + if (ftdi_set_baudrate(device->ftdi_ctx, baudrate) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + // Set the character size. + switch (databits) { + case 7: + ft_bits = BITS_7; + break; + case 8: + ft_bits = BITS_8; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the parity type. + switch (parity) { + case SERIAL_PARITY_NONE: // No parity + ft_parity = NONE; + break; + case SERIAL_PARITY_EVEN: // Even parity + ft_parity = EVEN; + break; + case SERIAL_PARITY_ODD: // Odd parity + ft_parity = ODD; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the number of stop bits. + switch (stopbits) { + case 1: // One stopbit + ft_stopbits = STOP_BIT_1; + break; + case 2: // Two stopbits + ft_stopbits = STOP_BIT_2; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the attributes + if (ftdi_set_line_property(device->ftdi_ctx, ft_bits, ft_stopbits, ft_parity)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + + // Set the flow control. + switch (flowcontrol) { + case SERIAL_FLOWCONTROL_NONE: // No flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_DISABLE_FLOW_CTRL) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + case SERIAL_FLOWCONTROL_HARDWARE: // Hardware (RTS/CTS) flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_RTS_CTS_HS) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + case SERIAL_FLOWCONTROL_SOFTWARE: // Software (XON/XOFF) flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_XON_XOFF_HS) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + default: + return DC_STATUS_INVALIDARGS; + } + + device->baudrate = baudrate; + device->nbits = 1 + databits + stopbits + (parity ? 1 : 0); + + return DC_STATUS_SUCCESS; +} + +// +// Configure the serial port (timeouts). +// +static int serial_ftdi_set_timeout (serial_t *device, long timeout) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Timeout: value=%li", timeout); + + device->timeout = timeout; + + return 0; +} + +static int serial_ftdi_set_halfduplex (serial_t *device, int value) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // Most ftdi chips support full duplex operation. ft232rl does. + // Crosscheck other chips. + + device->halfduplex = value; + + return 0; +} + +static int serial_ftdi_read (serial_t *device, void *data, unsigned int size) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // The total timeout. + long timeout = device->timeout; + + // The absolute target time. + struct timeval tve; + + static int backoff = 1; + int init = 1; + unsigned int nbytes = 0; + while (nbytes < size) { + struct timeval tvt; + if (timeout > 0) { + struct timeval now; + if (gettimeofday (&now, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + + if (init) { + // Calculate the initial timeout. + tvt.tv_sec = (timeout / 1000); + tvt.tv_usec = (timeout % 1000) * 1000; + // Calculate the target time. + timeradd (&now, &tvt, &tve); + } else { + // Calculate the remaining timeout. + if (timercmp (&now, &tve, <)) + timersub (&tve, &now, &tvt); + else + timerclear (&tvt); + } + init = 0; + } else if (timeout == 0) { + timerclear (&tvt); + } + + int n = ftdi_read_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); + if (n < 0) { + if (n == LIBUSB_ERROR_INTERRUPTED) + continue; //Retry. + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; //Error during read call. + } else if (n == 0) { + // Exponential backoff. + if (backoff > MAX_BACKOFF) { + ERROR(device->context, "%s", "FTDI read timed out."); + return -1; + } + serial_ftdi_sleep (device, backoff); + backoff *= 2; + } else { + // Reset backoff to 1 on success. + backoff = 1; + } + + nbytes += n; + } + + INFO (device->context, "Read %d bytes", nbytes); + + return nbytes; +} + +static int serial_ftdi_write (serial_t *device, const void *data, unsigned int size) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + struct timeval tve, tvb; + if (device->halfduplex) { + // Get the current time. + if (gettimeofday (&tvb, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + } + + unsigned int nbytes = 0; + while (nbytes < size) { + + int n = ftdi_write_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); + if (n < 0) { + if (n == LIBUSB_ERROR_INTERRUPTED) + continue; // Retry. + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; // Error during write call. + } else if (n == 0) { + break; // EOF. + } + + nbytes += n; + } + + if (device->halfduplex) { + // Get the current time. + if (gettimeofday (&tve, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + + // Calculate the elapsed time (microseconds). + struct timeval tvt; + timersub (&tve, &tvb, &tvt); + unsigned long elapsed = tvt.tv_sec * 1000000 + tvt.tv_usec; + + // Calculate the expected duration (microseconds). A 2 millisecond fudge + // factor is added because it improves the success rate significantly. + unsigned long expected = 1000000.0 * device->nbits / device->baudrate * size + 0.5 + 2000; + + // Wait for the remaining time. + if (elapsed < expected) { + unsigned long remaining = expected - elapsed; + + // The remaining time is rounded up to the nearest millisecond to + // match the Windows implementation. The higher resolution is + // pointless anyway, since we already added a fudge factor above. + serial_ftdi_sleep (device, (remaining + 999) / 1000); + } + } + + INFO (device->context, "Wrote %d bytes", nbytes); + + return nbytes; +} + +static int serial_ftdi_flush (serial_t *device, int queue) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Flush: queue=%u, input=%i, output=%i", queue, + serial_ftdi_get_received (device), + serial_ftdi_get_transmitted (device)); + + switch (queue) { + case SERIAL_QUEUE_INPUT: + if (ftdi_usb_purge_tx_buffer(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + case SERIAL_QUEUE_OUTPUT: + if (ftdi_usb_purge_rx_buffer(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + default: + if (ftdi_usb_purge_buffers(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + } + + return 0; +} + +static int serial_ftdi_send_break (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument)a + + INFO (device->context, "Break : One time period."); + + // no direct functions for sending break signals in libftdi. + // there is a suggestion to lower the baudrate and sending NUL + // and resetting the baudrate up again. But it has flaws. + // Not implementing it before researching more. + + return -1; +} + +static int serial_ftdi_set_break (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Break: value=%i", level); + + // Not implemented in libftdi yet. Research it further. + + return -1; +} + +static int serial_ftdi_set_dtr (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "DTR: value=%i", level); + + if (ftdi_setdtr(device->ftdi_ctx, level)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + return 0; +} + +static int serial_ftdi_set_rts (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "RTS: value=%i", level); + + if (ftdi_setrts(device->ftdi_ctx, level)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + return 0; +} + +const dc_serial_operations_t serial_ftdi_ops = { + .open = serial_ftdi_open, + .close = serial_ftdi_close, + .read = serial_ftdi_read, + .write = serial_ftdi_write, + .flush = serial_ftdi_flush, + .get_received = serial_ftdi_get_received, + .get_transmitted = NULL, /*NOT USED ANYWHERE! serial_ftdi_get_transmitted */ + .set_timeout = serial_ftdi_set_timeout +#ifdef FIXED_SSRF_CUSTOM_SERIAL + , + .configure = serial_ftdi_configure, +//static int serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) + .set_halfduplex = serial_ftdi_set_halfduplex, +//static int serial_ftdi_set_halfduplex (serial_t *device, int value) + .send_break = serial_ftdi_send_break, +//static int serial_ftdi_send_break (serial_t *device) + .set_break = serial_ftdi_set_break, +//static int serial_ftdi_set_break (serial_t *device, int level) + .set_dtr = serial_ftdi_set_dtr, +//static int serial_ftdi_set_dtr (serial_t *device, int level) + .set_rts = serial_ftdi_set_rts +//static int serial_ftdi_set_rts (serial_t *device, int level) +#endif +}; + +dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); + + if (serial_device == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Initialize data and function pointers + dc_serial_init(serial_device, NULL, &serial_ftdi_ops); + + // Open the serial device. + dc_status_t rc = (dc_status_t) serial_ftdi_open (&serial_device->port, context, NULL); + if (rc != DC_STATUS_SUCCESS) { + free (serial_device); + return rc; + } + + // Set the type of the device + serial_device->type = DC_TRANSPORT_USB;; + + *out = serial_device; + + return DC_STATUS_SUCCESS; +} diff --git a/core/sha1.c b/core/sha1.c new file mode 100644 index 000000000..acf8c5d9f --- /dev/null +++ b/core/sha1.c @@ -0,0 +1,300 @@ +/* + * SHA1 routine optimized to do word accesses rather than byte accesses, + * and to avoid unnecessary copies into the context array. + * + * This was initially based on the Mozilla SHA1 implementation, although + * none of the original Mozilla code remains. + */ + +/* this is only to get definitions for memcpy(), ntohl() and htonl() */ +#include <string.h> +#include <stdint.h> +#ifdef WIN32 +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include "sha1.h" + +#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) + +/* + * Force usage of rol or ror by selecting the one with the smaller constant. + * It _can_ generate slightly smaller code (a constant of 1 is special), but + * perhaps more importantly it's possibly faster on any uarch that does a + * rotate with a loop. + */ + +#define SHA_ASM(op, x, n) ({ unsigned int __res; __asm__(op " %1,%0":"=r" (__res):"i" (n), "0" (x)); __res; }) +#define SHA_ROL(x, n) SHA_ASM("rol", x, n) +#define SHA_ROR(x, n) SHA_ASM("ror", x, n) + +#else + +#define SHA_ROT(X, l, r) (((X) << (l)) | ((X) >> (r))) +#define SHA_ROL(X, n) SHA_ROT(X, n, 32 - (n)) +#define SHA_ROR(X, n) SHA_ROT(X, 32 - (n), n) + +#endif + +/* + * If you have 32 registers or more, the compiler can (and should) + * try to change the array[] accesses into registers. However, on + * machines with less than ~25 registers, that won't really work, + * and at least gcc will make an unholy mess of it. + * + * So to avoid that mess which just slows things down, we force + * the stores to memory to actually happen (we might be better off + * with a 'W(t)=(val);asm("":"+m" (W(t))' there instead, as + * suggested by Artur Skawina - that will also make gcc unable to + * try to do the silly "optimize away loads" part because it won't + * see what the value will be). + * + * Ben Herrenschmidt reports that on PPC, the C version comes close + * to the optimized asm with this (ie on PPC you don't want that + * 'volatile', since there are lots of registers). + * + * On ARM we get the best code generation by forcing a full memory barrier + * between each SHA_ROUND, otherwise gcc happily get wild with spilling and + * the stack frame size simply explode and performance goes down the drain. + */ + +#if defined(__i386__) || defined(__x86_64__) +#define setW(x, val) (*(volatile unsigned int *)&W(x) = (val)) +#elif defined(__GNUC__) && defined(__arm__) +#define setW(x, val) \ + do { \ + W(x) = (val); \ + __asm__("" :: : "memory"); \ + } while (0) +#else +#define setW(x, val) (W(x) = (val)) +#endif + +/* + * Performance might be improved if the CPU architecture is OK with + * unaligned 32-bit loads and a fast ntohl() is available. + * Otherwise fall back to byte loads and shifts which is portable, + * and is faster on architectures with memory alignment issues. + */ + +#if defined(__i386__) || defined(__x86_64__) || \ + defined(_M_IX86) || defined(_M_X64) || \ + defined(__ppc__) || defined(__ppc64__) || \ + defined(__powerpc__) || defined(__powerpc64__) || \ + defined(__s390__) || defined(__s390x__) + +#define get_be32(p) ntohl(*(unsigned int *)(p)) +#define put_be32(p, v) \ + do { \ + *(unsigned int *)(p) = htonl(v); \ + } while (0) + +#else + +#define get_be32(p) ( \ + (*((unsigned char *)(p) + 0) << 24) | \ + (*((unsigned char *)(p) + 1) << 16) | \ + (*((unsigned char *)(p) + 2) << 8) | \ + (*((unsigned char *)(p) + 3) << 0)) +#define put_be32(p, v) \ + do { \ + unsigned int __v = (v); \ + *((unsigned char *)(p) + 0) = __v >> 24; \ + *((unsigned char *)(p) + 1) = __v >> 16; \ + *((unsigned char *)(p) + 2) = __v >> 8; \ + *((unsigned char *)(p) + 3) = __v >> 0; \ + } while (0) + +#endif + +/* This "rolls" over the 512-bit array */ +#define W(x) (array[(x) & 15]) + +/* + * Where do we get the source from? The first 16 iterations get it from + * the input data, the next mix it from the 512-bit array. + */ +#define SHA_SRC(t) get_be32((unsigned char *)block + (t) * 4) +#define SHA_MIX(t) SHA_ROL(W((t) + 13) ^ W((t) + 8) ^ W((t) + 2) ^ W(t), 1); + +#define SHA_ROUND(t, input, fn, constant, A, B, C, D, E) \ + do { \ + unsigned int TEMP = input(t); \ + setW(t, TEMP); \ + E += TEMP + SHA_ROL(A, 5) + (fn) + (constant); \ + B = SHA_ROR(B, 2); \ + } while (0) + +#define T_0_15(t, A, B, C, D, E) SHA_ROUND(t, SHA_SRC, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) +#define T_16_19(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) +#define T_20_39(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0x6ed9eba1, A, B, C, D, E) +#define T_40_59(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, ((B &C) + (D &(B ^ C))), 0x8f1bbcdc, A, B, C, D, E) +#define T_60_79(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0xca62c1d6, A, B, C, D, E) + +static void blk_SHA1_Block(blk_SHA_CTX *ctx, const void *block) +{ + unsigned int A, B, C, D, E; + unsigned int array[16]; + + A = ctx->H[0]; + B = ctx->H[1]; + C = ctx->H[2]; + D = ctx->H[3]; + E = ctx->H[4]; + + /* Round 1 - iterations 0-16 take their input from 'block' */ + T_0_15(0, A, B, C, D, E); + T_0_15(1, E, A, B, C, D); + T_0_15(2, D, E, A, B, C); + T_0_15(3, C, D, E, A, B); + T_0_15(4, B, C, D, E, A); + T_0_15(5, A, B, C, D, E); + T_0_15(6, E, A, B, C, D); + T_0_15(7, D, E, A, B, C); + T_0_15(8, C, D, E, A, B); + T_0_15(9, B, C, D, E, A); + T_0_15(10, A, B, C, D, E); + T_0_15(11, E, A, B, C, D); + T_0_15(12, D, E, A, B, C); + T_0_15(13, C, D, E, A, B); + T_0_15(14, B, C, D, E, A); + T_0_15(15, A, B, C, D, E); + + /* Round 1 - tail. Input from 512-bit mixing array */ + T_16_19(16, E, A, B, C, D); + T_16_19(17, D, E, A, B, C); + T_16_19(18, C, D, E, A, B); + T_16_19(19, B, C, D, E, A); + + /* Round 2 */ + T_20_39(20, A, B, C, D, E); + T_20_39(21, E, A, B, C, D); + T_20_39(22, D, E, A, B, C); + T_20_39(23, C, D, E, A, B); + T_20_39(24, B, C, D, E, A); + T_20_39(25, A, B, C, D, E); + T_20_39(26, E, A, B, C, D); + T_20_39(27, D, E, A, B, C); + T_20_39(28, C, D, E, A, B); + T_20_39(29, B, C, D, E, A); + T_20_39(30, A, B, C, D, E); + T_20_39(31, E, A, B, C, D); + T_20_39(32, D, E, A, B, C); + T_20_39(33, C, D, E, A, B); + T_20_39(34, B, C, D, E, A); + T_20_39(35, A, B, C, D, E); + T_20_39(36, E, A, B, C, D); + T_20_39(37, D, E, A, B, C); + T_20_39(38, C, D, E, A, B); + T_20_39(39, B, C, D, E, A); + + /* Round 3 */ + T_40_59(40, A, B, C, D, E); + T_40_59(41, E, A, B, C, D); + T_40_59(42, D, E, A, B, C); + T_40_59(43, C, D, E, A, B); + T_40_59(44, B, C, D, E, A); + T_40_59(45, A, B, C, D, E); + T_40_59(46, E, A, B, C, D); + T_40_59(47, D, E, A, B, C); + T_40_59(48, C, D, E, A, B); + T_40_59(49, B, C, D, E, A); + T_40_59(50, A, B, C, D, E); + T_40_59(51, E, A, B, C, D); + T_40_59(52, D, E, A, B, C); + T_40_59(53, C, D, E, A, B); + T_40_59(54, B, C, D, E, A); + T_40_59(55, A, B, C, D, E); + T_40_59(56, E, A, B, C, D); + T_40_59(57, D, E, A, B, C); + T_40_59(58, C, D, E, A, B); + T_40_59(59, B, C, D, E, A); + + /* Round 4 */ + T_60_79(60, A, B, C, D, E); + T_60_79(61, E, A, B, C, D); + T_60_79(62, D, E, A, B, C); + T_60_79(63, C, D, E, A, B); + T_60_79(64, B, C, D, E, A); + T_60_79(65, A, B, C, D, E); + T_60_79(66, E, A, B, C, D); + T_60_79(67, D, E, A, B, C); + T_60_79(68, C, D, E, A, B); + T_60_79(69, B, C, D, E, A); + T_60_79(70, A, B, C, D, E); + T_60_79(71, E, A, B, C, D); + T_60_79(72, D, E, A, B, C); + T_60_79(73, C, D, E, A, B); + T_60_79(74, B, C, D, E, A); + T_60_79(75, A, B, C, D, E); + T_60_79(76, E, A, B, C, D); + T_60_79(77, D, E, A, B, C); + T_60_79(78, C, D, E, A, B); + T_60_79(79, B, C, D, E, A); + + ctx->H[0] += A; + ctx->H[1] += B; + ctx->H[2] += C; + ctx->H[3] += D; + ctx->H[4] += E; +} + +void blk_SHA1_Init(blk_SHA_CTX *ctx) +{ + ctx->size = 0; + + /* Initialize H with the magic constants (see FIPS180 for constants) */ + ctx->H[0] = 0x67452301; + ctx->H[1] = 0xefcdab89; + ctx->H[2] = 0x98badcfe; + ctx->H[3] = 0x10325476; + ctx->H[4] = 0xc3d2e1f0; +} + +void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *data, unsigned long len) +{ + unsigned int lenW = ctx->size & 63; + + ctx->size += len; + + /* Read the data into W and process blocks as they get full */ + if (lenW) { + unsigned int left = 64 - lenW; + if (len < left) + left = len; + memcpy(lenW + (char *)ctx->W, data, left); + lenW = (lenW + left) & 63; + len -= left; + data = ((const char *)data + left); + if (lenW) + return; + blk_SHA1_Block(ctx, ctx->W); + } + while (len >= 64) { + blk_SHA1_Block(ctx, data); + data = ((const char *)data + 64); + len -= 64; + } + if (len) + memcpy(ctx->W, data, len); +} + +void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx) +{ + static const unsigned char pad[64] = { 0x80 }; + unsigned int padlen[2]; + int i; + + /* Pad with a binary 1 (ie 0x80), then zeroes, then length */ + padlen[0] = htonl((uint32_t)(ctx->size >> 29)); + padlen[1] = htonl((uint32_t)(ctx->size << 3)); + + i = ctx->size & 63; + blk_SHA1_Update(ctx, pad, 1 + (63 & (55 - i))); + blk_SHA1_Update(ctx, padlen, 8); + + /* Output hash */ + for (i = 0; i < 5; i++) + put_be32(hashout + i * 4, ctx->H[i]); +} diff --git a/core/sha1.h b/core/sha1.h new file mode 100644 index 000000000..cab6ff77d --- /dev/null +++ b/core/sha1.h @@ -0,0 +1,38 @@ +/* + * SHA1 routine optimized to do word accesses rather than byte accesses, + * and to avoid unnecessary copies into the context array. + * + * This was initially based on the Mozilla SHA1 implementation, although + * none of the original Mozilla code remains. + */ +#ifndef SHA1_H +#define SHA1_H + +typedef struct +{ + unsigned long long size; + unsigned int H[5]; + unsigned int W[16]; +} blk_SHA_CTX; + +void blk_SHA1_Init(blk_SHA_CTX *ctx); +void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *dataIn, unsigned long len); +void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx); + +/* Make us use the standard names */ +#define SHA_CTX blk_SHA_CTX +#define SHA1_Init blk_SHA1_Init +#define SHA1_Update blk_SHA1_Update +#define SHA1_Final blk_SHA1_Final + +/* Trivial helper function */ +static inline void SHA1(const void *dataIn, unsigned long len, unsigned char hashout[20]) +{ + SHA_CTX ctx; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, dataIn, len); + SHA1_Final(hashout, &ctx); +} + +#endif // SHA1_H diff --git a/core/statistics.c b/core/statistics.c new file mode 100644 index 000000000..6a05cffc1 --- /dev/null +++ b/core/statistics.c @@ -0,0 +1,404 @@ +/* statistics.c + * + * core logic for the Info & Stats page - + * char *get_time_string(int seconds, int maxdays); + * char *get_minutes(int seconds); + * void process_all_dives(struct dive *dive, struct dive **prev_dive); + * void get_selected_dives_text(char *buffer, int size); + */ +#include "gettext.h" +#include <string.h> +#include <ctype.h> + +#include "dive.h" +#include "display.h" +#include "divelist.h" +#include "statistics.h" + +static stats_t stats; +stats_t stats_selection; +stats_t *stats_monthly = NULL; +stats_t *stats_yearly = NULL; +stats_t *stats_by_trip = NULL; +stats_t *stats_by_type = NULL; + +static void process_temperatures(struct dive *dp, stats_t *stats) +{ + int min_temp, mean_temp, max_temp = 0; + + max_temp = dp->maxtemp.mkelvin; + if (max_temp && (!stats->max_temp || max_temp > stats->max_temp)) + stats->max_temp = max_temp; + + min_temp = dp->mintemp.mkelvin; + if (min_temp && (!stats->min_temp || min_temp < stats->min_temp)) + stats->min_temp = min_temp; + + if (min_temp || max_temp) { + mean_temp = min_temp; + if (mean_temp) + mean_temp = (mean_temp + max_temp) / 2; + else + mean_temp = max_temp; + stats->combined_temp += get_temp_units(mean_temp, NULL); + stats->combined_count++; + } +} + +static void process_dive(struct dive *dp, stats_t *stats) +{ + int old_tt, sac_time = 0; + uint32_t duration = dp->duration.seconds; + + old_tt = stats->total_time.seconds; + stats->total_time.seconds += duration; + if (duration > stats->longest_time.seconds) + stats->longest_time.seconds = duration; + if (stats->shortest_time.seconds == 0 || duration < stats->shortest_time.seconds) + stats->shortest_time.seconds = duration; + if (dp->maxdepth.mm > stats->max_depth.mm) + stats->max_depth.mm = dp->maxdepth.mm; + if (stats->min_depth.mm == 0 || dp->maxdepth.mm < stats->min_depth.mm) + stats->min_depth.mm = dp->maxdepth.mm; + + process_temperatures(dp, stats); + + /* Maybe we should drop zero-duration dives */ + if (!duration) + return; + stats->avg_depth.mm = (1.0 * old_tt * stats->avg_depth.mm + + duration * dp->meandepth.mm) / + stats->total_time.seconds; + if (dp->sac > 100) { /* less than .1 l/min is bogus, even with a pSCR */ + sac_time = stats->total_sac_time + duration; + stats->avg_sac.mliter = (1.0 * stats->total_sac_time * stats->avg_sac.mliter + + duration * dp->sac) / + sac_time; + if (dp->sac > stats->max_sac.mliter) + stats->max_sac.mliter = dp->sac; + if (stats->min_sac.mliter == 0 || dp->sac < stats->min_sac.mliter) + stats->min_sac.mliter = dp->sac; + stats->total_sac_time = sac_time; + } +} + +char *get_minutes(int seconds) +{ + static char buf[80]; + snprintf(buf, sizeof(buf), "%d:%.2d", FRACTION(seconds, 60)); + return buf; +} + +void process_all_dives(struct dive *dive, struct dive **prev_dive) +{ + int idx; + struct dive *dp; + struct tm tm; + int current_year = 0; + int current_month = 0; + int year_iter = 0; + int month_iter = 0; + int prev_month = 0, prev_year = 0; + int trip_iter = 0; + dive_trip_t *trip_ptr = 0; + unsigned int size, tsize; + + *prev_dive = NULL; + memset(&stats, 0, sizeof(stats)); + if (dive_table.nr > 0) { + stats.shortest_time.seconds = dive_table.dives[0]->duration.seconds; + stats.min_depth.mm = dive_table.dives[0]->maxdepth.mm; + stats.selection_size = dive_table.nr; + } + + /* allocate sufficient space to hold the worst + * case (one dive per year or all dives during + * one month) for yearly and monthly statistics*/ + + free(stats_yearly); + free(stats_monthly); + free(stats_by_trip); + free(stats_by_type); + + size = sizeof(stats_t) * (dive_table.nr + 1); + tsize = sizeof(stats_t) * (NUM_DC_TYPE + 1); + stats_yearly = malloc(size); + stats_monthly = malloc(size); + stats_by_trip = malloc(size); + stats_by_type = malloc(tsize); + if (!stats_yearly || !stats_monthly || !stats_by_trip || !stats_by_type) + return; + memset(stats_yearly, 0, size); + memset(stats_monthly, 0, size); + memset(stats_by_trip, 0, size); + memset(stats_by_type, 0, tsize); + stats_yearly[0].is_year = true; + + /* Setting the is_trip to true to show the location as first + * field in the statistics window */ + stats_by_type[0].location = strdup("All (by type stats)"); + stats_by_type[0].is_trip = true; + stats_by_type[1].location = strdup("OC"); + stats_by_type[1].is_trip = true; + stats_by_type[2].location = strdup("CCR"); + stats_by_type[2].is_trip = true; + stats_by_type[3].location = strdup("pSCR"); + stats_by_type[3].is_trip = true; + stats_by_type[4].location = strdup("Freedive"); + stats_by_type[4].is_trip = true; + + /* this relies on the fact that the dives in the dive_table + * are in chronological order */ + for_each_dive (idx, dp) { + if (dive && dp->when == dive->when) { + /* that's the one we are showing */ + if (idx > 0) + *prev_dive = dive_table.dives[idx - 1]; + } + process_dive(dp, &stats); + + /* yearly statistics */ + utc_mkdate(dp->when, &tm); + if (current_year == 0) + current_year = tm.tm_year + 1900; + + if (current_year != tm.tm_year + 1900) { + current_year = tm.tm_year + 1900; + process_dive(dp, &(stats_yearly[++year_iter])); + stats_yearly[year_iter].is_year = true; + } else { + process_dive(dp, &(stats_yearly[year_iter])); + } + stats_yearly[year_iter].selection_size++; + stats_yearly[year_iter].period = current_year; + + /* stats_by_type[0] is all the dives combined */ + stats_by_type[0].selection_size++; + process_dive(dp, &(stats_by_type[0])); + + process_dive(dp, &(stats_by_type[dp->dc.divemode + 1])); + stats_by_type[dp->dc.divemode + 1].selection_size++; + + if (dp->divetrip != NULL) { + if (trip_ptr != dp->divetrip) { + trip_ptr = dp->divetrip; + trip_iter++; + } + + /* stats_by_trip[0] is all the dives combined */ + stats_by_trip[0].selection_size++; + process_dive(dp, &(stats_by_trip[0])); + stats_by_trip[0].is_trip = true; + stats_by_trip[0].location = strdup("All (by trip stats)"); + + process_dive(dp, &(stats_by_trip[trip_iter])); + stats_by_trip[trip_iter].selection_size++; + stats_by_trip[trip_iter].is_trip = true; + stats_by_trip[trip_iter].location = dp->divetrip->location; + } + + /* monthly statistics */ + if (current_month == 0) { + current_month = tm.tm_mon + 1; + } else { + if (current_month != tm.tm_mon + 1) + current_month = tm.tm_mon + 1; + if (prev_month != current_month || prev_year != current_year) + month_iter++; + } + process_dive(dp, &(stats_monthly[month_iter])); + stats_monthly[month_iter].selection_size++; + stats_monthly[month_iter].period = current_month; + prev_month = current_month; + prev_year = current_year; + } +} + +/* make sure we skip the selected summary entries */ +void process_selected_dives(void) +{ + struct dive *dive; + unsigned int i, nr; + + memset(&stats_selection, 0, sizeof(stats_selection)); + + nr = 0; + for_each_dive(i, dive) { + if (dive->selected) { + process_dive(dive, &stats_selection); + nr++; + } + } + stats_selection.selection_size = nr; +} + +char *get_time_string_s(int seconds, int maxdays, bool freediving) +{ + static char buf[80]; + if (maxdays && seconds > 3600 * 24 * maxdays) { + snprintf(buf, sizeof(buf), translate("gettextFromC", "more than %d days"), maxdays); + } else { + int days = seconds / 3600 / 24; + int hours = (seconds - days * 3600 * 24) / 3600; + int minutes = (seconds - days * 3600 * 24 - hours * 3600) / 60; + int secs = (seconds - days * 3600 * 24 - hours * 3600 - minutes*60); + if (days > 0) + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dd %dh %dmin"), days, hours, minutes); + else + if (freediving && seconds < 3600) + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dmin %dsecs"), minutes, secs); + else + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dh %dmin"), hours, minutes); + } + return buf; +} + +/* this gets called when at least two but not all dives are selected */ +static void get_ranges(char *buffer, int size) +{ + int i, len; + int first = -1, last = -1; + struct dive *dive; + + snprintf(buffer, size, "%s", translate("gettextFromC", "for dives #")); + for_each_dive (i, dive) { + if (!dive->selected) + continue; + if (dive->number < 1) { + /* uhh - weird numbers - bail */ + snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dives")); + return; + } + len = strlen(buffer); + if (last == -1) { + snprintf(buffer + len, size - len, "%d", dive->number); + first = last = dive->number; + } else { + if (dive->number == last + 1) { + last++; + continue; + } else { + if (first == last) + snprintf(buffer + len, size - len, ", %d", dive->number); + else if (first + 1 == last) + snprintf(buffer + len, size - len, ", %d, %d", last, dive->number); + else + snprintf(buffer + len, size - len, "-%d, %d", last, dive->number); + first = last = dive->number; + } + } + } + len = strlen(buffer); + if (first != last) { + if (first + 1 == last) + snprintf(buffer + len, size - len, ", %d", last); + else + snprintf(buffer + len, size - len, "-%d", last); + } +} + +void get_selected_dives_text(char *buffer, size_t size) +{ + if (amount_selected == 1) { + if (current_dive) + snprintf(buffer, size, translate("gettextFromC", "for dive #%d"), current_dive->number); + else + snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dive")); + } else if (amount_selected == (unsigned int)dive_table.nr) { + snprintf(buffer, size, "%s", translate("gettextFromC", "for all dives")); + } else if (amount_selected == 0) { + snprintf(buffer, size, "%s", translate("gettextFromC", "(no dives)")); + } else { + get_ranges(buffer, size); + if (strlen(buffer) == size - 1) { + /* add our own ellipse... the way Pango does this is ugly + * as it will leave partial numbers there which I don't like */ + size_t offset = 4; + while (offset < size && isdigit(buffer[size - offset])) + offset++; + strcpy(buffer + size - offset, "..."); + } + } +} + +#define SOME_GAS 5000 // 5bar drop in cylinder pressure makes cylinder used + +bool is_cylinder_used(struct dive *dive, int idx) +{ + struct divecomputer *dc; + bool firstGasExplicit = false; + if (cylinder_none(&dive->cylinder[idx])) + return false; + + if ((dive->cylinder[idx].start.mbar - dive->cylinder[idx].end.mbar) > SOME_GAS) + return true; + for_each_dc(dive, dc) { + struct event *event = get_next_event(dc->events, "gaschange"); + while (event) { + if (dc->sample && (event->time.seconds == 0 || + (dc->samples && dc->sample[0].time.seconds == event->time.seconds))) + firstGasExplicit = true; + if (get_cylinder_index(dive, event) == idx) + return true; + event = get_next_event(event->next, "gaschange"); + } + if (dc->divemode == CCR && (idx == dive->diluent_cylinder_index || idx == dive->oxygen_cylinder_index)) + return true; + } + if (idx == 0 && !firstGasExplicit) + return true; + return false; +} + +void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]) +{ + int idx; + for (idx = 0; idx < MAX_CYLINDERS; idx++) { + cylinder_t *cyl = &dive->cylinder[idx]; + pressure_t start, end; + + if (!is_cylinder_used(dive, idx)) + continue; + + start = cyl->start.mbar ? cyl->start : cyl->sample_start; + end = cyl->end.mbar ? cyl->end : cyl->sample_end; + if (end.mbar && start.mbar > end.mbar) + gases[idx].mliter = gas_volume(cyl, start) - gas_volume(cyl, end); + } +} + +/* Quite crude reverse-blender-function, but it produces a approx result */ +static void get_gas_parts(struct gasmix mix, volume_t vol, int o2_in_topup, volume_t *o2, volume_t *he) +{ + volume_t air = {}; + + if (gasmix_is_air(&mix)) { + o2->mliter = 0; + he->mliter = 0; + return; + } + + air.mliter = rint(((double)vol.mliter * (1000 - get_he(&mix) - get_o2(&mix))) / (1000 - o2_in_topup)); + he->mliter = rint(((double)vol.mliter * get_he(&mix)) / 1000.0); + o2->mliter += vol.mliter - he->mliter - air.mliter; +} + +void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot) +{ + int i, j; + struct dive *d; + for_each_dive (i, d) { + if (!d->selected) + continue; + volume_t diveGases[MAX_CYLINDERS] = {}; + get_gas_used(d, diveGases); + for (j = 0; j < MAX_CYLINDERS; j++) { + if (diveGases[j].mliter) { + volume_t o2 = {}, he = {}; + get_gas_parts(d->cylinder[j].gasmix, diveGases[j], O2_IN_AIR, &o2, &he); + o2_tot->mliter += o2.mliter; + he_tot->mliter += he.mliter; + } + } + } +} diff --git a/core/statistics.h b/core/statistics.h new file mode 100644 index 000000000..015c3481e --- /dev/null +++ b/core/statistics.h @@ -0,0 +1,59 @@ +/* + * statistics.h + * + * core logic functions called from statistics UI + * common types and variables + */ + +#ifndef STATISTICS_H +#define STATISTICS_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + int period; + duration_t total_time; + /* avg_time is simply total_time / nr -- let's not keep this */ + duration_t shortest_time; + duration_t longest_time; + depth_t max_depth; + depth_t min_depth; + depth_t avg_depth; + volume_t max_sac; + volume_t min_sac; + volume_t avg_sac; + int max_temp; + int min_temp; + double combined_temp; + unsigned int combined_count; + unsigned int selection_size; + unsigned int total_sac_time; + bool is_year; + bool is_trip; + char *location; +} stats_t; +extern stats_t stats_selection; +extern stats_t *stats_yearly; +extern stats_t *stats_monthly; +extern stats_t *stats_by_trip; +extern stats_t *stats_by_type; + +extern char *get_time_string_s(int seconds, int maxdays, bool freediving); +extern char *get_minutes(int seconds); +extern void process_all_dives(struct dive *dive, struct dive **prev_dive); +extern void get_selected_dives_text(char *buffer, size_t size); +extern void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]); +extern void process_selected_dives(void); +void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot); + +inline char *get_time_string(int seconds, int maxdays) { + return get_time_string_s( seconds, maxdays, false); +} +#ifdef __cplusplus +} +#endif + +#endif // STATISTICS_H diff --git a/core/strndup.h b/core/strndup.h new file mode 100644 index 000000000..84e18b60f --- /dev/null +++ b/core/strndup.h @@ -0,0 +1,21 @@ +#ifndef STRNDUP_H +#define STRNDUP_H +#if __WIN32__ +static char *strndup (const char *s, size_t n) +{ + char *cpy; + size_t len = strlen(s); + if (n < len) + len = n; + if ((cpy = malloc(len + 1)) != + NULL) { + cpy[len] = + '\0'; + memcpy(cpy, + s, + len); + } + return cpy; +} +#endif +#endif /* STRNDUP_H */ diff --git a/core/strtod.c b/core/strtod.c new file mode 100644 index 000000000..81e5d42d1 --- /dev/null +++ b/core/strtod.c @@ -0,0 +1,128 @@ +/* + * Sane helper for 'strtod()'. + * + * Sad that we even need this, but the C library version has + * insane locale behavior, and while the Qt "doDouble()" routines + * are better in that regard, they don't have an end pointer + * (having replaced it with the completely idiotic "ok" boolean + * pointer instead). + * + * I wonder what drugs people are on sometimes. + * + * Right now we support the following flags to limit the + * parsing some ways: + * + * STRTOD_NO_SIGN - don't accept signs + * STRTOD_NO_DOT - no decimal dots, I'm European + * STRTOD_NO_COMMA - no comma, please, I'm C locale + * STRTOD_NO_EXPONENT - no exponent parsing, I'm human + * + * The "negative" flags are so that the common case can just + * use a flag value of 0, and only if you have some special + * requirements do you need to state those with explicit flags. + * + * So if you want the C locale kind of parsing, you'd use the + * STRTOD_NO_COMMA flag to disallow a decimal comma. But if you + * want a more relaxed "Hey, Europeans are people too, even if + * they have locales with commas", just pass in a zero flag. + */ +#include <ctype.h> +#include "dive.h" + +double strtod_flags(const char *str, const char **ptr, unsigned int flags) +{ + char c; + const char *p = str, *ep; + double val = 0.0; + double decimal = 1.0; + int sign = 0, esign = 0; + int numbers = 0, dot = 0; + + /* skip spaces */ + while (isspace(c = *p++)) + /* */; + + /* optional sign */ + if (!(flags & STRTOD_NO_SIGN)) { + switch (c) { + case '-': + sign = 1; + /* fallthrough */ + case '+': + c = *p++; + } + } + + /* Mantissa */ + for (;; c = *p++) { + if ((c == '.' && !(flags & STRTOD_NO_DOT)) || + (c == ',' && !(flags & STRTOD_NO_COMMA))) { + if (dot) + goto done; + dot = 1; + continue; + } + if (c >= '0' && c <= '9') { + numbers++; + val = (val * 10) + (c - '0'); + if (dot) + decimal *= 10; + continue; + } + if (c != 'e' && c != 'E') + goto done; + if (flags & STRTOD_NO_EXPONENT) + goto done; + break; + } + + if (!numbers) + goto done; + + /* Exponent */ + ep = p; + c = *ep++; + switch (c) { + case '-': + esign = 1; + /* fallthrough */ + case '+': + c = *ep++; + } + + if (c >= '0' && c <= '9') { + p = ep; + int exponent = c - '0'; + + for (;;) { + c = *p++; + if (c < '0' || c > '9') + break; + exponent *= 10; + exponent += c - '0'; + } + + /* We're not going to bother playing games */ + if (exponent > 308) + exponent = 308; + + while (exponent-- > 0) { + if (esign) + decimal *= 10; + else + decimal /= 10; + } + } + +done: + if (!numbers) + goto no_conversion; + if (ptr) + *ptr = p - 1; + return (sign ? -val : val) / decimal; + +no_conversion: + if (ptr) + *ptr = str; + return 0.0; +} diff --git a/core/subsurface-qt/DiveObjectHelper.cpp b/core/subsurface-qt/DiveObjectHelper.cpp new file mode 100644 index 000000000..ab88d2f3c --- /dev/null +++ b/core/subsurface-qt/DiveObjectHelper.cpp @@ -0,0 +1,338 @@ +#include "DiveObjectHelper.h" + +#include <QDateTime> +#include <QTextDocument> + +#include "../qthelper.h" +#include "../helpers.h" + +static QString EMPTY_DIVE_STRING = QStringLiteral("--"); +enum returnPressureSelector {START_PRESSURE, END_PRESSURE}; + +static QString getFormattedWeight(struct dive *dive, unsigned int idx) +{ + weightsystem_t *weight = &dive->weightsystem[idx]; + if (!weight->description) + return QString(EMPTY_DIVE_STRING); + QString fmt = QString(weight->description); + fmt += ", " + get_weight_string(weight->weight, true); + return fmt; +} + +static QString getFormattedCylinder(struct dive *dive, unsigned int idx) +{ + cylinder_t *cyl = &dive->cylinder[idx]; + const char *desc = cyl->type.description; + if (!desc && idx > 0) + return QString(EMPTY_DIVE_STRING); + QString fmt = desc ? QString(desc) : QObject::tr("unknown"); + fmt += ", " + get_volume_string(cyl->type.size, true); + fmt += ", " + get_pressure_string(cyl->type.workingpressure, true); + fmt += ", " + get_pressure_string(cyl->start, false) + " - " + get_pressure_string(cyl->end, true); + fmt += ", " + get_gas_string(cyl->gasmix); + return fmt; +} + +static QString getPressures(struct dive *dive, enum returnPressureSelector ret) +{ + cylinder_t *cyl = &dive->cylinder[0]; + QString fmt; + if (ret == START_PRESSURE) { + if (cyl->start.mbar) + fmt = get_pressure_string(cyl->start, true); + else if (cyl->sample_start.mbar) + fmt = get_pressure_string(cyl->sample_start, true); + } + if (ret == END_PRESSURE) { + if (cyl->end.mbar) + fmt = get_pressure_string(cyl->end, true); + else if(cyl->sample_end.mbar) + fmt = get_pressure_string(cyl->sample_end, true); + } + return fmt; +} + +DiveObjectHelper::DiveObjectHelper(struct dive *d) : + m_dive(d) +{ +} + +DiveObjectHelper::~DiveObjectHelper() +{ +} + +int DiveObjectHelper::number() const +{ + return m_dive->number; +} + +int DiveObjectHelper::id() const +{ + return m_dive->id; +} + +QString DiveObjectHelper::date() const +{ + QDateTime localTime = QDateTime::fromTime_t(m_dive->when - gettimezoneoffset(m_dive->when)); + localTime.setTimeSpec(Qt::UTC); + return localTime.date().toString(prefs.date_format); +} + +timestamp_t DiveObjectHelper::timestamp() const +{ + return m_dive->when; +} + +QString DiveObjectHelper::time() const +{ + QDateTime localTime = QDateTime::fromTime_t(m_dive->when - gettimezoneoffset(m_dive->when)); + localTime.setTimeSpec(Qt::UTC); + return localTime.time().toString(prefs.time_format); +} + +QString DiveObjectHelper::location() const +{ + return get_dive_location(m_dive) ? QString::fromUtf8(get_dive_location(m_dive)) : EMPTY_DIVE_STRING; +} + +QString DiveObjectHelper::gps() const +{ + struct dive_site *ds = get_dive_site_by_uuid(m_dive->dive_site_uuid); + return ds ? QString(printGPSCoords(ds->latitude.udeg, ds->longitude.udeg)) : QString(); +} +QString DiveObjectHelper::duration() const +{ + return get_dive_duration_string(m_dive->duration.seconds, QObject::tr("h:"), QObject::tr("min")); +} + +bool DiveObjectHelper::noDive() const +{ + return m_dive->duration.seconds == 0 && m_dive->dc.duration.seconds == 0; +} + +QString DiveObjectHelper::depth() const +{ + return get_depth_string(m_dive->dc.maxdepth.mm, true, true); +} + +QString DiveObjectHelper::divemaster() const +{ + return m_dive->divemaster ? m_dive->divemaster : EMPTY_DIVE_STRING; +} + +QString DiveObjectHelper::buddy() const +{ + return m_dive->buddy ? m_dive->buddy : EMPTY_DIVE_STRING; +} + +QString DiveObjectHelper::airTemp() const +{ + QString temp = get_temperature_string(m_dive->airtemp, true); + if (temp.isEmpty()) { + temp = EMPTY_DIVE_STRING; + } + return temp; +} + +QString DiveObjectHelper::waterTemp() const +{ + QString temp = get_temperature_string(m_dive->watertemp, true); + if (temp.isEmpty()) { + temp = EMPTY_DIVE_STRING; + } + return temp; +} + +QString DiveObjectHelper::notes() const +{ + QString tmp = m_dive->notes ? QString::fromUtf8(m_dive->notes) : EMPTY_DIVE_STRING; + if (same_string(m_dive->dc.model, "planned dive")) { + QTextDocument notes; + #define _NOTES_BR "\n" + tmp.replace("<thead>", "<thead>" _NOTES_BR) + .replace("<br>", "<br>" _NOTES_BR) + .replace("<tr>", "<tr>" _NOTES_BR) + .replace("</tr>", "</tr>" _NOTES_BR); + notes.setHtml(tmp); + tmp = notes.toPlainText(); + tmp.replace(_NOTES_BR, "<br>"); + #undef _NOTES_BR + } else { + tmp.replace("\n", "<br>"); + } + return tmp; +} + +QString DiveObjectHelper::tags() const +{ + static char buffer[256]; + taglist_get_tagstring(m_dive->tag_list, buffer, 256); + return QString(buffer); +} + +QString DiveObjectHelper::gas() const +{ + /*WARNING: here should be the gastlist, returned + * from the get_gas_string function or this is correct? + */ + QString gas, gases; + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (!is_cylinder_used(m_dive, i)) + continue; + gas = m_dive->cylinder[i].type.description; + if (!gas.isEmpty()) + gas += QChar(' '); + gas += gasname(&m_dive->cylinder[i].gasmix); + // if has a description and if such gas is not already present + if (!gas.isEmpty() && gases.indexOf(gas) == -1) { + if (!gases.isEmpty()) + gases += QString(" / "); + gases += gas; + } + } + return gases; +} + +QString DiveObjectHelper::sac() const +{ + if (!m_dive->sac) + return QString(); + const char *unit; + int decimal; + double value = get_volume_units(m_dive->sac, &decimal, &unit); + return QString::number(value, 'f', decimal).append(unit); +} + +QString DiveObjectHelper::weightList() const +{ + QString weights; + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + QString w = getFormattedWeight(m_dive, i); + if (w == EMPTY_DIVE_STRING) + continue; + weights += w + "; "; + } + return weights; +} + +QStringList DiveObjectHelper::weights() const +{ + QStringList weights; + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) + weights << getFormattedWeight(m_dive, i); + return weights; +} + +bool DiveObjectHelper::singleWeight() const +{ + return weightsystem_none(&m_dive->weightsystem[1]); +} + +QString DiveObjectHelper::weight(int idx) const +{ + if ( (idx < 0) || idx > MAX_WEIGHTSYSTEMS ) + return QString(EMPTY_DIVE_STRING); + return getFormattedWeight(m_dive, idx); +} + +QString DiveObjectHelper::suit() const +{ + return m_dive->suit ? m_dive->suit : EMPTY_DIVE_STRING; +} + +QString DiveObjectHelper::cylinderList() const +{ + QString cylinders; + for (int i = 0; i < MAX_CYLINDERS; i++) { + QString cyl = getFormattedCylinder(m_dive, i); + if (cyl == EMPTY_DIVE_STRING) + continue; + cylinders += cyl + "; "; + } + return cylinders; +} + +QStringList DiveObjectHelper::cylinders() const +{ + QStringList cylinders; + for (int i = 0; i < MAX_CYLINDERS; i++) + cylinders << getFormattedCylinder(m_dive, i); + return cylinders; +} + +QString DiveObjectHelper::cylinder(int idx) const +{ + if ( (idx < 0) || idx > MAX_CYLINDERS) + return QString(EMPTY_DIVE_STRING); + return getFormattedCylinder(m_dive, idx); +} + +QString DiveObjectHelper::trip() const +{ + return m_dive->divetrip ? m_dive->divetrip->location : EMPTY_DIVE_STRING; +} + +// combine the pointer address with the trip location so that +// we detect multiple, destinct trips to the same location +QString DiveObjectHelper::tripMeta() const +{ + QString ret = EMPTY_DIVE_STRING; + if (m_dive->divetrip) + ret = QString::number((quint64)m_dive->divetrip, 16) + QLatin1Literal("::") + m_dive->divetrip->location; + return ret; +} + +QString DiveObjectHelper::maxcns() const +{ + return QString(m_dive->maxcns); +} + +QString DiveObjectHelper::otu() const +{ + return QString(m_dive->otu); +} + +int DiveObjectHelper::rating() const +{ + return m_dive->rating; +} + +QString DiveObjectHelper::sumWeight() const +{ + weight_t sum = { 0 }; + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++){ + sum.grams += m_dive->weightsystem[i].weight.grams; + } + return get_weight_string(sum, true); +} + +QString DiveObjectHelper::getCylinder() const +{ + QString getCylinder; + if (is_cylinder_used(m_dive, 1)){ + getCylinder = QObject::tr("Multiple"); + } + else { + getCylinder = m_dive->cylinder[0].type.description; + } + return getCylinder; +} + +QString DiveObjectHelper::startPressure() const +{ + QString startPressure = getPressures(m_dive, START_PRESSURE); + return startPressure; +} + +QString DiveObjectHelper::endPressure() const +{ + QString endPressure = getPressures(m_dive, END_PRESSURE); + return endPressure; +} + +QString DiveObjectHelper::firstGas() const +{ + QString gas; + gas = get_gas_string(m_dive->cylinder[0].gasmix); + return gas; +} diff --git a/core/subsurface-qt/DiveObjectHelper.h b/core/subsurface-qt/DiveObjectHelper.h new file mode 100644 index 000000000..602775ef8 --- /dev/null +++ b/core/subsurface-qt/DiveObjectHelper.h @@ -0,0 +1,89 @@ +#ifndef DIVE_QOBJECT_H +#define DIVE_QOBJECT_H + +#include "../dive.h" +#include <QObject> +#include <QString> +#include <QStringList> + +class DiveObjectHelper : public QObject { + Q_OBJECT + Q_PROPERTY(int number READ number CONSTANT) + Q_PROPERTY(int id READ id CONSTANT) + Q_PROPERTY(int rating READ rating CONSTANT) + Q_PROPERTY(QString date READ date CONSTANT) + Q_PROPERTY(QString time READ time CONSTANT) + Q_PROPERTY(QString location READ location CONSTANT) + Q_PROPERTY(QString gps READ gps CONSTANT) + Q_PROPERTY(QString duration READ duration CONSTANT) + Q_PROPERTY(bool noDive READ noDive CONSTANT) + Q_PROPERTY(QString depth READ depth CONSTANT) + Q_PROPERTY(QString divemaster READ divemaster CONSTANT) + Q_PROPERTY(QString buddy READ buddy CONSTANT) + Q_PROPERTY(QString airTemp READ airTemp CONSTANT) + Q_PROPERTY(QString waterTemp READ waterTemp CONSTANT) + Q_PROPERTY(QString notes READ notes CONSTANT) + Q_PROPERTY(QString tags READ tags CONSTANT) + Q_PROPERTY(QString gas READ gas CONSTANT) + Q_PROPERTY(QString sac READ sac CONSTANT) + Q_PROPERTY(QString weightList READ weightList CONSTANT) + Q_PROPERTY(QStringList weights READ weights CONSTANT) + Q_PROPERTY(bool singleWeight READ singleWeight CONSTANT) + Q_PROPERTY(QString suit READ suit CONSTANT) + Q_PROPERTY(QString cylinderList READ cylinderList CONSTANT) + Q_PROPERTY(QStringList cylinders READ cylinders CONSTANT) + Q_PROPERTY(QString trip READ trip CONSTANT) + Q_PROPERTY(QString tripMeta READ tripMeta CONSTANT) + Q_PROPERTY(QString maxcns READ maxcns CONSTANT) + Q_PROPERTY(QString otu READ otu CONSTANT) + Q_PROPERTY(QString sumWeight READ sumWeight CONSTANT) + Q_PROPERTY(QString getCylinder READ getCylinder CONSTANT) + Q_PROPERTY(QString startPressure READ startPressure CONSTANT) + Q_PROPERTY(QString endPressure READ endPressure CONSTANT) + Q_PROPERTY(QString firstGas READ firstGas CONSTANT) +public: + DiveObjectHelper(struct dive *dive = NULL); + ~DiveObjectHelper(); + int number() const; + int id() const; + int rating() const; + QString date() const; + timestamp_t timestamp() const; + QString time() const; + QString location() const; + QString gps() const; + QString duration() const; + bool noDive() const; + QString depth() const; + QString divemaster() const; + QString buddy() const; + QString airTemp() const; + QString waterTemp() const; + QString notes() const; + QString tags() const; + QString gas() const; + QString sac() const; + QString weightList() const; + QStringList weights() const; + QString weight(int idx) const; + bool singleWeight() const; + QString suit() const; + QString cylinderList() const; + QStringList cylinders() const; + QString cylinder(int idx) const; + QString trip() const; + QString tripMeta() const; + QString maxcns() const; + QString otu() const; + QString sumWeight() const; + QString getCylinder() const; + QString startPressure() const; + QString endPressure() const; + QString firstGas() const; + +private: + struct dive *m_dive; +}; + Q_DECLARE_METATYPE(DiveObjectHelper *) + +#endif diff --git a/core/subsurface-qt/SettingsObjectWrapper.cpp b/core/subsurface-qt/SettingsObjectWrapper.cpp new file mode 100644 index 000000000..e43be1a9b --- /dev/null +++ b/core/subsurface-qt/SettingsObjectWrapper.cpp @@ -0,0 +1,1617 @@ +#include "SettingsObjectWrapper.h" +#include <QSettings> +#include <QApplication> +#include <QFont> + +#include "../dive.h" // TODO: remove copy_string from dive.h + + +static QString tecDetails = QStringLiteral("TecDetails"); + +PartialPressureGasSettings::PartialPressureGasSettings(QObject* parent): + QObject(parent), + group("TecDetails") +{ + +} + +short PartialPressureGasSettings::showPo2() const +{ + return prefs.pp_graphs.po2; +} + +short PartialPressureGasSettings::showPn2() const +{ + return prefs.pp_graphs.pn2; +} + +short PartialPressureGasSettings::showPhe() const +{ + return prefs.pp_graphs.phe; +} + +double PartialPressureGasSettings::po2Threshold() const +{ + return prefs.pp_graphs.po2_threshold; +} + +double PartialPressureGasSettings::pn2Threshold() const +{ + return prefs.pp_graphs.pn2_threshold; +} + +double PartialPressureGasSettings::pheThreshold() const +{ + return prefs.pp_graphs.phe_threshold; +} + +void PartialPressureGasSettings::setShowPo2(short value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("po2graph", value); + prefs.pp_graphs.po2 = value; + emit showPo2Changed(value); +} + +void PartialPressureGasSettings::setShowPn2(short value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("pn2graph", value); + prefs.pp_graphs.pn2 = value; + emit showPn2Changed(value); +} + +void PartialPressureGasSettings::setShowPhe(short value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("phegraph", value); + prefs.pp_graphs.phe = value; + emit showPheChanged(value); +} + +void PartialPressureGasSettings::setPo2Threshold(double value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("po2threshold", value); + prefs.pp_graphs.po2_threshold = value; + emit po2ThresholdChanged(value); +} + +void PartialPressureGasSettings::setPn2Threshold(double value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("pn2threshold", value); + prefs.pp_graphs.pn2_threshold = value; + emit pn2ThresholdChanged(value); +} + +void PartialPressureGasSettings::setPheThreshold(double value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("phethreshold", value); + prefs.pp_graphs.phe_threshold = value; + emit pheThresholdChanged(value); +} + + +TechnicalDetailsSettings::TechnicalDetailsSettings(QObject* parent): QObject(parent) +{ + +} + +double TechnicalDetailsSettings:: modp02() const +{ + return prefs.modpO2; +} + +bool TechnicalDetailsSettings::ead() const +{ + return prefs.ead; +} + +bool TechnicalDetailsSettings::dcceiling() const +{ + return prefs.dcceiling; +} + +bool TechnicalDetailsSettings::redceiling() const +{ + return prefs.redceiling; +} + +bool TechnicalDetailsSettings::calcceiling() const +{ + return prefs.calcceiling; +} + +bool TechnicalDetailsSettings::calcceiling3m() const +{ + return prefs.calcceiling3m; +} + +bool TechnicalDetailsSettings::calcalltissues() const +{ + return prefs.calcalltissues; +} + +bool TechnicalDetailsSettings::calcndltts() const +{ + return prefs.calcndltts; +} + +bool TechnicalDetailsSettings::gflow() const +{ + return prefs.gflow; +} + +bool TechnicalDetailsSettings::gfhigh() const +{ + return prefs.gfhigh; +} + +bool TechnicalDetailsSettings::hrgraph() const +{ + return prefs.hrgraph; +} + +bool TechnicalDetailsSettings::tankBar() const +{ + return prefs.tankbar; +} + +bool TechnicalDetailsSettings::percentageGraph() const +{ + return prefs.percentagegraph; +} + +bool TechnicalDetailsSettings::rulerGraph() const +{ + return prefs.rulergraph; +} + +bool TechnicalDetailsSettings::showCCRSetpoint() const +{ + return prefs.show_ccr_setpoint; +} + +bool TechnicalDetailsSettings::showCCRSensors() const +{ + return prefs.show_ccr_sensors; +} + +bool TechnicalDetailsSettings::zoomedPlot() const +{ + return prefs.zoomed_plot; +} + +bool TechnicalDetailsSettings::showSac() const +{ + return prefs.show_sac; +} + +bool TechnicalDetailsSettings::gfLowAtMaxDepth() const +{ + return prefs.gf_low_at_maxdepth; +} + +bool TechnicalDetailsSettings::displayUnusedTanks() const +{ + return prefs.display_unused_tanks; +} + +bool TechnicalDetailsSettings::showAverageDepth() const +{ + return prefs.show_average_depth; +} + +bool TechnicalDetailsSettings::mod() const +{ + return prefs.mod; +} + +bool TechnicalDetailsSettings::showPicturesInProfile() const +{ + return prefs.show_pictures_in_profile; +} + +void TechnicalDetailsSettings::setModp02(double value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("modpO2", value); + prefs.modpO2 = value; + emit modpO2Changed(value); +} + +void TechnicalDetailsSettings::setShowPicturesInProfile(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("show_pictures_in_profile", value); + prefs.show_pictures_in_profile = value; + emit showPicturesInProfileChanged(value); +} + +void TechnicalDetailsSettings::setEad(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("ead", value); + prefs.ead = value; + emit eadChanged(value); +} + +void TechnicalDetailsSettings::setMod(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("mod", value); + prefs.mod = value; + emit modChanged(value); +} + +void TechnicalDetailsSettings::setDCceiling(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("dcceiling", value); + prefs.dcceiling = value; + emit dcceilingChanged(value); +} + +void TechnicalDetailsSettings::setRedceiling(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("redceiling", value); + prefs.redceiling = value; + emit redceilingChanged(value); +} + +void TechnicalDetailsSettings::setCalcceiling(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("calcceiling", value); + prefs.calcceiling = value; + emit calcceilingChanged(value); +} + +void TechnicalDetailsSettings::setCalcceiling3m(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("calcceiling3m", value); + prefs.calcceiling3m = value; + emit calcceiling3mChanged(value); +} + +void TechnicalDetailsSettings::setCalcalltissues(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("calcalltissues", value); + prefs.calcalltissues = value; + emit calcalltissuesChanged(value); +} + +void TechnicalDetailsSettings::setCalcndltts(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("calcndltts", value); + prefs.calcndltts = value; + emit calcndlttsChanged(value); +} + +void TechnicalDetailsSettings::setGflow(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("gflow", value); + prefs.gflow = value; + set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); + emit gflowChanged(value); +} + +void TechnicalDetailsSettings::setGfhigh(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("gfhigh", value); + prefs.gfhigh = value; + set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); + emit gfhighChanged(value); +} + +void TechnicalDetailsSettings::setHRgraph(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("hrgraph", value); + prefs.hrgraph = value; + emit hrgraphChanged(value); +} + +void TechnicalDetailsSettings::setTankBar(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("tankbar", value); + prefs.tankbar = value; + emit tankBarChanged(value); +} + +void TechnicalDetailsSettings::setPercentageGraph(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("percentagegraph", value); + prefs.percentagegraph = value; + emit percentageGraphChanged(value); +} + +void TechnicalDetailsSettings::setRulerGraph(bool value) +{ + /* TODO: search for the QSettings of the RulerBar */ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("RulerBar", value); + prefs.rulergraph = value; + emit rulerGraphChanged(value); +} + +void TechnicalDetailsSettings::setShowCCRSetpoint(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("show_ccr_setpoint", value); + prefs.show_ccr_setpoint = value; + emit showCCRSetpointChanged(value); +} + +void TechnicalDetailsSettings::setShowCCRSensors(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("show_ccr_sensors", value); + prefs.show_ccr_sensors = value; + emit showCCRSensorsChanged(value); +} + +void TechnicalDetailsSettings::setZoomedPlot(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("zoomed_plot", value); + prefs.zoomed_plot = value; + emit zoomedPlotChanged(value); +} + +void TechnicalDetailsSettings::setShowSac(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("show_sac", value); + prefs.show_sac = value; + emit showSacChanged(value); +} + +void TechnicalDetailsSettings::setGfLowAtMaxDepth(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("gf_low_at_maxdepth", value); + prefs.gf_low_at_maxdepth = value; + set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); + emit gfLowAtMaxDepthChanged(value); +} + +void TechnicalDetailsSettings::setDisplayUnusedTanks(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("display_unused_tanks", value); + prefs.display_unused_tanks = value; + emit displayUnusedTanksChanged(value); +} + +void TechnicalDetailsSettings::setShowAverageDepth(bool value) +{ + QSettings s; + s.beginGroup(tecDetails); + s.setValue("show_average_depth", value); + prefs.show_average_depth = value; + emit showAverageDepthChanged(value); +} + + + +FacebookSettings::FacebookSettings(QObject *parent) : + QObject(parent), + group(QStringLiteral("WebApps")), + subgroup(QStringLiteral("Facebook")) +{ +} + +QString FacebookSettings::accessToken() const +{ + return QString(prefs.facebook.access_token); +} + +QString FacebookSettings::userId() const +{ + return QString(prefs.facebook.user_id); +} + +QString FacebookSettings::albumId() const +{ + return QString(prefs.facebook.album_id); +} + +void FacebookSettings::setAccessToken (const QString& value) +{ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup(group); + s.beginGroup(subgroup); + s.setValue("ConnectToken", value); +#endif + prefs.facebook.access_token = copy_string(qPrintable(value)); + emit accessTokenChanged(value); +} + +void FacebookSettings::setUserId(const QString& value) +{ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup(group); + s.beginGroup(subgroup); + s.setValue("UserId", value); +#endif + prefs.facebook.user_id = copy_string(qPrintable(value)); + emit userIdChanged(value); +} + +void FacebookSettings::setAlbumId(const QString& value) +{ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup(group); + s.beginGroup(subgroup); + s.setValue("AlbumId", value); +#endif + prefs.facebook.album_id = copy_string(qPrintable(value)); + emit albumIdChanged(value); +} + + +GeocodingPreferences::GeocodingPreferences(QObject *parent) : + QObject(parent), + group(QStringLiteral("geocoding")) +{ + +} + +bool GeocodingPreferences::enableGeocoding() const +{ + return prefs.geocoding.enable_geocoding; +} + +bool GeocodingPreferences::parseDiveWithoutGps() const +{ + return prefs.geocoding.parse_dive_without_gps; +} + +bool GeocodingPreferences::tagExistingDives() const +{ + return prefs.geocoding.tag_existing_dives; +} + +taxonomy_category GeocodingPreferences::firstTaxonomyCategory() const +{ + return prefs.geocoding.category[0]; +} + +taxonomy_category GeocodingPreferences::secondTaxonomyCategory() const +{ + return prefs.geocoding.category[1]; +} + +taxonomy_category GeocodingPreferences::thirdTaxonomyCategory() const +{ + return prefs.geocoding.category[2]; +} + +void GeocodingPreferences::setEnableGeocoding(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("enable_geocoding", value); + prefs.geocoding.enable_geocoding = value; + emit enableGeocodingChanged(value); +} + +void GeocodingPreferences::setParseDiveWithoutGps(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("parse_dives_without_gps", value); + prefs.geocoding.parse_dive_without_gps = value; + emit parseDiveWithoutGpsChanged(value); +} + +void GeocodingPreferences::setTagExistingDives(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("tag_existing_dives", value); + prefs.geocoding.tag_existing_dives = value; + emit tagExistingDivesChanged(value); +} + +void GeocodingPreferences::setFirstTaxonomyCategory(taxonomy_category value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("cat0", value); + prefs.show_average_depth = value; + emit firstTaxonomyCategoryChanged(value); +} + +void GeocodingPreferences::setSecondTaxonomyCategory(taxonomy_category value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("cat1", value); + prefs.show_average_depth = value; + emit secondTaxonomyCategoryChanged(value); +} + +void GeocodingPreferences::setThirdTaxonomyCategory(taxonomy_category value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("cat2", value); + prefs.show_average_depth = value; + emit thirdTaxonomyCategoryChanged(value); +} + +ProxySettings::ProxySettings(QObject *parent) : + QObject(parent), + group(QStringLiteral("Network")) +{ +} + +int ProxySettings::type() const +{ + return prefs.proxy_type; +} + +QString ProxySettings::host() const +{ + return prefs.proxy_host; +} + +int ProxySettings::port() const +{ + return prefs.proxy_port; +} + +short ProxySettings::auth() const +{ + return prefs.proxy_auth; +} + +QString ProxySettings::user() const +{ + return prefs.proxy_user; +} + +QString ProxySettings::pass() const +{ + return prefs.proxy_pass; +} + +void ProxySettings::setType(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_type", value); + prefs.proxy_type = value; + emit typeChanged(value); +} + +void ProxySettings::setHost(const QString& value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_host", value); + free(prefs.proxy_host); + prefs.proxy_host = copy_string(qPrintable(value));; + emit hostChanged(value); +} + +void ProxySettings::setPort(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_port", value); + prefs.proxy_port = value; + emit portChanged(value); +} + +void ProxySettings::setAuth(short value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_auth", value); + prefs.proxy_auth = value; + emit authChanged(value); +} + +void ProxySettings::setUser(const QString& value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_user", value); + free(prefs.proxy_user); + prefs.proxy_user = copy_string(qPrintable(value)); + emit userChanged(value); +} + +void ProxySettings::setPass(const QString& value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("proxy_pass", value); + free(prefs.proxy_pass); + prefs.proxy_pass = copy_string(qPrintable(value)); + emit passChanged(value); +} + +CloudStorageSettings::CloudStorageSettings(QObject *parent) : + QObject(parent), + group(QStringLiteral("CloudStorage")) +{ + +} + +bool CloudStorageSettings::gitLocalOnly() const +{ + return prefs.git_local_only; +} + +QString CloudStorageSettings::password() const +{ + return QString(prefs.cloud_storage_password); +} + +QString CloudStorageSettings::newPassword() const +{ + return QString(prefs.cloud_storage_newpassword); +} + +QString CloudStorageSettings::email() const +{ + return QString(prefs.cloud_storage_email); +} + +QString CloudStorageSettings::emailEncoded() const +{ + return QString(prefs.cloud_storage_email_encoded); +} + +bool CloudStorageSettings::savePasswordLocal() const +{ + return prefs.save_password_local; +} + +short CloudStorageSettings::verificationStatus() const +{ + return prefs.cloud_verification_status; +} + +bool CloudStorageSettings::backgroundSync() const +{ + return prefs.cloud_background_sync; +} + +QString CloudStorageSettings::userId() const +{ + return QString(prefs.userid); +} + +QString CloudStorageSettings::baseUrl() const +{ + return QString(prefs.cloud_base_url); +} + +QString CloudStorageSettings::gitUrl() const +{ + return QString(prefs.cloud_git_url); +} + +void CloudStorageSettings::setPassword(const QString& value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("password", value); + free(prefs.proxy_pass); + prefs.proxy_pass = copy_string(qPrintable(value)); + emit passwordChanged(value); +} + +void CloudStorageSettings::setNewPassword(const QString& value) +{ + /*TODO: This looks like wrong, but 'new password' is not saved on disk, why it's on prefs? */ + free(prefs.cloud_storage_newpassword); + prefs.cloud_storage_newpassword = copy_string(qPrintable(value)); + emit newPasswordChanged(value); +} + +void CloudStorageSettings::setEmail(const QString& value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("email", value); + free(prefs.cloud_storage_email); + prefs.cloud_storage_email = copy_string(qPrintable(value)); + emit emailChanged(value); +} + +void CloudStorageSettings::setUserId(const QString& value) +{ + //WARNING: UserId is stored outside of any group, but it belongs to Cloud Storage. + QSettings s; + s.setValue("subsurface_webservice_uid", value); + free(prefs.userid); + prefs.userid = copy_string(qPrintable(value)); + emit userIdChanged(value); +} + +void CloudStorageSettings::setEmailEncoded(const QString& value) +{ + /*TODO: This looks like wrong, but 'email encoded' is not saved on disk, why it's on prefs? */ + free(prefs.cloud_storage_email_encoded); + prefs.cloud_storage_email_encoded = copy_string(qPrintable(value)); + emit emailEncodedChanged(value); +} + +void CloudStorageSettings::setSavePasswordLocal(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("save_password_local", value); + prefs.save_password_local = value; + emit savePasswordLocalChanged(value); +} + +void CloudStorageSettings::setVerificationStatus(short value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("cloud_verification_status", value); + prefs.cloud_verification_status = value; + emit verificationStatusChanged(value); +} + +void CloudStorageSettings::setBackgroundSync(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("cloud_background_sync", value); + prefs.cloud_background_sync = value; + emit backgroundSyncChanged(value); +} + +void CloudStorageSettings::setBaseUrl(const QString& value) +{ + free((void*)prefs.cloud_base_url); + free((void*)prefs.cloud_git_url); + prefs.cloud_base_url = copy_string(qPrintable(value)); + prefs.cloud_git_url = copy_string(qPrintable(QString(prefs.cloud_base_url) + "/git")); +} + +void CloudStorageSettings::setGitUrl(const QString& value) +{ + Q_UNUSED(value); /* no op */ +} + +void CloudStorageSettings::setGitLocalOnly(bool value) +{ + prefs.git_local_only = value; +} + +DivePlannerSettings::DivePlannerSettings(QObject *parent) : + QObject(parent), + group(QStringLiteral("Planner")) +{ +} + +bool DivePlannerSettings::lastStop() const +{ + return prefs.last_stop; +} + +bool DivePlannerSettings::verbatimPlan() const +{ + return prefs.verbatim_plan; +} + +bool DivePlannerSettings::displayRuntime() const +{ + return prefs.display_runtime; +} + +bool DivePlannerSettings::displayDuration() const +{ + return prefs.display_duration; +} + +bool DivePlannerSettings::displayTransitions() const +{ + return prefs.display_transitions; +} + +bool DivePlannerSettings::doo2breaks() const +{ + return prefs.doo2breaks; +} + +bool DivePlannerSettings::dropStoneMode() const +{ + return prefs.drop_stone_mode; +} + +bool DivePlannerSettings::safetyStop() const +{ + return prefs.safetystop; +} + +bool DivePlannerSettings::switchAtRequiredStop() const +{ + return prefs.switch_at_req_stop; +} + +int DivePlannerSettings::ascrate75() const +{ + return prefs.ascrate75; +} + +int DivePlannerSettings::ascrate50() const +{ + return prefs.ascrate50; +} + +int DivePlannerSettings::ascratestops() const +{ + return prefs.ascratestops; +} + +int DivePlannerSettings::ascratelast6m() const +{ + return prefs.ascratelast6m; +} + +int DivePlannerSettings::descrate() const +{ + return prefs.descrate; +} + +int DivePlannerSettings::bottompo2() const +{ + return prefs.bottompo2; +} + +int DivePlannerSettings::decopo2() const +{ + return prefs.decopo2; +} + +int DivePlannerSettings::reserveGas() const +{ + return prefs.reserve_gas; +} + +int DivePlannerSettings::minSwitchDuration() const +{ + return prefs.min_switch_duration; +} + +int DivePlannerSettings::bottomSac() const +{ + return prefs.bottomsac; +} + +int DivePlannerSettings::decoSac() const +{ + return prefs.decosac; +} + +short DivePlannerSettings::conservatismLevel() const +{ + return prefs.conservatism_level; +} + +deco_mode DivePlannerSettings::decoMode() const +{ + return prefs.deco_mode; +} + +void DivePlannerSettings::setLastStop(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("last_stop", value); + prefs.last_stop = value; + emit lastStopChanged(value); +} + +void DivePlannerSettings::setVerbatimPlan(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("verbatim_plan", value); + prefs.verbatim_plan = value; + emit verbatimPlanChanged(value); +} + +void DivePlannerSettings::setDisplayRuntime(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("display_runtime", value); + prefs.display_runtime = value; + emit displayRuntimeChanged(value); +} + +void DivePlannerSettings::setDisplayDuration(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("display_duration", value); + prefs.display_duration = value; + emit displayDurationChanged(value); +} + +void DivePlannerSettings::setDisplayTransitions(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("display_transitions", value); + prefs.display_transitions = value; + emit displayTransitionsChanged(value); +} + +void DivePlannerSettings::setDoo2breaks(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("doo2breaks", value); + prefs.doo2breaks = value; + emit doo2breaksChanged(value); +} + +void DivePlannerSettings::setDropStoneMode(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("drop_stone_mode", value); + prefs.drop_stone_mode = value; + emit dropStoneModeChanged(value); +} + +void DivePlannerSettings::setSafetyStop(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("safetystop", value); + prefs.safetystop = value; + emit safetyStopChanged(value); +} + +void DivePlannerSettings::setSwitchAtRequiredStop(bool value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("switch_at_req_stop", value); + prefs.switch_at_req_stop = value; + emit switchAtRequiredStopChanged(value); +} + +void DivePlannerSettings::setAscrate75(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("ascrate75", value); + prefs.ascrate75 = value; + emit ascrate75Changed(value); +} + +void DivePlannerSettings::setAscrate50(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("ascrate50", value); + prefs.ascrate50 = value; + emit ascrate50Changed(value); +} + +void DivePlannerSettings::setAscratestops(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("ascratestops", value); + prefs.ascratestops = value; + emit ascratestopsChanged(value); +} + +void DivePlannerSettings::setAscratelast6m(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("ascratelast6m", value); + prefs.ascratelast6m = value; + emit ascratelast6mChanged(value); +} + +void DivePlannerSettings::setDescrate(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("descrate", value); + prefs.descrate = value; + emit descrateChanged(value); +} + +void DivePlannerSettings::setBottompo2(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("bottompo2", value); + prefs.bottompo2 = value; + emit bottompo2Changed(value); +} + +void DivePlannerSettings::setDecopo2(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("decopo2", value); + prefs.decopo2 = value; + emit decopo2Changed(value); +} + +void DivePlannerSettings::setReserveGas(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("reserve_gas", value); + prefs.reserve_gas = value; + emit reserveGasChanged(value); +} + +void DivePlannerSettings::setMinSwitchDuration(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("min_switch_duration", value); + prefs.min_switch_duration = value; + emit minSwitchDurationChanged(value); +} + +void DivePlannerSettings::setBottomSac(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("bottomsac", value); + prefs.bottomsac = value; + emit bottomSacChanged(value); +} + +void DivePlannerSettings::setSecoSac(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("decosac", value); + prefs.decosac = value; + emit decoSacChanged(value); +} + +void DivePlannerSettings::setConservatismLevel(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("conservatism", value); + prefs.conservatism_level = value; + emit conservatismLevelChanged(value); +} + +void DivePlannerSettings::setDecoMode(deco_mode value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("deco_mode", value); + prefs.deco_mode = value; + emit decoModeChanged(value); +} + +UnitsSettings::UnitsSettings(QObject *parent) : + QObject(parent), + group(QStringLiteral("Units")) +{ + +} + +int UnitsSettings::length() const +{ + return prefs.units.length; +} + +int UnitsSettings::pressure() const +{ + return prefs.units.pressure; +} + +int UnitsSettings::volume() const +{ + return prefs.units.volume; +} + +int UnitsSettings::temperature() const +{ + return prefs.units.temperature; +} + +int UnitsSettings::weight() const +{ + return prefs.units.weight; +} + +int UnitsSettings::verticalSpeedTime() const +{ + return prefs.units.vertical_speed_time; +} + +QString UnitsSettings::unitSystem() const +{ + return QString(); /*FIXME: there's no char * units on the prefs. */ +} + +bool UnitsSettings::coordinatesTraditional() const +{ + return prefs.coordinates_traditional; +} + +void UnitsSettings::setLength(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("length", value); + prefs.units.length = (units::LENGHT) value; + emit lengthChanged(value); +} + +void UnitsSettings::setPressure(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("pressure", value); + prefs.units.pressure = (units::PRESSURE) value; + emit pressureChanged(value); +} + +void UnitsSettings::setVolume(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("volume", value); + prefs.units.volume = (units::VOLUME) value; + emit volumeChanged(value); +} + +void UnitsSettings::setTemperature(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("temperature", value); + prefs.units.temperature = (units::TEMPERATURE) value; + emit temperatureChanged(value); +} + +void UnitsSettings::setWeight(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("weight", value); + prefs.units.weight = (units::WEIGHT) value; + emit weightChanged(value); +} + +void UnitsSettings::setVerticalSpeedTime(int value) +{ + QSettings s; + s.beginGroup(group); + s.setValue("vertical_speed_time", value); + prefs.units.vertical_speed_time = (units::TIME) value; + emit verticalSpeedTimeChanged(value); +} + +void UnitsSettings::setCoordinatesTraditional(bool value) +{ + QSettings s; + s.setValue("coordinates", value); + prefs.coordinates_traditional = value; + emit coordinatesTraditionalChanged(value); +} + +void UnitsSettings::setUnitSystem(const QString& value) +{ + QSettings s; + s.setValue("unit_system", value); + + if (value == QStringLiteral("metric")) { + prefs.unit_system = METRIC; + prefs.units = SI_units; + } else if (value == QStringLiteral("imperial")) { + prefs.unit_system = IMPERIAL; + prefs.units = IMPERIAL_units; + } else { + prefs.unit_system = PERSONALIZE; + } + + emit unitSystemChanged(value); + // TODO: emit the other values here? +} + +GeneralSettingsObjectWrapper::GeneralSettingsObjectWrapper(QObject *parent) : + QObject(parent), + group(QStringLiteral("GeneralSettings")) +{ +} + +QString GeneralSettingsObjectWrapper::defaultFilename() const +{ + return prefs.default_filename; +} + +QString GeneralSettingsObjectWrapper::defaultCylinder() const +{ + return prefs.default_cylinder; +} + +short GeneralSettingsObjectWrapper::defaultFileBehavior() const +{ + return prefs.default_file_behavior; +} + +bool GeneralSettingsObjectWrapper::useDefaultFile() const +{ + return prefs.use_default_file; +} + +int GeneralSettingsObjectWrapper::defaultSetPoint() const +{ + return prefs.defaultsetpoint; +} + +int GeneralSettingsObjectWrapper::o2Consumption() const +{ + return prefs.o2consumption; +} + +int GeneralSettingsObjectWrapper::pscrRatio() const +{ + return prefs.pscr_ratio; +} + +void GeneralSettingsObjectWrapper::setDefaultFilename(const QString& value) +{ + QSettings s; + s.setValue("default_filename", value); + prefs.default_filename = copy_string(qPrintable(value)); + emit defaultFilenameChanged(value); +} + +void GeneralSettingsObjectWrapper::setDefaultCylinder(const QString& value) +{ + QSettings s; + s.setValue("default_cylinder", value); + prefs.default_cylinder = copy_string(qPrintable(value)); + emit defaultCylinderChanged(value); +} + +void GeneralSettingsObjectWrapper::setDefaultFileBehavior(short value) +{ + QSettings s; + s.setValue("default_file_behavior", value); + prefs.default_file_behavior = value; + if (prefs.default_file_behavior == UNDEFINED_DEFAULT_FILE) { + // undefined, so check if there's a filename set and + // use that, otherwise go with no default file + if (QString(prefs.default_filename).isEmpty()) + prefs.default_file_behavior = NO_DEFAULT_FILE; + else + prefs.default_file_behavior = LOCAL_DEFAULT_FILE; + } + emit defaultFileBehaviorChanged(value); +} + +void GeneralSettingsObjectWrapper::setUseDefaultFile(bool value) +{ + QSettings s; + s.setValue("use_default_file", value); + prefs.use_default_file = value; + emit useDefaultFileChanged(value); +} + +void GeneralSettingsObjectWrapper::setDefaultSetPoint(int value) +{ + QSettings s; + s.setValue("defaultsetpoint", value); + prefs.defaultsetpoint = value; + emit defaultSetPointChanged(value); +} + +void GeneralSettingsObjectWrapper::setO2Consumption(int value) +{ + QSettings s; + s.setValue("o2consumption", value); + prefs.o2consumption = value; + emit o2ConsumptionChanged(value); +} + +void GeneralSettingsObjectWrapper::setPscrRatio(int value) +{ + QSettings s; + s.setValue("pscr_ratio", value); + prefs.pscr_ratio = value; + emit pscrRatioChanged(value); +} + +DisplaySettingsObjectWrapper::DisplaySettingsObjectWrapper(QObject *parent) : + QObject(parent), + group(QStringLiteral("Display")) +{ +} + +QString DisplaySettingsObjectWrapper::divelistFont() const +{ + return prefs.divelist_font; +} + +double DisplaySettingsObjectWrapper::fontSize() const +{ + return prefs.font_size; +} + +short DisplaySettingsObjectWrapper::displayInvalidDives() const +{ + return prefs.display_invalid_dives; +} + +void DisplaySettingsObjectWrapper::setDivelistFont(const QString& value) +{ + QSettings s; + s.setValue("divelist_font", value); + QString newValue = value; + if (value.contains(",")) + newValue = value.left(value.indexOf(",")); + + if (!subsurface_ignore_font(newValue.toUtf8().constData())) { + free((void *)prefs.divelist_font); + prefs.divelist_font = strdup(newValue.toUtf8().constData()); + qApp->setFont(QFont(newValue)); + } + emit divelistFontChanged(newValue); +} + +void DisplaySettingsObjectWrapper::setFontSize(double value) +{ + QSettings s; + s.setValue("font_size", value); + prefs.font_size = value; + QFont defaultFont = qApp->font(); + defaultFont.setPointSizeF(prefs.font_size); + qApp->setFont(defaultFont); + emit fontSizeChanged(value); +} + +void DisplaySettingsObjectWrapper::setDisplayInvalidDives(short value) +{ + QSettings s; + s.setValue("displayinvalid", value); + prefs.display_invalid_dives = value; + emit displayInvalidDivesChanged(value); +} + +LanguageSettingsObjectWrapper::LanguageSettingsObjectWrapper(QObject *parent) : + QObject(parent), + group("Language") +{ +} + +QString LanguageSettingsObjectWrapper::language() const +{ + return prefs.locale.language; +} + +QString LanguageSettingsObjectWrapper::timeFormat() const +{ + return prefs.time_format; +} + +QString LanguageSettingsObjectWrapper::dateFormat() const +{ + return prefs.date_format; +} + +QString LanguageSettingsObjectWrapper::dateFormatShort() const +{ + return prefs.date_format_short; +} + +bool LanguageSettingsObjectWrapper::timeFormatOverride() const +{ + return prefs.time_format_override; +} + +bool LanguageSettingsObjectWrapper::dateFormatOverride() const +{ + return prefs.date_format_override; +} + +bool LanguageSettingsObjectWrapper::useSystemLanguage() const +{ + return prefs.locale.use_system_language; +} + +void LanguageSettingsObjectWrapper::setUseSystemLanguage(bool value) +{ + QSettings s; + s.setValue("UseSystemLanguage", value); + prefs.locale.use_system_language = copy_string(qPrintable(value)); + emit useSystemLanguageChanged(value); +} + +void LanguageSettingsObjectWrapper::setLanguage(const QString& value) +{ + QSettings s; + s.setValue("UiLanguage", value); + prefs.locale.language = copy_string(qPrintable(value)); + emit languageChanged(value); +} + +void LanguageSettingsObjectWrapper::setTimeFormat(const QString& value) +{ + QSettings s; + s.setValue("time_format", value); + prefs.time_format = copy_string(qPrintable(value));; + emit timeFormatChanged(value); +} + +void LanguageSettingsObjectWrapper::setDateFormat(const QString& value) +{ + QSettings s; + s.setValue("date_format", value); + prefs.date_format = copy_string(qPrintable(value));; + emit dateFormatChanged(value); +} + +void LanguageSettingsObjectWrapper::setDateFormatShort(const QString& value) +{ + QSettings s; + s.setValue("date_format_short", value); + prefs.date_format_short = copy_string(qPrintable(value));; + emit dateFormatShortChanged(value); +} + +void LanguageSettingsObjectWrapper::setTimeFormatOverride(bool value) +{ + QSettings s; + s.setValue("time_format_override", value); + prefs.time_format_override = value; + emit timeFormatOverrideChanged(value); +} + +void LanguageSettingsObjectWrapper::setDateFormatOverride(bool value) +{ + QSettings s; + s.setValue("date_format_override", value); + prefs.date_format_override = value; + emit dateFormatOverrideChanged(value); +} + +AnimationsSettingsObjectWrapper::AnimationsSettingsObjectWrapper(QObject* parent): + QObject(parent), + group("Animations") + +{ +} + +int AnimationsSettingsObjectWrapper::animationSpeed() const +{ + return prefs.animation_speed; +} + +void AnimationsSettingsObjectWrapper::setAnimationSpeed(int value) +{ + QSettings s; + s.setValue("animation_speed", value); + prefs.animation_speed = value; + emit animationSpeedChanged(value); +} + +LocationServiceSettingsObjectWrapper::LocationServiceSettingsObjectWrapper(QObject* parent): + QObject(parent), + group("locationService") +{ +} + +int LocationServiceSettingsObjectWrapper::distanceThreshold() const +{ + return prefs.distance_threshold; +} + +int LocationServiceSettingsObjectWrapper::timeThreshold() const +{ + return prefs.time_threshold; +} + +void LocationServiceSettingsObjectWrapper::setDistanceThreshold(int value) +{ + QSettings s; + s.setValue("distance_threshold", value); + prefs.distance_threshold = value; + emit distanceThresholdChanged(value); +} + +void LocationServiceSettingsObjectWrapper::setTimeThreshold(int value) +{ + QSettings s; + s.setValue("time_threshold", value); + prefs.time_threshold = value; + emit timeThresholdChanged( value); +} + +SettingsObjectWrapper::SettingsObjectWrapper(QObject* parent): +QObject(parent), + techDetails(new TechnicalDetailsSettings(this)), + pp_gas(new PartialPressureGasSettings(this)), + facebook(new FacebookSettings(this)), + geocoding(new GeocodingPreferences(this)), + proxy(new ProxySettings(this)), + cloud_storage(new CloudStorageSettings(this)), + planner_settings(new DivePlannerSettings(this)), + unit_settings(new UnitsSettings(this)), + general_settings(new GeneralSettingsObjectWrapper(this)), + display_settings(new DisplaySettingsObjectWrapper(this)), + language_settings(new LanguageSettingsObjectWrapper(this)), + animation_settings(new AnimationsSettingsObjectWrapper(this)), + location_settings(new LocationServiceSettingsObjectWrapper(this)) +{ +} + +void SettingsObjectWrapper::setSaveUserIdLocal(short int value) +{ + Q_UNUSED(value); + //TODO: Find where this is stored on the preferences. +} + +short int SettingsObjectWrapper::saveUserIdLocal() const +{ + return prefs.save_userid_local; +} + +SettingsObjectWrapper* SettingsObjectWrapper::instance() +{ + static SettingsObjectWrapper settings; + return &settings; +} diff --git a/core/subsurface-qt/SettingsObjectWrapper.h b/core/subsurface-qt/SettingsObjectWrapper.h new file mode 100644 index 000000000..f115e2d86 --- /dev/null +++ b/core/subsurface-qt/SettingsObjectWrapper.h @@ -0,0 +1,642 @@ +#ifndef SETTINGSOBJECTWRAPPER_H +#define SETTINGSOBJECTWRAPPER_H + +#include <QObject> + +#include "../pref.h" +#include "../prefs-macros.h" + +/* Wrapper class for the Settings. This will allow + * seamlessy integration of the settings with the QML + * and QWidget frontends. This class will be huge, since + * I need tons of properties, one for each option. */ + +/* Control the state of the Partial Pressure Graphs preferences */ +class PartialPressureGasSettings : public QObject { + Q_OBJECT + Q_PROPERTY(short show_po2 READ showPo2 WRITE setShowPo2 NOTIFY showPo2Changed) + Q_PROPERTY(short show_pn2 READ showPn2 WRITE setShowPn2 NOTIFY showPn2Changed) + Q_PROPERTY(short show_phe READ showPhe WRITE setShowPhe NOTIFY showPheChanged) + Q_PROPERTY(double po2_threshold READ po2Threshold WRITE setPo2Threshold NOTIFY po2ThresholdChanged) + Q_PROPERTY(double pn2_threshold READ pn2Threshold WRITE setPn2Threshold NOTIFY pn2ThresholdChanged) + Q_PROPERTY(double phe_threshold READ pheThreshold WRITE setPheThreshold NOTIFY pheThresholdChanged) + +public: + PartialPressureGasSettings(QObject *parent); + short showPo2() const; + short showPn2() const; + short showPhe() const; + double po2Threshold() const; + double pn2Threshold() const; + double pheThreshold() const; + +public slots: + void setShowPo2(short value); + void setShowPn2(short value); + void setShowPhe(short value); + void setPo2Threshold(double value); + void setPn2Threshold(double value); + void setPheThreshold(double value); + +signals: + void showPo2Changed(short value); + void showPn2Changed(short value); + void showPheChanged(short value); + void po2ThresholdChanged(double value); + void pn2ThresholdChanged(double value); + void pheThresholdChanged(double value); +private: + QString group; +}; + +class TechnicalDetailsSettings : public QObject { + Q_OBJECT + Q_PROPERTY(double modpO2 READ modp02 WRITE setModp02 NOTIFY modpO2Changed) + Q_PROPERTY(bool ead READ ead WRITE setEad NOTIFY eadChanged) + Q_PROPERTY(bool mod READ mod WRITE setMod NOTIFY modChanged); + Q_PROPERTY(bool dcceiling READ dcceiling WRITE setDCceiling NOTIFY dcceilingChanged) + Q_PROPERTY(bool redceiling READ redceiling WRITE setRedceiling NOTIFY redceilingChanged) + Q_PROPERTY(bool calcceiling READ calcceiling WRITE setCalcceiling NOTIFY calcceilingChanged) + Q_PROPERTY(bool calcceiling3m READ calcceiling3m WRITE setCalcceiling3m NOTIFY calcceiling3mChanged) + Q_PROPERTY(bool calcalltissues READ calcalltissues WRITE setCalcalltissues NOTIFY calcalltissuesChanged) + Q_PROPERTY(bool calcndltts READ calcndltts WRITE setCalcndltts NOTIFY calcndlttsChanged) + Q_PROPERTY(bool gflow READ gflow WRITE setGflow NOTIFY gflowChanged) + Q_PROPERTY(bool gfhigh READ gfhigh WRITE setGfhigh NOTIFY gfhighChanged) + Q_PROPERTY(bool hrgraph READ hrgraph WRITE setHRgraph NOTIFY hrgraphChanged) + Q_PROPERTY(bool tankbar READ tankBar WRITE setTankBar NOTIFY tankBarChanged) + Q_PROPERTY(bool percentagegraph READ percentageGraph WRITE setPercentageGraph NOTIFY percentageGraphChanged) + Q_PROPERTY(bool rulergraph READ rulerGraph WRITE setRulerGraph NOTIFY rulerGraphChanged) + Q_PROPERTY(bool show_ccr_setpoint READ showCCRSetpoint WRITE setShowCCRSetpoint NOTIFY showCCRSetpointChanged) + Q_PROPERTY(bool show_ccr_sensors READ showCCRSensors WRITE setShowCCRSensors NOTIFY showCCRSensorsChanged) + Q_PROPERTY(bool zoomed_plot READ zoomedPlot WRITE setZoomedPlot NOTIFY zoomedPlotChanged) + Q_PROPERTY(bool show_sac READ showSac WRITE setShowSac NOTIFY showSacChanged) + Q_PROPERTY(bool gf_low_at_maxdepth READ gfLowAtMaxDepth WRITE setGfLowAtMaxDepth NOTIFY gfLowAtMaxDepthChanged) + Q_PROPERTY(bool display_unused_tanks READ displayUnusedTanks WRITE setDisplayUnusedTanks NOTIFY displayUnusedTanksChanged) + Q_PROPERTY(bool show_average_depth READ showAverageDepth WRITE setShowAverageDepth NOTIFY showAverageDepthChanged) + Q_PROPERTY(bool show_pictures_in_profile READ showPicturesInProfile WRITE setShowPicturesInProfile NOTIFY showPicturesInProfileChanged) +public: + TechnicalDetailsSettings(QObject *parent); + + double modp02() const; + bool ead() const; + bool mod() const; + bool dcceiling() const; + bool redceiling() const; + bool calcceiling() const; + bool calcceiling3m() const; + bool calcalltissues() const; + bool calcndltts() const; + bool gflow() const; + bool gfhigh() const; + bool hrgraph() const; + bool tankBar() const; + bool percentageGraph() const; + bool rulerGraph() const; + bool showCCRSetpoint() const; + bool showCCRSensors() const; + bool zoomedPlot() const; + bool showSac() const; + bool gfLowAtMaxDepth() const; + bool displayUnusedTanks() const; + bool showAverageDepth() const; + bool showPicturesInProfile() const; + +public slots: + void setMod(bool value); + void setModp02(double value); + void setEad(bool value); + void setDCceiling(bool value); + void setRedceiling(bool value); + void setCalcceiling(bool value); + void setCalcceiling3m(bool value); + void setCalcalltissues(bool value); + void setCalcndltts(bool value); + void setGflow(bool value); + void setGfhigh(bool value); + void setHRgraph(bool value); + void setTankBar(bool value); + void setPercentageGraph(bool value); + void setRulerGraph(bool value); + void setShowCCRSetpoint(bool value); + void setShowCCRSensors(bool value); + void setZoomedPlot(bool value); + void setShowSac(bool value); + void setGfLowAtMaxDepth(bool value); + void setDisplayUnusedTanks(bool value); + void setShowAverageDepth(bool value); + void setShowPicturesInProfile(bool value); + +signals: + void modpO2Changed(double value); + void eadChanged(bool value); + void modChanged(bool value); + void dcceilingChanged(bool value); + void redceilingChanged(bool value); + void calcceilingChanged(bool value); + void calcceiling3mChanged(bool value); + void calcalltissuesChanged(bool value); + void calcndlttsChanged(bool value); + void gflowChanged(bool value); + void gfhighChanged(bool value); + void hrgraphChanged(bool value); + void tankBarChanged(bool value); + void percentageGraphChanged(bool value); + void rulerGraphChanged(bool value); + void showCCRSetpointChanged(bool value); + void showCCRSensorsChanged(bool value); + void zoomedPlotChanged(bool value); + void showSacChanged(bool value); + void gfLowAtMaxDepthChanged(bool value); + void displayUnusedTanksChanged(bool value); + void showAverageDepthChanged(bool value); + void showPicturesInProfileChanged(bool value); +}; + +/* Control the state of the Facebook preferences */ +class FacebookSettings : public QObject { + Q_OBJECT + Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged) + Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) + Q_PROPERTY(QString albumId READ albumId WRITE setAlbumId NOTIFY albumIdChanged) + +public: + FacebookSettings(QObject *parent); + QString accessToken() const; + QString userId() const; + QString albumId() const; + +public slots: + void setAccessToken (const QString& value); + void setUserId(const QString& value); + void setAlbumId(const QString& value); + +signals: + void accessTokenChanged(const QString& value); + void userIdChanged(const QString& value); + void albumIdChanged(const QString& value); +private: + QString group; + QString subgroup; +}; + +/* Control the state of the Geocoding preferences */ +class GeocodingPreferences : public QObject { + Q_OBJECT + Q_PROPERTY(bool enable_geocoding READ enableGeocoding WRITE setEnableGeocoding NOTIFY enableGeocodingChanged) + Q_PROPERTY(bool parse_dive_without_gps READ parseDiveWithoutGps WRITE setParseDiveWithoutGps NOTIFY parseDiveWithoutGpsChanged) + Q_PROPERTY(bool tag_existing_dives READ tagExistingDives WRITE setTagExistingDives NOTIFY tagExistingDivesChanged) + Q_PROPERTY(taxonomy_category first_category READ firstTaxonomyCategory WRITE setFirstTaxonomyCategory NOTIFY firstTaxonomyCategoryChanged) + Q_PROPERTY(taxonomy_category second_category READ secondTaxonomyCategory WRITE setSecondTaxonomyCategory NOTIFY secondTaxonomyCategoryChanged) + Q_PROPERTY(taxonomy_category third_category READ thirdTaxonomyCategory WRITE setThirdTaxonomyCategory NOTIFY thirdTaxonomyCategoryChanged) +public: + GeocodingPreferences(QObject *parent); + bool enableGeocoding() const; + bool parseDiveWithoutGps() const; + bool tagExistingDives() const; + taxonomy_category firstTaxonomyCategory() const; + taxonomy_category secondTaxonomyCategory() const; + taxonomy_category thirdTaxonomyCategory() const; + +public slots: + void setEnableGeocoding(bool value); + void setParseDiveWithoutGps(bool value); + void setTagExistingDives(bool value); + void setFirstTaxonomyCategory(taxonomy_category value); + void setSecondTaxonomyCategory(taxonomy_category value); + void setThirdTaxonomyCategory(taxonomy_category value); + +signals: + void enableGeocodingChanged(bool value); + void parseDiveWithoutGpsChanged(bool value); + void tagExistingDivesChanged(bool value); + void firstTaxonomyCategoryChanged(taxonomy_category value); + void secondTaxonomyCategoryChanged(taxonomy_category value); + void thirdTaxonomyCategoryChanged(taxonomy_category value); +private: + QString group; +}; + +class ProxySettings : public QObject { + Q_OBJECT + Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged) + Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) + Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) + Q_PROPERTY(short auth READ auth WRITE setAuth NOTIFY authChanged) + Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged) + Q_PROPERTY(QString pass READ pass WRITE setPass NOTIFY passChanged) + +public: + ProxySettings(QObject *parent); + int type() const; + QString host() const; + int port() const; + short auth() const; + QString user() const; + QString pass() const; + +public slots: + void setType(int value); + void setHost(const QString& value); + void setPort(int value); + void setAuth(short value); + void setUser(const QString& value); + void setPass(const QString& value); + +signals: + void typeChanged(int value); + void hostChanged(const QString& value); + void portChanged(int value); + void authChanged(short value); + void userChanged(const QString& value); + void passChanged(const QString& value); +private: + QString group; +}; + +class CloudStorageSettings : public QObject { + Q_OBJECT + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString newpassword READ newPassword WRITE setNewPassword NOTIFY newPasswordChanged) + Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged) + Q_PROPERTY(QString email_encoded READ emailEncoded WRITE setEmailEncoded NOTIFY emailEncodedChanged) + Q_PROPERTY(QString userid READ userId WRITE setUserId NOTIFY userIdChanged) + Q_PROPERTY(QString base_url READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged) + Q_PROPERTY(QString git_url READ gitUrl WRITE setGitUrl NOTIFY gitUrlChanged) + Q_PROPERTY(bool git_local_only READ gitLocalOnly WRITE setGitLocalOnly NOTIFY gitLocalOnlyChanged) + Q_PROPERTY(bool save_password_local READ savePasswordLocal WRITE setSavePasswordLocal NOTIFY savePasswordLocalChanged) + Q_PROPERTY(short verification_status READ verificationStatus WRITE setVerificationStatus NOTIFY verificationStatusChanged) + Q_PROPERTY(bool background_sync READ backgroundSync WRITE setBackgroundSync NOTIFY backgroundSyncChanged) +public: + CloudStorageSettings(QObject *parent); + QString password() const; + QString newPassword() const; + QString email() const; + QString emailEncoded() const; + QString userId() const; + QString baseUrl() const; + QString gitUrl() const; + bool savePasswordLocal() const; + short verificationStatus() const; + bool backgroundSync() const; + bool gitLocalOnly() const; + +public slots: + void setPassword(const QString& value); + void setNewPassword(const QString& value); + void setEmail(const QString& value); + void setEmailEncoded(const QString& value); + void setUserId(const QString& value); + void setBaseUrl(const QString& value); + void setGitUrl(const QString& value); + void setSavePasswordLocal(bool value); + void setVerificationStatus(short value); + void setBackgroundSync(bool value); + void setGitLocalOnly(bool value); + +signals: + void passwordChanged(const QString& value); + void newPasswordChanged(const QString& value); + void emailChanged(const QString& value); + void emailEncodedChanged(const QString& value); + void userIdChanged(const QString& value); + void baseUrlChanged(const QString& value); + void gitUrlChanged(const QString& value); + void savePasswordLocalChanged(bool value); + void verificationStatusChanged(short value); + void backgroundSyncChanged(bool value); + void gitLocalOnlyChanged(bool value); +private: + QString group; +}; + +class DivePlannerSettings : public QObject { + Q_OBJECT + Q_PROPERTY(bool last_stop READ lastStop WRITE setLastStop NOTIFY lastStopChanged) + Q_PROPERTY(bool verbatim_plan READ verbatimPlan WRITE setVerbatimPlan NOTIFY verbatimPlanChanged) + Q_PROPERTY(bool display_runtime READ displayRuntime WRITE setDisplayRuntime NOTIFY displayRuntimeChanged) + Q_PROPERTY(bool display_duration READ displayDuration WRITE setDisplayDuration NOTIFY displayDurationChanged) + Q_PROPERTY(bool display_transitions READ displayTransitions WRITE setDisplayTransitions NOTIFY displayTransitionsChanged) + Q_PROPERTY(bool doo2breaks READ doo2breaks WRITE setDoo2breaks NOTIFY doo2breaksChanged) + Q_PROPERTY(bool drop_stone_mode READ dropStoneMode WRITE setDropStoneMode NOTIFY dropStoneModeChanged) + Q_PROPERTY(bool safetystop READ safetyStop WRITE setSafetyStop NOTIFY safetyStopChanged) + Q_PROPERTY(bool switch_at_req_stop READ switchAtRequiredStop WRITE setSwitchAtRequiredStop NOTIFY switchAtRequiredStopChanged) + Q_PROPERTY(int ascrate75 READ ascrate75 WRITE setAscrate75 NOTIFY ascrate75Changed) + Q_PROPERTY(int ascrate50 READ ascrate50 WRITE setAscrate50 NOTIFY ascrate50Changed) + Q_PROPERTY(int ascratestops READ ascratestops WRITE setAscratestops NOTIFY ascratestopsChanged) + Q_PROPERTY(int ascratelast6m READ ascratelast6m WRITE setAscratelast6m NOTIFY ascratelast6mChanged) + Q_PROPERTY(int descrate READ descrate WRITE setDescrate NOTIFY descrateChanged) + Q_PROPERTY(int bottompo2 READ bottompo2 WRITE setBottompo2 NOTIFY bottompo2Changed) + Q_PROPERTY(int decopo2 READ decopo2 WRITE setDecopo2 NOTIFY decopo2Changed) + Q_PROPERTY(int reserve_gas READ reserveGas WRITE setReserveGas NOTIFY reserveGasChanged) + Q_PROPERTY(int min_switch_duration READ minSwitchDuration WRITE setMinSwitchDuration NOTIFY minSwitchDurationChanged) + Q_PROPERTY(int bottomsac READ bottomSac WRITE setBottomSac NOTIFY bottomSacChanged) + Q_PROPERTY(int decosac READ decoSac WRITE setSecoSac NOTIFY decoSacChanged) + Q_PROPERTY(short conservatism_level READ conservatismLevel WRITE setConservatismLevel NOTIFY conservatismLevelChanged) + Q_PROPERTY(deco_mode decoMode READ decoMode WRITE setDecoMode NOTIFY decoModeChanged) + +public: + DivePlannerSettings(QObject *parent = 0); + bool lastStop() const; + bool verbatimPlan() const; + bool displayRuntime() const; + bool displayDuration() const; + bool displayTransitions() const; + bool doo2breaks() const; + bool dropStoneMode() const; + bool safetyStop() const; + bool switchAtRequiredStop() const; + int ascrate75() const; + int ascrate50() const; + int ascratestops() const; + int ascratelast6m() const; + int descrate() const; + int bottompo2() const; + int decopo2() const; + int reserveGas() const; + int minSwitchDuration() const; + int bottomSac() const; + int decoSac() const; + short conservatismLevel() const; + deco_mode decoMode() const; + +public slots: + void setLastStop(bool value); + void setVerbatimPlan(bool value); + void setDisplayRuntime(bool value); + void setDisplayDuration(bool value); + void setDisplayTransitions(bool value); + void setDoo2breaks(bool value); + void setDropStoneMode(bool value); + void setSafetyStop(bool value); + void setSwitchAtRequiredStop(bool value); + void setAscrate75(int value); + void setAscrate50(int value); + void setAscratestops(int value); + void setAscratelast6m(int value); + void setDescrate(int value); + void setBottompo2(int value); + void setDecopo2(int value); + void setReserveGas(int value); + void setMinSwitchDuration(int value); + void setBottomSac(int value); + void setSecoSac(int value); + void setConservatismLevel(int value); + void setDecoMode(deco_mode value); + +signals: + void lastStopChanged(bool value); + void verbatimPlanChanged(bool value); + void displayRuntimeChanged(bool value); + void displayDurationChanged(bool value); + void displayTransitionsChanged(bool value); + void doo2breaksChanged(bool value); + void dropStoneModeChanged(bool value); + void safetyStopChanged(bool value); + void switchAtRequiredStopChanged(bool value); + void ascrate75Changed(int value); + void ascrate50Changed(int value); + void ascratestopsChanged(int value); + void ascratelast6mChanged(int value); + void descrateChanged(int value); + void bottompo2Changed(int value); + void decopo2Changed(int value); + void reserveGasChanged(int value); + void minSwitchDurationChanged(int value); + void bottomSacChanged(int value); + void decoSacChanged(int value); + void conservatismLevelChanged(int value); + void decoModeChanged(deco_mode value); + +private: + QString group; +}; + +class UnitsSettings : public QObject { + Q_OBJECT + Q_PROPERTY(int length READ length WRITE setLength NOTIFY lengthChanged) + Q_PROPERTY(int pressure READ pressure WRITE setPressure NOTIFY pressureChanged) + Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged) + Q_PROPERTY(int temperature READ temperature WRITE setTemperature NOTIFY temperatureChanged) + Q_PROPERTY(int weight READ weight WRITE setWeight NOTIFY weightChanged) + Q_PROPERTY(QString unit_system READ unitSystem WRITE setUnitSystem NOTIFY unitSystemChanged) + Q_PROPERTY(bool coordinates_traditional READ coordinatesTraditional WRITE setCoordinatesTraditional NOTIFY coordinatesTraditionalChanged) + Q_PROPERTY(int vertical_speed_time READ verticalSpeedTime WRITE setVerticalSpeedTime NOTIFY verticalSpeedTimeChanged) + +public: + UnitsSettings(QObject *parent = 0); + int length() const; + int pressure() const; + int volume() const; + int temperature() const; + int weight() const; + int verticalSpeedTime() const; + QString unitSystem() const; + bool coordinatesTraditional() const; + +public slots: + void setLength(int value); + void setPressure(int value); + void setVolume(int value); + void setTemperature(int value); + void setWeight(int value); + void setVerticalSpeedTime(int value); + void setUnitSystem(const QString& value); + void setCoordinatesTraditional(bool value); + +signals: + void lengthChanged(int value); + void pressureChanged(int value); + void volumeChanged(int value); + void temperatureChanged(int value); + void weightChanged(int value); + void verticalSpeedTimeChanged(int value); + void unitSystemChanged(const QString& value); + void coordinatesTraditionalChanged(bool value); +private: + QString group; +}; + +class GeneralSettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged) + Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged) + Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged) + Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged) + Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged) + Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged) + Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged) + +public: + GeneralSettingsObjectWrapper(QObject *parent); + QString defaultFilename() const; + QString defaultCylinder() const; + short defaultFileBehavior() const; + bool useDefaultFile() const; + int defaultSetPoint() const; + int o2Consumption() const; + int pscrRatio() const; + +public slots: + void setDefaultFilename (const QString& value); + void setDefaultCylinder (const QString& value); + void setDefaultFileBehavior (short value); + void setUseDefaultFile (bool value); + void setDefaultSetPoint (int value); + void setO2Consumption (int value); + void setPscrRatio (int value); + +signals: + void defaultFilenameChanged(const QString& value); + void defaultCylinderChanged(const QString& value); + void defaultFileBehaviorChanged(short value); + void useDefaultFileChanged(bool value); + void defaultSetPointChanged(int value); + void o2ConsumptionChanged(int value); + void pscrRatioChanged(int value); +private: + QString group; +}; + +class DisplaySettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(QString divelist_font READ divelistFont WRITE setDivelistFont NOTIFY divelistFontChanged) + Q_PROPERTY(double font_size READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) + Q_PROPERTY(short display_invalid_dives READ displayInvalidDives WRITE setDisplayInvalidDives NOTIFY displayInvalidDivesChanged) +public: + DisplaySettingsObjectWrapper(QObject *parent); + QString divelistFont() const; + double fontSize() const; + short displayInvalidDives() const; +public slots: + void setDivelistFont(const QString& value); + void setFontSize(double value); + void setDisplayInvalidDives(short value); +signals: + void divelistFontChanged(const QString& value); + void fontSizeChanged(double value); + void displayInvalidDivesChanged(short value); +private: + QString group; +}; + +class LanguageSettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) + Q_PROPERTY(QString time_format READ timeFormat WRITE setTimeFormat NOTIFY timeFormatChanged) + Q_PROPERTY(QString date_format READ dateFormat WRITE setDateFormat NOTIFY dateFormatChanged) + Q_PROPERTY(QString date_format_short READ dateFormatShort WRITE setDateFormatShort NOTIFY dateFormatShortChanged) + Q_PROPERTY(bool time_format_override READ timeFormatOverride WRITE setTimeFormatOverride NOTIFY timeFormatOverrideChanged) + Q_PROPERTY(bool date_format_override READ dateFormatOverride WRITE setDateFormatOverride NOTIFY dateFormatOverrideChanged) + Q_PROPERTY(bool use_system_language READ useSystemLanguage WRITE setUseSystemLanguage NOTIFY useSystemLanguageChanged) + +public: + LanguageSettingsObjectWrapper(QObject *parent); + QString language() const; + QString timeFormat() const; + QString dateFormat() const; + QString dateFormatShort() const; + bool timeFormatOverride() const; + bool dateFormatOverride() const; + bool useSystemLanguage() const; + +public slots: + void setLanguage (const QString& value); + void setTimeFormat (const QString& value); + void setDateFormat (const QString& value); + void setDateFormatShort (const QString& value); + void setTimeFormatOverride (bool value); + void setDateFormatOverride (bool value); + void setUseSystemLanguage (bool value); +signals: + void languageChanged(const QString& value); + void timeFormatChanged(const QString& value); + void dateFormatChanged(const QString& value); + void dateFormatShortChanged(const QString& value); + void timeFormatOverrideChanged(bool value); + void dateFormatOverrideChanged(bool value); + void useSystemLanguageChanged(bool value); + +private: + QString group; +}; + +class AnimationsSettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(int animation_speed READ animationSpeed WRITE setAnimationSpeed NOTIFY animationSpeedChanged) +public: + AnimationsSettingsObjectWrapper(QObject *parent); + int animationSpeed() const; + +public slots: + void setAnimationSpeed(int value); + +signals: + void animationSpeedChanged(int value); + +private: + QString group; +}; + +class LocationServiceSettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(int time_threshold READ timeThreshold WRITE setTimeThreshold NOTIFY timeThresholdChanged) + Q_PROPERTY(int distance_threshold READ distanceThreshold WRITE setDistanceThreshold NOTIFY distanceThresholdChanged) +public: + LocationServiceSettingsObjectWrapper(QObject *parent); + int timeThreshold() const; + int distanceThreshold() const; +public slots: + void setTimeThreshold(int value); + void setDistanceThreshold(int value); +signals: + void timeThresholdChanged(int value); + void distanceThresholdChanged(int value); +private: + QString group; +}; + +class SettingsObjectWrapper : public QObject { + Q_OBJECT + Q_PROPERTY(short save_userid_local READ saveUserIdLocal WRITE setSaveUserIdLocal NOTIFY saveUserIdLocalChanged) + + Q_PROPERTY(TechnicalDetailsSettings* techical_details MEMBER techDetails CONSTANT) + Q_PROPERTY(PartialPressureGasSettings* pp_gas MEMBER pp_gas CONSTANT) + Q_PROPERTY(FacebookSettings* facebook MEMBER facebook CONSTANT) + Q_PROPERTY(GeocodingPreferences* geocoding MEMBER geocoding CONSTANT) + Q_PROPERTY(ProxySettings* proxy MEMBER proxy CONSTANT) + Q_PROPERTY(CloudStorageSettings* cloud_storage MEMBER cloud_storage CONSTANT) + Q_PROPERTY(DivePlannerSettings* planner MEMBER planner_settings CONSTANT) + Q_PROPERTY(UnitsSettings* units MEMBER unit_settings CONSTANT) + + Q_PROPERTY(GeneralSettingsObjectWrapper* general MEMBER general_settings CONSTANT) + Q_PROPERTY(DisplaySettingsObjectWrapper* display MEMBER display_settings CONSTANT) + Q_PROPERTY(LanguageSettingsObjectWrapper* language MEMBER language_settings CONSTANT) + Q_PROPERTY(AnimationsSettingsObjectWrapper* animation MEMBER animation_settings CONSTANT) + Q_PROPERTY(LocationServiceSettingsObjectWrapper* Location MEMBER location_settings CONSTANT) +public: + static SettingsObjectWrapper *instance(); + short saveUserIdLocal() const; + + TechnicalDetailsSettings *techDetails; + PartialPressureGasSettings *pp_gas; + FacebookSettings *facebook; + GeocodingPreferences *geocoding; + ProxySettings *proxy; + CloudStorageSettings *cloud_storage; + DivePlannerSettings *planner_settings; + UnitsSettings *unit_settings; + GeneralSettingsObjectWrapper *general_settings; + DisplaySettingsObjectWrapper *display_settings; + LanguageSettingsObjectWrapper *language_settings; + AnimationsSettingsObjectWrapper *animation_settings; + LocationServiceSettingsObjectWrapper *location_settings; + +public slots: + void setSaveUserIdLocal(short value); +private: + SettingsObjectWrapper(QObject *parent = NULL); +signals: + void saveUserIdLocalChanged(short value); +}; + +#endif diff --git a/core/subsurfacestartup.c b/core/subsurfacestartup.c new file mode 100644 index 000000000..6e0dede1c --- /dev/null +++ b/core/subsurfacestartup.c @@ -0,0 +1,310 @@ +#include "subsurfacestartup.h" +#include "version.h" +#include <stdbool.h> +#include <string.h> +#include "gettext.h" +#include "qthelperfromc.h" +#include "git-access.h" +#include "libdivecomputer/version.h" + +struct preferences prefs, informational_prefs; +struct preferences default_prefs = { + .cloud_base_url = "https://cloud.subsurface-divelog.org/", + .units = SI_UNITS, + .unit_system = METRIC, + .coordinates_traditional = true, + .pp_graphs = { + .po2 = false, + .pn2 = false, + .phe = false, + .po2_threshold = 1.6, + .pn2_threshold = 4.0, + .phe_threshold = 13.0, + }, + .mod = false, + .modpO2 = 1.6, + .ead = false, + .hrgraph = false, + .percentagegraph = false, + .dcceiling = true, + .redceiling = false, + .calcceiling = false, + .calcceiling3m = false, + .calcndltts = false, + .gflow = 30, + .gfhigh = 75, + .animation_speed = 500, + .gf_low_at_maxdepth = false, + .show_ccr_setpoint = false, + .show_ccr_sensors = false, + .font_size = -1, + .display_invalid_dives = false, + .show_sac = false, + .display_unused_tanks = false, + .show_average_depth = true, + .ascrate75 = 9000 / 60, + .ascrate50 = 6000 / 60, + .ascratestops = 6000 / 60, + .ascratelast6m = 1000 / 60, + .descrate = 18000 / 60, + .bottompo2 = 1400, + .decopo2 = 1600, + .doo2breaks = false, + .drop_stone_mode = false, + .switch_at_req_stop = false, + .min_switch_duration = 60, + .last_stop = false, + .verbatim_plan = false, + .display_runtime = true, + .display_duration = true, + .display_transitions = true, + .safetystop = true, + .bottomsac = 20000, + .decosac = 17000, + .reserve_gas=40000, + .o2consumption = 720, + .pscr_ratio = 100, + .show_pictures_in_profile = true, + .tankbar = false, + .facebook = { + .user_id = NULL, + .album_id = NULL, + .access_token = NULL + }, + .defaultsetpoint = 1100, + .cloud_background_sync = true, + .geocoding = { + .enable_geocoding = true, + .parse_dive_without_gps = false, + .tag_existing_dives = false, + .category = { 0 } + }, + .deco_mode = BUEHLMANN, + .conservatism_level = 3, + .distance_threshold = 1000, + .time_threshold = 600 +}; + +int run_survey; + +struct units *get_units() +{ + return &prefs.units; +} + +/* random helper functions, used here or elsewhere */ +static int sortfn(const void *_a, const void *_b) +{ + const struct dive *a = (const struct dive *)*(void **)_a; + const struct dive *b = (const struct dive *)*(void **)_b; + + if (a->when < b->when) + return -1; + if (a->when > b->when) + return 1; + return 0; +} + +void sort_table(struct dive_table *table) +{ + qsort(table->dives, table->nr, sizeof(struct dive *), sortfn); +} + +const char *weekday(int wday) +{ + static const char wday_array[7][7] = { + /*++GETTEXT: these are three letter days - we allow up to six code bytes */ + QT_TRANSLATE_NOOP("gettextFromC", "Sun"), QT_TRANSLATE_NOOP("gettextFromC", "Mon"), QT_TRANSLATE_NOOP("gettextFromC", "Tue"), QT_TRANSLATE_NOOP("gettextFromC", "Wed"), QT_TRANSLATE_NOOP("gettextFromC", "Thu"), QT_TRANSLATE_NOOP("gettextFromC", "Fri"), QT_TRANSLATE_NOOP("gettextFromC", "Sat") + }; + return translate("gettextFromC", wday_array[wday]); +} + +const char *monthname(int mon) +{ + static const char month_array[12][7] = { + /*++GETTEXT: these are three letter months - we allow up to six code bytes*/ + QT_TRANSLATE_NOOP("gettextFromC", "Jan"), QT_TRANSLATE_NOOP("gettextFromC", "Feb"), QT_TRANSLATE_NOOP("gettextFromC", "Mar"), QT_TRANSLATE_NOOP("gettextFromC", "Apr"), QT_TRANSLATE_NOOP("gettextFromC", "May"), QT_TRANSLATE_NOOP("gettextFromC", "Jun"), + QT_TRANSLATE_NOOP("gettextFromC", "Jul"), QT_TRANSLATE_NOOP("gettextFromC", "Aug"), QT_TRANSLATE_NOOP("gettextFromC", "Sep"), QT_TRANSLATE_NOOP("gettextFromC", "Oct"), QT_TRANSLATE_NOOP("gettextFromC", "Nov"), QT_TRANSLATE_NOOP("gettextFromC", "Dec"), + }; + return translate("gettextFromC", month_array[mon]); +} + +/* + * track whether we switched to importing dives + */ +bool imported = false; + +static void print_version() +{ + printf("Subsurface v%s, ", subsurface_git_version()); + printf("built with libdivecomputer v%s\n", dc_version(NULL)); +} + +void print_files() +{ + const char *branch = 0; + const char *remote = 0; + const char *filename, *local_git; + + filename = cloud_url(); + + is_git_repository(filename, &branch, &remote, true); + printf("\nFile locations:\n\n"); + if (branch && remote) { + local_git = get_local_dir(remote, branch); + printf("Local git storage: %s\n", local_git); + } else { + printf("Unable to get local git directory\n"); + } + char *tmp = cloud_url(); + printf("Cloud URL: %s\n", tmp); + free(tmp); + tmp = hashfile_name_string(); + printf("Image hashes: %s\n", tmp); + free(tmp); + tmp = picturedir_string(); + printf("Local picture directory: %s\n\n", tmp); + free(tmp); +} + +static void print_help() +{ + print_version(); + printf("\nUsage: subsurface [options] [logfile ...] [--import logfile ...]"); + printf("\n\noptions include:"); + printf("\n --help|-h This help text"); + printf("\n --import logfile ... Logs before this option is treated as base, everything after is imported"); + printf("\n --verbose|-v Verbose debug (repeat to increase verbosity)"); + printf("\n --version Prints current version"); + printf("\n --survey Offer to submit a user survey"); + printf("\n --win32console Create a dedicated console if needed (Windows only). Add option before everything else\n\n"); +} + +void parse_argument(const char *arg) +{ + const char *p = arg + 1; + + do { + switch (*p) { + case 'h': + print_help(); + exit(0); + case 'v': + verbose++; + continue; + case 'q': + quit++; + continue; + case '-': + /* long options with -- */ + if (strcmp(arg, "--help") == 0) { + print_help(); + exit(0); + } + if (strcmp(arg, "--import") == 0) { + imported = true; /* mark the dives so far as the base, * everything after is imported */ + return; + } + if (strcmp(arg, "--verbose") == 0) { + verbose++; + return; + } + if (strcmp(arg, "--version") == 0) { + print_version(); + exit(0); + } + if (strcmp(arg, "--survey") == 0) { + run_survey = true; + return; + } + if (strcmp(arg, "--allow_run_as_root") == 0) { + ++force_root; + return; + } + if (strcmp(arg, "--win32console") == 0) + return; + /* fallthrough */ + case 'p': + /* ignore process serial number argument when run as native macosx app */ + if (strncmp(arg, "-psQT_TR_NOOP(", 5) == 0) { + return; + } + /* fallthrough */ + default: + fprintf(stderr, "Bad argument '%s'\n", arg); + exit(1); + } + } while (*++p); +} + +/* + * Under a POSIX setup, the locale string should have a format + * like [language[_territory][.codeset][@modifier]]. + * + * So search for the underscore, and see if the "territory" is + * US, and turn on imperial units by default. + * + * I guess Burma and Liberia should trigger this too. I'm too + * lazy to look up the territory names, though. + */ +void setup_system_prefs(void) +{ + const char *env; + + subsurface_OS_pref_setup(); + default_prefs.divelist_font = strdup(system_divelist_default_font); + default_prefs.font_size = system_divelist_default_font_size; + default_prefs.default_filename = system_default_filename(); + + env = getenv("LC_MEASUREMENT"); + if (!env) + env = getenv("LC_ALL"); + if (!env) + env = getenv("LANG"); + if (!env) + return; + env = strchr(env, '_'); + if (!env) + return; + env++; + if (strncmp(env, "US", 2)) + return; + + default_prefs.units = IMPERIAL_units; +} + +/* copy a preferences block, including making copies of all included strings */ +void copy_prefs(struct preferences *src, struct preferences *dest) +{ + *dest = *src; + dest->divelist_font = copy_string(src->divelist_font); + dest->default_filename = copy_string(src->default_filename); + dest->default_cylinder = copy_string(src->default_cylinder); + dest->cloud_base_url = copy_string(src->cloud_base_url); + dest->cloud_git_url = copy_string(src->cloud_git_url); + dest->userid = copy_string(src->userid); + dest->proxy_host = copy_string(src->proxy_host); + dest->proxy_user = copy_string(src->proxy_user); + dest->proxy_pass = copy_string(src->proxy_pass); + dest->time_format = copy_string(src->time_format); + dest->date_format = copy_string(src->date_format); + dest->date_format_short = copy_string(src->date_format_short); + dest->cloud_storage_password = copy_string(src->cloud_storage_password); + dest->cloud_storage_newpassword = copy_string(src->cloud_storage_newpassword); + dest->cloud_storage_email = copy_string(src->cloud_storage_email); + dest->cloud_storage_email_encoded = copy_string(src->cloud_storage_email_encoded); + dest->facebook.access_token = copy_string(src->facebook.access_token); + dest->facebook.user_id = copy_string(src->facebook.user_id); + dest->facebook.album_id = copy_string(src->facebook.album_id); +} + +/* + * Free strduped prefs before exit. + * + * These are not real leaks but they plug the holes found by eg. + * valgrind so you can find the real leaks. + */ +void free_prefs(void) +{ + // nop +} diff --git a/core/subsurfacestartup.h b/core/subsurfacestartup.h new file mode 100644 index 000000000..3ccc24aa4 --- /dev/null +++ b/core/subsurfacestartup.h @@ -0,0 +1,26 @@ +#ifndef SUBSURFACESTARTUP_H +#define SUBSURFACESTARTUP_H + +#include "dive.h" +#include "divelist.h" +#include "libdivecomputer.h" + +#ifdef __cplusplus +extern "C" { +#else +#include <stdbool.h> +#endif + +extern bool imported; + +void setup_system_prefs(void); +void parse_argument(const char *arg); +void free_prefs(void); +void copy_prefs(struct preferences *src, struct preferences *dest); +void print_files(void); + +#ifdef __cplusplus +} +#endif + +#endif // SUBSURFACESTARTUP_H diff --git a/core/subsurfacesysinfo.cpp b/core/subsurfacesysinfo.cpp new file mode 100644 index 000000000..a7173b169 --- /dev/null +++ b/core/subsurfacesysinfo.cpp @@ -0,0 +1,620 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 Intel Corporation +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "subsurfacesysinfo.h" +#include <QString> + +#ifdef Q_OS_UNIX +#include <sys/utsname.h> +#endif + +#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) + +#ifndef QStringLiteral +# define QStringLiteral QString::fromUtf8 +#endif + +#ifdef Q_OS_UNIX +#include <qplatformdefs.h> +#endif + +#ifdef __APPLE__ +#include <CoreServices/CoreServices.h> +#endif + +// --- this is a copy of Qt 5.4's src/corelib/global/archdetect.cpp --- + +// main part: processor type +#if defined(Q_PROCESSOR_ALPHA) +# define ARCH_PROCESSOR "alpha" +#elif defined(Q_PROCESSOR_ARM) +# define ARCH_PROCESSOR "arm" +#elif defined(Q_PROCESSOR_AVR32) +# define ARCH_PROCESSOR "avr32" +#elif defined(Q_PROCESSOR_BLACKFIN) +# define ARCH_PROCESSOR "bfin" +#elif defined(Q_PROCESSOR_X86_32) +# define ARCH_PROCESSOR "i386" +#elif defined(Q_PROCESSOR_X86_64) +# define ARCH_PROCESSOR "x86_64" +#elif defined(Q_PROCESSOR_IA64) +# define ARCH_PROCESSOR "ia64" +#elif defined(Q_PROCESSOR_MIPS) +# define ARCH_PROCESSOR "mips" +#elif defined(Q_PROCESSOR_POWER) +# define ARCH_PROCESSOR "power" +#elif defined(Q_PROCESSOR_S390) +# define ARCH_PROCESSOR "s390" +#elif defined(Q_PROCESSOR_SH) +# define ARCH_PROCESSOR "sh" +#elif defined(Q_PROCESSOR_SPARC) +# define ARCH_PROCESSOR "sparc" +#else +# define ARCH_PROCESSOR "unknown" +#endif + +// endinanness +#if defined(Q_LITTLE_ENDIAN) +# define ARCH_ENDIANNESS "little_endian" +#elif defined(Q_BIG_ENDIAN) +# define ARCH_ENDIANNESS "big_endian" +#endif + +// pointer type +#if defined(Q_OS_WIN64) || (defined(Q_OS_WINRT) && defined(_M_X64)) +# define ARCH_POINTER "llp64" +#elif defined(__LP64__) || QT_POINTER_SIZE - 0 == 8 +# define ARCH_POINTER "lp64" +#else +# define ARCH_POINTER "ilp32" +#endif + +// secondary: ABI string (includes the dash) +#if defined(__ARM_EABI__) || defined(__mips_eabi) +# define ARCH_ABI1 "-eabi" +#elif defined(_MIPS_SIM) +# if _MIPS_SIM == _ABIO32 +# define ARCH_ABI1 "-o32" +# elif _MIPS_SIM == _ABIN32 +# define ARCH_ABI1 "-n32" +# elif _MIPS_SIM == _ABI64 +# define ARCH_ABI1 "-n64" +# elif _MIPS_SIM == _ABIO64 +# define ARCH_ABI1 "-o64" +# endif +#else +# define ARCH_ABI1 "" +#endif +#if defined(__ARM_PCS_VFP) || defined(__mips_hard_float) +# define ARCH_ABI2 "-hardfloat" +#else +# define ARCH_ABI2 "" +#endif + +#define ARCH_ABI ARCH_ABI1 ARCH_ABI2 + +#define ARCH_FULL ARCH_PROCESSOR "-" ARCH_ENDIANNESS "-" ARCH_POINTER ARCH_ABI + +// --- end of archdetect.cpp --- +// copied from Qt 5.4.1's src/corelib/global/qglobal.cpp + +#if defined(Q_OS_WIN) || defined(Q_OS_CYGWIN) || defined(Q_OS_WINCE) || defined(Q_OS_WINRT) + +QT_BEGIN_INCLUDE_NAMESPACE +#include "qt_windows.h" +QT_END_INCLUDE_NAMESPACE + +#ifndef Q_OS_WINRT +# ifndef Q_OS_WINCE +// Fallback for determining Windows versions >= 8 by looping using the +// version check macros. Note that it will return build number=0 to avoid +// inefficient looping. +static inline void determineWinOsVersionFallbackPost8(OSVERSIONINFO *result) +{ + result->dwBuildNumber = 0; + DWORDLONG conditionMask = 0; + VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); + OSVERSIONINFOEX checkVersion = { sizeof(OSVERSIONINFOEX), result->dwMajorVersion, 0, + result->dwBuildNumber, result->dwPlatformId, {'\0'}, 0, 0, 0, 0, 0 }; + for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMajorVersion) + result->dwMajorVersion = checkVersion.dwMajorVersion; + conditionMask = 0; + checkVersion.dwMajorVersion = result->dwMajorVersion; + checkVersion.dwMinorVersion = 0; + VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); + for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMinorVersion) + result->dwMinorVersion = checkVersion.dwMinorVersion; +} + +# endif // !Q_OS_WINCE + +static inline OSVERSIONINFO winOsVersion() +{ + OSVERSIONINFO result = { sizeof(OSVERSIONINFO), 0, 0, 0, 0, {'\0'}}; + // GetVersionEx() has been deprecated in Windows 8.1 and will return + // only Windows 8 from that version on. +# if defined(_MSC_VER) && _MSC_VER >= 1800 +# pragma warning( push ) +# pragma warning( disable : 4996 ) +# endif + GetVersionEx(&result); +# if defined(_MSC_VER) && _MSC_VER >= 1800 +# pragma warning( pop ) +# endif +# ifndef Q_OS_WINCE + if (result.dwMajorVersion == 6 && result.dwMinorVersion == 2) { + determineWinOsVersionFallbackPost8(&result); + } +# endif // !Q_OS_WINCE + return result; +} +#endif // !Q_OS_WINRT + +static const char *winVer_helper() +{ + switch (int(SubsurfaceSysInfo::WindowsVersion)) { + case SubsurfaceSysInfo::WV_NT: + return "NT"; + case SubsurfaceSysInfo::WV_2000: + return "2000"; + case SubsurfaceSysInfo::WV_XP: + return "XP"; + case SubsurfaceSysInfo::WV_2003: + return "2003"; + case SubsurfaceSysInfo::WV_VISTA: + return "Vista"; + case SubsurfaceSysInfo::WV_WINDOWS7: + return "7"; + case SubsurfaceSysInfo::WV_WINDOWS8: + return "8"; + case SubsurfaceSysInfo::WV_WINDOWS8_1: + return "8.1"; + + case SubsurfaceSysInfo::WV_CE: + return "CE"; + case SubsurfaceSysInfo::WV_CENET: + return "CENET"; + case SubsurfaceSysInfo::WV_CE_5: + return "CE5"; + case SubsurfaceSysInfo::WV_CE_6: + return "CE6"; + } + // unknown, future version + return 0; +} +#endif + +#if defined(Q_OS_UNIX) +# if (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_FREEBSD) +# define USE_ETC_OS_RELEASE +struct QUnixOSVersion +{ + // from /etc/os-release + QString productType; // $ID + QString productVersion; // $VERSION_ID + QString prettyName; // $PRETTY_NAME +}; + +static QString unquote(const char *begin, const char *end) +{ + if (*begin == '"') { + Q_ASSERT(end[-1] == '"'); + return QString::fromLatin1(begin + 1, end - begin - 2); + } + return QString::fromLatin1(begin, end - begin); +} + +static bool readEtcOsRelease(QUnixOSVersion &v) +{ + // we're avoiding QFile here + int fd = QT_OPEN("/etc/os-release", O_RDONLY); + if (fd == -1) + return false; + + QT_STATBUF sbuf; + if (QT_FSTAT(fd, &sbuf) == -1) { + QT_CLOSE(fd); + return false; + } + + QByteArray buffer(sbuf.st_size, Qt::Uninitialized); + buffer.resize(QT_READ(fd, buffer.data(), sbuf.st_size)); + QT_CLOSE(fd); + + const char *ptr = buffer.constData(); + const char *end = buffer.constEnd(); + const char *eol; + for ( ; ptr != end; ptr = eol + 1) { + static const char idString[] = "ID="; + static const char prettyNameString[] = "PRETTY_NAME="; + static const char versionIdString[] = "VERSION_ID="; + + // find the end of the line after ptr + eol = static_cast<const char *>(memchr(ptr, '\n', end - ptr)); + if (!eol) + eol = end - 1; + + // note: we're doing a binary search here, so comparison + // must always be sorted + int cmp = strncmp(ptr, idString, strlen(idString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(idString); + v.productType = unquote(ptr, eol); + continue; + } + + cmp = strncmp(ptr, prettyNameString, strlen(prettyNameString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(prettyNameString); + v.prettyName = unquote(ptr, eol); + continue; + } + + cmp = strncmp(ptr, versionIdString, strlen(versionIdString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(versionIdString); + v.productVersion = unquote(ptr, eol); + continue; + } + } + + return true; +} +# endif // USE_ETC_OS_RELEASE +#endif // Q_OS_UNIX + +static QString unknownText() +{ + return QStringLiteral("unknown"); +} + +QString SubsurfaceSysInfo::buildCpuArchitecture() +{ + return QStringLiteral(ARCH_PROCESSOR); +} + +QString SubsurfaceSysInfo::currentCpuArchitecture() +{ +#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE) + // We don't need to catch all the CPU architectures in this function; + // only those where the host CPU might be different than the build target + // (usually, 64-bit platforms). + SYSTEM_INFO info; + GetNativeSystemInfo(&info); + switch (info.wProcessorArchitecture) { +# ifdef PROCESSOR_ARCHITECTURE_AMD64 + case PROCESSOR_ARCHITECTURE_AMD64: + return QStringLiteral("x86_64"); +# endif +# ifdef PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 + case PROCESSOR_ARCHITECTURE_IA32_ON_WIN64: +# endif + case PROCESSOR_ARCHITECTURE_IA64: + return QStringLiteral("ia64"); + } +#elif defined(Q_OS_UNIX) + long ret = -1; + struct utsname u; + +# if defined(Q_OS_SOLARIS) + // We need a special call for Solaris because uname(2) on x86 returns "i86pc" for + // both 32- and 64-bit CPUs. Reference: + // http://docs.oracle.com/cd/E18752_01/html/816-5167/sysinfo-2.html#REFMAN2sysinfo-2 + // http://fxr.watson.org/fxr/source/common/syscall/systeminfo.c?v=OPENSOLARIS + // http://fxr.watson.org/fxr/source/common/conf/param.c?v=OPENSOLARIS;im=10#L530 + if (ret == -1) + ret = sysinfo(SI_ARCHITECTURE_64, u.machine, sizeof u.machine); +# endif + + if (ret == -1) + ret = uname(&u); + + // we could use detectUnixVersion() above, but we only need a field no other function does + if (ret != -1) { + // the use of QT_BUILD_INTERNAL here is simply to ensure all branches build + // as we don't often build on some of the less common platforms +# if defined(Q_PROCESSOR_ARM) || defined(QT_BUILD_INTERNAL) + if (strcmp(u.machine, "aarch64") == 0) + return QStringLiteral("arm64"); + if (strncmp(u.machine, "armv", 4) == 0) + return QStringLiteral("arm"); +# endif +# if defined(Q_PROCESSOR_POWER) || defined(QT_BUILD_INTERNAL) + // harmonize "powerpc" and "ppc" to "power" + if (strncmp(u.machine, "ppc", 3) == 0) + return QLatin1String("power") + QLatin1String(u.machine + 3); + if (strncmp(u.machine, "powerpc", 7) == 0) + return QLatin1String("power") + QLatin1String(u.machine + 7); + if (strcmp(u.machine, "Power Macintosh") == 0) + return QLatin1String("power"); +# endif +# if defined(Q_PROCESSOR_SPARC) || defined(QT_BUILD_INTERNAL) + // Solaris sysinfo(2) (above) uses "sparcv9", but uname -m says "sun4u"; + // Linux says "sparc64" + if (strcmp(u.machine, "sun4u") == 0 || strcmp(u.machine, "sparc64") == 0) + return QStringLiteral("sparcv9"); + if (strcmp(u.machine, "sparc32") == 0) + return QStringLiteral("sparc"); +# endif +# if defined(Q_PROCESSOR_X86) || defined(QT_BUILD_INTERNAL) + // harmonize all "i?86" to "i386" + if (strlen(u.machine) == 4 && u.machine[0] == 'i' + && u.machine[2] == '8' && u.machine[3] == '6') + return QStringLiteral("i386"); + if (strcmp(u.machine, "amd64") == 0) // Solaris + return QStringLiteral("x86_64"); +# endif + return QString::fromLatin1(u.machine); + } +#endif + return buildCpuArchitecture(); +} + + +QString SubsurfaceSysInfo::buildAbi() +{ + return QLatin1String(ARCH_FULL); +} + +QString SubsurfaceSysInfo::kernelType() +{ +#if defined(Q_OS_WINCE) + return QStringLiteral("wince"); +#elif defined(Q_OS_WIN) + return QStringLiteral("winnt"); +#elif defined(Q_OS_UNIX) + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname).toLower(); +#endif + return unknownText(); +} + +QString SubsurfaceSysInfo::kernelVersion() +{ +#ifdef Q_OS_WINRT + // TBD + return QString(); +#elif defined(Q_OS_WIN) + const OSVERSIONINFO osver = winOsVersion(); + return QString::number(int(osver.dwMajorVersion)) + QLatin1Char('.') + QString::number(int(osver.dwMinorVersion)) + + QLatin1Char('.') + QString::number(int(osver.dwBuildNumber)); +#else + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.release); + return QString(); +#endif +} + +QString SubsurfaceSysInfo::productType() +{ + // similar, but not identical to QFileSelectorPrivate::platformSelectors +#if defined(Q_OS_WINPHONE) + return QStringLiteral("winphone"); +#elif defined(Q_OS_WINRT) + return QStringLiteral("winrt"); +#elif defined(Q_OS_WINCE) + return QStringLiteral("wince"); +#elif defined(Q_OS_WIN) + return QStringLiteral("windows"); + +#elif defined(Q_OS_BLACKBERRY) + return QStringLiteral("blackberry"); +#elif defined(Q_OS_QNX) + return QStringLiteral("qnx"); + +#elif defined(Q_OS_ANDROID) + return QStringLiteral("android"); + +#elif defined(Q_OS_IOS) + return QStringLiteral("ios"); +#elif defined(Q_OS_OSX) + return QStringLiteral("osx"); +#elif defined(Q_OS_DARWIN) + return QStringLiteral("darwin"); + +#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.productType.isEmpty()) + return unixOsVersion.productType; +#endif + return unknownText(); +} + +QString SubsurfaceSysInfo::productVersion() +{ +#if defined(Q_OS_IOS) + int major = (int(MacintoshVersion) >> 4) & 0xf; + int minor = int(MacintoshVersion) & 0xf; + if (Q_LIKELY(major < 10 && minor < 10)) { + char buf[4] = { char(major + '0'), '.', char(minor + '0'), '\0' }; + return QString::fromLatin1(buf, 3); + } + return QString::number(major) + QLatin1Char('.') + QString::number(minor); +#elif defined(Q_OS_OSX) + int minor = int(MacintoshVersion) - 2; // we're not running on Mac OS 9 + Q_ASSERT(minor < 100); + char buf[] = "10.0\0"; + if (Q_LIKELY(minor < 10)) { + buf[3] += minor; + } else { + buf[3] += minor / 10; + buf[4] = '0' + minor % 10; + } + return QString::fromLatin1(buf); +#elif defined(Q_OS_WIN) + const char *version = winVer_helper(); + if (version) + return QString::fromLatin1(version).toLower(); + // fall through + + // Android and Blackberry should not fall through to the Unix code +#elif defined(Q_OS_ANDROID) + // TBD +#elif defined(Q_OS_BLACKBERRY) + deviceinfo_details_t *deviceInfo; + if (deviceinfo_get_details(&deviceInfo) == BPS_SUCCESS) { + QString bbVersion = QString::fromLatin1(deviceinfo_details_get_device_os_version(deviceInfo)); + deviceinfo_free_details(&deviceInfo); + return bbVersion; + } +#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.productVersion.isEmpty()) + return unixOsVersion.productVersion; +#endif + + // fallback + return unknownText(); +} + +QString SubsurfaceSysInfo::prettyProductName() +{ +#if defined(Q_OS_IOS) + return QLatin1String("iOS ") + productVersion(); +#elif defined(Q_OS_OSX) + // get the known codenames + const char *basename = 0; + switch (int(MacintoshVersion)) { + case MV_CHEETAH: + case MV_PUMA: + case MV_JAGUAR: + case MV_PANTHER: + case MV_TIGER: + // This version of Qt does not run on those versions of OS X + // so this case label will never be reached + Q_UNREACHABLE(); + break; + case MV_LEOPARD: + basename = "Mac OS X Leopard ("; + break; + case MV_SNOWLEOPARD: + basename = "Mac OS X Snow Leopard ("; + break; + case MV_LION: + basename = "Mac OS X Lion ("; + break; + case MV_MOUNTAINLION: + basename = "OS X Mountain Lion ("; + break; + case MV_MAVERICKS: + basename = "OS X Mavericks ("; + break; +#ifdef MV_YOSEMITE + case MV_YOSEMITE: +#else + case 0x000C: // MV_YOSEMITE +#endif + basename = "OS X Yosemite ("; + break; +#ifdef MV_ELCAPITAN + case MV_ELCAPITAN : +#else + case 0x000D: // MV_ELCAPITAN +#endif + basename = "OS X El Capitan ("; + break; + } + if (basename) + return QLatin1String(basename) + productVersion() + QLatin1Char(')'); + + // a future version of OS X + return QLatin1String("OS X ") + productVersion(); +#elif defined(Q_OS_WINPHONE) + return QLatin1String("Windows Phone ") + QLatin1String(winVer_helper()); +#elif defined(Q_OS_WIN) + return QLatin1String("Windows ") + QLatin1String(winVer_helper()); +#elif defined(Q_OS_ANDROID) + return QLatin1String("Android ") + productVersion(); +#elif defined(Q_OS_BLACKBERRY) + return QLatin1String("BlackBerry ") + productVersion(); +#elif defined(Q_OS_UNIX) +# ifdef USE_ETC_OS_RELEASE + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.prettyName.isEmpty()) + return unixOsVersion.prettyName; +# endif + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname) + QLatin1Char(' ') + QString::fromLatin1(u.release); +#endif + return unknownText(); +} + +#endif // Qt >= 5.4 + +QString SubsurfaceSysInfo::prettyOsName() +{ + // Matches the pre-release version of Qt 5.4 + QString pretty = prettyProductName(); +#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) + // QSysInfo::kernelType() returns lowercase ("linux" instead of "Linux") + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname) + QLatin1String(" (") + pretty + QLatin1Char(')'); +#endif + return pretty; +} + +extern "C" { +bool isWin7Or8() +{ +#ifdef Q_OS_WIN + return (QSysInfo::WindowsVersion & QSysInfo::WV_NT_based) >= QSysInfo::WV_WINDOWS7; +#else + return false; +#endif +} +} diff --git a/core/subsurfacesysinfo.h b/core/subsurfacesysinfo.h new file mode 100644 index 000000000..b2c267b83 --- /dev/null +++ b/core/subsurfacesysinfo.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 Intel Corporation +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SUBSURFACESYSINFO_H +#define SUBSURFACESYSINFO_H + +#include <QSysInfo> + +class SubsurfaceSysInfo : public QSysInfo { +public: +#if QT_VERSION < 0x050400 + static QString buildCpuArchitecture(); + static QString currentCpuArchitecture(); + static QString buildAbi(); + + static QString kernelType(); + static QString kernelVersion(); + static QString productType(); + static QString productVersion(); + static QString prettyProductName(); +#endif + static QString prettyOsName(); +}; + + +#endif // SUBSURFACESYSINFO_H diff --git a/core/taxonomy.c b/core/taxonomy.c new file mode 100644 index 000000000..670d85ad0 --- /dev/null +++ b/core/taxonomy.c @@ -0,0 +1,48 @@ +#include "taxonomy.h" +#include "gettext.h" +#include <stdlib.h> + +char *taxonomy_category_names[TC_NR_CATEGORIES] = { + QT_TRANSLATE_NOOP("getTextFromC", "None"), + QT_TRANSLATE_NOOP("getTextFromC", "Ocean"), + QT_TRANSLATE_NOOP("getTextFromC", "Country"), + QT_TRANSLATE_NOOP("getTextFromC", "State"), + QT_TRANSLATE_NOOP("getTextFromC", "County"), + QT_TRANSLATE_NOOP("getTextFromC", "Town"), + QT_TRANSLATE_NOOP("getTextFromC", "City") +}; + +// these are the names for geoname.org +char *taxonomy_api_names[TC_NR_CATEGORIES] = { + "none", + "name", + "countryName", + "adminName1", + "adminName2", + "toponymName", + "adminName3" +}; + +struct taxonomy *alloc_taxonomy() +{ + return calloc(TC_NR_CATEGORIES, sizeof(struct taxonomy)); +} + +void free_taxonomy(struct taxonomy_data *t) +{ + if (t) { + for (int i = 0; i < t->nr; i++) + free((void *)t->category[i].value); + free(t->category); + t->category = NULL; + t->nr = 0; + } +} + +int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat) +{ + for (int i = 0; i < t->nr; i++) + if (t->category[i].category == cat) + return i; + return -1; +} diff --git a/core/taxonomy.h b/core/taxonomy.h new file mode 100644 index 000000000..51245d562 --- /dev/null +++ b/core/taxonomy.h @@ -0,0 +1,41 @@ +#ifndef TAXONOMY_H +#define TAXONOMY_H + +#ifdef __cplusplus +extern "C" { +#endif + +enum taxonomy_category { + TC_NONE, + TC_OCEAN, + TC_COUNTRY, + TC_ADMIN_L1, + TC_ADMIN_L2, + TC_LOCALNAME, + TC_ADMIN_L3, + TC_NR_CATEGORIES +}; + +extern char *taxonomy_category_names[TC_NR_CATEGORIES]; +extern char *taxonomy_api_names[TC_NR_CATEGORIES]; + +struct taxonomy { + int category; /* the category for this tag: ocean, country, admin_l1, admin_l2, localname, etc */ + const char *value; /* the value returned, parsed, or manually entered for that category */ + enum { GEOCODED, PARSED, MANUAL, COPIED } origin; +}; + +/* the data block contains 3 taxonomy structures - unused ones have a tag value of NONE */ +struct taxonomy_data { + int nr; + struct taxonomy *category; +}; + +struct taxonomy *alloc_taxonomy(); +void free_taxonomy(struct taxonomy_data *t); +int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat); + +#ifdef __cplusplus +} +#endif +#endif // TAXONOMY_H diff --git a/core/time.c b/core/time.c new file mode 100644 index 000000000..0893f19d8 --- /dev/null +++ b/core/time.c @@ -0,0 +1,98 @@ +#include <string.h> +#include "dive.h" + +/* + * Convert 64-bit timestamp to 'struct tm' in UTC. + * + * On 32-bit machines, only do 64-bit arithmetic for the seconds + * part, after that we do everything in 'long'. 64-bit divides + * are unnecessary once you're counting minutes (32-bit minutes: + * 8000+ years). + */ +void utc_mkdate(timestamp_t timestamp, struct tm *tm) +{ + static const unsigned int mdays[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + static const unsigned int mdays_leap[] = { + 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + unsigned long val; + unsigned int leapyears; + int m; + const unsigned int *mp; + + memset(tm, 0, sizeof(*tm)); + + /* seconds since 1970 -> minutes since 1970 */ + tm->tm_sec = timestamp % 60; + val = timestamp /= 60; + + /* Do the simple stuff */ + tm->tm_min = val % 60; + val /= 60; + tm->tm_hour = val % 24; + val /= 24; + + /* Jan 1, 1970 was a Thursday (tm_wday=4) */ + tm->tm_wday = (val + 4) % 7; + + /* + * Now we're in "days since Jan 1, 1970". To make things easier, + * let's make it "days since Jan 1, 1968", since that's a leap-year + */ + val += 365 + 366; + + /* This only works up until 2099 (2100 isn't a leap-year) */ + leapyears = val / (365 * 4 + 1); + val %= (365 * 4 + 1); + tm->tm_year = 68 + leapyears * 4; + + /* Handle the leap-year itself */ + mp = mdays_leap; + if (val > 365) { + tm->tm_year++; + val -= 366; + tm->tm_year += val / 365; + val %= 365; + mp = mdays; + } + + for (m = 0; m < 12; m++) { + if (val < *mp) + break; + val -= *mp++; + } + tm->tm_mday = val + 1; + tm->tm_mon = m; +} + +timestamp_t utc_mktime(struct tm *tm) +{ + static const int mdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + int year = tm->tm_year; + int month = tm->tm_mon; + int day = tm->tm_mday; + + /* First normalize relative to 1900 */ + if (year < 70) + year += 100; + else if (year > 1900) + year -= 1900; + + /* Normalized to Jan 1, 1970: unix time */ + year -= 70; + + if (year < 0 || year > 129) /* algo only works for 1970-2099 */ + return -1; + if (month < 0 || month > 11) /* array bounds */ + return -1; + if (month < 2 || (year + 2) % 4) + day--; + if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) + return -1; + return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24 * 60 * 60UL + + tm->tm_hour * 60 * 60 + tm->tm_min * 60 + tm->tm_sec; +} diff --git a/core/uemis-downloader.c b/core/uemis-downloader.c new file mode 100644 index 000000000..b9b532303 --- /dev/null +++ b/core/uemis-downloader.c @@ -0,0 +1,1403 @@ +/* + * uemis-downloader.c + * + * Copyright (c) Dirk Hohndel <dirk@hohndel.org> + * released under GPL2 + * + * very (VERY) loosely based on the algorithms found in Java code by Fabian Gast <fgast@only640k.net> + * which was released under the BSD-STYLE BEER WARE LICENSE + * I believe that I only used the information about HOW to do this (download data from the Uemis + * Zurich) but did not actually use any of his copyrighted code, therefore the license under which + * he released his code does not apply to this new implementation in C + * + * Modified by Guido Lerch guido.lerch@gmail.com in August 2015 + */ +#include <fcntl.h> +#include <dirent.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> + +#include "gettext.h" +#include "libdivecomputer.h" +#include "uemis.h" +#include "divelist.h" + +#define ERR_FS_ALMOST_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is almost full.\nDisconnect/reconnect the dive computer\nand click \'Retry\'") +#define ERR_FS_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is full.\nDisconnect/reconnect the dive computer\nand click Retry") +#define ERR_FS_SHORT_WRITE QT_TRANSLATE_NOOP("gettextFromC", "Short write to req.txt file.\nIs the Uemis Zurich plugged in correctly?") +#define ERR_NO_FILES QT_TRANSLATE_NOOP("gettextFromC", "No dives to download.") +#define BUFLEN 2048 +#define BUFLEN 2048 +#define NUM_PARAM_BUFS 10 + +// debugging setup +// #define UEMIS_DEBUG 1 + 2 + 4 + 8 + 16 + 32 + +#define UEMIS_MAX_FILES 4000 +#define UEMIS_MEM_FULL 1 +#define UEMIS_MEM_OK 0 +#define UEMIS_SPOT_BLOCK_SIZE 1 +#define UEMIS_DIVE_DETAILS_SIZE 2 +#define UEMIS_LOG_BLOCK_SIZE 10 +#define UEMIS_CHECK_LOG 1 +#define UEMIS_CHECK_DETAILS 2 +#define UEMIS_CHECK_SINGLE_DIVE 3 + +#if UEMIS_DEBUG +const char *home, *user, *d_time; +static int debug_round = 0; +#define debugfile stderr +#endif + +#if UEMIS_DEBUG & 64 /* we are reading from a copy of the filesystem, not the device - no need to wait */ +#define UEMIS_TIMEOUT 50 /* 50ns */ +#define UEMIS_LONG_TIMEOUT 500 /* 500ns */ +#define UEMIS_MAX_TIMEOUT 2000 /* 2ms */ +#else +#define UEMIS_TIMEOUT 50000 /* 50ms */ +#define UEMIS_LONG_TIMEOUT 500000 /* 500ms */ +#define UEMIS_MAX_TIMEOUT 2000000 /* 2s */ +#endif + +static char *param_buff[NUM_PARAM_BUFS]; +static char *reqtxt_path; +static int reqtxt_file; +static int filenr; +static int number_of_files; +static char *mbuf = NULL; +static int mbuf_size = 0; + +static int max_mem_used = -1; +static int next_table_index = 0; +static int dive_to_read = 0; + +static int max_deleted_seen = -1; + +/* helper function to parse the Uemis data structures */ +static void uemis_ts(char *buffer, void *_when) +{ + struct tm tm; + timestamp_t *when = _when; + + memset(&tm, 0, sizeof(tm)); + sscanf(buffer, "%d-%d-%dT%d:%d:%d", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec); + tm.tm_mon -= 1; + tm.tm_year -= 1900; + *when = utc_mktime(&tm); +} + +/* float minutes */ +static void uemis_duration(char *buffer, duration_t *duration) +{ + duration->seconds = rint(ascii_strtod(buffer, NULL) * 60); +} + +/* int cm */ +static void uemis_depth(char *buffer, depth_t *depth) +{ + depth->mm = atoi(buffer) * 10; +} + +static void uemis_get_index(char *buffer, int *idx) +{ + *idx = atoi(buffer); +} + +/* space separated */ +static void uemis_add_string(const char *buffer, char **text, const char *delimit) +{ + /* do nothing if this is an empty buffer (Uemis sometimes returns a single + * space for empty buffers) */ + if (!buffer || !*buffer || (*buffer == ' ' && *(buffer + 1) == '\0')) + return; + if (!*text) { + *text = strdup(buffer); + } else { + char *buf = malloc(strlen(buffer) + strlen(*text) + 2); + strcpy(buf, *text); + strcat(buf, delimit); + strcat(buf, buffer); + free(*text); + *text = buf; + } +} + +/* still unclear if it ever reports lbs */ +static void uemis_get_weight(char *buffer, weightsystem_t *weight, int diveid) +{ + weight->weight.grams = uemis_get_weight_unit(diveid) ? + lbs_to_grams(ascii_strtod(buffer, NULL)) : + ascii_strtod(buffer, NULL) * 1000; + weight->description = strdup(translate("gettextFromC", "unknown")); +} + +static struct dive *uemis_start_dive(uint32_t deviceid) +{ + struct dive *dive = alloc_dive(); + dive->downloaded = true; + dive->dc.model = strdup("Uemis Zurich"); + dive->dc.deviceid = deviceid; + return dive; +} + +static struct dive *get_dive_by_uemis_diveid(device_data_t *devdata, uint32_t object_id) +{ + for (int i = 0; i < devdata->download_table->nr; i++) { + if (object_id == devdata->download_table->dives[i]->dc.diveid) + return devdata->download_table->dives[i]; + } + return NULL; +} + +static void record_uemis_dive(device_data_t *devdata, struct dive *dive) +{ + if (devdata->create_new_trip) { + if (!devdata->trip) + devdata->trip = create_and_hookup_trip_from_dive(dive); + else + add_dive_to_trip(dive, devdata->trip); + } + record_dive_to_table(dive, devdata->download_table); +} + +/* send text to the importer progress bar */ +static void uemis_info(const char *fmt, ...) +{ + static char buffer[256]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + progress_bar_text = buffer; + if (verbose) + fprintf(stderr, "Uemis downloader: %s\n", buffer); +} + +static long bytes_available(int file) +{ + long result; + long now = lseek(file, 0, SEEK_CUR); + if (now == -1) + return 0; + result = lseek(file, 0, SEEK_END); + lseek(file, now, SEEK_SET); + if (now == -1 || result == -1) + return 0; + return result; +} + +static int number_of_file(char *path) +{ + int count = 0; +#ifdef WIN32 + struct _wdirent *entry; + _WDIR *dirp = (_WDIR *)subsurface_opendir(path); +#else + struct dirent *entry; + DIR *dirp = (DIR *)subsurface_opendir(path); +#endif + + while (dirp) { +#ifdef WIN32 + entry = _wreaddir(dirp); + if (!entry) + break; +#else + entry = readdir(dirp); + if (!entry) + break; + if (strstr(entry->d_name, ".TXT") || strstr(entry->d_name, ".txt")) /* If the entry is a regular file */ +#endif + count++; + } +#ifdef WIN32 + _wclosedir(dirp); +#else + closedir(dirp); +#endif + return count; +} + +static char *build_filename(const char *path, const char *name) +{ + int len = strlen(path) + strlen(name) + 2; + char *buf = malloc(len); +#if WIN32 + snprintf(buf, len, "%s\\%s", path, name); +#else + snprintf(buf, len, "%s/%s", path, name); +#endif + return buf; +} + +/* Check if there's a req.txt file and get the starting filenr from it. + * Test for the maximum number of ANS files (I believe this is always + * 4000 but in case there are differences depending on firmware, this + * code is easy enough */ +static bool uemis_init(const char *path) +{ + char *ans_path; + int i; + + if (!path) + return false; + /* let's check if this is indeed a Uemis DC */ + reqtxt_path = build_filename(path, "req.txt"); + reqtxt_file = subsurface_open(reqtxt_path, O_RDONLY | O_CREAT, 0666); + if (reqtxt_file < 0) { +#if UEMIS_DEBUG & 1 + fprintf(debugfile, ":EE req.txt can't be opened\n"); +#endif + return false; + } + if (bytes_available(reqtxt_file) > 5) { + char tmp[6]; + read(reqtxt_file, tmp, 5); + tmp[5] = '\0'; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "::r req.txt \"%s\"\n", tmp); +#endif + if (sscanf(tmp + 1, "%d", &filenr) != 1) + return false; + } else { + filenr = 0; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "::r req.txt skipped as there were fewer than 5 bytes\n"); +#endif + } + close(reqtxt_file); + + /* It would be nice if we could simply go back to the first set of + * ANS files. But with a FAT filesystem that isn't possible */ + ans_path = build_filename(path, "ANS"); + number_of_files = number_of_file(ans_path); + free(ans_path); + /* initialize the array in which we collect the answers */ + for (i = 0; i < NUM_PARAM_BUFS; i++) + param_buff[i] = ""; + return true; +} + +static void str_append_with_delim(char *s, char *t) +{ + int len = strlen(s); + snprintf(s + len, BUFLEN - len, "%s{", t); +} + +/* The communication protocol with the DC is truly funky. + * After you write your request to the req.txt file you call this function. + * It writes the number of the next ANS file at the beginning of the req.txt + * file (prefixed by 'n' or 'r') and then again at the very end of it, after + * the full request (this time without the prefix). + * Then it syncs (not needed on Windows) and closes the file. */ +static void trigger_response(int file, char *command, int nr, long tailpos) +{ + char fl[10]; + + snprintf(fl, 8, "%s%04d", command, nr); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, ":tr %s (after seeks)\n", fl); +#endif + if (lseek(file, 0, SEEK_SET) == -1) + goto fs_error; + if (write(file, fl, strlen(fl)) == -1) + goto fs_error; + if (lseek(file, tailpos, SEEK_SET) == -1) + goto fs_error; + if (write(file, fl + 1, strlen(fl + 1)) == -1) + goto fs_error; +#ifndef WIN32 + fsync(file); +#endif +fs_error: + close(file); +} + +static char *next_token(char **buf) +{ + char *q, *p = strchr(*buf, '{'); + if (p) + *p = '\0'; + else + p = *buf + strlen(*buf) - 1; + q = *buf; + *buf = p + 1; + return q; +} + +/* poor man's tokenizer that understands a quoted delimiter ('{') */ +static char *next_segment(char *buf, int *offset, int size) +{ + int i = *offset; + int seg_size; + bool done = false; + char *segment; + + while (!done) { + if (i < size) { + if (i < size - 1 && buf[i] == '\\' && + (buf[i + 1] == '\\' || buf[i + 1] == '{')) + memcpy(buf + i, buf + i + 1, size - i - 1); + else if (buf[i] == '{') + done = true; + i++; + } else { + done = true; + } + } + seg_size = i - *offset - 1; + if (seg_size < 0) + seg_size = 0; + segment = malloc(seg_size + 1); + memcpy(segment, buf + *offset, seg_size); + segment[seg_size] = '\0'; + *offset = i; + return segment; +} + +/* a dynamically growing buffer to store the potentially massive responses. + * The binary data block can be more than 100k in size (base64 encoded) */ +static void buffer_add(char **buffer, int *buffer_size, char *buf) +{ + if (!buf) + return; + if (!*buffer) { + *buffer = strdup(buf); + *buffer_size = strlen(*buffer) + 1; + } else { + *buffer_size += strlen(buf); + *buffer = realloc(*buffer, *buffer_size); + strcat(*buffer, buf); + } +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "added \"%s\" to buffer - new length %d\n", buf, *buffer_size); +#endif +} + +/* are there more ANS files we can check? */ +static bool next_file(int max) +{ + if (filenr >= max) + return false; + filenr++; + return true; +} + +/* try and do a quick decode - without trying to get to fancy in case the data + * straddles a block boundary... + * we are parsing something that looks like this: + * object_id{int{2{date{ts{2011-04-05T12:38:04{duration{float{12.000... + */ +static char *first_object_id_val(char *buf) +{ + char *object, *bufend; + if (!buf) + return NULL; + bufend = buf + strlen(buf); + object = strstr(buf, "object_id"); + if (object && object + 14 < bufend) { + /* get the value */ + char tmp[36]; + char *p = object + 14; + char *t = tmp; + +#if UEMIS_DEBUG & 4 + char debugbuf[50]; + strncpy(debugbuf, object, 49); + debugbuf[49] = '\0'; + fprintf(debugfile, "buf |%s|\n", debugbuf); +#endif + while (p < bufend && *p != '{' && t < tmp + 9) + *t++ = *p++; + if (*p == '{') { + /* found the object_id - let's quickly look for the date */ + if (strncmp(p, "{date{ts{", 9) == 0 && strstr(p, "{duration{") != NULL) { + /* cool - that was easy */ + *t++ = ','; + *t++ = ' '; + /* skip the 9 characters we just found and take the date, ignoring the seconds + * and replace the silly 'T' in the middle with a space */ + strncpy(t, p + 9, 16); + if (*(t + 10) == 'T') + *(t + 10) = ' '; + t += 16; + } + *t = '\0'; + return strdup(tmp); + } + } + return NULL; +} + +/* ultra-simplistic; it doesn't deal with the case when the object_id is + * split across two chunks. It also doesn't deal with the discrepancy between + * object_id and dive number as understood by the dive computer */ +static void show_progress(char *buf, const char *what) +{ + char *val = first_object_id_val(buf); + if (val) { +/* let the user know what we are working on */ +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "reading %s\n %s\n %s\n", what, val, buf); +#endif + uemis_info(translate("gettextFromC", "%s %s"), what, val); + free(val); + } +} + +static void uemis_increased_timeout(int *timeout) +{ + if (*timeout < UEMIS_MAX_TIMEOUT) + *timeout += UEMIS_LONG_TIMEOUT; + usleep(*timeout); +} + +/* send a request to the dive computer and collect the answer */ +static bool uemis_get_answer(const char *path, char *request, int n_param_in, + int n_param_out, const char **error_text) +{ + int i = 0, file_length; + char sb[BUFLEN]; + char fl[13]; + char tmp[101]; + const char *what = translate("gettextFromC", "data"); + bool searching = true; + bool assembling_mbuf = false; + bool ismulti = false; + bool found_answer = false; + bool more_files = true; + bool answer_in_mbuf = false; + char *ans_path; + int ans_file; + int timeout = UEMIS_LONG_TIMEOUT; + + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; +#ifdef UEMIS_DEBUG + fprintf(debugfile, "open %s failed with errno %d\n", reqtxt_path, errno); +#endif + return false; + } + snprintf(sb, BUFLEN, "n%04d12345678", filenr); + str_append_with_delim(sb, request); + for (i = 0; i < n_param_in; i++) + str_append_with_delim(sb, param_buff[i]); + if (!strcmp(request, "getDivelogs") || !strcmp(request, "getDeviceData") || !strcmp(request, "getDirectory") || + !strcmp(request, "getDivespot") || !strcmp(request, "getDive")) { + answer_in_mbuf = true; + str_append_with_delim(sb, ""); + if (!strcmp(request, "getDivelogs")) + what = translate("gettextFromC", "divelog #"); + else if (!strcmp(request, "getDivespot")) + what = translate("gettextFromC", "divespot #"); + else if (!strcmp(request, "getDive")) + what = translate("gettextFromC", "details for #"); + } + str_append_with_delim(sb, ""); + file_length = strlen(sb); + snprintf(fl, 10, "%08d", file_length - 13); + memcpy(sb + 5, fl, strlen(fl)); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "::w req.txt \"%s\"\n", sb); +#endif + int written = write(reqtxt_file, sb, strlen(sb)); + if (written == -1 || (size_t)written != strlen(sb)) { + *error_text = translate("gettextFromC", ERR_FS_SHORT_WRITE); + return false; + } + if (!next_file(number_of_files)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + } + trigger_response(reqtxt_file, "n", filenr, file_length); + usleep(timeout); + free(mbuf); + mbuf = NULL; + mbuf_size = 0; + while (searching || assembling_mbuf) { + if (import_thread_cancelled) + return false; + progress_bar_fraction = filenr / (double)UEMIS_MAX_FILES; + snprintf(fl, 13, "ANS%d.TXT", filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + read(ans_file, tmp, 100); + close(ans_file); +#if UEMIS_DEBUG & 8 + tmp[100] = '\0'; + fprintf(debugfile, "::t %s \"%s\"\n", ans_path, tmp); +#elif UEMIS_DEBUG & 4 + char pbuf[4]; + pbuf[0] = tmp[0]; + pbuf[1] = tmp[1]; + pbuf[2] = tmp[2]; + pbuf[3] = 0; + fprintf(debugfile, "::t %s \"%s...\"\n", ans_path, pbuf); +#endif + free(ans_path); + if (tmp[0] == '1') { + searching = false; + if (tmp[1] == 'm') { + assembling_mbuf = true; + ismulti = true; + } + if (tmp[2] == 'e') + assembling_mbuf = false; + if (assembling_mbuf) { + if (!next_file(number_of_files)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + assembling_mbuf = false; + } + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; + fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); + return false; + } + trigger_response(reqtxt_file, "n", filenr, file_length); + } + } else { + if (!next_file(number_of_files - 1)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + assembling_mbuf = false; + searching = false; + } + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; + fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); + return false; + } + trigger_response(reqtxt_file, "r", filenr, file_length); + uemis_increased_timeout(&timeout); + } + if (ismulti && more_files && tmp[0] == '1') { + int size; + snprintf(fl, 13, "ANS%d.TXT", assembling_mbuf ? filenr - 2 : filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + free(ans_path); + size = bytes_available(ans_file); + if (size > 3) { + char *buf; + int r; + if (lseek(ans_file, 3, SEEK_CUR) == -1) + goto fs_error; + buf = malloc(size - 2); + if ((r = read(ans_file, buf, size - 3)) != size - 3) { + free(buf); + goto fs_error; + } + buf[r] = '\0'; + buffer_add(&mbuf, &mbuf_size, buf); + show_progress(buf, what); + free(buf); + param_buff[3]++; + } + close(ans_file); + timeout = UEMIS_TIMEOUT; + usleep(UEMIS_TIMEOUT); + } + } + if (more_files) { + int size = 0, j = 0; + char *buf = NULL; + + if (!ismulti) { + snprintf(fl, 13, "ANS%d.TXT", filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + free(ans_path); + size = bytes_available(ans_file); + if (size > 3) { + int r; + if (lseek(ans_file, 3, SEEK_CUR) == -1) + goto fs_error; + buf = malloc(size - 2); + if ((r = read(ans_file, buf, size - 3)) != size - 3) { + free(buf); + goto fs_error; + } + buf[r] = '\0'; + buffer_add(&mbuf, &mbuf_size, buf); + show_progress(buf, what); +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "::r %s \"%s\"\n", fl, buf); +#endif + } + size -= 3; + close(ans_file); + } else { + ismulti = false; + } +#if UEMIS_DEBUG & 8 + fprintf(debugfile, ":r: %s\n", buf); +#endif + if (!answer_in_mbuf) + for (i = 0; i < n_param_out && j < size; i++) + param_buff[i] = next_segment(buf, &j, size); + found_answer = true; + free(buf); + } +#if UEMIS_DEBUG & 1 + for (i = 0; i < n_param_out; i++) + fprintf(debugfile, "::: %d: %s\n", i, param_buff[i]); +#endif + return found_answer; +fs_error: + close(ans_file); + return false; +} + +static bool parse_divespot(char *buf) +{ + char *bp = buf + 1; + char *tp = next_token(&bp); + char *tag, *type, *val; + char locationstring[1024] = ""; + int divespot, len; + double latitude = 0.0, longitude = 0.0; + + // dive spot got deleted, so fail here + if (strstr(bp, "deleted{bool{true")) + return false; + // not a dive spot, fail here + if (strcmp(tp, "divespot")) + return false; + do + tag = next_token(&bp); + while (*tag && strcmp(tag, "object_id")); + if (!*tag) + return false; + next_token(&bp); + val = next_token(&bp); + divespot = atoi(val); + do { + tag = next_token(&bp); + type = next_token(&bp); + val = next_token(&bp); + if (!strcmp(type, "string") && *val && strcmp(val, " ")) { + len = strlen(locationstring); + snprintf(locationstring + len, sizeof(locationstring) - len, + "%s%s", len ? ", " : "", val); + } else if (!strcmp(type, "float")) { + if (!strcmp(tag, "longitude")) + longitude = ascii_strtod(val, NULL); + else if (!strcmp(tag, "latitude")) + latitude = ascii_strtod(val, NULL); + } + } while (tag && *tag); + + uemis_set_divelocation(divespot, locationstring, longitude, latitude); + return true; +} + +static char *suit[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "wetsuit"), QT_TRANSLATE_NOOP("gettextFromC", "semidry"), QT_TRANSLATE_NOOP("gettextFromC", "drysuit")}; +static char *suit_type[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "shorty"), QT_TRANSLATE_NOOP("gettextFromC", "vest"), QT_TRANSLATE_NOOP("gettextFromC", "long john"), QT_TRANSLATE_NOOP("gettextFromC", "jacket"), QT_TRANSLATE_NOOP("gettextFromC", "full suit"), QT_TRANSLATE_NOOP("gettextFromC", "2 pcs full suit")}; +static char *suit_thickness[] = {"", "0.5-2mm", "2-3mm", "3-5mm", "5-7mm", "8mm+", QT_TRANSLATE_NOOP("gettextFromC", "membrane")}; + +static void parse_tag(struct dive *dive, char *tag, char *val) +{ + /* we can ignore computer_id, water and gas as those are redundant + * with the binary data and would just get overwritten */ +#if UEMIS_DEBUG & 4 + if (strcmp(tag, "file_content")) + fprintf(debugfile, "Adding to dive %d : %s = %s\n", dive->dc.diveid, tag, val); +#endif + if (!strcmp(tag, "date")) { + uemis_ts(val, &dive->when); + } else if (!strcmp(tag, "duration")) { + uemis_duration(val, &dive->dc.duration); + } else if (!strcmp(tag, "depth")) { + uemis_depth(val, &dive->dc.maxdepth); + } else if (!strcmp(tag, "file_content")) { + uemis_parse_divelog_binary(val, dive); + } else if (!strcmp(tag, "altitude")) { + uemis_get_index(val, &dive->dc.surface_pressure.mbar); + } else if (!strcmp(tag, "f32Weight")) { + uemis_get_weight(val, &dive->weightsystem[0], dive->dc.diveid); + } else if (!strcmp(tag, "notes")) { + uemis_add_string(val, &dive->notes, " "); + } else if (!strcmp(tag, "u8DiveSuit")) { + if (*suit[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "u8DiveSuitType")) { + if (*suit_type[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit_type[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "u8SuitThickness")) { + if (*suit_thickness[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit_thickness[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "nickname")) { + uemis_add_string(val, &dive->buddy, ","); + } +} + +static bool uemis_delete_dive(device_data_t *devdata, uint32_t diveid) +{ + struct dive *dive = NULL; + + if (devdata->download_table->dives[devdata->download_table->nr - 1]->dc.diveid == diveid) { + /* we hit the last one in the array */ + dive = devdata->download_table->dives[devdata->download_table->nr - 1]; + } else { + for (int i = 0; i < devdata->download_table->nr - 1; i++) { + if (devdata->download_table->dives[i]->dc.diveid == diveid) { + dive = devdata->download_table->dives[i]; + for (int x = i; x < devdata->download_table->nr - 1; x++) + devdata->download_table->dives[i] = devdata->download_table->dives[x + 1]; + } + } + } + if (dive) { + devdata->download_table->dives[--devdata->download_table->nr] = NULL; + if (dive->tripflag != TF_NONE) + remove_dive_from_trip(dive, false); + + free(dive->dc.sample); + free((void *)dive->notes); + free((void *)dive->divemaster); + free((void *)dive->buddy); + free((void *)dive->suit); + taglist_free(dive->tag_list); + free(dive); + + return true; + } + return false; +} + +/* This function is called for both divelog and dive information that we get + * from the SDA (what an insane design, btw). The object_id in the divelog + * matches the logfilenr in the dive information (which has its own, often + * different object_id) - we use this as the diveid. + * We create the dive when parsing the divelog and then later, when we parse + * the dive information we locate the already created dive via its diveid. + * Most things just get parsed and converted into our internal data structures, + * but the dive location API is even more crazy. We just get an id that is an + * index into yet another data store that we read out later. In order to + * correctly populate the location and gps data from that we need to remember + * the addresses of those fields for every dive that references the divespot. */ +static bool process_raw_buffer(device_data_t *devdata, uint32_t deviceid, char *inbuf, char **max_divenr, int *for_dive) +{ + char *buf = strdup(inbuf); + char *tp, *bp, *tag, *type, *val; + bool done = false; + int inbuflen = strlen(inbuf); + char *endptr = buf + inbuflen; + bool is_log = false, is_dive = false; + char *sections[10]; + size_t s, nr_sections = 0; + struct dive *dive = NULL; + char dive_no[10]; + +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "p_r_b %s\n", inbuf); +#endif + if (for_dive) + *for_dive = -1; + bp = buf + 1; + tp = next_token(&bp); + if (strcmp(tp, "divelog") == 0) { + /* this is a divelog */ + is_log = true; + tp = next_token(&bp); + /* is it a valid entry or nothing ? */ + if (strcmp(tp, "1.0") != 0 || strstr(inbuf, "divelog{1.0{{{{")) { + free(buf); + return false; + } + } else if (strcmp(tp, "dive") == 0) { + /* this is dive detail */ + is_dive = true; + tp = next_token(&bp); + if (strcmp(tp, "1.0") != 0) { + free(buf); + return false; + } + } else { + /* don't understand the buffer */ + free(buf); + return false; + } + if (is_log) { + dive = uemis_start_dive(deviceid); + } else { + /* remember, we don't know if this is the right entry, + * so first test if this is even a valid entry */ + if (strstr(inbuf, "deleted{bool{true")) { +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "p_r_b entry deleted\n"); +#endif + /* oops, this one isn't valid, suggest to try the previous one */ + free(buf); + return false; + } + /* quickhack and workaround to capture the original dive_no + * I am doing this so I don't have to change the original design + * but when parsing a dive we never parse the dive number because + * at the time it's being read the *dive variable is not set because + * the dive_no tag comes before the object_id in the uemis ans file + */ + dive_no[0] = '\0'; + char *dive_no_buf = strdup(inbuf); + char *dive_no_ptr = strstr(dive_no_buf, "dive_no{int{") + 12; + if (dive_no_ptr) { + char *dive_no_end = strstr(dive_no_ptr, "{"); + if (dive_no_end) { + *dive_no_end = '\0'; + strncpy(dive_no, dive_no_ptr, 9); + dive_no[9] = '\0'; + } + } + free(dive_no_buf); + } + while (!done) { + /* the valid buffer ends with a series of delimiters */ + if (bp >= endptr - 2 || !strcmp(bp, "{{")) + break; + tag = next_token(&bp); + /* we also end if we get an empty tag */ + if (*tag == '\0') + break; + for (s = 0; s < nr_sections; s++) + if (!strcmp(tag, sections[s])) { + tag = next_token(&bp); + break; + } + type = next_token(&bp); + if (!strcmp(type, "1.0")) { + /* this tells us the sections that will follow; the tag here + * is of the format dive-<section> */ + sections[nr_sections] = strchr(tag, '-') + 1; +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "Expect to find section %s\n", sections[nr_sections]); +#endif + if (nr_sections < sizeof(sections) - 1) + nr_sections++; + continue; + } + val = next_token(&bp); +#if UEMIS_DEBUG & 8 + if (strlen(val) < 20) + fprintf(debugfile, "Parsed %s, %s, %s\n*************************\n", tag, type, val); +#endif + if (is_log && strcmp(tag, "object_id") == 0) { + free(*max_divenr); + *max_divenr = strdup(val); + dive->dc.diveid = atoi(val); +#if UEMIS_DEBUG % 2 + fprintf(debugfile, "Adding new dive from log with object_id %d.\n", atoi(val)); +#endif + } else if (is_dive && strcmp(tag, "logfilenr") == 0) { + /* this one tells us which dive we are adding data to */ + dive = get_dive_by_uemis_diveid(devdata, atoi(val)); + if (strcmp(dive_no, "0")) + dive->number = atoi(dive_no); + if (for_dive) + *for_dive = atoi(val); + } else if (!is_log && dive && !strcmp(tag, "divespot_id")) { + int divespot_id = atoi(val); + if (divespot_id != -1) { + dive->dive_site_uuid = create_dive_site("from Uemis", dive->when); + uemis_mark_divelocation(dive->dc.diveid, divespot_id, dive->dive_site_uuid); + } +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Created divesite %d for diveid : %d\n", dive->dive_site_uuid, dive->dc.diveid); +#endif + } else if (dive) { + parse_tag(dive, tag, val); + } + if (is_log && !strcmp(tag, "file_content")) + done = true; + /* done with one dive (got the file_content tag), but there could be more: + * a '{' indicates the end of the record - but we need to see another "{{" + * later in the buffer to know that the next record is complete (it could + * be a short read because of some error */ + if (done && ++bp < endptr && *bp != '{' && strstr(bp, "{{")) { + done = false; + record_uemis_dive(devdata, dive); + mark_divelist_changed(true); + dive = uemis_start_dive(deviceid); + } + } + if (is_log) { + if (dive->dc.diveid) { + record_uemis_dive(devdata, dive); + mark_divelist_changed(true); + } else { /* partial dive */ + free(dive); + free(buf); + return false; + } + } + free(buf); + return true; +} + +static char *uemis_get_divenr(char *deviceidstr, int force) +{ + uint32_t deviceid, maxdiveid = 0; + int i; + char divenr[10]; + struct dive_table *table; + deviceid = atoi(deviceidstr); + + /* + * If we are are retrying after a disconnect/reconnect, we + * will look up the highest dive number in the dives we + * already have. + * + * Also, if "force_download" is true, do this even if we + * don't have any dives (maxdiveid will remain zero) + */ + if (force || downloadTable.nr) + table = &downloadTable; + else + table = &dive_table; + + for (i = 0; i < table->nr; i++) { + struct dive *d = table->dives[i]; + struct divecomputer *dc; + if (!d) + continue; + for_each_dc (d, dc) { + if (dc->model && !strcmp(dc->model, "Uemis Zurich") && + (dc->deviceid == 0 || dc->deviceid == 0x7fffffff || dc->deviceid == deviceid) && + dc->diveid > maxdiveid) + maxdiveid = dc->diveid; + } + } + if (max_deleted_seen >= 0 && maxdiveid < (uint32_t)max_deleted_seen) { + maxdiveid = max_deleted_seen; +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "overriding max seen with max deleted seen %d\n", max_deleted_seen); +#endif + } + snprintf(divenr, 10, "%d", maxdiveid); + return strdup(divenr); +} + +#if UEMIS_DEBUG +static int bufCnt = 0; +static bool do_dump_buffer_to_file(char *buf, char *prefix) +{ + char path[100]; + char date[40]; + char obid[40]; + if (!buf) + return false; + + if (strstr(buf, "date{ts{")) + strncpy(date, strstr(buf, "date{ts{"), sizeof(date)); + else + strncpy(date, "date{ts{no-date{", sizeof(date)); + + if (!strstr(buf, "object_id{int{")) + return false; + + strncpy(obid, strstr(buf, "object_id{int{"), sizeof(obid)); + char *ptr1 = strstr(date, "date{ts{"); + char *ptr2 = strstr(obid, "object_id{int{"); + char *pdate = next_token(&ptr1); + pdate = next_token(&ptr1); + pdate = next_token(&ptr1); + char *pobid = next_token(&ptr2); + pobid = next_token(&ptr2); + pobid = next_token(&ptr2); + snprintf(path, sizeof(path), "/%s/%s/UEMIS Dump/%s_%s_Uemis_dump_%s_in_round_%d_%d.txt", home, user, prefix, pdate, pobid, debug_round, bufCnt); + int dumpFile = subsurface_open(path, O_RDWR | O_CREAT, 0666); + if (dumpFile == -1) + return false; + write(dumpFile, buf, strlen(buf)); + close(dumpFile); + bufCnt++; + return true; +} +#endif + +/* do some more sophisticated calculations here to try and predict if the next round of + * divelog/divedetail reads will fit into the UEMIS buffer, + * filenr holds now the uemis filenr after having read several logs including the dive details, + * fCapacity will five us the average number of files needed for all currently loaded data + * remember the maximum file usage per dive + * return : UEMIS_MEM_OK if there is enough memory for a full round + * UEMIS_MEM_CRITICAL if the memory is good for reading the dive logs + * UEMIS_MEM_FULL if the memory is exhausted + */ +static int get_memory(struct dive_table *td, int checkpoint) +{ + if (td->nr <= 0) + return UEMIS_MEM_OK; + + switch (checkpoint) { + case UEMIS_CHECK_LOG: + if (filenr / td->nr > max_mem_used) + max_mem_used = filenr / td->nr; + + /* check if a full block of dive logs + dive details and dive spot fit into the UEMIS buffer */ +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "max_mem_used %d (from td->nr %d) * block_size %d > max_files %d - filenr %d?\n", max_mem_used, td->nr, UEMIS_LOG_BLOCK_SIZE, UEMIS_MAX_FILES, filenr); +#endif + if (max_mem_used * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + case UEMIS_CHECK_DETAILS: + /* check if the next set of dive details and dive spot fit into the UEMIS buffer */ + if ((UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE) * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + case UEMIS_CHECK_SINGLE_DIVE: + if (UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + } + return UEMIS_MEM_OK; +} + +/* mark a dive as deleted by setting download to false + * this will be picked up by some cleaning statement later */ +static void do_delete_dives(struct dive_table *td, int idx) +{ + for (int x = idx; x < td->nr; x++) + td->dives[x]->downloaded = false; +} + +static bool load_uemis_divespot(const char *mountpath, int divespot_id) +{ + char divespotnr[10]; + snprintf(divespotnr, sizeof(divespotnr), "%d", divespot_id); + param_buff[2] = divespotnr; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "getDivespot %d\n", divespot_id); +#endif + bool success = uemis_get_answer(mountpath, "getDivespot", 3, 0, NULL); + if (mbuf && success) { +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(mbuf, "Spot"); +#endif + return parse_divespot(mbuf); + } + return false; +} + +static void get_uemis_divespot(const char *mountpath, int divespot_id, struct dive *dive) +{ + struct dive_site *nds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (nds && nds->name && strstr(nds->name,"from Uemis")) { + if (load_uemis_divespot(mountpath, divespot_id)) { + /* get the divesite based on the diveid, this should give us + * the newly created site + */ + struct dive_site *ods = NULL; + /* with the divesite name we got from parse_dive, that is called on load_uemis_divespot + * we search all existing divesites if we have one with the same name already. The function + * returns the first found which is luckily not the newly created. + */ + (void)get_dive_site_uuid_by_name(nds->name, &ods); + if (ods) { + /* if the uuid's are the same, the new site is a duplicate and can be deleted */ + if (nds->uuid != ods->uuid) { + delete_dive_site(nds->uuid); + dive->dive_site_uuid = ods->uuid; + } + } + } else { + /* if we can't load the dive site details, delete the site we + * created in process_raw_buffer + */ + delete_dive_site(dive->dive_site_uuid); + } + } +} + +static bool get_matching_dive(int idx, char *newmax, int *uemis_mem_status, struct device_data_t *data, const char *mountpath, const char deviceidnr) +{ + struct dive *dive = data->download_table->dives[idx]; + char log_file_no_to_find[20]; + char dive_to_read_buf[10]; + bool found = false; + bool found_below = false; + bool found_above = false; + int deleted_files = 0; + + snprintf(log_file_no_to_find, sizeof(log_file_no_to_find), "logfilenr{int{%d", dive->dc.diveid); +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Looking for dive details to go with divelog id %d\n", dive->dc.diveid); +#endif + while (!found) { + if (import_thread_cancelled) + break; + snprintf(dive_to_read_buf, sizeof(dive_to_read_buf), "%d", dive_to_read); + param_buff[2] = dive_to_read_buf; + (void)uemis_get_answer(mountpath, "getDive", 3, 0, NULL); +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(mbuf, "Dive"); +#endif + *uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_SINGLE_DIVE); + if (*uemis_mem_status == UEMIS_MEM_OK) { + /* if the memory isn's completely full we can try to read more divelog vs. dive details + * UEMIS_MEM_CRITICAL means not enough space for a full round but the dive details + * and the divespots should fit into the UEMIS memory + * The match we do here is to map the object_id to the logfilenr, we do this + * by iterating through the last set of loaded divelogs and then find the corresponding + * dive with the matching logfilenr */ + if (mbuf) { + if (strstr(mbuf, log_file_no_to_find)) { + /* we found the logfilenr that matches our object_id from the divelog we were looking for + * we mark the search successful even if the dive has been deleted. */ + found = true; + if (strstr(mbuf, "deleted{bool{true") == NULL) { + process_raw_buffer(data, deviceidnr, mbuf, &newmax, NULL); + /* remember the last log file number as it is very likely that subsequent dives + * have the same or higher logfile number. + * UEMIS unfortunately deletes dives by deleting the dive details and not the logs. */ +#if UEMIS_DEBUG & 2 + d_time = get_dive_date_c_string(dive->when); + fprintf(debugfile, "Matching divelog id %d from %s with dive details %d\n", dive->dc.diveid, d_time, dive_to_read); +#endif + int divespot_id = uemis_get_divespot_id_by_diveid(dive->dc.diveid); + if (divespot_id >= 0) + get_uemis_divespot(mountpath, divespot_id, dive); + + } else { + /* in this case we found a deleted file, so let's increment */ +#if UEMIS_DEBUG & 2 + d_time = get_dive_date_c_string(dive->when); + fprintf(debugfile, "TRY matching divelog id %d from %s with dive details %d but details are deleted\n", dive->dc.diveid, d_time, dive_to_read); +#endif + deleted_files++; + max_deleted_seen = dive_to_read; + /* mark this log entry as deleted and cleanup later, otherwise we mess up our array */ + dive->downloaded = false; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Deleted dive from %s, with id %d from table -- newmax is %s\n", d_time, dive->dc.diveid, newmax); +#endif + } + } else { + uint32_t nr_found = 0; + char *logfilenr = strstr(mbuf, "logfilenr"); + if (logfilenr) { + sscanf(logfilenr, "logfilenr{int{%u", &nr_found); + if (nr_found >= dive->dc.diveid || nr_found == 0) { + found_above = true; + dive_to_read = dive_to_read - 2; + } else { + found_below = true; + } + if (dive_to_read < -1) + dive_to_read = -1; + } + } + } + if (found_above && found_below) + break; + dive_to_read++; + } else { + /* At this point the memory of the UEMIS is full, let's cleanup all divelog files were + * we could not match the details to. */ + do_delete_dives(data->download_table, idx); + return false; + } + } + /* decrement iDiveToRead by the amount of deleted entries found to assure + * we are not missing any valid matches when processing subsequent logs */ + dive_to_read = (dive_to_read - deleted_files > 0 ? dive_to_read - deleted_files : 0); + deleted_files = 0; + return true; +} + +const char *do_uemis_import(device_data_t *data) +{ + const char *mountpath = data->devname; + short force_download = data->force_download; + char *newmax = NULL; + int first, start, end = -2; + uint32_t deviceidnr; + char *deviceid = NULL; + const char *result = NULL; + char *endptr; + bool success, once = true; + int match_dive_and_log = 0; + int uemis_mem_status = UEMIS_MEM_OK; + +#if UEMIS_DEBUG + home = getenv("HOME"); + user = getenv("LOGNAME"); +#endif + uemis_info(translate("gettextFromC", "Initialise communication")); + if (!uemis_init(mountpath)) { + free(reqtxt_path); + return translate("gettextFromC", "Uemis init failed"); + } + + if (!uemis_get_answer(mountpath, "getDeviceId", 0, 1, &result)) + goto bail; + deviceid = strdup(param_buff[0]); + deviceidnr = atoi(deviceid); + + /* param_buff[0] is still valid */ + if (!uemis_get_answer(mountpath, "initSession", 1, 6, &result)) + goto bail; + + uemis_info(translate("gettextFromC", "Start download")); + if (!uemis_get_answer(mountpath, "processSync", 0, 2, &result)) + goto bail; + + /* before starting the long download, check if user pressed cancel */ + if (import_thread_cancelled) + goto bail; + + param_buff[1] = "notempty"; + newmax = uemis_get_divenr(deviceid, force_download); + if (verbose) + fprintf(stderr, "Uemis downloader: start looking at dive nr %s", newmax); + + first = start = atoi(newmax); + dive_to_read = first; + for (;;) { +#if UEMIS_DEBUG & 2 + debug_round++; +#endif +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i inner loop start %d end %d newmax %s\n", start, end, newmax); +#endif + /* start at the last filled download table index */ + match_dive_and_log = data->download_table->nr; + sprintf(newmax, "%d", start); + param_buff[2] = newmax; + param_buff[3] = 0; + success = uemis_get_answer(mountpath, "getDivelogs", 3, 0, &result); + uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_DETAILS); + /* first, remove any leading garbage... this needs to start with a '{' */ + char *realmbuf = mbuf; + if (mbuf) + realmbuf = strchr(mbuf, '{'); + if (success && realmbuf && uemis_mem_status != UEMIS_MEM_FULL) { +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(realmbuf, "Divelogs"); +#endif + /* process the buffer we have assembled */ + if (!process_raw_buffer(data, deviceidnr, realmbuf, &newmax, NULL)) { + /* if no dives were downloaded, mark end appropriately */ + if (end == -2) + end = start - 1; + success = false; + } + if (once) { + char *t = first_object_id_val(realmbuf); + if (t && atoi(t) > start) + start = atoi(t); + free(t); + once = false; + } + /* clean up mbuf */ + endptr = strstr(realmbuf, "{{{"); + if (endptr) + *(endptr + 2) = '\0'; + /* last object_id we parsed */ + sscanf(newmax, "%d", &end); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i after download and parse start %d end %d newmax %s progress %4.2f\n", start, end, newmax, progress_bar_fraction); +#endif + /* The way this works is that I am reading the current dive from what has been loaded during the getDiveLogs call to the UEMIS. + * As the object_id of the divelog entry and the object_id of the dive details are not necessarily the same, the match needs + * to happen based on the logfilenr. + * What the following part does is to optimize the mapping by using + * dive_to_read = the dive details entry that need to be read using the object_id + * logFileNoToFind = map the logfilenr of the dive details with the object_id = diveid from the get dive logs */ + for (int i = match_dive_and_log; i < data->download_table->nr; i++) { + bool success = get_matching_dive(i, newmax, &uemis_mem_status, data, mountpath, deviceidnr); + if (!success) + break; + if (import_thread_cancelled) + break; + } + + start = end; + + /* Do some memory checking here */ + uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_LOG); + if (uemis_mem_status != UEMIS_MEM_OK) { +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i out of memory, bailing\n"); +#endif + break; + } + /* if the user clicked cancel, exit gracefully */ + if (import_thread_cancelled) { +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i thread canceled, bailing\n"); +#endif + break; + } + /* if we got an error or got nothing back, stop trying */ + if (!success || !param_buff[3]) { +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i after download nothing found, giving up\n"); +#endif + break; + } +#if UEMIS_DEBUG & 2 + if (debug_round != -1) + if (debug_round-- == 0) { + fprintf(debugfile, "d_u_i debug_round is now 0, bailing\n"); + goto bail; + } +#endif + } else { + /* some of the loading from the UEMIS failed at the divelog level + * if the memory status = full, we can't even load the divespots and/or buddies. + * The loaded block of divelogs is useless and all new loaded divelogs need to + * be deleted from the download_table. + */ + if (uemis_mem_status == UEMIS_MEM_FULL) + do_delete_dives(data->download_table, match_dive_and_log); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i out of memory, bailing instead of processing\n"); +#endif + break; + } + } + + if (end == -2 && sscanf(newmax, "%d", &end) != 1) + end = start; + +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Done: read from object_id %d to %d\n", first, end); +#endif + + /* Regardless on where we are with the memory situation, it's time now + * to see if we have to clean some dead bodies from our download table */ + next_table_index = 0; + while (next_table_index < data->download_table->nr) { + if (!data->download_table->dives[next_table_index]->downloaded) + uemis_delete_dive(data, data->download_table->dives[next_table_index]->dc.diveid); + else + next_table_index++; + } + + if (uemis_mem_status != UEMIS_MEM_OK) + result = translate("gettextFromC", ERR_FS_ALMOST_FULL); + +bail: + (void)uemis_get_answer(mountpath, "terminateSync", 0, 3, &result); + if (!strcmp(param_buff[0], "error")) { + if (!strcmp(param_buff[2], "Out of Memory")) + result = translate("gettextFromC", ERR_FS_FULL); + else + result = param_buff[2]; + } + free(deviceid); + free(reqtxt_path); + if (!data->download_table->nr) + result = translate("gettextFromC", ERR_NO_FILES); + return result; +} diff --git a/core/uemis.c b/core/uemis.c new file mode 100644 index 000000000..5635d5630 --- /dev/null +++ b/core/uemis.c @@ -0,0 +1,392 @@ +/* + * uemis.c + * + * UEMIS SDA file importer + * AUTHOR: Dirk Hohndel - Copyright 2011 + * + * Licensed under the MIT license. + */ +#include <stdio.h> +#include <string.h> + +#include "gettext.h" + +#include "dive.h" +#include "uemis.h" +#include <libdivecomputer/parser.h> +#include <libdivecomputer/version.h> + +/* + * following code is based on code found in at base64.sourceforge.net/b64.c + * AUTHOR: Bob Trower 08/04/01 + * COPYRIGHT: Copyright (c) Trantor Standard Systems Inc., 2001 + * NOTE: This source code may be used as you wish, subject to + * the MIT license. + */ +/* + * Translation Table to decode (created by Bob Trower) + */ +static const char cd64[] = "|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ[\\]^_`abcdefghijklmnopq"; + +/* + * decodeblock -- decode 4 '6-bit' characters into 3 8-bit binary bytes + */ +static void decodeblock(unsigned char in[4], unsigned char out[3]) +{ + out[0] = (unsigned char)(in[0] << 2 | in[1] >> 4); + out[1] = (unsigned char)(in[1] << 4 | in[2] >> 2); + out[2] = (unsigned char)(((in[2] << 6) & 0xc0) | in[3]); +} + +/* + * decode a base64 encoded stream discarding padding, line breaks and noise + */ +static void decode(uint8_t *inbuf, uint8_t *outbuf, int inbuf_len) +{ + uint8_t in[4], out[3], v; + int i, len, indx_in = 0, indx_out = 0; + + while (indx_in < inbuf_len) { + for (len = 0, i = 0; i < 4 && (indx_in < inbuf_len); i++) { + v = 0; + while ((indx_in < inbuf_len) && v == 0) { + v = inbuf[indx_in++]; + v = ((v < 43 || v > 122) ? 0 : cd64[v - 43]); + if (v) + v = ((v == '$') ? 0 : v - 61); + } + if (indx_in < inbuf_len) { + len++; + if (v) + in[i] = (v - 1); + } else + in[i] = 0; + } + if (len) { + decodeblock(in, out); + for (i = 0; i < len - 1; i++) + outbuf[indx_out++] = out[i]; + } + } +} +/* end code from Bob Trower */ + +/* + * convert the base64 data blog + */ +static int uemis_convert_base64(char *base64, uint8_t **data) +{ + int len, datalen; + + len = strlen(base64); + datalen = (len / 4 + 1) * 3; + if (datalen < 0x123 + 0x25) + /* less than header + 1 sample??? */ + fprintf(stderr, "suspiciously short data block %d\n", datalen); + + *data = malloc(datalen); + if (!*data) { + fprintf(stderr, "Out of memory\n"); + return 0; + } + decode((unsigned char *)base64, *data, len); + + if (memcmp(*data, "Dive\01\00\00", 7)) + fprintf(stderr, "Missing Dive100 header\n"); + + return datalen; +} + +struct uemis_helper { + uint32_t diveid; + int lbs; + int divespot; + int dive_site_uuid; + struct uemis_helper *next; +}; +static struct uemis_helper *uemis_helper = NULL; + +static struct uemis_helper *uemis_get_helper(uint32_t diveid) +{ + struct uemis_helper **php = &uemis_helper; + struct uemis_helper *hp = *php; + + while (hp) { + if (hp->diveid == diveid) + return hp; + if (hp->next) { + hp = hp->next; + continue; + } + php = &hp->next; + break; + } + hp = *php = calloc(1, sizeof(struct uemis_helper)); + hp->diveid = diveid; + hp->next = NULL; + return hp; +} + +static void uemis_weight_unit(int diveid, int lbs) +{ + struct uemis_helper *hp = uemis_get_helper(diveid); + if (hp) + hp->lbs = lbs; +} + +int uemis_get_weight_unit(uint32_t diveid) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->diveid == diveid) + return hp->lbs; + hp = hp->next; + } + /* odd - we should have found this; default to kg */ + return 0; +} + +void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid) +{ + struct uemis_helper *hp = uemis_get_helper(diveid); + hp->divespot = divespot; + hp->dive_site_uuid = dive_site_uuid; +} + +/* support finding a dive spot based on the diveid */ +int uemis_get_divespot_id_by_diveid(uint32_t diveid) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->diveid == diveid) + return hp->divespot; + hp = hp->next; + } + return -1; +} + +void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->divespot == divespot) { + struct dive_site *ds = get_dive_site_by_uuid(hp->dive_site_uuid); + if (ds) { + ds->name = strdup(text); + ds->longitude.udeg = round(longitude * 1000000); + ds->latitude.udeg = round(latitude * 1000000); + } + } + hp = hp->next; + } +} + +/* Create events from the flag bits and other data in the sample; + * These bits basically represent what is displayed on screen at sample time. + * Many of these 'warnings' are way hyper-active and seriously clutter the + * profile plot - so these are disabled by default + * + * we mark all the strings for translation, but we store the untranslated + * strings and only convert them when displaying them on screen - this way + * when we write them to the XML file we'll always have the English strings, + * regardless of locale + */ +static void uemis_event(struct dive *dive, struct divecomputer *dc, struct sample *sample, uemis_sample_t *u_sample) +{ + uint8_t *flags = u_sample->flags; + int stopdepth; + static int lastndl; + + if (flags[1] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Safety stop violation")); + if (flags[1] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed alarm")); +#if WANT_CRAZY_WARNINGS + if (flags[1] & 0x06) /* both bits 1 and 2 are a warning */ + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed warning")); + if (flags[1] & 0x10) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ green warning")); +#endif + if (flags[1] & 0x20) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend warning")); + if (flags[1] & 0x40) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend alarm")); + /* flags[2] reflects the deco / time bar + * flags[3] reflects more display details on deco and pO2 */ + if (flags[4] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank pressure info")); + if (flags[4] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT warning")); + if (flags[4] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT alert")); + if (flags[4] & 0x40) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank change suggested")); + if (flags[4] & 0x80) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Depth limit exceeded")); + if (flags[5] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Max deco time warning")); + if (flags[5] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time info")); + if (flags[5] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time alert")); + if (flags[5] & 0x10) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Marker")); + if (flags[6] & 0x02) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "No tank data")); + if (flags[6] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery warning")); + if (flags[6] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery alert")); +/* flags[7] reflects the little on screen icons that remind of previous + * warnings / alerts - not useful for events */ + +#if UEMIS_DEBUG & 32 + int i, j; + for (i = 0; i < 8; i++) { + printf(" %d: ", 29 + i); + for (j = 7; j >= 0; j--) + printf("%c", flags[i] & 1 << j ? '1' : '0'); + } + printf("\n"); +#endif + /* now add deco / NDL + * we don't use events but store this in the sample - that makes much more sense + * for the way we display this information + * What we know about the encoding so far: + * flags[3].bit0 | flags[5].bit1 != 0 ==> in deco + * flags[0].bit7 == 1 ==> Safety Stop + * otherwise NDL */ + stopdepth = rel_mbar_to_depth(u_sample->hold_depth, dive); + if ((flags[3] & 1) | (flags[5] & 2)) { + /* deco */ + sample->in_deco = true; + sample->stopdepth.mm = stopdepth; + sample->stoptime.seconds = u_sample->hold_time * 60; + sample->ndl.seconds = 0; + } else if (flags[0] & 128) { + /* safety stop - distinguished from deco stop by having + * both ndl and stop information */ + sample->in_deco = false; + sample->stopdepth.mm = stopdepth; + sample->stoptime.seconds = u_sample->hold_time * 60; + sample->ndl.seconds = lastndl; + } else { + /* NDL */ + sample->in_deco = false; + lastndl = sample->ndl.seconds = u_sample->hold_time * 60; + sample->stopdepth.mm = 0; + sample->stoptime.seconds = 0; + } +#if UEMIS_DEBUG & 32 + printf("%dm:%ds: p_amb_tol:%d surface:%d holdtime:%d holddepth:%d/%d ---> stopdepth:%d stoptime:%d ndl:%d\n", + sample->time.seconds / 60, sample->time.seconds % 60, u_sample->p_amb_tol, dive->dc.surface_pressure.mbar, + u_sample->hold_time, u_sample->hold_depth, stopdepth, sample->stopdepth.mm, sample->stoptime.seconds, sample->ndl.seconds); +#endif +} + +/* + * parse uemis base64 data blob into struct dive + */ +void uemis_parse_divelog_binary(char *base64, void *datap) +{ + int datalen; + int i; + uint8_t *data; + struct sample *sample = NULL; + uemis_sample_t *u_sample; + struct dive *dive = datap; + struct divecomputer *dc = &dive->dc; + int template, gasoffset; + uint8_t active = 0; + char version[5]; + + datalen = uemis_convert_base64(base64, &data); + dive->dc.airtemp.mkelvin = C_to_mkelvin((*(uint16_t *)(data + 45)) / 10.0); + dive->dc.surface_pressure.mbar = *(uint16_t *)(data + 43); + if (*(uint8_t *)(data + 19)) + dive->dc.salinity = SEAWATER_SALINITY; /* avg grams per 10l sea water */ + else + dive->dc.salinity = FRESHWATER_SALINITY; /* grams per 10l fresh water */ + + /* this will allow us to find the last dive read so far from this computer */ + dc->model = strdup("Uemis Zurich"); + dc->deviceid = *(uint32_t *)(data + 9); + dc->diveid = *(uint16_t *)(data + 7); + /* remember the weight units used in this dive - we may need this later when + * parsing the weight */ + uemis_weight_unit(dc->diveid, *(uint8_t *)(data + 24)); + /* dive template in use: + 0 = air + 1 = nitrox (B) + 2 = nitrox (B+D) + 3 = nitrox (B+T+D) + uemis cylinder data is insane - it stores seven tank settings in a block + and the template tells us which of the four groups of tanks we need to look at + */ + gasoffset = template = *(uint8_t *)(data + 115); + if (template == 3) + gasoffset = 4; + if (template == 0) + template = 1; + for (i = 0; i < template; i++) { + float volume = *(float *)(data + 116 + 25 * (gasoffset + i)) * 1000.0; + /* uemis always assumes a working pressure of 202.6bar (!?!?) - I first thought + * it was 3000psi, but testing against all my dives gets me that strange number. + * Still, that's of course completely bogus and shows they don't get how + * cylinders are named in non-metric parts of the world... + * we store the incorrect working pressure to get the SAC calculations "close" + * but the user will have to correct this manually + */ + dive->cylinder[i].type.size.mliter = rint(volume); + dive->cylinder[i].type.workingpressure.mbar = 202600; + dive->cylinder[i].gasmix.o2.permille = *(uint8_t *)(data + 120 + 25 * (gasoffset + i)) * 10; + dive->cylinder[i].gasmix.he.permille = 0; + } + /* first byte of divelog data is at offset 0x123 */ + i = 0x123; + u_sample = (uemis_sample_t *)(data + i); + while ((i <= datalen) && (data[i] != 0 || data[i + 1] != 0)) { + if (u_sample->active_tank != active) { + if (u_sample->active_tank >= MAX_CYLINDERS) { + fprintf(stderr, "got invalid sensor #%d was #%d\n", u_sample->active_tank, active); + } else { + active = u_sample->active_tank; + add_gas_switch_event(dive, dc, u_sample->dive_time, active); + } + } + sample = prepare_sample(dc); + sample->time.seconds = u_sample->dive_time; + sample->depth.mm = rel_mbar_to_depth(u_sample->water_pressure, dive); + sample->temperature.mkelvin = C_to_mkelvin(u_sample->dive_temperature / 10.0); + sample->sensor = active; + sample->cylinderpressure.mbar = + (u_sample->tank_pressure_high * 256 + u_sample->tank_pressure_low) * 10; + sample->cns = u_sample->cns; + uemis_event(dive, dc, sample, u_sample); + finish_sample(dc); + i += 0x25; + u_sample++; + } + if (sample) + dive->dc.duration.seconds = sample->time.seconds - 1; + + /* get data from the footer */ + char buffer[24]; + + snprintf(version, sizeof(version), "%1u.%02u", data[18], data[17]); + add_extra_data(dc, "FW Version", version); + snprintf(buffer, sizeof(buffer), "%08x", *(uint32_t *)(data + 9)); + add_extra_data(dc, "Serial", buffer); + snprintf(buffer, sizeof(buffer), "%d", *(uint16_t *)(data + i + 35)); + add_extra_data(dc, "main battery after dive", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 24), 60)); + add_extra_data(dc, "no fly time", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 26), 60)); + add_extra_data(dc, "no dive time", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 28), 60)); + add_extra_data(dc, "desat time", buffer); + snprintf(buffer, sizeof(buffer), "%u", *(uint16_t *)(data + i + 30)); + add_extra_data(dc, "allowed altitude", buffer); + + return; +} diff --git a/core/uemis.h b/core/uemis.h new file mode 100644 index 000000000..1758b4b32 --- /dev/null +++ b/core/uemis.h @@ -0,0 +1,54 @@ +/* + * defines and prototypes for the uemis Zurich SDA file parser + */ + +#ifndef UEMIS_H +#define UEMIS_H + +#include <stdint.h> +#include "dive.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void uemis_parse_divelog_binary(char *base64, void *divep); +int uemis_get_weight_unit(uint32_t diveid); +void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid); +void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude); +int uemis_get_divespot_id_by_diveid(uint32_t diveid); + +typedef struct +{ + uint16_t dive_time; + uint16_t water_pressure; // (in cbar) + uint16_t dive_temperature; // (in dC) + uint8_t ascent_speed; // (units unclear) + uint8_t work_fact; + uint8_t cold_fact; + uint8_t bubble_fact; + uint16_t ascent_time; + uint16_t ascent_time_opt; + uint16_t p_amb_tol; + uint16_t satt; + uint16_t hold_depth; + uint16_t hold_time; + uint8_t active_tank; + // bloody glib, when compiled for Windows, forces the whole program to use + // the Windows packing rules. So to avoid problems on Windows (and since + // only tank_pressure is currently used and that exactly once) I give in and + // make this silly low byte / high byte 8bit entries + uint8_t tank_pressure_low; // (in cbar) + uint8_t tank_pressure_high; + uint8_t consumption_low; // (units unclear) + uint8_t consumption_high; + uint8_t rgt; // (remaining gas time in minutes) + uint8_t cns; + uint8_t flags[8]; +} __attribute((packed)) uemis_sample_t; + +#ifdef __cplusplus +} +#endif + +#endif // UEMIS_H diff --git a/core/units.h b/core/units.h new file mode 100644 index 000000000..029bb64fa --- /dev/null +++ b/core/units.h @@ -0,0 +1,280 @@ +#ifndef UNITS_H +#define UNITS_H + +#include <math.h> +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define O2_IN_AIR 209 // permille +#define N2_IN_AIR 781 +#define O2_DENSITY 1429 // mg/Liter +#define N2_DENSITY 1251 +#define HE_DENSITY 179 +#define SURFACE_PRESSURE 1013 // mbar +#define SURFACE_PRESSURE_STRING "1013" +#define ZERO_C_IN_MKELVIN 273150 // mKelvin + +#ifdef __cplusplus +#define M_OR_FT(_m, _f) ((prefs.units.length == units::METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) +#else +#define M_OR_FT(_m, _f) ((prefs.units.length == METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) +#endif + +/* Salinity is expressed in weight in grams per 10l */ +#define SEAWATER_SALINITY 10300 +#define FRESHWATER_SALINITY 10000 + +#include <stdint.h> +/* + * Some silly typedefs to make our units very explicit. + * + * Also, the units are chosen so that values can be expressible as + * integers, so that we never have FP rounding issues. And they + * are small enough that converting to/from imperial units doesn't + * really matter. + * + * We also strive to make '0' a meaningless number saying "not + * initialized", since many values are things that may not have + * been reported (eg cylinder pressure or temperature from dive + * computers that don't support them). But sometimes -1 is an even + * more explicit way of saying "not there". + * + * Thus "millibar" for pressure, for example, or "millikelvin" for + * temperatures. Doing temperatures in celsius or fahrenheit would + * make for loss of precision when converting from one to the other, + * and using millikelvin is SI-like but also means that a temperature + * of '0' is clearly just a missing temperature or cylinder pressure. + * + * Also strive to use units that can not possibly be mistaken for a + * valid value in a "normal" system without conversion. If the max + * depth of a dive is '20000', you probably didn't convert from mm on + * output, or if the max depth gets reported as "0.2ft" it was either + * a really boring dive, or there was some missing input conversion, + * and a 60-ft dive got recorded as 60mm. + * + * Doing these as "structs containing value" means that we always + * have to explicitly write out those units in order to get at the + * actual value. So there is hopefully little fear of using a value + * in millikelvin as Fahrenheit by mistake. + * + * We don't actually use these all yet, so maybe they'll change, but + * I made a number of types as guidelines. + */ +typedef int64_t timestamp_t; + +typedef struct +{ + uint32_t seconds; // durations up to 68 yrs +} duration_t; + +typedef struct +{ + int32_t seconds; // offsets up to +/- 34 yrs +} offset_t; + +typedef struct +{ + int32_t mm; +} depth_t; // depth to 2000 km + +typedef struct +{ + int32_t mbar; // pressure up to 2000 bar +} pressure_t; + +typedef struct +{ + uint16_t mbar; +} o2pressure_t; // pressure up to 65 bar + +typedef struct +{ + int16_t degrees; +} bearing_t; // compass bearing + +typedef struct +{ + uint32_t mkelvin; // up to 1750 degrees K (temperatures in K are always positive) +} temperature_t; + +typedef struct +{ + int mliter; +} volume_t; + +typedef struct +{ + int permille; +} fraction_t; + +typedef struct +{ + int grams; +} weight_t; + +typedef struct +{ + int udeg; +} degrees_t; + +static inline double udeg_to_radians(int udeg) +{ + return (udeg * M_PI) / (1000000.0 * 180.0); +} + +static inline double grams_to_lbs(int grams) +{ + return grams / 453.6; +} + +static inline int lbs_to_grams(double lbs) +{ + return rint(lbs * 453.6); +} + +static inline double ml_to_cuft(int ml) +{ + return ml / 28316.8466; +} + +static inline double cuft_to_l(double cuft) +{ + return cuft * 28.3168466; +} + +static inline double mm_to_feet(int mm) +{ + return mm * 0.00328084; +} + +static inline double m_to_mile(int m) +{ + return m / 1609.344; +} + +static inline unsigned long feet_to_mm(double feet) +{ + return rint(feet * 304.8); +} + +static inline int to_feet(depth_t depth) +{ + return rint(mm_to_feet(depth.mm)); +} + +static inline double mkelvin_to_C(int mkelvin) +{ + return (mkelvin - ZERO_C_IN_MKELVIN) / 1000.0; +} + +static inline double mkelvin_to_F(int mkelvin) +{ + return mkelvin * 9 / 5000.0 - 459.670; +} + +static inline unsigned long F_to_mkelvin(double f) +{ + return rint((f - 32) * 1000 / 1.8 + ZERO_C_IN_MKELVIN); +} + +static inline unsigned long C_to_mkelvin(double c) +{ + return rint(c * 1000 + ZERO_C_IN_MKELVIN); +} + +static inline double psi_to_bar(double psi) +{ + return psi / 14.5037738; +} + +static inline long psi_to_mbar(double psi) +{ + return rint(psi_to_bar(psi) * 1000); +} + +static inline int to_PSI(pressure_t pressure) +{ + return rint(pressure.mbar * 0.0145037738); +} + +static inline double bar_to_atm(double bar) +{ + return bar / SURFACE_PRESSURE * 1000; +} + +static inline double mbar_to_atm(int mbar) +{ + return (double)mbar / SURFACE_PRESSURE; +} + +static inline int mbar_to_PSI(int mbar) +{ + pressure_t p = { mbar }; + return to_PSI(p); +} + +/* + * We keep our internal data in well-specified units, but + * the input and output may come in some random format. This + * keeps track of those units. + */ +/* turns out in Win32 PASCAL is defined as a calling convention */ +#ifdef WIN32 +#undef PASCAL +#endif +struct units { + enum LENGHT { + METERS, + FEET + } length; + enum VOLUME { + LITER, + CUFT + } volume; + enum PRESSURE { + BAR, + PSI, + PASCAL + } pressure; + enum TEMPERATURE { + CELSIUS, + FAHRENHEIT, + KELVIN + } temperature; + enum WEIGHT { + KG, + LBS + } weight; + enum TIME { + SECONDS, + MINUTES + } vertical_speed_time; +}; + +/* + * We're going to default to SI units for input. Yes, + * technically the SI unit for pressure is Pascal, but + * we default to bar (10^5 pascal), which people + * actually use. Similarly, C instead of Kelvin. + * And kg instead of g. + */ +#define SI_UNITS \ + { \ + .length = METERS, .volume = LITER, .pressure = BAR, .temperature = CELSIUS, .weight = KG, .vertical_speed_time = MINUTES \ + } + +#define IMPERIAL_UNITS \ + { \ + .length = FEET, .volume = CUFT, .pressure = PSI, .temperature = FAHRENHEIT, .weight = LBS, .vertical_speed_time = MINUTES \ + } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/version.c b/core/version.c new file mode 100644 index 000000000..764e4d2db --- /dev/null +++ b/core/version.c @@ -0,0 +1,18 @@ +#include "ssrf-version.h" + +const char *subsurface_git_version(void) +{ + return GIT_VERSION_STRING; +} + +const char *subsurface_canonical_version(void) +{ + return CANONICAL_VERSION_STRING; +} + +#ifdef SUBSURFACE_MOBILE +const char *subsurface_mobile_version(void) +{ + return MOBILE_VERSION_STRING; +} +#endif diff --git a/core/version.h b/core/version.h new file mode 100644 index 000000000..0a3204bd9 --- /dev/null +++ b/core/version.h @@ -0,0 +1,19 @@ +#ifndef VERSION_H +#define VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +const char *subsurface_git_version(void); +const char *subsurface_canonical_version(void); + +#ifdef SUBSURFACE_MOBILE +const char *subsurface_mobile_version(void); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/core/webservice.h b/core/webservice.h new file mode 100644 index 000000000..052b8aae7 --- /dev/null +++ b/core/webservice.h @@ -0,0 +1,24 @@ +#ifndef WEBSERVICE_H +#define WEBSERVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +//extern void webservice_download_dialog(void); +//extern bool webservice_request_user_xml(const gchar *, gchar **, unsigned int *, unsigned int *); +extern int divelogde_upload(char *fn, char **error); +extern unsigned int download_dialog_parse_response(char *xmldata, unsigned int len); + +enum { + DD_STATUS_OK, + DD_STATUS_ERROR_CONNECT, + DD_STATUS_ERROR_ID, + DD_STATUS_ERROR_PARSE, +}; + + +#ifdef __cplusplus +} +#endif +#endif // WEBSERVICE_H diff --git a/core/windows.c b/core/windows.c new file mode 100644 index 000000000..58d3beaad --- /dev/null +++ b/core/windows.c @@ -0,0 +1,454 @@ +/* windows.c */ +/* implements Windows specific functions */ +#include <io.h> +#include "dive.h" +#include "display.h" +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x500 +#include <windows.h> +#include <shlobj.h> +#include <stdio.h> +#include <fcntl.h> +#include <assert.h> +#include <dirent.h> +#include <zip.h> +#include <lmcons.h> + +const char non_standard_system_divelist_default_font[] = "Calibri"; +const char current_system_divelist_default_font[] = "Segoe UI"; +const char *system_divelist_default_font = non_standard_system_divelist_default_font; +double system_divelist_default_font_size = -1; + +void subsurface_user_info(struct user_info *user) +{ /* Encourage use of at least libgit2-0.20 */ } + +extern bool isWin7Or8(); + +void subsurface_OS_pref_setup(void) +{ + if (isWin7Or8()) + system_divelist_default_font = current_system_divelist_default_font; +} + +bool subsurface_ignore_font(const char *font) +{ + // if this is running on a recent enough version of Windows and the font + // passed in is the pre 4.3 default font, ignore it + if (isWin7Or8() && strcmp(font, non_standard_system_divelist_default_font) == 0) + return true; + return false; +} + +/* this function returns the Win32 Roaming path for the current user as UTF-8. + * it never returns NULL but fallsback to .\ instead! + * the append argument will append a wchar_t string to the end of the path. + */ +static const char *system_default_path_append(const wchar_t *append) +{ + wchar_t wpath[MAX_PATH] = { 0 }; + const char *fname = "system_default_path_append()"; + + /* obtain the user path via SHGetFolderPathW. + * this API is deprecated but still supported on modern Win32. + * fallback to .\ if it fails. + */ + if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, wpath))) { + fprintf(stderr, "%s: cannot obtain path!\n", fname); + wpath[0] = L'.'; + wpath[1] = L'\0'; + } + + wcscat(wpath, L"\\Subsurface"); + if (append) { + wcscat(wpath, L"\\"); + wcscat(wpath, append); + } + + /* attempt to convert the UTF-16 string to UTF-8. + * resize the buffer and fallback to .\Subsurface if it fails. + */ + const int wsz = wcslen(wpath); + const int sz = WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, NULL, 0, NULL, NULL); + char *path = (char *)malloc(sz + 1); + if (!sz) + goto fallback; + if (WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, path, sz, NULL, NULL)) { + path[sz] = '\0'; + return path; + } + +fallback: + fprintf(stderr, "%s: cannot obtain path as UTF-8!\n", fname); + const char *local = ".\\Subsurface"; + const int len = strlen(local) + 1; + path = (char *)realloc(path, len); + memset(path, 0, len); + strcat(path, local); + return path; +} + +/* by passing NULL to system_default_path_append() we obtain the pure path. + * '\' not included at the end. + */ +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +/* obtain the Roaming path and append "\\<USERNAME>.xml" to it. + */ +const char *system_default_filename(void) +{ + static wchar_t filename[UNLEN + 5] = { 0 }; + if (!*filename) { + wchar_t username[UNLEN + 1] = { 0 }; + DWORD username_len = UNLEN + 1; + GetUserNameW(username, &username_len); + wcscat(filename, username); + wcscat(filename, L".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1; + DWORD i; + if (dc_type != DC_TYPE_UEMIS) { + // Open the registry key. + HKEY hKey; + LONG rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", 0, KEY_QUERY_VALUE, &hKey); + if (rc != ERROR_SUCCESS) { + return -1; + } + + // Get the number of values. + DWORD count = 0; + rc = RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &count, NULL, NULL, NULL, NULL); + if (rc != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + for (i = 0; i < count; ++i) { + // Get the value name, data and type. + char name[512], data[512]; + DWORD name_len = sizeof(name); + DWORD data_len = sizeof(data); + DWORD type = 0; + rc = RegEnumValue(hKey, i, name, &name_len, NULL, &type, (LPBYTE)data, &data_len); + if (rc != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + + // Ignore non-string values. + if (type != REG_SZ) + continue; + + // Prevent a possible buffer overflow. + if (data_len >= sizeof(data)) { + RegCloseKey(hKey); + return -1; + } + + // Null terminate the string. + data[data_len] = 0; + + callback(data, userdata); + index++; + if (is_default_dive_computer_device(name)) + index = i; + } + + RegCloseKey(hKey); + } + if (dc_type != DC_TYPE_SERIAL) { + int i; + int count_drives = 0; + const int bufdef = 512; + const char *dlabels[] = {"UEMISSDA", NULL}; + char bufname[bufdef], bufval[bufdef], *p; + DWORD bufname_len; + + /* add drive letters that match labels */ + memset(bufname, 0, bufdef); + bufname_len = bufdef; + if (GetLogicalDriveStringsA(bufname_len, bufname)) { + p = bufname; + + while (*p) { + memset(bufval, 0, bufdef); + if (GetVolumeInformationA(p, bufval, bufdef, NULL, NULL, NULL, NULL, 0)) { + for (i = 0; dlabels[i] != NULL; i++) + if (!strcmp(bufval, dlabels[i])) { + char data[512]; + snprintf(data, sizeof(data), "%s (%s)", p, dlabels[i]); + callback(data, userdata); + if (is_default_dive_computer_device(p)) + index = count_drives; + count_drives++; + } + } + p = &p[strlen(p) + 1]; + } + if (count_drives == 1) /* we found exactly one Uemis "drive" */ + index = 0; /* make it the selected "device" */ + } + } + return index; +} + +/* this function converts a utf-8 string to win32's utf-16 2 byte string. + * the caller function should manage the allocated memory. + */ +static wchar_t *utf8_to_utf16_fl(const char *utf8, char *file, int line) +{ + assert(utf8 != NULL); + assert(file != NULL); + assert(line); + /* estimate buffer size */ + const int sz = strlen(utf8) + 1; + wchar_t *utf16 = (wchar_t *)malloc(sizeof(wchar_t) * sz); + if (!utf16) { + fprintf(stderr, "%s:%d: %s %d.", file, line, "cannot allocate buffer of size", sz); + return NULL; + } + if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16, sz)) + return utf16; + fprintf(stderr, "%s:%d: %s", file, line, "cannot convert string."); + free((void *)utf16); + return NULL; +} + +#define utf8_to_utf16(s) utf8_to_utf16_fl(s, __FILE__, __LINE__) + +/* bellow we provide a set of wrappers for some I/O functions to use wchar_t. + * on win32 this solves the issue that we need paths to be utf-16 encoded. + */ +int subsurface_rename(const char *path, const char *newpath) +{ + int ret = -1; + if (!path || !newpath) + return ret; + + wchar_t *wpath = utf8_to_utf16(path); + wchar_t *wnewpath = utf8_to_utf16(newpath); + + if (wpath && wnewpath) + ret = _wrename(wpath, wnewpath); + free((void *)wpath); + free((void *)wnewpath); + return ret; +} + +// if the QDir based rename fails, we try this one +int subsurface_dir_rename(const char *path, const char *newpath) +{ + // check if the folder exists + BOOL exists = FALSE; + DWORD attrib = GetFileAttributes(path); + if (attrib != INVALID_FILE_ATTRIBUTES && attrib & FILE_ATTRIBUTE_DIRECTORY) + exists = TRUE; + if (!exists && verbose) { + fprintf(stderr, "folder not found or path is not a folder: %s\n", path); + return EXIT_FAILURE; + } + + // list of error codes: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx + DWORD errorCode; + + // if this fails something has already obatained (more) exclusive access to the folder + HANDLE h = CreateFile(path, GENERIC_WRITE, FILE_SHARE_WRITE | + FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + if (h == INVALID_HANDLE_VALUE) { + errorCode = GetLastError(); + if (verbose) + fprintf(stderr, "cannot obtain exclusive write access for folder: %u\n", (unsigned int)errorCode ); + return EXIT_FAILURE; + } else { + if (verbose) + fprintf(stderr, "exclusive write access obtained...closing handle!"); + CloseHandle(h); + + // attempt to rename + BOOL result = MoveFile(path, newpath); + if (!result) { + errorCode = GetLastError(); + if (verbose) + fprintf(stderr, "rename failed: %u\n", (unsigned int)errorCode); + return EXIT_FAILURE; + } + if (verbose > 1) + fprintf(stderr, "folder rename success: %s ---> %s\n", path, newpath); + } + return EXIT_SUCCESS; +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + int ret = -1; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _wopen(wpath, oflags, mode); + free((void *)wpath); + return ret; +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + FILE *ret = NULL; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) { + const int len = strlen(mode); + wchar_t wmode[len + 1]; + for (int i = 0; i < len; i++) + wmode[i] = (wchar_t)mode[i]; + wmode[len] = 0; + ret = _wfopen(wpath, wmode); + } + free((void *)wpath); + return ret; +} + +/* here we return a void pointer instead of _WDIR or DIR pointer */ +void *subsurface_opendir(const char *path) +{ + _WDIR *ret = NULL; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _wopendir(wpath); + free((void *)wpath); + return (void *)ret; +} + +int subsurface_access(const char *path, int mode) +{ + int ret = -1; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _waccess(wpath, mode); + free((void *)wpath); + return ret; +} + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ +#if defined(LIBZIP_VERSION_MAJOR) + /* libzip 0.10 has zip_fdopen, let's use it since zip_open doesn't have a + * wchar_t version */ + int fd = subsurface_open(path, O_RDONLY | O_BINARY, 0); + struct zip *ret = zip_fdopen(fd, flags, errorp); + if (!ret) + close(fd); + return ret; +#else + return zip_open(path, flags, errorp); +#endif +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +static struct { + bool allocated; + UINT cp; + FILE *out, *err; +} console_desc; + +void subsurface_console_init(bool dedicated) +{ + (void)console_desc; + /* if this is a console app already, do nothing */ +#ifndef WIN32_CONSOLE_APP + /* just in case of multiple calls */ + memset((void *)&console_desc, 0, sizeof(console_desc)); + /* the AttachConsole(..) call can be used to determine if the parent process + * is a terminal. if it succeeds, there is no need for a dedicated console + * window and we don't need to call the AllocConsole() function. on the other + * hand if the user has set the 'dedicated' flag to 'true' and if AttachConsole() + * has failed, we create a dedicated console window. + */ + console_desc.allocated = AttachConsole(ATTACH_PARENT_PROCESS); + if (console_desc.allocated) + dedicated = false; + if (!console_desc.allocated && dedicated) + console_desc.allocated = AllocConsole(); + if (!console_desc.allocated) + return; + + console_desc.cp = GetConsoleCP(); + SetConsoleOutputCP(CP_UTF8); /* make the ouput utf8 */ + + /* set some console modes; we don't need to reset these back. + * ENABLE_EXTENDED_FLAGS = 0x0080, ENABLE_QUICK_EDIT_MODE = 0x0040 */ + HANDLE h_in = GetStdHandle(STD_INPUT_HANDLE); + if (h_in) { + SetConsoleMode(h_in, 0x0080 | 0x0040); + CloseHandle(h_in); + } + + /* dedicated only; disable the 'x' button as it will close the main process as well */ + HWND h_cw = GetConsoleWindow(); + if (h_cw && dedicated) { + SetWindowTextA(h_cw, "Subsurface Console"); + HMENU h_menu = GetSystemMenu(h_cw, 0); + if (h_menu) { + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED); + DrawMenuBar(h_cw); + } + SetConsoleCtrlHandler(NULL, TRUE); /* disable the CTRL handler */ + } + + /* redirect; on win32, CON is a reserved pipe target, like NUL */ + console_desc.out = freopen("CON", "w", stdout); + console_desc.err = freopen("CON", "w", stderr); + if (!dedicated) + puts(""); /* add an empty line */ +#endif +} + +void subsurface_console_exit(void) +{ +#ifndef WIN32_CONSOLE_APP + if (!console_desc.allocated) + return; + + /* close handles */ + if (console_desc.out) + fclose(console_desc.out); + if (console_desc.err) + fclose(console_desc.err); + + /* reset code page and free */ + SetConsoleOutputCP(console_desc.cp); + FreeConsole(); +#endif +} + +bool subsurface_user_is_root() +{ + /* FIXME: Detect admin rights */ + return (false); +} diff --git a/core/windowtitleupdate.cpp b/core/windowtitleupdate.cpp new file mode 100644 index 000000000..963455f1d --- /dev/null +++ b/core/windowtitleupdate.cpp @@ -0,0 +1,32 @@ +#include "windowtitleupdate.h" + +WindowTitleUpdate *WindowTitleUpdate::m_instance = NULL; + +WindowTitleUpdate::WindowTitleUpdate(QObject *parent) : QObject(parent) +{ + Q_ASSERT_X(m_instance == NULL, "WindowTitleUpdate", "WindowTitleUpdate recreated!"); + + m_instance = this; +} + +WindowTitleUpdate *WindowTitleUpdate::instance() +{ + return m_instance; +} + +WindowTitleUpdate::~WindowTitleUpdate() +{ + m_instance = NULL; +} + +void WindowTitleUpdate::emitSignal() +{ + emit updateTitle(); +} + +extern "C" void updateWindowTitle() +{ + WindowTitleUpdate *wt = WindowTitleUpdate::instance(); + if (wt) + wt->emitSignal(); +} diff --git a/core/windowtitleupdate.h b/core/windowtitleupdate.h new file mode 100644 index 000000000..8650e5868 --- /dev/null +++ b/core/windowtitleupdate.h @@ -0,0 +1,20 @@ +#ifndef WINDOWTITLEUPDATE_H +#define WINDOWTITLEUPDATE_H + +#include <QObject> + +class WindowTitleUpdate : public QObject +{ + Q_OBJECT +public: + explicit WindowTitleUpdate(QObject *parent = 0); + ~WindowTitleUpdate(); + static WindowTitleUpdate *instance(); + void emitSignal(); +signals: + void updateTitle(); +private: + static WindowTitleUpdate *m_instance; +}; + +#endif // WINDOWTITLEUPDATE_H diff --git a/core/worldmap-options.h b/core/worldmap-options.h new file mode 100644 index 000000000..177443563 --- /dev/null +++ b/core/worldmap-options.h @@ -0,0 +1,7 @@ +#ifndef WORLDMAP_OPTIONS_H +#define WORLDMAP_OPTIONS_H + +const char *map_options = "center: new google.maps.LatLng(0,0),\n\tzoom: 3,\n\tminZoom: 2,\n\tmapTypeId: google.maps.MapTypeId.SATELLITE\n\t"; +const char *css = "\n\thtml { height: 100% }\n\tbody { height: 100%; margin: 0; padding: 0 }\n\t#map-canvas { height: 100% }\n"; + +#endif // WORLDMAP-OPTIONS_H diff --git a/core/worldmap-save.c b/core/worldmap-save.c new file mode 100644 index 000000000..e7e8bcc30 --- /dev/null +++ b/core/worldmap-save.c @@ -0,0 +1,117 @@ +// Clang has a bug on zero-initialization of C structs. +#pragma clang diagnostic ignored "-Wmissing-field-initializers" + +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + +#include "dive.h" +#include "membuffer.h" +#include "save-html.h" +#include "worldmap-save.h" +#include "worldmap-options.h" +#include "gettext.h" + +char *getGoogleApi() +{ + /* google maps api auth*/ + return "https://maps.googleapis.com/maps/api/js?key=AIzaSyDzo9PWsqYDDSddVswg_13rpD9oH_dLuoQ"; +} + +void writeMarkers(struct membuffer *b, const bool selected_only) +{ + int i, dive_no = 0; + struct dive *dive; + char pre[1000], post[1000]; + + for_each_dive (i, dive) { + if (selected_only) { + if (!dive->selected) + continue; + } + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds || !dive_site_has_gps_location(ds)) + continue; + put_degrees(b, ds->latitude, "temp = new google.maps.Marker({position: new google.maps.LatLng(", ""); + put_degrees(b, ds->longitude, ",", ")});\n"); + put_string(b, "markers.push(temp);\ntempinfowindow = new google.maps.InfoWindow({content: '<div id=\"content\">'+'<div id=\"siteNotice\">'+'</div>'+'<div id=\"bodyContent\">"); + snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Date:")); + put_HTML_date(b, dive, pre, "</p>"); + snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Time:")); + put_HTML_time(b, dive, pre, "</p>"); + snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Duration:")); + snprintf(post, sizeof(post), " %s</p>", translate("gettextFromC", "min")); + put_duration(b, dive->duration, pre, post); + put_string(b, "<p> "); + put_HTML_quoted(b, translate("gettextFromC", "Max. depth:")); + put_HTML_depth(b, dive, " ", "</p>"); + put_string(b, "<p> "); + put_HTML_quoted(b, translate("gettextFromC", "Air temp.:")); + put_HTML_airtemp(b, dive, " ", "</p>"); + put_string(b, "<p> "); + put_HTML_quoted(b, translate("gettextFromC", "Water temp.:")); + put_HTML_watertemp(b, dive, " ", "</p>"); + snprintf(pre, sizeof(pre), "<p>%s <b>", translate("gettextFromC", "Location:")); + put_string(b, pre); + put_HTML_quoted(b, get_dive_location(dive)); + put_string(b, "</b></p>"); + snprintf(pre, sizeof(pre), "<p> %s ", translate("gettextFromC", "Notes:")); + put_HTML_notes(b, dive, pre, " </p>"); + put_string(b, "</p>'+'</div>'+'</div>'});\ninfowindows.push(tempinfowindow);\n"); + put_format(b, "google.maps.event.addListener(markers[%d], 'mouseover', function() {\ninfowindows[%d].open(map,markers[%d]);}", dive_no, dive_no, dive_no); + put_format(b, ");google.maps.event.addListener(markers[%d], 'mouseout', function() {\ninfowindows[%d].close();});\n", dive_no, dive_no); + dive_no++; + } +} + +void insert_html_header(struct membuffer *b) +{ + put_string(b, "<!DOCTYPE html>\n<html>\n<head>\n"); + put_string(b, "<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" />\n<title>World Map</title>\n"); + put_string(b, "<meta charset=\"UTF-8\">"); +} + +void insert_css(struct membuffer *b) +{ + put_format(b, "<style type=\"text/css\">%s</style>\n", css); +} + +void insert_javascript(struct membuffer *b, const bool selected_only) +{ + put_string(b, "<script type=\"text/javascript\" src=\""); + put_string(b, getGoogleApi()); + put_string(b, "&sensor=false\">\n</script>\n<script type=\"text/javascript\">\nvar map;\n"); + put_format(b, "function initialize() {\nvar mapOptions = {\n\t%s,", map_options); + put_string(b, "rotateControl: false,\n\tstreetViewControl: false,\n\tmapTypeControl: false\n};\n"); + put_string(b, "map = new google.maps.Map(document.getElementById(\"map-canvas\"),mapOptions);\nvar markers = new Array();"); + put_string(b, "\nvar infowindows = new Array();\nvar temp;\nvar tempinfowindow;\n"); + writeMarkers(b, selected_only); + put_string(b, "\nfor(var i=0;i<markers.length;i++)\n\tmarkers[i].setMap(map);\n}\n"); + put_string(b, "google.maps.event.addDomListener(window, 'load', initialize);</script>\n"); +} + +void export(struct membuffer *b, const bool selected_only) +{ + insert_html_header(b); + insert_css(b); + insert_javascript(b, selected_only); + put_string(b, "\t</head>\n<body>\n<div id=\"map-canvas\"></div>\n</body>\n</html>"); +} + +void export_worldmap_HTML(const char *file_name, const bool selected_only) +{ + FILE *f; + + struct membuffer buf = { 0 }; + export(&buf, selected_only); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/core/worldmap-save.h b/core/worldmap-save.h new file mode 100644 index 000000000..102ea40e5 --- /dev/null +++ b/core/worldmap-save.h @@ -0,0 +1,15 @@ +#ifndef WORLDMAP_SAVE_H +#define WORLDMAP_SAVE_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern void export_worldmap_HTML(const char *file_name, const bool selected_only); + + +#ifdef __cplusplus +} +#endif + +#endif |