
/* 
 * Argyll Color Correction System
 *
 * SwatchMate Cube related functions
 *
 * Author: Graeme W. Gill
 * Date:   18/5/2015
 *
 * Copyright 1996 - 2015, Graeme W. Gill
 * All rights reserved.
 *
 * This material is licenced under the GNU GENERAL PUBLIC LICENSE Version 2 or later :-
 * see the License2.txt file for licencing details.
 *
 * Based on specbos.c
 */

/* 
   If you make use of the instrument driver code here, please note
   that it is the author(s) of the code who take responsibility
   for its operation. Any problems or queries regarding driving
   instruments with the Argyll drivers, should be directed to
   the Argyll's author(s), and not to any other party.

   If there is some instrument feature or function that you
   would like supported here, it is recommended that you
   contact Argyll's author(s) first, rather than attempt to
   modify the software yourself, if you don't have firm knowledge
   of the instrument communicate protocols. There is a chance
   that an instrument could be damaged by an incautious command
   sequence, and the instrument companies generally cannot and
   will not support developers that they have not qualified
   and agreed to support.
 */

/*

	TTBD:

	Need inst call to clear optional user calibrations -
	add capability, then option to check if set, and
	option to clear.


	Investigate clash between button triggered and progromatic.
	Need to check why calibration times out ??

	Need to make startup more robust - often fails to find instrument ?
	Need to cleanup shutdown (^C) ?

	Like to test on OS X and Linux.
	- hard to do this, as FT231XS driver support is only recent. 

	Like to add BlueTooth LE to MSWin/OS X/Linux.
	- hard to do this, as BTLE support is only recent.

*/


#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#ifndef SALONEINSTLIB
#include "copyright.h"
#include "aconfig.h"
#include "numlib.h"
#else	/* !SALONEINSTLIB */
#include "sa_config.h"
#include "numsup.h"
#endif /* !SALONEINSTLIB */
#include "xspect.h"
#include "insttypes.h"
#include "conv.h"
#include "icoms.h"
#include "inst.h"
#include "rspec.h"
#include "smcube.h"
#include "cubecal.h"		/* Calibration */

#undef USEW				/* [Und] Use white sensor rather than R,G,B */

#define ENABLE_NONVCAL	/* [Def] Enable saving calibration state between program runs in a file */

#define WCALTOUT  (1 * 60 * 60) /* [1 Hour ??] White Calibration timeout in seconds */

#define DEFTO 1.0		/* [1.0] Default command timeout */

/* Cube white reference RGB reflectivity as measured by cube */
//static double cwref[3] = { 0.646601, 0.668981, 0.703421 };
static double cwref[3] = { 0.795893, 0.818593, 0.855143 };

/* Assumed 45/0 RGB reflectance of gloss black reference */
static double glref[3] = { 0.012632, 0.009568, 0.010130 };

/* Default black offset if not calibrated with a light trap */
static double dsoff[3] = { 0.059465, 0.063213, 0.069603 };

/* Default gloss offset if not calibrated with a gloss reference */
static double dgoff[3] = { 0.056007, 0.052993, 0.054589 };

/* Assumed temperature coefficients for sensor output */
static double tempc[3] = { 0.0048, 0.0017, 0.0014 };

/* ------------------------------------------------- */

static inst_code smcube_interp_code(inst *pp, int ec);
static inst_code smcube_poll_measure(smcube *p, double to, int nd);
static inst_code smcube_black_calib(smcube *p, int ctype);
static inst_code smcube_get_temp(smcube *p, double *tval);
static inst_code smcube_get_cal_temp(smcube *p, int addr, double *tval);

#define MAX_MES_SIZE 500		/* Maximum normal message reply size */

/* Interpret an icoms error into a SMCUBE error */
static int icoms2smcube_err(int se) {
	if (se != ICOM_OK) {
		if (se & ICOM_TO)
			return SMCUBE_TIMEOUT; 
		return SMCUBE_COMS_FAIL;
	}
	return SMCUBE_OK;
}

/* Commands */
static inst_code smcube_ping(smcube *p);
static inst_code smcube_get_version(smcube *p, int *val);
static inst_code smcube_get_idle_time(smcube *p, int *pitime, int nd);
static inst_code smcube_fact_measure(smcube *p, double *XYZ);
static inst_code smcube_get_cal_val(smcube *p, int addr, double *cval);
static inst_code smcube_fact_white_calib(smcube *p);
static inst_code smcube_meas_wrgb(smcube *p, int ichan, int *wrgb);


static inst_code smcube_white(smcube *p, int ctype);
static inst_code smcube_measure(smcube *p, double *XYZ);

static void cube_rgb2XYZ(double *xyz, double *irgb);

/* Do a full command/response echange with the smcube */
/* (This level is not multi-thread safe) */
/* Return the smcube error code. */
static int
smcube_fcommand(
struct _smcube *p,
unsigned char *in,		/* Command string */
int ilen,				/* Number of bytes to send */
unsigned char *out,		/* Reply string buffer */
int olen,				/* Number of bytes expected in reply (buffer expected to be MAX_MES_SIZE) */
double to,				/* Timeout for response in seconds */
int nd					/* nz to disable debug messages */
) {
	int se;

	if (!nd) a1logd(p->log, 4, "smcube_fcommand: command '%s'\n", icoms_tohex(in,ilen));
	if ((se = p->icom->write(p->icom, (char *)in, ilen, 0.2)) != 0) {
		if (!nd) a1logd(p->log, 1, "smcube_fcommand: failure on serial write '%s' 0x%x\n",
		                                                                 icoms_tohex(in,ilen),se);
		return icoms2smcube_err(se);
	}

	/* Now wait for a reply */
	if ((se = p->icom->read(p->icom, (char *)out, MAX_MES_SIZE, NULL, NULL, olen, to)) != 0) {
		if (!nd) a1logd(p->log, 1, "smcube_fcommand: failure on serial 0x%x\n",se);
		return icoms2smcube_err(se);
	}
	if (!nd) a1logd(p->log, 4, "smcube_fcommand: returned '%s' err 0x%x\n",
	                                              icoms_tohex(out,olen), se);
	return se;
}

/* Do a normal command/response echange with the smcube. */
/* (This level is not multi-thread safe) */
/* Return the inst code */
static inst_code
smcube_command(
struct _smcube *p,
unsigned char *in,		/* Command string */
int ilen,				/* Number of bytes to send */
unsigned char *out,		/* Reply string buffer */
int olen,				/* Number of bytes expected in reply (buffer expected to be MAX_MES_SIZE) */
double to) {			/* Timout in seconds */
	int rv = smcube_fcommand(p, in, ilen, out, olen, to, 0);
	return smcube_interp_code((inst *)p, rv);
}

/* Establish communications with a smcube */
/* Return SMCUBE_COMS_FAIL on failure to establish communications */
static inst_code
smcube_init_coms(inst *pp, baud_rate br, flow_control fc, double tout) {
	smcube *p = (smcube *) pp;
	unsigned char buf[MAX_MES_SIZE];
	baud_rate brt[] = { baud_38400, baud_nc };
	unsigned int etime;
	unsigned int i;
	instType itype = pp->itype;
	int se;

	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_init_coms: About to init Serial I/O\n");


	if (p->icom->port_type(p->icom) != icomt_serial
	 && p->icom->port_type(p->icom) != icomt_usbserial) {
		a1logd(p->log, 1, "smcube_init_coms: wrong communications type for device!\n");
		return inst_coms_fail;
	}
	
	if (p->bt) {
		amutex_lock(p->lock);

		/* Check instrument is responding */
		buf[0] = 0x7e, buf[1] = 0x00, buf[2] = 0x02, buf[3] = 0x00; /* Ping command */

		if ((ev = smcube_command(p, buf, 4, buf, 4, DEFTO)) != inst_ok) {
			amutex_unlock(p->lock);
			a1logd(p->log, 2, "smcube_init_coms: ping didn't return\n");
			return ev;
		}
		if (buf[0] != 0x7e || buf[1] != 0x20 || buf[2] != 0x02 || buf[3] != 0x00) {
			amutex_unlock(p->lock);
			a1logd(p->log, 2, "smcube_init_coms: ping reply is wrong\n");
			return inst_unknown_model;
		}
		amutex_unlock(p->lock);

	} else {

		amutex_lock(p->lock);

		/* The tick to give up on */
		etime = msec_time() + (long)(1500.0 + 0.5);
	
		a1logd(p->log, 1, "smcube_init_coms: Trying different baud rates (%u msec to go)\n",etime - msec_time());
	
		/* Until we time out, find the correct baud rate */
		for (i = 0; msec_time() < etime; i++) {
			if (brt[i] == baud_nc) {
				i = 0;
			}
			a1logd(p->log, 5, "smcube_init_coms: trying %s baud\n",baud_rate_to_str(brt[i]));
			if ((se = p->icom->set_ser_port(p->icom, fc_Hardware, brt[i], parity_none,
				                         stop_1, length_8)) != ICOM_OK) { 
				amutex_unlock(p->lock);
				a1logd(p->log, 5, "smcube_init_coms: set_ser_port failed with 0x%x\n",se);
				return smcube_interp_code((inst *)p, icoms2smcube_err(se));;		/* Give up */
			}
	
			/* Check instrument is responding */
			buf[0] = 0x7e, buf[1] = 0x00, buf[2] = 0x02, buf[3] = 0x00; /* Ping command */
			if (((ev = smcube_command(p, buf, 4, buf, 4, DEFTO)) & inst_mask)
				                                                       != inst_coms_fail) {
				break;		/* We've got coms or user abort */
			}
	
			/* Check for user abort */
			if (p->uicallback != NULL) {
				inst_code ev;
				if ((ev = p->uicallback(p->uic_cntx, inst_negcoms)) == inst_user_abort) {
					amutex_unlock(p->lock);
					a1logd(p->log, 1, "smcube_init_coms: user aborted\n");
					return inst_user_abort;
				}
			}
		}
	
		if (msec_time() >= etime) {		/* We haven't established comms */
			amutex_unlock(p->lock);
			a1logd(p->log, 2, "smcube_init_coms: failed to establish coms\n");
			return inst_coms_fail;
		}
	
		/* Check the response */
		if (buf[0] != 0x7e || buf[1] != 0x20 || buf[2] != 0x02 || buf[3] != 0x00) {
			amutex_unlock(p->lock);
			a1logd(p->log, 2, "smcube_init_coms: ping didn't return\n");
			return inst_unknown_model;
		}
		amutex_unlock(p->lock);
	}
	a1logd(p->log, 2, "smcube_init_coms: init coms has suceeded\n");

	p->gotcoms = 1;

	return inst_ok;
}

/* Get idle time loop and user measurement detect */ 
/* Poll the instrument at 500msec intervals */
int smcube_mon_thread(void *pp) {
	int nfailed = 0;
	smcube *p = (smcube *)pp;
	inst_code rv1 = inst_ok; 
	a1logd(p->log,3,"Polling thread started\n");
	/* Try indefinitely, in case instrument is put to sleep */
	for (;;) {
		int itime;

		/* See if there is a button generated measure */
		rv1 = smcube_poll_measure(p, 0.1, 1);
		if ((rv1 & inst_mask) == inst_user_trig) {
			a1logd(p->log,3,"Found user trigger\n");
			p->switch_count++;
			if (!p->hide_switch && p->eventcallback != NULL) {
				p->eventcallback(p->event_cntx, inst_event_switch);
			}
		}
		
		/* Presumably this stops it going to sleep */
		rv1 = smcube_get_idle_time(p, &itime, 1); 

		if (p->th_term) {
			p->th_termed = 1;
			break;
		}
		if (rv1 != inst_ok) {
			nfailed++;
			a1logd(p->log,3,"Monitor thread failed with 0x%x\n",rv1);
			continue;
		}
		msec_sleep(500);
	}
	a1logd(p->log,3,"Monitor thread returning\n");
	return rv1;
}

/* Try and read the user measurement, and then trigger measure here. */
int smcube_utrig_thread(void *pp) {
	smcube *p = (smcube *)pp;
	inst_code rv = inst_ok; 

	/* Give caller a chance to return */
	msec_sleep(50);

	/* See if there is a button generated measure */
	rv = smcube_poll_measure(p, 0.1, 1);
	if ((rv & inst_mask) == inst_user_trig) {
		p->switch_count++;
		if (!p->hide_switch && p->eventcallback != NULL) {
			a1logd(p->log,3,"Found user trigger\n");
			p->eventcallback(p->event_cntx, inst_event_switch);
		}
	}
	return 0;
}

/* icoms interrupt callback - used did measurement ? */
int smcube_interrupt(icoms *icom, int icom_int) {
	smcube *p = (smcube *)icom->icntx;		/* Fetch the instrument context */
	inst_code rv = inst_ok; 

	a1logd(p->log,3,"smcube_interrupt called with %d\n",icom_int);

	if (icom_int != icomi_data_available)
		return ICOM_OK;

	/* See if there is a measurement */
	new_athread(smcube_utrig_thread, (void *)p);

	return 0;
}


/* Initialise the SMCUBE */
/* return non-zero on an error, with dtp error code */
static inst_code
smcube_init_inst(inst *pp) {
	smcube *p = (smcube *)pp;
	char mes[100];
	inst_code ev = inst_ok;
	int ver;

	a1logd(p->log, 2, "smcube_init_inst: called\n");

	if (p->gotcoms == 0)
		return inst_internal_error;		/* Must establish coms before calling init */

#ifdef NEVER
	if ((ev = smcube_get_version(p, &p->version)) != inst_ok) {
		return SMCUBE_UNKNOWN_MODEL;
	}
#endif

	amutex_lock(p->lock);
	
	if (p->log->verb) {
		/* Hmm. There is nothing to report */
	}

	if (!p->bt) {
		/* Start the polling loop thread */
		if ((p->th = new_athread(smcube_mon_thread, (void *)p)) == NULL) {
			amutex_unlock(p->lock);
			return SMCUBE_INT_THREADFAILED;
		}
	} else {
		/* Get called back if data becomes available */
		p->icom->interrupt = smcube_interrupt;
	}

	p->lo_secs = 2000000000;		/* A very long time */

#ifdef ENABLE_NONVCAL
	/* Restore idarl calibration from the local system */
	smcube_restore_calibration(p);
	/* Touch it so that we know when the instrument was last opened */
	smcube_touch_calibration(p);
#endif

	p->inited = 1;
	a1logd(p->log, 2, "smcube_init_inst: instrument inited OK\n");
	amutex_unlock(p->lock);

	if (p->log->verb) {
		a1logv(p->log, 1, " Version: %d\n",p->version);
	}

#ifdef NEVER
	/* Debug - dump the calibration */
	smcube_dump_cal(p);
#endif


	return inst_ok;
}

/* Read a single sample */
/* Return the dtp error code */
static inst_code
smcube_read_sample(
inst *pp,
char *name,			/* Strip name (7 chars) */
ipatch *val,		/* Pointer to instrument patch value */
instClamping clamp) {		/* NZ if clamp XYZ/Lab to be +ve */
	smcube *p = (smcube *)pp;
	int ec; 
	int switch_trig = 0;
	int user_trig = 0;
	inst_code rv = inst_protocol_error;

	if (!p->gotcoms)
		return inst_no_coms;
	if (!p->inited)
		return inst_no_init;

	/* Signal a calibration is needed */
	if (p->want_wcalib && !p->noinitcalib) {
		return inst_needs_cal;		/* Get user to calibrate */
	}

	if (p->trig == inst_opt_trig_user_switch) {
		int currcount = p->switch_count;		/* Variable set by thread */

		p->hide_switch = 1;						/* Supress switch events */

		currcount = p->switch_count;			/* Variable set by thread */
		while (currcount == p->switch_count) {
			int cerr;

			/* Don't trigger on user key if scan, only trigger */
			/* on instrument switch */
			if (p->uicallback != NULL
			 && (rv = p->uicallback(p->uic_cntx, inst_armed)) != inst_ok) {
				if (rv == inst_user_abort) {
					return rv;				/* Abort */
				}
				if (rv == inst_user_trig) {
					user_trig = 1;
					break;						/* Trigger */
				}
			}
			msec_sleep(100);
		}
		if (currcount != p->switch_count)
			switch_trig = 1;

		a1logd(p->log,3,"############# triggered ##############\n");
		if (p->uicallback)	/* Notify of trigger */
			p->uicallback(p->uic_cntx, inst_triggered); 

		p->hide_switch = 0;						/* Enable switch events again */

	} else if (p->trig == inst_opt_trig_user) {

		if (p->uicallback == NULL) {
			a1logd(p->log, 1, "smcube: inst_opt_trig_user but no uicallback function set!\n");
			return inst_unsupported;
		}

		for (;;) {
			if ((rv = p->uicallback(p->uic_cntx, inst_armed)) != inst_ok) {
				if (rv == inst_user_abort) {
					return rv;				/* Abort */
				}
				if (rv == inst_user_trig) {
					user_trig = 1;
					break;					/* Trigger */
				}
			}
			msec_sleep(200);
		}
		/* Notify of trigger */
		if (p->uicallback)
			p->uicallback(p->uic_cntx, inst_triggered); 

	/* Progromatic Trigger */
	} else {
		/* Check for abort */
		if (p->uicallback != NULL
		 && (rv = p->uicallback(p->uic_cntx, inst_armed)) == inst_user_abort) {
			return rv;				/* Abort */
		}
	}


	/* Take a measurement */
	if (p->icx < 2) {		/* Argyll calibrated measurement */
	
		rv = smcube_measure(p, val->XYZ);

	/* Original factory measurement */
	} else {
		if (switch_trig) {
			icmCpy3(val->XYZ, p->XYZ);
			rv = inst_ok;

		} else {
			rv = smcube_fact_measure(p, val->XYZ);
		}
	}


    if (rv != inst_ok) {
        return rv;
    }

	/* This may not change anything since instrument may clamp */
	if (clamp)
		icmClamp3(val->XYZ, val->XYZ);
	val->loc[0] = '\000';

	val->mtype = inst_mrt_reflective;
	val->XYZ_v = 1;		/* These are absolute XYZ readings */

	val->sp.spec_n = 0;
	val->duration = 0.0;
	rv = inst_ok;



	if (user_trig)
		return inst_user_trig;
	return rv;
}

/* Return needed and available inst_cal_type's */
static inst_code smcube_get_n_a_cals(inst *pp, inst_cal_type *pn_cals, inst_cal_type *pa_cals) {
	smcube *p = (smcube *)pp;
	time_t curtime = time(NULL);
	inst_cal_type n_cals = inst_calt_none;
	inst_cal_type a_cals = inst_calt_none;
	int white_valid = p->white_valid;
		
	if ((curtime - p->wdate) > WCALTOUT) {
		a1logd(p->log,2,"Invalidating white cal as %d secs from last cal\n",curtime - p->wdate);
		white_valid = 0;
	}

	if (!white_valid
	 || (p->want_wcalib && !p->noinitcalib))
		n_cals |= inst_calt_ref_white;

	a_cals |= inst_calt_ref_white;
	a_cals |= inst_calt_ref_dark;

	/* Gloss calibration if in gloss calibrated mode */
	if (p->icx == 1)
		a_cals |= inst_calt_ref_dark_gl;

	if (pn_cals != NULL)
		*pn_cals = n_cals;

	if (pa_cals != NULL)
		*pa_cals = a_cals;

	a1logd(p->log,3,"smcube: returning n_cals 0x%x, a_cals 0x%x\n",n_cals, a_cals);

	if (pn_cals != NULL)
		*pn_cals = n_cals;

	if (pa_cals != NULL)
		*pa_cals = a_cals;

	return inst_ok;
}

/* Request an instrument calibration. */
inst_code smcube_calibrate(
inst *pp,
inst_cal_type *calt,	/* Calibration type to do/remaining */
inst_cal_cond *calc,	/* Current condition/desired condition */
char id[CALIDLEN]		/* Condition identifier (ie. white reference ID) */
) {
	smcube *p = (smcube *)pp;
    inst_cal_type needed, available;
	int dosave = 0;
	inst_code ev = inst_ok;

	if (!p->gotcoms)
		return inst_no_coms;
	if (!p->inited)
		return inst_no_init;

	id[0] = '\000';

	if ((ev = smcube_get_n_a_cals((inst *)p, &needed, &available)) != inst_ok)
		return ev;

	/* Translate inst_calt_all/needed into something specific */
	if (*calt == inst_calt_all
	 || *calt == inst_calt_needed
	 || *calt == inst_calt_available) {
		if (*calt == inst_calt_all) 
			*calt = (needed & inst_calt_n_dfrble_mask) | inst_calt_ap_flag;
		else if (*calt == inst_calt_needed)
			*calt = needed & inst_calt_n_dfrble_mask;
		else if (*calt == inst_calt_available)
			*calt = available & inst_calt_n_dfrble_mask;

		a1logd(p->log,4,"smcube_calibrate: doing calt 0x%x\n",calt);

		if ((*calt & inst_calt_n_dfrble_mask) == 0)		/* Nothing todo */
			return inst_ok;
	}

	/* See if it's a calibration we understand */
	if (*calt & ~available & inst_calt_all_mask) { 
		return inst_unsupported;
	}

	if (*calt & inst_calt_ref_white) {		/* White calibration */
		time_t cdate = time(NULL);

		if ((*calc & inst_calc_cond_mask) != inst_calc_man_ref_white) {
			*calc = inst_calc_man_ref_white;
			ev = inst_cal_setup;
			goto done;
		}

		if ((ev = smcube_fact_white_calib(p)) != inst_ok) {
			goto done;
		}

		p->white_valid = 1;
		p->want_wcalib = 0;
		p->wdate = cdate;
		*calt &= ~inst_calt_ref_white;
		dosave = 1;
	}

	/* Light trap calibration: */

	/* Is the user skipping the Light trap calibration ? */
	if (*calt & inst_calt_ref_dark
	 && (*calc & inst_calc_cond_mask) == inst_calc_man_ref_dark
	 && *calc & inst_calc_optional_flag) {
		*calt &= ~inst_calt_ref_dark;
	}

	if (*calt & inst_calt_ref_dark) {
		time_t cdate = time(NULL);

		if ((*calc & inst_calc_cond_mask) != inst_calc_man_ref_dark) {
			*calc = inst_calc_man_ref_dark | inst_calc_optional_flag;
			ev = inst_cal_setup;
			goto done;
		}

		if ((ev = smcube_black_calib(p, 0)) != inst_ok) {
			goto done;
		}

		p->dark_valid = 1;
		p->dark_default = 0;
		p->ddate = cdate;
		*calt &= ~inst_calt_ref_dark;
		dosave = 1;
	}

	/* Gloss black calibration */

	/* Is the user skipping the Gloss calibration ? */
	if (*calt & inst_calt_ref_dark_gl
	 && (*calc & inst_calc_cond_mask) == inst_calc_man_dark_gloss
	 && *calc & inst_calc_optional_flag) {
		*calt &= ~inst_calt_ref_dark_gl;
	}

	if (*calt & inst_calt_ref_dark_gl) {
		time_t cdate = time(NULL);

		if ((*calc & inst_calc_cond_mask) != inst_calc_man_dark_gloss) {
			*calc = inst_calc_man_dark_gloss | inst_calc_optional_flag;
			ev = inst_cal_setup;
			goto done;
		}

		if ((ev = smcube_black_calib(p, 1)) != inst_ok) {
			goto done;
		}

		p->gloss_valid = 1;
		p->gloss_default = 0;
		p->gdate = cdate;
		*calt &= ~inst_calt_ref_dark_gl;
		dosave = 1;
	}

  done:;

#ifdef ENABLE_NONVCAL
	if (dosave) {
		/* Save the idark calibration to a file */
		smcube_save_calibration(p);
	}
#endif

	return ev;
}

/* Error codes interpretation */
static char *
smcube_interp_error(inst *pp, int ec) {
//	smcube *p = (smcube *)pp;
	ec &= inst_imask;
	switch (ec) {
		case SMCUBE_INTERNAL_ERROR:
			return "Internal software error";
		case SMCUBE_TIMEOUT:
			return "Communications timeout";
		case SMCUBE_COMS_FAIL:
			return "Communications failure";
		case SMCUBE_UNKNOWN_MODEL:
			return "Not a SwatchMate Cube";
		case SMCUBE_DATA_PARSE_ERROR:
			return "Data from smcube didn't parse as expected";

		case SMCUBE_OK:
			return "No device error";

		case SMCUBE_INT_THREADFAILED:
			return "Starting diffuser position thread failed";
		case SMCUBE_INT_ILL_WRITE:
			return "Attemp to write to factory calibration";
		case SMCUBE_INT_WHITE_CALIB:
			return "No valid white calibration";
		case SMCUBE_INT_BLACK_CALIB:
			return "No valid black calibration";
		case SMCUBE_INT_GLOSS_CALIB:
			return "No valid gloss calibration";
		case SMCUBE_INT_CAL_SAVE:
			return "Saving calibration file failed";
		case SMCUBE_INT_CAL_RESTORE:
			return "Restoring calibration file failed";
		case SMCUBE_INT_CAL_TOUCH:
			return "Touching calibration file failed";

		case SMCUBE_WHITE_CALIB_ERR:
			return "White calibration is outside expected range";
		case SMCUBE_BLACK_CALIB_ERR:
			return "Black calibration is outside expected range";
		case SMCUBE_GLOSS_CALIB_ERR:
			return "Gloss calibration is outside expected range";

		default:
			return "Unknown error code";
	}
}


/* Convert a machine specific error code into an abstract dtp code */
static inst_code 
smcube_interp_code(inst *pp, int ec) {

	ec &= inst_imask;
	switch (ec) {

		case SMCUBE_OK:
			return inst_ok;

		case SMCUBE_INTERNAL_ERROR:
		case SMCUBE_INT_THREADFAILED:
		case SMCUBE_INT_ILL_WRITE:
		case SMCUBE_INT_WHITE_CALIB:
		case SMCUBE_INT_BLACK_CALIB:
		case SMCUBE_INT_GLOSS_CALIB:
		case SMCUBE_INT_CAL_SAVE:
		case SMCUBE_INT_CAL_RESTORE:
		case SMCUBE_INT_CAL_TOUCH:
			return inst_internal_error | ec;

		case SMCUBE_TIMEOUT:
		case SMCUBE_COMS_FAIL:
			return inst_coms_fail | ec;

//			return inst_unknown_model | ec;

		case SMCUBE_DATA_PARSE_ERROR:
			return inst_protocol_error | ec;

//			return inst_wrong_config | ec;

//			return inst_bad_parameter | ec;

		case SMCUBE_WHITE_CALIB_ERR:
		case SMCUBE_BLACK_CALIB_ERR:
		case SMCUBE_GLOSS_CALIB_ERR:
			return inst_misread | ec;

//			return inst_hardware_fail | ec;
	}
	return inst_other_error | ec;
}

/* Destroy ourselves */
static void
smcube_del(inst *pp) {
	if (pp != NULL) {
		smcube *p = (smcube *)pp;

#ifdef ENABLE_NONVCAL
		smcube_touch_calibration(p);
#endif

		if (p->th != NULL) {		/* Terminate diffuser monitor thread  */
			int i;
			p->th_term = 1;			/* Tell thread to exit on error */
			for (i = 0; p->th_termed == 0 && i < 5; i++)
				msec_sleep(100);	/* Wait for thread to terminate */
			if (i >= 5) {
				a1logd(p->log,3,"smcube diffuser thread termination failed\n");
			}
			p->th->del(p->th);
		}
		if (p->icom != NULL)
			p->icom->del(p->icom);
		amutex_del(p->lock);
		free(p);
	}
}

/* Return the instrument mode capabilities */
static void smcube_capabilities(inst *pp,
inst_mode *pcap1,
inst2_capability *pcap2,
inst3_capability *pcap3) {
	smcube *p = (smcube *)pp;
	inst_mode cap1 = 0;
	inst2_capability cap2 = 0;

	cap1 |= inst_mode_ref_spot
	     |  inst_mode_colorimeter
	        ;

	/* can inst2_has_sensmode, but not report it asynchronously */
	cap2 |= inst2_prog_trig
	     |  inst2_user_trig
	     |  inst2_user_switch_trig
	     |  inst2_disptype			/* Calibration modes */
	     |  inst2_opt_calibs		/* Has optional calibrations that can be cleared */
	        ;

	if (pcap1 != NULL)
		*pcap1 = cap1;
	if (pcap2 != NULL)
		*pcap2 = cap2;
	if (pcap3 != NULL)
		*pcap3 = inst3_none;
}


/* Check device measurement mode */
static inst_code smcube_check_mode(inst *pp, inst_mode m) {
	inst_mode cap;

	if (!pp->gotcoms)
		return inst_no_coms;
	if (!pp->inited)
		return inst_no_init;

	pp->capabilities(pp, &cap, NULL, NULL);

	/* Simple test */
	if (m & ~cap)
		return inst_unsupported;

	/* General check mode against specific capabilities logic: */
	if (!IMODETST(m, inst_mode_ref_spot)) {
		return inst_unsupported;
	}

	return inst_ok;
}

/* Set device measurement mode */
static inst_code smcube_set_mode(inst *pp, inst_mode m) {
	smcube *p = (smcube *)pp;
	int refrmode;
	inst_code ev;

	if ((ev = smcube_check_mode(pp, m)) != inst_ok)
		return ev;

	p->mode = m;

	return inst_ok;
}

/* Calibration modes */
static inst_disptypesel smcube_disptypesel[4] = {
	{
		inst_dtflags_default,		/* flags */
		0,							/* cbix */
		"m",						/* sel */
		"Matte",					/* desc */
		0,							/* refr */
		disptech_none,				/* disptype */
		0							/* ix */
	},
	{
		inst_dtflags_none,			/* flags */
		0,							/* cbix */
		"g",						/* sel */
		"Gloss",					/* desc */
		0,							/* refr */
		disptech_none,				/* disptype */
		1							/* ix */
	},
	{
		inst_dtflags_none,			/* flags */
		0,							/* cbix */
		"N",						/* sel */
		"Native Calibration",		/* desc */
		0,							/* refr */
		disptech_none,				/* disptype */
		2							/* ix */
	},
	{
		inst_dtflags_end,
		0,
		"",
		"",
		0,
		disptech_none,
		0
	}
};

/* Get mode and option details */
static inst_code smcube_get_disptypesel(
inst *pp,
int *pnsels,				/* Return number of display types */
inst_disptypesel **psels,	/* Return the array of display types */
int allconfig,				/* nz to return list for all configs, not just current. */
int recreate				/* nz to re-check for new ccmx & ccss files */
) {
	smcube *p = (smcube *)pp;
	inst_code rv = inst_ok;

	if (pnsels != NULL)
		*pnsels = 3;

	if (psels != NULL)
		*psels = smcube_disptypesel;

	return inst_ok;
}

/* Given a display type entry, setup for that type */
static inst_code set_disp_type(smcube *p, inst_disptypesel *dentry) {
	inst_code rv;
	int refrmode;

	p->icx = dentry->ix;
	p->dtech = dentry->dtech;

	return inst_ok;
}

/* Set the display type - refresh or not */
static inst_code smcube_set_disptype(inst *pp, int ix) {
	smcube *p = (smcube *)pp;
	inst_code ev;
	inst_disptypesel *dentry;

	if (!p->gotcoms)
		return inst_no_coms;
	if (!p->inited)
		return inst_no_init;

	if (ix < 0 || ix > 2)
		return inst_unsupported;

	a1logd(p->log,5,"smcube smcube_set_disptype ix %d\n",ix);
	dentry = &smcube_disptypesel[ix];

	if ((ev = set_disp_type(p, dentry)) != inst_ok) {
		return ev;
	}

	return inst_ok;
}

/* Set the noinitcalib mode */
static void smcube_set_noinitcalib(smcube *p, int v, int losecs) {

	/* Ignore disabling init calib if more than losecs since instrument was open */
	if (v && losecs != 0 && p->lo_secs >= losecs) {
		a1logd(p->log,3,"initcalib disable ignored because %d >= %d secs\n",p->lo_secs,losecs);
		return;
	}
	p->noinitcalib = v;
}

static void set_optcalibs_default(smcube *p) {
	/* Default black offset */
	p->soff[0] = dsoff[0];
	p->soff[1] = dsoff[1];
	p->soff[2] = dsoff[2];
	p->dark_valid = 1;
	p->dark_default = 1;

	/* Default gloss offset */
	p->goff[0] = dgoff[0];
	p->goff[1] = dgoff[1];
	p->goff[2] = dgoff[2];
	p->gloss_valid = 1;
	p->gloss_default = 1;
}

/* 
 * set or reset an optional mode
 *
 * Some options talk to the instrument, and these will
 * error if it hasn't been initialised.
 */
static inst_code
smcube_get_set_opt(inst *pp, inst_opt_type m, ...)
{
	smcube *p = (smcube *)pp;
	inst_code ev = inst_ok;

	a1logd(p->log, 5, "smcube_get_set_opt: opt type 0x%x\n",m);

	if (m == inst_opt_initcalib) {		/* default */
		smcube_set_noinitcalib(p, 0, 0);
		return inst_ok;

	} else if (m == inst_opt_noinitcalib) {
		va_list args;
		int losecs = 0;

		va_start(args, m);
		losecs = va_arg(args, int);
		va_end(args);

		smcube_set_noinitcalib(p, 1, losecs);
		return inst_ok;

	/* Record the trigger mode */
	} else if (m == inst_opt_trig_prog
	 || m == inst_opt_trig_user
	 || m == inst_opt_trig_user_switch) {
		p->trig = m;
		return inst_ok;

	/* Is there a black or gloss optional user calibration being used ? */
	} else if (m == inst_opt_opt_calibs_valid) {
		va_list args;
		int *valid;

		va_start(args, m);
		valid = va_arg(args, int *);
		va_end(args);

		if (p->dark_default && p->gloss_default)
			*valid = 0;
		else
			*valid = 1;

		return inst_ok;

	/* Clear all the optional user calibrations back to default */
	} else if (m == inst_opt_clear_opt_calibs) {
		va_list args;

		set_optcalibs_default(p);

#ifdef ENABLE_NONVCAL
		/* Save the updated calibration state to a file */
		smcube_save_calibration(p);
#endif
		return inst_ok;
	}

	/* Get/Sets that require instrument coms. */
	if (!p->gotcoms)
		return inst_no_coms;
	if (!p->inited)
		return inst_no_init;

	return inst_unsupported;
}

/* Constructor */
extern smcube *new_smcube(icoms *icom, instType itype) {
	smcube *p;
	if ((p = (smcube *)calloc(sizeof(smcube),1)) == NULL) {
		a1loge(icom->log, 1, "new_smcube: malloc failed!\n");
		return NULL;
	}

	p->log = new_a1log_d(icom->log);

	p->init_coms         = smcube_init_coms;
	p->init_inst         = smcube_init_inst;
	p->capabilities      = smcube_capabilities;
	p->check_mode        = smcube_check_mode;
	p->set_mode          = smcube_set_mode;
	p->get_disptypesel   = smcube_get_disptypesel;
	p->set_disptype      = smcube_set_disptype;
	p->get_set_opt       = smcube_get_set_opt;
	p->read_sample       = smcube_read_sample;
	p->get_n_a_cals      = smcube_get_n_a_cals;
	p->calibrate         = smcube_calibrate;
	p->interp_error      = smcube_interp_error;
	p->del               = smcube_del;

	p->icom = icom;
	icom->icntx = (void *)p;	/* Allow us to get instrument from icom */
	p->itype = itype;

	amutex_init(p->lock);

	p->trig = inst_opt_trig_user;

	p->want_wcalib = 1;			/* Do a white calibration each time we open the device */

	set_optcalibs_default(p);

	return p;
}

/* ============================================================================== */
/* Implementation. All of these are thread safe unless noted */

static inst_code
smcube_ping(smcube *p) {
	unsigned char buf[MAX_MES_SIZE];
	inst_code ev = inst_ok;
	int se;

	a1logd(p->log, 2, "smcube_ping:\n");

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	buf[0] = 0x7e;
	buf[1] = 0x00;
	buf[2] = 0x02;
	buf[3] = 0x00;

	if ((ev = smcube_command(p, buf, 4, buf, 4, DEFTO)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check the response */
	if (buf[0] != 0x7e || buf[1] != 0x20 || buf[2] != 0x02 || buf[3] == 0x00) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}
	a1logd(p->log, 2, "smcube_init_coms: ping sucesss\n");

	amutex_unlock(p->lock);

	return inst_ok;
}

static inst_code
smcube_get_idle_time(smcube *p, int *pitime, int nd) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;
	int se;

	if (!nd)
		a1logd(p->log, 2, "smcube_get_idle_time:\n");

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 6);
	buf[0] = 0x7e;
	buf[1] = 0x02;
	buf[2] = 0x51;

	if ((se = smcube_fcommand(p, buf, 6, buf, 6, 0.2, nd)) != inst_ok) {
		amutex_unlock(p->lock);
		return smcube_interp_code((inst *)p, se);
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x51) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}
	itime = read_ORD16_be(buf + 4);

	if (!nd)
		a1logd(p->log, 2, "smcube_get_idle_time: returing %d\n",itime);

	if (pitime != NULL)
		*pitime = itime;

	return inst_ok;
}

/* Do a factory measurement */
static inst_code
smcube_fact_measure(smcube *p, double *XYZ) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_fact_measure:\n");

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 16);
	buf[0] = 0x7e;
	buf[1] =   12;
	buf[2] = 0x40;

	if ((ev = smcube_command(p, buf, 16, buf, 16, 3.5)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x40) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}
	XYZ[0] = IEEE754todouble(read_ORD32_be(buf + 4));
	XYZ[1] = IEEE754todouble(read_ORD32_be(buf + 8));
	XYZ[2] = IEEE754todouble(read_ORD32_be(buf + 12));
	a1logd(p->log, 2, "smcube_fact_measure: returing L*a*b* %f %f %f\n",XYZ[0], XYZ[1], XYZ[2]);

	icmLab2XYZ(&icmD50_100, XYZ, XYZ);

	a1logd(p->log, 2, "smcube_fact_measure: returing XYZ %f %f %f\n",XYZ[0], XYZ[1], XYZ[2]);

	return inst_ok;
}

/* Try and fetch a button generated measurement meassage */
/* Return inst_user_trig if found one */
static inst_code
smcube_poll_measure(smcube *p, double to, int nd) {
	unsigned char buf[MAX_MES_SIZE];
	int se;
	inst_code ev = inst_ok;

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	if ((se = p->icom->read(p->icom, (char *)buf, MAX_MES_SIZE, NULL, NULL, 16, to)) != 0
		                                                   && (se & ICOM_TO) == 0) {
		amutex_unlock(p->lock);
		return icoms2smcube_err(se);
	}
	amutex_unlock(p->lock);

	/* If we got a timeout, ignore the read */
	if ((se & ICOM_TO) != 0) {
		return inst_ok;
	}

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x40) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}
	p->XYZ[0] = IEEE754todouble(read_ORD32_be(buf + 4));
	p->XYZ[1] = IEEE754todouble(read_ORD32_be(buf + 8));
	p->XYZ[2] = IEEE754todouble(read_ORD32_be(buf + 12));
	if (!nd) a1logd(p->log, 2, "smcube_poll_measure: returing L*a*b* %f %f %f\n",p->XYZ[0], p->XYZ[1], p->XYZ[2]);

	icmLab2XYZ(&icmD50_100, p->XYZ, p->XYZ);

	return inst_user_trig;
}

/* wrgb channel numbers to use */
#ifdef USEW
# define RCH 0		/* Use the White channel */
# define GCH 0
# define BCH 0
#else
# define RCH 1		/* Use the R,G & B channels */
# define GCH 2
# define BCH 3
#endif

/* Measure a 4 channel intensity value */
/* 0 = White */
/* 1 = Red */
/* 2 = Green */
/* 3 = Blue */

static inst_code
smcube_meas_wrgb(smcube *p, int ichan, int *wrgb) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	int cmd = 0x47 + ichan;
	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_meas_wrgb: ichan %d\n",ichan);

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 12);
	buf[0] = 0x7e;
	buf[1] =    8;
	buf[2] =  cmd;

	if ((ev = smcube_command(p, buf, 12, buf, 12, 1.5)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != cmd) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	wrgb[0] = read_ORD16_be(buf + 4);
	wrgb[1] = read_ORD16_be(buf + 6);
	wrgb[2] = read_ORD16_be(buf + 8);
	wrgb[3] = read_ORD16_be(buf + 10);

	a1logd(p->log, 2, "smcube_meas_wrgb: WRGB %d %d %d %d\n",wrgb[0], wrgb[1], wrgb[2], wrgb[3]);

	return inst_ok;
}

/* Get the version information */
/* Doesn't seem to be implemented ? */
static inst_code smcube_get_version(smcube *p, int *val) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_version:\n");

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 5);
	buf[0] = 0x7e;
	buf[1] =    1;
	buf[2] =  0x19;

	if ((ev = smcube_command(p, buf, 5, buf, 5, 1.5)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x19) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	val[0] = read_ORD8(buf + 4);

	a1logd(p->log, 2, "smcube_version: val %d\n",val[0]);

	return inst_ok;
}


/* Read a calibration value */
static inst_code
smcube_get_cal_val(smcube *p, int addr, double *cval) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_get_cal_val: addr %d\n",addr);

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 9);
	buf[0] = 0x7e;
	buf[1] =    5;
	buf[2] = 0x04;

	buf[4] = addr;

	if ((ev = smcube_command(p, buf, 9, buf, 9, DEFTO)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x04) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	*cval = IEEE754todouble(read_ORD32_be(buf + 5));

	a1logd(p->log, 2, "smcube_get_cal_val: addr %d val %f",addr,*cval);

	return inst_ok;
}

/* Diagnostic - dump all the calibration values */
static void
smcube_dump_cal(smcube *p) {
	int i;
	double val;
	inst_code ev = inst_ok;

	for (i = 0; i < 89; i++) {
		if ((ev = smcube_get_cal_val(p, i, &val)) == inst_ok) {
			printf("Cal addr %d = %f\n",i,val);
		}
	}
}


/* Write a calibration value */
static inst_code
smcube_set_cal_val(smcube *p, int addr, double cval) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;

	a1logd(p->log, 2, "smcube_set_cal_val: addr %d value %f\n",addr,cval);

	if (!p->gotcoms)
		return inst_no_coms;

	if (addr <= 50
	|| (addr >= 69 && addr <= 77) 
	||  addr >= 87) {
		return smcube_interp_code((inst *)p, SMCUBE_INT_ILL_WRITE);
	}

	amutex_lock(p->lock);

	memset(buf, 0, 9);
	buf[0] = 0x7e;
	buf[1] =    5;
	buf[2] = 0x03;

	buf[4] = addr;
	write_ORD32_be(buf + 5, doubletoIEEE754(cval));

	if ((ev = smcube_command(p, buf, 9, buf, 9, DEFTO)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x03) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	a1logd(p->log, 2, "smcube_set_cal_val: addr %d OK\n",addr);

	return inst_ok;
}

/* Get the current temperature */
static inst_code
smcube_get_temp(smcube *p, double *tval) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;
	double temp = 0.0;

	a1logd(p->log, 2, "smcube_get_temp:\n");

	if (!p->gotcoms)
		return inst_no_coms;

	amutex_lock(p->lock);

	memset(buf, 0, 8);
	buf[0] = 0x7e;
	buf[1] =    4;
	buf[2] = 0x41;

	if ((ev = smcube_command(p, buf, 8, buf, 8, DEFTO)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x41) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	temp = IEEE754todouble(read_ORD32_be(buf + 4));

	a1logd(p->log, 2, "smcube_get_temp: val %f OK\n",temp);
	
	if (tval != NULL)
		*tval = temp;

	return inst_ok;
}

/* Get a calibration temperature. */
/* Is this stored automatically on smcube_set_cal_val() ? */ 
/* Typically this is from addr 78, i.e. first user grey scale slot */
static inst_code
smcube_get_cal_temp(smcube *p, int addr, double *tval) {
	unsigned char buf[MAX_MES_SIZE];
	int itime;
	inst_code ev = inst_ok;
	double temp = 0.0;

	a1logd(p->log, 2, "smcube_get_cal_temp: addr %d\n",addr);

	if (!p->gotcoms)
		return inst_no_coms;

	if (addr <= 50
	|| (addr >= 69 && addr <= 77) 
	||  addr >= 87) {
		return smcube_interp_code((inst *)p, SMCUBE_INT_ILL_WRITE);
	}

	amutex_lock(p->lock);

	memset(buf, 0, 9);
	buf[0] = 0x7e;
	buf[1] =    5;
	buf[2] = 0x05;

	buf[4] = addr;

	if ((ev = smcube_command(p, buf, 9, buf, 9, DEFTO)) != inst_ok) {
		amutex_unlock(p->lock);
		return ev;
	}
	amutex_unlock(p->lock);

	/* Check protocol */
	if (buf[0] != 0x7e || buf[2] != 0x05) {
		return smcube_interp_code((inst *)p, SMCUBE_DATA_PARSE_ERROR);
	}

	/* Check error code */
	if (buf[3] != 0) {
		return smcube_interp_code((inst *)p, buf[3]);
	}

	temp = IEEE754todouble(read_ORD32_be(buf + 5));

	a1logd(p->log, 2, "smcube_get_cal_temp: addr %d, val %f OK\n",addr,temp);
	
	if (tval != NULL)
		*tval = temp;

	return inst_ok;
}

/* Do a Factory & Argyll white calibration */
static inst_code
smcube_fact_white_calib(smcube *p) {
	inst_code ev = inst_ok;
	int i, j;
	int wrgb[3][4];

#ifdef USEW
	int normal[3] = { 10009, 20382, 37705 };
#else
	int normal[3] = { 9221, 13650, 28568 };
#endif

	a1logd(p->log, 2, "smcube_fact_white_calib:\n");

	/* Meaure the R, G * B */
	for (i = 0; i < 3; i++) {
		if ((ev = smcube_meas_wrgb(p, i + 1, wrgb[i])) != inst_ok) {
			return ev;
		}
	}

	/* Write the R, G, & B to the Cube User Grey Scale calibration */
	for (i = 0; i < 3; i++) {
		for (j = 0; j < 3; j++) {
			if ((ev = smcube_set_cal_val(p, 78 + i * 3 + j, (double)wrgb[i][j+1])) != inst_ok) {
				return ev;
			}
		}
	}

	/* Compute Argyll calibration scale factors */
	{
		double tmp[3];

		a1logd(p->log, 2, "smcube_white_calib: Got raw RGB %d %d %d\n",
		                              wrgb[0][RCH],wrgb[1][GCH],wrgb[2][BCH]);

		/* Sanity check raw values */
		if (wrgb[0][RCH] < (normal[0]/2) || wrgb[0][RCH] > (3 * normal[0]/2) 
		 || wrgb[1][GCH] < (normal[1]/2) || wrgb[1][GCH] > (3 * normal[1]/2) 
		 || wrgb[2][BCH] < (normal[2]/2) || wrgb[2][BCH] > (3 * normal[2]/2)) { 
			return smcube_interp_code((inst *)p, SMCUBE_WHITE_CALIB_ERR);
		}

		/* Add black offset into cwref */ 
		for (i = 0; i < 3; i++)
			tmp[i] = cwref[i] * (1.0 - p->soff[i]) + p->soff[i];

		p->sscale[0] = tmp[0]/(double)wrgb[0][RCH];
		p->sscale[1] = tmp[1]/(double)wrgb[1][GCH];
		p->sscale[2] = tmp[2]/(double)wrgb[2][BCH];

		if ((ev = smcube_get_cal_temp(p, 78, &p->ctemp)) != inst_ok)
			return ev;

		a1logd(p->log, 2, "smcube_fact_white_calib: Argyll cal = %e %e %e at temp %f\n",
		                           p->sscale[0], p->sscale[1], p->sscale[2], p->ctemp);
	}

	a1logd(p->log, 2, "smcube_fact_white_calib: done\n");

	return inst_ok;
}

/* Apply temperature compensation to the raw sensor readings */
static void temp_comp_raw(smcube *p, double *rgb, double tval) {
	int j;
	double tchange = tval - p->ctemp;

	for (j = 0; j < 3; j++) {
		rgb[j] *= (1.0 + tchange * tempc[j]);
	} 
}

/* -------------------------------------------------------------- */
/* Argyll calibrated measurement support */

static inst_code
smcube_black_calib(smcube *p, int ctype) {
	inst_code ev = inst_ok;
	int i, j;
	int wrgb[3][4];
	double rgb[3];
	double tval;

	a1logd(p->log, 2, "smcube_black_calib: type %s\n",ctype == 0 ? "trap" : "gloss");

	/* Meaure the R, G * B */
	for (i = 0; i < 3; i++) {
		if ((ev = smcube_meas_wrgb(p, i + 1, wrgb[i])) != inst_ok) {
			return ev;
		}
	}
	rgb[0] = (double)wrgb[0][RCH];
	rgb[1] = (double)wrgb[1][GCH];
	rgb[2] = (double)wrgb[2][BCH];

	if ((ev = smcube_get_temp(p, &tval)) != inst_ok) {
		return ev;
	}
	temp_comp_raw(p, rgb, tval);

	if (!p->white_valid) {
		return smcube_interp_code((inst *)p, SMCUBE_INT_WHITE_CALIB);
	}

	/* Black */
	if (ctype == 0) {

		for (i = 0; i < 3; i++) {
			rgb[i] = p->sscale[i] * rgb[i];
			if (rgb[i] < 0.0)
				rgb[i] = 0.0;
		}

		a1logd(p->log, 2, "smcube_black_calib: soff = %f %f %f, default %f %f %f\n",
		       rgb[0], rgb[1], rgb[2], dsoff[0], dsoff[1], dsoff[2]); 

		/* Sanity check values */
		for (i = 0; i < 3; i++) {
			if (rgb[i] < 0.5 * (dsoff[i])
			 || rgb[i] > 2.0 * (dsoff[i])) {
				a1logd(p->log, 1, "smcube_black_calib: rgb[%d] %f out of range %f .. %f\n",
				                                i, rgb[i], 0.5 * dsoff[i], 2.0 * dsoff[i]);
				break;
			}
		}

		if (i < 3) {
			return smcube_interp_code((inst *)p, SMCUBE_BLACK_CALIB_ERR);
		}

		for (i = 0; i < 3; i++)
			p->soff[i] = rgb[i];

	/* Gloss */
	} else {
		if (!p->dark_valid) {
			return smcube_interp_code((inst *)p, SMCUBE_INT_BLACK_CALIB);
		}

		/* Scale to 100% white reference */
		for (i = 0; i < 3; i++) {
			rgb[i] = p->sscale[i] * rgb[i];
		}

		/* Remove the black offset */
		for (i = 0; i < 3; i++) {
			rgb[i] = (rgb[i] - p->soff[i])/(1.0 - p->soff[i]);
			if (rgb[i] < 0.0)
				rgb[i] = 0.0;
		}
		
		/* Compute the gloss offset */
		for (i = 0; i < 3; i++) {
			rgb[i] = rgb[i] - glref[0];
			if (rgb[i] < 0.0)
				rgb[i] = 0.0;
		}
		a1logd(p->log, 2, "smcube_gloss_calib: goff = %f %f %f, default %f %f %f\n",
		       rgb[0], rgb[1], rgb[2], dgoff[0], dgoff[1], dgoff[2]); 

		/* Sanity check values */
		for (i = 0; i < 3; i++) {
			if (rgb[i] < 0.5 * dgoff[i]
			 || rgb[i] > 2.0 * dgoff[i]) {
				a1logd(p->log, 1, "smcube_gloss_calib: rgb[%d] %f out of range %f .. %f\n",
				                                i, rgb[i], 0.5 * dgoff[i], 2.0 * dgoff[i]);
				break;
			}
		}

		if (i < 3) {
			return smcube_interp_code((inst *)p, SMCUBE_GLOSS_CALIB_ERR);
		}
		
		for (i = 0; i < 3; i++)
			p->goff[i] = rgb[i];
	}

	a1logd(p->log, 2, "smcube_black_calib: done\n");

	return inst_ok;
}

static inst_code
smcube_measure(smcube *p, double *XYZ) {
	inst_code ev = inst_ok;
	int i, j;
	int wrgb[3][4];
	double rgb[3];
	double tval;

	a1logd(p->log, 2, "smcube_measure:\n");

	/* Meaure the R, G * B */
	for (i = 0; i < 3; i++) {
		if ((ev = smcube_meas_wrgb(p, i + 1, wrgb[i])) != inst_ok) {
			return ev;
		}
	}
	rgb[0] = (double)wrgb[0][RCH];
	rgb[1] = (double)wrgb[1][GCH];
	rgb[2] = (double)wrgb[2][BCH];

	a1logd(p->log, 2, "smcube_measure: Raw RGB %f %f %f\n",rgb[0],rgb[1],rgb[2]);

	if ((ev = smcube_get_temp(p, &tval)) != inst_ok) {
		return ev;
	}
	temp_comp_raw(p, rgb, tval);

	a1logd(p->log, 2, "smcube_measure: Temp comp. RGB %f %f %f\n",rgb[0],rgb[1],rgb[2]);

	if (!p->white_valid) {
		return smcube_interp_code((inst *)p, SMCUBE_INT_WHITE_CALIB);
	}

	/* Scale it to white */
	for (i = 0; i < 3; i++)
		rgb[i] *= p->sscale[i];

	a1logd(p->log, 2, "smcube_measure: Scaled RGB %f %f %f\n",rgb[0],rgb[1],rgb[2]);

	if (!p->dark_valid) {
		return smcube_interp_code((inst *)p, SMCUBE_INT_BLACK_CALIB);
	}

	/* Remove the black offset */
	for (i = 0; i < 3; i++) {
		rgb[i] = (rgb[i] - p->soff[i])/(1.0 - p->soff[i]);
		if (rgb[i] < 0.0)
			rgb[i] = 0.0;
	}
	a1logd(p->log, 2, "smcube_measure: Black offset RGB %f %f %f\n",rgb[0],rgb[1],rgb[2]);

	/* If gloss mode */
	if (p->icx == 1) {
		for (i = 0; i < 3; i++) {
			rgb[i] = (rgb[i] - p->goff[i])/(1.0 - p->goff[i]);
			if (rgb[i] < 0.0)
				rgb[i] = 0.0;
		}
		a1logd(p->log, 2, "smcube_measure: Gloss comp. RGB %f %f %f\n",rgb[0],rgb[1],rgb[2]);
	}

	a1logd(p->log, 2, "smcube_measure: RGB reflectance %f %f %f\n",rgb[0],rgb[1],rgb[2]);

#ifdef NEVER
/* Adjust for CC white target */
{
	double targ[3] = { 0.916179, 0.906579, 0.866163 };

	printf("white ref was = %f %f %f\n",cwref[0],cwref[1],cwref[2]);
	for (i = 0; i < 3; i++) {
		cwref[i] *= pow(targ[i]/rgb[i], 0.7);
	}
	printf("adjusted white ref = %f %f %f\n",cwref[0],cwref[1],cwref[2]);
}
#endif

	cube_rgb2XYZ(XYZ, rgb);

	a1logd(p->log, 2, "smcube_measure: done\n");

	return inst_ok;
}

/* ---------------------------- */

/* Convert RGB to XYZ using calibration table */
static void cube_rgb2XYZ(double *xyz, double *irgb) {
	int e;
	double rgb[3];
	int    ix[3];		/* Coordinate of cell */ 
	double co[3];		/* Coordinate offset with the grid cell */
	int    si[3];		/* co[] Sort index, [0] = smalest */

	/* Clip and apply rgb power */
	for (e = 0; e < 3; e++) {
		rgb[e] = irgb[e];
		if (rgb[e] < 0.0)
			rgb[e] = 0.0;
		if (rgb[e] > 1.0)
			rgb[e] = 1.0;
		rgb[e] = pow(rgb[e], clut.dpow);
	}

	/* We are using tetrahedral interpolation. */

	/* Compute base index into grid and coordinate offsets */
	{
		double res_1 = clut.res-1;
		int    res_2 = clut.res-2;
		int e;

		for (e = 0; e < 3; e++) {
			unsigned int x;
			double val;
			val = rgb[e] * res_1;
			if (val < 0.0) {
				val = 0.0;
			} else if (val > res_1) {
				val = res_1;
			}
			x = (unsigned int)floor(val);		/* Grid coordinate */
			if (x > res_2)
				x = res_2;
			co[e] = val - (double)x;	/* 1.0 - weight */
			ix[e] = x;					
		}
	}
	/* Do insertion sort on coordinates, smallest to largest. */
	{
		int e, f, vf;
		double v;
		for (e = 0; e < 3; e++)
			si[e] = e;						/* Initial unsorted indexes */

		for (e = 1; e < 3; e++) {
			f = e;
			v = co[si[f]];
			vf = f;
			while (f > 0 && co[si[f-1]] > v) {
				si[f] = si[f-1];
				f--;
			}
			si[f] = vf;
		}
	}
	/* Now compute the weightings, simplex vertices and output values */
	{
		int e, f;
		double w;		/* Current vertex weight */

		w = 1.0 - co[si[3-1]];		/* Vertex at base of cell */
		for (f = 0; f < 3; f++)
			xyz[f] = w * clut.table[ix[0]][ix[1]][ix[2]][f];

		for (e = 3-1; e > 0; e--) {	/* Middle verticies */
			w = co[si[e]] - co[si[e-1]];
			ix[si[e]]++;
			for (f = 0; f < 3; f++)
				xyz[f] += w * clut.table[ix[0]][ix[1]][ix[2]][f];
		}

		w = co[si[0]];
		ix[si[0]]++;				/* Far corner from base of cell */
		for (f = 0; f < 3; f++)
			xyz[f] += w * clut.table[ix[0]][ix[1]][ix[2]][f];
	}

	if (clut.islab)
		icmLab2XYZ(&icmD50_100, xyz, xyz);
	else
		icmScale3(xyz, xyz, 100.0); 	/* ??? */
}

/* =============================================================================== */
/* Calibration info save/restore */

/* The cube doesn't have an easily accessible serial number :-( */
/* So if you have more than one, you'll be sharing the same calibration !! */

int smcube_save_calibration(smcube *p) {
	int ev = SMCUBE_OK;
	int i;
	char fname[100];		/* Name */
	calf x;
	int argyllversion = ARGYLL_VERSION;
	int valid;
	int ss;

	snprintf(fname, 99, ".smcube.cal");

	if (calf_open(&x, p->log, fname, 1)) {
		x.ef = 2;
		goto done;
	}

	ss = sizeof(smcube);

	/* Some file identification */
	calf_wints(&x, &argyllversion, 1);
	calf_wints(&x, &ss, 1);

	/* Save all the calibrations */
	calf_wints(&x, &p->white_valid, 1);
	calf_wtime_ts(&x, &p->wdate, 1);
	calf_wdoubles(&x, p->sscale, 3);
	calf_wdoubles(&x, &p->ctemp, 1);

	/* Only save dark cal if it is not the fallback default */ 
	valid = p->dark_valid && !p->dark_default;
	calf_wints(&x, &valid, 1);
	calf_wtime_ts(&x, &p->ddate, 1);
	calf_wdoubles(&x, p->soff, 3);

	/* Only save gloss cal if it is not the fallback default */ 
	valid = p->gloss_valid && !p->gloss_default;
	calf_wints(&x, &valid, 1);
	calf_wtime_ts(&x, &p->gdate, 1);
	calf_wdoubles(&x, p->goff, 3);

	a1logd(p->log,3,"nbytes = %d, Checkum = 0x%x\n",x.nbytes,x.chsum);
	calf_wints(&x, (int *)(&x.chsum), 1);

	if (calf_done(&x))
		x.ef = 3;

  done:;
	if (x.ef != 0) {
		a1logd(p->log,2,"Writing calibration file failed with %d\n",x.ef);
		ev = SMCUBE_INT_CAL_SAVE;
	} else {
		a1logd(p->log,2,"Writing calibration file succeeded\n");
	}

	return ev;
}

/* Restore the all modes calibration from the local system */
int smcube_restore_calibration(smcube *p) {
	int ev = SMCUBE_OK;
	int i, j;
	char fname[100];		/* Name */
	calf x;
	int argyllversion;
	int valid;
	int ss, nbytes, chsum1, chsum2;

	snprintf(fname, 99, ".smcube.cal");

	if (calf_open(&x, p->log, fname, 0)) {
		x.ef = 2;
		goto done;
	}

	/* Last modified time */
	p->lo_secs = x.lo_secs;

	/* Do a dumy read to check the checksum, then a real read */
	for (x.rd = 0; x.rd < 2; x.rd++) {
		calf_rewind(&x);

		/* Check the file identification */
		calf_rints2(&x, &argyllversion, 1);
		calf_rints2(&x, &ss, 1);

		if (x.ef != 0
		 || argyllversion != ARGYLL_VERSION
		 || ss != (sizeof(smcube))) {
			a1logd(p->log,2,"Identification didn't verify\n");
			if (x.ef == 0)
				x.ef = 4;
			goto done;
		}

		/* Read all the calibrations */
		calf_rints(&x, &p->white_valid, 1);
		calf_rtime_ts(&x, &p->wdate, 1);
		calf_rdoubles(&x, p->sscale, 3);
		calf_rdoubles(&x, &p->ctemp, 1);
	
		calf_rints(&x, &valid, 1);
		calf_rtime_ts(&x, &p->ddate, 1);
		calf_rdoubles(&x, p->soff, 3);
		if (x.rd > 0) {
			if (valid) {
				p->dark_default = 0;
			} else {
				/* Use fallback default black offset */
				p->soff[0] = dsoff[0];
				p->soff[1] = dsoff[1];
				p->soff[2] = dsoff[2];
				p->dark_valid = 1;
				p->dark_default = 1;
			}
		}
	
		calf_rints(&x, &valid, 1);
		calf_rtime_ts(&x, &p->gdate, 1);
		calf_rdoubles(&x, p->goff, 3);
		if (x.rd > 0) {
			if (valid) {
				p->gloss_default = 0;
			} else {
				/* Use fallback default gloss offset */
				p->goff[0] = dgoff[0];
				p->goff[1] = dgoff[1];
				p->goff[2] = dgoff[2];
				p->gloss_valid = 1;
				p->gloss_default = 1;
			}
		}

		/* Check the checksum */
		chsum1 = x.chsum;
		nbytes = x.nbytes;
		calf_rints2(&x, &chsum2, 1);
	
		if (x.ef != 0
		 || chsum1 != chsum2) {
			a1logd(p->log,2,"Checksum didn't verify, bytes %d, got 0x%x, expected 0x%x\n",nbytes,chsum1, chsum2);
			if (x.ef == 0)
				x.ef = 5;
			goto done;
		}
	}

	a1logd(p->log,5,"smcube_restore_calibration done\n");
 done:;

	if (calf_done(&x))
		x.ef = 3;

	if (x.ef != 0) {
		a1logd(p->log,2,"Reading calibration file failed with %d\n",x.ef);
		ev = SMCUBE_INT_CAL_RESTORE;
	}

	return ev;
}

int smcube_touch_calibration(smcube *p) {
	int ev = SMCUBE_OK;
	char fname[100];		/* Name */
	int rv;

	snprintf(fname, 99, ".smcube.cal");

	if (calf_touch(p->log, fname)) {
		a1logd(p->log,2,"Touching calibration file time failed with\n");
		return SMCUBE_INT_CAL_TOUCH;
	}

	return SMCUBE_OK;
}


