/************************************************************************/
/* Copyright 2009 MBARI													*/
/************************************************************************/
/* Summary	: I/O Functions for ModBus									*/
/* Filename : modbusio.c												*/
/* Author	: Robert Herlien (rah)										*/
/* Project	: Respirometers												*/
/* Revision: 1.0														*/
/* Created	: 03/03/2009												*/
/*																			*/
/* 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:												*/
/* 3mar2009 rah - created												*/
/************************************************************************/
/* Serial I/O to ModBus serial expanders/slaves is serialized among all	*/
/* four serial ports, and protected by MUTEX modSerSem.  Because of 	*/
/* this mutual exclusion, we can share an I/O buffer, modSerData, and 	*/
/* PIC timer 3 for detecting the end of ModBus responses.				*/
/* In fact, it would be possible to relax the mutual exclusion a bit,	*/
/* and run 2 or 3 serial ports in parallel.  It's not possible to run	*/
/* all 4 in parallel, because there aren't enough PIC timers.  But it 	*/
/* would be possible, for example, to mutex 1&2 and 3&4, running two	*/
/* ports simultaneously.  This would require 2 Mutexes, 2 I/O buffers,	*/
/* and 2 timers. Right now we have much more throughput than we need,	*/
/* but this is a  way to increase throughput if needed in the future.	*/
/************************************************************************/

#include <mbariTypes.h>					/* MBARI type definitions		*/
#include <mbariConst.h>					/* MBARI constants				*/
#include <oasis.h>						/* OASIS controller definitions */
#include <custom.h>						/* Custom def's for deployment	*/
#include <dig_io.h>						/* OASIS digital 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 <modbus.h>						/* Modbus protocol library		*/
#include <modbusio.h>					/* Modbus (serial) I/O			*/
#include <modspi.h>						/* Modbus over SPI				*/
#include <remote.h>						/* Remote I/O library			*/
#include <utils.h>						/* OASIS Utility routines		*/
#include <syslog.h>						/* Respirometer system logger	*/
#include <debug.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


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

Extern RemID	remIDs[];


/********************************/
/*		Global Data				*/
/********************************/

Global Errno		modErrno;

/* Single I/O buffer because ModBus Serial I/O is protected by modSerSem*/
Global volatile ModBusIO	modSerData;


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

MLocal Semaphore		modSerSem;
MLocal Semaphore		modSpiSem;
MLocal ModBusStruct		modBusSer[PIC_UARTS];


/* Forward Declarations			*/
MLocal void modSerIdle(Port_t bus);
inline void stopModSerTimer(void);


/************************************************************************/
/* Function	   : initModBus												*/
/* Purpose	   : Initialize this module									*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
void initModBus(Void)
{
	memset(modBusSer, 0, sizeof(modBusSer));
	memset((void *)&modSerData, 0, sizeof(modSerData));

	semCreateMutex(&modSerSem);					/* Create mutex sems	*/
	semCreateMutex(&modSpiSem);
	semName(&modSerSem, "ModSerSem");
	semName(&modSpiSem, "ModSpiSem");

	modSpiInit();

} /* initModBus() */


/************************************************************************/
/* Function    : modSerIdle                                             */
/* Purpose     : Go to Idle Serial ModBus state							*/
/* Inputs      : None                                                   */
/* Outputs     : None                                                   */
/************************************************************************/
MLocal void modSerIdle(Port_t bus)
{
    modSerData.state = MB_IDLE;			/* Show Idle state              */
    stopModSerTimer();					/* Turn off Modbus timer        */
	setSerModbus(bus, FALSE);			/* Re-init ser modes, etc		*/
}


/************************************************************************/
/* Function    : stopModSerTimer                                        */
/* Purpose     : Turn off Modbus Timer                                  */
/* Inputs      : None                                                   */
/* Outputs     : None                                                   */
/************************************************************************/
inline void stopModSerTimer(void)
{
    TMR3 = 0x0000;                      /* clear timer count            */
    IEC0CLR = _IEC0_T3IE_MASK;          /* Turn off interrupts          */
    IFS0CLR = _IFS0_T3IF_MASK;          /* clear interrupt flag         */
}

ModBusStruct *getSerModStruct(void)
{
	return(modBusSer);
}


/************************************************************************/
/* Function	   : getModbusPort											*/
/* Purpose	   : Get MUTEX for Modbus serial port						*/
/* Inputs	   : ModBusHandle											*/
/* Outputs	   : OK or Error Number										*/
/************************************************************************/
Errno getModbusPort(ModBusHandle mh)
{
	if (mh->busType == MB_SER)
	{
		if (getSerialSemTmout(mh->port, MODSEM_TMOUT) != OK)
			return(PORT_IN_USE);
		if (semTakeTmout(&modSerSem, MODSEM_TMOUT) != OK)
		{
			releaseSerialSem(mh->port);
			return(PORT_IN_USE);
		}
	}
	else if (mh->busType == MB_SPI)
	{
		if (semTakeTmout(&modSpiSem, MODSEM_TMOUT) != OK)
			return(PORT_IN_USE);
	}
	else
		return(ILLEGAL_PORT);

	return(OK);
		
} /* getModbusPort() */


/************************************************************************/
/* Function	   : releaseModbusPort										*/
/* Purpose	   : Release MUTEX for Modbus serial port					*/
/* Inputs	   : Port number											*/
/* Outputs	   : None													*/
/************************************************************************/
void releaseModbusPort(ModBusHandle mh)
{
	if (mh->busType == MB_SER)
	{
		semGive(&modSerSem);
		releaseSerialSem(mh->port);
	}
	else if (mh->busType == MB_SPI)
		semGive(&modSpiSem);

} /* releaseModbusPort() */


/************************************************************************/
/* Function	   : modBusConfigureSer										*/
/* Purpose	   : Configure a serial ModBus bus							*/
/* Inputs	   : Serial port, baud rate, serial mode, surge delay, flags*/
/* Outputs	   : OK, ILLEGAL_PORT, or PORT_ERROR						*/
/************************************************************************/
Errno modBusConfigureSer(Port_t port, Nat32 baud, Nat32 sermode,
						 Nat32 surgeDelay, Nat32 flags)
{
	ModBusStruct		*mbp;
	SerStruct			*ssp;

	if ((port <= CPU_SER) || (port >= PIC_UARTS))
		return(ILLEGAL_PORT);

	if (getSerialSemTmout(port, MODSEM_TMOUT) != OK)
		return(PORT_IN_USE);

	mbp = &modBusSer[port];

	/* CAN'T RECONFIGURE BAUD/MODE IF PORT IS IN USE! */
	if (mbp->openCount > 0)
		if	((baud != mbp->baud) || (sermode != mbp->sermode))
		{
			releaseSerialSem(port);
			return(PORT_ERROR);
		}

	/* Set serial port type to ModBus		*/
	if	((ssp = getSerialStruct(port)) == NULL)
	{
		releaseSerialSem(port);
		return(PORT_ERROR);
	}
	else
		ssp->serType = MB_PORT;

	memset(mbp, 0, sizeof(ModBusStruct));
	mbp->busType = MB_SER;
	mbp->port = port;
	mbp->baud = baud;
	mbp->sermode = sermode;
	mbp->surgeDelayMs = surgeDelay;
	mbp->flags = flags;
	mbp->busIsPowered = FALSE;
	mbp->ioBuf = &modSerData;
	mbp->ident = IDENT;

	modSerIdle(port);
	releaseSerialSem(port);
	return(OK);

} /* modBusConfigureSer() */


/************************************************************************/
/* Function	   : modBusConfigured										*/
/* Purpose	   : Report whether modbus was configured					*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : TRUE or FALSE											*/
/************************************************************************/
MBool modBusConfigured(Port_t port)
{
	if ((port <= CPU_SER) || (port >= PIC_UARTS))
		return(FALSE);

	return(modBusSer[port].ident == IDENT);

} /* modBusConfigured() */


/************************************************************************/
/* Function	   : modBusPwrOn											*/
/* Purpose	   : Turn on power to a ModBus								*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : OK or ERROR											*/
/* Comment	   : Caller must own serial semaphore						*/
/************************************************************************/
MLocal Errno modBusPwrOn(Port_t port)
{
	ModBusStruct *mbp = &modBusSer[port];

	if ((port <= CPU_SER) || (port >= PIC_UARTS) ||
		(modBusSer[port].ident != IDENT))
		return(ERROR);

	digPwrOn(port);					/* Power on / enable ser relay	*/
	digSerEnable(port);				/* Enable the EIA driver		*/
	if (digSense485(port))
		digSnd485(port);

	serPortInit(port, mbp->baud, mbp->sermode);
	mbp->busIsPowered = TRUE;
	task_delay(TICKS_PER_SECOND/4);
	return(OK);

} /* modBusPwrOn() */


/************************************************************************/
/* Function	   : modBusPwrOff											*/
/* Purpose	   : Turn off power to a ModBus								*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : OK or ERROR											*/
/* Comment	   : Caller must own serial semaphore						*/
/************************************************************************/
MLocal Errno modBusPwrOff(Port_t port)
{
	ModBusStruct *mbp = &modBusSer[port];

	if ((port <= CPU_SER) || (port >= PIC_UARTS) ||
		(modBusSer[port].ident != IDENT))
		return(ERROR);

	digSerDisable(port);				/* Disable the EIA driver		*/
	digPwrOff(port);					/* Deselect the serial port		*/
	mbp->busIsPowered = FALSE;
	return(OK);

} /* modBusPwrOff() */


/************************************************************************/
/* Function	   : modBusIsPowered										*/
/* Purpose	   : Return powered status of one serial ModBus				*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : TRUE if powered, FALSE if not powered or bad port num	*/
/* Comment     : Only called for busType MB_SER							*/
/************************************************************************/
MBool modBusIsPowered(Port_t port)
{
	if ((port <= CPU_SER) || (port >= PIC_UARTS) ||
		(modBusSer[port].ident != IDENT))
		return(FALSE);
	
	return(modBusSer[port].busIsPowered);

} /* modBusIsPowered() */


/************************************************************************/
/* Function	   : modBusOpen												*/
/* Purpose	   : Open a ModBus bus										*/
/* Inputs	   : Remote ID ptr											*/
/* Outputs	   : ModBusHandle or NULL if failed							*/
/************************************************************************/
ModBusHandle modBusOpen(RemID *rp)
{
	Port_t		port = rp->bus;
	ModBusStruct *mbp = &modBusSer[rp->bus];

	if (rp->busType == MB_SPI)
		return(modBusSpiOpen(port));
	
	if ((port <= CPU_SER) || (port >= PIC_UARTS) ||
		(mbp->ident != IDENT))
	{
		modErrno = ILLEGAL_PORT;
		return(NULL);
	}

	mbp = &modBusSer[port];

	if (getModbusPort(mbp) != OK)
		return(NULL);
	
	if (mbp->openCount <= 0)
		mbp->openCount = 0;

	if (!mbp->busIsPowered)
		modBusPwrOn(port);

	mbp->openCount++;

	if (rp->remType == RemUnknown)		/* If haven't yet determined	*/
		findRemoteSlaveType(mbp, rp);	/* slave type, do it now		*/
	
	releaseModbusPort(mbp);

	return(mbp);

} /* modBusOpen() */


/************************************************************************/
/* Function	   : modBusClose											*/
/* Purpose	   : Close a ModBus bus										*/
/* Inputs	   : ModBusHandle											*/
/* Outputs	   : OK or PORT_ERROR										*/
/************************************************************************/
Errno modBusClose(ModBusHandle mh)
{
	Errno	rtn;
	Port_t	port = mh->port;

	if (mh->ident != IDENT)
		return(PORT_ERROR);

	if ((rtn = getModbusPort(mh)) != OK)
		return(rtn);
	
	if (--mh->openCount <= 0)
	{
		mh->openCount = 0;

		if (mh->busType == MB_SER)
		{
			switch (mh->flags & PWROFF_BITS)
			{
			  case KEEP_PWR:
				  break;

			  case PWROFF_IF_NO_PWR:
				  if (!remIsPowerOn(port))
					  modBusPwrOff(port);
				  break;

			  case PWROFF_IF_NO_PWRIO:
				  if (!remIsPowerOn(port) && !remIsOutputOn(port))
					  modBusPwrOff(port);
				  break;

			  default:
				  modBusPwrOff(port);
				  break;
			}
		}
	}

	releaseModbusPort(mh);
	return(OK);

} /* modBusClose() */


/************************************************************************/
/* Function	   : modBusSerIO											*/
/* Purpose	   : Send/receive one serial ModBus transaction				*/
/* Inputs	   : ModBusHandle, length to send exclusive of CRC			*/
/* Outputs	   : Error Number											*/
/* Comment	   : Calling thread MUST own modSerSem and serial port sem!	*/
/************************************************************************/
MLocal Errno modBusSerIO(ModBusHandle mh, Nat16 sndLen)
{
	Nat16		crc;

	if (mh->port >= PIC_UARTS)
		return(ILLEGAL_PORT);
	
	modErrno = OK;
	modSerData.ioTask = xTaskGetCurrentTaskHandle();
	modSerData.port = mh->port;

	crc = mod_crc16((Byte *)modSerData.sndBuf, sndLen);
	modSerData.sndBuf[sndLen++] = crc >> 8;
	modSerData.sndBuf[sndLen++] = crc & 0x00FF;

    modSerData.state = MB_SEND;			/* Go to sending state          */
    modSerData.sndLen = sndLen;			/* Save msg length              */
    modSerData.sndPos = 0;				/* Init send position           */
    modSerData.rcvLen = 0;				/* Init rcv length				*/

	serTxFlush(mh->port);
	setSerModbus(mh->port, TRUE);			/* Start up serial Tx			*/

	if (xTaskNotifyWait(~0, ~0, NULL, RCV_TMOUT) != pdTRUE)
		modErrno = COMM_TIME_OUT;		/* If timed out, set error code */
		
    modSerIdle(mh->port);				/* Go to idle state             */

	if ((modErrno == OK) &&
		(check_mod_crc16((Byte *)modSerData.rcvBuf, modSerData.rcvLen) != OK))
		modErrno = INVALID_CRC;
	
    return(modErrno);

} /* modBusSerIO() */


/************************************************************************/
/* Function	   : modBusIO												*/
/* Purpose	   : Send/receive one ModBus transaction					*/
/* Inputs	   : ModBusHandle, RemID ptr length to send exclusive of CRC*/
/* Outputs	   : Error Number											*/
/* Comment	   : Calling thread MUST own semaphore for mh->serport!		*/
/************************************************************************/
Errno modBusIO(ModBusHandle mh, RemID *rp, Nat16 sndLen)
{
	Errno		rtn;

	if (mh->ident != IDENT)
		return(modErrno = PORT_ERROR);

	if (mh->busType == MB_SER)
		rtn = modBusSerIO(mh, sndLen);
	else  if (mh->busType == MB_SPI)
		rtn = modSpiIO(mh, sndLen);
	else
		rtn = ILLEGAL_PORT;

	modErrno = rtn;

	mh->totMsgs++;

	if (rp)
		rp->msgs++;

	if (rtn != OK)
	{
		mh->totErrs++;
		if (rp)
			rp->errs++;
	}

	logModResult(mh, rp, rtn, sndLen);
	return(rtn);

} /* modBusIO() */


/************************************************************************/
/* Function	   : showOneModBus											*/
/* Purpose	   : Display One ModBus configuration to screen				*/
/* Inputs	   : ModBusHandle											*/
/* Outputs	   : OK														*/
/************************************************************************/
MLocal CmdRtn showOneModBus(ModBusHandle mh)
{
	Nat16		j;
	RemID		*idp;

	if ((mh->ident == IDENT) && (mh->busType != MB_UNASSIGNED))
	{
		if (mh->busType == MB_SPI)
			xprintf("SPI Channel %u, open %u, total msgs %u, total errs %u\n",
				mh->port, mh->openCount, mh->totMsgs, mh->totErrs);
		else
			xprintf("Channel %u at %u baud, open %u, %s total msgs %u, total errs %u\n",
					mh->port, mh->baud, mh->openCount,
					(mh->busIsPowered ? "ON, " : "OFF,"),  mh->totMsgs, mh->totErrs);

		if (mh->totMsgs > 0)
		{
			xputs("	 ID:errs/msgs - ");
			for (j = 0, idp = remIDs; j < REMOTE_IDS; j++, idp++)
				if (idp->bus == mh->port)
					xprintf("%u:%u/%u ", idp->slaveID, idp->errs, idp->msgs);
			xputs("\n");
		}
	}

	return(OK);

} /* showOneModBus() */


/************************************************************************/
/* Function	   : showModBus												*/
/* Purpose	   : Display ModBus configuration to screen					*/
/* Inputs	   : None													*/
/* Outputs	   : OK														*/
/************************************************************************/
CmdRtn showModBus(Void)
{
	Nat16		i;
	ModBusHandle mh;

	for (i = 0, mh = getSpiModStruct(); i < NUM_DBS; i++, mh++)
		showOneModBus(mh);

	for (i = 0, mh = modBusSer; i < PIC_UARTS; i++, mh++)
		showOneModBus(mh);

	return(OK);

} /* showModBus() */


/************************************************************************/
/* Function	   : logOneModBus											*/
/* Purpose	   : Helper to Script function to log ModBus status			*/
/* Inputs	   : ModBusHandle											*/
/* Outputs	   : OK														*/
/************************************************************************/
MLocal Errno logOneModBus(ModBusHandle mh)
{
	Nat16		j, n;
	RemID		*idp;
	char		*p;

	if ((mh->ident == IDENT) && (mh->busType != MB_UNASSIGNED) &&
		(mh->totMsgs > 0))
	{
		n = 0;
		p = (char *)modSerData.sndBuf;
		*p = '\0';

		for (j = 0, idp = remIDs; j < REMOTE_IDS; j++, idp++)
			if (idp->bus == mh->port)
			{
				if (idp->busType == MB_SPI)
					n += sprintf(p+n, "%u:%u/%u ", idp->slaveID, idp->errs,
								 idp->msgs);
				else if (idp->busType == MB_SER)
					n += sprintf(p+n, "%u:%u/%u (%u) ", idp->slaveID, idp->errs,
								 idp->msgs, serGetRxOvflCnt(mh->port));
			}

		sysLogPrintf("%s Modbus %u - %s", mh->port, p,
					 (idp->busType == MB_SPI) ? "SPI" : "");
	}

	return(OK);

} /* logOneModBus() */


/************************************************************************/
/* Function	   : logModBus												*/
/* Purpose	   : Script function to log ModBus status to sysLog			*/
/* Inputs	   : argc, argv												*/
/* Outputs	   : OK														*/
/************************************************************************/
Errno logModBus(Int32 argc, char **argv, MBool syntaxOnly)
{
	Nat16		i;
	ModBusHandle mh;

	if (syntaxOnly)
		return(OK);

	/* We take the modSerSem for the duration of logging the status.	*/
	/* This not only freezes the ModBus counts, it also allows us to use*/
	/* the buffers in modSerData as a print buffer.						*/

	if (semTakeTmout(&modSerSem, MODSEM_TMOUT) == OK)
	{
		for (i = 0, mh = getSpiModStruct(); i < NUM_DBS; i++, mh++)
			logOneModBus(mh);

		for (i = 0, mh = modBusSer; i < PIC_UARTS; i++, mh++)
			logOneModBus(mh);

		semGive(&modSerSem);
	}

	return(OK);

} /* logModBus() */


/************************************************************************/
/* Function	   : usrModPower											*/
/* Purpose	   : User routine to turn on/off power to a ModBus			*/
/* Inputs	   : Parm mask, bus, on/off boolean							*/
/* Outputs	   : OK														*/
/************************************************************************/
CmdRtn usrModPower(Nat16 pmask, Parm_t port, Parm_t onoff)
{
	ModBusStruct *mbp = &modBusSer[port];
	Errno		rtn;

	if ((pmask & 3) != 3)
	{
		xprintf("Usage: ModPower <bus> <0|1>\n");
		xprintf("  (0 for off, 1 for on)\n");
		return(ERROR);
	}

	if ((port <= CPU_SER) || (port >= PIC_UARTS) ||
		(mbp->ident != IDENT))
		return(ERROR);

	getSerialSem(port);

	if (onoff)
	{
		xprintf("Turning on bus %u\n", port);
		rtn = modBusPwrOn(port);
	}
	else
	{
		if (mbp->openCount > 0)
			xprintf("WARNING - Turning off bus %u even though bus is open (in use)!!!\n\n",
					port);
		else
			xprintf("Turning off bus %u\n", port);

		rtn = modBusPwrOff(port);
	}

	releaseSerialSem(port);

	return(rtn);

} /* usrModPower */



/************************************************************************/
/* Function    : _T3IntHandler                                          */
/* Purpose     : Interrupt Handler for T3, Modbus timer                 */
/* Inputs      : None                                                   */
/* Outputs     : None                                                   */
/* Comment     : Called from assembly language wrapper in port_asm.S    */
/************************************************************************/
void _T3IntHandler(void)
{
    modSerIdle(modSerData.port);			/* Set Idle state			*/
	vTaskNotifyGiveFromISR(modSerData.ioTask, NULL);
	portYIELD();
}
