Add support for Liquivision dive computers
This commit is contained in:
parent
91acd9bb2d
commit
71a149d776
@ -91,6 +91,7 @@ static const backend_table_t g_backends[] = {
|
||||
{"cochran", DC_FAMILY_COCHRAN_COMMANDER, 0},
|
||||
{"divecomputereu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0},
|
||||
{"mclean", DC_FAMILY_MCLEAN_EXTREME, 0},
|
||||
{"lynx", DC_FAMILY_LIQUIVISION_LYNX, 0},
|
||||
};
|
||||
|
||||
static const transport_table_t g_transports[] = {
|
||||
|
||||
@ -106,6 +106,8 @@ typedef enum dc_family_t {
|
||||
DC_FAMILY_TECDIVING_DIVECOMPUTEREU = (15 << 16),
|
||||
/* McLean */
|
||||
DC_FAMILY_MCLEAN_EXTREME = (16 << 16),
|
||||
/* Liquivision */
|
||||
DC_FAMILY_LIQUIVISION_LYNX = (17 << 16),
|
||||
} dc_family_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -318,6 +318,14 @@
|
||||
RelativePath="..\src\iterator.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\liquivision_lynx.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\liquivision_lynx_parser.c"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\mares_common.c"
|
||||
>
|
||||
@ -684,6 +692,10 @@
|
||||
RelativePath="..\include\libdivecomputer\iterator.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\liquivision_lynx.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\src\mares_common.h"
|
||||
>
|
||||
|
||||
@ -73,6 +73,7 @@ libdivecomputer_la_SOURCES = \
|
||||
cochran_commander.h cochran_commander.c cochran_commander_parser.c \
|
||||
tecdiving_divecomputereu.h tecdiving_divecomputereu.c tecdiving_divecomputereu_parser.c \
|
||||
mclean_extreme.h mclean_extreme.c mclean_extreme_parser.c \
|
||||
liquivision_lynx.h liquivision_lynx.c liquivision_lynx_parser.c \
|
||||
socket.h socket.c \
|
||||
irda.c \
|
||||
usbhid.c \
|
||||
|
||||
@ -157,3 +157,48 @@ checksum_crc32 (const unsigned char data[], unsigned int size)
|
||||
|
||||
return crc ^ 0xffffffff;
|
||||
}
|
||||
|
||||
unsigned int
|
||||
checksum_crc32b (const unsigned char data[], unsigned int size)
|
||||
{
|
||||
static const unsigned int crc_table[] = {
|
||||
0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
|
||||
0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
|
||||
0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75,
|
||||
0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD,
|
||||
0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5,
|
||||
0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D,
|
||||
0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95,
|
||||
0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D,
|
||||
0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072,
|
||||
0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA,
|
||||
0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02,
|
||||
0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
|
||||
0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692,
|
||||
0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A,
|
||||
0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2,
|
||||
0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A,
|
||||
0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB,
|
||||
0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53,
|
||||
0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B,
|
||||
0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623,
|
||||
0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B,
|
||||
0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3,
|
||||
0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B,
|
||||
0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3,
|
||||
0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C,
|
||||
0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24,
|
||||
0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
|
||||
0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654,
|
||||
0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C,
|
||||
0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4,
|
||||
0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C,
|
||||
0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4,
|
||||
};
|
||||
|
||||
unsigned int crc = 0xffffffff;
|
||||
for (unsigned int i = 0; i < size; ++i)
|
||||
crc = crc_table[((crc >> 24) ^ data[i]) & 0xFF] ^ (crc << 8);
|
||||
|
||||
return crc ^ 0xffffffff;
|
||||
}
|
||||
|
||||
@ -44,6 +44,9 @@ checksum_crc16_ccitt (const unsigned char data[], unsigned int size, unsigned sh
|
||||
unsigned int
|
||||
checksum_crc32 (const unsigned char data[], unsigned int size);
|
||||
|
||||
unsigned int
|
||||
checksum_crc32b (const unsigned char data[], unsigned int size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
@ -380,6 +380,11 @@ static const dc_descriptor_t g_descriptors[] = {
|
||||
{"Tecdiving", "DiveComputer.eu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_tecdiving},
|
||||
/* McLean Extreme */
|
||||
{ "McLean", "Extreme", DC_FAMILY_MCLEAN_EXTREME, 0, DC_TRANSPORT_SERIAL | DC_TRANSPORT_BLUETOOTH, dc_filter_mclean},
|
||||
/* Liquivision */
|
||||
{"Liquivision", "Xen", DC_FAMILY_LIQUIVISION_LYNX, 0, DC_TRANSPORT_SERIAL, NULL},
|
||||
{"Liquivision", "Xeo", DC_FAMILY_LIQUIVISION_LYNX, 1, DC_TRANSPORT_SERIAL, NULL},
|
||||
{"Liquivision", "Lynx", DC_FAMILY_LIQUIVISION_LYNX, 2, DC_TRANSPORT_SERIAL, NULL},
|
||||
{"Liquivision", "Kaon", DC_FAMILY_LIQUIVISION_LYNX, 3, DC_TRANSPORT_SERIAL, NULL},
|
||||
};
|
||||
|
||||
static int
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
#include "cochran_commander.h"
|
||||
#include "tecdiving_divecomputereu.h"
|
||||
#include "mclean_extreme.h"
|
||||
#include "liquivision_lynx.h"
|
||||
|
||||
#include "device-private.h"
|
||||
#include "context-private.h"
|
||||
@ -215,6 +216,9 @@ dc_device_open (dc_device_t **out, dc_context_t *context, dc_descriptor_t *descr
|
||||
case DC_FAMILY_MCLEAN_EXTREME:
|
||||
rc = mclean_extreme_device_open (&device, context, iostream);
|
||||
break;
|
||||
case DC_FAMILY_LIQUIVISION_LYNX:
|
||||
rc = liquivision_lynx_device_open (&device, context, iostream);
|
||||
break;
|
||||
default:
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
}
|
||||
|
||||
677
src/liquivision_lynx.c
Normal file
677
src/liquivision_lynx.c
Normal file
@ -0,0 +1,677 @@
|
||||
/*
|
||||
* libdivecomputer
|
||||
*
|
||||
* Copyright (C) 2020 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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <libdivecomputer/units.h>
|
||||
|
||||
#include "liquivision_lynx.h"
|
||||
#include "context-private.h"
|
||||
#include "device-private.h"
|
||||
#include "ringbuffer.h"
|
||||
#include "rbstream.h"
|
||||
#include "checksum.h"
|
||||
#include "array.h"
|
||||
|
||||
#define ISINSTANCE(device) dc_device_isinstance((device), &liquivision_lynx_device_vtable)
|
||||
|
||||
#define XEN 0
|
||||
#define XEO 1
|
||||
#define LYNX 2
|
||||
#define KAON 3
|
||||
|
||||
#define XEN_V1 0x83321485 // Not supported
|
||||
#define XEN_V2 0x83321502
|
||||
#define XEN_V3 0x83328401
|
||||
|
||||
#define XEO_V1_A 0x17485623
|
||||
#define XEO_V1_B 0x27485623
|
||||
#define XEO_V2_A 0x17488401
|
||||
#define XEO_V2_B 0x27488401
|
||||
#define XEO_V3_A 0x17488402
|
||||
#define XEO_V3_B 0x27488402
|
||||
|
||||
#define LYNX_V1 0x67488403
|
||||
#define LYNX_V2 0x67488404
|
||||
#define LYNX_V3 0x67488405
|
||||
|
||||
#define KAON_V1 0x37488402
|
||||
#define KAON_V2 0x47488402
|
||||
|
||||
#define MAXRETRIES 2
|
||||
#define MAXPACKET 12
|
||||
#define SEGMENTSIZE 0x400
|
||||
#define PAGESIZE 0x1000
|
||||
#define MEMSIZE 0x200000
|
||||
|
||||
#define RB_LOGBOOK_BEGIN (1 * PAGESIZE)
|
||||
#define RB_LOGBOOK_END (25 * PAGESIZE)
|
||||
#define RB_LOGBOOK_SIZE (RB_LOGBOOK_END - RB_LOGBOOK_BEGIN)
|
||||
#define RB_LOGBOOK_DISTANCE(a,b) ringbuffer_distance (a, b, 1, RB_LOGBOOK_BEGIN, RB_LOGBOOK_END)
|
||||
|
||||
#define RB_PROFILE_BEGIN (25 * PAGESIZE)
|
||||
#define RB_PROFILE_END (500 * PAGESIZE)
|
||||
#define RB_PROFILE_SIZE (RB_PROFILE_END - RB_PROFILE_BEGIN)
|
||||
#define RB_PROFILE_DISTANCE(a,b) ringbuffer_distance (a, b, 1, RB_PROFILE_BEGIN, RB_PROFILE_END)
|
||||
|
||||
#define SZ_HEADER_XEN 80
|
||||
#define SZ_HEADER_OTHER 96
|
||||
#define SZ_HEADER_MAX SZ_HEADER_OTHER
|
||||
|
||||
typedef struct liquivision_lynx_device_t {
|
||||
dc_device_t base;
|
||||
dc_iostream_t *iostream;
|
||||
unsigned char fingerprint[4];
|
||||
} liquivision_lynx_device_t;
|
||||
|
||||
static dc_status_t liquivision_lynx_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size);
|
||||
static dc_status_t liquivision_lynx_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size);
|
||||
static dc_status_t liquivision_lynx_device_dump (dc_device_t *abstract, dc_buffer_t *buffer);
|
||||
static dc_status_t liquivision_lynx_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata);
|
||||
static dc_status_t liquivision_lynx_device_close (dc_device_t *abstract);
|
||||
|
||||
static const dc_device_vtable_t liquivision_lynx_device_vtable = {
|
||||
sizeof(liquivision_lynx_device_t),
|
||||
DC_FAMILY_LIQUIVISION_LYNX,
|
||||
liquivision_lynx_device_set_fingerprint, /* set_fingerprint */
|
||||
liquivision_lynx_device_read, /* read */
|
||||
NULL, /* write */
|
||||
liquivision_lynx_device_dump, /* dump */
|
||||
liquivision_lynx_device_foreach, /* foreach */
|
||||
NULL, /* timesync */
|
||||
liquivision_lynx_device_close /* close */
|
||||
};
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_send (liquivision_lynx_device_t *device, const unsigned char data[], unsigned int size)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
dc_device_t *abstract = (dc_device_t *) device;
|
||||
|
||||
if (size > MAXPACKET)
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
|
||||
// Build the packet.
|
||||
unsigned char packet[2 + MAXPACKET + 2] = {0};
|
||||
packet[0] = 0x00;
|
||||
packet[1] = 0xB1;
|
||||
if (size) {
|
||||
memcpy (packet + 2, data, size);
|
||||
}
|
||||
packet[2 + size + 0] = 0x0B;
|
||||
packet[2 + size + 1] = 0x0E;
|
||||
|
||||
// Send the packet to the device.
|
||||
status = dc_iostream_write (device->iostream, packet, size + 4, NULL);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to send the packet.");
|
||||
return status;
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_recv (liquivision_lynx_device_t *device, unsigned char data[], unsigned int size)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
dc_device_t *abstract = (dc_device_t *) device;
|
||||
|
||||
if (size > SEGMENTSIZE)
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
|
||||
// Receive the packet from the device.
|
||||
unsigned char packet[1 + SEGMENTSIZE + 2] = {0};
|
||||
status = dc_iostream_read (device->iostream, packet, 1 + size + 2, NULL);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to receive the packet.");
|
||||
return status;
|
||||
}
|
||||
|
||||
// Verify the start byte.
|
||||
if (packet[0] != 0xC5) {
|
||||
ERROR (abstract->context, "Unexpected answer start byte (%02x).", packet[0]);
|
||||
return DC_STATUS_PROTOCOL;
|
||||
}
|
||||
|
||||
// Verify the checksum.
|
||||
unsigned short crc = array_uint16_be (packet + 1 + size);
|
||||
unsigned short ccrc = checksum_crc16_ccitt (packet + 1, size, 0xffff);
|
||||
if (crc != ccrc) {
|
||||
ERROR (abstract->context, "Unexpected answer checksum (%04x %04x).", crc, ccrc);
|
||||
return DC_STATUS_PROTOCOL;
|
||||
}
|
||||
|
||||
if (size) {
|
||||
memcpy (data, packet + 1, size);
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_packet (liquivision_lynx_device_t *device, const unsigned char command[], unsigned int csize, unsigned char answer[], unsigned int asize)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
dc_device_t *abstract = (dc_device_t *) device;
|
||||
|
||||
if (device_is_cancelled (abstract))
|
||||
return DC_STATUS_CANCELLED;
|
||||
|
||||
status = liquivision_lynx_send (device, command, csize);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to send the command.");
|
||||
return status;
|
||||
}
|
||||
|
||||
if (asize) {
|
||||
status = liquivision_lynx_recv (device, answer, asize);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to receive the answer.");
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_transfer (liquivision_lynx_device_t *device, const unsigned char command[], unsigned int csize, unsigned char answer[], unsigned int asize)
|
||||
{
|
||||
unsigned int nretries = 0;
|
||||
dc_status_t rc = DC_STATUS_SUCCESS;
|
||||
while ((rc = liquivision_lynx_packet (device, command, csize, answer, asize)) != DC_STATUS_SUCCESS) {
|
||||
if (rc != DC_STATUS_TIMEOUT && rc != DC_STATUS_PROTOCOL)
|
||||
return rc;
|
||||
|
||||
// Abort if the maximum number of retries is reached.
|
||||
if (nretries++ >= MAXRETRIES)
|
||||
return rc;
|
||||
|
||||
// Delay the next attempt.
|
||||
dc_iostream_sleep (device->iostream, 100);
|
||||
dc_iostream_purge (device->iostream, DC_DIRECTION_INPUT);
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
dc_status_t
|
||||
liquivision_lynx_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
liquivision_lynx_device_t *device = NULL;
|
||||
|
||||
if (out == NULL)
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
|
||||
// Allocate memory.
|
||||
device = (liquivision_lynx_device_t *) dc_device_allocate (context, &liquivision_lynx_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 (9600 8N1).
|
||||
status = dc_iostream_configure (device->iostream, 9600, 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 (3000 ms).
|
||||
status = dc_iostream_set_timeout (device->iostream, 3000);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (context, "Failed to set the timeout.");
|
||||
goto error_free;
|
||||
}
|
||||
|
||||
// Set the DTR line.
|
||||
status = dc_iostream_set_dtr (device->iostream, 0);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (context, "Failed to set the DTR line.");
|
||||
goto error_free;
|
||||
}
|
||||
|
||||
// Set the RTS line.
|
||||
status = dc_iostream_set_rts (device->iostream, 0);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (context, "Failed to set the RTS line.");
|
||||
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);
|
||||
|
||||
// Wakeup the device.
|
||||
for (unsigned int i = 0; i < 6000; ++i) {
|
||||
const unsigned char init[] = {0xAA};
|
||||
dc_iostream_write (device->iostream, init, sizeof (init), NULL);
|
||||
}
|
||||
|
||||
*out = (dc_device_t *) device;
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
|
||||
error_free:
|
||||
dc_device_deallocate ((dc_device_t *) device);
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size)
|
||||
{
|
||||
liquivision_lynx_device_t *device = (liquivision_lynx_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
|
||||
liquivision_lynx_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
liquivision_lynx_device_t *device = (liquivision_lynx_device_t *) abstract;
|
||||
|
||||
if ((address % SEGMENTSIZE != 0) ||
|
||||
(size % SEGMENTSIZE != 0))
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
|
||||
// Get the page and segment number.
|
||||
unsigned int page = (address / PAGESIZE);
|
||||
unsigned int segment = (address % PAGESIZE) / SEGMENTSIZE;
|
||||
|
||||
unsigned int nbytes = 0;
|
||||
while (nbytes < size) {
|
||||
const unsigned char command[] = {
|
||||
0x50, 0x41, 0x47, 0x45,
|
||||
'0' + ((page / 100) % 10),
|
||||
'0' + ((page / 10) % 10),
|
||||
'0' + ((page / 1) % 10),
|
||||
'0' + ((page / 100) % 10),
|
||||
'0' + ((page / 10) % 10),
|
||||
'0' + ((page / 1) % 10),
|
||||
'0' + segment,
|
||||
'0' + segment
|
||||
};
|
||||
|
||||
status = liquivision_lynx_transfer (device, command, sizeof(command), data + nbytes, SEGMENTSIZE);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to read page %u segment %u.", page, segment);
|
||||
return status;
|
||||
}
|
||||
|
||||
nbytes += SEGMENTSIZE;
|
||||
segment++;
|
||||
if (segment == (PAGESIZE / SEGMENTSIZE)) {
|
||||
segment = 0;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_device_dump (dc_device_t *abstract, dc_buffer_t *buffer)
|
||||
{
|
||||
// Allocate the required amount of memory.
|
||||
if (!dc_buffer_resize (buffer, MEMSIZE)) {
|
||||
ERROR (abstract->context, "Insufficient buffer space available.");
|
||||
return DC_STATUS_NOMEMORY;
|
||||
}
|
||||
|
||||
return device_dump_read (abstract, dc_buffer_get_data (buffer),
|
||||
dc_buffer_get_size (buffer), SEGMENTSIZE);
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
liquivision_lynx_device_t *device = (liquivision_lynx_device_t *) abstract;
|
||||
|
||||
// Enable progress notifications.
|
||||
dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
|
||||
progress.maximum = SEGMENTSIZE + RB_LOGBOOK_SIZE + RB_PROFILE_SIZE;
|
||||
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
||||
|
||||
// Send the info command.
|
||||
unsigned char rsp_info[6] = {0};
|
||||
const unsigned char cmd_info[] = {0x49, 0x4E, 0x46, 0x4F, 0x49, 0x4E, 0x46, 0x4F, 0x49, 0x4E, 0x46, 0x4F};
|
||||
status = liquivision_lynx_transfer (device, cmd_info, sizeof(cmd_info), rsp_info, sizeof(rsp_info));
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to send the info command.");
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Get the model and version.
|
||||
unsigned int model = array_uint16_le(rsp_info + 0);
|
||||
unsigned int version = array_uint32_le(rsp_info + 2);
|
||||
|
||||
// Send the more info command.
|
||||
unsigned char rsp_more[12] = {0};
|
||||
const unsigned char cmd_more[] = {0x4D, 0x4F, 0x52, 0x45, 0x49, 0x4E, 0x46, 0x4F, 0x4D, 0x4F, 0x52, 0x45};
|
||||
status = liquivision_lynx_transfer (device, cmd_more, sizeof(cmd_more), rsp_more, sizeof(rsp_more));
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to send the more info command.");
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Emit a device info event.
|
||||
dc_event_devinfo_t devinfo;
|
||||
devinfo.model = model;
|
||||
devinfo.firmware = 0;
|
||||
devinfo.serial = array_uint32_le(rsp_more + 0);
|
||||
device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo);
|
||||
|
||||
// Read the config segment.
|
||||
unsigned char config[SEGMENTSIZE] = {0};
|
||||
status = liquivision_lynx_device_read (abstract, 0, config, sizeof (config));
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to read the memory.");
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Get the header size.
|
||||
unsigned int headersize = (model == XEN) ? SZ_HEADER_XEN : SZ_HEADER_OTHER;
|
||||
|
||||
// Get the number of headers per page.
|
||||
unsigned int npages = PAGESIZE / headersize;
|
||||
|
||||
// Get the logbook pointers.
|
||||
unsigned int begin = array_uint16_le (config + 0x46);
|
||||
unsigned int end = array_uint16_le (config + 0x48);
|
||||
unsigned int rb_logbook_begin = RB_LOGBOOK_BEGIN + (begin / npages) * PAGESIZE + (begin % npages) * headersize;
|
||||
unsigned int rb_logbook_end = RB_LOGBOOK_BEGIN + (end / npages) * PAGESIZE + (end % npages) * headersize;
|
||||
if (rb_logbook_begin < RB_LOGBOOK_BEGIN || rb_logbook_begin > RB_LOGBOOK_END ||
|
||||
rb_logbook_end < RB_LOGBOOK_BEGIN || rb_logbook_end > RB_LOGBOOK_END) {
|
||||
ERROR (abstract->context, "Invalid logbook pointers (%04x, %04x).",
|
||||
rb_logbook_begin, rb_logbook_end);
|
||||
status = DC_STATUS_DATAFORMAT;
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Calculate the logbook size.
|
||||
#if 0
|
||||
unsigned int rb_logbook_size = RB_LOGBOOK_DISTANCE (rb_logbook_begin, rb_logbook_end);
|
||||
#else
|
||||
// The logbook begin pointer is explicitly ignored, because it only takes
|
||||
// into account dives for which the profile is still available.
|
||||
unsigned int rb_logbook_size = RB_LOGBOOK_SIZE;
|
||||
#endif
|
||||
|
||||
// Get the profile pointers.
|
||||
unsigned int rb_profile_begin = array_uint32_le (config + 0x4A);
|
||||
unsigned int rb_profile_end = array_uint32_le (config + 0x4E);
|
||||
if (rb_profile_begin < RB_PROFILE_BEGIN || rb_profile_begin > RB_PROFILE_END ||
|
||||
rb_profile_end < RB_PROFILE_BEGIN || rb_profile_end > RB_PROFILE_END) {
|
||||
ERROR (abstract->context, "Invalid profile pointers (%04x, %04x).",
|
||||
rb_profile_begin, rb_profile_end);
|
||||
status = DC_STATUS_DATAFORMAT;
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Update and emit a progress event.
|
||||
progress.current += SEGMENTSIZE;
|
||||
progress.maximum -= RB_LOGBOOK_SIZE - rb_logbook_size;
|
||||
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
||||
|
||||
// Allocate memory for the logbook entries.
|
||||
unsigned char *logbook = (unsigned char *) malloc (rb_logbook_size);
|
||||
if (logbook == NULL) {
|
||||
status = DC_STATUS_NOMEMORY;
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
// Create the ringbuffer stream.
|
||||
dc_rbstream_t *rblogbook = NULL;
|
||||
status = dc_rbstream_new (&rblogbook, abstract, SEGMENTSIZE, SEGMENTSIZE, RB_LOGBOOK_BEGIN, RB_LOGBOOK_END, rb_logbook_end);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to create the ringbuffer stream.");
|
||||
goto error_free_logbook;
|
||||
}
|
||||
|
||||
// The logbook ringbuffer is read backwards to retrieve the most recent
|
||||
// entries first. If an already downloaded entry is identified (by means
|
||||
// of its fingerprint), the transfer is aborted immediately to reduce
|
||||
// the transfer time.
|
||||
unsigned int nbytes = 0;
|
||||
unsigned int offset = rb_logbook_size;
|
||||
unsigned int address = rb_logbook_end;
|
||||
while (nbytes < rb_logbook_size) {
|
||||
// Handle the ringbuffer wrap point.
|
||||
if (address == RB_LOGBOOK_BEGIN)
|
||||
address = RB_LOGBOOK_END;
|
||||
|
||||
// Skip the padding bytes.
|
||||
if ((address % PAGESIZE) == 0) {
|
||||
unsigned int padding = PAGESIZE % headersize;
|
||||
unsigned char dummy[SZ_HEADER_MAX] = {0};
|
||||
status = dc_rbstream_read (rblogbook, &progress, dummy, padding);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to read the memory.");
|
||||
goto error_free_rblogbook;
|
||||
}
|
||||
|
||||
address -= padding;
|
||||
nbytes += padding;
|
||||
}
|
||||
|
||||
// Move to the start of the current entry.
|
||||
address -= headersize;
|
||||
offset -= headersize;
|
||||
|
||||
// Read the logbook entry.
|
||||
status = dc_rbstream_read (rblogbook, &progress, logbook + offset, headersize);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to read the memory.");
|
||||
goto error_free_rblogbook;
|
||||
}
|
||||
|
||||
nbytes += headersize;
|
||||
|
||||
if (array_isequal (logbook + offset, headersize, 0xFF)) {
|
||||
offset += headersize;
|
||||
break;
|
||||
}
|
||||
|
||||
// Verify the checksum.
|
||||
unsigned int unused = 2;
|
||||
if (version == XEO_V1_A || version == XEO_V1_B) {
|
||||
unused = 6;
|
||||
}
|
||||
unsigned char header[SZ_HEADER_MAX] = {0};
|
||||
memcpy (header + 0, rsp_info + 2, 4);
|
||||
memcpy (header + 4, logbook + offset + 4, headersize - 4);
|
||||
unsigned int crc = array_uint32_le (logbook + offset + 0);
|
||||
unsigned int ccrc = checksum_crc32b (header, headersize - unused);
|
||||
if (crc != ccrc) {
|
||||
WARNING (abstract->context, "Invalid dive checksum (%08x %08x)", crc, ccrc);
|
||||
status = DC_STATUS_DATAFORMAT;
|
||||
goto error_free_rblogbook;
|
||||
}
|
||||
|
||||
// Compare the fingerprint to identify previously downloaded entries.
|
||||
if (memcmp (logbook + offset, device->fingerprint, sizeof(device->fingerprint)) == 0) {
|
||||
offset += headersize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update and emit a progress event.
|
||||
progress.maximum -= rb_logbook_size - nbytes;
|
||||
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
||||
|
||||
// Go through the logbook entries a first time, to calculate the total
|
||||
// amount of bytes in the profile ringbuffer.
|
||||
unsigned int rb_profile_size = 0;
|
||||
|
||||
// Traverse the logbook ringbuffer backwards to retrieve the most recent
|
||||
// dives first. The logbook ringbuffer is linearized at this point, so
|
||||
// we do not have to take into account any memory wrapping near the end
|
||||
// of the memory buffer.
|
||||
unsigned int remaining = RB_PROFILE_SIZE;
|
||||
unsigned int previous = rb_profile_end;
|
||||
unsigned int entry = rb_logbook_size;
|
||||
while (entry != offset) {
|
||||
// Move to the start of the current entry.
|
||||
entry -= headersize;
|
||||
|
||||
// Get the profile pointer.
|
||||
unsigned int current = array_uint32_le (logbook + entry + 16);
|
||||
if (current < RB_PROFILE_BEGIN || current >= RB_PROFILE_END) {
|
||||
ERROR (abstract->context, "Invalid profile ringbuffer pointer (%08x).", current);
|
||||
status = DC_STATUS_DATAFORMAT;
|
||||
goto error_free_rblogbook;
|
||||
}
|
||||
|
||||
// Calculate the length.
|
||||
unsigned int length = RB_PROFILE_DISTANCE (current, previous);
|
||||
|
||||
// Make sure the profile size is valid.
|
||||
if (length > remaining) {
|
||||
remaining = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
// Update the total profile size.
|
||||
rb_profile_size += length;
|
||||
|
||||
// Move to the start of the current dive.
|
||||
remaining -= length;
|
||||
previous = current;
|
||||
}
|
||||
|
||||
// At this point, we know the exact amount of data
|
||||
// that needs to be transferred for the profiles.
|
||||
progress.maximum -= RB_PROFILE_SIZE - rb_profile_size;
|
||||
device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
|
||||
|
||||
// Allocate memory for the profile data.
|
||||
unsigned char *profile = (unsigned char *) malloc (headersize + rb_profile_size);
|
||||
if (profile == NULL) {
|
||||
status = DC_STATUS_NOMEMORY;
|
||||
goto error_free_rblogbook;
|
||||
}
|
||||
|
||||
// Create the ringbuffer stream.
|
||||
dc_rbstream_t *rbprofile = NULL;
|
||||
status = dc_rbstream_new (&rbprofile, abstract, SEGMENTSIZE, SEGMENTSIZE, RB_PROFILE_BEGIN, RB_PROFILE_END, rb_profile_end);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to create the ringbuffer stream.");
|
||||
goto error_free_profile;
|
||||
}
|
||||
|
||||
// Traverse the logbook ringbuffer backwards to retrieve the most recent
|
||||
// dives first. The logbook ringbuffer is linearized at this point, so
|
||||
// we do not have to take into account any memory wrapping near the end
|
||||
// of the memory buffer.
|
||||
remaining = rb_profile_size;
|
||||
previous = rb_profile_end;
|
||||
entry = rb_logbook_size;
|
||||
while (entry != offset) {
|
||||
// Move to the start of the current entry.
|
||||
entry -= headersize;
|
||||
|
||||
// Get the profile pointer.
|
||||
unsigned int current = array_uint32_le (logbook + entry + 16);
|
||||
if (current < RB_PROFILE_BEGIN || current >= RB_PROFILE_END) {
|
||||
ERROR (abstract->context, "Invalid profile ringbuffer pointer (%08x).", current);
|
||||
status = DC_STATUS_DATAFORMAT;
|
||||
goto error_free_rbprofile;
|
||||
}
|
||||
|
||||
// Calculate the length.
|
||||
unsigned int length = RB_PROFILE_DISTANCE (current, previous);
|
||||
|
||||
// Make sure the profile size is valid.
|
||||
if (length > remaining) {
|
||||
remaining = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
// Move to the start of the current dive.
|
||||
remaining -= length;
|
||||
previous = current;
|
||||
|
||||
// Read the dive.
|
||||
status = dc_rbstream_read (rbprofile, &progress, profile + remaining + headersize, length);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to read the dive.");
|
||||
goto error_free_rbprofile;
|
||||
}
|
||||
|
||||
// Prepend the logbook entry to the profile data. The memory buffer is
|
||||
// large enough to store this entry. The checksum is replaced with the
|
||||
// flash version number.
|
||||
memcpy (profile + remaining + 0, rsp_info + 2, 4);
|
||||
memcpy (profile + remaining + 4, logbook + entry + 4, headersize - 4);
|
||||
|
||||
if (callback && !callback (profile + remaining, headersize + length, logbook + entry, sizeof(device->fingerprint), userdata)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
error_free_rbprofile:
|
||||
dc_rbstream_free (rbprofile);
|
||||
error_free_profile:
|
||||
free (profile);
|
||||
error_free_rblogbook:
|
||||
dc_rbstream_free (rblogbook);
|
||||
error_free_logbook:
|
||||
free (logbook);
|
||||
error_exit:
|
||||
return status;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_device_close (dc_device_t *abstract)
|
||||
{
|
||||
dc_status_t status = DC_STATUS_SUCCESS;
|
||||
liquivision_lynx_device_t *device = (liquivision_lynx_device_t*) abstract;
|
||||
dc_status_t rc = DC_STATUS_SUCCESS;
|
||||
|
||||
// Send the finish command.
|
||||
const unsigned char cmd_finish[] = {0x46, 0x49, 0x4E, 0x49, 0x53, 0x48, 0x46, 0x49, 0x4E, 0x49, 0x53, 0x48};
|
||||
status = liquivision_lynx_transfer (device, cmd_finish, sizeof(cmd_finish), NULL, 0);
|
||||
if (status != DC_STATUS_SUCCESS) {
|
||||
ERROR (abstract->context, "Failed to send the finish command.");
|
||||
dc_status_set_error(&status, rc);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
43
src/liquivision_lynx.h
Normal file
43
src/liquivision_lynx.h
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* libdivecomputer
|
||||
*
|
||||
* Copyright (C) 2020 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
|
||||
*/
|
||||
|
||||
#ifndef LIQUIVISION_LYNX_H
|
||||
#define LIQUIVISION_LYNX_H
|
||||
|
||||
#include <libdivecomputer/context.h>
|
||||
#include <libdivecomputer/iostream.h>
|
||||
#include <libdivecomputer/device.h>
|
||||
#include <libdivecomputer/parser.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
dc_status_t
|
||||
liquivision_lynx_device_open (dc_device_t **device, dc_context_t *context, dc_iostream_t *iostream);
|
||||
|
||||
dc_status_t
|
||||
liquivision_lynx_parser_create (dc_parser_t **parser, dc_context_t *context, unsigned int model);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
#endif /* LIQUIVISION_LYNX_H */
|
||||
596
src/liquivision_lynx_parser.c
Normal file
596
src/liquivision_lynx_parser.c
Normal file
@ -0,0 +1,596 @@
|
||||
/*
|
||||
* libdivecomputer
|
||||
*
|
||||
* Copyright (C) 2020 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 <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <libdivecomputer/units.h>
|
||||
|
||||
#include "liquivision_lynx.h"
|
||||
#include "context-private.h"
|
||||
#include "parser-private.h"
|
||||
#include "array.h"
|
||||
|
||||
#define ISINSTANCE(parser) dc_parser_isinstance((parser), &liquivision_lynx_parser_vtable)
|
||||
|
||||
#define C_ARRAY_SIZE(array) (sizeof (array) / sizeof *(array))
|
||||
|
||||
#define XEN 0
|
||||
#define XEO 1
|
||||
#define LYNX 2
|
||||
#define KAON 3
|
||||
|
||||
#define XEN_V1 0x83321485 // Not supported
|
||||
#define XEN_V2 0x83321502
|
||||
#define XEN_V3 0x83328401
|
||||
|
||||
#define XEO_V1_A 0x17485623
|
||||
#define XEO_V1_B 0x27485623
|
||||
#define XEO_V2_A 0x17488401
|
||||
#define XEO_V2_B 0x27488401
|
||||
#define XEO_V3_A 0x17488402
|
||||
#define XEO_V3_B 0x27488402
|
||||
|
||||
#define LYNX_V1 0x67488403
|
||||
#define LYNX_V2 0x67488404
|
||||
#define LYNX_V3 0x67488405
|
||||
|
||||
#define KAON_V1 0x37488402
|
||||
#define KAON_V2 0x47488402
|
||||
|
||||
#define SZ_HEADER_XEN 80
|
||||
#define SZ_HEADER_OTHER 96
|
||||
|
||||
#define FRESH 0
|
||||
#define BRACKISH 1
|
||||
#define SALT 2
|
||||
|
||||
#define DECO 0
|
||||
#define GAUGE 1
|
||||
#define TEC 2
|
||||
#define REC 3
|
||||
|
||||
#define NORMAL 0
|
||||
#define BOOKMARK 1
|
||||
#define ALARM_DEPTH 2
|
||||
#define ALARM_TIME 3
|
||||
#define ALARM_VELOCITY 4
|
||||
#define DECOSTOP 5
|
||||
#define DECOSTOP_BREACHED 6
|
||||
#define GASMIX 7
|
||||
#define SETPOINT 8
|
||||
#define BAILOUT_ON 9
|
||||
#define BAILOUT_OFF 10
|
||||
#define EMERGENCY_ON 11
|
||||
#define EMERGENCY_OFF 12
|
||||
#define LOST_GAS 13
|
||||
#define SAFETY_STOP 14
|
||||
#define TANK_PRESSURE 15
|
||||
#define TANK_LIST 16
|
||||
|
||||
#define NGASMIXES 11
|
||||
#define NTANKS 11
|
||||
|
||||
#define INVALID 0xFFFFFFFF
|
||||
|
||||
typedef struct liquivision_lynx_parser_t liquivision_lynx_parser_t;
|
||||
|
||||
typedef struct liquivision_lynx_gasmix_t {
|
||||
unsigned int oxygen;
|
||||
unsigned int helium;
|
||||
} liquivision_lynx_gasmix_t;
|
||||
|
||||
typedef struct liquivision_lynx_tank_t {
|
||||
unsigned int id;
|
||||
unsigned int beginpressure;
|
||||
unsigned int endpressure;
|
||||
} liquivision_lynx_tank_t;
|
||||
|
||||
struct liquivision_lynx_parser_t {
|
||||
dc_parser_t base;
|
||||
unsigned int model;
|
||||
unsigned int headersize;
|
||||
// Cached fields.
|
||||
unsigned int cached;
|
||||
unsigned int ngasmixes;
|
||||
unsigned int ntanks;
|
||||
liquivision_lynx_gasmix_t gasmix[NGASMIXES];
|
||||
liquivision_lynx_tank_t tank[NTANKS];
|
||||
};
|
||||
|
||||
static dc_status_t liquivision_lynx_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size);
|
||||
static dc_status_t liquivision_lynx_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime);
|
||||
static dc_status_t liquivision_lynx_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
|
||||
static dc_status_t liquivision_lynx_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata);
|
||||
|
||||
static const dc_parser_vtable_t liquivision_lynx_parser_vtable = {
|
||||
sizeof(liquivision_lynx_parser_t),
|
||||
DC_FAMILY_LIQUIVISION_LYNX,
|
||||
liquivision_lynx_parser_set_data, /* set_data */
|
||||
liquivision_lynx_parser_get_datetime, /* datetime */
|
||||
liquivision_lynx_parser_get_field, /* fields */
|
||||
liquivision_lynx_parser_samples_foreach, /* samples_foreach */
|
||||
NULL /* destroy */
|
||||
};
|
||||
|
||||
|
||||
dc_status_t
|
||||
liquivision_lynx_parser_create (dc_parser_t **out, dc_context_t *context, unsigned int model)
|
||||
{
|
||||
liquivision_lynx_parser_t *parser = NULL;
|
||||
|
||||
if (out == NULL)
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
|
||||
// Allocate memory.
|
||||
parser = (liquivision_lynx_parser_t *) dc_parser_allocate (context, &liquivision_lynx_parser_vtable);
|
||||
if (parser == NULL) {
|
||||
ERROR (context, "Failed to allocate memory.");
|
||||
return DC_STATUS_NOMEMORY;
|
||||
}
|
||||
|
||||
// Set the default values.
|
||||
parser->model = model;
|
||||
parser->headersize = (model == XEN) ? SZ_HEADER_XEN : SZ_HEADER_OTHER;
|
||||
parser->cached = 0;
|
||||
parser->ngasmixes = 0;
|
||||
parser->ntanks = 0;
|
||||
for (unsigned int i = 0; i < NGASMIXES; ++i) {
|
||||
parser->gasmix[i].oxygen = 0;
|
||||
parser->gasmix[i].helium = 0;
|
||||
}
|
||||
for (unsigned int i = 0; i < NTANKS; ++i) {
|
||||
parser->tank[i].id = 0;
|
||||
parser->tank[i].beginpressure = 0;
|
||||
parser->tank[i].endpressure = 0;
|
||||
}
|
||||
|
||||
*out = (dc_parser_t *) parser;
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
|
||||
{
|
||||
liquivision_lynx_parser_t *parser = (liquivision_lynx_parser_t *) abstract;
|
||||
|
||||
// Reset the cache.
|
||||
parser->cached = 0;
|
||||
parser->ngasmixes = 0;
|
||||
parser->ntanks = 0;
|
||||
for (unsigned int i = 0; i < NGASMIXES; ++i) {
|
||||
parser->gasmix[i].oxygen = 0;
|
||||
parser->gasmix[i].helium = 0;
|
||||
}
|
||||
for (unsigned int i = 0; i < NTANKS; ++i) {
|
||||
parser->tank[i].id = 0;
|
||||
parser->tank[i].beginpressure = 0;
|
||||
parser->tank[i].endpressure = 0;
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime)
|
||||
{
|
||||
liquivision_lynx_parser_t *parser = (liquivision_lynx_parser_t *) abstract;
|
||||
|
||||
if (abstract->size < parser->headersize)
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
|
||||
const unsigned char *p = abstract->data + 40;
|
||||
|
||||
if (datetime) {
|
||||
datetime->year = array_uint16_le (p + 18);
|
||||
datetime->month = array_uint16_le (p + 16) + 1;
|
||||
datetime->day = array_uint16_le (p + 12) + 1;
|
||||
datetime->hour = array_uint16_le (p + 8);
|
||||
datetime->minute = array_uint16_le (p + 6);
|
||||
datetime->second = array_uint16_le (p + 4);
|
||||
datetime->timezone = DC_TIMEZONE_NONE;
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
|
||||
{
|
||||
liquivision_lynx_parser_t *parser = (liquivision_lynx_parser_t *) abstract;
|
||||
|
||||
if (abstract->size < parser->headersize)
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
|
||||
if (!parser->cached) {
|
||||
dc_status_t rc = liquivision_lynx_parser_samples_foreach (abstract, NULL, NULL);
|
||||
if (rc != DC_STATUS_SUCCESS)
|
||||
return rc;
|
||||
}
|
||||
|
||||
dc_gasmix_t *gasmix = (dc_gasmix_t *) value;
|
||||
dc_tank_t *tank = (dc_tank_t *) value;
|
||||
dc_salinity_t *water = (dc_salinity_t *) value;
|
||||
|
||||
if (value) {
|
||||
switch (type) {
|
||||
case DC_FIELD_DIVETIME:
|
||||
*((unsigned int *) value) = array_uint32_le (abstract->data + 4);
|
||||
break;
|
||||
case DC_FIELD_MAXDEPTH:
|
||||
*((double *) value) = array_uint16_le (abstract->data + 28) / 100.0;
|
||||
break;
|
||||
case DC_FIELD_AVGDEPTH:
|
||||
*((double *) value) = array_uint16_le (abstract->data + 30) / 100.0;
|
||||
break;
|
||||
case DC_FIELD_TEMPERATURE_MINIMUM:
|
||||
*((double *) value) = (signed short) array_uint16_le (abstract->data + 34) / 10.0;
|
||||
break;
|
||||
case DC_FIELD_TEMPERATURE_MAXIMUM:
|
||||
*((double *) value) = (signed short) array_uint16_le (abstract->data + 36) / 10.0;
|
||||
break;
|
||||
case DC_FIELD_SALINITY:
|
||||
switch (abstract->data[38]) {
|
||||
case FRESH:
|
||||
water->type = DC_WATER_FRESH;
|
||||
water->density = 1000.0;
|
||||
break;
|
||||
case BRACKISH:
|
||||
water->type = DC_WATER_SALT;
|
||||
water->density = 1015.0;
|
||||
break;
|
||||
case SALT:
|
||||
water->type = DC_WATER_SALT;
|
||||
water->density = 1025.0;
|
||||
break;
|
||||
default:
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
break;
|
||||
case DC_FIELD_ATMOSPHERIC:
|
||||
*((double *) value) = array_uint16_le (abstract->data + 26) / 1000.0;
|
||||
break;
|
||||
case DC_FIELD_DIVEMODE:
|
||||
if (parser->model == XEN) {
|
||||
*((unsigned int *) value) = DC_DIVEMODE_GAUGE;
|
||||
} else {
|
||||
switch (abstract->data[92] & 0x0F) {
|
||||
case DECO:
|
||||
case TEC:
|
||||
case REC:
|
||||
*((unsigned int *) value) = DC_DIVEMODE_OC;
|
||||
break;
|
||||
case GAUGE:
|
||||
*((unsigned int *) value) = DC_DIVEMODE_GAUGE;
|
||||
break;
|
||||
default:
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DC_FIELD_GASMIX_COUNT:
|
||||
*((unsigned int *) value) = parser->ngasmixes;
|
||||
break;
|
||||
case DC_FIELD_GASMIX:
|
||||
gasmix->helium = parser->gasmix[flags].helium / 100.0;
|
||||
gasmix->oxygen = parser->gasmix[flags].oxygen / 100.0;
|
||||
gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
|
||||
break;
|
||||
case DC_FIELD_TANK_COUNT:
|
||||
*((unsigned int *) value) = parser->ntanks;
|
||||
break;
|
||||
case DC_FIELD_TANK:
|
||||
tank->type = DC_TANKVOLUME_NONE;
|
||||
tank->volume = 0.0;
|
||||
tank->workpressure = 0.0;
|
||||
tank->beginpressure = parser->tank[flags].beginpressure / 100.0;
|
||||
tank->endpressure = parser->tank[flags].endpressure / 100.0;
|
||||
tank->gasmix = DC_GASMIX_UNKNOWN;
|
||||
break;
|
||||
default:
|
||||
return DC_STATUS_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static dc_status_t
|
||||
liquivision_lynx_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
|
||||
{
|
||||
liquivision_lynx_parser_t *parser = (liquivision_lynx_parser_t *) abstract;
|
||||
const unsigned char *data = abstract->data;
|
||||
unsigned int size = abstract->size;
|
||||
|
||||
if (size < parser->headersize)
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
|
||||
// Get the version.
|
||||
unsigned int version = array_uint32_le(data);
|
||||
|
||||
// Get the sample interval.
|
||||
unsigned int interval_idx = data[39];
|
||||
const unsigned int intervals[] = {1, 2, 5, 10, 30, 60};
|
||||
if (interval_idx > C_ARRAY_SIZE(intervals)) {
|
||||
ERROR (abstract->context, "Invalid sample interval index %u", interval_idx);
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
unsigned int interval = intervals[interval_idx];
|
||||
|
||||
// Get the number of samples and events.
|
||||
unsigned int nsamples = array_uint32_le (data + 8);
|
||||
unsigned int nevents = array_uint32_le (data + 12);
|
||||
|
||||
unsigned int ngasmixes = 0;
|
||||
unsigned int ntanks = 0;
|
||||
liquivision_lynx_gasmix_t gasmix[NGASMIXES] = {0};
|
||||
liquivision_lynx_tank_t tank[NTANKS] = {0};
|
||||
unsigned int o2_previous = INVALID, he_previous = INVALID;
|
||||
unsigned int gasmix_idx = INVALID;
|
||||
unsigned int have_gasmix = 0;
|
||||
unsigned int tank_id_previous = INVALID;
|
||||
unsigned int tank_idx = INVALID;
|
||||
unsigned int pressure[NTANKS] = {0};
|
||||
unsigned int have_pressure = 0;
|
||||
unsigned int setpoint = 0, have_setpoint = 0;
|
||||
unsigned int deco = 0, have_deco = 0;
|
||||
|
||||
unsigned int time = 0;
|
||||
unsigned int samples = 0;
|
||||
unsigned int events = 0;
|
||||
unsigned int offset = parser->headersize;
|
||||
while (offset + 2 <= size) {
|
||||
dc_sample_value_t sample = {0};
|
||||
|
||||
unsigned int value = array_uint16_le (data + offset);
|
||||
offset += 2;
|
||||
|
||||
if (value & 0x8000) {
|
||||
if (events >= nevents) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + 4 > size) {
|
||||
ERROR (abstract->context, "Buffer overflow at offset %u", offset);
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
|
||||
unsigned int type = value & 0x7FFF;
|
||||
unsigned int timestamp = array_uint32_le (data + offset + 2);
|
||||
offset += 4;
|
||||
|
||||
// Get the sample length.
|
||||
unsigned int length = 0;
|
||||
switch (type) {
|
||||
case DECOSTOP:
|
||||
case GASMIX:
|
||||
length = 2;
|
||||
break;
|
||||
case SETPOINT:
|
||||
length = 1;
|
||||
break;
|
||||
case TANK_LIST:
|
||||
length = NTANKS * 2;
|
||||
break;
|
||||
case TANK_PRESSURE:
|
||||
if (version == LYNX_V1) {
|
||||
length = 4;
|
||||
} else {
|
||||
length = 6;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + length > size) {
|
||||
ERROR (abstract->context, "Buffer overflow at offset %u", offset);
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
|
||||
unsigned int o2 = 0, he = 0;
|
||||
unsigned int tank_id = 0, tank_pressure = 0;
|
||||
|
||||
switch (type) {
|
||||
case NORMAL:
|
||||
case BOOKMARK:
|
||||
case ALARM_DEPTH:
|
||||
case ALARM_TIME:
|
||||
case ALARM_VELOCITY:
|
||||
case DECOSTOP_BREACHED:
|
||||
case BAILOUT_ON:
|
||||
case BAILOUT_OFF:
|
||||
case EMERGENCY_ON:
|
||||
case EMERGENCY_OFF:
|
||||
case LOST_GAS:
|
||||
case SAFETY_STOP:
|
||||
break;
|
||||
case DECOSTOP:
|
||||
deco = array_uint16_le (data + offset);
|
||||
have_deco = 1;
|
||||
break;
|
||||
case GASMIX:
|
||||
o2 = data[offset + 0];
|
||||
he = data[offset + 1];
|
||||
if (o2 != o2_previous || he != he_previous) {
|
||||
// Find the gasmix in the list.
|
||||
unsigned int i = 0;
|
||||
while (i < ngasmixes) {
|
||||
if (o2 == gasmix[i].oxygen && he == gasmix[i].helium)
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Add it to list if not found.
|
||||
if (i >= ngasmixes) {
|
||||
if (i >= NGASMIXES) {
|
||||
ERROR (abstract->context, "Maximum number of gas mixes reached.");
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
gasmix[i].oxygen = o2;
|
||||
gasmix[i].helium = he;
|
||||
ngasmixes = i + 1;
|
||||
}
|
||||
|
||||
o2_previous = o2;
|
||||
he_previous = he;
|
||||
gasmix_idx = i;
|
||||
have_gasmix = 1;
|
||||
}
|
||||
break;
|
||||
case SETPOINT:
|
||||
setpoint = data[offset];
|
||||
have_setpoint = 1;
|
||||
break;
|
||||
case TANK_PRESSURE:
|
||||
tank_id = array_uint16_le (data + offset + 0);
|
||||
tank_pressure = array_uint16_le (data + offset + 2);
|
||||
if (tank_id != tank_id_previous) {
|
||||
// Find the tank in the list.
|
||||
unsigned int i = 0;
|
||||
while (i < ntanks) {
|
||||
if (tank_id == tank[i].id)
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Add a new tank if necessary.
|
||||
if (i >= ntanks) {
|
||||
if (i >= NTANKS) {
|
||||
ERROR (abstract->context, "Maximum number of tanks reached.");
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
tank[i].id = tank_id;
|
||||
tank[i].beginpressure = tank_pressure;
|
||||
tank[i].endpressure = tank_pressure;
|
||||
ntanks = i + 1;
|
||||
}
|
||||
|
||||
tank_id_previous = tank_id;
|
||||
tank_idx = i;
|
||||
}
|
||||
tank[tank_idx].endpressure = tank_pressure;
|
||||
pressure[tank_idx] = tank_pressure;
|
||||
have_pressure |= 1 << tank_idx;
|
||||
break;
|
||||
case TANK_LIST:
|
||||
break;
|
||||
default:
|
||||
WARNING (abstract->context, "Unknown event %u", type);
|
||||
break;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
events++;
|
||||
} else {
|
||||
if (samples >= nsamples) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the sample length.
|
||||
unsigned int length = 2;
|
||||
if (version == XEO_V1_A || version == XEO_V2_A ||
|
||||
version == XEO_V3_A || version == KAON_V1 ||
|
||||
version == KAON_V2) {
|
||||
length += 14;
|
||||
}
|
||||
|
||||
if (offset + length > size) {
|
||||
ERROR (abstract->context, "Buffer overflow at offset %u", offset);
|
||||
return DC_STATUS_DATAFORMAT;
|
||||
}
|
||||
|
||||
// Time (seconds).
|
||||
time += interval;
|
||||
sample.time = time;
|
||||
if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
|
||||
|
||||
// Depth (1/100 m).
|
||||
sample.depth = value / 100.0;
|
||||
if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);
|
||||
|
||||
// Temperature (1/10 °C).
|
||||
int temperature = (signed short) array_uint16_le (data + offset);
|
||||
sample.temperature = temperature / 10.0;
|
||||
if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
|
||||
|
||||
// Gas mix
|
||||
if (have_gasmix) {
|
||||
sample.gasmix = gasmix_idx;
|
||||
if (callback) callback (DC_SAMPLE_GASMIX, sample, userdata);
|
||||
have_gasmix = 0;
|
||||
}
|
||||
|
||||
// Setpoint (1/10 bar).
|
||||
if (have_setpoint) {
|
||||
sample.setpoint = setpoint / 10.0;
|
||||
if (callback) callback (DC_SAMPLE_SETPOINT, sample, userdata);
|
||||
have_setpoint = 0;
|
||||
}
|
||||
|
||||
// Tank pressure (1/100 bar).
|
||||
if (have_pressure) {
|
||||
for (unsigned int i = 0; i < ntanks; ++i) {
|
||||
if (have_pressure & (1 << i)) {
|
||||
sample.pressure.tank = i;
|
||||
sample.pressure.value = pressure[i] / 100.0;
|
||||
if (callback) callback (DC_SAMPLE_PRESSURE, sample, userdata);
|
||||
}
|
||||
}
|
||||
have_pressure = 0;
|
||||
}
|
||||
|
||||
// Deco/ndl
|
||||
if (have_deco) {
|
||||
if (deco) {
|
||||
sample.deco.type = DC_DECO_DECOSTOP;
|
||||
sample.deco.depth = deco / 100.0;
|
||||
} else {
|
||||
sample.deco.type = DC_DECO_NDL;
|
||||
sample.deco.depth = 0.0;
|
||||
}
|
||||
sample.deco.time = 0;
|
||||
if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
|
||||
have_deco = 0;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the data for later use.
|
||||
for (unsigned int i = 0; i < ntanks; ++i) {
|
||||
parser->tank[i] = tank[i];
|
||||
}
|
||||
for (unsigned int i = 0; i < ngasmixes; ++i) {
|
||||
parser->gasmix[i] = gasmix[i];
|
||||
}
|
||||
parser->ngasmixes = ngasmixes;
|
||||
parser->ntanks = ntanks;
|
||||
parser->cached = 1;
|
||||
|
||||
return DC_STATUS_SUCCESS;
|
||||
}
|
||||
@ -58,6 +58,7 @@
|
||||
#include "cochran_commander.h"
|
||||
#include "tecdiving_divecomputereu.h"
|
||||
#include "mclean_extreme.h"
|
||||
#include "liquivision_lynx.h"
|
||||
|
||||
#include "context-private.h"
|
||||
#include "parser-private.h"
|
||||
@ -176,6 +177,9 @@ dc_parser_new_internal (dc_parser_t **out, dc_context_t *context, dc_family_t fa
|
||||
case DC_FAMILY_MCLEAN_EXTREME:
|
||||
rc = mclean_extreme_parser_create (&parser, context);
|
||||
break;
|
||||
case DC_FAMILY_LIQUIVISION_LYNX:
|
||||
rc = liquivision_lynx_parser_create (&parser, context, model);
|
||||
break;
|
||||
default:
|
||||
return DC_STATUS_INVALIDARGS;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user