/************************************************************************/
/* Copyright 2009-2016 MBARI											*/
/************************************************************************/
/* Summary	: Remote serial and power ports for Modbus clients			*/
/* Filename : remote.c													*/
/* Author	: Robert Herlien (rah)										*/
/* Project	: Oasis5 Mooring Controller									*/
/* Revision: 1.0														*/
/* Created	: 08/31/2016												*/
/*																			*/
/* MBARI provides this documentation and code "as is", with no warranty,	*/
/* express or implied, of its quality or consistency. It is provided without*/
/* support and without obligation on the part of the Monterey Bay Aquarium	*/
/* Research Institute to assist in its use, correction, modification, or	*/
/* enhancement. This information should not be published or distributed to	*/
/* third parties without specific written permission from MBARI.			*/
/*																			*/
/************************************************************************/
/* Modification History:												*/
/* 07apr2009 rah - created Oasis4 version								*/
/* 31aug2016 rah - created Oasis5 version from Oasis4 version			*/
/************************************************************************/

#include <mbariTypes.h>					/* MBARI type definitions		*/
#include <oasis.h>						/* OASIS controller definitions */
#include <custom.h>						/* Custom def's for deployment	*/
#include <dig_io.h>						/* OASIS I/O definitions		*/
#include <olist.h>						/* OASIS Linked List library	*/
#include <sem.h>						/* OASIS Semaphore library		*/
#include <otask.h>						/* OASIS task dispatcher		*/
#include <serial.h>						/* OASIS Serial I/O				*/		
#include <utils.h>						/* OASIS Utility routines		*/
#include <modbus.h>						/* Modbus protocol library		*/
#include <modbusio.h>					/* Modbus (serial) I/O			*/
#include <remote.h>						/* Remote I/O library			*/
#include <parse.h>						/* Generic parser declarations	*/
#include <variable.h>					/* Variable definitions			*/
#include <debug.h>

#include <istdio.h>
#include <FreeRTOS.h>					/* FreeRTOS definitions			*/
#include <semphr.h>						/* FreeRTOS semaphore functions	*/


/********************************/
/*		External Data			*/
/********************************/

Extern Errno	modErrno;
Extern MBool	blinky;


/********************************************************/
/*				Global Data								*/
/* (Global only so we can share with usrRemoteCmds.c)	*/
/********************************************************/

Global RemSerPort		*serPorts[VIRT_SER_PORTS];
Global RemIOPort		*pwrPorts[VIRT_PWR_PORTS];
Global RemIOPort		*outPorts[VIRT_OUT_PORTS];
Global RemID			remIDs[REMOTE_IDS];


/********************************/
/*		Module Local Data		*/
/********************************/

MLocal Semaphore remSem;

MLocal char *assignWarning = 
	"WARNING! Virtual %s ports %u and %u assigned to same remote port\n";


/************************************************************************/
/* Function	   : initRemoteLib											*/
/* Purpose	   : Initialize this module									*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
void initRemoteLib(void)
{
	int			i;

	memset(serPorts, 0, sizeof(serPorts));
	memset(pwrPorts, 0, sizeof(pwrPorts));
	memset(outPorts, 0, sizeof(outPorts));
	memset(remIDs,   0, sizeof(remIDs));

	for (i = 0; i < NumberOf(remIDs); i++)
		remIDs[i].busType = MB_UNASSIGNED;

	semCreateMutex(&remSem);
	semName(&remSem, "remoteSem");

} /* initRemoteLib() */


/************************************************************************/
/* Function	   : takeRemoteSem											*/
/* Purpose	   : Take the remSem semaphore								*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
void takeRemoteSem(void)
{
	semTake(&remSem);

} /* takeRemoteSem() */


/************************************************************************/
/* Function	   : giveRemoteSem											*/
/* Purpose	   : Give back the remSem semaphore							*/
/* Inputs	   : Delay ms before giving sem								*/
/* Outputs	   : None													*/
/************************************************************************/
void giveRemoteSem(Nat32 delay_ms)
{
	if (delay_ms > 5)
		task_delay_ms(delay_ms);
	semGive(&remSem);

} /* giveRemoteSem() */


/************************************************************************/
/* Function	   : findRemPwrPort											*/
/* Purpose	   : Find remote power port number from string input		*/
/* Inputs	   : String to look up										*/
/* Outputs	   : Port number or ERROR									*/
/************************************************************************/
Int16 findRemPwrPort(char *s)
{
	Int32		longNum;
	VarStruct	*vp;
	char		*name;
	Int16		i;

	if (getLongVal(s, &longNum))
	{
		if ((longNum < 0) || (longNum >= PHYS_PWR_PORTS+VIRT_PWR_PORTS))
			return(ERROR);
		return((Int16)(longNum & 0xffff));
	}

	vp = findVariable(s);
	name = (vp == NULL) ? s : vp->str;

	for (i = 0; i < VIRT_PWR_PORTS; i++)
		if (pwrPorts[i] && (strcasecmp(name, pwrPorts[i]->name) == 0))
			return(i+PHYS_PWR_PORTS);

	return(ERROR);

} /* findRemPwrPort() */


/************************************************************************/
/* Function	   : findRemOutputPort										*/
/* Purpose	   : Find remote Output port number from string input		*/
/* Inputs	   : String to look up										*/
/* Outputs	   : Port number or ERROR									*/
/************************************************************************/
Int16 findRemOutputPort(char *s)
{
	Int32		longNum;
	VarStruct	*vp;
	char		*name;
	Int16		i;

	if (getLongVal(s, &longNum))
	{
		if ((longNum < 0) || (longNum >= VIRT_OUT_PORTS))
			return(ERROR);
		return((Int16)(longNum & 0xffff));
	}

	vp = findVariable(s);
	name = (vp == NULL) ? s : vp->str;

	for (i = 0; i < VIRT_OUT_PORTS; i++)	
		if (outPorts[i] && (strcasecmp(name, outPorts[i]->name) == 0))
			return(i);

	return(ERROR);

} /* findRemOutputPort() */


/************************************************************************/
/* Function	   : getVirtSerPort											*/
/* Purpose	   : Get ptr to RemSerPort, or NULL if port invalid			*/
/* Inputs	   : Virtual serial port number								*/
/* Outputs	   : RemSerHandle or NULL									*/
/************************************************************************/
RemSerHandle getVirtSerPort(Nat32 virtPort)
{
	if ((virtPort < BASE_SER_PORTS) || (virtPort >= (PHYS_SER_PORTS+VIRT_SER_PORTS)))
		return(NULL);

	return(serPorts[virtPort-PHYS_SER_PORTS]);

} /* getVirtSerPort() */


/************************************************************************/
/* Function	   : findRemoteID											*/
/* Purpose	   : Find a remID for a ModBus Client						*/
/* Inputs	   : Bus type, Modbus bus, slave ID							*/
/* Outputs	   : RemID ptr or NULL										*/
/************************************************************************/
RemID *findRemoteID(ModBusType busType, Port_t bus, Nat16 slave)
{
	RemID		*rp;
	Nat16		i;

	for (i = 0, rp = remIDs; i < NumberOf(remIDs); i++, rp++)
		if ((rp->busType == busType) &&
		    (rp->bus == bus) && (rp->slaveID == slave))
			return(rp);
	
	return(NULL);

} /* findRemoteID() */


/************************************************************************/
/* Function	   : findRemoteSlaveType									*/
/* Purpose	   : Find RemType of specified ModBus slave					*/
/* Inputs	   : ModbusHandle from ModBusOpen, RemID ptr to fill in		*/
/* Outputs	   : None													*/
/* Comments    : Sends ModBus msg to slave, fills in rp->remType		*/
/*               Caller already owns appropriate serial and modbus semaphores*/
/************************************************************************/
void findRemoteSlaveType(ModBusHandle mh, RemID *rp)
{
	/* Uses rp->remType to hold BOARD_TYPE field from slave, then changes*/
	/* it to RemType enum.												*/
	/* Note that 4 fields of RemID must be declared in correct order	*/
	/* for the readHoldingRegs() call to fill them in correctly.		*/

	if ((mh != NULL) && (rp != NULL))
	{
		if (readHoldingRegsOwnSem(mh, rp, BoardTypeReg, 4, &rp->remType) == OK)
		{
			switch(rp->remType)
			{
			  case 0: rp->remType = RemOrigChamber;
				  break;			/* Orig chamber returns all zeros	*/
			  case 1: rp->remType = RemChamber2; break;
			  case 2: rp->remType = RemFOCESN; break;
			  case 3: rp->remType = RemO5Expander; break;
			  case 4: rp->remType = RemO5DB; break;
			  default: rp->remType = RemUnknown; break;
			}
		}
		else
			rp->remType = RemOrigChamber;

		if (rp->remType == RemOrigChamber)
		{
			rp->nSer = CHAMBER_SER_PORTS;
			rp->nPwr = CHAMBER_PWR_PORTS;
			rp->nIO  = CHAMBER_OUT_PORTS;
		}
	}
}


/************************************************************************/
/* Function	   : assignRemoteID											*/
/* Purpose	   : Find or allocate a remID for a ModBus Client			*/
/* Inputs	   : Bus type, Modbus bus, slave ID							*/
/* Outputs	   : RemID ptr or NULL										*/
/************************************************************************/
RemID *assignRemoteID(ModBusType busType, Port_t bus, Nat16 slave)
{
	RemID		*rp;
	ModBusHandle mh;
	Nat16		i;

	if ((rp = findRemoteID(busType, bus, slave)) != NULL)
		return(rp);

	for (i = 0, rp = remIDs; i < NumberOf(remIDs); i++, rp++)
		if (rp->busType == MB_UNASSIGNED)	/* New controller.	Find a blank remID	*/
		{
			memset(rp, 0, sizeof(RemID));
			rp->busType = busType;
			rp->bus = bus;
			rp->slaveID = slave;
			rp->remType = RemUnknown;		/* Init remType to Unknown	*/

//			if ((mh = modBusOpen(rp)) != NULL)	/* Force remID discovery	*/
//				modBusClose(mh);

			return(rp);
		}

	return(NULL);

} /* assignRemoteID() */


/************************************************************************/
/* Function	   : assignVirtSerial										*/
/* Purpose	   : Assign a virtual serial port							*/
/* Inputs	   : Virtual port num, identifiers for remote port			*/
/* Outputs	   : OK or PORT_CONFLICT									*/
/************************************************************************/
Errno assignVirtSerial(char *name, Nat32 virtPort, ModBusType busType, Nat16 bus,
					   Nat16 slave, Nat16 port, Flt32 mult, Nat32 surgeDelay)
{
	RemSerPort	*rp;
	RemID		*idp;
	Nat16		i;

	if ((getVirtSerPort(virtPort) != NULL) || (bus >= PIC_UARTS) ||
		(slave <= 0) || (slave > 255))
		return(PORT_CONFLICT);

	if (((idp = assignRemoteID(busType, bus, slave)) == NULL) || (port >= MAX_SER_PER_SLAVE))
		return(PORT_CONFLICT);

	if ((rp = malloc(sizeof(RemSerPort))) == NULL)
		return(PORT_ERROR);

	memset(rp, 0, sizeof(RemSerPort));

	if (semCreateMutex(&rp->sem) == NULL)
		return(ERROR);
	
	isnprintf(rp->sem.sem_name, SEM_NAMELEN, "SerSem%02u", virtPort);

	rp->id = idp;
	rp->port = port;
	if (name)
		strncpy(rp->name, name, RNAME_SIZE);

	serPorts[virtPort-PHYS_SER_PORTS] = rp;

	for (i = 0; i < VIRT_SER_PORTS; i++)
	{
		if (serPorts[i] && (i != (virtPort-PHYS_SER_PORTS)) && serPorts[i]->id &&
			(serPorts[i]->id->busType != RemUnassigned) && (serPorts[i]->id->bus == bus) &&
			(serPorts[i]->id->slaveID == slave) && (serPorts[i]->port == port))
			xprintf(assignWarning, "serial", i+PHYS_SER_PORTS, virtPort);
	}
	
	return(OK);
}

/************************************************************************/
/* Function	   : assignVirtPwr											*/
/* Purpose	   : Assign a virtual power port							*/
/* Inputs	   : Virtual port num, identifiers for remote port			*/
/* Outputs	   : OK or PORT_CONFLICT									*/
/************************************************************************/
Errno assignVirtPwr(char *name, Nat32 virtPort, ModBusType busType,  Nat16 bus,
					Nat16 slave, Nat16 coil, Flt32 mult, Nat32 surgeDelay)
{
	RemIOPort	*rp;
	RemID		*idp;
	Nat16		i;

	if ((virtPort < PHYS_PWR_PORTS) || (virtPort >= (PHYS_PWR_PORTS+VIRT_PWR_PORTS)) ||
		(slave <= 0) || (slave > 255) || (pwrPorts[virtPort-PHYS_PWR_PORTS] != NULL))
		return(PORT_CONFLICT);				/* If the RemID is filled in, port is in use*/

	if (((idp = assignRemoteID(busType, bus, slave)) == NULL) || (coil >= MAX_PWR_PER_SLAVE))
		return(PORT_CONFLICT);

	if ((rp = malloc(sizeof(RemIOPort))) == NULL)
		return(PORT_ERROR);

	memset(rp, 0, sizeof(RemIOPort));

	rp->id = idp;
	rp->coil = coil;
	if (name)
		strncpy(rp->name, name, RNAME_SIZE);
	rp->multiplier = mult;
	rp->surgeDelayMs = surgeDelay;
	pwrPorts[virtPort-PHYS_PWR_PORTS] = rp;

	for (i = 0; i < VIRT_PWR_PORTS; i++)
	{
		if (pwrPorts[i] && (i != (virtPort-PHYS_PWR_PORTS)) && pwrPorts[i]->id && 
			(pwrPorts[i]->id->busType != RemUnassigned) && (pwrPorts[i]->id->bus == bus) &&
			(pwrPorts[i]->id->slaveID == slave) && (pwrPorts[i]->coil == coil))
			xprintf(assignWarning, "power", i+PHYS_PWR_PORTS, virtPort);
	}
	
	return(OK);
}

/************************************************************************/
/* Function	   : assignVirtOutput										*/
/* Purpose	   : Assign a virtual output port							*/
/* Inputs	   : Virtual port num, identifiers for remote port			*/
/* Outputs	   : OK or PORT_CONFLICT									*/
/************************************************************************/
Errno assignVirtOutput(char *name, Nat32 virtPort, ModBusType busType, Nat16 bus,
					   Nat16 slave, Nat16 coil, Flt32 mult, Nat32 surgeDelay)
{
	RemIOPort	*rp;
	RemID		*idp;
	Nat16		i;

	if ((virtPort >= VIRT_OUT_PORTS) || (slave <= 0) || (slave > 255) ||
		(outPorts[virtPort] != NULL))
		return(PORT_CONFLICT);				/* If the RemID is filled in, port is in use*/

	if (((idp = assignRemoteID(busType, bus, slave)) == NULL) || (coil >= MAX_OUTPUT_PER_SLAVE))
		return(PORT_CONFLICT);

	if ((rp = malloc(sizeof(RemIOPort))) == NULL)
		return(PORT_ERROR);

	memset(rp, 0, sizeof(RemIOPort));

	rp->id = idp;
	rp->coil = coil;
	if (name)
		strncpy(rp->name, name, RNAME_SIZE);
	rp->multiplier = mult;
	rp->surgeDelayMs = surgeDelay;
	outPorts[virtPort] = rp;

	for (i = 0; i < VIRT_OUT_PORTS; i++)
	{
		if (outPorts[i] && (i != virtPort) && outPorts[i]->id &&
			(outPorts[i]->id->busType != RemUnassigned) && (outPorts[i]->id->bus == bus)
			&& (outPorts[i]->id->slaveID == slave) && (outPorts[i]->coil == coil))
			xprintf(assignWarning, "output", i, virtPort);
	}
	
	return(OK);

}

/************************************************************************/
/* Function	   : openRemSerHandle										*/
/* Purpose	   : Local func to manipulate RemSerPort for an open		*/
/* Inputs	   : RemSerPort, boolean to open async						*/
/* Outputs	   : OK or ERROR											*/
/* Comment	   : If async==TRUE and semaphore not avail, return PORT_IN_USE*/
/************************************************************************/
Errno openRemSerHandle(RemSerHandle rsh, MBool async)
{
	if ((rsh == NULL) || (rsh->id == NULL) ||
		((rsh->handle = modBusOpen(rsh->id)) == NULL))
		return(ERROR);

	if (async && (xSemaphoreGetMutexHolder(rsh->sem.semHandle) != NULL))
		return(modErrno = PORT_IN_USE);

	semTake(&rsh->sem);
	rsh->owner = xTaskGetCurrentTaskHandle();
	rsh->bufPtr = rsh->buf;
	rsh->bufChars = 0;

	if (rsh->id->remType == RemOrigChamber)
		modErrno = writeOneCoil(rsh->handle, rsh->id, TxEnableChamber(rsh->port), TRUE);
	else
		modErrno = writeOneCoil(rsh->handle, rsh->id, TxEnable(rsh->port), TRUE);

	if (modErrno != OK)
		remSerClose(rsh);

	return(modErrno);

} /* openRemSerHandle() */


/************************************************************************/
/* Function	   : findRemSerHandle										*/
/* Purpose	   : Find a remote serial port handle						*/
/* Inputs	   : bus, slave, port										*/
/* Outputs	   : RemSerHandle, or NULL if error							*/
/************************************************************************/
RemSerHandle findRemSerHandle(ModBusType busType, Nat16 bus, Nat16 slave, Nat16 port)
{
	RemID		*idp;
	RemSerPort	*rp;
	Nat32		i;

	if (((idp = assignRemoteID(busType, bus, slave)) == NULL) ||
		(port >= idp->nSer))
	{
		modErrno = PORT_CONFLICT;
		return(NULL);
	}

	for (i = 0; i < VIRT_SER_PORTS; i++)
	{
		rp = serPorts[i];
		if (rp && rp->id && (rp->id->busType == busType) &&
			(rp->id->bus == bus) && (rp->id->slaveID == slave) &&
			(rp->port == port))
			return(rp);
	}

	/* No matching RemSerPort.  Assign a new one		*/
	for (i = VIRT_SER_PORTS-1; i; i--)
	{
		if ((rp = serPorts[i]) == NULL)
		{
			if (assignVirtSerial("tmpSer", i, busType, bus, slave, port, 0.0, 0) == OK)
				return(getVirtSerPort(i));
			else
				return(NULL);
		}
	}

	modErrno = PORT_ERROR;
	return(NULL);

} /* findRemSerHandle() */


/************************************************************************/
/* Function	   : remSerOpen												*/
/* Purpose	   : Open a remote serial port								*/
/* Inputs	   : bus, slave, port, boolean TRUE to open async			*/
/* Outputs	   : RemSerHandle, or NULL if error							*/
/* Comment	   : If async==TRUE and semaphore not avail, return NULL	*/
/************************************************************************/
RemSerHandle remSerOpen(ModBusType busType, Nat16 bus, Nat16 slave, Nat16 port, MBool async)
{
	RemSerHandle rsh;

	if ((rsh = findRemSerHandle(busType, bus, slave, port)) == NULL)
		return(NULL);

	return((openRemSerHandle(rsh, async) == OK) ? rsh : NULL);

} /* remSerOpen() */


/************************************************************************/
/* Function	   : remOpenVirtSer											*/
/* Purpose	   : Open a virtual serial port								*/
/* Inputs	   : Virtual serial port number, boolean to open async		*/
/* Outputs	   : Errno													*/
/* Comment	   : If async==TRUE and semaphore not avail, return PORT_IN_USE*/
/************************************************************************/
Errno remOpenVirtSer(Nat32 virtPort, MBool async)
{
	RemSerPort	*rp;

	if ((rp = getVirtSerPort(virtPort)) == NULL)
		return(PORT_CONFLICT);

	return(openRemSerHandle(rp, async));

} /* remOpenVirtSer() */


/************************************************************************/
/* Function	   : remSerClose											*/
/* Purpose	   : Close a remote serial port								*/
/* Inputs	   : RemSerHandle											*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remSerClose(RemSerHandle rsh)
{
	Errno		rtn;

	if ((rsh == NULL) || (rsh->handle == NULL) || 
		(xSemaphoreGetMutexHolder(rsh->sem.semHandle) != xTaskGetCurrentTaskHandle()))
		return(ERROR);

	if (rsh->id->remType == RemOrigChamber)
		writeOneCoil(rsh->handle, rsh->id, TxEnableChamber(rsh->port), FALSE);
	else
		writeOneCoil(rsh->handle, rsh->id, TxEnable(rsh->port), FALSE);

	rtn = modBusClose(rsh->handle);
	rsh->handle = NULL;
	rsh->owner = NULL;
	semGive(&rsh->sem);
	return(rtn);

} /* remSerClose() */


/************************************************************************/
/* Function	   : remCloseVirtSer										*/
/* Purpose	   : Close a virtual serial port							*/
/* Inputs	   : RemSerHandle											*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remCloseVirtSer(Nat32 virtPort)
{
	RemSerPort	*rp;

	if ((rp = getVirtSerPort(virtPort)) == NULL)
		return(PORT_CONFLICT);

	return(remSerClose(rp));

} /* remCloseVirtSer() */


/************************************************************************/
/* Function	   : remSerInit												*/
/* Purpose	   : Set baud rate and mode for a remote serial port		*/
/* Inputs	   : RemSerHandle, baud rate, mode							*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remSerInit(RemSerHandle rsh, Nat32 baud, Nat32 mode)
{
	Nat16		regs[2];

	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(ERROR);

	regs[0] = baud >> 2;

	switch(mode & PTY_MASK)
	{
	  case ODD_PTY:
		  regs[1] = REM_ODD_PTY;
		  break;

	  case EVEN_PTY:
		  regs[1] = REM_EVEN_PTY;
		  break;
		
	  case NO_PTY:
		  regs[1] = REM_NO_PTY;
		  break;
	}

	if (mode & STOP2)
		regs[1] |= REM_STOP2;

	modErrno = writeRegs(rsh->handle, rsh->id, BaudReg(rsh->port),
						 2, regs);

	if (mode & BIT7)
		return(BAD_PARM);

	return(modErrno);

} /* remSerInit() */


/************************************************************************/
/* Function	   : remVirtSerInit											*/
/* Purpose	   : Set baud rate for a virtual serial port				*/
/* Inputs	   : Virtual ser port number, baud rate, mode				*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remVirtSerInit(Nat32 virtPort, Nat32 baud, Nat32 mode)
{
	RemSerPort	*rp;

	if ((rp = getVirtSerPort(virtPort)) == NULL)
		return(PORT_CONFLICT);

	return(remSerInit(rp, baud, mode));

} /* remVirtSerInit() */


/************************************************************************/
/* Function	   : remPower												*/
/* Purpose	   : Turn on/off a remote power bit							*/
/* Inputs	   : Remote Power number, boolean							*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remPower(Nat32 pwrnum, MBool onoff)
{
	RemIOPort			*rp;
	ModBusHandle		mh;
	Errno				rtn;

	if ((pwrnum < PHYS_PWR_PORTS) || (pwrnum >= (PHYS_PWR_PORTS+VIRT_PWR_PORTS)))
		return(ERROR);

	if (((rp = pwrPorts[pwrnum - PHYS_PWR_PORTS]) == NULL) || (rp->id == NULL))
		return(ERROR);

	if ((mh = modBusOpen(rp->id)) == NULL)
		return(ERROR);

	if (onoff)
		takeRemoteSem();

	if (rp->id->remType == RemOrigChamber)
		rtn = writeOneCoil(mh, rp->id, PowerBitChamber(rp->coil), onoff);
	else
		rtn = writeOneCoil(mh, rp->id, PowerBit(rp->coil), onoff);

	if (rtn == OK)
		rp->coilState = onoff;

	if (onoff)
		giveRemoteSem(rp->surgeDelayMs);
	
	modBusClose(mh);
	return(rtn);

} /* remPower() */


/************************************************************************/
/* Function	   : remOutput												*/
/* Purpose	   : Turn on/off a remote output bit						*/
/* Inputs	   : Remote Output number, boolean							*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remOutput(Nat32 outnum, MBool onoff)
{
	RemIOPort			*rp;
	ModBusHandle		mh;
	Errno				rtn;

	if ( outnum >= VIRT_OUT_PORTS )
		return(ERROR);

	if (((rp = outPorts[outnum]) == NULL) || (rp->id == NULL))
		return(ERROR);

	if ((mh = modBusOpen(rp->id)) == NULL)
		return(ERROR);

	if (onoff)
		takeRemoteSem();

	rtn = writeOneCoil(mh, rp->id, OutputBit(rp->coil), onoff);

	if (rtn == OK)
		rp->coilState = onoff;

	if (onoff)
		giveRemoteSem(rp->surgeDelayMs);

	modBusClose(mh);
	return(rtn);

} /* remOutput() */


/************************************************************************/
/* Function	   : remIsPowerOn											*/
/* Purpose	   : See if any power bits set on a given ModBus			*/
/* Inputs	   : ModBus number											*/
/* Outputs	   : TRUE if any power bits set, else FALSE					*/
/* Comments    : Only called for Serial ModBus							*/
/************************************************************************/
Errno remIsPowerOn(Port_t bus)
{
	RemIOPort			*rp;
	Nat16				i;

	for (i = 0; i < NumberOf(pwrPorts); i++)
	{
		rp = pwrPorts[i];
		if (rp && rp->id && (rp->id->bus == bus) && rp->coilState)
			return(TRUE);
	}

	return(FALSE);

} /* remIsPowerOn() */


/************************************************************************/
/* Function	   : remIsOutputOn											*/
/* Purpose	   : See if any output bits set on a given ModBus			*/
/* Inputs	   : ModBus number											*/
/* Outputs	   : TRUE if any output bits set, else FALSE				*/
/************************************************************************/
Errno remIsOutputOn(Port_t bus)
{
	RemIOPort			*rp;
	Nat16				i;

	for (i = 0; i < NumberOf(pwrPorts); i++)
	{
		rp = outPorts[i];
		if (rp && rp->id && (rp->id->bus == bus) && rp->coilState)
			return(TRUE);
	}

	return(FALSE);

} /* remIsOutputOn() */


/************************************************************************/
/* Function	   : remOff													*/
/* Purpose	   : Turn off ALL remote outputs and power bits				*/
/* Inputs	   : None													*/
/* Outputs	   : OK														*/
/************************************************************************/
Errno remOff(Void)
{
	RemIOPort			*rp;
	ModBusHandle		mh;
	Nat16				i;

	for (i = 0; i < NumberOf(pwrPorts); i++)
	{
		rp = pwrPorts[i];
		if (rp && rp->id && ((mh = modBusOpen(rp->id)) != NULL))
		{
			if (rp->id->remType == RemOrigChamber)
				writeOneCoil(mh, rp->id, PowerBitChamber(rp->coil), FALSE);
			else
				writeOneCoil(mh, rp->id, PowerBit(rp->coil), FALSE);

			rp->coilState = FALSE;
			modBusClose(mh);
		}
	}

	for (i = 0; i < NumberOf(outPorts); i++)
	{
		rp = outPorts[i];
		if (rp && rp->id && ((mh = modBusOpen(rp->id)) != NULL))
		{
			writeOneCoil(mh, rp->id, OutputBit(rp->coil), FALSE);
			rp->coilState = FALSE;
			modBusClose(mh);
		}
	}

	return(OK);

} /* remOff() */


/************************************************************************/
/* Function	   : remOutPulseOn											*/
/* Purpose	   : Pulse on remote output bit								*/
/* Inputs	   : Remote Output number, Number of ms to pulse on			*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remOutPulseOn(Nat32 bitNum, Flt32 sec)
{
	RemIOPort			*rp;
	ModBusHandle		mh;
	Errno				rtn;
	Nat32				msOn, surgeMs;

	if ( bitNum >= VIRT_OUT_PORTS )
		return(ERROR);

	if (((rp = outPorts[bitNum]) == NULL) || (rp->id == NULL))
		return(ERROR);

	if ((msOn = (Nat32)(sec * 1000.)) < 2)
		return(OK);

	if ((mh = modBusOpen(rp->id)) == NULL)
		return(ERROR);

	takeRemoteSem();
	surgeMs = rp->surgeDelayMs;
	rtn = writeOneCoil(mh, rp->id, OutputBit(rp->coil), TRUE);

	if (surgeMs <= msOn)
	{
		giveRemoteSem(surgeMs);
		task_delay_ms(msOn - surgeMs);
		rtn |= writeOneCoil(mh, rp->id, OutputBit(rp->coil), FALSE);
	}
	else
	{
		task_delay_ms(msOn);
		rtn |= writeOneCoil(mh, rp->id, OutputBit(rp->coil), FALSE);
		giveRemoteSem(surgeMs - msOn);
	}

	rp->coilState = FALSE;
	modBusClose(mh);
	return(rtn);

} /* remOutPulseOn() */


/************************************************************************/
/* Function	   : remOutPulseOnByVolume									*/
/* Purpose	   : Pulse on remote output bit								*/
/* Inputs	   : Remote Output number, volume to pulse on				*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remOutPulseOnByVolume(Nat32 bitNum, Flt32 vol)
{
	RemIOPort			*rp;

	if ( bitNum >= VIRT_OUT_PORTS )
		return(ERROR);

	if (((rp = outPorts[bitNum]) == NULL) || (rp->id == NULL))
		return(ERROR);

	return(remOutPulseOn(bitNum, vol * rp->multiplier));

} /* remOutPulseOnByVolume() */


/************************************************************************/
/* Function	   : remOutPulseOff											*/
/* Purpose	   : Pulse off remote output bit							*/
/* Inputs	   : Remote Output number, Number of ms to pulse off		*/
/* Outputs	   : OK or ERROR											*/
/************************************************************************/
Errno remOutPulseOff(Nat32 bitNum, Flt32 sec)
{
	RemIOPort			*rp;
	ModBusHandle		mh;
	Errno				rtn;
	Nat32				msOn;

	if ( bitNum >= VIRT_OUT_PORTS )
		return(ERROR);

	if (((rp = outPorts[bitNum]) == NULL) || (rp->id == NULL))
		return(ERROR);

	if ((msOn = (Nat32)(sec * rp->multiplier * 1000.)) < 2)
		return(OK);

	if ((mh = modBusOpen(rp->id)) == NULL)
		return(ERROR);

	takeRemoteSem();
	rtn = writeOneCoil(mh, rp->id, OutputBit(rp->coil), FALSE);
	task_delay_ms(msOn);

	rtn |= writeOneCoil(mh, rp->id, OutputBit(rp->coil), TRUE);
	giveRemoteSem(rp->surgeDelayMs);

	rp->coilState = TRUE;
	modBusClose(mh);
	return(rtn);

} /* remOutPulseOff() */


/************************************************************************/
/* Function	   : remSerRead												*/
/* Purpose	   : Read a remote serial port								*/
/* Inputs	   : Remote serial handle, buffer ptr, length				*/
/* Outputs	   : Number of chars read									*/
/************************************************************************/
Nat32 remSerRead(RemSerHandle rsh, char *buffer, Nat32 len)
{
	Nat32		bytesRead;
	char		*p;
	Nat16		readLen, i, *q;

	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(0);

	for (p = buffer, bytesRead = 0; bytesRead < len; )
	{
		if (rsh->bufChars > 0)
		{
			*p++ = *(rsh->bufPtr)++;
			rsh->bufChars--;
			bytesRead++;
		}
		else
		{
			readLen = (len+1)/2;
			if (readLen > SERREG_SIZE)
				readLen = SERREG_SIZE;
			if (readLen < MIN_READ)
				readLen = MIN_READ;

			modErrno = readHoldingRegs(rsh->handle, rsh->id, RxQReg(rsh->port),
									   readLen+1, &rsh->rxQueued);
			if ((modErrno != OK) || (rsh->rxQueued == 0))
				return(bytesRead);
			rsh->bufPtr = rsh->buf;
			rsh->bufChars = (rsh->rxQueued >= 2*readLen) ? 
								2*readLen : rsh->rxQueued;

			/* May need to byte-swap the 16-bit registers to restore byte ordering*/
			q = (Nat16 *)(rsh->buf);
			for (i = 0; i < readLen; i++, q++)
				*q = mbtohs(*q);
		}
	}

	return(bytesRead);

} /* remSerRead() */


/************************************************************************/
/* Function	   : remSerWrite											*/
/* Purpose	   : Write a remote serial port								*/
/* Inputs	   : Remote serial handle, buffer ptr, length				*/
/* Outputs	   : Number of chars written								*/
/************************************************************************/
Nat32 remSerWrite(RemSerHandle rsh, const char *buffer, Nat32 len)
{
	Nat32		bytesWritten;
	const char	*p;
	Nat16		port, writeLen, writeQueuedReg, writeQueued, zeroCount;

	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(0);

	port = rsh->port;
	bytesWritten = 0;
	zeroCount = 0;
	for (p = buffer; bytesWritten < len; )
	{
		writeLen = len - bytesWritten;
		if (writeLen > SERBUF_SIZE)
			writeLen = SERBUF_SIZE;

		modErrno = writeReadSpecial(rsh->handle, rsh->id, TxQueuedReg(port),
									1, TxReqReg(port), 1, &writeLen, 
									(writeLen+1)/2, p, &writeQueuedReg);
		if (modErrno != OK)
			return(bytesWritten);

		writeQueued = writeQueuedReg & 0xff;
		bytesWritten += writeQueued;
		p += writeQueued;

		if (writeQueued < writeLen)
			task_delay(TICKS_PER_SECOND/20);

		if (writeQueued == 0)
		{
			if (++zeroCount > 10)
				return(bytesWritten);
		}
		else
			zeroCount = 0;
	}

	return(bytesWritten);

} /* remSerWrite() */


/************************************************************************/
/* Function	   : remSerDrain											*/
/* Purpose	   : Wait for serial output stream to finish sending		*/
/* Inputs	   : RemSerHandle, ticks to wait							*/
/* Outputs	   : Errno													*/
/************************************************************************/
Errno remSerDrain(RemSerHandle rsh, Nat32 tmout)
{
	Nat32		tmRemain;
	Nat16		status;
	Errno		rtn;

	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(ERROR);

	for (tmRemain = tmout; tmRemain > 0; tmRemain -= 10)
	{
		if ((rtn = readHoldingRegs(rsh->handle, rsh->id, SerTxStatusReg,
								   1, &status)) != OK)
			return(rtn);

		if (rsh->id->remType == RemOrigChamber)
		{
			if (TxBusyChamber(status, rsh->port) == 0)
				return(OK);
		}
		else
		{
			if (TxBusy(status, rsh->port) == 0)
				return(OK);
		}

		task_delay( TICKS_PER_SECOND/4 );
	}

	return(OK);

} /* remSerDrain() */


/************************************************************************/
/* Function	   : remSerIFlush											*/
/* Purpose	   : Flush remote serial input stream						*/
/* Inputs	   : RemSerHandle											*/
/* Outputs	   : Errno													*/
/************************************************************************/
Errno remSerIFlush(RemSerHandle rsh)
{
	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(ERROR);

	rsh->bufChars = 0;
	return(writeOneReg(rsh->handle, rsh->id, RxCtlReg(rsh->port),
					   REM_RX_FLUSH));

} /* remSerIFlush() */


/************************************************************************/
/* Function	   : remSerOFlush											*/
/* Purpose	   : Flush remote serial output stream						*/
/* Inputs	   : RemSerHandle											*/
/* Outputs	   : Errno													*/
/************************************************************************/
Errno remSerOFlush(RemSerHandle rsh)
{
	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(ERROR);

	return(writeOneReg(rsh->handle, rsh->id, TxCtlReg(rsh->port),
					   REM_TX_FLUSH));

} /* remSerOFlush() */


/************************************************************************/
/* Function	   : remSerRxCount											*/
/* Purpose	   : Get number of queued Rx characters						*/
/* Inputs	   : RemSerHandle											*/
/* Output	   : Number of queued Rx characters							*/
/************************************************************************/
Int16 remSerRxCount(RemSerHandle rsh)
{
	return((rsh == NULL) ? 0 : rsh->bufChars);

} /* remSerRxCount() */


/************************************************************************/
/* Function	   : remSerBreak											*/
/* Purpose	   : Send a break character to remote output stream			*/
/* Inputs	   : RemSerHandle, break time in ms							*/
/* Outputs	   : Errno													*/
/************************************************************************/
Errno remSerBreak(RemSerHandle rsh, Nat16 breakTime)
{
	if ((rsh == NULL) || (rsh->id == NULL) || (rsh->handle == NULL))
		return(ERROR);
	
	/* set break bit */
	writeOneReg(rsh->handle, rsh->id, TxBreakReg(rsh->port), REM_BREAK);

	/* wait for break delay */
	task_delay_ms(breakTime);

	/* clear break bit */
	return (writeOneReg(rsh->handle, rsh->id, TxBreakReg(rsh->port), 0));

} /* remSerBreak() */


/************************************************************************/
/* Function	   : remTaskExit											*/
/* Purpose	   : Close remote ports owned by exiting task				*/
/* Inputs	   : Task Descriptor ptr of exiting task					*/
/* Outputs	   : None													*/
/************************************************************************/
void remTaskExit(TaskHandle_t *td)
{
	RemSerPort	*rp;
	Nat16		i;

	for (i = 0; i < VIRT_SER_PORTS; i++)
	{
		rp = serPorts[i];
		if (rp && rp->id && (rp->id->busType != MB_UNASSIGNED) &&
			rp->owner == xTaskGetCurrentTaskHandle())
		{
			remSerClose(rp);
		}
	}

} /* remTaskExit() */


/************************************************************************/
/* Function	   : setAllSlaves											*/
/* Purpose	   : Set coil in all chamber controllers					*/
/* Inputs	   : Coil number, coil state								*/
/* Outputs	   : None													*/
/************************************************************************/
MLocal void setAllSlaves(Nat16 coil, MBool state)
{
	Nat16			i;
	RemID			*idp;
	ModBusHandle	mh;

	for (i = 0, idp = remIDs; i < NumberOf(remIDs); i++, idp++)
		if ((idp->busType == MB_SER) && (idp->remType == RemOrigChamber) &&
			((mh = modBusOpen(idp)) != NULL))
		{
			writeOneCoil(mh, idp, coil, state);
			modBusClose(mh);
		}

} /* setAllSlaves() */


/************************************************************************/
/* Function	   : remErrorThread											*/
/* Purpose	   : Thread that blinks remote LEDs to indicate error		*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
MLocal void remErrorThread(void *p)
{
	while(TRUE)
	{
		setAllSlaves(BlinkyDebugCoil, FALSE);
		setAllSlaves(BlinkyStateCoil, TRUE);
		task_delay(TICKS_PER_SECOND/10);		
		setAllSlaves(BlinkyStateCoil, FALSE);
		task_delay(TICKS_PER_SECOND/10);		

		setAllSlaves(BlinkyStateCoil, TRUE);
		task_delay(TICKS_PER_SECOND/10);		
		setAllSlaves(BlinkyStateCoil, FALSE);
		task_delay(TICKS_PER_SECOND/10);		

		setAllSlaves(BlinkyStateCoil, TRUE);
		task_delay(TICKS_PER_SECOND/10);		
		setAllSlaves(BlinkyStateCoil, FALSE);
		task_delay(TICKS_PER_SECOND/10);		

		delay_secs(2);
	}

} /* remErrorThread() */


/************************************************************************/
/* Function	   : remSetError											*/
/* Purpose	   : Create thread that blinks remote LEDs to indicate error*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
void remSetError()
{
	Nat32			i;
	RemID			*idp;
	ModBusHandle	mh;
	MBool			anyChambers = FALSE;

	for (i = 0, idp = remIDs; i < NumberOf(remIDs); i++, idp++)
		if ((idp->busType == MB_SER) && (idp->remType != RemUnassigned))
		{
			if (idp->remType == RemOrigChamber)
				anyChambers = TRUE;
			else
				if ((mh = modBusOpen(idp)) != NULL)
				{
					writeOneReg(mh, idp, BlinkyReg, BLINKY_ERROR_MODE);
					modBusClose(mh);
				}
		}

	if (anyChambers)
		task_create(remErrorThread, NULL, "ErrorBlinky", 
					MISC_STACK_WORDS, SCHED_PRIO);

} /* remSetError() */
