diff --git a/examples/universal.c b/examples/universal.c index 2b0fc81..56b1946 100644 --- a/examples/universal.c +++ b/examples/universal.c @@ -78,6 +78,7 @@ static const backend_table_t g_backends[] = { {"vyper", DC_FAMILY_SUUNTO_VYPER}, {"vyper2", DC_FAMILY_SUUNTO_VYPER2}, {"d9", DC_FAMILY_SUUNTO_D9}, + {"eonsteel", DC_FAMILY_SUUNTO_EONSTEEL}, {"aladin", DC_FAMILY_UWATEC_ALADIN}, {"memomouse", DC_FAMILY_UWATEC_MEMOMOUSE}, {"smart", DC_FAMILY_UWATEC_SMART}, diff --git a/include/libdivecomputer/common.h b/include/libdivecomputer/common.h index 983ec07..4bcc700 100644 --- a/include/libdivecomputer/common.h +++ b/include/libdivecomputer/common.h @@ -49,6 +49,7 @@ typedef enum dc_family_t { DC_FAMILY_SUUNTO_VYPER, DC_FAMILY_SUUNTO_VYPER2, DC_FAMILY_SUUNTO_D9, + DC_FAMILY_SUUNTO_EONSTEEL, /* Reefnet */ DC_FAMILY_REEFNET_SENSUS = (2 << 16), DC_FAMILY_REEFNET_SENSUSPRO, diff --git a/include/libdivecomputer/suunto.h b/include/libdivecomputer/suunto.h index 337dffd..3ff7c88 100644 --- a/include/libdivecomputer/suunto.h +++ b/include/libdivecomputer/suunto.h @@ -27,5 +27,6 @@ #include "suunto_vyper.h" #include "suunto_vyper2.h" #include "suunto_d9.h" +#include "suunto_eonsteel.h" #endif /* SUUNTO_H */ diff --git a/include/libdivecomputer/suunto_eonsteel.h b/include/libdivecomputer/suunto_eonsteel.h new file mode 100644 index 0000000..d12387c --- /dev/null +++ b/include/libdivecomputer/suunto_eonsteel.h @@ -0,0 +1,39 @@ +/* + * libdivecomputer + * + * Copyright (C) 2014 Linus Torvalds + * + * 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 + */ + +#ifndef EON_STEEL_H +#define EON_STEEL_H + +#include "context.h" +#include "device.h" +#include "parser.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +dc_status_t suunto_eonsteel_device_open(dc_device_t **device, dc_context_t *context, const char *name, unsigned int model); +dc_status_t suunto_eonsteel_parser_create(dc_parser_t **parser, dc_context_t *context, unsigned int model); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* EON_STEEL_H */ diff --git a/msvc/libdivecomputer.vcproj b/msvc/libdivecomputer.vcproj index 78a8190..ff22d90 100644 --- a/msvc/libdivecomputer.vcproj +++ b/msvc/libdivecomputer.vcproj @@ -402,6 +402,14 @@ RelativePath="..\src\suunto_eon_parser.c" > + + + + diff --git a/src/Makefile.am b/src/Makefile.am index 595f0c7..458f1dd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -24,6 +24,7 @@ libdivecomputer_la_SOURCES = \ suunto_vyper.c suunto_vyper_parser.c \ suunto_vyper2.c \ suunto_d9.c suunto_d9_parser.c \ + suunto_eonsteel.c suunto_eonsteel_parser.c \ reefnet_sensus.c reefnet_sensus_parser.c \ reefnet_sensuspro.c reefnet_sensuspro_parser.c \ reefnet_sensusultra.c reefnet_sensusultra_parser.c \ diff --git a/src/descriptor.c b/src/descriptor.c index fba9874..22ebe7c 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -77,6 +77,10 @@ static const dc_descriptor_t g_descriptors[] = { {"Suunto", "D6i", DC_FAMILY_SUUNTO_D9, 0x1A}, {"Suunto", "D9tx", DC_FAMILY_SUUNTO_D9, 0x1B}, {"Suunto", "DX", DC_FAMILY_SUUNTO_D9, 0x1C}, + /* Suunto EON Steel */ +#ifdef HAVE_LIBUSB + {"Suunto", "EON Steel", DC_FAMILY_SUUNTO_EONSTEEL, 0}, +#endif /* Uwatec Aladin */ {"Uwatec", "Aladin Air Twin", DC_FAMILY_UWATEC_ALADIN, 0x1C}, {"Uwatec", "Aladin Sport Plus", DC_FAMILY_UWATEC_ALADIN, 0x3E}, @@ -340,6 +344,8 @@ dc_descriptor_get_transport (dc_descriptor_t *descriptor) if (descriptor->type == DC_FAMILY_ATOMICS_COBALT) return DC_TRANSPORT_USB; + else if (descriptor->type == DC_FAMILY_SUUNTO_EONSTEEL) + return DC_TRANSPORT_USB; else if (descriptor->type == DC_FAMILY_UWATEC_SMART) return DC_TRANSPORT_IRDA; else diff --git a/src/device.c b/src/device.c index 9fad7a2..edc1cf2 100644 --- a/src/device.c +++ b/src/device.c @@ -82,6 +82,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr case DC_FAMILY_SUUNTO_D9: rc = suunto_d9_device_open (&device, context, name, dc_descriptor_get_model (descriptor)); break; + case DC_FAMILY_SUUNTO_EONSTEEL: + rc = suunto_eonsteel_device_open (&device, context, name, dc_descriptor_get_model (descriptor)); + break; case DC_FAMILY_UWATEC_ALADIN: rc = uwatec_aladin_device_open (&device, context, name); break; diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols index 14a96a8..55852ae 100644 --- a/src/libdivecomputer.symbols +++ b/src/libdivecomputer.symbols @@ -53,6 +53,7 @@ suunto_vyper_parser_create suunto_solution_parser_create suunto_eon_parser_create suunto_d9_parser_create +suunto_eonsteel_parser_create mares_nemo_parser_create mares_darwin_parser_create mares_iconhd_parser_create @@ -127,6 +128,7 @@ suunto_vyper2_device_reset_maxdepth suunto_vyper_device_open suunto_vyper_device_read_dive suunto_vyper_extract_dives +suunto_eonsteel_device_open uwatec_aladin_device_open uwatec_aladin_extract_dives uwatec_memomouse_device_open diff --git a/src/parser.c b/src/parser.c index 7cc7f4e..a17a586 100644 --- a/src/parser.c +++ b/src/parser.c @@ -66,6 +66,9 @@ dc_parser_new (dc_parser_t **out, dc_device_t *device) case DC_FAMILY_SUUNTO_D9: rc = suunto_d9_parser_create (&parser, context, device->devinfo.model); break; + case DC_FAMILY_SUUNTO_EONSTEEL: + rc = suunto_eonsteel_parser_create(&parser, context, device->devinfo.model); + break; case DC_FAMILY_UWATEC_ALADIN: case DC_FAMILY_UWATEC_MEMOMOUSE: rc = uwatec_memomouse_parser_create (&parser, context, device->clock.devtime, device->clock.systime); diff --git a/src/suunto_eonsteel.c b/src/suunto_eonsteel.c new file mode 100644 index 0000000..aeff089 --- /dev/null +++ b/src/suunto_eonsteel.c @@ -0,0 +1,613 @@ +/* + * libdivecomputer + * + * Copyright (C) 2014 Linus Torvalds + * + * 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 + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "context-private.h" +#include "device-private.h" +#include "array.h" + +#ifdef HAVE_LIBUSB + +#include + +struct eonsteel { + dc_device_t base; + + libusb_context *ctx; + libusb_device_handle *handle; + unsigned int magic; + unsigned short seq; +}; + +// The EON Steel implements a small filesystem +#define DIRTYPE_FILE 0x0001 +#define DIRTYPE_DIR 0x0002 + +struct directory_entry { + struct directory_entry *next; + int type; + int namelen; + char name[1]; +}; + +// EON Steel command numbers and other magic field values +#define INIT_CMD 0x00 +#define INIT_MAGIC 0x0001 +#define INIT_SEQ 0 +#define INIT_LEN 4 +#define INIT_DATA "\x02\x00\x2a\x00" + +#define READ_STRING_CMD 0x0411 + +#define FILE_LOOKUP_CMD 0x0010 +#define FILE_READ_CMD 0x0110 +#define FILE_STAT_CMD 0x0710 +#define FILE_CLOSE_CMD 0x0510 + +#define DIR_LOOKUP_CMD 0x0810 +#define READDIR_CMD 0x0910 +#define DIR_CLOSE_CMD 0x0a10 + +static const char *dive_directory = "0:/dives"; + +static struct directory_entry *alloc_dirent(int type, int len, const char *name) +{ + struct directory_entry *res; + + res = malloc(offsetof(struct directory_entry, name) + len + 1); + if (res) { + res->next = NULL; + res->type = type; + res->namelen = len; + memcpy(res->name, name, len); + res->name[len] = 0; + } + return res; +} + +static int report_error(const char *fmt, ...) +{ + char buffer[128]; + va_list args; + + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer)-1, fmt, args); + va_end(args); + fprintf(stderr, "%.*s\n", (int)sizeof(buffer), buffer); + return -1; +} + +static void put_le16(unsigned short val, void *dst) +{ + unsigned char *p = (unsigned char *)dst; + p[0] = val; + p[1] = val >> 8; +} + +static void put_le32(unsigned int val, void *dst) +{ + unsigned char *p = (unsigned char *)dst; + p[0] = val; + p[1] = val >> 8; + p[2] = val >> 16; + p[3] = val >> 24; +} + +static void debug(const char *name, const char *buf, int len) +{ + int i; + + fprintf(stderr, "%4d %s:", len, name); + for (i = 0; i < len; i++) + fprintf(stderr, " %02x", (unsigned char) buf[i]); + fprintf(stderr, "\n"); +} + +static void debug_text(const char *name, const char *buf, int len) +{ + int i; + + printf("text of %s:\n", name); + for (i = 0; i < len; i++) { + unsigned char c = buf[i]; + if (c > 31 && c < 127) + putchar(c); + else if (c == '\n') { + putchar('\\'); + putchar('n'); + } else { + static const char hex[16]="0123456789abcdef"; + putchar('\\'); + putchar('x'); + putchar(hex[c>>4]); + putchar(hex[c&15]); + } + } + printf("\nend of text\n"); +} + +static int receive_data(struct eonsteel *eon, unsigned char *buffer, int size) +{ + const int InEndpoint = 0x82; + unsigned char buf[64]; + int ret = 0; + + for (;;) { + int rc, transferred, len; + + rc = libusb_interrupt_transfer(eon->handle, InEndpoint, buf, sizeof(buf), &transferred, 5000); + if (rc || transferred != sizeof(buf)) + return report_error("incomplete read interrupt transfer"); +// dump every incoming packet? +// debug("rcv", buf, transferred); + if (buf[0] != 0x3f) + return report_error("read interrupt transfer returns wrong report type"); + len = buf[1]; + if (len > sizeof(buf)-2) + return report_error("read interrupt transfer reports short length"); + if (len > size) + return report_error("read interrupt transfer reports excessive length"); + memcpy(buffer+ret, buf+2, len); + size -= len; + ret += len; + if (len < sizeof(buf)-2) + break; + } + + return ret; +} + +static int send_cmd(struct eonsteel *eon, + unsigned short cmd, + unsigned int len, + const unsigned char *buffer) +{ + const int OutEndpoint = 0x02; + unsigned char buf[64]; + int transferred, rc; + unsigned short seq = eon->seq; + unsigned int magic = eon->magic; + + // Two-byte packet header, followed by 12 bytes of extended header + if (len > sizeof(buf)-2-12) + return report_error("send command with too much long"); + + memset(buf, 0, sizeof(buf)); + + buf[0] = 0x3f; + buf[1] = len + 12; + + // 2-byte LE command word + put_le16(cmd, buf+2); + + // 4-byte LE magic value (starts at 1) + put_le32(magic, buf+4); + + // 2-byte LE sequence number; + put_le16(seq, buf+8); + + // 4-byte LE length + put_le32(len, buf+10); + + // .. followed by actual data + memcpy(buf+14, buffer, len); + + rc = libusb_interrupt_transfer(eon->handle, OutEndpoint, buf, sizeof(buf), &transferred, 5000); + if (rc < 0) + return report_error("write interrupt transfer failed"); + +// dump every outgoing packet? +// debug("cmd", buf, sizeof(buf)); + return 0; +} + +/* + * Send a command, receive a reply + * + * This carefully checks the data fields in the reply for a match + * against the command, and then only returns the actual reply + * data itself. + * + * Also note that "receive_data()" itself will have removed the + * per-packet handshake bytes, so unlike "send_cmd()", this does + * not see the two initial 0x3f 0x?? bytes, and this the offsets + * for the cmd/magic/seq/len are off by two compared to the + * send_cmd() side. The offsets are the same in the actual raw + * packet. + */ +static int send_receive(struct eonsteel *eon, + unsigned short cmd, + unsigned int len_out, const unsigned char *out, + unsigned int len_in, unsigned char *in) +{ + int len, actual; + unsigned char buf[2048]; + + if (send_cmd(eon, cmd, len_out, out) < 0) + return -1; + len = receive_data(eon, buf, sizeof(buf)); + if (len < 10) + return report_error("short command reply (%d)", len); + if (array_uint16_le(buf) != cmd) + return report_error("command reply doesn't match command"); + if (array_uint32_le(buf+2) != eon->magic + 5) + return report_error("command reply doesn't match magic (got %08x, expected %08x)", array_uint32_le(buf+2), eon->magic + 5); + if (array_uint16_le(buf+6) != eon->seq) + return report_error("command reply doesn't match sequence number"); + actual = array_uint32_le(buf+8); + if (actual + 12 != len) + return report_error("command reply length mismatch (got %d, claimed %d)", len-12, actual); + if (len_in < actual) + return report_error("command reply returned too much data (got %d, had %d)", actual, len_in); + + // Successful command - increment sequence number + eon->seq++; + memcpy(in, buf+12, actual); + return actual; +} + +static int read_file(struct eonsteel *eon, const char *filename, dc_buffer_t *buf) +{ + unsigned char result[2560]; + unsigned char cmdbuf[64]; + unsigned int size, offset; + int rc, len; + + memset(cmdbuf, 0, sizeof(cmdbuf)); + len = strlen(filename) + 1; + if (len + 4 > sizeof(cmdbuf)) + return report_error("too long filename: %s", filename); + memcpy(cmdbuf+4, filename, len); + rc = send_receive(eon, FILE_LOOKUP_CMD, + len+4, cmdbuf, + sizeof(result), result); + if (rc < 0) + return report_error("unable to look up %s", filename); +// debug("lookup", result, rc); + + rc = send_receive(eon, FILE_STAT_CMD, + 0, "", + sizeof(result), result); + if (rc < 0) + return report_error("unable to stat %s", filename); +// debug("stat", result, rc); + + size = array_uint32_le(result+4); + offset = 0; + + while (size > 0) { + unsigned int ask, got, at; + + ask = size; + if (ask > 1024) + ask = 1024; + put_le32(1234, cmdbuf+0); // Not file offset, after all + put_le32(ask, cmdbuf+4); // Size of read + rc = send_receive(eon, FILE_READ_CMD, + 8, cmdbuf, + sizeof(result), result); + if (rc < 0) + return report_error("unable to read %s", filename); + if (rc < 8) + return report_error("got short read reply for %s", filename); + + // Not file offset, just stays unmodified. + at = array_uint32_le(result); + if (at != 1234) + return report_error("read of %s returned different offset than asked for (%d vs %d)", filename, at, offset); + + // Number of bytes actually read + got = array_uint32_le(result+4); + if (!got) + break; + if (rc < 8 + got) + return report_error("odd read size reply for offset %d of file %s", offset, filename); + + if (got > size) + got = size; + dc_buffer_append(buf, result+8, got); + offset += got; + size -= got; + } + + rc = send_receive(eon, FILE_CLOSE_CMD, + 0, "", + sizeof(result), result); + if (rc < 0) + report_error("cmd 0510 failed"); +// debug("close", result, rc); + + return offset; +} + +/* + * NOTE! This will create the list of dirent's in reverse order, + * with the last dirent first. That's intentional: for dives, + * we will want to look up the last dive first. + */ +static struct directory_entry *parse_dirent(int nr, const unsigned char *p, int len, struct directory_entry *old) +{ + while (len > 8) { + unsigned int type = array_uint32_le(p); + unsigned int namelen = array_uint32_le(p+4); + const unsigned char *name = p+8; + struct directory_entry *entry; + + if (namelen + 8 + 1 > len || name[namelen] != 0) { + report_error("corrupt dirent entry"); + break; + } +// debug("dir entry", p, 8); + p += 8 + namelen + 1; + len -= 8 + namelen + 1; + entry = alloc_dirent(type, namelen, name); + entry->next = old; + old = entry; + } + return old; +} + +static int get_file_list(struct eonsteel *eon, struct directory_entry **res) +{ + struct directory_entry *de = NULL; + unsigned char cmd[64]; + unsigned char result[2048]; + int rc, cmdlen; + + + *res = NULL; + put_le32(0, cmd); + strcpy(cmd+4, dive_directory); + cmdlen = 4+strlen(dive_directory)+1; + rc = send_receive(eon, DIR_LOOKUP_CMD, + cmdlen, cmd, + sizeof(result), result); + if (rc < 0) + report_error("cmd DIR_LOOKUP failed"); +// debug("DIR_LOOKUP", result, rc); + + for (;;) { + unsigned int nr, last; + + rc = send_receive(eon, READDIR_CMD, + 0, "", + sizeof(result), result); + if (rc < 0) + return report_error("readdir failed"); + if (rc < 8) + return report_error("short readdir result"); + nr = array_uint32_le(result); + last = array_uint32_le(result+4); +// debug("dir packet", result, 8); + + de = parse_dirent(nr, result+8, rc-8, de); + if (last) + break; + } + + rc = send_receive(eon, DIR_CLOSE_CMD, + 0, "", + sizeof(result), result); + if (rc < 0) + report_error("dir close failed"); + + *res = de; + return 0; +} + + +static dc_status_t eonsteel_device_foreach(dc_device_t *abstract, dc_dive_callback_t callback, void *userdata); +static dc_status_t eonsteel_device_close(dc_device_t *abstract); + +static const dc_device_vtable_t eonsteel_device_vtable = { + DC_FAMILY_SUUNTO_EONSTEEL, + NULL, /* set_fingerprint */ + NULL, /* read */ + NULL, /* write */ + NULL, /* dump */ + eonsteel_device_foreach, /* foreach */ + eonsteel_device_close /* close */ +}; + +static int initialize_eonsteel(struct eonsteel *eon) +{ + const int InEndpoint = 0x82; + unsigned char buf[64]; + + /* Get rid of any pending stale input first */ + for (;;) { + int transferred; + + int rc = libusb_interrupt_transfer(eon->handle, InEndpoint, buf, sizeof(buf), &transferred, 10); + if (rc < 0) + break; + if (!transferred) + break; + } + + if (send_cmd(eon, INIT_CMD, INIT_LEN, INIT_DATA)) + return report_error("Failed to send initialization command"); + if (receive_data(eon, buf, sizeof(buf)) < 0) + return report_error("Failed to receive initial reply"); + + // Don't ask + eon->magic = 0x00000005 | (buf[4] << 16) | (buf[5] << 24); + // Increment the sequence number for every command sent + eon->seq++; + return 0; +} + +dc_status_t suunto_eonsteel_device_open(dc_device_t **out, dc_context_t *context, const char *name, unsigned int model) +{ + struct eonsteel *eon; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + eon = calloc(1, sizeof(struct eonsteel)); + if (!eon) + return DC_STATUS_NOMEMORY; + + // Set up the magic handshake fields + eon->magic = INIT_MAGIC; + eon->seq = INIT_SEQ; + + // Set up the libdivecomputer interfaces + device_init(&eon->base, context, &eonsteel_device_vtable); + + if (libusb_init(&eon->ctx)) { + ERROR(context, "libusb_init() failed"); + return DC_STATUS_IO; + } + + eon->handle = libusb_open_device_with_vid_pid(eon->ctx, 0x1493, 0x0030); + if (!eon->handle) { + ERROR(context, "unable to open EON Steel device (%s)", strerror(errno)); + libusb_exit(eon->ctx); + return DC_STATUS_IO; + } + + libusb_set_auto_detach_kernel_driver(eon->handle, 1); + libusb_claim_interface(eon->handle, 0); + + if (initialize_eonsteel(eon) < 0) { + ERROR(context, "unable to initialize EON Steel device (%s)", strerror(errno)); + libusb_exit(eon->ctx); + return DC_STATUS_IO; + } + + *out = (dc_device_t *) eon; + + return DC_STATUS_SUCCESS; +} + +static int count_dir_entries(struct directory_entry *de) +{ + int count = 0; + while (de) { + count++; + de = de->next; + } + return count; +} + +static dc_status_t eonsteel_device_foreach(dc_device_t *abstract, dc_dive_callback_t callback, void *userdata) +{ + int skip = 0, rc; + struct directory_entry *de; + struct eonsteel *eon = (struct eonsteel *) abstract; + dc_buffer_t *file; + char pathname[64]; + unsigned int time; + dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER; + + if (get_file_list(eon, &de) < 0) + return DC_STATUS_IO; + + file = dc_buffer_new(0); + progress.maximum = count_dir_entries(de); + progress.current = 0; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + while (de) { + int len; + struct directory_entry *next = de->next; + unsigned char buf[4]; + + if (device_is_cancelled(abstract)) + skip = 1; + + switch (de->type) { + case DIRTYPE_DIR: + /* Ignore subdirectories in the dive directory */ + break; + case DIRTYPE_FILE: + if (skip) + break; + if (sscanf(de->name, "%x.LOG", &time) != 1) + break; + len = snprintf(pathname, sizeof(pathname), "%s/%s", dive_directory, de->name); + if (len >= sizeof(pathname)) + break; + + // Reset the membuffer, put the 4-byte length at the head. + dc_buffer_clear(file); + put_le32(time, buf); + dc_buffer_append(file, buf, 4); + + // Then read the filename into the rest of the buffer + rc = read_file(eon, pathname, file); + if (rc < 0) + break; + if (!callback) + break; + if (!callback(dc_buffer_get_data(file), dc_buffer_get_size(file), NULL, 0, userdata)) + skip = 1; + + // We've used up the buffer, so create a new one + file = dc_buffer_new(0); + } + progress.current++; + device_event_emit(abstract, DC_EVENT_PROGRESS, &progress); + + free(de); + de = next; + } + dc_buffer_free(file); + + return device_is_cancelled(abstract) ? DC_STATUS_CANCELLED : DC_STATUS_SUCCESS; +} + +static dc_status_t eonsteel_device_close(dc_device_t *abstract) +{ + struct eonsteel *eon = (struct eonsteel *) abstract; + + libusb_close(eon->handle); + libusb_exit(eon->ctx); + free(eon); + + return DC_STATUS_SUCCESS; +} + +#else // no LIBUSB support + +dc_status_t suunto_eonsteel_device_open(dc_device_t **out, dc_context_t *context, const char *name, unsigned int model) +{ + ERROR(context, "The Suunto EON Steel backend needs libusb-1.0"); + return DC_STATUS_UNSUPPORTED; +} + +#endif diff --git a/src/suunto_eonsteel_parser.c b/src/suunto_eonsteel_parser.c new file mode 100644 index 0000000..1f75395 --- /dev/null +++ b/src/suunto_eonsteel_parser.c @@ -0,0 +1,532 @@ +/* + * libdivecomputer + * + * Copyright (C) 2014 Linus Torvalds + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "context-private.h" +#include "parser-private.h" +#include "array.h" + +struct type_desc { + const char *desc, *format, *mod; +}; + +#define MAXTYPE 512 +#define MAXGASES 16 + +struct eon_parser { + dc_parser_t base; + struct type_desc type_desc[MAXTYPE]; + // field cache + struct { + unsigned int initialized; + unsigned int divetime; + double maxdepth; + double avgdepth; + unsigned int ngases; + dc_gasmix_t gasmix[MAXGASES]; + dc_salinity_t salinity; + double surface_pressure; + } cache; +}; + + +static int report_error(const char *fmt, ...) +{ + char buffer[128]; + va_list args; + + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer)-1, fmt, args); + va_end(args); + fprintf(stderr, "%.*s\n", (int)sizeof(buffer), buffer); + return -1; +} + +static unsigned char get_u8(const void *src) +{ + return *(const unsigned char *)src; +} + +static void debug(const char *name, const char *buf, int len) +{ + int i; + + fprintf(stderr, "%4d %s:", len, name); + for (i = 0; i < len; i++) + fprintf(stderr, " %02x", (unsigned char) buf[i]); + fprintf(stderr, "\n"); +} + +static void debug_text(const char *name, const char *buf, int len) +{ + int i; + + printf("text of %s:\n", name); + for (i = 0; i < len; i++) { + unsigned char c = buf[i]; + if (c > 31 && c < 127) + putchar(c); + else if (c == '\n') { + putchar('\\'); + putchar('n'); + } else { + static const char hex[16]="0123456789abcdef"; + putchar('\\'); + putchar('x'); + putchar(hex[c>>4]); + putchar(hex[c&15]); + } + } + printf("\nend of text\n"); +} + +typedef int (*eon_data_cb_t)(unsigned short type, const struct type_desc *desc, const void *data, int len, void *user); + +static int record_type(struct eon_parser *eon, unsigned short type, const char *name, int namelen) +{ + struct type_desc desc; + const char *next; + + desc.desc = desc.format = desc.mod = ""; + do { + int len; + char *p; + + next = strchr(name, '\n'); + if (next) { + len = next - name; + next++; + } else { + len = strlen(name); + } + + if (len < 5 || name[0] != '<' || name[4] != '>') + return report_error("Unexpected type description: %.*s", len, name); + p = malloc(len-4); + if (!p) + return report_error("out of memory"); + memcpy(p, name+5, len-5); + p[len-5] = 0; + + // PTH, GRP, FRM, MOD + switch (name[1]) { + case 'P': + case 'G': + desc.desc = p; + break; + case 'F': + desc.format = p; + break; + case 'M': + desc.mod = p; + break; + default: + return report_error("Unknown type descriptor: %.*s", len, name); + } + } while ((name = next) != NULL); + + if (type > MAXTYPE) + return report_error("Type out of range (%04x: '%s' '%s' '%s')", + type, desc.desc, desc.format, desc.mod); + + eon->type_desc[type] = desc; + return 0; +} + +static int traverse_entry(struct eon_parser *eon, const unsigned char *p, int len, eon_data_cb_t callback, void *user) +{ + const unsigned char *name, *data, *end, *last, *one_past_end = p + len; + int textlen, type; + int rc; + + // First two bytes: zero and text length + if (p[0]) { + debug("next", p, 8); + return report_error("Bad dive entry (%02x)", p[0]); + } + textlen = p[1]; + + name = p + 2; + if (textlen == 0xff) { + textlen = array_uint32_le(name); + name += 4; + } + + // Two bytes of 'type' followed by the name/descriptor, followed by the data + data = name + textlen; + type = array_uint16_le(name); + name += 2; + + if (*name != '<') { + fflush(NULL); + debug("bad", p, 16); + exit(1); + } + + record_type(eon, type, name, textlen-3); + + end = data; + last = data; + while (end < one_past_end && *end) { + const unsigned char *begin = end; + unsigned int type = *end++; + unsigned int len; + if (type == 0xff) { + type = array_uint16_le(end); + end += 2; + } + len = *end++; + + // I've never actually seen this case yet.. + // Just assuming from the other cases. + if (len == 0xff) { + debug("len-ff", end, 8); + len = array_uint32_le(end); + end += 4; + } + + if (type > MAXTYPE || !eon->type_desc[type].desc) { + debug("last", last, 16); + debug("this", begin, 16); + } else { + rc = callback(type, eon->type_desc+type, end, len, user); + if (rc < 0) + return rc; + } + + last = begin; + end += len; + } + + return end - p; +} + +static int traverse_data(struct eon_parser *eon, eon_data_cb_t callback, void *user) +{ + const unsigned char *data = eon->base.data; + int len = eon->base.size; + + // Dive files start with "SBEM" and four NUL characters + // Additionally, we've prepended the time as an extra + // 4-byte pre-header + if (len < 12 || memcmp(data+4, "SBEM", 4)) + return 0; + + data += 12; + len -= 12; + + while (len > 4) { + int i = traverse_entry(eon, data, len, callback, user); + if (i < 0) + return 1; + len -= i; + data += i; + } + return 0; +} + +struct sample_data { + struct eon_parser *eon; + dc_sample_callback_t callback; + void *userdata; + unsigned int time; +}; + +static void sample_time(struct sample_data *info, unsigned short time_delta) +{ + dc_sample_value_t sample = {0}; + + info->time += time_delta; + sample.time = info->time / 1000; + if (info->callback) info->callback(DC_SAMPLE_TIME, sample, info->userdata); +} + +static void sample_depth(struct sample_data *info, unsigned short depth) +{ + dc_sample_value_t sample = {0}; + + if (depth == 0xffff) + return; + + sample.depth = depth / 100.0; + if (info->callback) info->callback(DC_SAMPLE_DEPTH, sample, info->userdata); +} + +static void sample_temp(struct sample_data *info, short temp) +{ + dc_sample_value_t sample = {0}; + + if (temp < -3000) + return; + + sample.temperature = temp / 10.0; + if (info->callback) info->callback(DC_SAMPLE_TEMPERATURE, sample, info->userdata); +} + +static void sample_deco(struct sample_data *info, short ndl, unsigned short tts, unsigned ceiling) +{ + dc_sample_value_t sample = {0}; + + /* Are we in deco? */ + if (ndl < 0) { + sample.deco.type = DC_DECO_DECOSTOP; + if (tts != 0xffff) + sample.deco.time = tts; + if (ceiling != 0xffff) + sample.deco.depth = ceiling / 100.0; + } else { + sample.deco.type = DC_DECO_NDL; + sample.deco.time = ndl; + } + if (info->callback) info->callback(DC_SAMPLE_DECO, sample, info->userdata); +} + +static void sample_cylinder_pressure(struct sample_data *info, unsigned char idx, unsigned short pressure) +{ + dc_sample_value_t sample = {0}; + + if (pressure == 0xffff) + return; + + sample.pressure.tank = idx-1; + sample.pressure.value = pressure / 100.0; + if (info->callback) info->callback(DC_SAMPLE_PRESSURE, sample, info->userdata); +} + +static int traverse_samples(unsigned short type, const struct type_desc *desc, const void *data, int len, void *user) +{ + struct sample_data *info = user; + + switch (type) { + case 0x0001: // group: time in first word, depth in second + sample_time(info, array_uint16_le(data)); + sample_depth(info, array_uint16_le(data+2)); + sample_temp(info, array_uint16_le(data+4)); + sample_deco(info, array_uint16_le(data+8), array_uint16_le(data+10), array_uint16_le(data+12)); + break; + case 0x0002: // time in first word + sample_time(info, array_uint16_le(data)); + break; + case 0x0003: // depth in first word + sample_depth(info, array_uint16_le(data)); + break; + case 0x000a: // cylinder idx in first byte, pressure in next word + sample_cylinder_pressure(info, get_u8(data), array_uint16_le(data+1)); + break; + } + return 0; +} + +static dc_status_t eonsteel_parser_samples_foreach(dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata) +{ + struct eon_parser *eon = (void *)abstract; + struct sample_data data = { eon, callback, userdata, 0 }; + + traverse_data(eon, traverse_samples, &data); + return DC_STATUS_SUCCESS; +} + +// Ugly define thing makes the code much easier to read +// I'd love to use __typeof__, but that's a gcc'ism +#define field_value(p, set) \ + memcpy((p), &(set), sizeof(set)) + +static dc_status_t eonsteel_parser_get_field(dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value) +{ + struct eon_parser *eon = (struct eon_parser *)parser; + + if (!(eon->cache.initialized >> type)) + return DC_STATUS_UNSUPPORTED; + + switch (type) { + case DC_FIELD_DIVETIME: + field_value(value, eon->cache.divetime); + break; + case DC_FIELD_MAXDEPTH: + field_value(value, eon->cache.maxdepth); + break; + case DC_FIELD_AVGDEPTH: + field_value(value, eon->cache.avgdepth); + break; + case DC_FIELD_GASMIX_COUNT: + field_value(value, eon->cache.ngases); + break; + case DC_FIELD_GASMIX: + if (flags >= MAXGASES) + return DC_STATUS_UNSUPPORTED; + field_value(value, eon->cache.gasmix[flags]); + break; + case DC_FIELD_SALINITY: + field_value(value, eon->cache.salinity); + break; + case DC_FIELD_ATMOSPHERIC: + field_value(value, eon->cache.surface_pressure); + break; + } + return DC_STATUS_SUCCESS; +} + +/* + * The time of the dive is encoded in the filename, + * and we've saved it off as the four first bytes + * of the dive data (in little-endian format). + */ +static dc_status_t eonsteel_parser_get_datetime(dc_parser_t *parser, dc_datetime_t *datetime) +{ + if (parser->size < 4) + return DC_STATUS_UNSUPPORTED; + + dc_datetime_gmtime(datetime, array_uint32_le(parser->data)); + return DC_STATUS_SUCCESS; +} + +// time in ms +static void add_time_field(struct eon_parser *eon, unsigned short time_delta_ms) +{ + eon->cache.divetime += time_delta_ms; +} + +// depth in cm +static void set_depth_field(struct eon_parser *eon, unsigned short d) +{ + if (d != 0xffff) { + double depth = d / 100.0; + if (depth > eon->cache.maxdepth) + eon->cache.maxdepth = depth; + eon->cache.initialized |= 1 << DC_FIELD_MAXDEPTH; + } +} + +// gas type: 0=Off,1=Primary,2=?,3=Diluent +static void add_gas_type(struct eon_parser *eon, unsigned char type) +{ + if (eon->cache.ngases < MAXGASES) + eon->cache.ngases++; + eon->cache.initialized |= 1 << DC_FIELD_GASMIX_COUNT; +} + +// O2 percentage as a byte +static void add_gas_o2(struct eon_parser *eon, unsigned char o2) +{ + int idx = eon->cache.ngases-1; + if (idx >= 0) + eon->cache.gasmix[idx].oxygen = o2 / 100.0; + eon->cache.initialized |= 1 << DC_FIELD_GASMIX; +} + +// He percentage as a byte +static void add_gas_he(struct eon_parser *eon, unsigned char he) +{ + int idx = eon->cache.ngases-1; + if (idx >= 0) + eon->cache.gasmix[idx].helium = he / 100.0; + eon->cache.initialized |= 1 << DC_FIELD_GASMIX; +} + +static int traverse_fields(unsigned short type, const struct type_desc *desc, const void *data, int len, void *user) +{ + struct eon_parser *eon = user; + + switch (type) { + case 0x0001: // group: time in first word, depth in second + add_time_field(eon, array_uint16_le(data)); + set_depth_field(eon, array_uint16_le(data+2)); + break; + case 0x0002: // time in first word + add_time_field(eon, array_uint16_le(data)); + break; + case 0x0003: // depth in first word + set_depth_field(eon, array_uint16_le(data)); + break; + case 0x000d: // gas state in first byte + add_gas_type(eon, get_u8(data)); + break; + case 0x000e: // Oxygen percentage in first byte + add_gas_o2(eon, get_u8(data)); + break; + case 0x000f: // Helium percentage in first byte + add_gas_he(eon, get_u8(data)); + break; + } + return 0; +} + + +static void initialize_field_caches(struct eon_parser *eon) +{ + memset(&eon->cache, 0, sizeof(eon->cache)); + eon->cache.initialized = 1 << DC_FIELD_DIVETIME; + + traverse_data(eon, traverse_fields, eon); + + // The internal time fields are in ms and have to be added up + // like that. At the end, we translate it back to seconds. + eon->cache.divetime /= 1000; +} + +static dc_status_t eonsteel_parser_set_data(dc_parser_t *parser, const unsigned char *data, unsigned int size) +{ + struct eon_parser *eon = (void *)parser; + memset(eon->type_desc, 0, sizeof(eon->type_desc)); + initialize_field_caches(eon); + return DC_STATUS_SUCCESS; +} + +static dc_status_t eonsteel_parser_destroy(dc_parser_t *parser) +{ + free(parser); + return DC_STATUS_SUCCESS; +} + +static const dc_parser_vtable_t eonsteel_parser_vtable = { + DC_FAMILY_SUUNTO_EONSTEEL, + eonsteel_parser_set_data, /* set_data */ + eonsteel_parser_get_datetime, /* datetime */ + eonsteel_parser_get_field, /* fields */ + eonsteel_parser_samples_foreach, /* samples_foreach */ + eonsteel_parser_destroy /* destroy */ +}; + +dc_status_t suunto_eonsteel_parser_create(dc_parser_t **out, dc_context_t *context, unsigned int model) +{ + struct eon_parser *eon; + + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + eon = calloc(1, sizeof(*eon)); + + parser_init(&eon->base, context, &eonsteel_parser_vtable); + + *out = (dc_parser_t *) eon; + + return DC_STATUS_SUCCESS; +}