/*
 * Copyright (c) 2017, Suprock Technologies
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <float.h>

#include "serialize.h" // for reading values from bytes and writing values to bytes
#include "snprintf.h" // for msvc++ compatability

#include "asphodel.h"

#if defined(_MSC_VER) && _MSC_VER < 1900 // NOTE: 1900 is VS2015
#define INFINITY (DBL_MAX+DBL_MAX)
#endif

#define DEFAULT_POLL_TIME 250 // 0.25 seconds

typedef struct {
	AsphodelCommandCallback_t callback;
	void * closure;
	void * data[];
} BasicTransferClosure_t;

typedef struct {
	int completed;
	int status;
} BlockingClosure_t;

static void command_blocking_callback(int status, void * closure)
{
	BlockingClosure_t *c = (BlockingClosure_t *)closure;
	c->status = status;
	c->completed = 1;
}

#define MAKE_BLOCKING(device, func_call) \
do { \
	BlockingClosure_t c = {0, 0}; \
	void * closure = (void *)&c; \
	AsphodelCommandCallback_t callback = command_blocking_callback; \
	int status = func_call; \
	if (status != 0) return status; \
	while (!c.completed) { \
		status = device->poll_device(device, DEFAULT_POLL_TIME, &c.completed); \
		if (status != 0) return status; \
	} \
	return c.status; \
} while(0)


// for internal use only
static int asphodel_get_stream_channels_malloc(AsphodelDevice_t *device, int index, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure);
static int asphodel_get_channel_name_malloc(AsphodelDevice_t *device, int index, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure);
static int asphodel_get_channel_coefficients_malloc(AsphodelDevice_t *device, int index, float **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure);
static int asphodel_get_channel_chunk_malloc(AsphodelDevice_t *device, int index, uint8_t chunk_number, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure);

static void asphodel_get_protocol_version_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint16_t *version = (uint16_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 2)
		{
			*version = read_16bit_value(&params[0]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the protocol version supported by this device.
ASPHODEL_API int asphodel_get_protocol_version(AsphodelDevice_t *device, uint16_t *version, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = version;

	ret = device->do_transfer(device, CMD_GET_PROTOCOL_VERSION, NULL, 0, asphodel_get_protocol_version_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_protocol_version_blocking(AsphodelDevice_t *device, uint16_t *version)
{
	MAKE_BLOCKING(device, asphodel_get_protocol_version(device, version, callback, closure));
}

static void asphodel_get_protocol_version_string_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	char *buffer = (char*)c->data[0];
	size_t buffer_size = (size_t)c->data[1];

	if (status == 0)
	{
		if (param_length == 2)
		{
			uint8_t major = params[0];
			uint8_t minor = params[1] >> 4;
			uint8_t subminor = params[1] & 0x0F;

			snprintf(buffer, buffer_size, "%d.%d.%d", major, minor, subminor);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

ASPHODEL_API int asphodel_get_protocol_version_string(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_PROTOCOL_VERSION, NULL, 0, asphodel_get_protocol_version_string_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_protocol_version_string_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_protocol_version_string(device, buffer, buffer_size, callback, closure));
}

static void asphodel_get_board_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *rev = (uint8_t*)c->data[0];
	char *buffer = (char*)c->data[1];
	size_t buffer_size = (size_t)c->data[2];

	if (status == 0)
	{
		if (param_length >= 1)
		{
			size_t i = 0;

			*rev = params[0];

			while (i < param_length - 1 && i < buffer_size - 1)
			{
				buffer[i] = params[1 + i];
				i += 1;
			}

			// null terminate the buffer
			buffer[i] = '\0';
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the board revision number along with board name in string form (UTF-8).
ASPHODEL_API int asphodel_get_board_info(AsphodelDevice_t *device, uint8_t *rev, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 3 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = rev;
	c->data[1] = buffer;
	c->data[2] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_BOARD_INFO, NULL, 0, asphodel_get_board_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_board_info_blocking(AsphodelDevice_t *device, uint8_t *rev, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_board_info(device, rev, buffer, buffer_size, callback, closure));
}

static void asphodel_get_user_tag_locations_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	size_t *locations = (size_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 12)
		{
			locations[0] = read_16bit_value(&params[0]) * 4;
			locations[1] = read_16bit_value(&params[2]) * 4;
			locations[2] = read_16bit_value(&params[4]) * 4;
			locations[3] = read_16bit_value(&params[6]) * 4;
			locations[4] = read_16bit_value(&params[8]) * 4;
			locations[5] = read_16bit_value(&params[10]) * 4;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with the user tag offsets and lengths. Locations must be an arary of length 6.
ASPHODEL_API int asphodel_get_user_tag_locations(AsphodelDevice_t *device, size_t *locations, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = locations;

	ret = device->do_transfer(device, CMD_GET_USER_TAG_LOCATIONS, NULL, 0, asphodel_get_user_tag_locations_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_user_tag_locations_blocking(AsphodelDevice_t *device, size_t *locations)
{
	MAKE_BLOCKING(device, asphodel_get_user_tag_locations(device, locations, callback, closure));
}

static void simple_string_transfer_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	char *buffer = (char*)c->data[0];
	size_t buffer_size = (size_t)c->data[1];

	if (status == 0)
	{
		// Any value of param_length is acceptable

		size_t i = 0;

		while (i < param_length && i < buffer_size - 1)
		{
			buffer[i] = params[i];
			i += 1;
		}

		// null terminate the buffer
		buffer[i] = '\0';
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the build info of the device's firmware in string form (UTF-8).
ASPHODEL_API int asphodel_get_build_info(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_BUILD_INFO, NULL, 0, simple_string_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_build_info_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_build_info(device, buffer, buffer_size, callback, closure));
}

// Return the build date of the device's firmware in string form (UTF-8).
ASPHODEL_API int asphodel_get_build_date(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_BUILD_DATE, NULL, 0, simple_string_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_build_date_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_build_date(device, buffer, buffer_size, callback, closure));
}

// Return the chip family of the device's processor (e.g. "XMega") in string form (UTF-8).
ASPHODEL_API int asphodel_get_chip_family(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_CHIP_FAMILY, NULL, 0, simple_string_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_chip_family_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_chip_family(device, buffer, buffer_size, callback, closure));
}

// Return the chip model of the device's processor (e.g. "ATxmega256A3U") in string form (UTF-8).
ASPHODEL_API int asphodel_get_chip_model(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_CHIP_MODEL, NULL, 0, simple_string_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_chip_model_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_chip_model(device, buffer, buffer_size, callback, closure));
}

static void asphodel_get_chip_id_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	char *buffer = (char*)c->data[0];
	size_t buffer_size = (size_t)c->data[1];

	if (status == 0)
	{
		// Any value of param_length is acceptable

		size_t i = 0;

		while (i < (param_length * 2) && i < buffer_size - 1)
		{
			int nibble;

			if (i % 2 == 0)
			{
				// upper nibble
				nibble = params[i / 2] >> 4;
			}
			else
			{
				// lower nibble
				nibble = params[i / 2] & 0x0F;
			}

			if (nibble < 10)
			{
				buffer[i] = '0' + nibble;
			}
			else
			{
				buffer[i] = 'A' + (nibble - 10);
			}

			i += 1;
		}

		// null terminate the buffer
		buffer[i] = '\0';
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the chip ID of the processor in string form (UTF-8).
ASPHODEL_API int asphodel_get_chip_id(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_CHIP_ID, NULL, 0, asphodel_get_chip_id_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_chip_id_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_chip_id(device, buffer, buffer_size, callback, closure));
}

static void asphodel_get_nvm_size_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	size_t *size = (size_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 2)
		{
			*size = read_16bit_value(&params[0]) * 4;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the size of the NVM region in bytes.
ASPHODEL_API int asphodel_get_nvm_size(AsphodelDevice_t *device, size_t *size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = size;

	ret = device->do_transfer(device, CMD_GET_NVM_SIZE, NULL, 0, asphodel_get_nvm_size_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_nvm_size_blocking(AsphodelDevice_t *device, size_t *size)
{
	MAKE_BLOCKING(device, asphodel_get_nvm_size(device, size, callback, closure));
}

static void simple_no_reply_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;

	if (status == 0)
	{
		if (param_length == 0)
		{
			// nothing to do
			(void)params; // suppress unused parameter warning
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

static void asphodel_erase_nvm_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;

	if (status == 0)
	{
		if (param_length == 0)
		{
			// nothing to do
			(void)params; // suppress unused parameter warning
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_INCOMPLETE)
	{
		AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[0];
		uint16_t iteration_number = (uint16_t)(intptr_t)c->data[1];
		uint8_t params[2];

		iteration_number += 1;
		c->data[1] = (void*)(intptr_t)iteration_number;

		write_16bit_value(&params[0], iteration_number);

		status = device->do_transfer(device, CMD_ERASE_NVM, params, sizeof(params), asphodel_erase_nvm_cb, c);

		if (status == 0)
		{
			return;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Erase the NVM region.
ASPHODEL_API int asphodel_erase_nvm(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = device;
	c->data[1] = (void*)(intptr_t)0;

	ret = device->do_transfer(device, CMD_ERASE_NVM, NULL, 0, asphodel_erase_nvm_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_erase_nvm_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_erase_nvm(device, callback, closure));
}

// Write bytes to the NVM region. The start_address is given in bytes, and must be a multiple of 4.
// The length of the data must be a multiple of 4 and must be at most 2 less than the device's maximum outgoing
// parameter length. See write_nvm_section for a more user friendly function.
ASPHODEL_API int asphodel_write_nvm_raw(AsphodelDevice_t *device, size_t start_address, const uint8_t *data, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t *params;
	size_t i;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	params = (uint8_t *)malloc(length + 2);
	if (!params)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	params[0] = ((start_address / 4) >> 8) & 0xFF;
	params[1] = (start_address / 4) & 0xFF;

	for (i = 0; i < length; i++)
	{
		params[2 + i] = data[i];
	}

	ret = device->do_transfer(device, CMD_WRITE_NVM, params, length + 2, simple_no_reply_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_write_nvm_raw_blocking(AsphodelDevice_t *device, size_t start_address, const uint8_t *data, size_t length)
{
	MAKE_BLOCKING(device, asphodel_write_nvm_raw(device, start_address, data, length, callback, closure));
}

typedef struct {
	AsphodelDevice_t *device;
	AsphodelCommandCallback_t callback;
	void * closure;
	size_t address;
	uint8_t *data_start;
	uint8_t *data;
	size_t remaining_length;
	size_t max_write_length;
} NVMWriteSectionClosure_t;

static void asphodel_write_nvm_section_cb(int status, void * closure)
{
	NVMWriteSectionClosure_t *nvm_closure = (NVMWriteSectionClosure_t*)closure;

	if (status == 0)
	{
		if (nvm_closure->max_write_length >= nvm_closure->remaining_length)
		{
			// finished
			if (nvm_closure->callback)
			{
				nvm_closure->callback(0, nvm_closure->closure);
			}

			free(nvm_closure->data_start);
			free(nvm_closure);
		}
		else
		{
			// need more writes
			int ret;
			size_t bytes_to_write;

			nvm_closure->address += nvm_closure->max_write_length;
			nvm_closure->data += nvm_closure->max_write_length;
			nvm_closure->remaining_length -= nvm_closure->max_write_length;

			if (nvm_closure->remaining_length > nvm_closure->max_write_length)
			{
				bytes_to_write = nvm_closure->max_write_length;
			}
			else
			{
				bytes_to_write = nvm_closure->remaining_length;
			}

			ret = asphodel_write_nvm_raw(nvm_closure->device, nvm_closure->address, nvm_closure->data, bytes_to_write, asphodel_write_nvm_section_cb, nvm_closure);

			if (ret != 0)
			{
				// error
				if (nvm_closure->callback)
				{
					nvm_closure->callback(ret, nvm_closure->closure);
				}

				free(nvm_closure->data_start);
				free(nvm_closure);
			}
		}
	}
	else
	{
		// error
		if (nvm_closure->callback)
		{
			nvm_closure->callback(status, nvm_closure->closure);
		}

		free(nvm_closure->data_start);
		free(nvm_closure);
	}
}

// Write bytes to the NVM region. The start_address is given in bytes, and must be a multiple of 4.
// The length of the data must be a multiple of 4.
ASPHODEL_API int asphodel_write_nvm_section(AsphodelDevice_t *device, size_t start_address, const uint8_t *data, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	NVMWriteSectionClosure_t *nvm_closure;
	uint8_t *data_copy;
	size_t max_param_len;
	size_t max_write_length;

	if (length == 0)
	{
		// not really an error, but nothing has to be done
		if (callback)
		{
			callback(0, closure);
		}
		return 0;
	}
	else if (length % 4 != 0)
	{
		return ASPHODEL_BAD_PARAMETER;
	}

	// calculate the maximum write length
	max_param_len = device->get_max_outgoing_param_length(device);
	max_write_length = (max_param_len - 2) & ~0x03; // round down to next multiple of 4

	if (length <= max_write_length)
	{
		// a single write_nvm_raw is sufficient
		return asphodel_write_nvm_raw(device, start_address, data, length, callback, closure);
	}

	nvm_closure = (NVMWriteSectionClosure_t*)malloc(sizeof(NVMWriteSectionClosure_t));
	if (!nvm_closure)
	{
		return ASPHODEL_NO_MEM;
	}

	data_copy = (uint8_t*)malloc(length);
	if (!data_copy)
	{
		free(nvm_closure);
		return ASPHODEL_NO_MEM;
	}

	memcpy(data_copy, data, length);

	nvm_closure->device = device;
	nvm_closure->callback = callback;
	nvm_closure->closure = closure;
	nvm_closure->address = start_address;
	nvm_closure->data_start = data_copy;
	nvm_closure->data = data_copy;
	nvm_closure->remaining_length = length;
	nvm_closure->max_write_length = max_write_length;

	ret = asphodel_write_nvm_raw(device, start_address, nvm_closure->data, nvm_closure->max_write_length, asphodel_write_nvm_section_cb, nvm_closure);

	if (ret != 0)
	{
		free(nvm_closure);
		free(data_copy);
	}

	return ret;
}

ASPHODEL_API int asphodel_write_nvm_section_blocking(AsphodelDevice_t *device, size_t start_address, const uint8_t *data, size_t length)
{
	MAKE_BLOCKING(device, asphodel_write_nvm_section(device, start_address, data, length, callback, closure));
}

static void asphodel_read_nvm_raw_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *data = (uint8_t*)c->data[0];
	size_t *length = (size_t*)c->data[1];

	if (status == 0)
	{
		if (param_length % 4 == 0)
		{
			size_t i;

			for (i = 0; i < param_length && i < *length; i++)
			{
				data[i] = params[i];
			}

			*length = param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Read bytes from the NVM region. The start_address is given in bytes, and must be a multiple of 4.
// The number of bytes read is controlled by the device. The length parameter specifies the maximum number of bytes to
// write into data. See read_nvm_section for a more user friendly function.
ASPHODEL_API int asphodel_read_nvm_raw(AsphodelDevice_t *device, size_t start_address, uint8_t *data, size_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[2] = {
		((start_address / 4) >> 8) & 0xFF,
		(start_address / 4) & 0xFF,
	};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = data;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_READ_NVM, params, sizeof(params), asphodel_read_nvm_raw_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_read_nvm_raw_blocking(AsphodelDevice_t *device, size_t start_address, uint8_t *data, size_t *length)
{
	MAKE_BLOCKING(device, asphodel_read_nvm_raw(device, start_address, data, length, callback, closure));
}

typedef struct {
	AsphodelDevice_t *device;
	AsphodelCommandCallback_t callback;
	void * closure;
	size_t address;
	uint8_t *data;
	size_t remaining_length;
	size_t length; // modified by each asphodel_read_nvm_raw() call
} NVMReadSectionClosure_t;

static void asphodel_read_nvm_section_cb(int status, void * closure)
{
	NVMReadSectionClosure_t *nvm_closure = (NVMReadSectionClosure_t*)closure;

	if (status == 0)
	{
		if (nvm_closure->length >= nvm_closure->remaining_length)
		{
			// finished
			if (nvm_closure->callback)
			{
				nvm_closure->callback(0, nvm_closure->closure);
			}

			free(nvm_closure);
		}
		else
		{
			// need more reads

			if (nvm_closure->length > 0)
			{
				int ret;

				nvm_closure->address += nvm_closure->length;
				nvm_closure->data += nvm_closure->length;
				nvm_closure->remaining_length -= nvm_closure->length;
				nvm_closure->length = nvm_closure->remaining_length;

				ret = asphodel_read_nvm_raw(nvm_closure->device, nvm_closure->address, nvm_closure->data, &nvm_closure->length, asphodel_read_nvm_section_cb, nvm_closure);

				if (ret != 0)
				{
					// error
					if (nvm_closure->callback)
					{
						nvm_closure->callback(ret, nvm_closure->closure);
					}

					free(nvm_closure);
				}
			}
			else
			{
				// error: didn't read any bytes
				if (nvm_closure->callback)
				{
					nvm_closure->callback(ASPHODEL_BAD_REPLY_LENGTH, nvm_closure->closure);
				}

				free(nvm_closure);
			}
		}
	}
	else
	{
		// error
		if (nvm_closure->callback)
		{
			nvm_closure->callback(status, nvm_closure->closure);
		}

		free(nvm_closure);
	}
}

// Read a number of bytes from a specific section of the NVM region. The start_address is given in bytes, and must be
// a multiple of 4. Will read exactly 'length' number of bytes into data.
ASPHODEL_API int asphodel_read_nvm_section(AsphodelDevice_t *device, size_t start_address, uint8_t *data, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	NVMReadSectionClosure_t *nvm_closure = (NVMReadSectionClosure_t*)malloc(sizeof(NVMReadSectionClosure_t));

	if (!nvm_closure)
	{
		return ASPHODEL_NO_MEM;
	}

	nvm_closure->device = device;
	nvm_closure->callback = callback;
	nvm_closure->closure = closure;
	nvm_closure->address = start_address;
	nvm_closure->data = data;
	nvm_closure->remaining_length = length;
	nvm_closure->length = length;

	ret = asphodel_read_nvm_raw(device, start_address, data, &nvm_closure->length, asphodel_read_nvm_section_cb, nvm_closure);

	if (ret != 0)
	{
		free(nvm_closure);
	}

	return ret;
}

ASPHODEL_API int asphodel_read_nvm_section_blocking(AsphodelDevice_t *device, size_t start_address, uint8_t *data, size_t length)
{
	MAKE_BLOCKING(device, asphodel_read_nvm_section(device, start_address, data, length, callback, closure));
}

static void asphodel_read_user_tag_string_cb(int status, void * closure)
{
	NVMReadSectionClosure_t *nvm_closure = (NVMReadSectionClosure_t*)closure;

	if (status == 0)
	{
		if (nvm_closure->length >= nvm_closure->remaining_length)
		{
			// finished; check the bytes just recieved for terminators
			size_t i;
			for (i = 0; i < nvm_closure->remaining_length; i++)
			{
				uint8_t c = nvm_closure->data[i];
				if (c == '\0' || c == 0xFF)
				{
					nvm_closure->data[i] = '\0';
					break;
				}
			}

			// just in case we didn't find one in the data (won't hurt)
			nvm_closure->data[i] = '\0';

			if (nvm_closure->callback)
			{
				nvm_closure->callback(0, nvm_closure->closure);
			}

			free(nvm_closure);
		}
		else
		{
			// look for terminators
			size_t i;
			int found = 0;
			for (i = 0; i < nvm_closure->length; i++)
			{
				uint8_t c = nvm_closure->data[i];
				if (c == '\0' || c == 0xFF)
				{
					nvm_closure->data[i] = '\0';
					found = 1;
					break;
				}
			}

			if (found)
			{
				// found a terminator; finished
				if (nvm_closure->callback)
				{
					nvm_closure->callback(0, nvm_closure->closure);
				}

				free(nvm_closure);
			}
			else
			{
				// need more reads
				if (nvm_closure->length > 0)
				{
					int ret;

					nvm_closure->address += nvm_closure->length;
					nvm_closure->data += nvm_closure->length;
					nvm_closure->remaining_length -= nvm_closure->length;
					nvm_closure->length = nvm_closure->remaining_length;

					ret = asphodel_read_nvm_raw(nvm_closure->device, nvm_closure->address, nvm_closure->data, &nvm_closure->length, asphodel_read_user_tag_string_cb, nvm_closure);

					if (ret != 0)
					{
						// error
						if (nvm_closure->callback)
						{
							nvm_closure->callback(ret, nvm_closure->closure);
						}

						free(nvm_closure);
					}
				}
				else
				{
					// error: didn't read any bytes
					if (nvm_closure->callback)
					{
						nvm_closure->callback(ASPHODEL_BAD_REPLY_LENGTH, nvm_closure->closure);
					}

					free(nvm_closure);
				}
			}
		}
	}
	else
	{
		// error
		if (nvm_closure->callback)
		{
			nvm_closure->callback(status, nvm_closure->closure);
		}

		free(nvm_closure);
	}
}

// Read a string from a user tag location. Offset and length are in bytes, and must be a multiple of 4 (guaranteed if
// they came from the get_user_tag_locations). Buffer will be written with a null-terminated UTF-8 string. Up to
// buffer_size bytes will be written to the buffer. After completion buffer_size will hold the number of bytes in the
// string (not including the null terminator), rather than the number of bytes written to the buffer.
ASPHODEL_API int asphodel_read_user_tag_string(AsphodelDevice_t *device, size_t offset, size_t length, char *buffer, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	NVMReadSectionClosure_t *nvm_closure = (NVMReadSectionClosure_t*)malloc(sizeof(NVMReadSectionClosure_t));

	if (!nvm_closure)
	{
		return ASPHODEL_NO_MEM;
	}

	nvm_closure->device = device;
	nvm_closure->callback = callback;
	nvm_closure->closure = closure;
	nvm_closure->address = offset;
	nvm_closure->data = (uint8_t*)buffer;
	nvm_closure->remaining_length = length;
	nvm_closure->length = length;

	ret = asphodel_read_nvm_raw(device, offset, (uint8_t*)buffer, &nvm_closure->length, asphodel_read_user_tag_string_cb, nvm_closure);

	if (ret != 0)
	{
		free(nvm_closure);
	}

	return ret;
}

ASPHODEL_API int asphodel_read_user_tag_string_blocking(AsphodelDevice_t *device, size_t offset, size_t length, char *buffer)
{
	MAKE_BLOCKING(device, asphodel_read_user_tag_string(device, offset, length, buffer, callback, closure));
}

typedef struct {
	AsphodelDevice_t *device;
	AsphodelCommandCallback_t callback;
	void * closure;
	size_t offset;
	size_t length;
	size_t nvm_size;
	uint8_t *nvm_buffer;
	uint8_t *string_buffer;
} WriteUserTagClosure_t;

static void asphodel_write_user_tag_4th_cb(int status, void * closure)
{
	// callback from asphodel_write_nvm_section
	WriteUserTagClosure_t *write_tag_closure = (WriteUserTagClosure_t*)closure;

	if (status == 0)
	{
		// finished
		if (write_tag_closure->callback)
		{
			write_tag_closure->callback(0, write_tag_closure->closure);
		}

		free(write_tag_closure->nvm_buffer);
		free(write_tag_closure->string_buffer);
		free(write_tag_closure);
	}
	else
	{
		// error
		if (write_tag_closure->callback)
		{
			write_tag_closure->callback(status, write_tag_closure->closure);
		}

		free(write_tag_closure->nvm_buffer);
		free(write_tag_closure->string_buffer);
		free(write_tag_closure);
	}
}

static void asphodel_write_user_tag_3rd_cb(int status, void * closure)
{
	// callback from asphodel_erase_nvm
	WriteUserTagClosure_t *write_tag_closure = (WriteUserTagClosure_t*)closure;

	if (status == 0)
	{
		int ret;

		ret = asphodel_write_nvm_section(write_tag_closure->device, 0, write_tag_closure->nvm_buffer, write_tag_closure->nvm_size, asphodel_write_user_tag_4th_cb, write_tag_closure);

		if (ret != 0)
		{
			// error
			if (write_tag_closure->callback)
			{
				write_tag_closure->callback(ret, write_tag_closure->closure);
			}

			free(write_tag_closure->nvm_buffer);
			free(write_tag_closure->string_buffer);
			free(write_tag_closure);
		}
	}
	else
	{
		// error
		if (write_tag_closure->callback)
		{
			write_tag_closure->callback(status, write_tag_closure->closure);
		}

		free(write_tag_closure->nvm_buffer);
		free(write_tag_closure->string_buffer);
		free(write_tag_closure);
	}
}

static void asphodel_write_user_tag_2nd_cb(int status, void * closure)
{
	// callback from asphodel_read_nvm_section
	WriteUserTagClosure_t *write_tag_closure = (WriteUserTagClosure_t*)closure;

	if (status == 0)
	{
		int ret;
		size_t i;

		// copy the string into the NVM buffer
		for (i = 0; i < write_tag_closure->length; i++)
		{
			write_tag_closure->nvm_buffer[write_tag_closure->offset + i] = write_tag_closure->string_buffer[i];
		}

		ret = asphodel_erase_nvm(write_tag_closure->device, asphodel_write_user_tag_3rd_cb, write_tag_closure);

		if (ret != 0)
		{
			// error
			if (write_tag_closure->callback)
			{
				write_tag_closure->callback(ret, write_tag_closure->closure);
			}

			free(write_tag_closure->nvm_buffer);
			free(write_tag_closure->string_buffer);
			free(write_tag_closure);
		}
	}
	else
	{
		// error
		if (write_tag_closure->callback)
		{
			write_tag_closure->callback(status, write_tag_closure->closure);
		}

		free(write_tag_closure->nvm_buffer);
		free(write_tag_closure->string_buffer);
		free(write_tag_closure);
	}
}

static void asphodel_write_user_tag_1st_cb(int status, void * closure)
{
	// callback from asphodel_get_nvm_size
	WriteUserTagClosure_t *write_tag_closure = (WriteUserTagClosure_t*)closure;

	if (status == 0)
	{
		int ret;

		// allocate a buffer to hold the NVM
		write_tag_closure->nvm_buffer = (uint8_t *)malloc(write_tag_closure->nvm_size);
		if (!write_tag_closure->nvm_buffer)
		{
			// error: out of memory
			if (write_tag_closure->callback)
			{
				write_tag_closure->callback(ASPHODEL_NO_MEM, write_tag_closure->closure);
			}

			free(write_tag_closure->string_buffer);
			free(write_tag_closure);
			return;
		}

		ret = asphodel_read_nvm_section(write_tag_closure->device, 0, write_tag_closure->nvm_buffer, write_tag_closure->nvm_size, asphodel_write_user_tag_2nd_cb, write_tag_closure);

		if (ret != 0)
		{
			// error
			if (write_tag_closure->callback)
			{
				write_tag_closure->callback(ret, write_tag_closure->closure);
			}

			free(write_tag_closure->nvm_buffer);
			free(write_tag_closure->string_buffer);
			free(write_tag_closure);
		}
	}
	else
	{
		// error
		if (write_tag_closure->callback)
		{
			write_tag_closure->callback(status, write_tag_closure->closure);
		}

		free(write_tag_closure->string_buffer);
		free(write_tag_closure);
	}
}

// Write a string to a user tag location. Erases and rewrites the NVM. Offset and length are in bytes, and must be a
// multiple of 4 (guaranteed if they came from the get_user_tag_locations). Buffer should be a null-terminated UTF-8
// string. Additional bytes in the location will be filled with zeros.
ASPHODEL_API int asphodel_write_user_tag_string(AsphodelDevice_t *device, size_t offset, size_t length, const char *buffer, AsphodelCommandCallback_t callback, void * closure)
{
	WriteUserTagClosure_t *write_tag_closure = (WriteUserTagClosure_t*)malloc(sizeof(WriteUserTagClosure_t));
	uint8_t *string_buffer;
	int ret;
	size_t i;
	int found_end;

	if (!write_tag_closure)
	{
		return ASPHODEL_NO_MEM;
	}

	string_buffer = (uint8_t*)malloc(length);
	if (!string_buffer)
	{
		free(write_tag_closure);
		return ASPHODEL_NO_MEM;
	}

	write_tag_closure->device = device;
	write_tag_closure->callback = callback;
	write_tag_closure->closure = closure;
	write_tag_closure->offset = offset;
	write_tag_closure->length = length;
	write_tag_closure->string_buffer = string_buffer;

	found_end = 0;
	for (i = 0; i < length; i++)
	{
		if (!found_end)
		{
			string_buffer[i] = buffer[i];

			if (buffer[i] == '\0')
			{
				found_end = 1;
			}
		}
		else
		{
			string_buffer[i] = '\0';
		}
	}

	ret = asphodel_get_nvm_size(device, &write_tag_closure->nvm_size, asphodel_write_user_tag_1st_cb, write_tag_closure);

	if (ret != 0)
	{
		free(write_tag_closure);
		free(string_buffer);
	}

	return ret;
}

ASPHODEL_API int asphodel_write_user_tag_string_blocking(AsphodelDevice_t *device, size_t offset, size_t length, const char *buffer)
{
	MAKE_BLOCKING(device, asphodel_write_user_tag_string(device, offset, length, buffer, callback, closure));
}

// Performs a "soft" reset. Flushes any device side communication and disables any enabled streams.
ASPHODEL_API int asphodel_flush(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_FLUSH, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_flush_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_flush(device, callback, closure));
}

static void asphodel_reset_or_bootloader_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;

	(void)params; // suppress unused parameter warning
	(void)param_length; // suppress unused parameter warning

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}
}

// Reset the device.
ASPHODEL_API int asphodel_reset(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer_reset(device, CMD_RESET, NULL, 0, asphodel_reset_or_bootloader_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_reset_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_reset(device, callback, closure));
}

// Return the bootloader info string for the device (e.g. "XMega AES") in string form (UTF-8).
ASPHODEL_API int asphodel_get_bootloader_info(AsphodelDevice_t *device, char *buffer, size_t buffer_size, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = (void *)buffer_size;

	ret = device->do_transfer(device, CMD_GET_BOOTLOADER_INFO, NULL, 0, simple_string_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_bootloader_info_blocking(AsphodelDevice_t *device, char *buffer, size_t buffer_size)
{
	MAKE_BLOCKING(device, asphodel_get_bootloader_info(device, buffer, buffer_size, callback, closure));
}

// Reset the device and start the device's bootloader.
ASPHODEL_API int asphodel_bootloader_jump(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer_reset(device, CMD_BOOTLOADER_JUMP, NULL, 0, asphodel_reset_or_bootloader_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_bootloader_jump_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_bootloader_jump(device, callback, closure));
}

static void simple_count_transfer_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *count = (int*)c->data[0];

	if (status == 0)
	{
		if (param_length == 1)
		{
			*count = params[0];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the number of RGB LEDs present.
ASPHODEL_API int asphodel_get_rgb_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_RGB_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_rgb_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_rgb_count(device, count, callback, closure));
}

static void asphodel_get_rgb_values_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *values = (uint8_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 3)
		{
			values[0] = params[0];
			values[1] = params[1];
			values[2] = params[2];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the present setting of a specific RGB LED.
// values must be a length 3 array.
ASPHODEL_API int asphodel_get_rgb_values(AsphodelDevice_t *device, int index, uint8_t *values, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = values;

	ret = device->do_transfer(device, CMD_GET_RGB_VALUES, params, sizeof(params), asphodel_get_rgb_values_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_rgb_values_blocking(AsphodelDevice_t *device, int index, uint8_t *values)
{
	MAKE_BLOCKING(device, asphodel_get_rgb_values(device, index, values, callback, closure));
}

// Set the value of a specific RGB LED.
// values must be a length 3 array
ASPHODEL_API int asphodel_set_rgb_values(AsphodelDevice_t *device, int index, const uint8_t *values, int instant, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[4] = {
		index,
		values[0],
		values[1],
		values[2],
	};

	uint8_t cmd;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	if (instant)
	{
		cmd = CMD_SET_RGB_INSTANT;
	}
	else
	{
		cmd = CMD_SET_RGB;
	}

	ret = device->do_transfer(device, cmd, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_rgb_values_blocking(AsphodelDevice_t *device, int index, const uint8_t *values, int instant)
{
	MAKE_BLOCKING(device, asphodel_set_rgb_values(device, index, values, instant, callback, closure));
}

// Set the value of a specific RGB LED.
ASPHODEL_API int asphodel_set_rgb_values_hex(AsphodelDevice_t *device, int index, uint32_t color, int instant, AsphodelCommandCallback_t callback, void * closure)
{
	uint8_t values[3] = {
		(color >> 16) & 0xFF,
		(color >> 8) & 0xFF,
		color & 0xFF,
	};

	return asphodel_set_rgb_values(device, index, values, instant, callback, closure);
}

ASPHODEL_API int asphodel_set_rgb_values_hex_blocking(AsphodelDevice_t *device, int index, uint32_t color, int instant)
{
	MAKE_BLOCKING(device, asphodel_set_rgb_values_hex(device, index, color, instant, callback, closure));
}

// Return the number of stand-alone (not RGB) LEDs present.
ASPHODEL_API int asphodel_get_led_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_LED_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_led_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_led_count(device, count, callback, closure));
}

static void asphodel_get_led_value_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *value = (uint8_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 1)
		{
			*value = params[0];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the present setting of the specific LED.
ASPHODEL_API int asphodel_get_led_value(AsphodelDevice_t *device, int index, uint8_t *value, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = value;

	ret = device->do_transfer(device, CMD_GET_LED_VALUE, params, sizeof(params), asphodel_get_led_value_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_led_value_blocking(AsphodelDevice_t *device, int index, uint8_t *value)
{
	MAKE_BLOCKING(device, asphodel_get_led_value(device, index, value, callback, closure));
}

// Set the value of a specific LED.
ASPHODEL_API int asphodel_set_led_value(AsphodelDevice_t *device, int index, uint8_t value, int instant, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[2] = {
		index,
		value,
	};

	uint8_t cmd;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	if (instant)
	{
		cmd = CMD_SET_LED_INSTANT;
	}
	else
	{
		cmd = CMD_SET_LED;
	}

	ret = device->do_transfer(device, cmd, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_led_value_blocking(AsphodelDevice_t *device, int index, uint8_t value, int instant)
{
	MAKE_BLOCKING(device, asphodel_set_led_value(device, index, value, instant, callback, closure));
}

static void asphodel_get_stream_count_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *count = (int*)c->data[0];
	uint8_t *filler_bits = (uint8_t*)c->data[1];
	uint8_t *id_bits = (uint8_t*)c->data[2];

	if (status == 0)
	{
		if (param_length == 3)
		{
			*count = params[0];
			*filler_bits = params[1];
			*id_bits = params[2];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the number of streams present and ID size information
ASPHODEL_API int asphodel_get_stream_count(AsphodelDevice_t *device, int *count, uint8_t *filler_bits, uint8_t *id_bits, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 3 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;
	c->data[1] = filler_bits;
	c->data[2] = id_bits;

	ret = device->do_transfer(device, CMD_GET_STREAM_COUNT_AND_ID, NULL, 0, asphodel_get_stream_count_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_count_blocking(AsphodelDevice_t *device, int *count, uint8_t *filler_bits, uint8_t *id_bits)
{
	MAKE_BLOCKING(device, asphodel_get_stream_count(device, count, filler_bits, id_bits, callback, closure));
}

static void asphodel_get_stream_2nd_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelStreamInfo_t **stream_info = (AsphodelStreamInfo_t**)c->data[0];
	AsphodelStreamInfo_t *allocated_stream_info = (AsphodelStreamInfo_t*)c->data[1];

	if (status == 0)
	{
		*stream_info = allocated_stream_info;

		if (c->callback)
		{
			c->callback(status, c->closure);
		}
	}
	else
	{
		// error
		if (c->callback)
		{
			c->callback(status, c->closure);
		}

		free(allocated_stream_info);
	}

	free(c);
}

static void asphodel_get_stream_1st_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelStreamInfo_t *allocated_stream_info = (AsphodelStreamInfo_t*)c->data[1];
	int index = (int)(intptr_t)c->data[2];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[3];

	if (status == 0)
	{
		int ret;

		ret = asphodel_get_stream_channels_malloc(device, index, (uint8_t**)&allocated_stream_info->channel_index_list,
				&allocated_stream_info->channel_count, asphodel_get_stream_2nd_cb, c);

		if (ret != 0)
		{
			// error
			if (c->callback)
			{
				c->callback(ret, c->closure);
			}

			free(allocated_stream_info);
			free(c);
		}
	}
	else
	{
		// error
		if (c->callback)
		{
			c->callback(status, c->closure);
		}

		free(allocated_stream_info);
		free(c);
	}
}

// Allocate and fill a AsphodelStreamInfo_t structure. Must be freed with asphodel_free_stream() when finished.
ASPHODEL_API int asphodel_get_stream(AsphodelDevice_t *device, int index, AsphodelStreamInfo_t **stream_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 4 * sizeof(void *));
	AsphodelStreamInfo_t *allocated_stream_info;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	allocated_stream_info = (AsphodelStreamInfo_t*)malloc(sizeof(AsphodelStreamInfo_t));

	if (!allocated_stream_info)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = stream_info;
	c->data[1] = allocated_stream_info;
	c->data[2] = (void*)(intptr_t)index;
	c->data[3] = device;

	// make sure any pointers are NULL so we can use asphodel_free_stream() on errors
	allocated_stream_info->channel_index_list = NULL;

	ret = asphodel_get_stream_format(device, index, allocated_stream_info, asphodel_get_stream_1st_cb, c);

	if (ret != 0)
	{
		free(c);
		free(allocated_stream_info);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_blocking(AsphodelDevice_t *device, int index, AsphodelStreamInfo_t **stream_info)
{
	MAKE_BLOCKING(device, asphodel_get_stream(device, index, stream_info, callback, closure));
}

// Free a stream created by asphodel_get_stream() or asphodel_get_stream_blocking(). Streams created any other way
// must NOT be used with this function.
ASPHODEL_API void asphodel_free_stream(AsphodelStreamInfo_t *stream_info)
{
	free((void*)stream_info->channel_index_list);
	free(stream_info);
}

static void asphodel_get_stream_channels_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *channels = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length >= 1 && param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				channels[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the channel indexes for a specific stream. The length parameter should hold the maximum number of indexes to
// write into the array. When the command is finished it will hold the number of channels present on the stream (as
// opposed to the number of indexes actually written to the array).
ASPHODEL_API int asphodel_get_stream_channels(AsphodelDevice_t *device, int index, uint8_t *channels, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = channels;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_STREAM_CHANNELS, params, sizeof(params), asphodel_get_stream_channels_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_channels_blocking(AsphodelDevice_t *device, int index, uint8_t *channels, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_stream_channels(device, index, channels, length, callback, closure));
}

static void asphodel_get_stream_channels_malloc_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t **dest = (uint8_t**)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length >= 1 && param_length <= 255)
		{
			uint8_t *array = (uint8_t*)malloc(param_length);

			if (array == NULL)
			{
				status = ASPHODEL_NO_MEM;
			}
			else
			{
				memcpy(array, params, param_length);
				*dest = array;
				*length = (uint8_t)param_length;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// internal use only
static int asphodel_get_stream_channels_malloc(AsphodelDevice_t *device, int index, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = dest;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_STREAM_CHANNELS, params, sizeof(params), asphodel_get_stream_channels_malloc_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

static void asphodel_get_stream_format_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelStreamInfo_t *stream_info = (AsphodelStreamInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 14)
		{
			stream_info->filler_bits = params[0];
			stream_info->counter_bits = params[1];
			stream_info->rate = read_float_value(&params[2]);
			stream_info->rate_error = read_float_value(&params[6]);
			stream_info->warm_up_delay = read_float_value(&params[10]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the stream information for a specific stream into stream_info.
ASPHODEL_API int asphodel_get_stream_format(AsphodelDevice_t *device, int index, AsphodelStreamInfo_t *stream_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = stream_info;

	ret = device->do_transfer(device, CMD_GET_STREAM_FORMAT, params, sizeof(params), asphodel_get_stream_format_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_format_blocking(AsphodelDevice_t *device, int index, AsphodelStreamInfo_t *stream_info)
{
	MAKE_BLOCKING(device, asphodel_get_stream_format(device, index, stream_info, callback, closure));
}

// Enable or disable a specific stream.
ASPHODEL_API int asphodel_enable_stream(AsphodelDevice_t *device, int index, int enable, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[2] = {
		index,
		enable != 0,
	};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_ENABLE_STREAM, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_enable_stream_blocking(AsphodelDevice_t *device, int index, int enable)
{
	MAKE_BLOCKING(device, asphodel_enable_stream(device, index, enable, callback, closure));
}

// Enable or disable a specific stream's warm up function.
ASPHODEL_API int asphodel_warm_up_stream(AsphodelDevice_t *device, int index, int enable, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[2] = {
		index,
		enable != 0,
	};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_WARM_UP_STREAM, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_warm_up_stream_blocking(AsphodelDevice_t *device, int index, int enable)
{
	MAKE_BLOCKING(device, asphodel_warm_up_stream(device, index, enable, callback, closure));
}

static void asphodel_get_stream_status_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *enable = (int*)c->data[0];
	int *warm_up = (int*)c->data[1];

	if (status == 0)
	{
		if (param_length == 2)
		{
			if (enable != NULL)
			{
				*enable = params[0];
			}

			if (warm_up != NULL)
			{
				*warm_up = params[1];
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the enable and warm up status of a specific stream.
ASPHODEL_API int asphodel_get_stream_status(AsphodelDevice_t *device, int index, int *enable, int *warm_up, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = enable;
	c->data[1] = warm_up;

	ret = device->do_transfer(device, CMD_GET_STREAM_STATUS, params, sizeof(params), asphodel_get_stream_status_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_status_blocking(AsphodelDevice_t *device, int index, int *enable, int *warm_up)
{
	MAKE_BLOCKING(device, asphodel_get_stream_status(device, index, enable, warm_up, callback, closure));
}

static void asphodel_get_stream_rate_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *available = (int*)c->data[0];
	int *channel_index = (int*)c->data[1];
	int *invert = (int*)c->data[2];
	float *scale = (float*)c->data[3];
	float *offset = (float*)c->data[4];

	if (status == 0)
	{
		if (param_length == 0)
		{
			*available = 0;
		}
		else if (param_length == 10)
		{
			*available = 1;

			*channel_index = params[0];
			*invert = params[1];
			*scale = read_float_value(&params[2]);
			*offset = read_float_value(&params[6]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_UNIMPLEMENTED_COMMAND)
	{
		// device is too old to support this command; so report unavailable
		*available = 0;
		status = 0;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return stream rate channel information. The available parameter is a boolean (0/1). NOTE: if available is zero then
// the other function outputs have not been written.
ASPHODEL_API int asphodel_get_stream_rate_info(AsphodelDevice_t *device, int index, int *available, int *channel_index, int *invert, float *scale, float *offset, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 5 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = available;
	c->data[1] = channel_index;
	c->data[2] = invert;
	c->data[3] = scale;
	c->data[4] = offset;

	ret = device->do_transfer(device, CMD_GET_STREAM_RATE_INFO, params, sizeof(params), asphodel_get_stream_rate_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stream_rate_info_blocking(AsphodelDevice_t *device, int index, int *available, int *channel_index, int *invert, float *scale, float *offset)
{
	MAKE_BLOCKING(device, asphodel_get_stream_rate_info(device, index, available, channel_index, invert, scale, offset, callback, closure));
}

// Return the number of channels present.
ASPHODEL_API int asphodel_get_channel_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_channel_count(device, count, callback, closure));
}

static void asphodel_get_channel_3rd_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelChannelInfo_t **channel_info = (AsphodelChannelInfo_t**)c->data[0];
	AsphodelChannelInfo_t *allocated_channel_info = (AsphodelChannelInfo_t*)c->data[1];
	int index = (int)(intptr_t)c->data[2];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[3];
	uint8_t *chunk_count = (uint8_t*)&c->data[4];

	if (status == 0)
	{
		if (*chunk_count == 0)
		{
			// finished last chunk
			*channel_info = allocated_channel_info;
			if (c->callback)
			{
				c->callback(0, c->closure);
			}

			free(c);
		}
		else
		{
			// still have chunks to do
			int ret;
			uint8_t chunk_index = *chunk_count - 1;

			*chunk_count -= 1;

			ret = asphodel_get_channel_chunk_malloc(device, index, chunk_index, (uint8_t**)&allocated_channel_info->chunks[chunk_index],
					&allocated_channel_info->chunk_lengths[chunk_index], asphodel_get_channel_3rd_cb, c);

			if (ret != 0)
			{
				// error
				if (c->callback)
				{
					c->callback(ret, c->closure);
				}

				asphodel_free_channel(allocated_channel_info);
				free(c);
			}
		}
	}
	else
	{
		// error
		if (c->callback)
		{
			c->callback(status, c->closure);
		}

		asphodel_free_channel(allocated_channel_info);
		free(c);
	}
}

static void asphodel_get_channel_2nd_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelChannelInfo_t *allocated_channel_info = (AsphodelChannelInfo_t*)c->data[1];
	int index = (int)(intptr_t)c->data[2];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[3];

	if (status == 0)
	{
		int ret;

		ret = asphodel_get_channel_coefficients_malloc(device, index, (float**)&allocated_channel_info->coefficients,
			&allocated_channel_info->coefficients_length, asphodel_get_channel_3rd_cb, c);

		if (ret != 0)
		{
			// error
			if (c->callback)
			{
				c->callback(ret, c->closure);
			}

			asphodel_free_channel(allocated_channel_info);
			free(c);
		}
	}
	else
	{
		// error
		if (c->callback)
		{
			c->callback(status, c->closure);
		}

		asphodel_free_channel(allocated_channel_info);
		free(c);
	}
}

static void asphodel_get_channel_1st_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelChannelInfo_t *allocated_channel_info = (AsphodelChannelInfo_t*)c->data[1];
	int index = (int)(intptr_t)c->data[2];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[3];
	uint8_t *chunk_count = (uint8_t*)&c->data[4];

	if (status == 0)
	{
		int ret;

		*chunk_count = allocated_channel_info->chunk_count; // will be decremented for each chunk in a later callback

		if (allocated_channel_info->chunk_count == 0)
		{
			allocated_channel_info->chunk_lengths = NULL;
			allocated_channel_info->chunks = NULL;
		}
		else
		{
			allocated_channel_info->chunk_lengths = (uint8_t*)malloc(allocated_channel_info->chunk_count);
			allocated_channel_info->chunks = (const uint8_t**)malloc(sizeof(uint8_t*) * allocated_channel_info->chunk_count);

			if (allocated_channel_info->chunk_lengths == NULL || allocated_channel_info->chunks == NULL)
			{
				// error
				if (c->callback)
				{
					c->callback(ASPHODEL_NO_MEM, c->closure);
				}

				free(allocated_channel_info);
				free(c);
				return;
			}

			// make sure chunks is all NULLs
			memset((void*)allocated_channel_info->chunks, 0, sizeof(uint8_t*) * allocated_channel_info->chunk_count);
		}

		ret = asphodel_get_channel_name_malloc(device, index, (uint8_t**)&allocated_channel_info->name,
			&allocated_channel_info->name_length, asphodel_get_channel_2nd_cb, c);

		if (ret != 0)
		{
			// error
			if (c->callback)
			{
				c->callback(ret, c->closure);
			}

			free(allocated_channel_info);
			free(c);
		}
	}
	else
	{
		// error
		if (c->callback)
		{
			c->callback(status, c->closure);
		}

		free(allocated_channel_info);
		free(c);
	}
}

// Allocate and fill a AsphodelChannelInfo_t structure. Must be freed with asphodel_free_channel() when finished.
ASPHODEL_API int asphodel_get_channel(AsphodelDevice_t *device, int index, AsphodelChannelInfo_t **channel_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 5 * sizeof(void *));
	AsphodelChannelInfo_t *allocated_channel_info;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	allocated_channel_info = (AsphodelChannelInfo_t*)malloc(sizeof(AsphodelChannelInfo_t));

	if (!allocated_channel_info)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = channel_info;
	c->data[1] = allocated_channel_info;
	c->data[2] = (void*)(intptr_t)index;
	c->data[3] = device;
	*(uint8_t*)&c->data[4] = 0; // will be used as chunk index later

	// make sure any pointers are NULL so we can use asphodel_free_channel() on errors
	allocated_channel_info->name = NULL;
	allocated_channel_info->coefficients = NULL;
	allocated_channel_info->chunk_lengths = NULL;
	allocated_channel_info->chunk_count = 0;
	allocated_channel_info->chunks = NULL;

	ret = asphodel_get_channel_info(device, index, allocated_channel_info, asphodel_get_channel_1st_cb, c);

	if (ret != 0)
	{
		free(c);
		free(allocated_channel_info);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_blocking(AsphodelDevice_t *device, int index, AsphodelChannelInfo_t **channel_info)
{
	MAKE_BLOCKING(device, asphodel_get_channel(device, index, channel_info, callback, closure));
}

// Free a channel created by asphodel_get_channel() or asphodel_get_channel_blocking(). Channels created any other way
// must NOT be used with this function.
ASPHODEL_API void asphodel_free_channel(AsphodelChannelInfo_t *channel_info)
{
	int i;

	free((uint8_t*)channel_info->name);
	free((float*)channel_info->coefficients);
	free(channel_info->chunk_lengths);

	for (i = 0; i < channel_info->chunk_count; i++)
	{
		free((uint8_t*)channel_info->chunks[i]);
	}

	free((uint8_t**)channel_info->chunks);
	free(channel_info);
}

static void asphodel_get_name_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	char *buffer = (char*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length < 255)
		{
			size_t i;

			// make sure to leave room for the null terminator
			for (i = 0; i + 1 < *length && i < param_length; i++)
			{
				buffer[i] = (char)params[i];
			}

			// null terminate
			buffer[i] = '\0';

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the name of a specific channel in string form (UTF-8). The length parameter should hold the maximum number
// of bytes to write into buffer. Upon completion, the length parameter will hold the length of the channel name
// not including the null terminator. The length parameter may be set larger than its initial value if the buffer
// was not big enough to hold the entire channel name.
ASPHODEL_API int asphodel_get_channel_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_channel_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_name_malloc_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t **dest = (uint8_t**)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length == 0)
		{
			// technically ok
			*dest = NULL;
			*length = 0;
		}
		else if (param_length < 255)
		{
			uint8_t *name = (uint8_t*)malloc(param_length + 1);

			if (name == NULL)
			{
				status = ASPHODEL_NO_MEM;
			}
			else
			{
				memcpy(name, params, param_length);

				// null terminate
				name[param_length] = '\0';

				*dest = name;
				*length = (uint8_t)param_length;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// internal use only
static int asphodel_get_channel_name_malloc(AsphodelDevice_t *device, int index, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = dest;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_NAME, params, sizeof(params), asphodel_get_name_malloc_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

static void asphodel_get_channel_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelChannelInfo_t *channel_info = (AsphodelChannelInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 22)
		{
			channel_info->channel_type = params[0];
			channel_info->unit_type = params[1];
			channel_info->filler_bits = read_16bit_value(&params[2]);
			channel_info->data_bits = read_16bit_value(&params[4]);
			channel_info->samples = params[6];
			channel_info->bits_per_sample = read_16bit_value(&params[7]);
			channel_info->minimum = read_float_value(&params[9]);
			channel_info->maximum = read_float_value(&params[13]);
			channel_info->resolution = read_float_value(&params[17]);
			channel_info->chunk_count = params[21];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the channel information for a specific channel into channel_info.
ASPHODEL_API int asphodel_get_channel_info(AsphodelDevice_t *device, int index, AsphodelChannelInfo_t *channel_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = channel_info;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_INFO, params, sizeof(params), asphodel_get_channel_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_info_blocking(AsphodelDevice_t *device, int index, AsphodelChannelInfo_t *channel_info)
{
	MAKE_BLOCKING(device, asphodel_get_channel_info(device, index, channel_info, callback, closure));
}

static void asphodel_get_channel_coefficients_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[0];
	int index = (int)(intptr_t)c->data[1];
	float *coefficients = (float*)c->data[2];
	uint8_t *length = (uint8_t*)c->data[3];
	size_t split_length = (size_t)(intptr_t)c->data[4];
	size_t count = (size_t)(intptr_t)c->data[5];

	if (status == 0)
	{
		if (param_length % 4 == 0 && param_length < 255 * 4)
		{
			size_t array_length = param_length / 4;

			if (array_length + count <= 255)
			{
				size_t i;
				for (i = 0; i < (count + *length) && i < array_length; i++)
				{
					coefficients[i + count] = read_float_value(&params[i * 4]);
				}

				if (array_length != split_length || *length <= array_length + count)
				{
					// this is the final transfer
					*length = (uint8_t)(count + array_length);
				}
				else
				{
					// may be more coefficients waiting to be transferred
					size_t new_count = count + array_length;
					c->data[5] = (void*)(intptr_t)new_count;

					uint8_t params[2] = {index, (uint8_t)new_count};

					status = device->do_transfer(device, CMD_GET_CHANNEL_COEFFICIENTS, params, sizeof(params), asphodel_get_channel_coefficients_cb, c);

					if (status == 0)
					{
						// callback will be called by a later transfer
						return;
					}
				}
			}
			else
			{
				status = ASPHODEL_BAD_REPLY_LENGTH;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else
	{
		// got an error
		if (count > 0 && status == ERROR_CODE_BAD_CMD_LENGTH)
		{
			// old device; ignore
			*length = (uint8_t)count;
			status = 0;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with the coefficients from the specified channel. The length parameter should hold the maximum number
// of coefficients to write into the array. When the command is finished it will hold the number of coefficients
// present on the channel (as opposed to the number of coefficients actually written to the array).
ASPHODEL_API int asphodel_get_channel_coefficients(AsphodelDevice_t *device, int index, float *coefficients, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 6 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	size_t split_length = device->get_max_incoming_param_length(device) / 4;

	c->callback = callback;
	c->closure = closure;
	c->data[0] = device;
	c->data[1] = (void*)(intptr_t)index;
	c->data[2] = coefficients;
	c->data[3] = length;
	c->data[4] = (void*)(intptr_t)split_length;
	c->data[5] = (void*)(intptr_t)0; // number of received coefficients

	ret = device->do_transfer(device, CMD_GET_CHANNEL_COEFFICIENTS, params, sizeof(params), asphodel_get_channel_coefficients_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_coefficients_blocking(AsphodelDevice_t *device, int index, float *coefficients, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_channel_coefficients(device, index, coefficients, length, callback, closure));
}

static void asphodel_get_channel_coefficients_malloc_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[0];
	int index = (int)(intptr_t)c->data[1];
	float **dest = (float**)c->data[2];
	float *coefficients = (float*)c->data[3];
	uint8_t *length = (uint8_t*)c->data[4];
	size_t split_length = (size_t)(intptr_t)c->data[5];
	size_t count = (size_t)(intptr_t)c->data[6];

	if (status == 0)
	{
		if (param_length == 0)
		{
			if (count == 0)
			{
				// wasn't already allocated
				*dest = NULL;
			}
			else
			{
				*dest = coefficients;
			}

			*length = (uint8_t)count;
		}
		else if (param_length % 4 == 0 && param_length < 255 * 4)
		{
			size_t array_length = param_length / 4;

			if (array_length + count <= 255)
			{
				if (count == 0)
				{
					// need to allocate array; this is the first response

					if (array_length != split_length)
					{
						// allocate exact size; we have all the coefficients
						coefficients = (float*)malloc(sizeof(float) * array_length);
					}
					else
					{
						// allocate maximum size; we may need more than we already have
						coefficients = (float*)malloc(sizeof(float) * 255);
						c->data[3] = coefficients;
					}
				}

				if (coefficients == NULL)
				{
					status = ASPHODEL_NO_MEM;
				}
				else
				{
					size_t i;
					for (i = 0; i < array_length; i++)
					{
						coefficients[i + count] = read_float_value(&params[i * 4]);
					}

					if (array_length != split_length)
					{
						// this is the final transfer
						*dest = coefficients;
						*length = (uint8_t)(count + array_length);
					}
					else
					{
						// may be more coefficients waiting to be transferred
						size_t new_count = count + array_length;
						c->data[6] = (void*)(intptr_t)new_count;

						uint8_t params[2] = {index, (uint8_t)new_count};

						status = device->do_transfer(device, CMD_GET_CHANNEL_COEFFICIENTS, params, sizeof(params), asphodel_get_channel_coefficients_malloc_cb, c);

						if (status == 0)
						{
							// callback will be called by a later transfer
							return;
						}
					}
				}
			}
			else
			{
				status = ASPHODEL_BAD_REPLY_LENGTH;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else
	{
		// got an error
		if (count > 0 && status == ERROR_CODE_BAD_CMD_LENGTH)
		{
			// old device; ignore
			*dest = coefficients;
			*length = (uint8_t)count;
			status = 0;
		}
		else
		{
			free(coefficients);
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// internal use only
static int asphodel_get_channel_coefficients_malloc(AsphodelDevice_t *device, int index, float **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 7 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	size_t split_length = device->get_max_incoming_param_length(device) / 4;

	c->callback = callback;
	c->closure = closure;
	c->data[0] = device;
	c->data[1] = (void*)(intptr_t)index;
	c->data[2] = dest;
	c->data[3] = NULL; // coefficients array
	c->data[4] = length;
	c->data[5] = (void*)(intptr_t)split_length;
	c->data[6] = (void*)(intptr_t)0; // number of received coefficients

	ret = device->do_transfer(device, CMD_GET_CHANNEL_COEFFICIENTS, params, sizeof(params), asphodel_get_channel_coefficients_malloc_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

static void asphodel_get_channel_chunk_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *chunk = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				chunk[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with a chunk for the specified channel. The length parameter should hold the maximum number
// of bytes to write into the array. When the command is finished, the length parameter will hold the size of the
// chunk (as opposed to the number of bytes actually written to the array).
ASPHODEL_API int asphodel_get_channel_chunk(AsphodelDevice_t *device, int index, uint8_t chunk_number, uint8_t *chunk, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[2] = {index, chunk_number};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = chunk;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_CHUNK, params, sizeof(params), asphodel_get_channel_chunk_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_chunk_blocking(AsphodelDevice_t *device, int index, uint8_t chunk_number, uint8_t *chunk, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_channel_chunk(device, index, chunk_number, chunk, length, callback, closure));
}


static void asphodel_get_channel_chunk_malloc_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t **dest = (uint8_t**)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length == 0)
		{
			*dest = NULL;
			*length = 0;
		}
		else if (param_length <= 255)
		{
			uint8_t *chunk = (uint8_t*)malloc(param_length);

			if (chunk == NULL)
			{
				status = ASPHODEL_NO_MEM;
			}
			else
			{
				memcpy(chunk, params, param_length);

				*dest = chunk;
				*length = (uint8_t)param_length;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with a chunk for the specified channel. The length parameter should hold the maximum number
// of bytes to write into the array. When the command is finished, the length parameter will hold the size of the
// chunk (as opposed to the number of bytes actually written to the array).
static int asphodel_get_channel_chunk_malloc(AsphodelDevice_t *device, int index, uint8_t chunk_number, uint8_t **dest, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[2] = {index, chunk_number};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = dest;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_CHUNK, params, sizeof(params), asphodel_get_channel_chunk_malloc_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

static void asphodel_channel_specific_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *reply = (uint8_t*)c->data[0];
	uint8_t *reply_length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *reply_length && i < param_length; i++)
			{
				reply[i] = params[i];
			}

			*reply_length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Performa a channel specific transfer. The format of the data depends on the channel type. The reply_length parameter
// should hold the maximum number of bytes to write into the reply array. When the command is finished, the
// reply_length parameter will hold the size of the recieved reply (as opposed to the number of bytes actually written
// to the reply array).
ASPHODEL_API int asphodel_channel_specific(AsphodelDevice_t *device, int index, uint8_t *data, uint8_t data_length, uint8_t *reply, uint8_t *reply_length, AsphodelCommandCallback_t callback, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t *params;
	size_t i;
	int ret;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = reply;
	c->data[1] = reply_length;

	params = (uint8_t*)malloc(data_length + 1);
	if (!params)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	params[0] = index;

	for (i = 0; i < data_length; i++)
	{
		params[1 + i] = data[i];
	}

	ret = device->do_transfer(device, CMD_CHANNEL_SPECIFIC, params, data_length + 1, asphodel_channel_specific_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_channel_specific_blocking(AsphodelDevice_t *device, int index, uint8_t *data, uint8_t data_length, uint8_t *reply, uint8_t *reply_length)
{
	MAKE_BLOCKING(device, asphodel_channel_specific(device, index, data, data_length, reply, reply_length, callback, closure));
}

static void asphodel_get_channel_calibration_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *available = (int*)c->data[0];
	AsphodelChannelCalibration_t *calibration = (AsphodelChannelCalibration_t*)c->data[1];

	if (status == 0)
	{
		if (param_length == 0)
		{
			*available = 0;
		}
		else if (param_length == 18)
		{
			*available = 1;

			calibration->base_setting_index = params[0];
			calibration->resolution_setting_index = params[1];
			calibration->scale = read_float_value(&params[2]);
			calibration->offset = read_float_value(&params[6]);
			calibration->minimum = read_float_value(&params[10]);
			calibration->maximum = read_float_value(&params[14]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return channel calibration information. The available parameter is a boolean (0/1). NOTE: if available is zero then
// the calibration structure values have not been written.
ASPHODEL_API int asphodel_get_channel_calibration(AsphodelDevice_t *device, int index, int *available, AsphodelChannelCalibration_t *calibration, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = { index };

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = available;
	c->data[1] = calibration;

	ret = device->do_transfer(device, CMD_GET_CHANNEL_CALIBRATION, params, sizeof(params), asphodel_get_channel_calibration_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_channel_calibration_blocking(AsphodelDevice_t *device, int index, int *available, AsphodelChannelCalibration_t *calibration)
{
	MAKE_BLOCKING(device, asphodel_get_channel_calibration(device, index, available, calibration, callback, closure));
}

// Return the number of supplies present.
ASPHODEL_API int asphodel_get_supply_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_SUPPLY_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_supply_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_supply_count(device, count, callback, closure));
}

// Return the name of a specific supply in string form (UTF-8). The length parameter should hold the maximum number
// of bytes to write into buffer. Upon completion, the length parameter will hold the length of the supply name not
// including the null terminator. The length parameter may be set larger than its initial value if the buffer was not
// big enough to hold the entire supply name.
ASPHODEL_API int asphodel_get_supply_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_SUPPLY_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_supply_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_supply_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_supply_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelSupplyInfo_t *supply_info = (AsphodelSupplyInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 14)
		{
			supply_info->unit_type = params[0];
			supply_info->is_battery = params[1] != 0;
			supply_info->nominal = read_32bit_value(&params[2]);
			supply_info->scale = read_float_value(&params[6]);
			supply_info->offset = read_float_value(&params[10]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the channel information for a specific supply into supply_info.
ASPHODEL_API int asphodel_get_supply_info(AsphodelDevice_t *device, int index, AsphodelSupplyInfo_t *supply_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = supply_info;

	ret = device->do_transfer(device, CMD_GET_SUPPLY_INFO, params, sizeof(params), asphodel_get_supply_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_supply_info_blocking(AsphodelDevice_t *device, int index, AsphodelSupplyInfo_t *supply_info)
{
	MAKE_BLOCKING(device, asphodel_get_supply_info(device, index, supply_info, callback, closure));
}

static void asphodel_check_supply_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[0];
	int index = (int)(intptr_t)c->data[1];
	int32_t *measurement = (int32_t*)c->data[2];
	uint8_t *result = (uint8_t*)c->data[3];
	int tries = (int)(intptr_t)c->data[4];

	if (status == 0)
	{
		if (param_length == 5)
		{
			// done
			*measurement = read_32bit_value(&params[0]);
			*result = params[4];
		}
		else if (param_length == 0)
		{
			if (tries == 1)
			{
				// tried too many times
				status = ASPHODEL_TOO_MANY_TRIES;
			}
			else
			{
				// need another transfer
				uint8_t tx_params[1] = {index};

				if (tries > 1)
				{
					// decrement tries
					c->data[4] = (void*)(intptr_t)(tries - 1);
				}

				status = device->do_transfer(device, CMD_CHECK_SUPPLY, tx_params, sizeof(tx_params), asphodel_check_supply_cb, c);

				if (status == 0)
				{
					// everything is ok; the next transfer will handle cleanup
					return;
				}
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Perform a measurement on the specified supply. Write the channel information for a specific supply into supply_info.
ASPHODEL_API int asphodel_check_supply(AsphodelDevice_t *device, int index, int32_t *measurement, uint8_t *result, int tries, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 5 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = device;
	c->data[1] = (void*)(intptr_t)index;
	c->data[2] = measurement;
	c->data[3] = result;
	c->data[4] = (void*)(intptr_t)tries;

	ret = device->do_transfer(device, CMD_CHECK_SUPPLY, params, sizeof(params), asphodel_check_supply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_check_supply_blocking(AsphodelDevice_t *device, int index, int32_t *measurement, uint8_t *result, int tries)
{
	MAKE_BLOCKING(device, asphodel_check_supply(device, index, measurement, result, tries, callback, closure));
}

// Return the number of control variables present.
ASPHODEL_API int asphodel_get_ctrl_var_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_CTRL_VAR_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_ctrl_var_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_ctrl_var_count(device, count, callback, closure));
}

// Return the name of a specific control variable in string form (UTF-8). The length parameter should hold the maximum
// number of bytes to write into buffer. Upon completion, the length parameter will hold the length of the control
// variable name not including the null terminator. The length parameter may be set larger than its initial value if
// the buffer was not big enough to hold the entire control variable name.
ASPHODEL_API int asphodel_get_ctrl_var_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CTRL_VAR_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_ctrl_var_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_ctrl_var_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_ctrl_var_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelCtrlVarInfo_t *ctrl_var_info = (AsphodelCtrlVarInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 17)
		{
			ctrl_var_info->unit_type = params[0];
			ctrl_var_info->minimum = read_32bit_value(&params[1]);
			ctrl_var_info->maximum = read_32bit_value(&params[5]);
			ctrl_var_info->scale = read_float_value(&params[9]);
			ctrl_var_info->offset = read_float_value(&params[13]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the information for a specific control variable into ctrl_var_info.
ASPHODEL_API int asphodel_get_ctrl_var_info(AsphodelDevice_t *device, int index, AsphodelCtrlVarInfo_t *ctrl_var_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = ctrl_var_info;

	ret = device->do_transfer(device, CMD_GET_CTRL_VAR_INFO, params, sizeof(params), asphodel_get_ctrl_var_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_ctrl_var_info_blocking(AsphodelDevice_t *device, int index, AsphodelCtrlVarInfo_t *ctrl_var_info)
{
	MAKE_BLOCKING(device, asphodel_get_ctrl_var_info(device, index, ctrl_var_info, callback, closure));
}

static void asphodel_get_ctrl_var_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int32_t *value = (int32_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 4)
		{
			*value = read_32bit_value(&params[0]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Get the value of a specific control variable.
ASPHODEL_API int asphodel_get_ctrl_var(AsphodelDevice_t *device, int index, int32_t *value, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = value;

	ret = device->do_transfer(device, CMD_GET_CTRL_VAR, params, sizeof(params), asphodel_get_ctrl_var_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_ctrl_var_blocking(AsphodelDevice_t *device, int index, int32_t *value)
{
	MAKE_BLOCKING(device, asphodel_get_ctrl_var(device, index, value, callback, closure));
}

// Set the value of a specific control variable.
ASPHODEL_API int asphodel_set_ctrl_var(AsphodelDevice_t *device, int index, int32_t value, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[5] = {index, 0};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[1], value);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_SET_CTRL_VAR, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_ctrl_var_blocking(AsphodelDevice_t *device, int index, int32_t value)
{
	MAKE_BLOCKING(device, asphodel_set_ctrl_var(device, index, value, callback, closure));
}

// Return the number of settings present.
ASPHODEL_API int asphodel_get_setting_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_SETTING_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_setting_count(device, count, callback, closure));
}

// Return the name of a specific setting in string form (UTF-8). The length parameter should hold the maximum number of
// bytes to write into buffer. Upon completion, the length parameter will hold the length of the setting name not
// including the null terminator. The length parameter may be set larger than its initial value if the buffer was not
// big enough to hold the entire setting name.
ASPHODEL_API int asphodel_get_setting_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_SETTING_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_setting_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_setting_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelSettingInfo_t *setting_info = (AsphodelSettingInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length >= 1)
		{
			setting_info->setting_type = params[0];

			switch (setting_info->setting_type)
			{
			case SETTING_TYPE_BYTE:
			case SETTING_TYPE_BOOLEAN:
			case SETTING_TYPE_UNIT_TYPE:
			case SETTING_TYPE_CHANNEL_TYPE:
				if (param_length == 4)
				{
					setting_info->u.byte_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.byte_setting.nvm_word_byte = params[3];
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_BYTE_ARRAY:
				if (param_length == 7)
				{
					setting_info->u.byte_array_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.byte_array_setting.maximum_length = params[3];
					setting_info->u.byte_array_setting.length_nvm_word = read_16bit_value(&params[4]);
					setting_info->u.byte_array_setting.length_nvm_word_byte = params[6];
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_STRING:
				if (param_length == 4)
				{
					setting_info->u.string_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.string_setting.maximum_length = params[3];
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_INT32:
				if (param_length == 11)
				{
					setting_info->u.int32_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.int32_setting.minimum = read_32bit_value(&params[3]);
					setting_info->u.int32_setting.maximum = read_32bit_value(&params[7]);
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_INT32_SCALED:
				if (param_length == 20)
				{
					setting_info->u.int32_scaled_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.int32_scaled_setting.minimum = read_32bit_value(&params[3]);
					setting_info->u.int32_scaled_setting.maximum = read_32bit_value(&params[7]);
					setting_info->u.int32_scaled_setting.unit_type = params[11];
					setting_info->u.int32_scaled_setting.scale = read_float_value(&params[12]);
					setting_info->u.int32_scaled_setting.offset = read_float_value(&params[16]);
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_FLOAT:
				if (param_length == 20)
				{
					setting_info->u.float_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.float_setting.minimum = read_float_value(&params[3]);
					setting_info->u.float_setting.maximum = read_float_value(&params[7]);
					setting_info->u.float_setting.unit_type = params[11];
					setting_info->u.float_setting.scale = read_float_value(&params[12]);
					setting_info->u.float_setting.offset = read_float_value(&params[16]);
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_FLOAT_ARRAY:
				if (param_length == 24)
				{
					setting_info->u.float_array_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.float_array_setting.minimum = read_float_value(&params[3]);
					setting_info->u.float_array_setting.maximum = read_float_value(&params[7]);
					setting_info->u.float_array_setting.unit_type = params[11];
					setting_info->u.float_array_setting.scale = read_float_value(&params[12]);
					setting_info->u.float_array_setting.offset = read_float_value(&params[16]);
					setting_info->u.float_array_setting.maximum_length = params[20];
					setting_info->u.float_array_setting.length_nvm_word = read_16bit_value(&params[21]);
					setting_info->u.float_array_setting.length_nvm_word_byte = params[23];
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			case SETTING_TYPE_CUSTOM_ENUM:
				if (param_length == 5)
				{
					setting_info->u.custom_enum_setting.nvm_word = read_16bit_value(&params[1]);
					setting_info->u.custom_enum_setting.nvm_word_byte = params[3];
					setting_info->u.custom_enum_setting.custom_enum_index = params[4];
				}
				else
				{
					status = ASPHODEL_BAD_REPLY_LENGTH;
				}
				break;
			default:
				break; // unknown setting type, don't try to read any information
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the information for a specific setting into setting_info.
ASPHODEL_API int asphodel_get_setting_info(AsphodelDevice_t *device, int index, AsphodelSettingInfo_t *setting_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = setting_info;

	ret = device->do_transfer(device, CMD_GET_SETTING_INFO, params, sizeof(params), asphodel_get_setting_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_info_blocking(AsphodelDevice_t *device, int index, AsphodelSettingInfo_t *setting_info)
{
	MAKE_BLOCKING(device, asphodel_get_setting_info(device, index, setting_info, callback, closure));
}

static void asphodel_get_setting_default_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *default_bytes = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				default_bytes[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_UNIMPLEMENTED_COMMAND)
	{
		// device is too old to support this command; so report an empty default
		*length = 0;
		status = 0;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with the default bytes for the specified setting. The length parameter should hold the maximum number
// of bytes to write into the array. When the command is finished, the length parameter will hold the size of the
// default bytes (as opposed to the number of bytes actually written to the array).
ASPHODEL_API int asphodel_get_setting_default(AsphodelDevice_t *device, int index, uint8_t *default_bytes, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = default_bytes;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_SETTING_DEFAULT, params, sizeof(params), asphodel_get_setting_default_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_default_blocking(AsphodelDevice_t *device, int index, uint8_t *default_bytes, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_setting_default(device, index, default_bytes, length, callback, closure));
}

static void asphodel_get_custom_enum_counts_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *counts = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				counts[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_UNIMPLEMENTED_COMMAND)
	{
		// device is too old to support this command; so report a count of 0
		*length = 0;
		status = 0;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the number of elements for each custom enumeration on the device. The length parameter should hold the
// maximum number of counts to write into the array. When the command is finished it will hold the number of custom
// enumerations present on the device (as opposed to the number of counts actually written to the array).
ASPHODEL_API int asphodel_get_custom_enum_counts(AsphodelDevice_t *device, uint8_t *counts, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = counts;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CUSTOM_ENUM_COUNTS, NULL, 0, asphodel_get_custom_enum_counts_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_custom_enum_counts_blocking(AsphodelDevice_t *device, uint8_t *counts, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_custom_enum_counts(device, counts, length, callback, closure));
}

// Return the name of a specific custom enumeration value in string form (UTF-8). The length parameter should hold the
// maximum number of bytes to write into buffer. Upon completion, the length parameter will hold the length of the
// custom enumeration value name not including the null terminator. The length parameter may be set larger than its
// initial value if the buffer was not big enough to hold the entire channel name.
ASPHODEL_API int asphodel_get_custom_enum_value_name(AsphodelDevice_t *device, int index, int value, char *buffer, uint8_t *length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[2] = {index, value};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_CUSTOM_ENUM_VALUE_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_custom_enum_value_name_blocking(AsphodelDevice_t *device, int index, int value, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_custom_enum_value_name(device, index, value, buffer, length, callback, closure));
}

static void asphodel_get_setting_category_count_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *count = (int*)c->data[0];

	if (status == 0)
	{
		if (param_length == 1)
		{
			*count = params[0];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_UNIMPLEMENTED_COMMAND)
	{
		// device is too old to support this command; so report a count of 0
		*count = 0;
		status = 0;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the number of setting categories present.
ASPHODEL_API int asphodel_get_setting_category_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback,
		void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_SETTING_CATEGORY_COUNT, NULL, 0, asphodel_get_setting_category_count_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_category_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_setting_category_count(device, count, callback, closure));
}

// Return the name of a specific setting category in string form (UTF-8). The length parameter should hold the maximum
// number of bytes to write into buffer. Upon completion, the length parameter will hold the length of the setting
// category name not including the null terminator. The length parameter may be set larger than its initial value if
// the buffer was not big enough to hold the entire setting category name.
ASPHODEL_API int asphodel_get_setting_category_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_SETTING_CATEGORY_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_category_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_setting_category_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_setting_category_settings_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *settings = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length >= 1 && param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				settings[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the setting indexes for a specific setting category. The length parameter should hold the maximum number of
// indexes to write into the array. When the command is finished it will hold the number of settings present on the
// setting category (as opposed to the number of indexes actually written to the array).
ASPHODEL_API int asphodel_get_setting_category_settings(AsphodelDevice_t *device, int index, uint8_t *settings, uint8_t *length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = settings;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_SETTING_CATERORY_SETTINGS, params, sizeof(params), asphodel_get_setting_category_settings_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_setting_category_settings_blocking(AsphodelDevice_t *device, int index, uint8_t *settings, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_setting_category_settings(device, index, settings, length, callback, closure));
}

// Set the mode of the device to a specific value.
ASPHODEL_API int asphodel_set_device_mode(AsphodelDevice_t *device, uint8_t mode, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[1] = {mode};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_SET_DEVICE_MODE, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_device_mode_blocking(AsphodelDevice_t *device, uint8_t mode)
{
	MAKE_BLOCKING(device, asphodel_set_device_mode(device, mode, callback, closure));
}

static void asphodel_get_device_mode_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *mode = (uint8_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 1)
		{
			*mode = params[0];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the present setting of the device mode.
ASPHODEL_API int asphodel_get_device_mode(AsphodelDevice_t *device, uint8_t *mode, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = mode;

	ret = device->do_transfer(device, CMD_GET_DEVICE_MODE, NULL, 0, asphodel_get_device_mode_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_device_mode_blocking(AsphodelDevice_t *device, uint8_t *mode)
{
	MAKE_BLOCKING(device, asphodel_get_device_mode(device, mode, callback, closure));
}

// Return the number of GPIO ports present.
ASPHODEL_API int asphodel_get_gpio_port_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_GPIO_PORT_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_gpio_port_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_gpio_port_count(device, count, callback, closure));
}

// Return the name of a specific GPIO port in string form (UTF-8). The length parameter should hold the maximum number
// of bytes to write into buffer. Upon completion, the length parameter will hold the length of the GPIO port name not
// including the null terminator. The length parameter may be set larger than its initial value if the buffer was not
// big enough to hold the entire GPIO port name.
ASPHODEL_API int asphodel_get_gpio_port_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_GPIO_PORT_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_gpio_port_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_gpio_port_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_gpio_port_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelGPIOPortInfo_t *gpio_port_info = (AsphodelGPIOPortInfo_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 20)
		{
			gpio_port_info->input_pins = read_32bit_value(&params[0]);
			gpio_port_info->output_pins = read_32bit_value(&params[4]);
			gpio_port_info->floating_pins = read_32bit_value(&params[8]);
			gpio_port_info->loaded_pins = read_32bit_value(&params[12]);
			gpio_port_info->overridden_pins = read_32bit_value(&params[16]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Write the information for a specific GPIO port into gpio_port_info.
ASPHODEL_API int asphodel_get_gpio_port_info(AsphodelDevice_t *device, int index, AsphodelGPIOPortInfo_t *gpio_port_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = gpio_port_info;

	ret = device->do_transfer(device, CMD_GET_GPIO_PORT_INFO, params, sizeof(params), asphodel_get_gpio_port_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_gpio_port_info_blocking(AsphodelDevice_t *device, int index, AsphodelGPIOPortInfo_t *gpio_port_info)
{
	MAKE_BLOCKING(device, asphodel_get_gpio_port_info(device, index, gpio_port_info, callback, closure));
}

static void asphodel_get_gpio_port_values_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t *pin_values = (uint32_t *)c->data[0];

	if (status == 0)
	{
		if (param_length == 4)
		{
			*pin_values = read_32bit_value(&params[0]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Get the pin values of a specific GPIO port.
ASPHODEL_API int asphodel_get_gpio_port_values(AsphodelDevice_t *device, int index, uint32_t *pin_values, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = pin_values;

	ret = device->do_transfer(device, CMD_GET_GPIO_PORT_VALUES, params, sizeof(params), asphodel_get_gpio_port_values_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_gpio_port_values_blocking(AsphodelDevice_t *device, int index, uint32_t *pin_values)
{
	MAKE_BLOCKING(device, asphodel_get_gpio_port_values(device, index, pin_values, callback, closure));
}

// Set the pin mode for a set of pins on a specific GPIO port.
ASPHODEL_API int asphodel_set_gpio_port_modes(AsphodelDevice_t *device, int index, uint8_t mode, uint32_t pins, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[6] = {index, mode, 0};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[2], pins);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_SET_GPIO_PORT_MODES, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_gpio_port_modes_blocking(AsphodelDevice_t *device, int index, uint8_t mode, uint32_t pins)
{
	MAKE_BLOCKING(device, asphodel_set_gpio_port_modes(device, index, mode, pins, callback, closure));
}

// Disable hardware overrides on all GPIO pins. Only a device reset can restore the device to normal operations.
ASPHODEL_API int asphodel_disable_gpio_overrides(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_DISABLE_GPIO_PORT_OVERRIDES, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_disable_gpio_overrides_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_disable_gpio_overrides(device, callback, closure));
}

static void asphodel_get_bus_counts_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *spi_count = (int*)c->data[0];
	int *i2c_count = (int*)c->data[1];

	if (status == 0)
	{
		if (param_length == 2)
		{
			*spi_count = params[0];
			*i2c_count = params[1];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the number of SPI and I2C busses present.
ASPHODEL_API int asphodel_get_bus_counts(AsphodelDevice_t *device, int *spi_count, int *i2c_count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = spi_count;
	c->data[1] = i2c_count;

	ret = device->do_transfer(device, CMD_GET_BUS_COUNTS, NULL, 0, asphodel_get_bus_counts_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_bus_counts_blocking(AsphodelDevice_t *device, int *spi_count, int *i2c_count)
{
	MAKE_BLOCKING(device, asphodel_get_bus_counts(device, spi_count, i2c_count, callback, closure));
}

// Set the CS mode for a specific SPI bus.
ASPHODEL_API int asphodel_set_spi_cs_mode(AsphodelDevice_t *device, int index, uint8_t cs_mode, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[2] = {index, cs_mode};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_SET_SPI_CS_MODE, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_spi_cs_mode_blocking(AsphodelDevice_t *device, int index, uint8_t cs_mode)
{
	MAKE_BLOCKING(device, asphodel_set_spi_cs_mode(device, index, cs_mode, callback, closure));
}

static void bus_read_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *rx_data = (uint8_t *)c->data[0];
	uint8_t read_length = (uint8_t)(intptr_t)c->data[1];

	if (status == 0)
	{
		if (param_length == read_length)
		{
			memcpy(rx_data, params, param_length);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Does a transfer on the specified SPI bus. The TX data is transmitted. The RX data buffer must be at least as long
// as the transmission length.
ASPHODEL_API int asphodel_do_spi_transfer(AsphodelDevice_t *device, int index, const uint8_t *tx_data, uint8_t *rx_data, uint8_t data_length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (data_length == 0 || data_length > device->get_max_incoming_param_length(device))
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	params = (uint8_t*)malloc(data_length + 1);
	if (params == NULL)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	params[0] = index;
	memcpy(&params[1], tx_data, data_length);

	c->callback = callback;
	c->closure = closure;
	c->data[0] = rx_data;
	c->data[1] = (void*)(intptr_t)data_length;

	ret = device->do_transfer(device, CMD_DO_SPI_TRANSFER, params, data_length + 1, bus_read_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_spi_transfer_blocking(AsphodelDevice_t *device, int index, const uint8_t *tx_data, uint8_t *rx_data, uint8_t data_length)
{
	MAKE_BLOCKING(device, asphodel_do_spi_transfer(device, index, tx_data, rx_data, data_length, callback, closure));
}

// Does a write to the given address on the specified I2C bus.
ASPHODEL_API int asphodel_do_i2c_write(AsphodelDevice_t *device, int index, uint8_t addr, const uint8_t *tx_data, uint8_t write_length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (write_length == 0)
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	params = (uint8_t*)malloc(write_length + 2);
	if (params == NULL)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	params[0] = index;
	params[1] = addr;
	memcpy(&params[2], tx_data, write_length);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_DO_I2C_WRITE, params, write_length + 2, simple_no_reply_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_i2c_write_blocking(AsphodelDevice_t *device, int index, uint8_t addr, const uint8_t *tx_data, uint8_t write_length)
{
	MAKE_BLOCKING(device, asphodel_do_i2c_write(device, index, addr, tx_data, write_length, callback, closure));
}

// Does a read from the given address on the specified I2C bus.
ASPHODEL_API int asphodel_do_i2c_read(AsphodelDevice_t *device, int index, uint8_t addr, uint8_t *rx_data, uint8_t read_length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[3] = {index, addr, read_length};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (read_length == 0 || read_length > device->get_max_incoming_param_length(device))
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = rx_data;
	c->data[1] = (void*)(intptr_t)read_length;

	ret = device->do_transfer(device, CMD_DO_I2C_READ, params, sizeof(params), bus_read_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_i2c_read_blocking(AsphodelDevice_t *device, int index, uint8_t addr, uint8_t *rx_data, uint8_t read_length)
{
	MAKE_BLOCKING(device, asphodel_do_i2c_read(device, index, addr, rx_data, read_length, callback, closure));
}

// Does a write, then a read from the given address on the specified I2C bus.
ASPHODEL_API int asphodel_do_i2c_write_read(AsphodelDevice_t *device, int index, uint8_t addr, const uint8_t *tx_data, uint8_t write_length,
		uint8_t *rx_data, uint8_t read_length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (write_length == 0 || read_length == 0 || read_length > device->get_max_incoming_param_length(device))
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	params = (uint8_t*)malloc(write_length + 3);
	if (params == NULL)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	params[0] = index;
	params[1] = addr;
	params[2] = read_length;
	memcpy(&params[3], tx_data, write_length);

	c->callback = callback;
	c->closure = closure;
	c->data[0] = rx_data;
	c->data[1] = (void*)(intptr_t)read_length;

	ret = device->do_transfer(device, CMD_DO_I2C_WRITE_READ, params, write_length + 3, bus_read_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_i2c_write_read_blocking(AsphodelDevice_t *device, int index, uint8_t addr, const uint8_t *tx_data, uint8_t write_length,
		uint8_t *rx_data, uint8_t read_length)
{
	MAKE_BLOCKING(device, asphodel_do_i2c_write_read(device, index, addr, tx_data, write_length, rx_data, read_length, callback, closure));
}

// Do a fixed channel test with the radio hardware. For testing purposes only.
ASPHODEL_API int asphodel_do_radio_fixed_test(AsphodelDevice_t *device, uint16_t channel, uint16_t duration, uint8_t mode,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[5];

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_16bit_value(&params[0], channel);
	write_16bit_value(&params[2], duration);
	params[4] = mode;

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_DO_RADIO_FIXED_TEST, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_radio_fixed_test_blocking(AsphodelDevice_t *device, uint16_t channel, uint16_t duration, uint8_t mode)
{
	MAKE_BLOCKING(device, asphodel_do_radio_fixed_test(device, channel, duration, mode, callback, closure));
}

// Do a sweep test with the radio hardware. For testing purposes only.
ASPHODEL_API int asphodel_do_radio_sweep_test(AsphodelDevice_t *device, uint16_t start_channel, uint16_t stop_channel, uint16_t hop_interval,
		uint16_t hop_count, uint8_t mode, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[9];

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_16bit_value(&params[0], start_channel);
	write_16bit_value(&params[2], stop_channel);
	write_16bit_value(&params[4], hop_interval);
	write_16bit_value(&params[6], hop_count);
	params[8] = mode;

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_DO_RADIO_SWEEP_TEST, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_do_radio_sweep_test_blocking(AsphodelDevice_t *device, uint16_t start_channel, uint16_t stop_channel,
		uint16_t hop_interval, uint16_t hop_count, uint8_t mode)
{
	MAKE_BLOCKING(device, asphodel_do_radio_sweep_test(device, start_channel, stop_channel, hop_interval, hop_count, mode, callback, closure));
}

// Return the number of info regions present. For testing purposes only.
ASPHODEL_API int asphodel_get_info_region_count(AsphodelDevice_t *device, int *count, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = count;

	ret = device->do_transfer(device, CMD_GET_INFO_REGION_COUNT, NULL, 0, simple_count_transfer_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_info_region_count_blocking(AsphodelDevice_t *device, int *count)
{
	MAKE_BLOCKING(device, asphodel_get_info_region_count(device, count, callback, closure));
}

// Return the name of a specific info region in string form (UTF-8). The length parameter should hold the maximum number
// of bytes to write into buffer. Upon completion, the length parameter will hold the length of the info region name not
// including the null terminator. The length parameter may be set larger than its initial value if the buffer was not
// big enough to hold the entire info region name. For testing purposes only.
ASPHODEL_API int asphodel_get_info_region_name(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = buffer;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_INFO_REGION_NAME, params, sizeof(params), asphodel_get_name_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_info_region_name_blocking(AsphodelDevice_t *device, int index, char *buffer, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_info_region_name(device, index, buffer, length, callback, closure));
}

static void asphodel_get_info_region_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *data = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				data[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Reads data from a specific info region. The length parameter should hold the maximum number of bytes to write into
// the array. When the command is finished it will hold the number of bytes present in the info region (as opposed to
// the number of bytes actually written to the array). For testing purposes only.
ASPHODEL_API int asphodel_get_info_region(AsphodelDevice_t *device, int index, uint8_t *data, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t params[1] = {index};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = data;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_INFO_REGION, params, sizeof(params), asphodel_get_info_region_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_info_region_blocking(AsphodelDevice_t *device, int index, uint8_t *data, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_info_region(device, index, data, length, callback, closure));
}

static void asphodel_get_stack_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t *stack_info = (uint32_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 8)
		{
			stack_info[0] = read_32bit_value(&params[0]);
			stack_info[1] = read_32bit_value(&params[4]);
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

ASPHODEL_API int asphodel_get_stack_info(AsphodelDevice_t *device, uint32_t *stack_info, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = stack_info;

	ret = device->do_transfer(device, CMD_GET_STACK_INFO, NULL, 0, asphodel_get_stack_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_stack_info_blocking(AsphodelDevice_t *device, uint32_t *stack_info)
{
	MAKE_BLOCKING(device, asphodel_get_stack_info(device, stack_info, callback, closure));
}

static void asphodel_echo_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *reply = (uint8_t*)c->data[0];
	size_t *reply_length = (size_t*)c->data[1];

	if (status == 0)
	{
		size_t i;

		for (i = 0; i < *reply_length && i < param_length; i++)
		{
			reply[i] = params[i];
		}

		*reply_length = param_length;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Echo raw bytes. For testing purposes only.
ASPHODEL_API int asphodel_echo_raw(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = reply;
	c->data[1] = reply_length;

	ret = device->do_transfer(device, CMD_ECHO_RAW, data, data_length, asphodel_echo_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_echo_raw_blocking(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length)
{
	MAKE_BLOCKING(device, asphodel_echo_raw(device, data, data_length, reply, reply_length, callback, closure));
}

// Echo bytes as transaction. For testing purposes only.
ASPHODEL_API int asphodel_echo_transaction(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = reply;
	c->data[1] = reply_length;

	ret = device->do_transfer(device, CMD_ECHO_TRANSACTION, data, data_length, asphodel_echo_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_echo_transaction_blocking(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length)
{
	MAKE_BLOCKING(device, asphodel_echo_transaction(device, data, data_length, reply, reply_length, callback, closure));
}

// Echo parameters. For testing purposes only.
ASPHODEL_API int asphodel_echo_params(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = reply;
	c->data[1] = reply_length;

	ret = device->do_transfer(device, CMD_ECHO_PARAMS, data, data_length, asphodel_echo_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_echo_params_blocking(AsphodelDevice_t *device, const uint8_t *data, size_t data_length, uint8_t *reply, size_t *reply_length)
{
	MAKE_BLOCKING(device, asphodel_echo_params(device, data, data_length, reply, reply_length, callback, closure));
}

// Enable or disable the RF power output.
ASPHODEL_API int asphodel_enable_rf_power(AsphodelDevice_t *device, int enable, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[1] = {
		enable != 0,
	};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_ENABLE_RF_POWER, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_enable_rf_power_blocking(AsphodelDevice_t *device, int enable)
{
	MAKE_BLOCKING(device, asphodel_enable_rf_power(device, enable, callback, closure));
}

static void asphodel_get_rf_power_status_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *enabled = (int*)c->data[0];

	if (status == 0)
	{
		if (param_length == 1)
		{
			*enabled = params[0];
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Retrieve the enabled state of the RF power output.
ASPHODEL_API int asphodel_get_rf_power_status(AsphodelDevice_t *device, int *enabled, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = enabled;

	ret = device->do_transfer(device, CMD_GET_RF_POWER_STATUS, NULL, 0, asphodel_get_rf_power_status_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_rf_power_status_blocking(AsphodelDevice_t *device, int *enabled)
{
	MAKE_BLOCKING(device, asphodel_get_rf_power_status(device, enabled, callback, closure));
}

static void get_ctrl_var_indexes_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *ctrl_var_indexes = (uint8_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length <= 255)
		{
			size_t i;

			for (i = 0; i < *length && i < param_length; i++)
			{
				ctrl_var_indexes[i] = params[i];
			}

			*length = (uint8_t)param_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the control variable indexes that are related to RF power transmission. The length parameter should hold the
// maximum number of indexes to write into the array. When the command is finished it will hold the number of indexes
// reported by the device (as opposed to the number of indexes actually written to the array).
ASPHODEL_API int asphodel_get_rf_power_ctrl_vars(AsphodelDevice_t *device, uint8_t *ctrl_var_indexes, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = ctrl_var_indexes;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_RF_POWER_CTRL_VARS, NULL, 0, get_ctrl_var_indexes_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_rf_power_ctrl_vars_blocking(AsphodelDevice_t *device, uint8_t *ctrl_var_indexes, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_rf_power_ctrl_vars(device, ctrl_var_indexes, length, callback, closure));
}

// Sets or resets the RF Power timeout. The timeout parameter is specified in milliseconds. If the timeout duration
// passes without the device receiving another timeout reset command, then the device will disable the RF power output
// (if applicable). Sending a timeout value of 0 will disable the timeout functionality.
ASPHODEL_API int asphodel_reset_rf_power_timeout(AsphodelDevice_t *device, uint32_t timeout, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[4];

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[0], timeout);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_RESET_RF_POWER_TIMEOUT, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_reset_rf_power_timeout_blocking(AsphodelDevice_t *device, uint32_t timeout)
{
	MAKE_BLOCKING(device, asphodel_reset_rf_power_timeout(device, timeout, callback, closure));
}

// Stop the radio. Works on scanning, connected, and connecting radios. Has no effect on already stopped radios.
ASPHODEL_API int asphodel_stop_radio(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_STOP_RADIO, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_stop_radio_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_stop_radio(device, callback, closure));
}

// Start a scan with the radio. The radio will remain scanning until stopped. Scan results can be retreived using
// asphodel_get_start_radio_scan_results(). Starting a scan will remove any old scan results still in the device.
ASPHODEL_API int asphodel_start_radio_scan(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_START_RADIO_SCAN, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_start_radio_scan_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_start_radio_scan(device, callback, closure));
}

static void asphodel_get_raw_radio_scan_results_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t *serials = (uint32_t*)c->data[0];
	size_t *length = (size_t*)c->data[1];

	if (status == 0)
	{
		if (param_length % 4 == 0 && param_length < 255 * 4)
		{
			size_t array_length = param_length / 4;
			size_t i;

			for (i = 0; i < *length && i < array_length; i++)
			{
				serials[i] = read_32bit_value(&params[i * 4]);
			}

			*length = array_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Query the device for scanned serial numbers. The device will return at most
// floor(device->get_max_incoming_param_length()/4) serial numbers at a time. Another query should be performed to make
// sure there are no more. See asphodel_get_radio_scan_results() for a more user-friendly version.
ASPHODEL_API int asphodel_get_raw_radio_scan_results(AsphodelDevice_t *device, uint32_t *serials, size_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = serials;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_RADIO_SCAN_RESULTS, NULL, 0, asphodel_get_raw_radio_scan_results_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_raw_radio_scan_results_blocking(AsphodelDevice_t *device, uint32_t *serials, size_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_raw_radio_scan_results(device, serials, length, callback, closure));
}

static void asphodel_get_radio_scan_results_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t **serials = (uint32_t**)c->data[0];
	size_t *length = (size_t*)c->data[1];
	size_t max_incoming_serials = (size_t)(intptr_t)c->data[2];
	uint32_t *buffer = (uint32_t*)c->data[3];
	size_t buffer_filled = (size_t)(intptr_t)c->data[4];
	size_t buffer_length = (size_t)(intptr_t)c->data[5];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[6];

	if (status == 0)
	{
		if (param_length % 4 == 0 && param_length < 255 * 4)
		{
			size_t array_length = param_length / 4;
			size_t i;

			for (i = 0; i < array_length; i++)
			{
				size_t j;
				uint32_t serial = read_32bit_value(&params[i * 4]);
				int found = 0;

				// see if the serial is already present
				for (j = 0; j < buffer_filled; j++)
				{
					if (buffer[j] == serial)
					{
						found = 1;
						break;
					}
				}

				if (!found)
				{
					// add the value to the buffer
					if (buffer_filled == buffer_length)
					{
						// the buffer is already filled: make room
						size_t new_buffer_length = buffer_length + array_length - i; // add enough spots to finish this reply
						uint32_t *new_buffer = (uint32_t*)realloc(buffer, sizeof(uint32_t) * new_buffer_length);
						if (new_buffer == NULL)
						{
							free(buffer);
							if (c->callback != NULL)
							{
								c->callback(ASPHODEL_NO_MEM, c->closure);
							}
							free(c);
							return;
						}

						buffer = new_buffer;
						buffer_length = new_buffer_length;
					}

					buffer[buffer_filled] = serial;
					buffer_filled += 1;
				}
			}

			if (array_length < max_incoming_serials)
			{
				// got less than the maximum: must be finished
				*serials = buffer;
				*length = buffer_filled;
			}
			else
			{
				// got the maximum amount; there may be more to get
				int ret;

				// write back the values to the closure
				c->data[3] = buffer;
				c->data[4] = (void*)(intptr_t)buffer_filled;
				c->data[5] = (void*)(intptr_t)buffer_length;

				ret = device->do_transfer(device, CMD_GET_RADIO_SCAN_RESULTS, NULL, 0, asphodel_get_radio_scan_results_cb, c);

				if (ret == 0)
				{
					// ok
					return;
				}
				else
				{
					// got an error
					status = ret;
				}
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Will return query the device for scanned serial numbers until no more are returned. Each array entry will be unique.
// Entries are unsorted. Must call asphodel_free_radio_scan_results() on the result array when finished.
ASPHODEL_API int asphodel_get_radio_scan_results(AsphodelDevice_t *device, uint32_t **serials, size_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 7 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = serials;
	c->data[1] = length;
	c->data[2] = (void*)(intptr_t)(device->get_max_incoming_param_length(device) / 4); // maximum
	c->data[3] = NULL; // holds the buffer
	c->data[4] = (void*)(intptr_t)0; // holds the buffer filled size
	c->data[5] = (void*)(intptr_t)0; // holds the buffer size
	c->data[6] = device;

	ret = device->do_transfer(device, CMD_GET_RADIO_SCAN_RESULTS, NULL, 0, asphodel_get_radio_scan_results_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_scan_results_blocking(AsphodelDevice_t *device, uint32_t **serials, size_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_radio_scan_results(device, serials, length, callback, closure));
}

// Must be called on the arary returned by asphodel_get_all_radio_scan_results() or by
// asphodel_get_all_radio_scan_results_blocking() when finished with the results.
ASPHODEL_API void asphodel_free_radio_scan_results(uint32_t *serials)
{
	if (serials != NULL)
	{
		free(serials);
	}
}

static void asphodel_get_raw_radio_extra_scan_results_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelExtraScanResult_t *results = (AsphodelExtraScanResult_t*)c->data[0];
	size_t *length = (size_t*)c->data[1];

	if (status == 0)
	{
		if (param_length % 6 == 0 && param_length < 255 * 6)
		{
			size_t array_length = param_length / 6;
			size_t i;

			for (i = 0; i < *length && i < array_length; i++)
			{
				results[i].serial_number = read_32bit_value(&params[i * 6]);
				results[i].asphodel_type = params[i * 6 + 4];
				results[i].device_mode = params[i * 6 + 5];
			}

			*length = array_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Query the device for scan results. The device will return at most floor(device->get_max_incoming_param_length()/6)
// results at a time. Another query should be performed to make sure there are no more. See
// asphodel_get_radio_extra_scan_results() for a more user-friendly version.
ASPHODEL_API int asphodel_get_raw_radio_extra_scan_results(AsphodelDevice_t *device, AsphodelExtraScanResult_t *results, size_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = results;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_RADIO_EXTRA_SCAN_RESULTS, NULL, 0, asphodel_get_raw_radio_extra_scan_results_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_raw_radio_extra_scan_results_blocking(AsphodelDevice_t *device, AsphodelExtraScanResult_t *results, size_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_raw_radio_extra_scan_results(device, results, length, callback, closure));
}

static void asphodel_get_radio_extra_scan_results_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	AsphodelExtraScanResult_t **results = (AsphodelExtraScanResult_t**)c->data[0];
	size_t *length = (size_t*)c->data[1];
	size_t max_incoming_serials = (size_t)(intptr_t)c->data[2];
	AsphodelExtraScanResult_t *buffer = (AsphodelExtraScanResult_t*)c->data[3];
	size_t buffer_filled = (size_t)(intptr_t)c->data[4];
	size_t buffer_length = (size_t)(intptr_t)c->data[5];
	AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[6];

	if (status == 0)
	{
		if (param_length % 6 == 0 && param_length < 255 * 6)
		{
			size_t array_length = param_length / 6;
			size_t i;

			for (i = 0; i < array_length; i++)
			{
				size_t j;
				uint32_t serial = read_32bit_value(&params[i * 6]);
				uint8_t asphodel_type = params[i * 6 + 4];
				uint8_t device_mode = params[i * 6 + 5];
				int found = 0;

				// see if the serial is already present
				for (j = 0; j < buffer_filled; j++)
				{
					if (buffer[j].serial_number == serial)
					{
						found = 1;
						break;
					}
				}

				if (!found)
				{
					// add the value to the buffer
					if (buffer_filled == buffer_length)
					{
						// the buffer is already filled: make room
						size_t new_buffer_length = buffer_length + array_length - i; // add enough spots to finish this reply
						AsphodelExtraScanResult_t *new_buffer = (AsphodelExtraScanResult_t*)realloc(buffer, sizeof(AsphodelExtraScanResult_t) * new_buffer_length);
						if (new_buffer == NULL)
						{
							free(buffer);
							if (c->callback != NULL)
							{
								c->callback(ASPHODEL_NO_MEM, c->closure);
							}
							free(c);
							return;
						}

						buffer = new_buffer;
						buffer_length = new_buffer_length;
					}

					buffer[buffer_filled].serial_number = serial;
					buffer[buffer_filled].asphodel_type = asphodel_type;
					buffer[buffer_filled].device_mode = device_mode;
					buffer_filled += 1;
				}
			}

			if (array_length < max_incoming_serials)
			{
				// got less than the maximum: must be finished
				*results = buffer;
				*length = buffer_filled;
			}
			else
			{
				// got the maximum amount; there may be more to get
				int ret;

				// write back the values to the closure
				c->data[3] = buffer;
				c->data[4] = (void*)(intptr_t)buffer_filled;
				c->data[5] = (void*)(intptr_t)buffer_length;

				ret = device->do_transfer(device, CMD_GET_RADIO_EXTRA_SCAN_RESULTS, NULL, 0, asphodel_get_radio_extra_scan_results_cb, c);

				if (ret == 0)
				{
					// ok
					return;
				}
				else
				{
					// got an error
					status = ret;
				}
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Will query the device for scan results until no more are returned. Each array entry will have a unique serial
// number. Entries are unsorted. Must call asphodel_free_radio_extra_scan_results() on the result array when finished.
ASPHODEL_API int asphodel_get_radio_extra_scan_results(AsphodelDevice_t *device, AsphodelExtraScanResult_t **results, size_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 7 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = results;
	c->data[1] = length;
	c->data[2] = (void*)(intptr_t)(device->get_max_incoming_param_length(device) / 6); // maximum
	c->data[3] = NULL; // holds the buffer
	c->data[4] = (void*)(intptr_t)0; // holds the buffer filled size
	c->data[5] = (void*)(intptr_t)0; // holds the buffer size
	c->data[6] = device;

	ret = device->do_transfer(device, CMD_GET_RADIO_EXTRA_SCAN_RESULTS, NULL, 0, asphodel_get_radio_extra_scan_results_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_extra_scan_results_blocking(AsphodelDevice_t *device, AsphodelExtraScanResult_t **results, size_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_radio_extra_scan_results(device, results, length, callback, closure));
}

// Must be called on the arary returned by asphodel_get_radio_extra_scan_results() or by
// asphodel_get_radio_extra_scan_results_blocking() when finished with the results.
ASPHODEL_API void asphodel_free_radio_extra_scan_results(AsphodelExtraScanResult_t *results)
{
	if (results != NULL)
	{
		free(results);
	}
}

static void asphodel_get_radio_scan_power_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int8_t *powers = (int8_t*)c->data[0];
	size_t length = (size_t)c->data[1];

	if (status == 0)
	{
		if (param_length == length)
		{
			size_t i;
			for (i = 0; i < length; i++)
			{
				powers[i] = (int8_t)params[i];
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Will return the received radio power during the scan for the specified serial numbers. A power reading of 0x7F means
// there was no information for that serial number. The array length must be less than or equal to the smaller of
// (max_outgoing_param_length / 4) and (max_incoming_param_len / 1).
ASPHODEL_API int asphodel_get_radio_scan_power(AsphodelDevice_t *device, const uint32_t *serials, int8_t *powers, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));
	uint8_t *params;
	size_t i;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = powers;
	c->data[1] = (void*)(intptr_t)length;

	if (length == 0 || length > device->get_max_incoming_param_length(device) || length > (device->get_max_outgoing_param_length(device) / 4))
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	params = (uint8_t *)malloc(length * 4);
	if (!params)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	for (i = 0; i < length; i++)
	{
		write_32bit_value(&params[i * 4], serials[i]);
	}

	ret = device->do_transfer(device, CMD_GET_RADIO_SCAN_POWER, params, length * 4, asphodel_get_radio_scan_power_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_scan_power_blocking(AsphodelDevice_t *device, const uint32_t *serials, int8_t *powers, size_t length)
{
	MAKE_BLOCKING(device, asphodel_get_radio_scan_power(device, serials, powers, length, callback, closure));
}

// Connect the radio's remote to a specific serial number.
ASPHODEL_API int asphodel_connect_radio(AsphodelDevice_t *device, uint32_t serial_number, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[4];

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[0], serial_number);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_CONNECT_RADIO, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_connect_radio_blocking(AsphodelDevice_t *device, uint32_t serial_number)
{
	MAKE_BLOCKING(device, asphodel_connect_radio(device, serial_number, callback, closure));
}

static void asphodel_get_radio_status_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *connected = (int*)c->data[0];
	uint32_t *serial_number = (uint32_t*)c->data[1];
	uint8_t *protocol_type = (uint8_t*)c->data[2];
	int *scanning = (int*)c->data[3];

	if (status == 0)
	{
		if (param_length == 7)
		{
			if (connected != NULL)
			{
				*connected = params[0];
			}

			if (serial_number != NULL)
			{
				*serial_number = read_32bit_value(&params[1]);
			}

			if (protocol_type != NULL)
			{
				*protocol_type = params[5];
			}

			if (scanning != NULL)
			{
				*scanning = params[6];
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Retrieve the current state of the radio. The state of the radio can be determined from the return values as follows:
// Radio Stopped: connected=0, serial_number=0, protocol_type=0, scanning=0
// Radio Scanning: connected=0, serial_number=0, protocol_type=0, scanning=1
// Radio Connecting: connected=0, serial_number=<sn>, protocol_type=0, scanning=0
// Radio Connected: connected=1, serial_number=<sn>, protocol_type=<type>, scanning=0
ASPHODEL_API int asphodel_get_radio_status(AsphodelDevice_t *device, int *connected, uint32_t *serial_number, uint8_t *protocol_type,
		int *scanning, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 4 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = connected;
	c->data[1] = serial_number;
	c->data[2] = protocol_type;
	c->data[3] = scanning;

	ret = device->do_transfer(device, CMD_GET_RADIO_STATUS, NULL, 0, asphodel_get_radio_status_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_status_blocking(AsphodelDevice_t *device, int *connected, uint32_t *serial_number, uint8_t *protocol_type, int *scanning)
{
	MAKE_BLOCKING(device, asphodel_get_radio_status(device, connected, serial_number, protocol_type, scanning, callback, closure));
}

// Return the control variable indexes that are related to radio operation. The length parameter should hold the
// maximum number of indexes to write into the array. When the command is finished it will hold the number of indexes
// reported by the device (as opposed to the number of indexes actually written to the array).
ASPHODEL_API int asphodel_get_radio_ctrl_vars(AsphodelDevice_t *device, uint8_t *ctrl_var_indexes, uint8_t *length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = ctrl_var_indexes;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_RADIO_CTRL_VARS, NULL, 0, get_ctrl_var_indexes_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_ctrl_vars_blocking(AsphodelDevice_t *device, uint8_t *ctrl_var_indexes, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_radio_ctrl_vars(device, ctrl_var_indexes, length, callback, closure));
}

static void asphodel_get_radio_default_serial_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t *serial_number = (uint32_t*)c->data[0];

	if (status == 0)
	{
		if (param_length == 4)
		{
			if (serial_number != NULL)
			{
				*serial_number = read_32bit_value(&params[0]);
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_UNIMPLEMENTED_COMMAND)
	{
		// device is too old to support this command; so report default serial of zero
		if (serial_number != NULL)
		{
			*serial_number = 0;
		}
		status = 0;
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the default serial number configured for use with the radio. A default serial number of 0 means no serial
// number has been set as the default, or the functionality has been disabled.
ASPHODEL_API int asphodel_get_radio_default_serial(AsphodelDevice_t *device, uint32_t *serial_number, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = serial_number;

	ret = device->do_transfer(device, CMD_GET_RADIO_DEFAULT_SERIAL, NULL, 0, asphodel_get_radio_default_serial_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_radio_default_serial_blocking(AsphodelDevice_t *device, uint32_t *serial_number)
{
	MAKE_BLOCKING(device, asphodel_get_radio_default_serial(device, serial_number, callback, closure));
}

// Start a bootloader scan with the radio. The radio will remain scanning until stopped. Scan results can be retreived
// using asphodel_get_start_radio_scan_results(). Starting a scan will remove any old scan results still in the device.
ASPHODEL_API int asphodel_start_radio_scan_boot(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_START_RADIO_SCAN_BOOT, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_start_radio_scan_boot_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_start_radio_scan_boot(device, callback, closure));
}

// Connect the radio's remote to a specific serial number, in bootloader mode.
ASPHODEL_API int asphodel_connect_radio_boot(AsphodelDevice_t *device, uint32_t serial_number, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t params[4];

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[0], serial_number);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_CONNECT_RADIO_BOOT, params, sizeof(params), simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_connect_radio_boot_blocking(AsphodelDevice_t *device, uint32_t serial_number)
{
	MAKE_BLOCKING(device, asphodel_connect_radio_boot(device, serial_number, callback, closure));
}

// Stop the remote's radio. Has no effect on already stopped radios.
ASPHODEL_API int asphodel_stop_remote(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_STOP_REMOTE, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_stop_remote_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_stop_remote(device, callback, closure));
}

// Restarts the remote's radio, with the previously connected serial number.
ASPHODEL_API int asphodel_restart_remote(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_RESTART_REMOTE, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_restart_remote_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_restart_remote(device, callback, closure));
}

static void asphodel_get_remote_status_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	int *connected = (int*)c->data[0];
	uint32_t *serial_number = (uint32_t*)c->data[1];
	uint8_t *protocol_type = (uint8_t*)c->data[2];

	if (status == 0)
	{
		if (param_length == 6)
		{
			if (connected != NULL)
			{
				*connected = params[0];
			}

			if (serial_number != NULL)
			{
				*serial_number = read_32bit_value(&params[1]);
			}

			if (protocol_type != NULL)
			{
				*protocol_type = params[5];
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Return the remote's status. Will provide the serial number of the currently connected (or last connected) device.
// If no serial number has ever been used with the device, the serial number will be 0.
ASPHODEL_API int asphodel_get_remote_status(AsphodelDevice_t *device, int *connected, uint32_t *serial_number, uint8_t *protocol_type,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 3 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = connected;
	c->data[1] = serial_number;
	c->data[2] = protocol_type;

	ret = device->do_transfer(device, CMD_GET_REMOTE_STATUS, NULL, 0, asphodel_get_remote_status_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_remote_status_blocking(AsphodelDevice_t *device, int *connected, uint32_t *serial_number, uint8_t *protocol_type)
{
	MAKE_BLOCKING(device, asphodel_get_remote_status(device, connected, serial_number, protocol_type, callback, closure));
}

// Restarts the remote's radio, with the previously connected serial number. Forces the use of application mode.
ASPHODEL_API int asphodel_restart_remote_app(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_RESTART_REMOTE_APP, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_restart_remote_app_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_restart_remote_app(device, callback, closure));
}

// Restarts the remote's radio, with the previously connected serial number. Forces the use of bootloader mode.
ASPHODEL_API int asphodel_restart_remote_boot(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_RESTART_REMOTE_BOOT, NULL, 0, simple_no_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_restart_remote_boot_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_restart_remote_boot(device, callback, closure));
}


// Start the main program from the bootloader. Implicitly resets the device, like asphodel_reset().
ASPHODEL_API int asphodel_bootloader_start_program(AsphodelDevice_t *device, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer_reset(device, CMD_BOOTLOADER_START_PROGRAM, NULL, 0, asphodel_reset_or_bootloader_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_bootloader_start_program_blocking(AsphodelDevice_t *device)
{
	MAKE_BLOCKING(device, asphodel_bootloader_start_program(device, callback, closure));
}

static void asphodel_get_bootloader_page_info_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint32_t *page_info = (uint32_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length >= 8 && param_length % 8 == 0 && param_length <= (255 * 4))
		{
			size_t array_length = param_length / 4;
			size_t i;

			for (i = 0; i < *length && i < array_length; i++)
			{
				uint32_t value = read_32bit_value(&params[i * 4]);
				page_info[i] = value;
			}

			*length = (uint8_t)array_length;
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Returns page information from the bootloader. The length parameter should hold the maximum number of entries to
// write into the array. When the command is finished it will hold the number of entries available on the device (as
// opposed to the number of entries actually written to the array). Entries are paris of page count and page size, in
// that order. The total number of pages is the sum of all page counts. The total number of bytes is the sum of the
// products of each entry pair (i.e. page count * page size).
ASPHODEL_API int asphodel_get_bootloader_page_info(AsphodelDevice_t *device, uint32_t *page_info, uint8_t *length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = page_info;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_BOOTLOADER_PAGE_INFO, NULL, 0, asphodel_get_bootloader_page_info_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_bootloader_page_info_blocking(AsphodelDevice_t *device, uint32_t *page_info, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_bootloader_page_info(device, page_info, length, callback, closure));
}


static void asphodel_get_bootloader_block_sizes_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint16_t *block_sizes = (uint16_t*)c->data[0];
	uint8_t *length = (uint8_t*)c->data[1];

	if (status == 0)
	{
		if (param_length % 2 == 0 && param_length >= 2 && param_length < 255 * 2)
		{
			size_t array_length = param_length / 2;
			size_t i;
			uint16_t last = 0;
			uint8_t ok = 1;

			for (i = 0; i < *length && i < array_length; i++)
			{
				uint16_t value = read_16bit_value(&params[i * 2]);
				if (value <= last)
				{
					status = ASPHODEL_MALFORMED_REPLY;
					ok = 0;
					break;
				}
				last = value;
				block_sizes[i] = value;
			}

			if (ok)
			{
				*length = (uint8_t)array_length;
			}
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Fill an array with the allowed code block sizes for the device. The length parameter should hold the maximum number
// of block sizes to write into the array. When the command is finished it will hold the number of block sizes
// available on the device (as opposed to the number of block sizes actually written to the array).
// The array of code block sizes will be sorted, ascending. There will be no duplicates, and all values will be greater
// than zero.
ASPHODEL_API int asphodel_get_bootloader_block_sizes(AsphodelDevice_t *device, uint16_t *block_sizes, uint8_t *length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 2 * sizeof(void *));

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = block_sizes;
	c->data[1] = length;

	ret = device->do_transfer(device, CMD_GET_BOOTLOADER_BLOCK_SIZES, NULL, 0, asphodel_get_bootloader_block_sizes_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_get_bootloader_block_sizes_blocking(AsphodelDevice_t *device, uint16_t *block_sizes, uint8_t *length)
{
	MAKE_BLOCKING(device, asphodel_get_bootloader_block_sizes(device, block_sizes, length, callback, closure));
}

// Prepares a page for writing. Must be called before writing code blocks or verifying pages. The nonce, if any, is
// used by the device to decode the written code blocks.
ASPHODEL_API int asphodel_start_bootloader_page(AsphodelDevice_t *device, uint32_t page_number, uint8_t *nonce, size_t length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	params = (uint8_t*)malloc(4 + length);
	if (params == NULL)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	write_32bit_value(&params[0], page_number);
	memcpy(&params[4], nonce, length);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_START_BOOTLOADER_PAGE, params, 4 + length, simple_no_reply_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_start_bootloader_page_blocking(AsphodelDevice_t *device, uint32_t page_number, uint8_t *nonce, size_t length)
{
	MAKE_BLOCKING(device, asphodel_start_bootloader_page(device, page_number, nonce, length, callback, closure));
}

// Write a code block to the current page. Code blocks must be sent strictly sequentally. Blocks are only accepted by
// the device in certain sizes. See asphodel_get_bootloader_block_sizes() for allowable code block sizes.
// NOTE: after the final code block is written to a page, the page must be finished with a call to
// asphodel_finish_bootloader_page().
ASPHODEL_API int asphodel_write_bootloader_code_block(AsphodelDevice_t *device, uint8_t *data, size_t length,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (length == 0)
	{
		free(c);
		return ASPHODEL_BAD_PARAMETER;
	}

	params = (uint8_t*)malloc(length);
	if (params == NULL)
	{
		free(c);
		return ASPHODEL_NO_MEM;
	}

	memcpy(params, data, length);

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_WRITE_BOOTLOADER_CODE_BLOCK, params, length, simple_no_reply_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_write_bootloader_code_block_blocking(AsphodelDevice_t *device, uint8_t *data, size_t length)
{
	MAKE_BLOCKING(device, asphodel_write_bootloader_code_block(device, data, length, callback, closure));
}

typedef struct {
	AsphodelDevice_t *device;
	AsphodelCommandCallback_t callback;
	void * closure;
	uint8_t *data_start;
	uint8_t *data;
	size_t remaining_length;
	uint16_t *block_sizes;
	uint8_t block_sizes_length;
} WriteBootloaderPageClosure_t;

static void asphodel_write_bootloader_page_cb(int status, void * closure)
{
	WriteBootloaderPageClosure_t *write_page_closure = (WriteBootloaderPageClosure_t*)closure;

	if (status == 0)
	{
		if (write_page_closure->remaining_length == 0)
		{
			// finished
			if (write_page_closure->callback)
			{
				write_page_closure->callback(0, write_page_closure->closure);
			}

			free(write_page_closure->data_start);
			free(write_page_closure->block_sizes);
			free(write_page_closure);
		}
		else
		{
			// need more writes
			int ret;
			uint16_t next_transfer_size = 0;
			uint8_t i;
			uint8_t *next_transfer_data = write_page_closure->data;

			for (i = 0; i < write_page_closure->block_sizes_length; i++)
			{
				uint16_t block_size = write_page_closure->block_sizes[i];
				if (block_size <= write_page_closure->remaining_length)
				{
					next_transfer_size = block_size;
				}
				else
				{
					break;
				}
			}

			write_page_closure->remaining_length -= next_transfer_size;
			write_page_closure->data += next_transfer_size;

			ret = asphodel_write_bootloader_code_block(write_page_closure->device, next_transfer_data, next_transfer_size,
					asphodel_write_bootloader_page_cb, write_page_closure);

			if (ret != 0)
			{
				// error
				if (write_page_closure->callback)
				{
					write_page_closure->callback(ret, write_page_closure->closure);
				}

				free(write_page_closure->data_start);
				free(write_page_closure->block_sizes);
				free(write_page_closure);
			}
		}
	}
	else
	{
		// error
		if (write_page_closure->callback)
		{
			write_page_closure->callback(status, write_page_closure->closure);
		}

		free(write_page_closure->data_start);
		free(write_page_closure->block_sizes);
		free(write_page_closure);
	}
}

// Wrapper which calls asphodel_write_bootloader_code_block() repeatedly with valid block sizes.
ASPHODEL_API int asphodel_write_bootloader_page(AsphodelDevice_t *device, uint8_t *data, size_t data_length, uint16_t *block_sizes,
		uint8_t block_sizes_length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	uint16_t *block_sizes_copy;
	uint16_t last_block_size = 0;
	uint16_t first_transfer_block_size = 0;
	uint8_t i;
	size_t remaining_length;
	WriteBootloaderPageClosure_t *write_page_closure;
	uint8_t *data_copy;

	if (data_length == 0)
	{
		// not really an error, but nothing has to be done
		if (callback)
		{
			callback(0, closure);
		}
		return 0;
	}

	if (block_sizes_length == 0)
	{
		return ASPHODEL_BAD_PARAMETER;
	}

	block_sizes_copy = (uint16_t*)malloc(sizeof(uint16_t) * block_sizes_length);
	if (!block_sizes_copy)
	{
		return ASPHODEL_NO_MEM;
	}

	// copy block sizes, and check that sizes are ascending and non-zero
	for (i = 0; i < block_sizes_length; i++)
	{
		uint16_t block_size = block_sizes[i];

		if (block_size <= last_block_size)
		{
			free(block_sizes_copy);
			return ASPHODEL_BAD_PARAMETER;
		}

		block_sizes_copy[i] = block_size;
		last_block_size = block_size;

		// check for an easy way out
		if (data_length == block_size)
		{
			// we can do the whole transfer in one go
			free(block_sizes_copy);
			return asphodel_write_bootloader_code_block(device, data, data_length, callback, closure);
		}

		// find the largest block size that's smaller than the data length
		if (data_length > block_size)
		{
			first_transfer_block_size = block_size;
		}
	}

	// make sure that data_length can be decomposed into block size transfers, with nothing left over
	remaining_length = data_length;
	for (i = 0; i < block_sizes_length; i++)
	{
		// NOTE: iterating backwards
		uint16_t block_size = block_sizes[block_sizes_length - i - 1];
		if (remaining_length >= block_size)
		{
			remaining_length = remaining_length % block_size;
		}
	}
	if (remaining_length != 0)
	{
		// it's not simple (or possible?) to split into valid block sizes.
		// while this is a valid error, it's unlikely to happen in the real world, since the device sets the valid block sizes with this sort of situation in mind
		free(block_sizes_copy);
		return ASPHODEL_BAD_PARAMETER;
	}

	// if we've gotten to this point, we're going to actually do things.

	write_page_closure = (WriteBootloaderPageClosure_t*)malloc(sizeof(WriteBootloaderPageClosure_t));
	if (!write_page_closure)
	{
		free(block_sizes_copy);
		return ASPHODEL_NO_MEM;
	}

	data_copy = (uint8_t*)malloc(data_length);
	if (!data_copy)
	{
		free(block_sizes_copy);
		free(write_page_closure);
		return ASPHODEL_NO_MEM;
	}

	memcpy(data_copy, data, data_length);

	write_page_closure->device = device;
	write_page_closure->callback = callback;
	write_page_closure->closure = closure;
	write_page_closure->data_start = data_copy;
	write_page_closure->data = data_copy + first_transfer_block_size;
	write_page_closure->remaining_length = data_length - first_transfer_block_size;
	write_page_closure->block_sizes = block_sizes_copy;
	write_page_closure->block_sizes_length = block_sizes_length;

	ret = asphodel_write_bootloader_code_block(device, data_copy, first_transfer_block_size, asphodel_write_bootloader_page_cb, write_page_closure);

	if (ret != 0)
	{
		free(block_sizes_copy);
		free(write_page_closure);
		free(data_copy);
	}

	return ret;
}

ASPHODEL_API int asphodel_write_bootloader_page_blocking(AsphodelDevice_t *device, uint8_t *data, size_t data_length, uint16_t *block_sizes,
		uint8_t block_sizes_length)
{
	MAKE_BLOCKING(device, asphodel_write_bootloader_page(device, data, data_length, block_sizes, block_sizes_length, callback, closure));
}

static void asphodel_finish_bootloader_page_cb(int status, const uint8_t *params, size_t param_length, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;

	if (status == 0)
	{
		if (param_length == 0)
		{
			// nothing to do
			(void)params; // suppress unused parameter warning
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}
	else if (status == ERROR_CODE_INCOMPLETE)
	{
		AsphodelDevice_t *device = (AsphodelDevice_t*)c->data[0];

		status = device->do_transfer(device, CMD_FINISH_BOOTLOADER_PAGE, NULL, 0, asphodel_finish_bootloader_page_cb, c);

		if (status == 0)
		{
			return;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Must be called after all code blocks for a specific page have been written. The MAC tag is used to verify the
// contents of the page.
ASPHODEL_API int asphodel_finish_bootloader_page(AsphodelDevice_t *device, uint8_t *mac_tag, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (length != 0)
	{
		params = (uint8_t*)malloc(length);
		if (params == NULL)
		{
			free(c);
			return ASPHODEL_NO_MEM;
		}

		memcpy(params, mac_tag, length);
	}
	else
	{
		params = NULL;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = device;

	ret = device->do_transfer(device, CMD_FINISH_BOOTLOADER_PAGE, params, length, asphodel_finish_bootloader_page_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_finish_bootloader_page_blocking(AsphodelDevice_t *device, uint8_t *mac_tag, size_t length)
{
	MAKE_BLOCKING(device, asphodel_finish_bootloader_page(device, mac_tag, length, callback, closure));
}

// Used to verify the contents of a page. The page contents are checked against the MAC tag to verify integrity.
// NOTE: the asphodel_start_bootloader_page() must be called for the page prior to verification.
ASPHODEL_API int asphodel_verify_bootloader_page(AsphodelDevice_t *device, uint8_t *mac_tag, size_t length, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t));
	uint8_t *params;

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	if (length != 0)
	{
		params = (uint8_t*)malloc(length);
		if (params == NULL)
		{
			free(c);
			return ASPHODEL_NO_MEM;
		}

		memcpy(params, mac_tag, length);
	}
	else
	{
		params = NULL;
	}

	c->callback = callback;
	c->closure = closure;

	ret = device->do_transfer(device, CMD_VERIFY_BOOTLOADER_PAGE, params, length, simple_no_reply_cb, c);

	free(params);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_verify_bootloader_page_blocking(AsphodelDevice_t *device, uint8_t *mac_tag, size_t length)
{
	MAKE_BLOCKING(device, asphodel_verify_bootloader_page(device, mac_tag, length, callback, closure));
}

// Supported types: CHANNEL_TYPE_SLOW_STRAIN, CHANNEL_TYPE_FAST_STRAIN, CHANNEL_TYPE_COMPOSITE_STRAIN
// Returns the number of strain bridges present on the given channel. Other strain functions called on this channel
// will accept bridge indexes from 0 to bridge_count-1.
// NOTE: this will return 0 for any unsupported channel type.
ASPHODEL_API int asphodel_get_strain_bridge_count(AsphodelChannelInfo_t *channel_info, int *bridge_count)
{
	if (channel_info->channel_type == CHANNEL_TYPE_SLOW_STRAIN || channel_info->channel_type == CHANNEL_TYPE_FAST_STRAIN)
	{
		*bridge_count = 1;
		return 0;
	}
	else if (channel_info->channel_type == CHANNEL_TYPE_COMPOSITE_STRAIN)
	{
		if (channel_info->chunk_count < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		if (channel_info->chunk_lengths[0] < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		*bridge_count = channel_info->chunks[0][0];
		return 0;
	}
	else
	{
		// not a strain channel
		*bridge_count = 0;
		return 0;
	}
}

// Supported types: CHANNEL_TYPE_SLOW_STRAIN, CHANNEL_TYPE_FAST_STRAIN, CHANNEL_TYPE_COMPOSITE_STRAIN
// Returns the subchannel index for a particular bridge index. This is necessary for collecting the streaming data for
// to be used with asphodel_check_strain_resistances().
ASPHODEL_API int asphodel_get_strain_bridge_subchannel(AsphodelChannelInfo_t *channel_info, int bridge_index, size_t *subchannel_index)
{
	if (channel_info->channel_type == CHANNEL_TYPE_SLOW_STRAIN || channel_info->channel_type == CHANNEL_TYPE_FAST_STRAIN)
	{
		if (bridge_index > 0)
		{
			// channels with multiple bridges are not supported
			return ASPHODEL_BAD_PARAMETER;
		}

		*subchannel_index = 0;
		return 0;
	}
	else if (channel_info->channel_type == CHANNEL_TYPE_COMPOSITE_STRAIN)
	{
		if (channel_info->chunk_count < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		if (channel_info->chunk_lengths[0] < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		int bridge_count = channel_info->chunks[0][0];

		if (bridge_count <= bridge_index)
		{
			return ASPHODEL_BAD_PARAMETER;
		}

		// composite subchannel is first, next is bridge index 0
		*subchannel_index = bridge_index + 1;
		return 0;
	}
	else
	{
		// not a strain channel
		return ASPHODEL_BAD_CHANNEL_TYPE;
	}
}

// Supported types: CHANNEL_TYPE_SLOW_STRAIN, CHANNEL_TYPE_FAST_STRAIN, CHANNEL_TYPE_COMPOSITE_STRAIN
// Fills the values array (length 5) with bridge values from the channel's chunk data.
// values[0] is the resistance of the positive sense resistor (in ohms). NOTE: 0.0 means not present.
// values[1] is the resistance of the negative sense resistor (in ohms). NOTE: 0.0 means not present.
// values[2] is the nominal resistance of the bridge elements (in ohms)
// values[3] is the minimum resistance of the bridge elements (in ohms)
// values[4] is the maximum resistance of the bridge elements (in ohms)
ASPHODEL_API int asphodel_get_strain_bridge_values(AsphodelChannelInfo_t *channel_info, int bridge_index, float *values)
{
	int chunk_index; // holds bridge values
	const uint8_t *chunk;
	size_t i;

	if (channel_info->channel_type == CHANNEL_TYPE_SLOW_STRAIN || channel_info->channel_type == CHANNEL_TYPE_FAST_STRAIN)
	{
		if (bridge_index > 0)
		{
			return ASPHODEL_BAD_PARAMETER;
		}

		chunk_index = 0;
	}
	else if (channel_info->channel_type == CHANNEL_TYPE_COMPOSITE_STRAIN)
	{
		if (channel_info->chunk_count < 1) // NOTE: not the only check on chunk count; just the first
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		if (channel_info->chunk_lengths[0] < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		int bridge_count = channel_info->chunks[0][0];

		if (bridge_count <= bridge_index)
		{
			return ASPHODEL_BAD_PARAMETER;
		}

		// first is control chunk, then composite subchannel name, then each bridge has a name, then the resistance chunks
		chunk_index = 2 + bridge_count + bridge_index;
	}
	else
	{
		return ASPHODEL_BAD_CHANNEL_TYPE;
	}

	if (channel_info->chunk_count <= chunk_index)
	{
		return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
	}

	if (channel_info->chunk_lengths[chunk_index] != 5 * 4)
	{
		// chunk can't be interpreted as float[5] array
		return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
	}

	chunk = channel_info->chunks[chunk_index];
	for (i = 0; i < 5; i++)
	{
		values[i] = read_float_value(&chunk[i * 4]);
	}

	return 0;
}

static void channel_specific_empty_reply_cb(int status, void * closure)
{
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)closure;
	uint8_t *reply_length = (uint8_t*)&c->data[0];

	if (status == 0)
	{
		if (*reply_length == 0)
		{
			// nothing to do
		}
		else
		{
			status = ASPHODEL_BAD_REPLY_LENGTH;
		}
	}

	if (c->callback != NULL)
	{
		c->callback(status, c->closure);
	}

	free(c);
}

// Supported types: CHANNEL_TYPE_SLOW_STRAIN, CHANNEL_TYPE_FAST_STRAIN, CHANNEL_TYPE_COMPOSITE_STRAIN
// Sets the output of the sense resistors. The side inputs are booleans (0 or 1).
// Wraps a call to asphodel_channel_specific().
ASPHODEL_API int asphodel_set_strain_outputs(AsphodelDevice_t *device, int channel_index, int bridge_index, int positive_side, int negative_side,
		AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));

	uint8_t params[4];
	uint8_t param_len;

	// NOTE: this is a bit hacky, but it's the best backwards compatible method I could determine
	if (bridge_index == 0)
	{
		params[0] = STRAIN_SET_OUTPUTS;
		params[1] = (positive_side != 0);
		params[2] = (negative_side != 0);
		param_len = 3;
	}
	else
	{
		params[0] = STRAIN_SET_OUTPUTS;
		params[1] = bridge_index;
		params[2] = (positive_side != 0);
		params[3] = (negative_side != 0);
		param_len = 4;
	}

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	*(uint8_t*)&c->data[0] = 0; // will be used as reply_length

	ret = asphodel_channel_specific(device, channel_index, params, param_len, NULL, (uint8_t*)&c->data[0], channel_specific_empty_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_set_strain_outputs_blocking(AsphodelDevice_t *device, int channel_index, int bridge_index, int positive_side, int negative_side)
{
	MAKE_BLOCKING(device, asphodel_set_strain_outputs(device, channel_index, bridge_index, positive_side, negative_side, callback, closure));
}

// Supported types: CHANNEL_TYPE_SLOW_STRAIN, CHANNEL_TYPE_FAST_STRAIN, CHANNEL_TYPE_COMPOSITE_STRAIN
// Checks the element resistances of a strain channel.
// The three inputs should be measured with various parameters to asphodel_set_strain_outputs():
// baseline should be the average value with positive_size=1, negative_side=0.
// positive_high should be the average value with positive_size=1, negative_side=0.
// negative_high should be the average value with positive_size=0, negative_side=1.
// NOTE: all measurements should be made in the channel's unit type, not a derived unit.
// positive_resistance will be the calculated resistance (in ohms) for the elements on the positive side of the bridge.
// negative_resistance will be the calculated resistance (in ohms) for the elements on the negative side of the bridge.
// passed will be set to 1 if the resistances are within an acceptable range. Otherwise, it will be set to 0.
ASPHODEL_API int asphodel_check_strain_resistances(AsphodelChannelInfo_t *channel_info, int bridge_index, double baseline,
		double positive_high, double negative_high, double *positive_resistance, double *negative_resistance, int *passed)
{
	int ret;
	double diff_scale;
	double pos_diff;
	double neg_diff;
	float bridge_values[5];

	if (channel_info->channel_type == CHANNEL_TYPE_SLOW_STRAIN || channel_info->channel_type == CHANNEL_TYPE_FAST_STRAIN)
	{
		if (bridge_index > 0)
		{
			return ASPHODEL_BAD_PARAMETER;
		}

		if (channel_info->coefficients_length < 3)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		// coefficients[2] holds the differential scale, to convert from the integer representation to the unitless bridge ratio.
		// We divide by coefficients[0] (i.e. unit scale) to get the original integer representation
		diff_scale = ((double)channel_info->coefficients[2]) / ((double)channel_info->coefficients[0]);
	}
	else if (channel_info->channel_type == CHANNEL_TYPE_COMPOSITE_STRAIN)
	{
		if (channel_info->chunk_count < 1) // NOTE: not the only check on chunk count; just the first
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		if (channel_info->chunk_lengths[0] < 1)
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		int bridge_count = channel_info->chunks[0][0];

		if (bridge_count <= bridge_index)
		{
			return ASPHODEL_BAD_PARAMETER;
		}

		if (channel_info->coefficients_length < 3 * (bridge_index + 1))
		{
			return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
		}

		// coefficients[2] holds the differential scale, to convert from the integer representation to the unitless bridge ratio.
		// We divide by coefficients[0] (i.e. unit scale) to get the original integer representation
		diff_scale = ((double)channel_info->coefficients[bridge_index * 3 + 2]) / ((double)channel_info->coefficients[bridge_index * 3]);
	}
	else
	{
		return ASPHODEL_BAD_CHANNEL_TYPE;
	}

	pos_diff = (positive_high - baseline) * diff_scale;
	neg_diff = (baseline - negative_high) * diff_scale;

	ret = asphodel_get_strain_bridge_values(channel_info, bridge_index, bridge_values);
	if (ret != 0)
	{
		return ret;
	}

	if (1.0 - pos_diff == 0.0)
	{
		// avoid divide by zero
		*positive_resistance = INFINITY;
	}
	else
	{
		// bridge_values[0] hold sense resistance
		*positive_resistance = 2.0 * bridge_values[0] * (pos_diff / (1.0 - pos_diff));
	}

	if (1.0 - neg_diff == 0.0)
	{
		// avoid divide by zero
		*negative_resistance = INFINITY;
	}
	else
	{
		// bridge_values[1] hold sense resistance
		*negative_resistance = 2.0 * bridge_values[1] * (neg_diff / (1.0 - neg_diff));
	}

	if (bridge_values[0] != 0.0) // a 0 value sense resistor means skip the check
	{
		// bridge_values[3] is minimum resistance, bridge_values[4] is maximum resistance
		if (*positive_resistance < bridge_values[3] || *positive_resistance > bridge_values[4])
		{
			*passed = 0;
			return 0; // return now; any failure is a failure
		}
	}

	if (bridge_values[1] != 0.0) // a 0 value sense resistor means skip the check
	{
		// bridge_values[3] is minimum resistance, bridge_values[4] is maximum resistance
		if (*negative_resistance < bridge_values[3] || *negative_resistance > bridge_values[4])
		{
			*passed = 0;
			return 0; // return now; any failure is a failure
		}
	}

	// if we got here, everything passed
	*passed = 1;
	return 0;
}

// Supported types: CHANNEL_TYPE_SLOW_ACCEL, CHANNEL_TYPE_PACKED_ACCEL, CHANNEL_TYPE_LINEAR_ACCEL
// Fills the values array (length 6) with self test limits from the channel's chunk data.
// Differences are (enabled - disabled). If a given difference is between the minimum and maximum, then the channel's
// self test passes; otherwise it fails.
// limits[0] is the minimum X difference
// limits[1] is the maximum X difference
// limits[2] is the minimum Y difference
// limits[3] is the maximum Y difference
// limits[4] is the minimum Z difference
// limits[5] is the maximum Z difference
ASPHODEL_API int asphodel_get_accel_self_test_limits(AsphodelChannelInfo_t *channel_info, float *limits)
{
	int chunk_index = 0; // holds self test limits
	const uint8_t *chunk;
	size_t i;

	if (channel_info->channel_type != CHANNEL_TYPE_SLOW_ACCEL && channel_info->channel_type != CHANNEL_TYPE_PACKED_ACCEL && channel_info->channel_type != CHANNEL_TYPE_LINEAR_ACCEL)
	{
		return ASPHODEL_BAD_CHANNEL_TYPE;
	}

	if (channel_info->chunk_count <= chunk_index)
	{
		return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
	}

	if (channel_info->chunk_lengths[chunk_index] != 6 * 4)
	{
		// chunk can't be interpreted as float[6] array
		return ASPHODEL_CHANNEL_FORMAT_UNSUPPORTED;
	}

	chunk = channel_info->chunks[chunk_index];
	for (i = 0; i < 6; i++)
	{
		limits[i] = read_float_value(&chunk[i * 4]);
	}

	return 0;
}

// Supported types: CHANNEL_TYPE_SLOW_ACCEL, CHANNEL_TYPE_PACKED_ACCEL, CHANNEL_TYPE_LINEAR_ACCEL
// Enables or disables the accel channel's self test functionality. Enable is a boolean (0/1).
// Wraps a call to asphodel_channel_specific().
ASPHODEL_API int asphodel_enable_accel_self_test(AsphodelDevice_t *device, int channel_index, int enable, AsphodelCommandCallback_t callback, void * closure)
{
	int ret;
	BasicTransferClosure_t *c = (BasicTransferClosure_t*)malloc(sizeof(BasicTransferClosure_t) + 1 * sizeof(void *));
	uint8_t params[2] = {ACCEL_ENABLE_SELF_TEST, (enable != 0)};

	if (!c)
	{
		return ASPHODEL_NO_MEM;
	}

	c->callback = callback;
	c->closure = closure;
	c->data[0] = 0; // will be used as reply_length

	ret = asphodel_channel_specific(device, channel_index, params, sizeof(params), NULL, (uint8_t*)&c->data[0], channel_specific_empty_reply_cb, c);

	if (ret != 0)
	{
		free(c);
	}

	return ret;
}

ASPHODEL_API int asphodel_enable_accel_self_test_blocking(AsphodelDevice_t *device, int channel_index, int enable)
{
	MAKE_BLOCKING(device, asphodel_enable_accel_self_test(device, channel_index, enable, callback, closure));
}

// Supported types: CHANNEL_TYPE_SLOW_ACCEL, CHANNEL_TYPE_PACKED_ACCEL, CHANNEL_TYPE_LINEAR_ACCEL
// Checks the self test functionality of an accel channel.
// The two inputs (arrays of length 3) should be measured with asphodel_enable_accel_self_test() disabled and enabled.
// NOTE: all measurements should be made in the channel's unit type, not a derived unit.
// passed will be set to 1 if the self test values are acceptable. Otherwise, it will be set to 0.
ASPHODEL_API int asphodel_check_accel_self_test(AsphodelChannelInfo_t *channel_info, double *disabled, double *enabled, int *passed)
{
	int ret;
	float limits[6];
	int i;

	if (channel_info->channel_type != CHANNEL_TYPE_SLOW_ACCEL && channel_info->channel_type != CHANNEL_TYPE_PACKED_ACCEL && channel_info->channel_type != CHANNEL_TYPE_LINEAR_ACCEL)
	{
		return ASPHODEL_BAD_CHANNEL_TYPE;
	}

	ret = asphodel_get_accel_self_test_limits(channel_info, limits);
	if (ret != 0)
	{
		return ret;
	}

	for (i = 0; i < 3; i++)
	{
		double diff = enabled[i] - disabled[i];
		double minimum = limits[i * 2];
		double maximum = limits[i * 2 + 1];
		if (diff < minimum || diff > maximum)
		{
			*passed = 0;
			return 0; // any failure is a failure
		}
	}

	*passed = 1;
	return 0;
}
