/*
 * libdivecomputer
 *
 * Copyright (C) 2013 Jef Driesen
 * Copyright (C) 2014 Anton Lundin
 *
 * 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 <stdio.h>  // FILE, fopen

#include "hw_ostc3.h"
#include "context-private.h"
#include "device-private.h"
#include "array.h"
#include "aes.h"
#include "platform.h"

#define ISINSTANCE(device) dc_device_isinstance((device), &hw_ostc3_device_vtable)

#define SZ_DISPLAY    16
#define SZ_CUSTOMTEXT 60
#define SZ_VERSION    (SZ_CUSTOMTEXT + 4)
#define SZ_HARDWARE   1
#define SZ_HARDWARE2  5
#define SZ_MEMORY     0x400000
#define SZ_CONFIG     4
#define SZ_FWINFO     4
#define SZ_FIRMWARE   0x01E000        // 120KB
#define SZ_FIRMWARE_BLOCK    0x1000   //   4KB
#define FIRMWARE_AREA      0x3E0000

#define RB_LOGBOOK_SIZE_COMPACT  16
#define RB_LOGBOOK_SIZE_FULL     256
#define RB_LOGBOOK_COUNT 256

#define S_BLOCK_READ 0x20
#define S_BLOCK_WRITE 0x30
#define S_ERASE    0x42
#define S_READY    0x4C
#define READY      0x4D
#define S_UPGRADE  0x50
#define HARDWARE2  0x60
#define HEADER     0x61
#define CLOCK      0x62
#define CUSTOMTEXT 0x63
#define DIVE       0x66
#define IDENTITY   0x69
#define HARDWARE   0x6A
#define S_FWINFO   0x6B
#define DISPLAY    0x6E
#define COMPACT    0x6D
#define READ       0x72
#define S_UPLOAD   0x73
#define WRITE      0x77
#define RESET      0x78
#define INIT       0xBB
#define EXIT       0xFF

#define INVALID    0xFFFFFFFF
#define UNKNOWN    0x00
#define OSTC3      0x0A
#define OSTC4      0x3B
#define SPORT      0x12
#define CR         0x05

#define NODELAY 0

typedef enum hw_ostc3_state_t {
	OPEN,
	DOWNLOAD,
	SERVICE,
	REBOOTING,
} hw_ostc3_state_t;

typedef struct hw_ostc3_device_t {
	dc_device_t base;
	dc_iostream_t *iostream;
	unsigned int hardware;
	unsigned int feature;
	unsigned int model;
	unsigned char fingerprint[5];
	hw_ostc3_state_t state;
	unsigned char cache[20];
	unsigned int available;
	unsigned int offset;
} hw_ostc3_device_t;

typedef struct hw_ostc3_logbook_t {
	unsigned int size;
	unsigned int profile;
	unsigned int fingerprint;
	unsigned int number;
} hw_ostc3_logbook_t;

typedef struct hw_ostc3_firmware_t {
	unsigned char data[SZ_FIRMWARE];
	unsigned int checksum;
} hw_ostc3_firmware_t;

// This key is used both for the Ostc3 and its cousin,
// the Ostc Sport.
// The Frog uses a similar protocol, and with another key.
static const unsigned char ostc3_key[16] = {
	0xF1, 0xE9, 0xB0, 0x30,
	0x45, 0x6F, 0xBE, 0x55,
	0xFF, 0xE7, 0xF8, 0x31,
	0x13, 0x6C, 0xF2, 0xFE
};

static dc_status_t hw_ostc3_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size);
static dc_status_t hw_ostc3_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size);
static dc_status_t hw_ostc3_device_write (dc_device_t *abstract, unsigned int address, const unsigned char data[], unsigned int size);
static dc_status_t hw_ostc3_device_dump (dc_device_t *abstract, dc_buffer_t *buffer);
static dc_status_t hw_ostc3_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata);
static dc_status_t hw_ostc3_device_timesync (dc_device_t *abstract, const dc_datetime_t *datetime);
static dc_status_t hw_ostc3_device_close (dc_device_t *abstract);

static const dc_device_vtable_t hw_ostc3_device_vtable = {
	sizeof(hw_ostc3_device_t),
	DC_FAMILY_HW_OSTC3,
	hw_ostc3_device_set_fingerprint, /* set_fingerprint */
	hw_ostc3_device_read, /* read */
	hw_ostc3_device_write, /* write */
	hw_ostc3_device_dump, /* dump */
	hw_ostc3_device_foreach, /* foreach */
	hw_ostc3_device_timesync, /* timesync */
	hw_ostc3_device_close /* close */
};

static const hw_ostc3_logbook_t hw_ostc3_logbook_compact = {
	RB_LOGBOOK_SIZE_COMPACT, /* size */
	0,  /* profile */
	3,  /* fingerprint */
	13, /* number */
};

static const hw_ostc3_logbook_t hw_ostc3_logbook_full = {
	RB_LOGBOOK_SIZE_FULL, /* size */
	9,  /* profile */
	12, /* fingerprint */
	80, /* number */
};


static int
hw_ostc3_strncpy (unsigned char *data, unsigned int size, const char *text)
{
	// Check the maximum length.
	size_t length = (text ? strlen (text) : 0);
	if (length > size) {
		return -1;
	}

	// Copy the text.
	if (length)
		memcpy (data, text, length);

	// Pad with spaces.
	memset (data + length, 0x20, size - length);

	return 0;
}

static dc_status_t
hw_ostc3_read (hw_ostc3_device_t *device, dc_event_progress_t *progress, unsigned char data[], size_t size)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	dc_transport_t transport = dc_iostream_get_transport(device->iostream);

	size_t nbytes = 0;
	while (nbytes < size) {
		if (transport == DC_TRANSPORT_BLE) {
			if (device->available == 0) {
				// Read a packet into the cache.
				size_t len = 0;
				rc = dc_iostream_read (device->iostream, device->cache, sizeof(device->cache), &len);
				if (rc != DC_STATUS_SUCCESS)
					return rc;

				device->available = len;
				device->offset = 0;
			}
		}

		// Set the minimum packet size.
		size_t length = (transport == DC_TRANSPORT_BLE) ? device->available : 1024;

		// Limit the packet size to the total size.
		if (nbytes + length > size)
			length = size - nbytes;

		if (transport == DC_TRANSPORT_BLE) {
			// Copy the data from the cached packet.
			memcpy (data + nbytes, device->cache + device->offset, length);
			device->available -= length;
			device->offset += length;
		} else {
			// Read the packet.
			rc = dc_iostream_read (device->iostream, data + nbytes, length, NULL);
			if (rc != DC_STATUS_SUCCESS)
				return rc;
		}

		// Update and emit a progress event.
		if (progress) {
			progress->current += length;
			device_event_emit ((dc_device_t *) device, DC_EVENT_PROGRESS, progress);
		}

		nbytes += length;
	}

	return rc;
}

static dc_status_t
hw_ostc3_write (hw_ostc3_device_t *device, dc_event_progress_t *progress, const unsigned char data[], size_t size)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	dc_transport_t transport = dc_iostream_get_transport(device->iostream);

	size_t nbytes = 0;
	while (nbytes < size) {
		// Set the maximum packet size.
		size_t length = (transport == DC_TRANSPORT_BLE) ? sizeof(device->cache) : 64;

		// Limit the packet size to the total size.
		if (nbytes + length > size)
			length = size - nbytes;

		// Write the packet.
		rc = dc_iostream_write (device->iostream, data + nbytes, length, NULL);
		if (rc != DC_STATUS_SUCCESS)
			return rc;

		// Update and emit a progress event.
		if (progress) {
			progress->current += length;
			device_event_emit ((dc_device_t *) device, DC_EVENT_PROGRESS, progress);
		}

		nbytes += length;
	}

	return rc;
}

static dc_status_t
hw_ostc3_transfer (hw_ostc3_device_t *device,
                  dc_event_progress_t *progress,
                  unsigned char cmd,
                  const unsigned char input[],
                  unsigned int isize,
                  unsigned char output[],
                  unsigned int osize,
                  unsigned int delay)
{
	dc_device_t *abstract = (dc_device_t *) device;
	dc_status_t status = DC_STATUS_SUCCESS;

	if (device_is_cancelled (abstract))
		return DC_STATUS_CANCELLED;

	// Get the correct ready byte for the current state.
	const unsigned char ready = (device->state == SERVICE ? S_READY : READY);

	// Send the command.
	unsigned char command[1] = {cmd};
	status = hw_ostc3_write (device, NULL, command, sizeof (command));
	if (status != DC_STATUS_SUCCESS) {
		ERROR (abstract->context, "Failed to send the command.");
		return status;
	}

	// Read the echo.
	unsigned char echo[1] = {0};
	status = hw_ostc3_read (device, NULL, echo, sizeof (echo));
	if (status != DC_STATUS_SUCCESS) {
		ERROR (abstract->context, "Failed to receive the echo.");
		return status;
	}

	// Verify the echo.
	if (memcmp (echo, command, sizeof (command)) != 0) {
		if (echo[0] == ready) {
			ERROR (abstract->context, "Unsupported command.");
			return DC_STATUS_UNSUPPORTED;
		} else {
			ERROR (abstract->context, "Unexpected echo.");
			return DC_STATUS_PROTOCOL;
		}
	}

	if (input) {
		// Send the input data packet.
		status = hw_ostc3_write (device, progress, input, isize);
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to send the data packet.");
			return status;
		}
	}

	if (output) {
		// Read the ouput data packet.
		status = hw_ostc3_read (device, progress, output, osize);
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to receive the answer.");
			return status;
		}
	}

	if (delay) {
		unsigned int count = delay / 100;
		for (unsigned int i = 0; i < count; ++i) {
			if (device->available)
				break;

			size_t available = 0;
			status = dc_iostream_get_available (device->iostream, &available);
			if (status == DC_STATUS_SUCCESS && available > 0)
				break;

			dc_iostream_sleep (device->iostream, 100);
		}
	}

	if (cmd != EXIT) {
		// Read the ready byte.
		unsigned char answer[1] = {0};
		status = hw_ostc3_read (device, NULL, answer, sizeof (answer));
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to receive the ready byte.");
			return status;
		}

		// Verify the ready byte.
		if (answer[0] != ready) {
			ERROR (abstract->context, "Unexpected ready byte.");
			return DC_STATUS_PROTOCOL;
		}
	}

	return DC_STATUS_SUCCESS;
}


dc_status_t
hw_ostc3_device_open (dc_device_t **out, dc_context_t *context, dc_iostream_t *iostream)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = NULL;

	if (out == NULL)
		return DC_STATUS_INVALIDARGS;

	// Allocate memory.
	device = (hw_ostc3_device_t *) dc_device_allocate (context, &hw_ostc3_device_vtable);
	if (device == NULL) {
		ERROR (context, "Failed to allocate memory.");
		return DC_STATUS_NOMEMORY;
	}

	// Set the default values.
	device->iostream = iostream;
	device->hardware = INVALID;
	device->feature = 0;
	device->model = 0;
	memset (device->fingerprint, 0, sizeof (device->fingerprint));
	memset (device->cache, 0, sizeof (device->cache));
	device->available = 0;
	device->offset = 0;

	// Set the serial communication protocol (115200 8N1).
	status = dc_iostream_configure (device->iostream, 115200, 8, DC_PARITY_NONE, DC_STOPBITS_ONE, DC_FLOWCONTROL_NONE);
	if (status != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to set the terminal attributes.");
		goto error_free;
	}

	// Set the timeout for receiving data (3000ms).
	status = dc_iostream_set_timeout (device->iostream, 3000);
	if (status != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to set the timeout.");
		goto error_free;
	}

	// Make sure everything is in a sane state.
	dc_iostream_sleep (device->iostream, 300);
	dc_iostream_purge (device->iostream, DC_DIRECTION_ALL);

	device->state = OPEN;

	*out = (dc_device_t *) device;

	return DC_STATUS_SUCCESS;

error_free:
	dc_device_deallocate ((dc_device_t *) device);
	return status;
}


static dc_status_t
hw_ostc3_device_id (hw_ostc3_device_t *device, unsigned char data[], unsigned int size)
{
	dc_status_t status = DC_STATUS_SUCCESS;

	if (size != SZ_HARDWARE && size != SZ_HARDWARE2)
		return DC_STATUS_INVALIDARGS;

	// Send the command.
	unsigned char hardware[SZ_HARDWARE2] = {0};
	status = hw_ostc3_transfer (device, NULL, HARDWARE2, NULL, 0, hardware, SZ_HARDWARE2, NODELAY);
	if (status == DC_STATUS_UNSUPPORTED) {
		status = hw_ostc3_transfer (device, NULL, HARDWARE, NULL, 0, hardware + 1, SZ_HARDWARE, NODELAY);
	}
	if (status != DC_STATUS_SUCCESS)
		return status;

	if (size == SZ_HARDWARE2) {
		memcpy (data, hardware, SZ_HARDWARE2);
	} else {
		memcpy (data, hardware + 1, SZ_HARDWARE);
	}

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_init_download (hw_ostc3_device_t *device)
{
	dc_device_t *abstract = (dc_device_t *) device;
	dc_context_t *context = (abstract ? abstract->context : NULL);

	// Send the init command.
	dc_status_t status = hw_ostc3_transfer (device, NULL, INIT, NULL, 0, NULL, 0, NODELAY);
	if (status != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to send the command.");
		return status;
	}

	device->state = DOWNLOAD;

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_init_service (hw_ostc3_device_t *device)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_device_t *abstract = (dc_device_t *) device;
	dc_context_t *context = (abstract ? abstract->context : NULL);

	unsigned char command[] = {0xAA, 0xAB, 0xCD, 0xEF};
	unsigned char output[5];

	// We cant use hw_ostc3_transfer here, due to the different echos
	status = hw_ostc3_write (device, NULL, command, sizeof (command));
	if (status != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to send the command.");
		return status;
	}

	// Give the device some time to enter service mode
	dc_iostream_sleep (device->iostream, 100);

	// Read the response
	status = hw_ostc3_read (device, NULL, output, sizeof (output));
	if (status != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to receive the echo.");
		return status;
	}

	// Verify the response to service mode
	if (output[0] != 0x4B || output[1] != 0xAB ||
			output[2] != 0xCD || output[3] != 0xEF ||
			output[4] != S_READY) {
		ERROR (context, "Failed to verify echo.");
		return DC_STATUS_PROTOCOL;
	}

	device->state = SERVICE;

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_init (hw_ostc3_device_t *device, hw_ostc3_state_t state)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	dc_device_t *abstract = (dc_device_t *) device;

	if (device->state == state) {
		// No change.
		rc = DC_STATUS_SUCCESS;
	} else if (device->state == OPEN) {
		// Change to download or service mode.
		if (state == DOWNLOAD) {
			rc = hw_ostc3_device_init_download(device);
		} else if (state == SERVICE) {
			rc = hw_ostc3_device_init_service(device);
		} else {
			rc = DC_STATUS_INVALIDARGS;
		}
	} else if (device->state == SERVICE && state == DOWNLOAD) {
		// Switching between service and download mode is not possible.
		// But in service mode, all download commands are supported too,
		// so there is no need to change the state.
		rc = DC_STATUS_SUCCESS;
	} else {
		// Not supported.
		rc = DC_STATUS_INVALIDARGS;
	}

	if (rc != DC_STATUS_SUCCESS)
		return rc;

	if (device->hardware != INVALID)
		return DC_STATUS_SUCCESS;

	// Read the hardware descriptor.
	unsigned char hardware[SZ_HARDWARE2] = {0, UNKNOWN};
	rc = hw_ostc3_device_id (device, hardware, sizeof(hardware));
	if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) {
		ERROR (abstract->context, "Failed to read the hardware descriptor.");
		return rc;
	}

	// Cache the descriptor.
	device->hardware = array_uint16_be(hardware + 0);
	device->feature = array_uint16_be(hardware + 2);
	device->model = hardware[4];

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_close (dc_device_t *abstract)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t*) abstract;
	dc_status_t rc = DC_STATUS_SUCCESS;

	// Send the exit command
	if (device->state == DOWNLOAD || device->state == SERVICE) {
		rc = hw_ostc3_transfer (device, NULL, EXIT, NULL, 0, NULL, 0, NODELAY);
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to send the command.");
			dc_status_set_error(&status, rc);
		}
	}

	return status;
}


static dc_status_t
hw_ostc3_device_set_fingerprint (dc_device_t *abstract, const unsigned char data[], unsigned int size)
{
	hw_ostc3_device_t *device = (hw_ostc3_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;
}


dc_status_t
hw_ostc3_device_version (dc_device_t *abstract, unsigned char data[], unsigned int size)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	if (size != SZ_VERSION)
		return DC_STATUS_INVALIDARGS;

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	rc = hw_ostc3_transfer (device, NULL, IDENTITY, NULL, 0, data, size, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}


dc_status_t
hw_ostc3_device_hardware (dc_device_t *abstract, unsigned char data[], unsigned int size)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	if (size != SZ_HARDWARE && size != SZ_HARDWARE2)
		return DC_STATUS_INVALIDARGS;

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	rc = hw_ostc3_device_id (device, data, size);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_foreach (dc_device_t *abstract, dc_dive_callback_t callback, void *userdata)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	// Enable progress notifications.
	dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
	progress.maximum = SZ_MEMORY;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Download the version data.
	unsigned char id[SZ_VERSION] = {0};
	rc = hw_ostc3_device_version (abstract, id, sizeof (id));
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (abstract->context, "Failed to read the version.");
		return rc;
	}

	// Emit a device info event.
	dc_event_devinfo_t devinfo;
	if (device->hardware == OSTC4) {
		devinfo.firmware = array_uint16_le (id + 2);
	} else {
		devinfo.firmware = array_uint16_be (id + 2);
	}
	devinfo.serial = array_uint16_le (id + 0);
	if (device->hardware != UNKNOWN) {
		devinfo.model = device->hardware;
	} else {
		// Fallback to the serial number.
		if (devinfo.serial > 10000)
			devinfo.model = SPORT;
		else
			devinfo.model = OSTC3;
	}
	device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo);

	// Allocate memory.
	unsigned char *header = (unsigned char *) malloc (RB_LOGBOOK_SIZE_FULL * RB_LOGBOOK_COUNT);
	if (header == NULL) {
		ERROR (abstract->context, "Failed to allocate memory.");
		return DC_STATUS_NOMEMORY;
	}

	// Download the compact logbook headers. If the firmware doesn't support
	// compact headers yet, fallback to downloading the full logbook headers.
	// This is slower, but also works for older firmware versions.
	unsigned int compact = 1;
	rc = hw_ostc3_transfer (device, &progress, COMPACT,
              NULL, 0, header, RB_LOGBOOK_SIZE_COMPACT * RB_LOGBOOK_COUNT, NODELAY);
	if (rc == DC_STATUS_UNSUPPORTED) {
		compact = 0;
		rc = hw_ostc3_transfer (device, &progress, HEADER,
		          NULL, 0, header, RB_LOGBOOK_SIZE_FULL * RB_LOGBOOK_COUNT, NODELAY);
	}
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (abstract->context, "Failed to read the header.");
		free (header);
		return rc;
	}

	// Get the correct logbook layout.
	const hw_ostc3_logbook_t *logbook = NULL;
	if (compact) {
		logbook = &hw_ostc3_logbook_compact;
	} else {
		logbook = &hw_ostc3_logbook_full;
	}

	// Locate the most recent dive.
	// The device maintains an internal counter which is incremented for every
	// dive, and the current value at the time of the dive is stored in the
	// dive header. Thus the most recent dive will have the highest value.
	unsigned int count = 0;
	unsigned int latest = 0;
	unsigned int maximum = 0;
	for (unsigned int i = 0; i < RB_LOGBOOK_COUNT; ++i) {
		unsigned int offset = i * logbook->size;

		// Ignore uninitialized header entries.
		if (array_isequal (header + offset, logbook->size, 0xFF))
			continue;

		// Get the internal dive number.
		unsigned int current = array_uint16_le (header + offset + logbook->number);
		if (current > maximum || device->hardware == OSTC4) {
			maximum = current;
			latest = i;
		}

		count++;
	}

	// Calculate the total and maximum size.
	unsigned int ndives = 0;
	unsigned int size = 0;
	unsigned int maxsize = 0;
	for (unsigned int i = 0; i < count; ++i) {
		unsigned int idx = (latest + RB_LOGBOOK_COUNT - i) % RB_LOGBOOK_COUNT;
		unsigned int offset = idx * logbook->size;

		// Uninitialized header entries should no longer be present at this
		// stage, unless the dives are interleaved with empty entries. But
		// that's something we don't support at all.
		if (array_isequal (header + offset, logbook->size, 0xFF)) {
			WARNING (abstract->context, "Unexpected empty header found.");
			break;
		}

		// Calculate the profile length.
		unsigned int length = RB_LOGBOOK_SIZE_FULL + array_uint24_le (header + offset + logbook->profile) - 3;
		if (!compact) {
			// Workaround for a bug in older firmware versions.
			unsigned int firmware = array_uint16_be (header + offset + 0x30);
			if (firmware < 93)
				length -= 3;
		}
		if (length < RB_LOGBOOK_SIZE_FULL) {
			ERROR (abstract->context, "Invalid profile length (%u bytes).", length);
			free (header);
			return DC_STATUS_DATAFORMAT;
		}

		// Check the fingerprint data.
		if (memcmp (header + offset + logbook->fingerprint, device->fingerprint, sizeof (device->fingerprint)) == 0)
			break;

		if (length > maxsize)
			maxsize = length;
		size += length;
		ndives++;
	}

	// Update and emit a progress event.
	progress.maximum = (logbook->size * RB_LOGBOOK_COUNT) + size + ndives;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	// Finish immediately if there are no dives available.
	if (ndives == 0) {
		free (header);
		return DC_STATUS_SUCCESS;
	}

	// Allocate enough memory for the largest dive.
	unsigned char *profile = (unsigned char *) malloc (maxsize);
	if (profile == NULL) {
		ERROR (abstract->context, "Failed to allocate memory.");
		free (header);
		return DC_STATUS_NOMEMORY;
	}

	// Download the dives.
	for (unsigned int i = 0; i < ndives; ++i) {
		unsigned int idx = (latest + RB_LOGBOOK_COUNT - i) % RB_LOGBOOK_COUNT;
		unsigned int offset = idx * logbook->size;

		// Calculate the profile length.
		unsigned int length = RB_LOGBOOK_SIZE_FULL + array_uint24_le (header + offset + logbook->profile) - 3;
		if (!compact) {
			// Workaround for a bug in older firmware versions.
			unsigned int firmware = array_uint16_be (header + offset + 0x30);
			if (firmware < 93)
				length -= 3;
		}

		// Download the dive.
		unsigned char number[1] = {idx};
		rc = hw_ostc3_transfer (device, &progress, DIVE,
			number, sizeof (number), profile, length, NODELAY);
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to read the dive.");
			free (profile);
			free (header);
			return rc;
		}

		// Verify the header in the logbook and profile are identical.
		if (!compact && memcmp (profile, header + offset, logbook->size) != 0) {
			ERROR (abstract->context, "Unexpected profile header.");
			free (profile);
			free (header);
			return rc;
		}

		// Detect invalid profile data.
		unsigned int delta = device->hardware == OSTC4 ? 3 : 0;
		if (length < RB_LOGBOOK_SIZE_FULL + 2 ||
			profile[length - 2] != 0xFD || profile[length - 1] != 0xFD) {
			// A valid profile should have at least a correct 2 byte
			// end-of-profile marker.
			WARNING (abstract->context, "Invalid profile end marker detected!");
			length = RB_LOGBOOK_SIZE_FULL;
		} else if (length == RB_LOGBOOK_SIZE_FULL + 2) {
			// A profile containing only the 2 byte end-of-profile
			// marker is considered a valid empty profile.
		} else if (length < RB_LOGBOOK_SIZE_FULL + 5 + 2 ||
			array_uint24_le (profile + RB_LOGBOOK_SIZE_FULL) + delta != array_uint24_le (profile + 9)) {
			// If there is more data available, then there should be a
			// valid profile header containing a length matching the
			// length in the dive header.
			WARNING (abstract->context, "Invalid profile header detected.");
			length = RB_LOGBOOK_SIZE_FULL;
		}

		if (callback && !callback (profile, length, profile + 12, sizeof (device->fingerprint), userdata))
			break;
	}

	free (profile);
	free (header);

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_timesync (dc_device_t *abstract, const dc_datetime_t *datetime)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (datetime == NULL) {
		ERROR (abstract->context, "Invalid parameter specified.");
		return DC_STATUS_INVALIDARGS;
	}

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	unsigned char packet[6] = {
		datetime->hour, datetime->minute, datetime->second,
		datetime->month, datetime->day, datetime->year - 2000};
	rc = hw_ostc3_transfer (device, NULL, CLOCK, packet, sizeof (packet), NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}


dc_status_t
hw_ostc3_device_display (dc_device_t *abstract, const char *text)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	// Pad the data packet with spaces.
	unsigned char packet[SZ_DISPLAY] = {0};
	if (hw_ostc3_strncpy (packet, sizeof (packet), text) != 0) {
		ERROR (abstract->context, "Invalid parameter specified.");
		return DC_STATUS_INVALIDARGS;
	}

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	rc = hw_ostc3_transfer (device, NULL, DISPLAY, packet, sizeof (packet), NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}


dc_status_t
hw_ostc3_device_customtext (dc_device_t *abstract, const char *text)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	// Pad the data packet with spaces.
	unsigned char packet[SZ_CUSTOMTEXT] = {0};
	if (hw_ostc3_strncpy (packet, sizeof (packet), text) != 0) {
		ERROR (abstract->context, "Invalid parameter specified.");
		return DC_STATUS_INVALIDARGS;
	}

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	rc = hw_ostc3_transfer (device, NULL, CUSTOMTEXT, packet, sizeof (packet), NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}

dc_status_t
hw_ostc3_device_config_read (dc_device_t *abstract, unsigned int config, unsigned char data[], unsigned int size)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	if (device->hardware == OSTC4 ? size != SZ_CONFIG : size > SZ_CONFIG) {
		ERROR (abstract->context, "Invalid parameter specified.");
		return DC_STATUS_INVALIDARGS;
	}

	// Send the command.
	unsigned char command[1] = {config};
	rc = hw_ostc3_transfer (device, NULL, READ, command, sizeof (command), data, size, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}

dc_status_t
hw_ostc3_device_config_write (dc_device_t *abstract, unsigned int config, const unsigned char data[], unsigned int size)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	if (device->hardware == OSTC4 ? size != SZ_CONFIG : size > SZ_CONFIG) {
		ERROR (abstract->context, "Invalid parameter specified.");
		return DC_STATUS_INVALIDARGS;
	}

	// Send the command.
	unsigned char command[SZ_CONFIG + 1] = {config};
	memcpy(command + 1, data, size);
	rc = hw_ostc3_transfer (device, NULL, WRITE, command, size + 1, NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}

dc_status_t
hw_ostc3_device_config_reset (dc_device_t *abstract)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	dc_status_t rc = hw_ostc3_device_init (device, DOWNLOAD);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	// Send the command.
	rc = hw_ostc3_transfer (device, NULL, RESET, NULL, 0, NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS)
		return rc;

	return DC_STATUS_SUCCESS;
}

// This is a variant of fletcher16 with a 16 bit sum instead of an 8 bit sum,
// and modulo 2^16 instead of 2^16-1
static unsigned int
hw_ostc3_firmware_checksum (const unsigned char data[], unsigned int size)
{
	unsigned short low = 0;
	unsigned short high = 0;
	for (unsigned int i = 0; i < size; i++) {
		low  += data[i];
		high += low;
	}
	return (((unsigned int)high) << 16) + low;
}

static dc_status_t
hw_ostc3_firmware_readline (FILE *fp, dc_context_t *context, unsigned int addr, unsigned char data[], unsigned int size)
{
	unsigned char ascii[39];
	unsigned char faddr_byte[3];
	unsigned int faddr = 0;
	int n = 0;

	if (size > 16) {
		ERROR (context, "Invalid arguments.");
		return DC_STATUS_INVALIDARGS;
	}

	// Read the start code.
	while (1) {
		n = fread (ascii, 1, 1, fp);
		if (n != 1) {
			ERROR (context, "Failed to read the start code.");
			return DC_STATUS_IO;
		}

		if (ascii[0] == ':')
			break;

		// Ignore CR and LF characters.
		if (ascii[0] != '\n' && ascii[0] != '\r') {
			ERROR (context, "Unexpected character (0x%02x).", ascii[0]);
			return DC_STATUS_DATAFORMAT;
		}
	}

	// Read the payload.
	n = fread (ascii + 1, 1, 6 + size * 2, fp);
	if (n != 6 + size * 2) {
		ERROR (context, "Failed to read the data.");
		return DC_STATUS_IO;
	}

	// Convert the address to binary representation.
	if (array_convert_hex2bin(ascii + 1, 6, faddr_byte, sizeof(faddr_byte)) != 0) {
		ERROR (context, "Invalid hexadecimal character.");
		return DC_STATUS_DATAFORMAT;
	}

	// Get the address.
	faddr = array_uint24_be (faddr_byte);
	if (faddr != addr) {
		ERROR (context, "Unexpected address (0x%06x, 0x%06x).", faddr, addr);
		return DC_STATUS_DATAFORMAT;
	}

	// Convert the payload to binary representation.
	if (array_convert_hex2bin (ascii + 1 + 6, size * 2, data, size) != 0) {
		ERROR (context, "Invalid hexadecimal character.");
		return DC_STATUS_DATAFORMAT;
	}

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_firmware_readfile3 (hw_ostc3_firmware_t *firmware, dc_context_t *context, const char *filename)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	FILE *fp = NULL;
	unsigned char iv[16] = {0};
	unsigned char tmpbuf[16] = {0};
	unsigned char encrypted[16] = {0};
	unsigned int bytes = 0, addr = 0;
	unsigned char checksum[4];

	if (firmware == NULL) {
		ERROR (context, "Invalid arguments.");
		return DC_STATUS_INVALIDARGS;
	}

	// Initialize the buffers.
	memset (firmware->data, 0xFF, sizeof (firmware->data));
	firmware->checksum = 0;

	fp = fopen (filename, "rb");
	if (fp == NULL) {
		ERROR (context, "Failed to open the file.");
		return DC_STATUS_IO;
	}

	rc = hw_ostc3_firmware_readline (fp, context, 0, iv, sizeof(iv));
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to parse header.");
		fclose (fp);
		return rc;
	}
	bytes += 16;

	// Load the iv for AES-FCB-mode
	AES128_ECB_encrypt (iv, ostc3_key, tmpbuf);

	for (addr = 0; addr < SZ_FIRMWARE; addr += 16, bytes += 16) {
		rc = hw_ostc3_firmware_readline (fp, context, bytes, encrypted, sizeof(encrypted));
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (context, "Failed to parse file data.");
			fclose (fp);
			return rc;
		}

		// Decrypt AES-FCB data
		for (unsigned int i = 0; i < 16; i++)
			firmware->data[addr + i] = encrypted[i] ^ tmpbuf[i];

		// Run the next round of encryption
		AES128_ECB_encrypt (encrypted, ostc3_key, tmpbuf);
	}

	// This file format contains a tail with the checksum in
	rc = hw_ostc3_firmware_readline (fp, context, bytes, checksum, sizeof(checksum));
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to parse file tail.");
		fclose (fp);
		return rc;
	}

	fclose (fp);

	unsigned int csum1 = array_uint32_le (checksum);
	unsigned int csum2 = hw_ostc3_firmware_checksum (firmware->data, sizeof(firmware->data));
	if (csum1 != csum2) {
		ERROR (context, "Failed to verify file checksum.");
		return DC_STATUS_DATAFORMAT;
	}

	firmware->checksum = csum1;

	return DC_STATUS_SUCCESS;
}

static dc_status_t
hw_ostc3_firmware_readfile4 (dc_buffer_t *buffer, dc_context_t *context, const char *filename)
{
	FILE *fp = NULL;

	if (buffer == NULL) {
		ERROR (context, "Invalid arguments.");
		return DC_STATUS_INVALIDARGS;
	}

	// Open the file.
	fp = fopen (filename, "rb");
	if (fp == NULL) {
		ERROR (context, "Failed to open the file.");
		return DC_STATUS_IO;
	}

	// Read the entire file into the buffer.
	size_t n = 0;
	unsigned char block[1024] = {0};
	while ((n = fread (block, 1, sizeof (block), fp)) > 0) {
		if (!dc_buffer_append (buffer, block, n)) {
			ERROR (context, "Insufficient buffer space available.");
			fclose (fp);
			return DC_STATUS_NOMEMORY;
		}
	}

	// Close the file.
	fclose (fp);

	// Verify the minimum size.
	size_t size = dc_buffer_get_size (buffer);
	if (size < 4) {
		ERROR (context, "Invalid file size.");
		return DC_STATUS_DATAFORMAT;

	}

	// Verify the checksum.
	const unsigned char *data = dc_buffer_get_data (buffer);
	unsigned int csum1 = array_uint32_le (data + size - 4);
	unsigned int csum2 = hw_ostc3_firmware_checksum (data, size - 4);
	if (csum1 != csum2) {
		ERROR (context, "Failed to verify file checksum.");
		return DC_STATUS_DATAFORMAT;
	}

	// Remove the checksum.
	dc_buffer_slice (buffer, 0, size - 4);

	return DC_STATUS_SUCCESS;
}

static dc_status_t
hw_ostc3_firmware_erase (hw_ostc3_device_t *device, unsigned int addr, unsigned int size)
{
	// Convert size to number of pages, rounded up.
	unsigned char blocks = ((size + SZ_FIRMWARE_BLOCK - 1) / SZ_FIRMWARE_BLOCK);

	// Erase just the needed pages.
	unsigned char buffer[4];
	array_uint24_be_set (buffer, addr);
	buffer[3] = blocks;

	return hw_ostc3_transfer (device, NULL, S_ERASE, buffer, sizeof (buffer), NULL, 0, NODELAY);
}

static dc_status_t
hw_ostc3_firmware_block_read (hw_ostc3_device_t *device, unsigned int addr, unsigned char block[], unsigned int block_size)
{
	unsigned char buffer[6];
	array_uint24_be_set (buffer, addr);
	array_uint24_be_set (buffer + 3, block_size);

	return hw_ostc3_transfer (device, NULL, S_BLOCK_READ, buffer, sizeof (buffer), block, block_size, NODELAY);
}

static dc_status_t
hw_ostc3_firmware_block_write (hw_ostc3_device_t *device, unsigned int addr, const unsigned char block[], unsigned int block_size)
{
	unsigned char buffer[3 + SZ_FIRMWARE_BLOCK];

	// We currenty only support writing max SZ_FIRMWARE_BLOCK sized blocks.
	if (block_size > SZ_FIRMWARE_BLOCK)
		return DC_STATUS_INVALIDARGS;

	array_uint24_be_set (buffer, addr);
	memcpy (buffer + 3, block, block_size);

	return hw_ostc3_transfer (device, NULL, S_BLOCK_WRITE, buffer, 3 + block_size, NULL, 0, NODELAY);
}

static dc_status_t
hw_ostc3_firmware_upgrade (dc_device_t *abstract, unsigned int checksum)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;
	dc_context_t *context = (abstract ? abstract->context : NULL);
	unsigned char buffer[5];
	array_uint32_le_set (buffer, checksum);

	// Compute a one byte checksum, so the device can validate the firmware image.
	buffer[4] = 0x55;
	for (unsigned int i = 0; i < 4; i++) {
		buffer[4] ^= buffer[i];
		buffer[4]  = (buffer[4]<<1 | buffer[4]>>7);
	}

	rc = hw_ostc3_transfer (device, NULL, S_UPGRADE, buffer, sizeof (buffer), NULL, 0, NODELAY);
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to send flash firmware command");
		return rc;
	}

	// Now the device resets, and if everything is well, it reprograms.
	device->state = REBOOTING;

	return DC_STATUS_SUCCESS;
}


static dc_status_t
hw_ostc3_device_fwupdate3 (dc_device_t *abstract, const char *filename)
{
	dc_status_t rc = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;
	dc_context_t *context = (abstract ? abstract->context : NULL);

	// Enable progress notifications.
	// load, erase, upload FZ, verify FZ, reprogram
	dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
	progress.maximum = 3 + SZ_FIRMWARE * 2 / SZ_FIRMWARE_BLOCK;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	// Allocate memory for the firmware data.
	hw_ostc3_firmware_t *firmware = (hw_ostc3_firmware_t *) malloc (sizeof (hw_ostc3_firmware_t));
	if (firmware == NULL) {
		ERROR (context, "Failed to allocate memory.");
		return DC_STATUS_NOMEMORY;
	}

	// Read the hex file.
	rc = hw_ostc3_firmware_readfile3 (firmware, context, filename);
	if (rc != DC_STATUS_SUCCESS) {
		free (firmware);
		return rc;
	}

	// Device open and firmware loaded
	progress.current++;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	hw_ostc3_device_display (abstract, " Erasing FW...");

	rc = hw_ostc3_firmware_erase (device, FIRMWARE_AREA, SZ_FIRMWARE);
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to erase old firmware");
		free (firmware);
		return rc;
	}

	// Memory erased
	progress.current++;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	hw_ostc3_device_display (abstract, " Uploading...");

	for (unsigned int len = 0; len < SZ_FIRMWARE; len += SZ_FIRMWARE_BLOCK) {
		char status[SZ_DISPLAY + 1]; // Status message on the display
		snprintf (status, sizeof(status), " Uploading %2d%%", (100 * len) / SZ_FIRMWARE);
		hw_ostc3_device_display (abstract, status);

		rc = hw_ostc3_firmware_block_write (device, FIRMWARE_AREA + len, firmware->data + len, SZ_FIRMWARE_BLOCK);
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (context, "Failed to write block to device");
			free(firmware);
			return rc;
		}
		// One block uploaded
		progress.current++;
		device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
	}

	hw_ostc3_device_display (abstract, " Verifying...");

	for (unsigned int len = 0; len < SZ_FIRMWARE; len += SZ_FIRMWARE_BLOCK) {
		unsigned char block[SZ_FIRMWARE_BLOCK];
		char status[SZ_DISPLAY + 1]; // Status message on the display
		snprintf (status, sizeof(status), " Verifying %2d%%", (100 * len) / SZ_FIRMWARE);
		hw_ostc3_device_display (abstract, status);

		rc = hw_ostc3_firmware_block_read (device, FIRMWARE_AREA + len, block, sizeof (block));
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (context, "Failed to read block.");
			free (firmware);
			return rc;
		}
		if (memcmp (firmware->data + len, block, sizeof (block)) != 0) {
			ERROR (context, "Failed verify.");
			hw_ostc3_device_display (abstract, " Verify FAILED");
			free (firmware);
			return DC_STATUS_PROTOCOL;
		}
		// One block verified
		progress.current++;
		device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
	}

	hw_ostc3_device_display (abstract, " Programming...");

	rc = hw_ostc3_firmware_upgrade (abstract, firmware->checksum);
	if (rc != DC_STATUS_SUCCESS) {
		ERROR (context, "Failed to start programing");
		free (firmware);
		return rc;
	}

	// Programing done!
	progress.current++;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	free (firmware);

	// Finished!
	return DC_STATUS_SUCCESS;
}

static dc_status_t
hw_ostc3_device_fwupdate4 (dc_device_t *abstract, const char *filename)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;
	dc_context_t *context = (abstract ? abstract->context : NULL);

	// Allocate memory for the firmware data.
	dc_buffer_t *buffer = dc_buffer_new (0);
	if (buffer == NULL) {
		ERROR (context, "Failed to allocate memory.");
		status = DC_STATUS_NOMEMORY;
		goto error;
	}

	// Read the firmware file.
	status = hw_ostc3_firmware_readfile4 (buffer, context, filename);
	if (status != DC_STATUS_SUCCESS) {
		goto error;
	}

	// Enable progress notifications.
	dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
	progress.maximum = dc_buffer_get_size (buffer);
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	// Cache the pointer and size.
	const unsigned char *data = dc_buffer_get_data (buffer);
	unsigned int size = dc_buffer_get_size (buffer);

	unsigned int offset = 0;
	while (offset + 4 <= size) {
		// Get the length of the firmware blob.
		unsigned int length = array_uint32_be(data + offset) + 20;
		if (offset + length > size) {
			status = DC_STATUS_DATAFORMAT;
			goto error;
		}

		// Get the blob type.
		unsigned char type = data[offset + 4];

		// Estimate the required delay.
		// After uploading the firmware blob, the device writes the data
		// to flash memory. Since this takes a significant amount of
		// time, the ready byte is delayed. Therefore, the standard
		// timeout is no longer sufficient. The delays are estimated
		// based on actual measurements of the delay per byte.
		unsigned int usecs = length;
		if (type == 0xFF) {
			// Firmware
			usecs *= 50;
		} else if (type == 0xFE) {
			// RTE
			usecs *= 500;
		} else {
			// Fonts
			usecs *= 25;
		}

		// Read the firmware version info.
		unsigned char fwinfo[SZ_FWINFO] = {0};
		status = hw_ostc3_transfer (device, NULL, S_FWINFO,
			data + offset + 4, 1, fwinfo, sizeof(fwinfo), NODELAY);
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to read the firmware info.");
			goto error;
		}

		// Upload the firmware blob.
		// The update is skipped if the two versions are already
		// identical, or if the blob is not present on the device.
		if (memcmp(data + offset + 12, fwinfo, sizeof(fwinfo)) != 0 &&
			!array_isequal(fwinfo, sizeof(fwinfo), 0xFF))
		{
			status = hw_ostc3_transfer (device, &progress, S_UPLOAD,
				data + offset, length, NULL, 0, usecs / 1000);
			if (status != DC_STATUS_SUCCESS) {
				goto error;
			}
		} else {
			// Update and emit a progress event.
			progress.current += length;
			device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);
		}

		offset += length;
	}

error:
	dc_buffer_free (buffer);
	return status;
}

dc_status_t
hw_ostc3_device_fwupdate (dc_device_t *abstract, const char *filename)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if (!ISINSTANCE (abstract))
		return DC_STATUS_INVALIDARGS;

	// Make sure the device is in service mode.
	status = hw_ostc3_device_init (device, SERVICE);
	if (status != DC_STATUS_SUCCESS) {
		return status;
	}

	if (device->hardware == OSTC4) {
		return hw_ostc3_device_fwupdate4 (abstract, filename);
	} else {
		return hw_ostc3_device_fwupdate3 (abstract, filename);
	}
}

static dc_status_t
hw_ostc3_device_read (dc_device_t *abstract, unsigned int address, unsigned char data[], unsigned int size)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if ((address % SZ_FIRMWARE_BLOCK != 0) ||
		(size % SZ_FIRMWARE_BLOCK != 0)) {
		ERROR (abstract->context, "Address or size not aligned to the page size!");
		return DC_STATUS_INVALIDARGS;
	}

	// Make sure the device is in service mode.
	status = hw_ostc3_device_init (device, SERVICE);
	if (status != DC_STATUS_SUCCESS) {
		return status;
	}

	if (device->hardware == OSTC4) {
		return DC_STATUS_UNSUPPORTED;
	}

	unsigned int nbytes = 0;
	while (nbytes < size) {
		// Read a memory page.
		status = hw_ostc3_firmware_block_read (device, address + nbytes, data + nbytes, SZ_FIRMWARE_BLOCK);
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to read block.");
			return status;
		}

		nbytes += SZ_FIRMWARE_BLOCK;
	}

	return DC_STATUS_SUCCESS;
}

static dc_status_t
hw_ostc3_device_write (dc_device_t *abstract, unsigned int address, const unsigned char data[], unsigned int size)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	if ((address % SZ_FIRMWARE_BLOCK != 0) ||
		(size % SZ_FIRMWARE_BLOCK != 0)) {
		ERROR (abstract->context, "Address or size not aligned to the page size!");
		return DC_STATUS_INVALIDARGS;
	}

	// Make sure the device is in service mode.
	status = hw_ostc3_device_init (device, SERVICE);
	if (status != DC_STATUS_SUCCESS) {
		return status;
	}

	if (device->hardware == OSTC4) {
		return DC_STATUS_UNSUPPORTED;
	}

	// Erase the memory pages.
	status = hw_ostc3_firmware_erase (device, address, size);
	if (status != DC_STATUS_SUCCESS) {
		ERROR (abstract->context, "Failed to erase blocks.");
		return status;
	}

	unsigned int nbytes = 0;
	while (nbytes < size) {
		// Write a memory page.
		status = hw_ostc3_firmware_block_write (device, address + nbytes, data + nbytes, SZ_FIRMWARE_BLOCK);
		if (status != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to write block.");
			return status;
		}

		nbytes += SZ_FIRMWARE_BLOCK;
	}

	return DC_STATUS_SUCCESS;
}

static dc_status_t
hw_ostc3_device_dump (dc_device_t *abstract, dc_buffer_t *buffer)
{
	hw_ostc3_device_t *device = (hw_ostc3_device_t *) abstract;

	// Enable progress notifications.
	dc_event_progress_t progress = EVENT_PROGRESS_INITIALIZER;
	progress.maximum = SZ_MEMORY;
	device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

	// Make sure the device is in service mode
	dc_status_t rc = hw_ostc3_device_init (device, SERVICE);
	if (rc != DC_STATUS_SUCCESS) {
		return rc;
	}

	// Allocate the required amount of memory.
	if (!dc_buffer_resize (buffer, SZ_MEMORY)) {
		ERROR (abstract->context, "Insufficient buffer space available.");
		return DC_STATUS_NOMEMORY;
	}

	unsigned char *data = dc_buffer_get_data (buffer);

	unsigned int nbytes = 0;
	while (nbytes < SZ_MEMORY) {
		// packet size. Can be almost arbetary size.
		unsigned int len = SZ_FIRMWARE_BLOCK;

		// Read a block
		rc = hw_ostc3_firmware_block_read (device, nbytes, data + nbytes, len);
		if (rc != DC_STATUS_SUCCESS) {
			ERROR (abstract->context, "Failed to read block.");
			return rc;
		}

		// Update and emit a progress event.
		progress.current += len;
		device_event_emit (abstract, DC_EVENT_PROGRESS, &progress);

		nbytes += len;
	}

	return DC_STATUS_SUCCESS;
}
