/************************************************************************/
/* 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												*/
/************************************************************************/

#include <mbariTypes.h>					/* MBARI type definitions		*/
#include <mbariConst.h>					/* MBARI constants				*/
#include <cfxpico.h>					/* Persistor PicoDOS definitions*/
#include <oasis.h>						/* OASIS controller definitions */
#include <custom.h>						/* Custom def's for deployment	*/
#include <io.h>							/* OASIS I/O definitions		*/
#include <spi.h>						/* OASIS SPI I/O definitions	*/
#include <list.h>						/* OASIS Linked List library	*/
#include <sem.h>						/* OASIS Semaphore library		*/
#include <task.h>						/* OASIS task dispatcher		*/
#include <serial.h>						/* OASIS Serial I/O				*/		
#include <modbus.h>						/* Modbus protocol library		*/
#include <remote.h>						/* Remote I/O library			*/
#include <utils.h>						/* OASIS Utility routines		*/
#include <syslog.h>						/* Respirometer system logger	*/
#include <serial.h>						/* Serial I/O library			*/
#include <debug.h>

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

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

Extern Nat32			drvPwrBits;		/* Power bits turned on by drivers	*/


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

Global Errno	modErrno;

Global ModBusStruct		modBus[PHYS_SER_PORTS];


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

MLocal Semaphore		modSem;


/************************************************************************/
/* Function	   : initModBus												*/
/* Purpose	   : Initialize this module									*/
/* Inputs	   : None													*/
/* Outputs	   : None													*/
/************************************************************************/
Void initModBus(Void)
{
	Nat16		w = 1;
	char		*p = (char *)&w;

	memset(modBus, 0, sizeof(modBus));
	sem_init( &modSem, MUTEX, 1 );

} /* initModBus() */


/************************************************************************/
/* Function	   : getModbusPort											*/
/* Purpose	   : Get MUTEX for Modbus serial port						*/
/* Inputs	   : Port number											*/
/* Outputs	   : None													*/
/************************************************************************/
Void getModbusPort(Nat16 serport)
{
	getSerialPort(serport);
	sem_take(&modSem);

} /* getModbusPort() */


/************************************************************************/
/* Function	   : releaseModbusPort										*/
/* Purpose	   : Release MUTEX for Modbus serial port					*/
/* Inputs	   : Port number											*/
/* Outputs	   : None													*/
/************************************************************************/
Void releaseModbusPort(Nat16 serport)
{
	sem_give(&modSem);
	releaseSerialPort(serport);

} /* releaseModbusPort() */


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

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

	getSerialPort(port);
	mbp = &modBus[port];

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

	memset(mbp, 0, sizeof(ModBusStruct));
	mbp->serport = port;
	mbp->baud = baud;
	mbp->sermode = sermode;
	mbp->surgeDelayMs = surgeDelay;
	mbp->flags = flags;
	mbp->ident = IDENT;
	mbp->serTmout = (2560L * TICKS_PER_SECOND)/baud + 1;
	if (mbp->serTmout < DEFAULT_SER_TMOUT)
		mbp->serTmout = DEFAULT_SER_TMOUT;

	mbp->turnaroundUsecs = DEFAULT_TURNAROUND_USECS;
	if (baud <= 38400)
		mbp->turnaroundUsecs += 20000000/baud;

	mbp->is485 = spiReadPIOBit(GPIO_CHIP, SENSE_485(port)) ? TRUE : FALSE;

	RTCGetTime(&mbp->rcvTime, &mbp->rcvTick);
	releaseSerialPort(port);
	return(OK);

} /* modBusConfigure() */


/************************************************************************/
/* Function	   : modBusConfigured										*/
/* Purpose	   : Report whether modbus was configured					*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : TRUE or FALSE											*/
/************************************************************************/
MBool modBusConfigured(Nat16 port)
{
	if ((port <= CPU_SER) || (port >= PHYS_SER_PORTS))
		return(FALSE);
	return(modBus[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(Nat16 port)
{
	ModBusStruct *mbp = &modBus[port];

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

	io_serRelay(port, TRUE);			/* Select the serial port		*/

	mbp->is485 = spiReadPIOBit(GPIO_CHIP, SENSE_485(port)) ? TRUE : FALSE;
										/* Enable serial driver			*/
	io_serEnable(port, (mbp->is485) ? RCV485 : RS232);
	io_pwron(port);								/* Turn on bus power			*/
	drvPwrBits |= 1L<<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(Nat16 port)
{
	ModBusStruct *mbp = &modBus[port];

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

	io_serEnable(port, COMMS_OFF);		/* Disable the EIA driver		*/
	io_serRelay(port, FALSE);			/* Deselect the serial port		*/
	io_pwroff(port);					/* Turn off bus power			*/
	drvPwrBits &= ~(1L<<port);
	mbp->busIsPowered = FALSE;
	return(OK);

} /* modBusPwrOff() */


/************************************************************************/
/* Function	   : modBusIsPowered										*/
/* Purpose	   : Return powered status of one ModBus					*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : TRUE if powered, FALSE if not powered or bad port num	*/
/************************************************************************/
MBool modBusIsPowered(Nat16 port)
{
	if ((port <= CPU_SER) || (port >= PHYS_SER_PORTS) ||
		(modBus[port].ident != IDENT))
		return(FALSE);
	
	return(modBus[port].busIsPowered);

} /* modBusIsPowered() */


/************************************************************************/
/* Function	   : modBusOpen												*/
/* Purpose	   : Open a ModBus bus										*/
/* Inputs	   : Serial port number										*/
/* Outputs	   : ModBusHandle or NULL if failed							*/
/************************************************************************/
ModBusHandle modBusOpen(Nat16 port)
{
	ModBusStruct *mbp = &modBus[port];

	if ((port <= CPU_SER) || (port >= PHYS_SER_PORTS) ||
		(mbp->ident != IDENT))
	{
		modErrno = ILLEGAL_PORT;
		return(NULL);
	}

	getSerialPort(port);

	if (mbp->openCount <= 0)
		mbp->openCount = 0;

	if (!mbp->busIsPowered)
	{
		takeRemoteSem();
		modBusPwrOn(port);
		giveRemoteSem(mbp->surgeDelayMs);
	}

	mbp->openCount++;
	releaseSerialPort(port);

	return(mbp);

} /* modBusOpen() */


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

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

	getSerialPort(port);

	if (--mh->openCount <= 0)
	{
		mh->openCount = 0;
		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;
		}
	}

	releaseSerialPort(port);
	return(OK);

} /* modBusClose() */


/************************************************************************/
/* Function	   : modSerRead												*/
/* Purpose	   : Faster polling version of ser-read						*/
/* Inputs	   : Phys serial port, buffer, num bytes to read, timeout	*/
/* Output	   : Number of bytes read									*/
/************************************************************************/
MLocal Int32 modSerRead(Nat16 port, Byte *buffer, Int32 nbytes, Int32 tmout)
{
	Reg Byte	*p = buffer;			/* Buffer ptr			*/
	Int16		c;						/* Serial character		*/
	Int32		bytesRcvd;				/* Num bytes received	*/
	Nat32		startTick = ioGetTick();

	taskCriticalEntry();
	for (bytesRcvd = 0; bytesRcvd < nbytes; )
	{
		if ((c = ser_getc(port)) != ERROR)
		{
			*p++ = c;
			bytesRcvd++;
		}
		else if ((Int32)(ioGetTick() - startTick) < tmout)
			task_delay_ms(2);
		else
			break;
	}

	taskCriticalExit();
	return(bytesRcvd);

} /* modSerRead() */


/************************************************************************/
/* Function	   : calcRcvLen												*/
/* Purpose	   : Calculate expected receive length from receive header	*/
/* Inputs	   : Receive buffer ptr with 3 bytes filled in				*/
/* Output	   : Expected total length of reply							*/
/************************************************************************/
MLocal Nat16 calcRcvLen(Byte *rcvBuf)
{
	Nat16		n = rcvBuf[2];

	switch (rcvBuf[1])
	{
	  case FC_READ_COILS:
	  case FC_READ_INPUTS:
		  return(BASE_RESPONSE_LEN + n);

	  case FC_READ_HOLDING_REGS:
	  case FC_READ_INPUT_REGS:
	  case FC_REPORT_SLAVE_ID:
	  case FC_WRITE_READ_MULT_REGS:
		  return(BASE_RESPONSE_LEN + n);

	  case FC_READ_FIFO_QUEUE:
		  return(BASE_RESPONSE_LEN + 1 + n);

	  default:
		  return(8);
	}

} /* calcRcvLen() */


/************************************************************************/
/* Function	   : modBusInternalIO										*/
/* Purpose	   : Internal function to do send/rcv						*/
/* Inputs	   : ModBusHandle, length to send exclusive of CRC			*/
/* Outputs	   : Error Number											*/
/* Comment	   : Calling thread MUST own semaphore for mh->serport!		*/
/************************************************************************/
MLocal Errno modBusInternalIO(ModBusHandle mh, Nat16 sndLen)
{
	Nat16		port = mh->serport;
	Byte		*sndbuf = mh->sndBuf;
	Byte		*rcvbuf = mh->rcvBuf;
	Nat16		crc, rcvLen;
	Int32		rtn;

	crc = mod_crc16(sndbuf, sndLen);
	sndbuf[sndLen++] = crc >> 8;
	sndbuf[sndLen++] = crc & 0x00FF;

	ser_oflush(port);

#ifdef DEBUG_MODBUS
	PinSet(IRQ7);
#endif
	if (mh->is485)
	{
		io_serEnable(port, SEND485);
		Delay10us();

		rtn = ser_write(port, sndbuf, sndLen, mh->serTmout);

		ser_waitSend(port);						/* Busy wait for sent to complete */
		RTCDelayMicroSeconds(mh->turnaroundUsecs);
		spiSetPIOBit(GPIO_CHIP, DE_485(port), 0);

		if (rtn != sndLen)
			return(SEND_FAILURE);
	}
	else		/* RS-232 Mode	*/
	{
		if (ser_write(port, sndbuf, sndLen, mh->serTmout) != sndLen)
			return(SEND_FAILURE);
	}

#ifdef DEBUG_MODBUS
	PinClear(IRQ7);
#endif
	ser_iflush(port);

	if ((mh->rcvLen = modSerRead(port, rcvbuf, 3, RCV_TMOUT_HEADER)) != 3)
		return(RCV_TIMEOUT);

	/* Check Slave's return ID	*/
	if (rcvbuf[0] != sndbuf[0])
		return(SLAVE_ID_MISMATCH);

	if (rcvbuf[1] != sndbuf[1])
	{											/* Function code mismatch	*/
		if (rcvbuf[1] == (sndbuf[1] | 0x80))	/* Error reply				*/
			return(-(rcvbuf[2]));				/* Negate err code in byte 2*/
		else
			return(FUNC_CODE_MISMATCH);
	}

	rcvLen = calcRcvLen(rcvbuf);

	mh->rcvLen = modSerRead(port, rcvbuf+3, rcvLen-3, mh->serTmout) + 3;
	if (mh->rcvLen != rcvLen)
		return(RCV_TIMEOUT);

	if (check_mod_crc16(rcvbuf, rcvLen) != OK)
		return(INVALID_CRC);

	return(OK);

} /* modBusInternalIO() */


/************************************************************************/
/* Function	   : findSlaveCntr											*/
/* Purpose	   : Helper function to find msg/error counter for this slave*/
/* Inputs	   : ModBusHandle, slave ID									*/
/* Outputs	   : Ptr to slave counter									*/
/************************************************************************/
SlaveCntr *findSlaveCntr(ModBusHandle mh, Nat16 slaveID)
{
	Nat16		i;
	SlaveCntr	*cntP;

	for (i = 0, cntP = &mh->slaveCnt[0]; i < MAX_SLAVES-1; i++, cntP++)
	{
		if (cntP->slaveID == slaveID)
			return(cntP);

		/* slaveID == 0 indicates an unallocated counter */
		/* Grab it and initialize it					*/
		if (cntP->slaveID == 0)
		{
			cntP->slaveID = slaveID;
			cntP->msgs = cntP->errs = 0;
			return(cntP);
		}
	}

	/* The last slaveCnt slot is the catchall, in case we have more than 15 slaves */
	return(&mh->slaveCnt[MAX_SLAVES-1]);

} /* findSlaveCntr() */


/************************************************************************/
/* Function	   : modBusIO												*/
/* Purpose	   : Send/receive one ModBus transaction					*/
/* Inputs	   : ModBusHandle, length to send exclusive of CRC			*/
/* Outputs	   : Error Number											*/
/* Comment	   : Calling thread MUST own semaphore for mh->serport!		*/
/************************************************************************/
Errno modBusIO(ModBusHandle mh, Nat16 sndLen)
{
	Int32		elapsedTicks;
	Nat32		cursecs;
	Nat16		curticks, slaveID;
	Errno		rtn;
	SlaveCntr	*cntP;

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

	RTCGetTime(&cursecs, &curticks);

	elapsedTicks = ((Int32)RTC_TICKS_PER_SECOND * (Int32)(cursecs - mh->rcvTime)) +
		(Int32)curticks - (Int32)(mh->rcvTick);

	if (elapsedTicks < INTERMSG_RTC_TICKS)
		task_delay_ms(2);

	rtn = modBusInternalIO(mh, sndLen);
	modErrno = rtn;

	RTCGetTime(&mh->rcvTime, &mh->rcvTick);
	mh->totMsgs++;

	slaveID = (Nat16)(mh->sndBuf[0]);
	cntP = findSlaveCntr(mh, slaveID);
	cntP->msgs++;

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

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

} /* modBusIO() */


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

	xprintf("ModBus channels configured:\n");

	for (i = CPU_SER+1; i < PHYS_SER_PORTS; i++)
		if (modBusConfigured(i))
		{
			mh = &modBus[i];
			xprintf("Channel %hd at %ld baud, %s, open %ld, %s total msgs %ld, total errs %ld\n",
					mh->serport, mh->baud, (mh->is485 ? "RS485" : "RS232"),
					mh->openCount, (mh->busIsPowered ? "ON, " : "OFF,"),  mh->totMsgs, mh->totErrs);
			if (mh->totMsgs > 0)
			{
				xputs("	 ID:errs/msgs - ");
				for (j = 0, cntP = &mh->slaveCnt[0]; j < MAX_SLAVES-1; j++, cntP++)
					if (cntP->slaveID > 0)
						xprintf("%hd:%ld/%ld ", cntP->slaveID, cntP->errs, cntP->msgs);
				xputs("\n");
			}
		}

	return(OK);

} /* showModBus() */


/************************************************************************/
/* Function	   : logModBus												*/
/* Purpose	   : Script function to log ModBus status to sysLog			*/
/* Inputs	   : argc, argv												*/
/* Outputs	   : OK														*/
/************************************************************************/
Errno logModBus(Int32 argc, char **argv, MBool syntaxOnly)
{
#pragma unused(argc, argv)
	Nat16		i, j, n;
	ModBusHandle mh;
	SlaveCntr	*cntP;
	char		buf[512];

	if (syntaxOnly)
		return(OK);

	for (i = CPU_SER+1; i < PHYS_SER_PORTS; i++)
		if (modBusConfigured(i))
		{
			mh = &modBus[i];

			if (mh->totMsgs > 0)
			{
				n = 0;
				buf[0] = '\0';

				for (j = 0, cntP = &mh->slaveCnt[0]; j < MAX_SLAVES-1; j++, cntP++)
					if (cntP->slaveID > 0)
						n += sprintf(buf+n, "%hd:%ld/%ld (%hd) ", cntP->slaveID, cntP->errs, cntP->msgs,
									 serGetRxOvflCnt(i));

				sysLogPrintf("Modbus %hd - %s", i, buf);
			}
		}

	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 = &modBus[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 >= PHYS_SER_PORTS) ||
		(mbp->ident != IDENT))
		return(ERROR);

	getSerialPort(port);

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

	releaseSerialPort(port);

	return(rtn);

} /* usrModPower */
