550 lines
16 KiB
C
550 lines
16 KiB
C
/*
|
|
* libdivecomputer
|
|
*
|
|
* Copyright (C) 2018 Jef Driesen
|
|
*
|
|
* 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 <string.h> // memcmp, memcpy
|
|
#include <stdlib.h> // malloc, free
|
|
|
|
#include "tecdiving_divecomputereu.h"
|
|
#include "context-private.h"
|
|
#include "device-private.h"
|
|
#include "array.h"
|
|
|
|
#define ISINSTANCE(device) dc_device_isinstance((device), &tecdiving_divecomputereu_device_vtable)
|
|
|
|
#define MAXRETRIES 14
|
|
|
|
#define STX 0x7E
|
|
|
|
#define CMD_INIT 0x53
|
|
#define CMD_LIST 0x57
|
|
#define CMD_DIVE 0x58
|
|
#define CMD_EXIT 0x59
|
|
|
|
#define RSP_INIT 0x56
|
|
#define RSP_LIST CMD_LIST
|
|
#define RSP_HEADER 0x51
|
|
#define RSP_PROFILE 0x52
|
|
|
|
#define SZ_MAXCMD 2
|
|
#define SZ_SUMMARY 7
|
|
#define SZ_SAMPLE 8
|
|
#define SZ_INIT 56
|
|
#define SZ_LIST (2 + 0x10000 * SZ_SUMMARY)
|
|
#define SZ_HEADER 100
|
|
#define SZ_PROFILE (1000 * SZ_SAMPLE)
|
|
|
|
#define NSTEPS 1000
|
|
#define STEP(i,n) (NSTEPS * (i) / (n))
|
|
|
|
typedef struct tecdiving_divecomputereu_device_t {
|
|
dc_device_t base;
|
|
dc_iostream_t *iostream;
|
|
unsigned char fingerprint[SZ_SUMMARY];
|
|
unsigned char version[SZ_INIT];
|
|
} tecdiving_divecomputereu_device_t;
|
|
|
|
static dc_status_t tecdiving_divecomputereu_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size);
|
|
static dc_status_t tecdiving_divecomputereu_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata);
|
|
static dc_status_t tecdiving_divecomputereu_device_close (dc_device_t *abstract);
|
|
|
|
static const dc_device_vtable_t tecdiving_divecomputereu_device_vtable = {
|
|
sizeof(tecdiving_divecomputereu_device_t),
|
|
DC_FAMILY_TECDIVING_DIVECOMPUTEREU,
|
|
tecdiving_divecomputereu_device_set_fingerprint, /* set_fingerprint */
|
|
NULL, /* read */
|
|
NULL, /* write */
|
|
NULL, /* dump */
|
|
tecdiving_divecomputereu_device_foreach, /* foreach */
|
|
NULL, /* timesync */
|
|
tecdiving_divecomputereu_device_close, /* close */
|
|
};
|
|
|
|
static unsigned short
|
|
checksum_crc (const unsigned char data[], unsigned int size, unsigned short init)
|
|
{
|
|
unsigned short crc = init;
|
|
for (unsigned int i = 0; i < size; ++i) {
|
|
crc ^= data[i] << 8;
|
|
if (crc & 0x8000) {
|
|
crc <<= 1;
|
|
crc ^= 0x1021;
|
|
} else {
|
|
crc <<= 1;
|
|
}
|
|
}
|
|
|
|
return crc;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_send (tecdiving_divecomputereu_device_t *device, unsigned char cmd, const unsigned char data[], size_t size)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
dc_device_t *abstract = (dc_device_t *) device;
|
|
unsigned short crc = 0;
|
|
|
|
if (device_is_cancelled (abstract))
|
|
return DC_STATUS_CANCELLED;
|
|
|
|
if (size > SZ_MAXCMD)
|
|
return DC_STATUS_INVALIDARGS;
|
|
|
|
// Setup the data packet
|
|
unsigned char packet[SZ_MAXCMD + 11] = {
|
|
STX,
|
|
0x00,
|
|
(size >> 0) & 0xFF,
|
|
(size >> 8) & 0xFF,
|
|
(size >> 16) & 0xFF,
|
|
(size >> 24) & 0xFF,
|
|
cmd,
|
|
};
|
|
if (size) {
|
|
memcpy(packet + 7, data, size);
|
|
}
|
|
crc = checksum_crc (packet + 1, size + 6, 0);
|
|
packet[size + 7] = (crc >> 8) & 0xFF;
|
|
packet[size + 8] = (crc ) & 0xFF;
|
|
packet[size + 9] = 0x00;
|
|
packet[size + 10] = 0x00;
|
|
|
|
// Give the dive computer some extra time.
|
|
dc_iostream_sleep (device->iostream, 300);
|
|
|
|
// Send the data packet.
|
|
status = dc_iostream_write (device->iostream, packet, size + 11, NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to send the command.");
|
|
return status;
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_receive (tecdiving_divecomputereu_device_t *device, unsigned char rsp, unsigned char data[], size_t size, size_t *actual)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
dc_device_t *abstract = (dc_device_t *) device;
|
|
unsigned char header[7];
|
|
unsigned int nretries = 0;
|
|
|
|
// Read the packet start byte.
|
|
// Unfortunately it takes a relative long time, about 6-8 seconds,
|
|
// before the STX byte arrives. Hence the standard timeout of one
|
|
// second is not sufficient, and we need to retry a few times on
|
|
// timeout. The advantage over using a single read operation with a
|
|
// large timeout is that we can give the user a chance to cancel the
|
|
// operation.
|
|
while (1) {
|
|
status = dc_iostream_read (device->iostream, header + 0, 1, NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
if (status != DC_STATUS_TIMEOUT) {
|
|
ERROR (abstract->context, "Failed to receive the packet start byte.");
|
|
return status;
|
|
}
|
|
|
|
// Abort if the maximum number of retries is reached.
|
|
if (nretries++ >= MAXRETRIES)
|
|
return status;
|
|
|
|
// Cancel if requested by the user.
|
|
if (device_is_cancelled (abstract))
|
|
return DC_STATUS_CANCELLED;
|
|
|
|
// Try again.
|
|
continue;
|
|
}
|
|
|
|
if (header[0] == STX)
|
|
break;
|
|
|
|
// Reset the retry counter.
|
|
nretries = 0;
|
|
}
|
|
|
|
// Read the packet header.
|
|
status = dc_iostream_read (device->iostream, header + 1, sizeof(header) - 1, NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the packet header.");
|
|
return status;
|
|
}
|
|
|
|
// Verify the type byte.
|
|
unsigned int type = header[1];
|
|
if (type != 0x00) {
|
|
ERROR (abstract->context, "Unexpected type byte (%02x).", type);
|
|
return DC_STATUS_PROTOCOL;
|
|
}
|
|
|
|
// Verify the length.
|
|
unsigned int length = array_uint32_le (header + 2);
|
|
if (length > size) {
|
|
ERROR (abstract->context, "Unexpected packet length (%u).", length);
|
|
return DC_STATUS_PROTOCOL;
|
|
}
|
|
|
|
// Get the command type.
|
|
unsigned int cmd = header[6];
|
|
if (cmd != rsp) {
|
|
ERROR (abstract->context, "Unexpected command byte (%02x).", cmd);
|
|
return DC_STATUS_PROTOCOL;
|
|
}
|
|
|
|
size_t nbytes = 0;
|
|
while (nbytes < length) {
|
|
// Set the maximum packet size.
|
|
size_t len = 1000;
|
|
|
|
// Limit the packet size to the total size.
|
|
if (nbytes + len > length)
|
|
len = length - nbytes;
|
|
|
|
// Read the packet payload.
|
|
status = dc_iostream_read (device->iostream, data + nbytes, len, NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the packet payload.");
|
|
return status;
|
|
}
|
|
|
|
nbytes += len;
|
|
}
|
|
|
|
// Read the packet checksum.
|
|
unsigned char checksum[4];
|
|
status = dc_iostream_read (device->iostream, checksum, sizeof(checksum), NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the packet checksum.");
|
|
return status;
|
|
}
|
|
|
|
// Verify the checksum.
|
|
unsigned short crc = array_uint16_be (checksum);
|
|
unsigned short ccrc = 0;
|
|
ccrc = checksum_crc (header + 1, sizeof(header) - 1, ccrc);
|
|
ccrc = checksum_crc (data, length, ccrc);
|
|
if (crc != ccrc || checksum[2] != 0x00 || checksum[3] != 0) {
|
|
ERROR (abstract->context, "Unexpected packet checksum.");
|
|
return DC_STATUS_PROTOCOL;
|
|
}
|
|
|
|
if (actual == NULL) {
|
|
// Verify the actual length.
|
|
if (length != size) {
|
|
ERROR (abstract->context, "Unexpected packet length (%u).", length);
|
|
return DC_STATUS_PROTOCOL;
|
|
}
|
|
} else {
|
|
// Return the actual length.
|
|
*actual = length;
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_readdive (dc_device_t *abstract, dc_event_progress_t *progress, unsigned int idx, dc_buffer_t *buffer)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
tecdiving_divecomputereu_device_t *device = (tecdiving_divecomputereu_device_t *) abstract;
|
|
|
|
// Erase the buffer.
|
|
dc_buffer_clear (buffer);
|
|
|
|
// Encode the one based logbook ID.
|
|
unsigned int number = idx + 1;
|
|
unsigned char id[] = {
|
|
(number >> 8) & 0xFF,
|
|
(number ) & 0xFF,
|
|
};
|
|
|
|
// Request the dive.
|
|
status = tecdiving_divecomputereu_send (device, CMD_DIVE, id, sizeof(id));
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to send the dive command.");
|
|
return status;
|
|
}
|
|
|
|
// Read the dive header.
|
|
unsigned char header[SZ_HEADER];
|
|
status = tecdiving_divecomputereu_receive (device, RSP_HEADER, header, sizeof(header), NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the dive header.");
|
|
return status;
|
|
}
|
|
|
|
// Get the number of samples.
|
|
unsigned int nsamples = array_uint32_be (header + 36);
|
|
|
|
// Calculate the total size.
|
|
unsigned int size = sizeof(header) + nsamples * SZ_SAMPLE;
|
|
|
|
// Update and emit a progress event.
|
|
if (progress) {
|
|
progress->current = (idx + 1) * NSTEPS + STEP(sizeof(header), size);
|
|
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
|
|
}
|
|
|
|
// Allocate memory for the dive.
|
|
if (!dc_buffer_resize (buffer, size)) {
|
|
ERROR (abstract->context, "Insufficient buffer space available.");
|
|
return DC_STATUS_NOMEMORY;
|
|
}
|
|
|
|
// Cache the pointer.
|
|
unsigned char *data = dc_buffer_get_data(buffer);
|
|
|
|
// Append the header.
|
|
memcpy (data, header, sizeof(header));
|
|
|
|
unsigned int nbytes = sizeof(header);
|
|
while (nbytes < size) {
|
|
// Get the packet size. The maximum size for a single data
|
|
// packet is 1000 samples.
|
|
unsigned int len = size - nbytes;
|
|
if (len > SZ_PROFILE)
|
|
len = SZ_PROFILE;
|
|
|
|
// Read the dive samples.
|
|
status = tecdiving_divecomputereu_receive (device, RSP_PROFILE, data + nbytes, len, NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the dive samples.");
|
|
return status;
|
|
}
|
|
|
|
nbytes += len;
|
|
|
|
// Update and emit a progress event.
|
|
if (progress) {
|
|
progress->current = (idx + 1) * NSTEPS + STEP(nbytes, size);
|
|
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
|
|
}
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
dc_status_t
|
|
tecdiving_divecomputereu_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
tecdiving_divecomputereu_device_t *device = NULL;
|
|
|
|
if (out == NULL)
|
|
return DC_STATUS_INVALIDARGS;
|
|
|
|
// Allocate memory.
|
|
device = (tecdiving_divecomputereu_device_t *) dc_device_allocate (context, &tecdiving_divecomputereu_device_vtable);
|
|
if (device == NULL) {
|
|
ERROR (context, "Failed to allocate memory.");
|
|
return DC_STATUS_NOMEMORY;
|
|
}
|
|
|
|
// Set the default values.
|
|
device->iostream = iostream;
|
|
memset (device->fingerprint, 0, sizeof (device->fingerprint));
|
|
|
|
// Set the serial communication protocol (115200 8N1).
|
|
status = dc_iostream_configure (device->iostream, 115200, 8, DC_PARITY_NONE, DC_STOPBITS_ONE, DC_FLOWCONTROL_NONE);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (context, "Failed to set the terminal attributes.");
|
|
goto error_free;
|
|
}
|
|
|
|
// Set the timeout for receiving data (1000ms).
|
|
status = dc_iostream_set_timeout (device->iostream, 1000);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (context, "Failed to set the timeout.");
|
|
goto error_free;
|
|
}
|
|
|
|
// Make sure everything is in a sane state.
|
|
dc_iostream_sleep (device->iostream, 100);
|
|
dc_iostream_purge (device->iostream, DC_DIRECTION_ALL);
|
|
|
|
// Send the init command.
|
|
status = tecdiving_divecomputereu_send (device, CMD_INIT, NULL, 0);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (context, "Failed to send the init command.");
|
|
goto error_free;
|
|
}
|
|
|
|
// Read the device info.
|
|
status = tecdiving_divecomputereu_receive (device, RSP_INIT, device->version, sizeof(device->version), NULL);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (context, "Failed to receive the device info.");
|
|
goto error_free;
|
|
}
|
|
|
|
*out = (dc_device_t *) device;
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
|
|
error_free:
|
|
dc_device_deallocate ((dc_device_t *) device);
|
|
return status;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_device_close (dc_device_t *abstract)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
tecdiving_divecomputereu_device_t *device = (tecdiving_divecomputereu_device_t *) abstract;
|
|
|
|
status = tecdiving_divecomputereu_send (device, CMD_EXIT, NULL, 0);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to send the exit command.");
|
|
return status;
|
|
}
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size)
|
|
{
|
|
tecdiving_divecomputereu_device_t *device = (tecdiving_divecomputereu_device_t *) abstract;
|
|
|
|
if (size && size != sizeof (device->fingerprint))
|
|
return DC_STATUS_INVALIDARGS;
|
|
|
|
if (size)
|
|
memcpy (device->fingerprint, data, sizeof (device->fingerprint));
|
|
else
|
|
memset (device->fingerprint, 0, sizeof (device->fingerprint));
|
|
|
|
return DC_STATUS_SUCCESS;
|
|
}
|
|
|
|
static dc_status_t
|
|
tecdiving_divecomputereu_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata)
|
|
{
|
|
dc_status_t status = DC_STATUS_SUCCESS;
|
|
tecdiving_divecomputereu_device_t *device = (tecdiving_divecomputereu_device_t *) abstract;
|
|
|
|
// Enable progress notifications.
|
|
dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
|
|
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
|
|
|
// Emit a device info event.
|
|
dc_event_devinfo_t devinfo;
|
|
devinfo.model = 0;
|
|
devinfo.firmware = 0;
|
|
devinfo.serial = array_uint16_be (device->version + 0x22) << 16 | array_uint16_be (device->version + 0x26);
|
|
device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo);
|
|
|
|
// Emit a vendor event.
|
|
dc_event_vendor_t vendor;
|
|
vendor.data = device->version;
|
|
vendor.size = sizeof(device->version);
|
|
device_event_emit (abstract, DC_EVENT_VENDOR, &vendor);
|
|
|
|
// Allocate memory for the dive list.
|
|
size_t length = SZ_LIST;
|
|
unsigned char *logbook = (unsigned char *) malloc (length);
|
|
if (logbook == NULL) {
|
|
status = DC_STATUS_NOMEMORY;
|
|
goto error_exit;
|
|
}
|
|
|
|
// Request the dive list.
|
|
status = tecdiving_divecomputereu_send (device, CMD_LIST, NULL, 0);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to send the list command.");
|
|
goto error_logbook_free;
|
|
}
|
|
|
|
// Read the dive list.
|
|
status = tecdiving_divecomputereu_receive (device, RSP_LIST, logbook, length, &length);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
ERROR (abstract->context, "Failed to receive the logbook.");
|
|
goto error_logbook_free;
|
|
}
|
|
|
|
// Verify the minimum length.
|
|
if (length < 2) {
|
|
status = DC_STATUS_DATAFORMAT;
|
|
goto error_logbook_free;
|
|
}
|
|
|
|
// Get the number of logbook entries.
|
|
unsigned int nlogbooks = array_uint16_be (logbook);
|
|
if (length != 2 + nlogbooks * SZ_SUMMARY) {
|
|
status = DC_STATUS_DATAFORMAT;
|
|
goto error_logbook_free;
|
|
}
|
|
|
|
// Count the number of dives to download.
|
|
unsigned int ndives = 0;
|
|
for (unsigned int i = 0; i < nlogbooks; ++i) {
|
|
unsigned int offset = 2 + i * SZ_SUMMARY;
|
|
|
|
if (memcmp(logbook + offset, device->fingerprint, sizeof(device->fingerprint)) == 0)
|
|
break;
|
|
|
|
ndives++;
|
|
}
|
|
|
|
// Update and emit a progress event.
|
|
progress.current = 1 * NSTEPS;
|
|
progress.maximum = (ndives + 1) * NSTEPS;
|
|
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
|
|
|
// Allocate a memory buffer for a single dive.
|
|
dc_buffer_t *buffer = dc_buffer_new(0);
|
|
if (buffer == NULL) {
|
|
status = DC_STATUS_NOMEMORY;
|
|
goto error_logbook_free;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < ndives; ++i) {
|
|
unsigned int offset = 2 + i * SZ_SUMMARY;
|
|
|
|
// Read the dive.
|
|
status = tecdiving_divecomputereu_readdive (abstract, &progress, i, buffer);
|
|
if (status != DC_STATUS_SUCCESS) {
|
|
goto error_buffer_free;
|
|
}
|
|
|
|
// Cache the pointer.
|
|
unsigned char *data = dc_buffer_get_data(buffer);
|
|
unsigned int size = dc_buffer_get_size(buffer);
|
|
|
|
// Verify the logbook entry.
|
|
if (memcmp (data, logbook + offset, SZ_SUMMARY) != 0) {
|
|
ERROR (abstract->context, "Dive header doesn't match logbook entry.");
|
|
status = DC_STATUS_DATAFORMAT;
|
|
goto error_buffer_free;
|
|
}
|
|
|
|
if (callback && !callback (data, size, data, sizeof(device->fingerprint), userdata)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
error_buffer_free:
|
|
dc_buffer_free (buffer);
|
|
error_logbook_free:
|
|
free (logbook);
|
|
error_exit:
|
|
return status;
|
|
}
|