Compare commits

...

8 commits

Author SHA1 Message Date
Egbert de Pauw
cedd7f9395
Merge 9bd1631103 into 91d8bfef55 2024-11-19 08:48:37 +13:00
Michael Keller
91d8bfef55 Import: Add Parsing for Divesoft Freedom+ Format Files.
Switch the Divesoft log file importer to use the parser in
libdivecomputer. This adds support for the newer Divesoft Freedom+ log
file format.
Also refactor the OSTCTools log file importer to share common
functionality.

Signed-off-by: Michael Keller <github@ike.ch>
2024-11-19 08:47:00 +13:00
Michael Keller
7f42acfdfb Import: Add Parsing for Divesoft Freedom+ Format Files.
Switch the Divesoft log file importer to use the parser in
libdivecomputer. This adds support for the newer Divesoft Freedom+ log
file format.
Also refactor the OSTCTools log file importer to share common
functionality.

Signed-off-by: Michael Keller <github@ike.ch>
2024-11-19 08:47:00 +13:00
Robert C. Helling
7dc92e170f Round MOD to lower depths
When computing the suggested switch depth for a gas,
we should take the next stop depth above the MOD, i.e.
round down. Otherwise we can produce MOD violation warnings.

We need, however, a bit of fudge as otherwise we do not
suggest to switch to o2 at 6m.

TestPlan uses the MOD to determine the depth to switch to
Tx21/35. This happens to be 65.378m. Therefore, switching
at 66m violates the MOD, the switch should be at 63m.
This then affects the gas usage.

Signed-off-by: Robert C. Helling <helling@atdotde.de>
2024-11-19 08:46:13 +13:00
Michael Keller
a898173ce7 CICD: Update the MXE Version Used to Build the Windows Version.
This fixes the missing 'Print' functionality in Windows.

Signed-off-by: Michael Keller <github@ike.ch>
2024-11-19 08:44:55 +13:00
Egbert de Pauw
9bd1631103
Merge branch 'subsurface:master' into qt6devel 2024-08-28 15:40:29 +02:00
Egbertdepauw
7201a40c5f Removed some testing code.
There was an unneeded line in the code, just for my own testing
purposes.

Signed-off-by: Egbertdepauw <egbert@despaankamer.nl>
2024-08-27 14:51:39 +02:00
Egbertdepauw
c5e5535f51 qt6: Make subsurface buildable with Qt6 on Macos
Building on macos with the Qt6 framework.

made changes to several files to make it possible to build subsurface
desktop with the Qt 6.5.x and higher framework.

Tested versions: 6.5.2, 6.6.3, 6.7.2

code builds and googlemaps works, made some adjustments to get panning
and zooming with mouse or trackpad working.

See issue #3577 "Build Fails, macOS" for build details.

Signed-off-by: Egbertdepauw <egbert@despaankamer.nl>
2024-08-27 13:51:37 +02:00
17 changed files with 245 additions and 628 deletions

View file

@ -12,8 +12,9 @@ jobs:
windows-mxe:
runs-on: ubuntu-latest
env:
VERSION: ${{ '3.3.0' }} # 'official' images should have a dot-zero version
mxe_sha: '974808c2ecb02866764d236fe533ae57ba342e7a'
VERSION: ${{ '3.4.0' }} # 'official' images should have a dot-zero version
# Fix this here as QTWebKit (needed for printing) is broken in newer versions
mxe_sha: 'd6377b2f2334694dbb040294fd0d848327e63328'
steps:
- uses: actions/checkout@v4

View file

@ -82,6 +82,7 @@ SOURCES += subsurface-mobile-main.cpp \
core/save-git.cpp \
core/datatrak.cpp \
core/ostctools.cpp \
core/divesoft.cpp \
core/planner.cpp \
core/save-xml.cpp \
core/cochran.cpp \

View file

@ -77,6 +77,7 @@ set(SUBSURFACE_CORE_LIB_SRCS
divesitetable.h
divesitehelpers.cpp
divesitehelpers.h
divesoft.cpp
downloadfromdcthread.cpp
downloadfromdcthread.h
event.cpp

View file

@ -2457,7 +2457,9 @@ int dive::mbar_to_depth(int mbar) const
depth_t dive::gas_mod(struct gasmix mix, pressure_t po2_limit, int roundto) const
{
double depth = (double) mbar_to_depth(po2_limit.mbar * 1000 / get_o2(mix));
return depth_t { .mm = int_cast<int>(depth / roundto) * roundto };
// Rounding should be towards lower=safer depths but we give a bit
// of fudge to all to switch to o2 at 6m. So from .9 we round up.
return depth_t { .mm = (int)(depth / roundto + 0.1) * roundto };
}
/* Maximum narcotic depth rounded to multiples of roundto mm */

48
core/divesoft.cpp Normal file
View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-2.0
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "errorhelper.h"
#include "subsurface-string.h"
#include "gettext.h"
#include "dive.h"
#include "divelist.h"
#include "divelog.h"
#include "extradata.h"
#include "format.h"
#include "libdivecomputer.h"
// As supplied by Divesoft
static const char divesoft_liberty_serial_prefix[] = "7026";
static const char divesoft_freedom_serial_prefix[] = "7044";
static const char divesoft_freedom_plus_serial_prefix[] = "7273";
// From libdivecomputer
static const int divesoft_liberty_model = 10;
static const int divesoft_freedom_model = 19;
int divesoft_import(const std::unique_ptr<std::vector<unsigned char>> &buffer, struct divelog *log)
{
int model = 0;
if (strncmp((char *)(buffer->data() + 52), divesoft_liberty_serial_prefix, 4) == 0)
model = divesoft_liberty_model;
else if (strncmp((char *)(buffer->data() + 52), divesoft_freedom_serial_prefix, 4) == 0 || strncmp((char *)(buffer->data() + 52), divesoft_freedom_plus_serial_prefix, 4) == 0)
model = divesoft_freedom_model;
device_data_t devdata;
int ret = prepare_device_descriptor(model, DC_FAMILY_DIVESOFT_FREEDOM, devdata);
if (ret == 0)
return report_error("%s", translate("gettextFromC", "Unknown DC"));
auto d = std::make_unique<dive>();
d->dcs[0].model = devdata.vendor + " " + devdata.model + " (Imported from file)";
// Parse the dive data
dc_status_t rc = libdc_buffer_parser(d.get(), &devdata, buffer->data(), buffer->size());
if (rc != DC_STATUS_SUCCESS)
return report_error(translate("gettextFromC", "Error - %s - parsing dive %d"), errmsg(rc), d->number);
log->dives.record_dive(std::move(d));
return 1;
}

View file

@ -267,6 +267,43 @@ bool remote_repo_uptodate(const char *filename, struct git_info *info)
return false;
}
static std::unique_ptr<std::vector<unsigned char>> read_into_buffer(const char *file)
{
const char *failed_to_read_msg = translate("gettextFromC", "Failed to read '%s'");
struct stat file_status;
if (stat(file, &file_status) < 0) {
report_error(failed_to_read_msg, file);
return NULL;
}
FILE *archive;
if ((archive = subsurface_fopen(file, "rb")) == NULL) {
report_error(failed_to_read_msg, file);
return NULL;
}
// Read dive's raw data
auto buffer = std::make_unique<std::vector<unsigned char>>(file_status.st_size, 0);
int i = 0, c;
while ((c = getc(archive)) != EOF) {
(*buffer)[i] = c;
i++;
}
if (ferror(archive)) {
report_error(failed_to_read_msg, file);
fclose(archive);
return NULL;
}
fclose(archive);
return buffer;
}
int parse_file(const char *filename, struct divelog *log)
{
struct git_info info;
@ -304,8 +341,13 @@ int parse_file(const char *filename, struct divelog *log)
}
/* Divesoft Freedom */
if (fmt && (!strcasecmp(fmt + 1, "DLF")))
return parse_dlf_buffer((unsigned char *)mem.data(), mem.size(), log);
if (fmt && (!strcasecmp(fmt + 1, "DLF"))) {
auto buffer = read_into_buffer(filename);
if (buffer == NULL)
return -1;
return divesoft_import(buffer, log);
}
/* DataTrak/Wlog */
if (fmt && !strcasecmp(fmt + 1, "LOG")) {
@ -321,8 +363,11 @@ int parse_file(const char *filename, struct divelog *log)
/* OSTCtools */
if (fmt && (!strcasecmp(fmt + 1, "DIVE"))) {
ostctools_import(filename, log);
return 0;
auto buffer = read_into_buffer(filename);
if (buffer == NULL)
return -1;
return ostctools_import(buffer, log);
}
return parse_file_buffer(filename, mem, log);

View file

@ -8,11 +8,13 @@
#include <stdio.h>
#include <vector>
#include <utility>
#include <memory>
struct divelog;
struct zip;
extern void ostctools_import(const char *file, struct divelog *log);
extern int ostctools_import(const std::unique_ptr<std::vector<unsigned char>> &buffer, struct divelog *log);
extern int divesoft_import(const std::unique_ptr<std::vector<unsigned char>> &buffer, struct divelog *log);
extern int parse_file(const char *filename, struct divelog *log);
extern int try_to_open_zip(const char *filename, struct divelog *log);

View file

@ -1534,6 +1534,26 @@ std::string do_libdivecomputer_import(device_data_t *data)
return err;
}
/*
* Fills a device_data_t structure with known dc data and a descriptor.
*/
int prepare_device_descriptor(int data_model, dc_family_t dc_fam, device_data_t &dev_data)
{
dev_data.device = NULL;
dev_data.context = NULL;
dc_descriptor_t *data_descriptor = get_descriptor(dc_fam, data_model);
if (data_descriptor) {
dev_data.descriptor = data_descriptor;
dev_data.vendor = dc_descriptor_get_vendor(data_descriptor);
dev_data.model = dc_descriptor_get_product(data_descriptor);
} else {
return 0;
}
return 1;
}
/*
* Parse data buffers instead of dc devices downloaded data.
* Intended to be used to parse profile data from binary files during import tasks.
@ -1543,7 +1563,7 @@ std::string do_libdivecomputer_import(device_data_t *data)
* 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 libdc_buffer_parser(struct dive *dive, device_data_t *data, const unsigned char *buffer, int size)
{
dc_status_t rc;
dc_parser_t *parser = NULL;
@ -1556,6 +1576,7 @@ dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned
case DC_FAMILY_HW_OSTC:
case DC_FAMILY_HW_FROG:
case DC_FAMILY_HW_OSTC3:
case DC_FAMILY_DIVESOFT_FREEDOM:
rc = dc_parser_new2(&parser, data->context, data->descriptor, buffer, size);
break;
default:

View file

@ -54,7 +54,8 @@ struct device_data_t {
const char *errmsg (dc_status_t rc);
std::string do_libdivecomputer_import(device_data_t *data);
dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size);
int prepare_device_descriptor(int data_model, dc_family_t dc_fam, device_data_t &dev_data);
dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, const 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);
dc_descriptor_t *get_descriptor(dc_family_t type, unsigned int model);

View file

@ -14,101 +14,48 @@
#include "format.h"
#include "libdivecomputer.h"
/*
* 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 = get_descriptor(dc_fam, data_model);
if (data_descriptor) {
dev_data.descriptor = data_descriptor;
dev_data.vendor = dc_descriptor_get_vendor(data_descriptor);
dev_data.model = dc_descriptor_get_product(data_descriptor);
} 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 divelog *log)
int ostctools_import(const std::unique_ptr<std::vector<unsigned char>> &buffer, struct divelog *log)
{
FILE *archive;
device_data_t devdata;
dc_family_t dc_fam;
std::vector<unsigned char> buffer(65536, 0);
unsigned char uc_tmp[2];
auto ostcdive = std::make_unique<dive>();
dc_status_t rc = DC_STATUS_SUCCESS;
int model, ret, i = 0, c;
unsigned int serial;
const char *failed_to_read_msg = translate("gettextFromC", "Failed to read '%s'");
// Open the archive
if ((archive = subsurface_fopen(file, "rb")) == NULL) {
report_error(failed_to_read_msg, file);
return;
}
if (buffer->size() < 456)
return report_error("%s", translate("gettextFromC", "Invalid OSTCTools file"));
// Read dive number from the log
if (fseek(archive, 258, 0) == -1) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
if (fread(uc_tmp, 1, 2, archive) != 2) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
ostcdive->number = uc_tmp[0] + (uc_tmp[1] << 8);
auto ostcdive = std::make_unique<dive>();
ostcdive->number = (*buffer)[258] + ((*buffer)[259] << 8);
// Read device's serial number
if (fseek(archive, 265, 0) == -1) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
if (fread(uc_tmp, 1, 2, archive) != 2) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
serial = uc_tmp[0] + (uc_tmp[1] << 8);
unsigned int serial = (*buffer)[265] + ((*buffer)[266] << 8);
// Read dive's raw data, header + profile
if (fseek(archive, 456, 0) == -1) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
while ((c = getc(archive)) != EOF) {
buffer[i] = c;
if (buffer[i] == 0xFD && buffer[i - 1] == 0xFD)
break;
// Trim the buffer to the actual dive data
buffer->erase(buffer->begin(), buffer->begin() + 456);
unsigned int i = 0;
bool end_marker = false;
for (auto c: *buffer) {
i++;
if (c == 0xFD) {
if (end_marker)
break;
else
end_marker = true;
} else {
end_marker = false;
}
}
if (ferror(archive)) {
report_error(failed_to_read_msg, file);
fclose(archive);
return;
}
fclose(archive);
if (end_marker)
buffer->erase(buffer->begin() + i, buffer->end());
// Try to determine the dc family based on the header type
if (buffer[2] == 0x20 || buffer[2] == 0x21) {
dc_family_t dc_fam;
if ((*buffer)[2] == 0x20 || (*buffer)[2] == 0x21) {
dc_fam = DC_FAMILY_HW_OSTC;
} else {
switch (buffer[8]) {
switch ((*buffer)[8]) {
case 0x22:
dc_fam = DC_FAMILY_HW_FROG;
break;
@ -117,12 +64,12 @@ void ostctools_import(const char *file, struct divelog *log)
dc_fam = DC_FAMILY_HW_OSTC3;
break;
default:
report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number);
return;
return report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number);
}
}
// Try to determine the model based on serial number
int model;
switch (dc_fam) {
case DC_FAMILY_HW_OSTC:
if (serial > 7000)
@ -145,17 +92,16 @@ void ostctools_import(const char *file, struct divelog *log)
}
// 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);
return;
}
device_data_t devdata;
int ret = prepare_device_descriptor(model, dc_fam, devdata);
if (ret == 0)
return report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number);
ostcdive->dcs[0].model = devdata.vendor + " " + devdata.model + " (Imported from OSTCTools)";
// Parse the dive data
rc = libdc_buffer_parser(ostcdive.get(), &devdata, buffer.data(), i + 1);
dc_status_t rc = libdc_buffer_parser(ostcdive.get(), &devdata, buffer->data(), buffer->size());
if (rc != DC_STATUS_SUCCESS)
report_error(translate("gettextFromC", "Error - %s - parsing dive %d"), errmsg(rc), ostcdive->number);
return 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, replace it.
@ -169,4 +115,6 @@ void ostctools_import(const char *file, struct divelog *log)
add_extra_data(&ostcdive->dcs[0], "Serial", ostcdive->dcs[0].serial);
log->dives.record_dive(std::move(ostcdive));
return 1;
}

View file

@ -1770,499 +1770,6 @@ int parse_xml_buffer(const char *url, const char *buffer, int, struct divelog *l
return ret;
}
/*
* 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, struct divelog *log)
{
using namespace std::string_literals;
unsigned char *ptr = buffer;
unsigned char event;
bool found;
unsigned int time = 0;
char serial[6];
struct battery_status {
uint16_t volt1;
uint8_t percent1;
uint16_t volt2;
uint8_t percent2;
};
struct battery_status battery_start = {0, 0, 0, 0};
struct battery_status battery_end = {0, 0, 0, 0};
uint16_t o2_sensor_calibration_values[4] = {0};
cylinder_t *cyl;
struct parser_state state;
state.log = log;
// Check for the correct file magic
if (ptr[0] != 'D' || ptr[1] != 'i' || ptr[2] != 'v' || ptr[3] != 'E')
return -1;
dive_start(&state);
divecomputer_start(&state);
state.cur_dc->model = "DLF import";
// (ptr[7] << 8) + ptr[6] Is "Serial"
snprintf(serial, sizeof(serial), "%d", (ptr[7] << 8) + ptr[6]);
state.cur_dc->serial = serial;
state.cur_dc->when = parse_dlf_timestamp(ptr + 8);
state.cur_dive->when = state.cur_dc->when;
state.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] & 0x38) >> 3) {
case 0: // unknown
case 1:
state.cur_dc->divemode = OC;
break;
case 2:
state.cur_dc->divemode = CCR;
break;
case 3:
state.cur_dc->divemode = CCR; // mCCR
break;
case 4:
state.cur_dc->divemode = FREEDIVE;
break;
case 5:
state.cur_dc->divemode = OC; // Gauge
break;
case 6:
state.cur_dc->divemode = PSCR; // ASCR
break;
case 7:
state.cur_dc->divemode = PSCR;
break;
}
state.cur_dc->maxdepth.mm = ((ptr[21] << 8) + ptr[20]) * 10;
state.cur_dc->surface_pressure.mbar = ((ptr[25] << 8) + ptr[24]) / 10;
// Declare initial mix as first cylinder
cyl = state.cur_dive->get_or_create_cylinder(0);
cyl->gasmix.o2.permille = ptr[26] * 10;
cyl->gasmix.he.permille = ptr[27] * 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 (state.cur_dc->divemode == CCR || state.cur_dc->divemode == PSCR)
state.cur_dc->no_o2sensors = 1;
for (; ptr < buffer + size; ptr += 16) {
time = ((ptr[0] >> 4) & 0x0f) +
((ptr[1] << 4) & 0xff0) +
((ptr[2] << 12) & 0x1f000);
event = ptr[0] & 0x0f;
switch (event) {
case 0:
/* Regular sample */
sample_start(&state);
state.cur_sample->time.seconds = time;
state.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 (state.cur_dc->divemode == CCR || state.cur_dc->divemode == PSCR)
state.cur_sample->o2sensor[0].mbar = ((ptr[7] << 8) + ptr[6]) / 10;
// In some test files, ndl / tts / temp is bogus if this bits are 1
// flag bits in ptr[11] & 0xF0 is probably involved to,
if ((ptr[2] >> 5) != 1) {
// NDL in minutes, 10 bit
state.cur_sample->ndl.seconds = (((ptr[9] & 0x03) << 8) + ptr[8]) * 60;
// TTS in minutes, 10 bit
state.cur_sample->tts.seconds = (((ptr[10] & 0x0F) << 6) + (ptr[9] >> 2)) * 60;
// Temperature in 1/10 C, 10 bit signed
state.cur_sample->temperature.mkelvin = ((ptr[11] & 0x20) ? -1 : 1) * (((ptr[11] & 0x1F) << 4) + (ptr[10] >> 4)) * 100 + ZERO_C_IN_MKELVIN;
}
state.cur_sample->stopdepth.mm = ((ptr[13] << 8) + ptr[12]) * 10;
if (state.cur_sample->stopdepth.mm)
state.cur_sample->in_deco = true;
//ptr[14] is helium content, always zero?
//ptr[15] is setpoint, what the computer thinks you should aim for?
sample_end(&state);
break;
case 1: /* dive event */
case 2: /* automatic parameter change */
case 3: /* diver error */
case 4: /* internal error */
case 5: /* device activity log */
//Event 18 is a button press. Lets ingore that event.
if (ptr[4] == 18)
continue;
event_start(&state);
state.cur_event.time.seconds = time;
switch (ptr[4]) {
case 1:
state.cur_event.name = "Setpoint Manual"s;
state.cur_event.value = ptr[6];
sample_start(&state);
state.cur_sample->setpoint.mbar = ptr[6] * 10;
sample_end(&state);
break;
case 2:
state.cur_event.name = "Setpoint Auto"s;
state.cur_event.value = ptr[6];
sample_start(&state);
state.cur_sample->setpoint.mbar = ptr[6] * 10;
sample_end(&state);
switch (ptr[7]) {
case 0:
state.cur_event.name += " Manual"s;
break;
case 1:
state.cur_event.name += " Auto Start"s;
break;
case 2:
state.cur_event.name += " Auto Hypox"s;
break;
case 3:
state.cur_event.name += " Auto Timeout"s;
break;
case 4:
state.cur_event.name += " Auto Ascent"s;
break;
case 5:
state.cur_event.name += " Auto Stall"s;
break;
case 6:
state.cur_event.name += " Auto SP Low"s;
break;
default:
break;
}
break;
case 3:
// obsolete
state.cur_event.name = "OC"s;
break;
case 4:
// obsolete
state.cur_event.name = "CCR"s;
break;
case 5:
state.cur_event.name = "gaschange"s;
state.cur_event.type = SAMPLE_EVENT_GASCHANGE2;
state.cur_event.value = ptr[7] << 8 ^ ptr[6];
for (const auto [i, cyl]: enumerated_range(state.cur_dive->cylinders)) {
if (cyl.gasmix.o2.permille == ptr[6] * 10 && cyl.gasmix.he.permille == ptr[7] * 10) {
found = true;
state.cur_event.gas.index = i;
break;
}
}
if (!found) {
cyl = cylinder_start(&state);
cyl->gasmix.o2.permille = ptr[6] * 10;
cyl->gasmix.he.permille = ptr[7] * 10;
cylinder_end(&state);
state.cur_event.gas.index = static_cast<int>(state.cur_dive->cylinders.size()) - 1;
}
break;
case 6:
state.cur_event.name = "Start"s;
break;
case 7:
state.cur_event.name = "Too Fast"s;
break;
case 8:
state.cur_event.name = "Above Ceiling"s;
break;
case 9:
state.cur_event.name = "Toxic"s;
break;
case 10:
state.cur_event.name = "Hypox"s;
break;
case 11:
state.cur_event.name = "Critical"s;
break;
case 12:
state.cur_event.name = "Sensor Disabled"s;
break;
case 13:
state.cur_event.name = "Sensor Enabled"s;
break;
case 14:
state.cur_event.name = "O2 Backup"s;
break;
case 15:
state.cur_event.name = "Peer Down"s;
break;
case 16:
state.cur_event.name = "HS Down"s;
break;
case 17:
state.cur_event.name = "Inconsistent"s;
break;
case 18:
// key pressed - It should never get in here
// as we ingored it at the parent 'case 5'.
break;
case 19:
// obsolete
state.cur_event.name = "SCR"s;
break;
case 20:
state.cur_event.name = "Above Stop"s;
break;
case 21:
state.cur_event.name = "Safety Miss"s;
break;
case 22:
state.cur_event.name = "Fatal"s;
break;
case 23:
state.cur_event.name = "gaschange"s;
state.cur_event.type = SAMPLE_EVENT_GASCHANGE2;
state.cur_event.value = ptr[7] << 8 ^ ptr[6];
event_end(&state);
break;
case 24:
state.cur_event.name = "gaschange"s;
state.cur_event.type = SAMPLE_EVENT_GASCHANGE2;
state.cur_event.value = ptr[7] << 8 ^ ptr[6];
event_end(&state);
// This is both a mode change and a gas change event
// so we encode it as two separate events.
event_start(&state);
state.cur_event.name = "Change Mode"s;
switch (ptr[8]) {
case 1:
state.cur_event.name += ": OC"s;
break;
case 2:
state.cur_event.name += ": CCR"s;
break;
case 3:
state.cur_event.name += ": mCCR"s;
break;
case 4:
state.cur_event.name += ": Free"s;
break;
case 5:
state.cur_event.name += ": Gauge"s;
break;
case 6:
state.cur_event.name += ": ASCR"s;
break;
case 7:
state.cur_event.name += ": PSCR"s;
break;
default:
break;
}
event_end(&state);
break;
case 25:
// uint16_t solenoid_bitmap = (ptr[7] << 8) + (ptr[6] << 0);
// uint32_t time = (ptr[11] << 24) + (ptr[10] << 16) + (ptr[9] << 8) + (ptr[8] << 0);
state.cur_event.name = format_string_std("CCR O2 solenoid %s", ptr[12] ? "opened": "closed");
break;
case 26:
state.cur_event.name = "User mark"s;
break;
case 27:
state.cur_event.name = format_string_std("%sGF Switch (%d/%d)", ptr[6] ? "Bailout, ": "", ptr[7], ptr[8]);
break;
case 28:
state.cur_event.name = "Peer Up"s;
break;
case 29:
state.cur_event.name = "HS Up"s;
break;
case 30:
state.cur_event.name = format_string_std("CNS %d%%", ptr[6]);
break;
default:
// No values above 30 had any description
break;
}
event_end(&state);
break;
case 6:
/* device configuration */
switch (((ptr[3] & 0x7f) << 3) + ((ptr[2] & 0xe0) >> 5)) {
// Buffer to print extra string into
// Local variables to temporary decode into
struct tm tm;
const char *device;
const char *deep_stops;
case 0: // TEST_CCR_FULL_1
utc_mkdate(parse_dlf_timestamp(ptr + 12), &tm);
add_extra_data(state.cur_dc, "TEST_CCR_FULL_1",
format_string_std("START=%04u-%02u-%02u %02u:%02u:%02u,TEST=%02X%02X%02X%02X,RESULT=%02X%02X%02X%02X",
tm.tm_year, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, ptr[7], ptr[6], ptr[5], ptr[4], ptr[11], ptr[10], ptr[9], ptr[8]));
break;
case 1: // TEST_CCR_PARTIAL_1
utc_mkdate(parse_dlf_timestamp(ptr + 12), &tm);
add_extra_data(state.cur_dc, "TEST_CCR_PARTIAL_1",
format_string_std("START=%04u-%02u-%02u %02u:%02u:%02u,TEST=%02X%02X%02X%02X,RESULT=%02X%02X%02X%02X",
tm.tm_year, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, ptr[7], ptr[6], ptr[5], ptr[4], ptr[11], ptr[10], ptr[9], ptr[8]));
break;
case 2: // CFG_OXYGEN_CALIBRATION
utc_mkdate(parse_dlf_timestamp(ptr + 12), &tm);
o2_sensor_calibration_values[0] = (ptr[5] << 8) + ptr[4];
o2_sensor_calibration_values[1] = (ptr[7] << 8) + ptr[6];
o2_sensor_calibration_values[2] = (ptr[9] << 8) + ptr[8];
o2_sensor_calibration_values[3] = (ptr[11] << 8) + ptr[10];
add_extra_data(state.cur_dc, "CFG_OXYGEN_CALIBRATION",
format_string_std("%04u,%04u,%04u,%04u,TIME=%04u-%02u-%02u %02u:%02u:%02u",
o2_sensor_calibration_values[0], o2_sensor_calibration_values[1], o2_sensor_calibration_values[2], o2_sensor_calibration_values[3], tm.tm_year, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec));
break;
case 3: // CFG_SERIAL
add_extra_data(state.cur_dc, "CFG_SERIAL",
format_string_std("PRODUCT=%c%c%c%c,SERIAL=%c%c%c%c%c%c%c%c",
ptr[4], ptr[5], ptr[6], ptr[7], ptr[8], ptr[9], ptr[10], ptr[11], ptr[12], ptr[13], ptr[14], ptr[15]));
break;
case 4: // CFG_CONFIG_DECO
switch ((ptr[5] & 0xC0) >> 6) {
case 0:
deep_stops = "none";
break;
case 1:
deep_stops = "Pyle";
break;
case 2:
deep_stops = "Sladek";
break;
default:
deep_stops = "unknown";
break;
}
add_extra_data(state.cur_dc, "CFG_CONFIG_DECO part 1",
format_string_std("%s,%s,%s,safety stop required=%s,last_stop=%s,deco_algorithm=%s,stop_rounding=%u,deep_stops=%s",
(ptr[4] & 0x80) ? "imperial" : "metric", (ptr[4] & 0x40) ? "sea" : "fresh", (ptr[4] & 0x30) ? "stops" : "ceiling", (ptr[4] & 0x10) ? "yes" : "no", (ptr[4] & 0x08) ? "6m" : "3m", (ptr[4] & 0x04) ? "VPM" : "Buhlmann+GF", (ptr[4] & 0x03) ? (ptr[4] & 0x03) * 30 : 1, deep_stops));
add_extra_data(state.cur_dc, "CFG_CONFIG_DECO part 2",
format_string_std("deep_stop_len=%u min,gas_switch_len=%u min,gf_low=%u,gf_high=%u,gf_low_bailout=%u,gf_high_bailout=%u,ppO2_low=%4.2f,ppO2_high=%4.2f",
(ptr[5] & 0x38) >> 3, ptr[5] & 0x07, ptr[6], ptr[7], ptr[8], ptr[9], ptr[10] / 100.0f, ptr[11] / 100.0f));
add_extra_data(state.cur_dc, "CFG_CONFIG_DECO part 3",
format_string_std("alarm_global=%u,alarm_cns=%u,alarm_ppO2=%u,alarm_ceiling=%u,alarm_stop_miss=%u,alarm_decentrate=%u,alarm_ascentrate=%u",
(ptr[12] & 0x80) >> 7, (ptr[12] & 0x40) >> 6, (ptr[12] & 0x20) >> 5, (ptr[12] & 0x10) >> 4, (ptr[12] & 0x08) >> 3, (ptr[12] & 0x04) >> 2, (ptr[12] & 0x02) >> 1));
break;
case 5: // CFG_VERSION
switch (ptr[4]) {
case 0:
device = "FREEDOM";
break;
case 1:
device = "LIBERTY_CU";
break;
case 2:
device = "LIBERTY_HS";
break;
default:
device = "UNKNOWN";
break;
}
add_extra_data(state.cur_dc, "CFG_VERSION",
format_string_std("DEVICE=%s,HW=%d.%d,FW=%d.%d.%d.%d,FLAGS=%04X",
device, ptr[5], ptr[6], ptr[7], ptr[8], ptr[9], (ptr[15] << 24) + (ptr[14] << 16) + (ptr[13] << 8) + (ptr[12]), (ptr[11] << 8) + ptr[10]));
break;
}
break;
case 7:
/* measure record */
switch (ptr[2] >> 5) {
case 1:
/* Record starting battery level */
if (!battery_start.volt1 && !battery_start.volt2) {
battery_start.volt1 = (ptr[5] << 8) + ptr[4];
battery_start.percent1 = ptr[6];
battery_start.volt2 = (ptr[9] << 8) + ptr[8];
battery_start.percent2 = ptr[10];
}
/* Measure Battery, recording the last reading only */
battery_end.volt1 = (ptr[5] << 8) + ptr[4];
battery_end.percent1 = ptr[6];
battery_end.volt2 = (ptr[9] << 8) + ptr[8];
battery_end.percent2 = ptr[10];
break;
case 2:
/* Measure He */
//report_info("%ds he2 cells(0.01 mV): %d %d", time, (ptr[5] << 8) + ptr[4], (ptr[9] << 8) + ptr[8]);
break;
case 3:
/* Measure Oxygen */
//report_info("%d s: o2 cells(0.01 mV): %d %d %d %d", time, (ptr[5] << 8) + ptr[4], (ptr[7] << 8) + ptr[6], (ptr[9] << 8) + ptr[8], (ptr[11] << 8) + ptr[10]);
// [Pa/mV] coeficient O2
// 100 Pa == 1 mbar
sample_start(&state);
state.cur_sample->time.seconds = time;
state.cur_sample->o2sensor[0].mbar = ( ((ptr[5] << 8) + ptr[4]) * o2_sensor_calibration_values[0]) / 10000;
state.cur_sample->o2sensor[1].mbar = ( ((ptr[7] << 8) + ptr[6]) * o2_sensor_calibration_values[1]) / 10000;
state.cur_sample->o2sensor[2].mbar = ( ((ptr[9] << 8) + ptr[8]) * o2_sensor_calibration_values[2]) / 10000;
state.cur_sample->o2sensor[3].mbar = ( ((ptr[11] << 8) + ptr[10]) * o2_sensor_calibration_values[3]) / 10000;
sample_end(&state);
break;
case 4:
/* Measure GPS */
state.cur_location.lat.udeg = (int)((ptr[7] << 24) + (ptr[6] << 16) + (ptr[5] << 8) + (ptr[4] << 0));
state.cur_location.lon.udeg = (int)((ptr[11] << 24) + (ptr[10] << 16) + (ptr[9] << 8) + (ptr[8] << 0));
state.log->sites.create("DLF imported"s, state.cur_location)->add_dive(state.cur_dive.get());
break;
default:
break;
}
break;
case 8:
/* Deco event */
break;
default:
/* Unknown... */
break;
}
}
/* Recording the starting battery status to extra data */
if (battery_start.volt1) {
std::string str = format_string_std("%dmV (%d%%)", battery_start.volt1, battery_start.percent1);
add_extra_data(state.cur_dc, "Battery 1 (start)", str.c_str());
str = format_string_std("%dmV (%d%%)", battery_start.volt2, battery_start.percent2);
add_extra_data(state.cur_dc, "Battery 2 (start)", str.c_str());
}
/* Recording the ending battery status to extra data */
if (battery_end.volt1) {
std::string str = format_string_std("%dmV (%d%%)", battery_end.volt1, battery_end.percent1);
add_extra_data(state.cur_dc, "Battery 1 (end)", str.c_str());
str = format_string_std("%dmV (%d%%)", battery_end.volt2, battery_end.percent2);
add_extra_data(state.cur_dc, "Battery 2 (end)", str.c_str());
}
divecomputer_end(&state);
dive_end(&state);
return 0;
}
void parse_xml_init()
{
LIBXML_TEST_VERSION

View file

@ -150,7 +150,6 @@ int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buf, i
int parse_shearwater_cloud_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct divelog *log);
int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct divelog *log);
int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct divelog *log);
int parse_dlf_buffer(unsigned char *buffer, size_t size, struct divelog *log);
std::string trimspace(const char *buffer);
#endif

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
import QtQuick 2.5
import QtLocation 5.3
import QtPositioning 5.3
import QtQuick
import QtLocation
import QtPositioning
import org.subsurfacedivelog.mobile 1.0
Item {
@ -15,7 +15,7 @@ Item {
id: mapHelper
map: map
editMode: false
onSelectedDivesChanged: rootItem.selectedDivesChanged(list)
onSelectedDivesChanged: (list) => { rootItem.selectedDivesChanged(list) }
onEditModeChanged: editMessage.isVisible = editMode === true ? 1 : 0
onCoordinatesChanged: {}
Component.onCompleted: {
@ -29,7 +29,6 @@ Item {
id: map
anchors.fill: parent
zoomLevel: defaultZoomIn
property var mapType
readonly property var defaultCenter: QtPositioning.coordinate(0, 0)
readonly property real defaultZoomIn: 12.0
@ -41,12 +40,46 @@ Item {
property real newZoomOut: 1.0
property var clickCoord: QtPositioning.coordinate(0, 0)
property bool isReady: false
Component.onCompleted: isReady = true
onZoomLevelChanged: {
if (isReady)
mapHelper.calculateSmallCircleRadius(map.center)
}
property geoCoordinate startCentroid
startCentroid: newCenter
PinchHandler {
id: pinch
target: null
onActiveChanged: if (active) {
map.startCentroid = map.toCoordinate(pinch.centroid.position, false)
}
onScaleChanged: (delta) => {
map.zoomLevel += Math.log2(delta)
map.alignCoordinateToPoint(map.startCentroid, pinch.centroid.position)
}
onRotationChanged: (delta) => {
map.bearing -= delta
map.alignCoordinateToPoint(map.startCentroid, pinch.centroid.position)
}
grabPermissions: PointerHandler.TakeOverForbidden
}
WheelHandler {
id: wheel
// workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432:
// Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler
// and we don't yet distinguish mice and trackpads on Wayland either
acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland"
? PointerDevice.Mouse | PointerDevice.TouchPad
: PointerDevice.Mouse
rotationScale: 1/120
property: "zoomLevel"
}
DragHandler {
id: drag
target: null
onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y)
}
MapItemView {
id: mapItemView
@ -67,7 +100,9 @@ Item {
}
MouseArea {
drag.target: (mapHelper.editMode && model.isSelected) ? mapItem : undefined
anchors.fill: parent
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!mapHelper.editMode && model.divesite)
mapHelper.selectedLocationChanged(model.divesite)
@ -122,8 +157,8 @@ Item {
MouseArea {
anchors.fill: parent
onPressed: { map.stopZoomAnimations(); mouse.accepted = false }
onWheel: { map.stopZoomAnimations(); wheel.accepted = false }
onPressed: (mouse) => { map.stopZoomAnimations(); mouse.accepted = false }
onWheel: (wheel) => { map.stopZoomAnimations(); wheel.accepted = false }
onDoubleClicked: map.doubleClickHandler(map.toCoordinate(Qt.point(mouseX, mouseY)))
}

View file

@ -4,20 +4,20 @@
#include <QDebug>
#include <QVector>
#include "qmlmapwidgethelper.h"
#include "core/divefilter.h"
#include "core/divelist.h"
#include "core/divelog.h"
#include "core/divesite.h"
#include "core/qthelper.h"
#include "core/range.h"
#include "qt-models/maplocationmodel.h"
#include "qmlmapwidgethelper.h"
#include "qt-models/divelocationmodel.h"
#include "qt-models/maplocationmodel.h"
#ifndef SUBSURFACE_MOBILE
#include "desktop-widgets/mapwidget.h"
#endif
#define SMALL_CIRCLE_RADIUS_PX 26.0
#define SMALL_CIRCLE_RADIUS_PX 26.0
MapWidgetHelper::MapWidgetHelper(QObject *parent) : QObject(parent)
{
@ -44,7 +44,7 @@ void MapWidgetHelper::centerOnDiveSite(struct dive_site *ds)
} else {
// dive site with GPS
m_mapLocationModel->setSelected(ds);
QGeoCoordinate dsCoord (ds->location.lat.udeg * 0.000001, ds->location.lon.udeg * 0.000001);
QGeoCoordinate dsCoord(ds->location.lat.udeg * 0.000001, ds->location.lon.udeg * 0.000001);
QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord)));
}
}
@ -69,7 +69,7 @@ void MapWidgetHelper::centerOnSelectedDiveSite()
// find the most top-left and bottom-right dive sites on the map coordinate system.
qreal minLat = 0.0, minLon = 0.0, maxLat = 0.0, maxLon = 0.0;
int count = 0;
for(struct dive_site *dss: selDS) {
for (struct dive_site *dss : selDS) {
if (!has_location(&dss->location))
continue;
qreal lat = dss->location.lat.udeg * 0.000001;
@ -92,7 +92,7 @@ void MapWidgetHelper::centerOnSelectedDiveSite()
// Pass coordinates to QML, either as a point or as a rectangle.
// If we didn't find any coordinates, do nothing.
if (count == 1) {
QGeoCoordinate dsCoord (selDS[0]->location.lat.udeg * 0.000001, selDS[0]->location.lon.udeg * 0.000001);
QGeoCoordinate dsCoord(selDS[0]->location.lat.udeg * 0.000001, selDS[0]->location.lon.udeg * 0.000001);
QMetaObject::invokeMethod(m_map, "centerOnCoordinate", Q_ARG(QVariant, QVariant::fromValue(dsCoord)));
} else if (count > 1) {
QGeoCoordinate coordTopLeft(minLat, minLon);
@ -134,7 +134,7 @@ void MapWidgetHelper::selectedLocationChanged(struct dive_site *ds_in)
return;
QGeoCoordinate locationCoord = location->coordinate;
for (auto [idx, dive]: enumerated_range(divelog.dives)) {
for (auto [idx, dive] : enumerated_range(divelog.dives)) {
struct dive_site *ds = dive->dive_site;
if (!ds || !ds->has_gps_location())
continue;
@ -151,9 +151,9 @@ void MapWidgetHelper::selectedLocationChanged(struct dive_site *ds_in)
}
int last; // get latest dive chronologically
if (!selectedDiveIds.isEmpty()) {
last = selectedDiveIds.last();
selectedDiveIds.clear();
selectedDiveIds.append(last);
last = selectedDiveIds.last();
selectedDiveIds.clear();
selectedDiveIds.append(last);
}
#endif
emit selectedDivesChanged(selectedDiveIds);
@ -162,7 +162,7 @@ void MapWidgetHelper::selectedLocationChanged(struct dive_site *ds_in)
void MapWidgetHelper::selectVisibleLocations()
{
QList<int> selectedDiveIds;
for (auto [idx, dive]: enumerated_range(divelog.dives)) {
for (auto [idx, dive] : enumerated_range(divelog.dives)) {
struct dive_site *ds = dive->dive_site;
if (!ds || ds->has_gps_location())
continue;
@ -171,7 +171,7 @@ void MapWidgetHelper::selectVisibleLocations()
QGeoCoordinate dsCoord(latitude, longitude);
QPointF point;
QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point),
Q_ARG(QGeoCoordinate, dsCoord));
Q_ARG(QGeoCoordinate, dsCoord));
if (!qIsNaN(point.x()))
#ifndef SUBSURFACE_MOBILE // indices on desktop
selectedDiveIds.append(idx);
@ -181,9 +181,9 @@ void MapWidgetHelper::selectVisibleLocations()
}
int last; // get latest dive chronologically
if (!selectedDiveIds.isEmpty()) {
last = selectedDiveIds.last();
selectedDiveIds.clear();
selectedDiveIds.append(last);
last = selectedDiveIds.last();
selectedDiveIds.clear();
selectedDiveIds.append(last);
}
#endif
emit selectedDivesChanged(selectedDiveIds);
@ -205,11 +205,11 @@ void MapWidgetHelper::calculateSmallCircleRadius(QGeoCoordinate coord)
{
QPointF point;
QMetaObject::invokeMethod(m_map, "fromCoordinate", Q_RETURN_ARG(QPointF, point),
Q_ARG(QGeoCoordinate, coord));
Q_ARG(QGeoCoordinate, coord));
QPointF point2(point.x() + SMALL_CIRCLE_RADIUS_PX, point.y());
QGeoCoordinate coord2;
QMetaObject::invokeMethod(m_map, "toCoordinate", Q_RETURN_ARG(QGeoCoordinate, coord2),
Q_ARG(QPointF, point2));
Q_ARG(QPointF, point2));
m_smallCircleRadius = coord2.distanceTo(coord);
}
@ -251,8 +251,8 @@ QString MapWidgetHelper::pluginObject()
{
QString lang = getUiLanguage().replace('_', '-');
QString cacheFolder = QString::fromStdString(system_default_directory() + "/googlemaps").replace("\\", "/");
return QStringLiteral("import QtQuick 2.0;"
"import QtLocation 5.3;"
return QStringLiteral("import QtQuick;"
"import QtLocation;"
"Plugin {"
" id: mapPlugin;"
" name: 'googlemaps';"
@ -263,5 +263,6 @@ QString MapWidgetHelper::pluginObject()
" console.warn('MapWidget.qml: cannot find a plugin named: ' + name);"
" }"
" }"
"}").arg(lang, cacheFolder);
"}")
.arg(lang, cacheFolder);
}

View file

@ -1,7 +1,10 @@
# Build the image using the --build-arg option, e.g.:
# docker build -t boret/myimage:0.1 --build-arg=mxe_sha=123ABC456 .
FROM ubuntu:24.04 as base
# We need to stick with 22.04 for now because the latest MXE version
# (db430dc676e6f5d77604af150b8acc1403af4fd7) does not build a working
# version of QtWebKit, which breaks printing in Subsurface.
FROM ubuntu:22.04 as base
# update and set up the packages we need for the build
RUN apt-get update && \
@ -54,8 +57,9 @@ RUN apt-get install -y \
xz-utils \
scons
# very often master is broken, so we pass in a known good SHA
ARG mxe_sha=d6377b2f2334694dbb040294fd0d848327e63328
# Default to 'master' if no build argument is passed in
ARG mxe_sha=master
# Very often master is broken, so we pass in a known good SHA
ENV _ver=${mxe_sha}
WORKDIR /win

View file

@ -169,8 +169,9 @@ for package in "${PACKAGES[@]}" ; do
git_checkout_library breeze-icons $CURRENT_BREEZE_ICONS https://github.com/kde/breeze-icons.git
;;
googlemaps)
git_checkout_library googlemaps master https://github.com/Subsurface/googlemaps.git
;;
#git_checkout_library googlemaps master https://github.com/Subsurface/googlemaps.git
git_checkout_library googlemaps master https://github.com/vladest/googlemaps.git
;;
hidapi)
git_checkout_library hidapi master https://github.com/libusb/hidapi.git
;;

View file

@ -865,17 +865,17 @@ void TestPlan::testVpmbMetricRepeat()
// check minimum gas result
dp = std::find_if(testPlan.dp.begin(), testPlan.dp.end(), [](auto &dp) { return dp.minimum_gas.mbar != 0; });
QCOMPARE(lrint(dp == testPlan.dp.end() ? 0.0 : dp->minimum_gas.mbar / 1000.0), 80l);
QCOMPARE(lrint(dp == testPlan.dp.end() ? 0.0 : dp->minimum_gas.mbar / 1000.0), 85l);
// print first ceiling
printf("First ceiling %.1f m\n", dive.mbar_to_depth(test_deco_state.first_ceiling_pressure.mbar) * 0.001);
QVERIFY(dive.dcs[0].events.size() >= 3);
// check first gas change to 21/35 at 66m
// check first gas change to 21/35 at 63m
struct event *ev = &dive.dcs[0].events[0];
QVERIFY(ev != NULL);
QCOMPARE(ev->gas.index, 1);
QCOMPARE(ev->gas.mix.o2.permille, 210);
QCOMPARE(ev->gas.mix.he.permille, 350);
QCOMPARE(get_depth_at_time(&dive.dcs[0], ev->time.seconds), 66000);
QCOMPARE(get_depth_at_time(&dive.dcs[0], ev->time.seconds), 63000);
// check second gas change to EAN50 at 21m
ev = &dive.dcs[0].events[1];
QCOMPARE(ev->gas.index, 2);