/* usbtenki: A library for accessing USBTenki sensors.
 * Copyright (C) 2018-2024 Dracal Technologies inc.
 * Copyright (C) 2007-2018  Raphael Assenat
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
#include <errno.h>

#include "usbtenki.h"
#include "usbtenki_cmds.h"
#include "chip_conv.h"
#include "libusb.h"

#define USBTENKI_DEV_TO_USB_DEVICE(d) ((struct usb_device*)(d))

#define MAX_USB_ATTEMPTS	5

#ifdef _WIN32
#	include <windows.h>
#	define usleep(t) Sleep(t/1000)
#else
#	include <unistd.h>
#endif

int g_usbtenki_verbose = 0;
int g_usbtenki_show_all_hidden_channels = 0;
int g_usbtenki_num_attempts = 3;
int g_has_at_least_one_unseen_dxc320 = 0;

libusb_context *g_libusbctx;

#define VIDPID_NOT_HANDLED		0
#define VIDPID_HANDLED			1 // Recognized by VID/PID, name irrelevant.
#define VIDPID_HANDLED_STRING	2 // Shared VID/PID, needs name test


static const char* const ERRORS[] = {
    [USBTENKI_SUCCESS] = "",
	[USBTENKI_ERR_UNDEF] = "Internal error",
	[USBTENKI_ERR_ASSERT] = "Assertion fault",
	[USBTENKI_ERR_TIMEOUT] = "Timeout",
	[USBTENKI_ERR_BROKEN_PIPE] = "Broken pipe",
	[USBTENKI_ERR_ALLOC] = "Memory allocation error",
	[USBTENKI_ERR_VALUE_OOR] = "Value out of range",
	[USBTENKI_ERR_USB_LIBERROR] = "USB error",
	[USBTENKI_ERR_USB_OPEN] = "Failed to open USB port",
	[USBTENKI_ERR_USB_GET_DESCRIPTOR] = "Failed to get USB descriptor",
	[USBTENKI_ERR_USB_GETCONFIG] = "Failed to get USB configuration",
	[USBTENKI_ERR_USB_SETCONFIG] = "Failed to set USB configuration",
	[USBTENKI_ERR_USB_CLAIM_INTERFACE] = "Failed to clair USB interface",
	[USBTENKI_ERR_USB_CTRL_MESSAGE] = "USB Control message error",
	[USBTENKI_ERR_USB_CMD_MISMATCH] = "USB command mismatch",
	[USBTENKI_ERR_USB_CHECKSUM] = "USB transfer corrupted",
	[USBTENKI_ERR_USB_UNDERFLOW] = "USB data underflow",
	[USBTENKI_ERR_USB_OVERFLOW] = "USB data overflow",
	[USBTENKI_ERR_DEVICE_OPEN] = "Failed to open device",
	[USBTENKI_ERR_DEVICE_NOT_FOUND] = "Device not found",
	[USBTENKI_ERR_DEVICE_CHANNEL_INVALID] = "Invalid channel",
	[USBTENKI_ERR_DEVICE_CHANNEL_MISMATCH] = "Channel mismatch",
	[USBTENKI_ERR_DEVICE_USERCALPOINT_NOT_SUPPORTED] = "Channel does not support calibration points",
	[USBTENKI_ERR_DEVICE_USERCALCURVE_NOT_SUPPORTED] = "Channel does not support calibration polynomial",
	[USBTENKI_ERR_DATA_INVALID] = "Invalid data",
	[USBTENKI_ERR_DATA_RAW_CONVERT] = "Failed to convert raw data",
	[USBTENKI_ERR_DATA_SET_ZERO] = "Failed to set zero value",
	[USBTENKI_ERR_DATA_AUTOCAL_SET] = "Failed to set autocalibration",
	[USBTENKI_ERR_DATA_AUTOCAL_GET] = "Failed to get autocalibration",
	[USBTENKI_ERR_DATA_AUTOCAL_LOCK] = "Failed to lock autocalibration",
	[USBTENKI_ERR_DATA_AUTOCAL_UNLOCK] = "Failed to unlock autocalibration",
	[USBTENKI_ERR_DATA_CALPOINT_INVALID] = "Invalid calibration point ID",
	[USBTENKI_ERR_DATA_CALPOINT_GET] = "Failed to get calibration points",
	[USBTENKI_ERR_DATA_CALPOINT_SET] = "Failed to set calibration chunk",
	[USBTENKI_ERR_DATA_CALPOINT_SAVE] = "Failed to save calibration point",
	[USBTENKI_ERR_DATA_CALCURVE_INVALID] = "Invalid calibration polynomial coefficient ID",
	[USBTENKI_ERR_DATA_CALCURVE_GET] = "Failed to get calibration polynomial coefficient",
	[USBTENKI_ERR_DATA_CALCURVE_SET] = "Failed to set calibration polynomial coefficient chunk",
	[USBTENKI_ERR_DATA_CALCURVE_SAVE] = "Failed to save calibration polynomial coefficient"	
};


static USBTenki_Error error = USBTENKI_SUCCESS;

int usbtenki_errno(void) {
	return (int)error;
}

void usbtenki_perror(const char* str) {
	fprintf(stderr, "%s: %s\n", str, usbtenki_strerror((int)error));
}

char * usbtenki_strerror(int errnum) {
	return (char*)ERRORS[errnum];
}


char isHandledVidPid(unsigned short vid, unsigned short pid)
{
	// The raphnet.net ID
	if ((vid == 0x1781) && (pid == 0x0a98)) {
		return VIDPID_HANDLED_STRING;
	}
	// The Dracal technoloiges inc. ID range
	if (vid == 0x289b) {
		if ((pid >= 0x0500) && (pid <= 0x6FF)) {
			return VIDPID_HANDLED;
		}
	}
	return VIDPID_NOT_HANDLED;
}

char isNameHandled(const char *str)
{
	if (strcmp(str, "USBTenki")==0)
		return 1;
	if (strcmp(str, "USB_Temp")==0)
		return 1;
	return 0;
}

char matchSerialNumber(const char *str)
{
	char *m = getenv("USBTENKI_SHOW_ONLY");
	if (!m) {
		return 1;
	}
	return strcmp(m, str) == 0;
}

int usbtenki_init(void)
{
	if (getenv("USBTENKI_VERBOSE")) {
		g_usbtenki_verbose = 1;
	}

	if (getenv("USBTENKI_SHOW_ALL_HIDDEN_CHANNELS")) {
		g_usbtenki_show_all_hidden_channels = 1;
	}

	return libusb_init(&g_libusbctx);
}

void usbtenki_shutdown(void)
{
#ifdef USE_OLD_LIBUSB
#else
	libusb_exit(g_libusbctx);
#endif
}

static unsigned char xor_buf(unsigned char *buf, int len)
{
	unsigned char x=0;
	while (len--) {
		x ^= *buf;
		buf++;
	}
	return x;
}


int usbtenki_command(USBTenki_dev_handle hdl, unsigned char cmd,
										unsigned int id, unsigned char *dst, int dst_max_size)
{
	unsigned char buffer[64];
	unsigned char xor;
	int n, i;
	int datlen = 0;
	static int first = 1, trace = 0;
	int attempts;

	if (first) {
		if (getenv("USBTENKI_TRACE")) {
			trace = 1;
		}
		first = 0;
	}

	if (!hdl) {
		return -10;
	}

	for (attempts = 0; attempts < MAX_USB_ATTEMPTS; attempts++)
	{
		n =	libusb_control_transfer(hdl,
			LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_IN, /* requesttype */
			cmd, 	/* request*/
			id & 0xffff,		/* value */
			id >> 16,			/* index */
			buffer, sizeof(buffer), 5000);

		if (trace) {
			printf("req: 0x%02x, val: 0x%02x, idx: 0x%02x <> %d: ",
				cmd, id & 0xffff, id >> 16, n);
			if (n>0) {
				for (i=0; i<n; i++) {
					printf("%02x ", buffer[i]);
				}
			}
			printf("\n");
		}

		if (n<0) {
			if (g_usbtenki_verbose) {
				usbtenki_perror(libusb_strerror(n));
			}
			switch (n) {
				case -ETIMEDOUT:
					error = USBTENKI_ERR_TIMEOUT;
					break;
				case -EPIPE:
					error = USBTENKI_ERR_BROKEN_PIPE;
					break;
				default:
					error = USBTENKI_ERR_USB_LIBERROR;
			}
#ifndef _WIN32
			usleep(1000);
#endif
			datlen = -1;
			continue;
		}

		/* Validate size first */
		if (n<2) {
			error = USBTENKI_ERR_USB_UNDERFLOW;
			if (g_usbtenki_verbose) {
				usbtenki_perror("Not enough data received");
			}
			datlen = -4;
			continue;
		}

		/* dont count command and xor */
		datlen = n - 2;

		if (datlen > dst_max_size) {
			error = USBTENKI_ERR_USB_OVERFLOW;
			if (g_usbtenki_verbose) {
				usbtenki_perror("Too much data received");
			}
			datlen = -3;
			continue;
		}

		/* Check if reply is for this command */
		if (buffer[0] != cmd) {
			/* Status busy? */
			if (buffer[0] == USBTENKI_STATUS) {
				if (buffer[1] == USBTENKI_STATUS_BUSY) {
					usleep(50000);
					continue; // retry
				}
			}
			error = USBTENKI_ERR_USB_CMD_MISMATCH;
			if (g_usbtenki_verbose) {
				usbtenki_perror("Unexpected data");
			}
			return -5;
		}

		/* Check xor */
		xor = xor_buf(buffer, n);
		if (xor) {
			// printf("XOR\n");
			if (g_usbtenki_verbose) {
				usbtenki_perror("Communication error");
			}
			datlen = -6;
			continue; // retry
		}

		break;

	}

	if (datlen > 0) {
		memcpy(dst, buffer+1, datlen);
		error = USBTENKI_SUCCESS;
	}

	return datlen;
}

void usbtenki_closeDevice(USBTenki_dev_handle hdl)
{
	libusb_device_handle *dev_handle = (libusb_device_handle*)hdl;

	if (dev_handle) {
		libusb_close(dev_handle);
	}
}

USBTenki_dev_handle usbtenki_openDevice(struct USBTenki_info *info)
{
	if (!info || !info->usb_device) {
		return NULL;
	}

	libusb_device_handle *dev_handle = NULL;

	int res = libusb_open((libusb_device *)info->usb_device, &dev_handle);
	if (res != 0) {
		error = USBTENKI_ERR_USB_OPEN;
		if (g_usbtenki_verbose) {
			usbtenki_perror(libusb_strerror(res));
		}
		if (res == LIBUSB_ERROR_NO_DEVICE) {
			libusb_unref_device((libusb_device *)info->usb_device);
			info->usb_device = NULL;
		}
		return NULL;
	}

	return (USBTenki_dev_handle)dev_handle;
}

USBTenki_dev_handle usbtenki_openBySerial(struct USBTenki_info *info)
{
	if (!info) {
		return NULL;
	}

	libusb_device **all_devices;
	libusb_device *dev = NULL;
	struct libusb_device_descriptor dev_desc;
	libusb_device_handle *dev_handle = NULL;
	unsigned char dev_serial[256];
	int found = 0;

	int n_devices = libusb_get_device_list(g_libusbctx, &all_devices);

	if (n_devices <= 0) {
		return NULL;
	}

	for (int i = 0; i < n_devices; i++) {
		dev = all_devices[i];
		libusb_get_device_descriptor(dev, &dev_desc);
		if (!isHandledVidPid(dev_desc.idVendor, dev_desc.idProduct)) {
			continue;
		}
		int res = libusb_open(dev, &dev_handle);
		if (res != 0) {
			continue;
		}
		for (int attempts = 0; attempts < MAX_USB_ATTEMPTS; attempts++) {
			res = libusb_get_string_descriptor_ascii(dev_handle, dev_desc.iSerialNumber, dev_serial, sizeof(info->str_serial));
			if (res > 0) {
				break;
			}
		}

		if (res <= 0 || (strcmp((char *)dev_serial, (char *)info->str_serial) != 0)) {
			libusb_close(dev_handle);
			continue;
		}
		else {
			found = 1;
			if (!info->usb_device) {
				libusb_ref_device(dev);
				info->usb_device = (USBTenki_device)dev;
				info->bus = libusb_get_bus_number(dev);
				info->address = libusb_get_device_address(dev);
				info->port = libusb_get_port_number(dev);
				libusb_get_port_numbers(dev, info->path, sizeof(info->path));
			}
			break;
		}
	}

	libusb_free_device_list(all_devices, 1);

	return found ? dev_handle : NULL;
}


struct USBTenki_list_ctx *usbtenki_initListCtx(void)
{
	struct USBTenki_list_ctx *ctx = calloc(1, sizeof(struct USBTenki_list_ctx));
	if (!ctx) {
		return ctx;
	}

	libusb_device **all_devices;
	libusb_device *dev;
	struct libusb_device_descriptor devdesc;
	libusb_device_handle *dev_handle;
	int res;

	error = USBTENKI_SUCCESS;

	const int n_devices = libusb_get_device_list(g_libusbctx, &all_devices);

	if (n_devices < 0) {
		error = USBTENKI_ERR_USB_LIBERROR;
		if (g_usbtenki_verbose) {
			usbtenki_perror(libusb_strerror(n_devices));
		}
		free(ctx);
		return NULL;
	}

	if (n_devices == 0) {
		libusb_free_device_list(all_devices, 0);
		free(ctx);
		return NULL;
	}

	// Scan devices to allocate memory for handled devices info
	// TODO: keep references in list_t with capacity = n_devices

	int n_handled = 0;
	for (int i = 0; i < n_devices; i++) {
		dev = all_devices[i];
		res = libusb_get_device_descriptor(dev, &devdesc);
		if (res < 0) {
			error = USBTENKI_ERR_USB_GET_DESCRIPTOR;
			if (g_usbtenki_verbose) {
				usbtenki_perror(libusb_strerror(res));
			}
			continue;
		}
		if (isHandledVidPid(devdesc.idVendor, devdesc.idProduct)) {
			n_handled++;
		}
	}

	if (n_handled == 0) {
		libusb_free_device_list(all_devices, 0);
		free(ctx);
		return NULL;
	}

	ctx->devices = calloc(n_handled, sizeof(struct USBTenki_info));

	if (ctx->devices) {
		int cur_device_index = 0;

		// Populate handled info list
		struct USBTenki_info *info;
		for (int i = 0; i < n_devices; i++) {

			if (cur_device_index >= n_handled) {
				continue; // Should not happen
			}

			dev = all_devices[i];
			res = libusb_get_device_descriptor(dev, &devdesc);
			if (res < 0) {
				continue;
			}

			if (isHandledVidPid(devdesc.idVendor, devdesc.idProduct)) {
				res = libusb_open(dev, &dev_handle);
				if (res != 0) {
					error = USBTENKI_ERR_USB_OPEN;
					if (g_usbtenki_verbose) {
						usbtenki_perror(libusb_strerror(res));
					}
					if (devdesc.idVendor == 0x289b && devdesc.idProduct == 0x0510) {
						g_has_at_least_one_unseen_dxc320 = 1;
					}
					continue;
				}

				info = &ctx->devices[cur_device_index];

				for (int attempts = 0; attempts < MAX_USB_ATTEMPTS; attempts++) {
					res = libusb_get_string_descriptor_ascii(dev_handle, devdesc.iProduct, (unsigned char *)info->str_prodname, sizeof(info->str_prodname));
					if (res > 0) {
						break;
					}
				}

				if (res <= 0) {
					fprintf(stderr, "Error reading product name (%d): %d\n", devdesc.iProduct, res);
					usbtenki_perror(libusb_strerror(res));
					if (devdesc.idVendor == 0x289b && devdesc.idProduct == 0x0510) {
						g_has_at_least_one_unseen_dxc320 = 1;
					}
					libusb_close(dev_handle);
					continue;
				}

				for (int attempts = 0; attempts < MAX_USB_ATTEMPTS; attempts++) {
					res = libusb_get_string_descriptor_ascii(dev_handle, devdesc.iSerialNumber, (unsigned char *)info->str_serial, sizeof(info->str_serial));
					if (res > 0){
						break;
					}
				}

				if (res <= 0) {
					fprintf(stderr, "Error reading serial (%d): %d\n", devdesc.iSerialNumber, res);
					usbtenki_perror(libusb_strerror(res));
					if (devdesc.idVendor == 0x289b && devdesc.idProduct == 0x0510) {
						g_has_at_least_one_unseen_dxc320 = 1;
					}
					libusb_close(dev_handle);
					continue;
				}

				cur_device_index++;
				libusb_ref_device(dev);
				info->usb_device = dev;

				info->bus = libusb_get_bus_number(dev);
				info->address = libusb_get_device_address(dev);
				info->port = libusb_get_port_number(dev);
				libusb_get_port_numbers(dev, info->path, sizeof(info->path));

				info->major = devdesc.bcdDevice >> 8;
				info->minor = ((devdesc.bcdDevice & 0xf0)>>4) * 10 + (devdesc.bcdDevice & 0xf);

				libusb_close(dev_handle);
			}
		}

		ctx->n_devices = cur_device_index;

		if (cur_device_index == 0) {
			free(ctx->devices);
			ctx->devices = NULL;
		} else if (cur_device_index < n_handled) {
			ctx->devices = realloc(ctx->devices, cur_device_index * sizeof(struct USBTenki_info));
		}
	}

	libusb_free_device_list(all_devices, 0);

	if (!ctx->devices) {
		free(ctx);
		ctx = NULL;
	}

	return ctx;
}

void usbtenki_freeListCtx(struct USBTenki_list_ctx *ctx)
{
	if (ctx) {
		if (ctx->devices) {
			free(ctx->devices);
		}
		free(ctx);
	}
}

int usbtenki_getUserCalibrationChannels(USBTenki_dev_handle hdl, uint16_t *channels)
{
	unsigned char channel_flags[2];

	int res = usbtenki_command(hdl, USBTENKI_GET_USER_CALIBRATION_CHANNELS, 0, channel_flags, sizeof(channel_flags));
	if (res <= 0) {
		error = USBTENKI_ERR_DEVICE_USERCALPOINT_NOT_SUPPORTED;
		return 0;
	}
	*channels = channel_flags[0] << 8 | channel_flags[1];

	error = USBTENKI_SUCCESS;
	return 1;
}


int usbtenki_getUserCalibrationPoints(USBTenki_dev_handle hdl, int channel_id, Point *points)
{
	unsigned char caldata[CAL_POINTS_SIZE];
	unsigned char *p = caldata;

	// format:
	// [0-7]  : First calibration point (sensed value)
	// [8-15] : First calibration point (real value)
	// ...

	int caldata_len = usbtenki_command(hdl, USBTENKI_GET_USER_CALIBRATION_POINTS, channel_id, caldata, sizeof(caldata));
	if (caldata_len <= 0) {
		error = USBTENKI_ERR_DEVICE_USERCALPOINT_NOT_SUPPORTED;
		return 0;
	}

	if (caldata_len == 24) {
		// Convert 6*float input to required 6*doubles
		float *calpt_src;
		double *calpt_dst;
		for (int i = 2 * CAL_POINTS_COUNT; i > 0; i--) {
			calpt_src = ((float *)caldata) + i - 1;
			calpt_dst = ((double *)caldata) + i - 1;
			*calpt_dst = (double)(*calpt_src);
		}
		caldata_len *= 2;
	}

	int n = caldata_len / sizeof(Point);
	
	if (n > CAL_POINTS_COUNT) {
		n = CAL_POINTS_COUNT;
	}

	// Retrieve n points
	for (int i = 0; i < n; i++) {
		points[i].x = *((double *)(p));
		p += sizeof(double);
		points[i].y = *((double *)(p));
		p += sizeof(double);
	}

	// Invalidate any remaining points in the destination array
	for (int i = n; i < CAL_POINTS_COUNT; i++) {
		points[i].x = NAN;
		points[i].y = NAN;
	}

	error = USBTENKI_SUCCESS;
	return n;
}

int usbtenki_setUserCalibrationPoint(USBTenki_dev_handle hdl, int channel_id, int point_id, const Point *point)
{
	if (point_id < 0 || point_id > CAL_POINTS_COUNT) {
		error = USBTENKI_ERR_DATA_CALPOINT_INVALID;
		return -1;
	}
	uint8_t repBuf[32];
	uint8_t buf[2 * sizeof(double)];
	int i, res;

	memcpy(buf, &point->x, sizeof(double));
	memcpy(buf + sizeof(double), &point->y, sizeof(double));

	for (i=0; i<sizeof(buf); i+=2) {
		// wIndex is the index within the 16 bytes of raw data
		// wValue contains two bytes of raw data
		res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_POINT, (i << 16) | buf[i] << 8 | buf[i+1], repBuf, sizeof(repBuf));
		if (res!=0) {
			error = USBTENKI_ERR_DATA_CALPOINT_SET;
			break;
		}
	}

	if (res == 0) {
		// Store point in EEPROM
		//
		// wIndex set to 0xffff to indicate we must store the value transmitted above in eeprom
		// wValue set to contain the channel and point id
		//
		res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_POINT, 0xffff0000 | channel_id << 8 | point_id, repBuf, sizeof(repBuf));
	}

	if (res != 0) {
		// Cannot set calibration point this way.
		// Retry using calibration curve coefficients

		Point points[CAL_POINTS_COUNT];
		int n_pts = usbtenki_getUserCalibrationPoints(hdl, channel_id, points);
		if (n_pts == 0) {
			return res;
		}

		points[point_id].x = point->x;
		points[point_id].y = point->y;

		return usbtenki_setUserCalibrationCoeffs(hdl, channel_id, points);
	}

	error = USBTENKI_SUCCESS;
	return 0;
}


int usbtenki_getUserCalibrationCoeffs(USBTenki_dev_handle hdl, int channel_id, Point * points, Coeff *coeffs) {

	unsigned char caldata[CALCURVE_COEFFS_COUNT * sizeof(Coeff)];
	unsigned char *p = caldata;
	
	// [0-7]   : First calibration coefficient  (Coeff coeffs[0])
	// [8-15]  : Second calibration coefficient (Coeff coeffs[1])
	// [16-23] : third calibration coefficient  (Coeff coeffs[2])

	int caldata_len = usbtenki_command(hdl, USBTENKI_GET_USER_CALIBRATION_COEFFS, channel_id, caldata, sizeof(caldata));
	if (caldata_len <= 0) {
		// Does not support polynomial storage
		error = USBTENKI_ERR_DEVICE_USERCALCURVE_NOT_SUPPORTED;
		return 0;
	}

	int n = caldata_len / sizeof(Coeff);
	
	if (n > CALCURVE_COEFFS_COUNT) {
		n = CALCURVE_COEFFS_COUNT;
	}

	// Retrieve n coefficients
	for (int i = 0; i < CALCURVE_COEFFS_COUNT; i++) {
		coeffs[i] = *((Coeff *)p);
		p += sizeof(Coeff);
	}

	error = USBTENKI_SUCCESS;
	return n;
}


int usbtenki_setUserCalibrationCoeffs(USBTenki_dev_handle hdl, int channel_id, const Point *points)
{
	uint8_t repBuf[50];
	uint8_t buf[sizeof(Point)];
	int i, res;

	Coeff coeffs[CALCURVE_COEFFS_COUNT] = {0, 1, 0};

	for (int point_id=0; point_id < CAL_POINTS_COUNT; point_id++) {
		if (POINT_IS_VALID(points[point_id])) {
			calcurve_coeffs(points, coeffs);
			break;
		}
	}

	for (int point_id=0; point_id<CAL_POINTS_COUNT; point_id++) {

		memcpy(buf, &points[point_id].x, sizeof(double));
		memcpy(buf + sizeof(double), &points[point_id].y, sizeof(double));

		// Side-effect: temporarily invalidates existing polynomial coefficients
		for (i=0; i<sizeof(Point); i+=2) {
			// wIndex is the index within the 16 bytes of raw data
			// wValue contains two bytes of raw data
			res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_COEFFS, (i << 16) | buf[i] << 8 | buf[i+1], repBuf, sizeof(repBuf));
			if (res!=0) {
				error = USBTENKI_ERR_DATA_CALPOINT_SET;
				return -1;
			}
		}
		// Store point in EEPROM
		//
		// wIndex set to 0xffff to indicate we must store the value transmitted above in eeprom
		// wValue set to contain the channel id and point id
		//
		res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_COEFFS, 0xffff0000 | channel_id << 8 | point_id, repBuf, sizeof(repBuf));
		if (res!=0) {
			error = USBTENKI_ERR_DATA_CALPOINT_SAVE;
			return -1;
		}
	}

	// Store corresponding new calibration polynomial coefficients
	for (int coeff_id=0; coeff_id<CALCURVE_COEFFS_COUNT; coeff_id++) {

		memcpy(buf, &coeffs[coeff_id], sizeof(Coeff));

		for (i=0; i<sizeof(Coeff); i+=2) {
			// wIndex is the index within the 8 bytes of raw data
			// wValue contains two bytes of raw data
			res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_COEFFS, (i << 16) | buf[i] << 8 | buf[i+1], repBuf, sizeof(repBuf));
			if (res!=0) {
				error = USBTENKI_ERR_DATA_CALCURVE_SET;
				return -1;
			}
		}

		// Store coefficient in EEPROM
		//
		// wIndex set to 0xffff to indicate we must store the value transmitted above in eeprom
		// wValue set to contain the channel id and (point id | 0x80)
		//
		res = usbtenki_command(hdl, USBTENKI_SET_USER_CALIBRATION_COEFFS, 0xffff0000 | channel_id << 8 | coeff_id | 0x80, repBuf, sizeof(repBuf));
		if (res!=0) {
			error = USBTENKI_ERR_DATA_CALCURVE_SAVE;
			return -1;
		}
	}

	error = USBTENKI_SUCCESS;
	return 0;
}

int usbtenki_getCalibration(USBTenki_dev_handle hdl, int id, unsigned char *dst, int dst_max_size)
{
	int res;

	res = usbtenki_command(hdl, USBTENKI_GET_CALIBRATION, id, dst, dst_max_size);

	return res;
}

int usbtenki_setCalibration(USBTenki_dev_handle hdl, int channel_id, uint8_t *buf, int len)
{
	int res, i;
	uint8_t repBuf[32];

	for (i = 0; i < len; i += 2) {
		// wIndex is the index within the 16 bytes of raw data
		// wValue contains two bytes of raw data
		res = usbtenki_command(hdl, USBTENKI_SET_CALIBRATION, (i << 16) | buf[i] << 8 | buf[i+1], repBuf, sizeof(repBuf));
		if (res!=0) {
			error = USBTENKI_ERR_UNDEF;
			return -1;
		}
	}
	res = usbtenki_command(hdl, USBTENKI_SET_CALIBRATION, 0xffff0000 | channel_id, repBuf, sizeof(repBuf));
	if (res!=0) {
		error = USBTENKI_ERR_UNDEF;
		return -1;
	}

	return len;
}

int usbtenki_getRaw(USBTenki_dev_handle hdl, int id, unsigned char *dst, int dst_max_size)
{
	return usbtenki_command(hdl, USBTENKI_GET_RAW, id, dst, dst_max_size);
}

int usbtenki_getNumChannels(USBTenki_dev_handle hdl)
{
	unsigned char dst[8];
	int res;

	res = usbtenki_command(hdl, USBTENKI_GET_NUM_CHANNELS, 0, dst, sizeof(dst));
	if (res<0)
		return res;
	if (res<1) /* Illegal for this command */
		return res;
	res = (int8_t)dst[0];
	return res;
}

int usbtenki_getNumVisibleChannels(USBTenki_dev_handle hdl, int max_channels)
{
	unsigned char channel_flags[2];
	int res = usbtenki_command(hdl, USBTENKI_GET_VISIBLE_CHANNELS, 0, channel_flags, sizeof(channel_flags));

	// printf("visible: %04x\n", *((uint16_t *)channel_flags));

	if (res <= 0) {
		return max_channels;
	}

	int channels = channel_flags[0] << 8 | channel_flags[1];

	int n_channels = 0;
	for (int i = 0; i < 16; i++) {
		if (channels & (1 << i)) {
			n_channels++;
		}
		else {
			break;
		}
	}

	return n_channels;
}

int usbtenki_getChipID(USBTenki_dev_handle hdl, int id)
{
	unsigned char dst[8];
	int res = usbtenki_command(hdl, USBTENKI_GET_CHIP_ID, id, dst, sizeof(dst));
	if (res<0)
		return res;
	if (res != 1 && res != 3) /* Illegal for this command */
		return res;
	return dst[0];
}

int usbtenki_readChannel(USBTenki_dev_handle hdl, struct USBTenki_channel *chn, unsigned long flags)
{
	return usbtenki_readChannelList(hdl, &chn->channel_id, 1, chn, 1, 1, flags);
}

/**
 * \param hdl Handle
 * \param channel_ids Array of channel IDs
 * \param num The number of channel IDs
 * \param dst The destination array
 * \param dst_total The total number of channels in 'dst'
 *
 * dst must have been setup by usbtenki_listChannels() first!
 *
 * This function only reads real channels (no virtuals)
 */
int usbtenki_readChannelList(USBTenki_dev_handle hdl, const int channel_ids[], int num, struct USBTenki_channel *dst, int dst_total, int num_attempts, unsigned long flags)
{
	int i, j;
	int n;
	unsigned char caldata[64];
	int caldata_len = 0;
	int caldata_chip = -1;
	int is_dxc320 = 0;

	for (i=0; i<num; i++)
	{
		// skip virtual channels
		if (channel_ids[i]>=USBTENKI_VIRTUAL_START)
			continue;

		// Look for the destination
		for (j=0; j<dst_total; j++) {
			if (channel_ids[i] == dst[j].channel_id) {
				break;
			}
		}
		if (j==dst_total) {
			error = USBTENKI_ERR_DEVICE_CHANNEL_INVALID;
			if (g_usbtenki_verbose) {
				char msg[32];
				sprintf(msg, "Channel %d", channel_ids[i]);
				usbtenki_perror(msg);
			}
			continue;
		}

		// FIXME: g_verbose vs USBTENKI_FLAG_VERBOSE
		for (n=0; n<num_attempts; n++) {
			if (flags & USBTENKI_FLAG_VERBOSE) {
				printf("usbtenki_getRaw %d/%d chn %d attempt %d\n", i+1, num, dst[j].channel_id, n+1);
				usleep(100000);
			}
			dst[j].raw_len = usbtenki_getRaw(hdl, dst[j].channel_id, dst[j].raw_data, sizeof(dst[j].raw_data));
			if (dst[j].raw_len<0) {
				usleep(200);
				continue;
			}
			break;
		}

		/* all attempts failed? */
		// FIXME: if (!error) {error = USBTENKI_ERR_UNDEF} to avoid overwriting
		if (n==num_attempts) {
			if (!error) {
				error = USBTENKI_ERR_UNDEF;
			}
			return -1;
		}

		// Fetch user calibration data
		dst[j].calpoints_count = usbtenki_getUserCalibrationPoints(hdl, channel_ids[i], dst[j].calpoints);

		// handle chips with calibration data

		// Detection of the new DXC320.
		if(dst_total > 0 && dst[0].chip_id == USBTENKI_CHIP_CO2_DXC300_COMPENSATED) {
			is_dxc320 = 1;
		}

		if (is_dxc320 == 1) {
			// All channels have their own calibration with only one call. No more sharing between channels.
			if (dst[j].chip_id == USBTENKI_CHIPX_MS5611_P) {
				for (n=0; n<num_attempts; n++) {
					caldata_len = usbtenki_getCalibration(hdl, i, caldata, sizeof(caldata));
					if (caldata_len<0) {
						usleep(200);
						continue;
					}
					break;
				}

				/* all attempts failed? */
				if (n==num_attempts) {
					if (!error) {
						error = USBTENKI_ERR_UNDEF;
					}
					return -1;
				}
			}

			if ((dst[j].chip_id == USBTENKI_CHIP_CO2_DXC300_P_COMP) ||
				(dst[j].chip_id == USBTENKI_CHIP_CO2_DXC300_T_COMP) ||
				(dst[j].chip_id == USBTENKI_CHIP_CO2_DXC300_RH_COMP)
			) {
				for (n=0; n<num_attempts; n++) {
					caldata_len = usbtenki_getCalibration(hdl, i, caldata, sizeof(caldata));
					if (caldata_len<0) {
						usleep(200);
						continue;
					}
					caldata_chip = dst[j].chip_id;
					break;
				}

				/* all attempts failed? */
				if (n==num_attempts) {
					if (!error) {
						error = USBTENKI_ERR_UNDEF;
					}
					return -1;
				}
			}
		} else { // Avoid false positive of old logic on new DXC320.
			if (dst[j].chip_id == USBTENKI_CHIP_PT100_RTD) {
	//			printf("Fetching PT100 calibration data...\n");
				for (n=0; n<num_attempts; n++) {
					caldata_len = usbtenki_getCalibration(hdl, 0, caldata, sizeof(caldata));
					if (caldata_len<0) {
						usleep(200);
						continue;
					}
					caldata_chip = USBTENKI_CHIP_PT100_RTD;
					break;
				}

				/* all attempts failed? */
				if (n==num_attempts) {
					if (!error) {
						error = USBTENKI_ERR_UNDEF;
					}
					return -1;
				}
			}

			if (	(caldata_chip != USBTENKI_CHIP_MS5611_P) &&
					(	(dst[j].chip_id == USBTENKI_CHIP_MS5611_P) ||
						(dst[j].chip_id == USBTENKI_CHIP_MS5611_T) ||
						(dst[j].chip_id == USBTENKI_CHIPX_MS5611_P) ||
						(dst[j].chip_id == USBTENKI_CHIPX_MS5611_T)
					)
				)
			{
				int offset = 0;

				// A PT100 RTD interface with a MS5611 pressure sensor has
				// its MS5611 calibration after the PT100 calibration data...
				if ((
						(dst[j].chip_id == USBTENKI_CHIP_MS5611_P) ||
						(dst[j].chip_id == USBTENKI_CHIPX_MS5611_P)
					) && dst[j].channel_id == 1
				) {
					offset = 1;
				}
				if ((
						(dst[j].chip_id == USBTENKI_CHIP_MS5611_T) ||
						(dst[j].chip_id == USBTENKI_CHIPX_MS5611_T)
					) && dst[j].channel_id == 2
				) {
					offset = 1;
				}

				//printf("Fetching MS5611 calibration data (offset: %d)...\n", offset);
				for (n=0; n<num_attempts; n++) {
					caldata_len = usbtenki_getCalibration(hdl, offset + 0, caldata, sizeof(caldata)-offset);
					if (caldata_len<0) {
						usleep(200);
						continue;
					}
					break;
				}
				/* all attempts failed? */
				if (n==num_attempts) {
					if (!error) {
						error = USBTENKI_ERR_UNDEF;
					}
					return -1;
				}

				for (n=0; n<num_attempts; n++) {
					int l;
					l = usbtenki_getCalibration(hdl, offset + 1, caldata + caldata_len, sizeof(caldata)-offset);
					if (l<0) {
						usleep(200);
						continue;
					}
					caldata_len += l;
					break;
				}
				/* all attempts failed? */
				if (n==num_attempts) {
					if (!error) {
						error = USBTENKI_ERR_UNDEF;
					}
					return -1;
				}

				caldata_chip = USBTENKI_CHIP_MS5611_P;
			}
		}

		// TODO flags, like old SHT75 coefficients
		chip_conv_raw_fn_t conv = chip_conv_raw_fn(dst[j].chip_id);
		int res = conv(&dst[j].quantity, dst[j].raw_data, dst[j].raw_len, caldata, caldata_len);

		if (res) {
			error = USBTENKI_ERR_DATA_RAW_CONVERT;
			if (g_usbtenki_verbose) {
				char msg[64];
				sprintf(msg, "chip %d, channel: %d", dst[j].chip_id, dst[j].channel_id);
				usbtenki_perror(msg);
			}
		}

	}

	return 0;
}

/**
 * \brief Populate an array of struct USBTEnki_channel from a device
 * \return The number of channels
 * This does not read the channels.
 */
int usbtenki_listChannels(USBTenki_dev_handle hdl, struct USBTenki_channel *dstArray, int arr_size)
{
	const int total_number_channels = usbtenki_getNumChannels(hdl);

	if (total_number_channels <= 0) {
		return total_number_channels;
	}

	const int number_visible_channels = usbtenki_getNumVisibleChannels(hdl, total_number_channels);
	const int number_channels = g_usbtenki_show_all_hidden_channels ? total_number_channels : number_visible_channels;
	const int max_iteration = arr_size < number_channels ? arr_size : number_channels;

	if (arr_size < number_channels && g_usbtenki_verbose) {
		fprintf(stderr, "warning: Channel list truncated\n");
	}

	for (int i = 0; i < max_iteration; ++i){
		memset(dstArray, 0, sizeof(struct USBTenki_channel));
		dstArray->channel_id = i;
		dstArray->chip_id = usbtenki_getChipID(hdl, i);
		dstArray++;
	}

	return number_channels;
}

int usbtenki_set_dxc120_asc(USBTenki_dev_handle hdl, int value)
{
	unsigned char repBuf[8];
	int res;
	res = usbtenki_command(hdl, USBTENKI_SCD30_ENABLE_ASC, value, repBuf, sizeof(repBuf));

	if (res != 0) {
		error = USBTENKI_ERR_DATA_AUTOCAL_SET;
		if (g_usbtenki_verbose) {
			usbtenki_perror("Error setting DXC120 auto-calibration");
		}
	}
	return res;
}

int usbtenki_set_dxc120_frc(USBTenki_dev_handle hdl, int value)
{
	unsigned char repBuf[8];
	int res;
	res = usbtenki_command(hdl, USBTENKI_SCD30_SET_FRC, value, repBuf, sizeof(repBuf));

	if (res != 0) {
		error = USBTENKI_ERR_DATA_AUTOCAL_SET;
		if (g_usbtenki_verbose) {
			usbtenki_perror("Error setting DXC120 auto-calibration");
		}
	}
	return res;
}


int usbtenki_get_dxc120_asc(USBTenki_dev_handle hdl)
{
	unsigned char repBuf[8];
	int res;
	res = usbtenki_command(hdl, USBTENKI_SCD30_GET_ASC, 0, repBuf, sizeof(repBuf));

	if (res != 1) {
		error = USBTENKI_ERR_DATA_AUTOCAL_GET;
		if (g_usbtenki_verbose) {
			usbtenki_perror("Error getting DXC120 auto-calibration");
		}
		return -1;
	}

	return repBuf[0];
}

int usbtenki_set_dxc220_kz(USBTenki_dev_handle hdl, int ppm)
{
	static const unsigned int UNLOCK_CODE = 0x56BA;
	unsigned char repBuf[8];
	int res;

	res =  usbtenki_command(hdl, USBTENKI_UNLOCK_CALIBRATION, UNLOCK_CODE, repBuf, sizeof(repBuf));
	if (res != 0) {
		error = USBTENKI_ERR_DATA_AUTOCAL_UNLOCK;
		if (g_usbtenki_verbose) {
			usbtenki_perror("Error unlocking calibration");
		}
	}
	else {
		int value = ppm;
		int factor = 1;

		while (value > UINT16_MAX) {
			value /= 10;
			factor *= 10;
		}

		res =  usbtenki_command(hdl, USBTENKI_ZERO_IN_KNOWN_PPM, (factor << 16) | value , repBuf, sizeof(repBuf));
		if (res != 0) {
			error = USBTENKI_ERR_DATA_SET_ZERO;
			if (g_usbtenki_verbose) {
				usbtenki_perror("Error setting zero in known concentration");
			}
		}
	}

	return res;
}
