Пример #1
0
void MainWindow::on_actionAddDive_triggered()
{
	if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING ||
	    ui.InfoWidget->isEditing()) {
		QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before trying to add a dive."));
		return;
	}
	dive_list()->rememberSelection();
	dive_list()->unselectDives();
	disableDcShortcuts();
	DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD);

	// now cheat - create one dive that we use to store the info tab data in
	struct dive *dive = alloc_dive();
	dive->when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset();
	dive->dc.model = "manually added dive"; // don't translate! this is stored in the XML file

	dive->latitude.udeg = 0;
	dive->longitude.udeg = 0;
	record_dive(dive);
	// this isn't in the UI yet, so let's call the C helper function - we'll fix this up when
	// accepting the dive
	select_dive(get_divenr(dive));
	ui.InfoWidget->setCurrentIndex(0);
	ui.InfoWidget->updateDiveInfo(selected_dive);
	ui.InfoWidget->addDiveStarted();
	ui.stackedWidget->setCurrentIndex(PLANNERPROFILE); // Planner.
	ui.infoPane->setCurrentIndex(MAINTAB);
	DivePlannerPointsModel::instance()->clear();
	DivePlannerPointsModel::instance()->createSimpleDive();
	ui.ListWidget->reload(DiveTripModel::CURRENT);
}
Пример #2
0
void UndoDeleteDive::redo()
{
	QList<struct dive*> newList;
	for (int i = 0; i < diveList.count(); i++) {
		// make a copy of the dive before deleting it
		struct dive* d = alloc_dive();
		copy_dive(diveList.at(i), d);
		newList.append(d);
		// check for trip - if this is the last dive in the trip
		// the trip will get deleted, so we need to remember it as well
		if (d->divetrip && d->divetrip->nrdives == 1) {
			struct dive_trip *undo_trip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip));
			*undo_trip = *d->divetrip;
			undo_trip->location = copy_string(d->divetrip->location);
			undo_trip->notes = copy_string(d->divetrip->notes);
			undo_trip->nrdives = 0;
			undo_trip->next = NULL;
			undo_trip->dives = NULL;
			// update all the dives who were in this trip to point to the copy of the
			// trip that we are about to delete implicitly when deleting its last dive below
			Q_FOREACH(struct dive *inner_dive, newList)
				if (inner_dive->divetrip == d->divetrip)
					inner_dive->divetrip = undo_trip;
			d->divetrip = undo_trip;
			tripList.append(undo_trip);
		}
		//delete the dive
		delete_single_dive(get_divenr(diveList.at(i)));
	}
	mark_divelist_changed(true);
	MainWindow::instance()->refreshDisplay();
	diveList.clear();
	diveList = newList;
}
Пример #3
0
void QMLManager::deleteDive(int id)
{
	struct dive *d = get_dive_by_uniq_id(id);
	if (!d) {
		qDebug() << "oops, trying to delete non-existing dive";
		return;
	}
	// clean up (or create) the storage for the deleted dive and trip (if applicable)
	if (!deletedDive)
		deletedDive = alloc_dive();
	else
		clear_dive(deletedDive);
	copy_dive(d, deletedDive);
	if (!deletedTrip) {
		deletedTrip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip));
	} else {
		free(deletedTrip->location);
		free(deletedTrip->notes);
		memset(deletedTrip, 0, sizeof(struct dive_trip));
	}
	// if this is the last dive in that trip, remember the trip as well
	if (d->divetrip && d->divetrip->nrdives == 1) {
		*deletedTrip = *d->divetrip;
		deletedTrip->location = copy_string(d->divetrip->location);
		deletedTrip->notes = copy_string(d->divetrip->notes);
		deletedTrip->nrdives = 0;
		deletedDive->divetrip = deletedTrip;
	}
	DiveListModel::instance()->removeDiveById(id);
	delete_single_dive(get_idx_by_uniq_id(id));
	changesNeedSaving();
}
Пример #4
0
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;
}
Пример #5
0
void DivePlannerPointsModel::createPlan(bool replanCopy)
{
	// Ok, so, here the diveplan creates a dive
	struct deco_state *cache = NULL;
	bool oldRecalc = setRecalc(false);
	removeDeco();
	createTemporaryPlan();
	setRecalc(oldRecalc);

	//TODO: C-based function here?
	struct decostop stoptable[60];
	plan(&diveplan, &displayed_dive, DECOTIMESTEP, stoptable, &cache, isPlanner(), true);
	free(cache);
	if (!current_dive || displayed_dive.id != current_dive->id) {
		// we were planning a new dive, not re-planning an existing on
		record_dive(clone_dive(&displayed_dive));
	} else if (current_dive && displayed_dive.id == current_dive->id) {
		// we are replanning a dive - make sure changes are reflected
		// correctly in the dive structure and copy it back into the dive table
		displayed_dive.maxdepth.mm = 0;
		displayed_dive.dc.maxdepth.mm = 0;
		fixup_dive(&displayed_dive);
		// Try to identify old planner output and remove only this part
		// Treat user provided text as plain text.
		QTextDocument notesDocument;
		notesDocument.setHtml(current_dive->notes);
		QString oldnotes(notesDocument.toPlainText());
		int disclaimerPosition = oldnotes.indexOf(disclaimer);
		if (disclaimerPosition == 0)
			oldnotes.clear();
		else if (disclaimerPosition >= 1)
			oldnotes.truncate(disclaimerPosition-1);
		// Deal with line breaks
		oldnotes.replace("\n", "<br>");
		oldnotes.append(displayed_dive.notes);
		displayed_dive.notes = strdup(oldnotes.toUtf8().data());
		// If we save as new create a copy of the dive here
		if (replanCopy) {
			struct dive *copy = alloc_dive();
			copy_dive(current_dive, copy);
			copy->id = 0;
			copy->selected = false;
			copy->divetrip = NULL;
			if (current_dive->divetrip)
				add_dive_to_trip(copy, current_dive->divetrip);
			record_dive(copy);
		}
		copy_dive(&displayed_dive, current_dive);
	}
	mark_divelist_changed(true);

	// Remove and clean the diveplan, so we don't delete
	// the dive by mistake.
	free_dps(&diveplan);
	setPlanMode(NOTHING);
	planCreated();
}
Пример #6
0
void DivePlannerPointsModel::computeVariations()
{
	bool oldRecalc = setRecalc(false);
	struct dive *dive = alloc_dive();
	copy_dive(&displayed_dive, dive);
	struct decostop original[60], deeper[60], shallower[60], shorter[60], longer[60];
	struct deco_state *cache = NULL, *save = NULL;
	struct diveplan plan_copy;
	struct divedatapoint *last_segment;

	if(in_planner() && prefs.display_variations) {
		cache_deco_state(&save);
		cloneDiveplan(&plan_copy);
		plan(&plan_copy, dive, 1, original, &cache, true, false);
		free_dps(&plan_copy);
		restore_deco_state(save, false);

		last_segment = cloneDiveplan(&plan_copy);
		last_segment->depth.mm += 1000;
		last_segment->next->depth.mm += 1000;
		plan(&plan_copy, dive, 1, deeper, &cache, true, false);
		free_dps(&plan_copy);
		restore_deco_state(save, false);

		last_segment = cloneDiveplan(&plan_copy);
		last_segment->depth.mm -= 1000;
		last_segment->next->depth.mm -= 1000;
		plan(&plan_copy, dive, 1, shallower, &cache, true, false);
		free_dps(&plan_copy);
		restore_deco_state(save, false);

		last_segment = cloneDiveplan(&plan_copy);
		last_segment->next->time += 60;
		plan(&plan_copy, dive, 1, longer, &cache, true, false);
		free_dps(&plan_copy);
		restore_deco_state(save, false);

		last_segment = cloneDiveplan(&plan_copy);
		last_segment->next->time -= 60;
		plan(&plan_copy, dive, 1, shorter, &cache, true, false);
		free_dps(&plan_copy);
		restore_deco_state(save, false);
#ifdef SHOWSTOPVARIATIONS
		printf("\n\n");
#endif

		QString notes(displayed_dive.notes);
		free(displayed_dive.notes);

		char buf[200];
		sprintf(buf, "+ %d:%02d /m + %d:%02d /min", FRACTION(analyzeVariations(shallower, original, deeper, "m"),60),
			FRACTION(analyzeVariations(shorter, original, longer, "min"), 60));

		displayed_dive.notes = strdup(notes.replace("VARIATIONS", QString(buf)).toUtf8().data());
	}
	setRecalc(oldRecalc);
}
Пример #7
0
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;
}
Пример #8
0
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;
	}
}
Пример #9
0
void MainWindow::on_actionAddDive_triggered()
{
	struct dive *dive;
	dive = alloc_dive();
	record_dive(dive);
	process_dives(FALSE, FALSE);

	ui->InfoWidget->reload();
	ui->globe->reload();
	ui->ListWidget->reload(DiveTripModel::TREE);
	ui->ListWidget->setFocus();
}
Пример #10
0
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;
}
Пример #11
0
// create a new dive. set the current time and add it to the end of the dive list
QString DiveListModel::startAddDive()
{
	struct dive *d;
	d = alloc_dive();
	d->when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset();
	struct dive *pd = get_dive(dive_table.nr - 1);
	int nr = 1;
	if (pd && pd->number > 0)
		nr = pd->number + 1;
	d->number = nr;
	d->dc.model = strdup("manually added dive");
	add_single_dive(-1, d);
	addDive(d);
	return QString::number(d->id);
}
Пример #12
0
void UndoDeleteDive::redo()
{
	QList<struct dive*> newList;
	for (int i = 0; i < diveList.count(); i++) {
		//make a copy of the dive before deleting it
		struct dive* d = alloc_dive();
		copy_dive(diveList.at(i), d);
		newList.append(d);
		//delete the dive
		delete_single_dive(get_divenr(diveList.at(i)));
	}
	mark_divelist_changed(true);
	MainWindow::instance()->refreshDisplay();
	diveList.clear();
	diveList = newList;
}
Пример #13
0
void DivePlannerPointsModel::createPlan(bool replanCopy)
{
	// Ok, so, here the diveplan creates a dive
	char *cache = NULL;
	bool oldRecalc = setRecalc(false);
	removeDeco();
	createTemporaryPlan();
	setRecalc(oldRecalc);

	//TODO: C-based function here?
	bool did_deco = plan(&diveplan, &cache, isPlanner(), true);
	free(cache);
	if (!current_dive || displayed_dive.id != current_dive->id) {
		// we were planning a new dive, not re-planning an existing on
		record_dive(clone_dive(&displayed_dive));
	} else if (current_dive && displayed_dive.id == current_dive->id) {
		// we are replanning a dive - make sure changes are reflected
		// correctly in the dive structure and copy it back into the dive table
		displayed_dive.maxdepth.mm = 0;
		displayed_dive.dc.maxdepth.mm = 0;
		fixup_dive(&displayed_dive);
		if (replanCopy) {
			struct dive *copy = alloc_dive();
			copy_dive(current_dive, copy);
			copy->id = 0;
			copy->selected = false;
			copy->divetrip = NULL;
			if (current_dive->divetrip)
				add_dive_to_trip(copy, current_dive->divetrip);
			record_dive(copy);
			QString oldnotes(current_dive->notes);
			if (oldnotes.indexOf(QString(disclaimer)) >= 0)
				oldnotes.truncate(oldnotes.indexOf(QString(disclaimer)));
			if (did_deco)
				oldnotes.append(displayed_dive.notes);
			displayed_dive.notes = strdup(oldnotes.toUtf8().data());
		}
		copy_dive(&displayed_dive, current_dive);
	}
	mark_divelist_changed(true);

	// Remove and clean the diveplan, so we don't delete
	// the dive by mistake.
	free_dps(&diveplan);
	setPlanMode(NOTHING);
	planCreated();
}
Пример #14
0
void MainWindow::on_actionAddDive_triggered()
{
	// clear the selection
	for (int i = 0; i < dive_table.nr; i++) {
		struct dive *d = get_dive(i);
		if (d && d->selected)
			deselect_dive(i);
	}
	disableDcShortcuts();
	DivePlannerPointsModel::instance()->setPlanMode(false);
	// now cheat - create one dive that we use to store the info tab data in
	struct dive *dive = alloc_dive();
	dive->when = QDateTime::currentMSecsSinceEpoch() / 1000L;
	const char* model = strdup(tr("manually added dive").toLocal8Bit().constData());
	dive->dc.model = model; // do not use tr here since it expects a char*.
	record_dive(dive);
	select_dive(get_divenr(dive));
	ui.InfoWidget->updateDiveInfo(selected_dive);
	ui.stackedWidget->setCurrentIndex(PLANNERPROFILE); // Planner.
	ui.infoPane->setCurrentIndex(MAINTAB);
	DivePlannerPointsModel::instance()->createSimpleDive();
	refreshDisplay();
	ui.InfoWidget->addDiveStarted();
}
Пример #15
0
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 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 (!empty_string(lineptr) && (lineptr = strchr(lineptr, '\n'))) {
			++lineptr;	// Skip over '\n'
			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_pressure(sample, 0, lrint(value * 1000));
						break;
					case 14:
						//Diluent Tank Pressure
						add_sample_pressure(sample, 1, lrint(value * 1000));
						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);
			finish_sample(dc);

			if (!lineptr || !*lineptr)
				break;
		}
		record_dive(dive);
		return 1;
	} else {
		return 0;
	}

	return 0;
}
Пример #16
0
static void cochran_parse_dive(const unsigned char *decode, unsigned mod,
			       const unsigned char *in, unsigned size,
			       struct dive_table *table)
{
	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:
	 */

	if (size < 0x4914 + config.logbook_size) {
		// Analyst calls this a "Corrupt Beginning Summary"
		free(buf);
		return;
	}

	// 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;
	unsigned int sample_pre_offset = 0, sample_end_offset = 0;

	// 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 = lrint((log[CMD_MAX_DEPTH] +
			log[CMD_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000);
		dc->meandepth.mm = lrint((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 = lrint(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;

		sample_pre_offset = array_uint32_le(log + CMD_PREDIVE_OFFSET);
		sample_end_offset = array_uint32_le(log + CMD_END_OFFSET);

		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 = lrint((log[EMC_MAX_DEPTH] +
			log[EMC_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000);
		dc->meandepth.mm = lrint((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 = lrint(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;

		sample_pre_offset = array_uint32_le(log + EMC_PREDIVE_OFFSET);
		sample_end_offset = array_uint32_le(log + EMC_END_OFFSET);

		break;
	}

	// Use the log information to determine actual profile sample size
	// Otherwise we will get surface time at end of dive.
	if (sample_pre_offset < sample_end_offset && sample_end_offset != 0xffffffff)
		sample_size = sample_end_offset - sample_pre_offset;

	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 = lrint(max_depth * FEET * 1000);
		dc->meandepth.mm = lrint(avg_depth * FEET * 1000);
		dc->watertemp.mkelvin = C_to_mkelvin((min_temp - 32) / 1.8);
		dc->duration.seconds = duration;
	}

	record_dive_to_table(dive, table);
	mark_divelist_changed(true);

	free(buf);
}
Пример #17
0
/*
 * OSTCTools stores the raw dive data in heavily padded files, one dive
 * each file. So it's not necesary 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),
		      *tmp;
	struct dive *ostcdive = alloc_dive();
	dc_status_t rc = 0;
	int model = 0, i = 0;
	unsigned int serial;
	struct extra_data *ptr;

	// Open the archive
	if ((archive = subsurface_fopen(file, "rb")) == NULL) {
		report_error(translate("gettextFromC", "Error: couldn't open the file"));
		return;
	}

	// Read dive number from the log
	tmp =  calloc(2,1);
	fseek(archive, 258, 0);
	fread(tmp, 1, 2, archive);
	ostcdive->number = tmp[0] + (tmp[1] << 8);
	free(tmp);

	// Read device's serial number
	tmp = calloc(2, 1);
	fseek(archive, 265, 0);
	fread(tmp, 1, 2, archive);
	serial = tmp[0] + (tmp[1] << 8);
	free(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
	switch (buffer[2]) {
		case 0x20:
		case 0x21:
			dc_fam = DC_FAMILY_HW_OSTC;
			break;
		case 0x22:
			dc_fam = DC_FAMILY_HW_FROG;
			break;
		case 0x23:
			dc_fam = DC_FAMILY_HW_OSTC3;
			break;
	}

	// Prepare data to pass to libdivecomputer. OSTC protocol doesn't include
	// a model number so will use 0.
	ostc_prepare_data(model, dc_fam, devdata);
	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("Libdc returned error -%s- for 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);

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

	free(devdata);
	free(buffer);
	record_dive_to_table(ostcdive, divetable);
	mark_divelist_changed(true);
	sort_table(divetable);
	fclose(archive);
}
Пример #18
0
/*
 * 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);
}
Пример #19
0
static int dive_cb(const unsigned char *data, unsigned int size,
	const unsigned char *fingerprint, unsigned int fsize,
	void *userdata)
{
	int rc;
	parser_t *parser = NULL;
	device_data_t *devdata = userdata;
	dc_datetime_t dt = {0};
	struct tm tm;
	struct dive *dive;

	rc = create_parser(devdata, &parser);
	if (rc != PARSER_STATUS_SUCCESS) {
		fprintf(stderr, "Unable to create parser for %s", devdata->name);
		return rc;
	}

	rc = parser_set_data(parser, data, size);
	if (rc != PARSER_STATUS_SUCCESS) {
		fprintf(stderr, "Error registering the data.");
		parser_destroy(parser);
		return rc;
	}

	dive = alloc_dive();
	rc = parser_get_datetime(parser, &dt);
	if (rc != PARSER_STATUS_SUCCESS && rc != PARSER_STATUS_UNSUPPORTED) {
		fprintf(stderr, "Error parsing the datetime.");
		parser_destroy (parser);
		return rc;
	}

	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 = utc_mktime(&tm);

	// Parse the divetime.
	printf("Parsing the divetime.\n");
	unsigned int divetime = 0;
	rc = parser_get_field (parser, FIELD_TYPE_DIVETIME, 0, &divetime);
	if (rc != PARSER_STATUS_SUCCESS && rc != PARSER_STATUS_UNSUPPORTED) {
		fprintf(stderr, "Error parsing the divetime.");
		parser_destroy(parser);
		return rc;
	}
	dive->duration.seconds = divetime;

	// Parse the maxdepth.
	printf("Parsing the maxdepth.\n");
	double maxdepth = 0.0;
	rc = parser_get_field(parser, FIELD_TYPE_MAXDEPTH, 0, &maxdepth);
	if (rc != PARSER_STATUS_SUCCESS && rc != PARSER_STATUS_UNSUPPORTED) {
		fprintf(stderr, "Error parsing the maxdepth.");
		parser_destroy(parser);
		return rc;
	}
	dive->maxdepth.mm = maxdepth * 1000 + 0.5;

	// Parse the gas mixes.
	printf("Parsing the gas mixes.\n");
	unsigned int ngases = 0;
	rc = parser_get_field(parser, FIELD_TYPE_GASMIX_COUNT, 0, &ngases);
	if (rc != PARSER_STATUS_SUCCESS && rc != PARSER_STATUS_UNSUPPORTED) {
		fprintf(stderr, "Error parsing the gas mix count.");
		parser_destroy(parser);
		return rc;
	}

	rc = parse_gasmixes(dive, parser, ngases);
	if (rc != PARSER_STATUS_SUCCESS) {
		fprintf(stderr, "Error parsing the gas mix.");
		parser_destroy(parser);
		return rc;
	}

	// Initialize the sample data.
	rc = parse_samples(&dive, parser);
	if (rc != PARSER_STATUS_SUCCESS) {
		fprintf(stderr, "Error parsing the samples.");
		parser_destroy(parser);
		return rc;
	}

	parser_destroy(parser);

	/* If we already saw this dive, abort. */
	if (find_dive(dive, devdata))
		return 0;

	record_dive(dive);
	return 1;
}
Пример #20
0
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;
	dc_datetime_t dt = {0};
	struct tm tm;
	struct dive *dive;

	rc = create_parser(devdata, &parser);
	if (rc != DC_STATUS_SUCCESS) {
		dev_info(devdata, _("Unable to create parser for %s %s"), devdata->vendor, devdata->product);
		return rc;
	}

	rc = dc_parser_set_data(parser, data, size);
	if (rc != DC_STATUS_SUCCESS) {
		dev_info(devdata, _("Error registering the data"));
		dc_parser_destroy(parser);
		return rc;
	}

	import_dive_number++;
	dive = alloc_dive();
	rc = dc_parser_get_datetime(parser, &dt);
	if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) {
		dev_info(devdata, _("Error parsing the datetime"));
		dc_parser_destroy(parser);
		return rc;
	}

	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 = utc_mktime(&tm);

	// Parse the divetime.
	dev_info(devdata, _("Dive %d: %s %d %04d"), import_dive_number,
		monthname(tm.tm_mon), tm.tm_mday, year(tm.tm_year));
	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, _("Error parsing the divetime"));
		dc_parser_destroy(parser);
		return rc;
	}
	dive->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, _("Error parsing the maxdepth"));
		dc_parser_destroy(parser);
		return rc;
	}
	dive->maxdepth.mm = maxdepth * 1000 + 0.5;

	// 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, _("Error parsing the gas mix count"));
		dc_parser_destroy(parser);
		return rc;
	}

	// Check if the libdivecomputer version already supports salinity
	double salinity = 1.03;
#ifdef DC_FIELD_SALINITY
	rc = dc_parser_get_field(parser, DC_FIELD_SALINITY, 0, &salinity);
	if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) {
		dev_info(devdata, _("Error obtaining water salinity"));
		dc_parser_destroy(parser);
		return rc;
	}
#endif
	dive->salinity = salinity * 10000.0 + 0.5;

	rc = parse_gasmixes(devdata, dive, parser, ngases);
	if (rc != DC_STATUS_SUCCESS) {
		dev_info(devdata, _("Error parsing the gas mix"));
		dc_parser_destroy(parser);
		return rc;
	}

	// Initialize the sample data.
	rc = parse_samples(devdata, &dive, parser);
	if (rc != DC_STATUS_SUCCESS) {
		dev_info(devdata, _("Error parsing the samples"));
		dc_parser_destroy(parser);
		return rc;
	}

	dc_parser_destroy(parser);

	/* If we already saw this dive, abort. */
	if (!devdata->force_download && find_dive(dive, devdata))
		return 0;

	dive->downloaded = TRUE;
	record_dive(dive);
	mark_divelist_changed(TRUE);
	return 1;
}