libdc/src/shearwater_common.c
Jef Driesen 25bd1f9853 Add support for time synchronization
Add time synchronisation for the Shearwater dive computers. All models
support setting the local time. Only the Teric has basic support for
time zones, and can set UTC time with a timezone offset.

Co-authored-by: Michael Keller <github@ike.ch>
2023-07-05 15:43:12 +02:00

747 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* libdivecomputer
*
* Copyright (C) 2013 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 "shearwater_common.h"
#include "context-private.h"
#include "platform.h"
#include "array.h"
#define SZ_PACKET 254
// SLIP special character codes
#define END 0xC0
#define ESC 0xDB
#define ESC_END 0xDC
#define ESC_ESC 0xDD
#define RDBI_REQUEST 0x22
#define RDBI_RESPONSE 0x62
#define WDBI_REQUEST 0x2E
#define WDBI_RESPONSE 0x6E
#define NAK 0x7F
dc_status_t
shearwater_common_setup (shearwater_common_device_t *device, dc_context_t *context, dc_iostream_t *iostream)
{
dc_status_t status = DC_STATUS_SUCCESS;
device->iostream = iostream;
// 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.");
return status;
}
// Set the timeout for receiving data (3000ms).
status = dc_iostream_set_timeout (device->iostream, 3000);
if (status != DC_STATUS_SUCCESS) {
ERROR (context, "Failed to set the timeout.");
return status;
}
// Make sure everything is in a sane state.
dc_iostream_sleep (device->iostream, 300);
dc_iostream_purge (device->iostream, DC_DIRECTION_ALL);
return DC_STATUS_SUCCESS;
}
static int
shearwater_common_decompress_lre (unsigned char *data, unsigned int size, dc_buffer_t *buffer, unsigned int *isfinal)
{
// The RLE decompression algorithm does interpret the binary data as a
// stream of 9 bit values. Therefore, the total number of bits needs to be
// a multiple of 9 bits.
unsigned int nbits = size * 8;
if (nbits % 9 != 0)
return -1;
unsigned int offset = 0;
while (offset + 9 <= nbits) {
// Extract the 9 bit value.
unsigned int byte = offset / 8;
unsigned int bit = offset % 8;
unsigned int shift = 16 - (bit + 9);
unsigned int value = (array_uint16_be (data + byte) >> shift) & 0x1FF;
// The 9th bit indicates whether the remaining 8 bits represent
// a run of zero bytes or not. If the bit is set, the value is
// not a run and doesnt need expansion. If the bit is not set,
// the value contains the number of zero bytes in the run. A
// zero-length run indicates the end of the compressed stream.
if (value & 0x100) {
// Append the data byte directly.
unsigned char c = value & 0xFF;
if (!dc_buffer_append (buffer, &c, 1))
return -1;
} else if (value == 0) {
// Reached the end of the compressed stream.
if (isfinal)
*isfinal = 1;
break;
} else {
// Expand the run with zero bytes.
if (!dc_buffer_resize (buffer, dc_buffer_get_size (buffer) + value))
return -1;
}
offset += 9;
}
return 0;
}
static int
shearwater_common_decompress_xor (unsigned char *data, unsigned int size)
{
// Each block of 32 bytes is XOR'ed (in-place) with the previous block,
// except for the first block, which is passed through unchanged.
for (unsigned int i = 32; i < size; ++i) {
data[i] ^= data[i - 32];
}
return 0;
}
static dc_status_t
shearwater_common_slip_write (shearwater_common_device_t *device, const unsigned char data[], unsigned int size)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_transport_t transport = dc_iostream_get_transport(device->iostream);
unsigned char buffer[32];
unsigned int nbytes = 0;
if (transport == DC_TRANSPORT_BLE) {
// Calculate the total number of bytes.
unsigned int count = 1;
for (unsigned int i = 0; i < size; ++i) {
unsigned char c = data[i];
if (c == END || c == ESC) {
count += 2;
} else {
count++;
}
}
// Calculate the total number of frames.
unsigned int nframes = (count + sizeof(buffer) - 1) / sizeof(buffer);
buffer[0] = nframes;
buffer[1] = 0;
nbytes = 2;
}
for (unsigned int i = 0; i < size; ++i) {
unsigned char c = data[i];
if (c == END || c == ESC) {
// Append the escape character.
buffer[nbytes++] = ESC;
// Flush the buffer if necessary.
if (nbytes >= sizeof(buffer)) {
status = dc_iostream_write (device->iostream, buffer, nbytes, NULL);
if (status != DC_STATUS_SUCCESS) {
ERROR (device->base.context, "Failed to send the packet.");
return status;
}
if (transport == DC_TRANSPORT_BLE) {
buffer[1]++;
nbytes = 2;
} else {
nbytes = 0;
}
}
// Escape the character.
if (c == END) {
c = ESC_END;
} else {
c = ESC_ESC;
}
}
// Append the character.
buffer[nbytes++] = c;
// Flush the buffer if necessary.
if (nbytes >= sizeof(buffer)) {
status = dc_iostream_write (device->iostream, buffer, nbytes, NULL);
if (status != DC_STATUS_SUCCESS) {
ERROR (device->base.context, "Failed to send the packet.");
return status;
}
if (transport == DC_TRANSPORT_BLE) {
buffer[1]++;
nbytes = 2;
} else {
nbytes = 0;
}
}
}
// Append the END character to indicate the end of the packet.
buffer[nbytes++] = END;
// Flush the buffer.
status = dc_iostream_write (device->iostream, buffer, nbytes, NULL);
if (status != DC_STATUS_SUCCESS) {
ERROR (device->base.context, "Failed to send the packet.");
return status;
}
return DC_STATUS_SUCCESS;
}
static dc_status_t
shearwater_common_slip_read (shearwater_common_device_t *device, unsigned char data[], unsigned int size, unsigned int *actual)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_transport_t transport = dc_iostream_get_transport(device->iostream);
unsigned char buffer[256];
unsigned int escaped = 0;
unsigned int nbytes = 0;
// Get the packet size.
size_t packetsize = (transport == DC_TRANSPORT_BLE) ? sizeof(buffer) : 1;
// Read bytes until a complete packet has been received. If the
// buffer runs out of space, bytes are dropped. The caller can
// detect this condition because the return value will be larger
// than the supplied buffer size.
while (1) {
size_t transferred = 0;
status = dc_iostream_read (device->iostream, buffer, packetsize, &transferred);
if (status != DC_STATUS_SUCCESS) {
ERROR (device->base.context, "Failed to receive the packet.");
return status;
}
size_t offset = 0;
if (transport == DC_TRANSPORT_BLE) {
if (transferred < 2) {
ERROR (device->base.context, "Invalid packet length (" DC_PRINTF_SIZE ").", transferred);
return DC_STATUS_PROTOCOL;
}
offset = 2;
}
for (size_t i = offset; i < transferred; ++i) {
unsigned char c = buffer[i];
if (c == END || c == ESC) {
if (escaped) {
// If the END or ESC characters are escaped, then we
// have a protocol violation, and an error is reported.
ERROR (device->base.context, "SLIP frame escaped the special character %02x.", c);
return DC_STATUS_PROTOCOL;
}
if (c == END) {
// If it's an END character then we're done.
// As a minor optimization, empty packets are ignored. This
// is to avoid bothering the upper layers with all the empty
// packets generated by the duplicate END characters which
// are sent to try to detect line noise.
if (nbytes) {
goto done;
}
} else {
// If it's an ESC character, get another character and then
// figure out what to store in the packet based on that.
escaped = 1;
}
continue;
}
if (escaped) {
// If it's not one of the two escaped characters, then we
// have a protocol violation. The best bet seems to be to
// leave the byte alone and just stuff it into the packet.
switch (c) {
case ESC_END:
c = END;
break;
case ESC_ESC:
c = ESC;
break;
default:
break;
}
escaped = 0;
}
if (nbytes < size)
data[nbytes] = c;
nbytes++;
}
}
done:
if (nbytes > size) {
ERROR (device->base.context, "Insufficient buffer space available.");
return DC_STATUS_PROTOCOL;
}
if (actual)
*actual = nbytes;
return status;
}
dc_status_t
shearwater_common_transfer (shearwater_common_device_t *device, const unsigned char input[], unsigned int isize, unsigned char output[], unsigned int osize, unsigned int *actual)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_device_t *abstract = (dc_device_t *) device;
unsigned char packet[SZ_PACKET + 4];
unsigned int n = 0;
if (isize > SZ_PACKET || osize > SZ_PACKET)
return DC_STATUS_INVALIDARGS;
if (device_is_cancelled (abstract))
return DC_STATUS_CANCELLED;
// Setup the request packet.
packet[0] = 0xFF;
packet[1] = 0x01;
packet[2] = isize + 1;
packet[3] = 0x00;
memcpy (packet + 4, input, isize);
// Send the request packet.
status = shearwater_common_slip_write (device, packet, isize + 4);
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to send the request packet.");
return status;
}
// Return early if no response packet is requested.
if (osize == 0) {
if (actual)
*actual = 0;
return DC_STATUS_SUCCESS;
}
// Receive the response packet.
status = shearwater_common_slip_read (device, packet, sizeof (packet), &n);
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to receive the response packet.");
return status;
}
// Validate the packet header.
if (n < 4 || packet[0] != 0x01 || packet[1] != 0xFF || packet[3] != 0x00) {
ERROR (abstract->context, "Invalid packet header.");
return DC_STATUS_PROTOCOL;
}
// Validate the packet length.
unsigned int length = packet[2];
if (length < 1 || length - 1 + 4 != n || length - 1 > osize) {
ERROR (abstract->context, "Invalid packet header.");
return DC_STATUS_PROTOCOL;
}
memcpy (output, packet + 4, length - 1);
if (actual)
*actual = length - 1;
return DC_STATUS_SUCCESS;
}
dc_status_t
shearwater_common_download (shearwater_common_device_t *device, dc_buffer_t *buffer, unsigned int address, unsigned int size, unsigned int compression, dc_event_progress_t *progress)
{
dc_device_t *abstract = (dc_device_t *) device;
dc_status_t rc = DC_STATUS_SUCCESS;
unsigned int n = 0;
unsigned char req_init[] = {
0x35,
(compression ? 0x10 : 0x00),
0x34,
(address >> 24) & 0xFF,
(address >> 16) & 0xFF,
(address >> 8) & 0xFF,
(address ) & 0xFF,
(size >> 16) & 0xFF,
(size >> 8) & 0xFF,
(size ) & 0xFF};
unsigned char req_block[] = {0x36, 0x00};
unsigned char req_quit[] = {0x37};
unsigned char response[SZ_PACKET];
// Erase the current contents of the buffer.
if (!dc_buffer_clear (buffer)) {
ERROR (abstract->context, "Insufficient buffer space available.");
return DC_STATUS_NOMEMORY;
}
// Enable progress notifications.
unsigned int initial = 0, current = 0, maximum = 3 + size + 1;
if (progress) {
initial = progress->current;
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
}
// Transfer the init request.
rc = shearwater_common_transfer (device, req_init, sizeof (req_init), response, 3, &n);
if (rc != DC_STATUS_SUCCESS) {
return rc;
}
// Verify the init response.
if (n != 3 || response[0] != 0x75 || response[1] != 0x10 || response[2] > SZ_PACKET) {
ERROR (abstract->context, "Unexpected response packet.");
return DC_STATUS_PROTOCOL;
}
// Update and emit a progress event.
if (progress) {
current += 3;
progress->current = initial + STEP (current, maximum);
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
}
unsigned int done = 0;
unsigned char block = 1;
unsigned int nbytes = 0;
while (nbytes < size && !done) {
// Transfer the block request.
req_block[1] = block;
rc = shearwater_common_transfer (device, req_block, sizeof (req_block), response, sizeof (response), &n);
if (rc != DC_STATUS_SUCCESS) {
return rc;
}
// Verify the block header.
if (n < 2 || response[0] != 0x76 || response[1] != block) {
ERROR (abstract->context, "Unexpected response packet.");
return DC_STATUS_PROTOCOL;
}
// Verify the block length.
unsigned int length = n - 2;
if (nbytes + length > size) {
ERROR (abstract->context, "Unexpected packet size.");
return DC_STATUS_PROTOCOL;
}
// Update and emit a progress event.
if (progress) {
current += length;
progress->current = initial + STEP (current, maximum);
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
}
if (compression) {
if (shearwater_common_decompress_lre (response + 2, length, buffer, &done) != 0) {
ERROR (abstract->context, "Decompression error (LRE phase).");
return DC_STATUS_PROTOCOL;
}
} else {
if (!dc_buffer_append (buffer, response + 2, length)) {
ERROR (abstract->context, "Insufficient buffer space available.");
return DC_STATUS_PROTOCOL;
}
}
nbytes += length;
block++;
}
if (compression) {
if (shearwater_common_decompress_xor (dc_buffer_get_data (buffer), dc_buffer_get_size (buffer)) != 0) {
ERROR (abstract->context, "Decompression error (XOR phase).");
return DC_STATUS_PROTOCOL;
}
}
// Transfer the quit request.
rc = shearwater_common_transfer (device, req_quit, sizeof (req_quit), response, 2, &n);
if (rc != DC_STATUS_SUCCESS) {
return rc;
}
// Verify the quit response.
if (n != 2 || response[0] != 0x77 || response[1] != 0x00) {
ERROR (abstract->context, "Unexpected response packet.");
return DC_STATUS_PROTOCOL;
}
// Update and emit a progress event.
if (progress) {
current += 1;
progress->current = initial + STEP (current, maximum);
device_event_emit (abstract, DC_EVENT_PROGRESS, progress);
}
return DC_STATUS_SUCCESS;
}
dc_status_t
shearwater_common_rdbi (shearwater_common_device_t *device, unsigned int id, unsigned char data[], unsigned int size)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_device_t *abstract = (dc_device_t *) device;
// Transfer the request.
unsigned int n = 0;
unsigned char request[] = {
RDBI_REQUEST,
(id >> 8) & 0xFF,
(id ) & 0xFF};
unsigned char response[SZ_PACKET];
status = shearwater_common_transfer (device, request, sizeof (request), response, sizeof (response), &n);
if (status != DC_STATUS_SUCCESS) {
return status;
}
// Verify the response.
if (n < 3 || response[0] != RDBI_RESPONSE || response[1] != request[1] || response[2] != request[2]) {
if (n == 3 && response[0] == NAK && response[1] == RDBI_REQUEST) {
ERROR (abstract->context, "Received NAK packet with error code 0x%02x.", response[2]);
return DC_STATUS_UNSUPPORTED;
}
ERROR (abstract->context, "Unexpected response packet.");
return DC_STATUS_PROTOCOL;
}
unsigned int length = n - 3;
if (length != size) {
ERROR (abstract->context, "Unexpected packet size (%u bytes).", length);
return DC_STATUS_PROTOCOL;
}
if (length) {
memcpy (data, response + 3, length);
}
return status;
}
dc_status_t
shearwater_common_wdbi (shearwater_common_device_t *device, unsigned int id, const unsigned char data[], unsigned int size)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_device_t *abstract = (dc_device_t *) device;
if (size + 3 > SZ_PACKET) {
return DC_STATUS_INVALIDARGS;
}
// Transfer the request.
unsigned int n = 0;
unsigned char request[SZ_PACKET] = {
WDBI_REQUEST,
(id >> 8) & 0xFF,
(id ) & 0xFF};
if (size) {
memcpy (request + 3, data, size);
}
unsigned char response[SZ_PACKET];
status = shearwater_common_transfer (device, request, size + 3, response, sizeof (response), &n);
if (status != DC_STATUS_SUCCESS) {
return status;
}
// Verify the response.
if (n < 3 || response[0] != WDBI_RESPONSE || response[1] != request[1] || response[2] != request[2]) {
if (n == 3 && response[0] == NAK && response[1] == WDBI_REQUEST) {
ERROR (abstract->context, "Received NAK packet with error code 0x%02x.", response[2]);
return DC_STATUS_UNSUPPORTED;
}
ERROR (abstract->context, "Unexpected response packet.");
return DC_STATUS_PROTOCOL;
}
return status;
}
dc_status_t
shearwater_common_timesync_local (shearwater_common_device_t *device, const dc_datetime_t *datetime)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_device_t *abstract = (dc_device_t *) device;
// Convert to local time.
dc_datetime_t local = *datetime;
local.timezone = DC_TIMEZONE_NONE;
dc_ticks_t ticks = dc_datetime_mktime (&local);
if (ticks == -1) {
ERROR (abstract->context, "Invalid date/time value specified.");
return DC_STATUS_INVALIDARGS;
}
const unsigned char timestamp[] = {
(ticks >> 24) & 0xFF,
(ticks >> 16) & 0xFF,
(ticks >> 8) & 0xFF,
(ticks ) & 0xFF,
};
status = shearwater_common_wdbi (device, ID_TIME_LOCAL, timestamp, sizeof(timestamp));
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to write the dive computer local time.");
return status;
}
return status;
}
dc_status_t
shearwater_common_timesync_utc (shearwater_common_device_t *device, const dc_datetime_t *datetime)
{
dc_status_t status = DC_STATUS_SUCCESS;
dc_device_t *abstract = (dc_device_t *) device;
// Convert to UTC time.
dc_ticks_t ticks = dc_datetime_mktime (datetime);
if (ticks == -1) {
ERROR (abstract->context, "Invalid date/time value specified.");
return DC_STATUS_INVALIDARGS;
}
const unsigned char timestamp[] = {
(ticks >> 24) & 0xFF,
(ticks >> 16) & 0xFF,
(ticks >> 8) & 0xFF,
(ticks ) & 0xFF,
};
status = shearwater_common_wdbi (device, ID_TIME_UTC, timestamp, sizeof(timestamp));
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to write the dive computer UTC time.");
return status;
}
int timezone = datetime->timezone / 60;
const unsigned char offset[] = {
(timezone >> 24) & 0xFF,
(timezone >> 16) & 0xFF,
(timezone >> 8) & 0xFF,
(timezone ) & 0xFF,
};
status = shearwater_common_wdbi (device, ID_TIME_OFFSET, offset, sizeof (offset));
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to write the dive computer timezone offset.");
return status;
}
// We don't have a way to determine the daylight savings time setting,
// but the required offset is already factored into the timezone offset.
const unsigned char dst[] = {0, 0, 0, 0};
status = shearwater_common_wdbi (device, ID_TIME_DST, dst, sizeof (dst));
if (status != DC_STATUS_SUCCESS) {
ERROR (abstract->context, "Failed to write the dive computer DST setting.");
return status;
}
return status;
}
unsigned int
shearwater_common_get_model (shearwater_common_device_t *device, unsigned int hardware)
{
unsigned int model = 0;
switch (hardware) {
case 0x0101:
case 0x0202:
model = PREDATOR;
break;
case 0x0404:
case 0x0909:
model = PETREL;
break;
case 0x0505:
case 0x0808:
case 0x0838:
case 0x08A5:
case 0x0B0B:
case 0x7828:
case 0x7B2C:
case 0x8838:
model = PETREL2;
break;
case 0xB407:
model = PETREL3;
break;
case 0x0606:
case 0x0A0A:
model = NERD;
break;
case 0x0E0D:
case 0x7E2D:
model = NERD2;
break;
case 0x0707:
model = PERDIX;
break;
case 0x0C0D:
case 0x7C2D:
case 0x8D6C:
model = PERDIXAI;
break;
case 0xC407:
model = PERDIX2;
break;
case 0x0F0F:
case 0x1F0A:
case 0x1F0F:
model = TERIC;
break;
case 0x1512:
model = PEREGRINE;
break;
default:
WARNING (device->base.context, "Unknown hardware type 0x%04x.", hardware);
}
return model;
}