/**********************************************************************
* Fake GPIB support using termios serial line                         *
**********************************************************************/       
/***********************************************************************
* Copyright (c) 2002 The University of Chicago, as Operator of Argonne
* National Laboratory, and the Regents of the University of
* California, as Operator of Los Alamos National Laboratory, and
* Berliner Elektronenspeicherring-Gesellschaft m.b.H. (BESSY).
* gpibCore is distributed subject to a Software License Agreement
* found in file LICENSE that is included with this distribution.
***********************************************************************/

/*
 * drvTermiosTtyGpib.c,v 1.18 2003/08/29 13:41:33 norume Exp
 */

#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>

#include <osiSock.h>
#include <cantProceed.h>
#include <errlog.h>
#include <iocsh.h>
#include <epicsSignal.h>
#include <epicsThread.h>
#include <epicsTime.h>
#include <epicsTimer.h>
#include <osiUnistd.h>
#include <epicsExport.h>
#include "drvHwGpibInterface.h"
#include "drvTermiosTtyGpib.h"

#ifndef vxWorks
#include <termios.h>
#endif

#define CONSECUTIVE_FAILURE_REPORT_LIMIT    40

/*
 * This structure holds the hardware-specific information for a single
 * 'GPIB' link.  There is one for each serial line.
 */
typedef struct {
    ibLink             head;            /* Device-independent information */
    char              *serialDeviceName;
    int                fd;
    int                consecutiveFailures;
    int                isRemote;
    struct sockaddr_in farAddr;
    epicsThreadId      threadId;
    epicsTimerQueueId  timerQueue;
    epicsTimerId       timer;
    int                timeoutFlag;
    int                wasClosed;
    epicsTimeStamp     whenClosed;
    int                openOnReset;
} ttyController_t;

int drvTermiosTtyGpibDebug = 0;
epicsExportAddress(int,drvTermiosTtyGpibDebug);
  
/*
 * Dummy routines for tty 'GPIB' operation
 */
static long
termiosTtyGpibProbe(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibAddressedCmd(ibLink *pibLink, int gpibAddr, int secondary,
                           char *data, int length, int timeout)
{
    return 0;
}
static long
termiosTtyGpibUniversalCmd(ibLink *pibLink, int cmd)
{
    return 0;
}
static long
termiosTtyGpibIfc(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibRen(ibLink *pibLink, int onOff)
{
    return 0;
}
static long
termiosTtyGpibSrqStatus(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibSrqEnable(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibSrqDisable(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibSerialPollBegin(ibLink *pibLink)
{
    return 0;
}
static long
termiosTtyGpibSerialPoll(ibLink *pibLink, int gpibAddr, int secondary, int timeout)
{
    return 0;
}
static long
termiosTtyGpibSerialPollEnd(ibLink *pibLink)
{
    return 0;
}

/*
 * Display failure message
 */
static void reportFailure(ttyController_t *tty, const char *fmt, ...)
{
    va_list args;
    epicsTimeStamp now;
    char cbuf[30];

    if(tty->consecutiveFailures < CONSECUTIVE_FAILURE_REPORT_LIMIT) {
        epicsTimeGetCurrent(&now);
        epicsTimeToStrftime(cbuf, sizeof cbuf, "%F %T.%02f", &now);
        errlogPrintf("%s  ", cbuf);
        va_start(args, fmt);
        errlogVprintf(fmt, args);
        va_end(args);
        if(++tty->consecutiveFailures == CONSECUTIVE_FAILURE_REPORT_LIMIT)
            errlogPrintf("Too many consecutive failures.  Reporting inhibited until operation succeeds.\n");
        va_end(args);
    }
}

/*
 * Show string in human-readable form
 */
static void
showString(const char *str, int length)
{
    while (length--) {
        char c = *str++;
        switch (c) {
        case '\n':  printf("\\n");  break;
        case '\r':  printf("\\r");  break;
        case '\t':  printf("\\t");  break;
        case '\\':  printf("\\\\");  break;
        default:
            if (isprint(c))
                printf("%c", c);   /* putchar(c) doesn't work on vxWorks */
            else
                printf("\\%03o", (unsigned char)c);
            break;
        }
    }
}

/*
 * Report link parameters
 */
static long
termiosTtyGpibReportLink(ibLink *pibLink, int level)
{
    ttyController_t *tty = (ttyController_t *)pibLink;

    printf("Termios 'GPIB' device:\n");
    printf("           Link number: %d\n", tty->head.link);
    printf("           Serial line: %s\n", tty->serialDeviceName);
    return 0;
}

static void
closeConnection(ttyController_t *tty)
{
    int fd;
    if ((fd = tty->fd) >= 0) {
        epicsTimeGetCurrent(&tty->whenClosed);
        tty->wasClosed = 1;
        tty->fd = -1;
        close(fd);
    }
}

/*
 * Unblock the I/O operation
 */
static void
timeoutHandler(void *p)
{
    volatile ttyController_t *tty = (ttyController_t *)p;
    epicsThreadId thread;

    if (drvTermiosTtyGpibDebug >= 3)
        printf("termiosTtyGpib: %s timeout (%p).\n", tty->serialDeviceName, (void*)tty->threadId);
    tty->timeoutFlag = 1;
    
    /*
     * Force the I/O thread out of its slow system call.
     */
    if (tty->isRemote) {
        switch(epicsSocketSystemCallInterruptMechanismQuery()) {
        case esscimqi_socketCloseRequired:
            closeConnection((ttyController_t *)tty);
            break;

        case esscimqi_socketBothShutdownRequired:
            shutdown(tty->fd, SHUT_RDWR);
            break;

        case esscimqi_socketSigAlarmRequired:
            if ((thread = tty->threadId) != NULL) {
                tty->threadId = NULL;
                epicsSignalRaiseSigAlarm(thread);
            }
            break;

        default:
            errlogPrintf("No mechanism for unblocking socket I/O!\n");
            break;
        }
    }
    else {
#ifdef vxWorks
        ioctl(tty->fd, FIOCANCEL, 0);
#else
        tcflush(tty->fd, TCOFLUSH);
        if ((thread = tty->threadId) != NULL) {
            tty->threadId = NULL;
            epicsSignalRaiseSigAlarm(thread);
        }
#endif
    }
}

/*
 * Open connection initially or after timeout
 */
static int
openConnection (ttyController_t *tty)
{
    int i;

    /*
     * Sanity check
     */
    if (tty->fd >= 0) {
        errlogPrintf("termiosTtyGpib: Link already open!\n");
        return -1;
    }
    tty->timeoutFlag = 0;
    tty->threadId = NULL;

    /*
     * Create timeout mechanism
     */
    if ((tty->timerQueue == NULL)
     && ((tty->timerQueue = epicsTimerQueueAllocate(1, epicsThreadPriorityBaseMax)) == NULL)) {
        errlogPrintf("termiosTtyGpib: Can't create timer queue.\n");
        return -1;
    }
    if ((tty->timer == NULL) 
     && ((tty->timer = epicsTimerQueueCreateTimer(tty->timerQueue, timeoutHandler, tty)) == NULL)) {
        errlogPrintf("termiosTtyGpib: Can't create timer.\n");
        return -1;
    }

    /*
     * Don't try to open a connection too soon after breaking the previous one
     */
    if (tty->wasClosed) {
        epicsTimeStamp now;
        double sec;

        epicsTimeGetCurrent(&now);
        sec = epicsTimeDiffInSeconds(&now, &tty->whenClosed);
        if ((sec >= 0) && (sec < 2.0))
            epicsThreadSleep(2.0 - sec);
    }

    /*
     * Create the remote or local connection
     */
    if (drvTermiosTtyGpibDebug >= 1)
        printf("termiosTtyGpib open connection to %s\n", tty->serialDeviceName);
    if (tty->isRemote) {
        /*
         * Create the socket
         */
        if ((tty->fd = epicsSocketCreate(PF_INET, SOCK_STREAM, 0)) < 0) {
            errlogPrintf("termiosTtyGpib: Can't create socket: %s\n", strerror(errno));
            return -1;
        }

        /*
         * Connect to the remote terminal server
         */
        tty->threadId = epicsThreadGetIdSelf();
        epicsTimerStartDelay(tty->timer, 10.0);
        i = connect(tty->fd, (struct sockaddr *)&tty->farAddr, sizeof tty->farAddr);
        epicsTimerCancel(tty->timer);
        if (tty->timeoutFlag) {
            reportFailure(tty, "termiosTtyGpib: Timed out attempting to connect to %s\n", tty->serialDeviceName);
            return -1;
        }
        if (i < 0) {
            reportFailure(tty, "termiosTtyGpib: Can't connect to %s: %s\n", tty->serialDeviceName, strerror(errno));
            close(tty->fd);
            tty->fd = -1;
            return -1;
        }
    }
    else {
        /*
         * Open serial line and get configuration
         */
        tty->threadId = epicsThreadGetIdSelf();
        epicsTimerStartDelay(tty->timer, 10.0);
        if ((tty->fd = open(tty->serialDeviceName, O_RDWR|O_NOCTTY, 0)) < 0) {
            reportFailure(tty, "termiosTtyGpib: Can't open `%s': %s\n", tty->serialDeviceName, strerror(errno));
            return -1;
        }
        epicsTimerCancel(tty->timer);
        if (tty->timeoutFlag) {
            reportFailure(tty, "termiosTtyGpib: Timed out attempting to open %s\n", tty->serialDeviceName);
            return -1;
        }
#ifndef vxWorks
        {
        struct termios     termios;
        if (tcgetattr(tty->fd, &termios) < 0) {
            reportFailure(tty, "termiosTty: Can't get `%s' attributes: %s\n", tty->serialDeviceName, strerror(errno));
            close(tty->fd);
            tty->fd = -1;
            return -1;
        }

        /*
         * The c_cflags field is left unchanged.
         * It is assumed that these parameters have been set up with a previous stty command.
         */
        termios.c_iflag = IGNPAR | INPCK;
        termios.c_lflag = 0;
        termios.c_oflag = 0;
        if (tcsetattr(tty->fd, TCSANOW, &termios) < 0) {
            reportFailure(tty, "termiosTtyGpib: Can't get `%s' attributes: %s\n", tty->serialDeviceName, strerror(errno));
            close(tty->fd);
            tty->fd = -1;
            return -1;
        }
        tcflush(tty->fd, TCIOFLUSH);
        }
#endif /* vxWorks */

    }
    if(tty->consecutiveFailures == CONSECUTIVE_FAILURE_REPORT_LIMIT)
        errlogPrintf("termiosTtyGpib: Reconnected to %s\n", tty->serialDeviceName);
    if (drvTermiosTtyGpibDebug >= 1)
        printf("termiosTtyGpib opened connection to %s\n", tty->serialDeviceName);
    return 0;
}

/*
 * Create a link
 */
static long
termiosTtyGpibGenLink(ibLink *pibLink, int pass)
{
    ttyController_t *tty = (ttyController_t *)pibLink;

    if (pass == 0)
        return drvGpibLinkStart(pibLink);
    if (openConnection(tty) < 0) {
      if (tty->openOnReset)
        errlogPrintf("termiosTtyGpib requires drvGpibResetLink(%d) to connect to %s.\n", tty->head.link, tty->serialDeviceName);
      else
        errlogPrintf("termiosTtyGpib will attempt to connect to %s later.\n", tty->serialDeviceName);
    }
    
    return 0;
}

static long
termiosTtyGpibDestroyLink(ibLink *pibLink)
{
    ttyController_t *tty = (ttyController_t *)pibLink;

    if (tty->timer)
        epicsTimerCancel(tty->timer);
    closeConnection(tty);
    return 0;
}

/*
 * Write to the serial line
 */
static long
termiosTtyGpibWrite(ibLink *pibLink, int gpibAddr, int secondary,
            char *buffer, int length, int timeout)
{
    ttyController_t *tty = (ttyController_t *)pibLink;
    int wrote;

    if (drvTermiosTtyGpibDebug >= 1) {
        printf("termiosTtyGpibWrite %d ", length);
        if (drvTermiosTtyGpibDebug >= 2)
            showString(buffer, length);
        printf("\n");
    }
    if (tty->fd < 0) {      
      if (tty->openOnReset)        return -1;
      if (openConnection(tty) < 0) return -1;     
    }
    tty->threadId = epicsThreadGetIdSelf();
    epicsTimerStartDelay(tty->timer, timeout / 1000.0);
    wrote = write(tty->fd, buffer, length);
    epicsTimerCancel(tty->timer);
    if (tty->timeoutFlag) {
        if (tty->openOnReset)
          reportFailure(tty, "termiosTtyGpibWrite %s timeout - reconnect using drvGpibResetLink(%d)\n", tty->serialDeviceName, tty->head.link);
        else
          reportFailure(tty, "termiosTtyGpibWrite %s timeout\n", tty->serialDeviceName);
        closeConnection(tty);
        return -1;
    }
    if (wrote != length) {
        reportFailure(tty, "termiosTtyGpibWrite %s failed: %s\n", tty->serialDeviceName, strerror(errno));
        closeConnection(tty);
        return -1;
    }
    tty->consecutiveFailures = 0;
    return 0;
}

/*
 * Read from the serial line
 * It is tempting to consider the use of cooked (ICANON) termios mode
 * to read characters when the end-of-string character is valid, but
 * there are a couple of reasons why this isn't a good idea.
 *  1) The cooked buffer inside the kernel is a fixed size.  A long
 *     input message could end up being truncated.
 *  2) If the EOS character were changed to -1 there could be problem
 *     with losing characters already on the cooked queue.
 * Question: When a read returns a positive value should we immediately
 *           return those character to our caller or should we loop
 *           until EOS is received or the character count is reached?
 *           For now I'm looping here.
 * Question: Given that we're looking for the EOS, if the EOS character
 *           is found in the middle of a reply, what number of characters
 *           should be returned to our caller?  The total number of characters
 *           read, or just the number up to, and including, the EOS
 *           character?  For now, I'm returning the total number read.
 */
static long
termiosTtyGpibRead(ibLink *pibLink, int gpibAddr, int secondary,
            char *buffer, int length, int timeout, int eos)
{
    ttyController_t *tty = (ttyController_t *)pibLink;
    int thisRead;
    int totalRead = 0;
    
    if (tty->fd < 0) {      
      if (tty->openOnReset)        return -1;
      if (openConnection(tty) < 0) return -1;     
    }
    tty->threadId = epicsThreadGetIdSelf();
    for (;;) {
        epicsTimerStartDelay(tty->timer, timeout / 1000.0);
        thisRead = read(tty->fd, buffer, length);
        epicsTimerCancel(tty->timer);
        if (tty->timeoutFlag) {
            reportFailure(tty, "termiosTtyGpibRead %s timeout\n", tty->serialDeviceName);
            break;
        }
        if (thisRead < 0) {
            reportFailure(tty, "termiosTtyGpibRead %s failed: %s\n", tty->serialDeviceName, strerror(errno));
            break;
        }
        if (thisRead == 0) {
            reportFailure(tty, "termiosTtyGpibRead %s EOF\n", tty->serialDeviceName);
            break;
        }
        if (drvTermiosTtyGpibDebug >= 1) {
            printf("termiosTtyGpibRead %d ", thisRead);
            if ((thisRead > 0) && (drvTermiosTtyGpibDebug >= 2))
                showString(buffer, thisRead);
            printf("\n");
        }
        totalRead += thisRead;
        length -= thisRead;
        if ((length == 0)
         || ((eos >= 0) && (memchr(buffer, eos, thisRead) != NULL))) {
            tty->consecutiveFailures = 0;
            return totalRead;  /* OR memchr()- original buffer ??? */
        }
        buffer += thisRead;
    }
    closeConnection(tty);
    if (tty->consecutiveFailures < CONSECUTIVE_FAILURE_REPORT_LIMIT) {
        tty->consecutiveFailures++;
        if (tty->openOnReset)
          errlogPrintf("%s - reconnect using drvGpibResetLink(%d)\n", tty->serialDeviceName, tty->head.link);
    }
    return -1;
}

/*
 * The entry table for this driver
 */
drvHwGpibEt drvTermiosTtyGpib = {
    termiosTtyGpibGenLink,
    termiosTtyGpibDestroyLink,
    termiosTtyGpibReportLink,
    termiosTtyGpibProbe,
    termiosTtyGpibRead,
    termiosTtyGpibWrite,
    termiosTtyGpibAddressedCmd,
    termiosTtyGpibUniversalCmd,
    termiosTtyGpibIfc,
    termiosTtyGpibRen,
    termiosTtyGpibSrqStatus,
    termiosTtyGpibSrqEnable,
    termiosTtyGpibSrqDisable,
    termiosTtyGpibSerialPollBegin,
    termiosTtyGpibSerialPoll,
    termiosTtyGpibSerialPollEnd
};

/*
 * Clean up a ttyController
 */
static void
ttyCleanup(ttyController_t *tty)
{
    if (tty) {
        if (tty->fd >= 0)
            close(tty->fd);
        free(tty->serialDeviceName);
        free(tty);
    }
}


/*
 * Create and configure a serial 'GPIB' device
 */
int
drvTermiosTtyGpibConfigure(     /* RETURNS: 0 or -1 */
    int     linkNumber,         /* unique identifier */
    char   *serialDeviceName,   /* serial line name (e.g. /dev/ttyS0) */
    int     openOnReset        /* 1 = open conn on link reset only */
) {
    ttyController_t *tty;
    char *cp;

    /*
     * Argument checking
     */
    if (serialDeviceName == NULL) {
        errlogPrintf("drvTermiosTtyGpibConfigure: Missing serial device name argument\n");
        return -1;
    }

    /*
     * Allocate device
     */
    tty = (ttyController_t *)callocMustSucceed(1, sizeof *tty, "drvTermiosTtyGpibConfigure()");
    tty->fd = -1;
    tty->serialDeviceName = malloc(strlen(serialDeviceName)+1);
    if (tty->serialDeviceName == NULL) {
        errlogPrintf("drvTermiosTtyGpibConfigure: Insufficient memory.\n");
        ttyCleanup(tty);
        return -1;
    }
    strcpy(tty->serialDeviceName, serialDeviceName);

    /*
     * Parse configuration parameters
     */
    if ((cp = strchr(tty->serialDeviceName, ':')) != NULL) {
        int port;

        *cp = '\0';
        if(hostToIPAddr(tty->serialDeviceName, &tty->farAddr.sin_addr) < 0) {
            *cp = ':';
            errlogPrintf("drvTermiosTtyGpibConfigure: Unknown host '%s'.\n", tty->serialDeviceName);
            ttyCleanup(tty);
            return -1;
        }
        port = strtol(cp+1, NULL, 0);
        tty->farAddr.sin_port = htons(port);
        tty->farAddr.sin_family = AF_INET;
        memset(tty->farAddr.sin_zero, 0, sizeof tty->farAddr.sin_zero);
        *cp = ':';
        tty->isRemote = 1;
    }
    tty->openOnReset = openOnReset;

    /*
     *  Link with higher level routines
     */
    tty->head.link = linkNumber;
    tty->head.pdrvHwGpibEt = &drvTermiosTtyGpib;
    if (drvGpibLinkInit(&tty->head) < 0) {
        errlogPrintf("drvTermiosTtyGpibConfigure: Can't init GPIB driver.\n");
        ttyCleanup(tty);
        return -1;
    }
    return 0;
}

/*
 * IOC shell command registration
 */
static const iocshArg drvTermiosTtyGpibConfigureArg0 = { "link number",iocshArgInt};
static const iocshArg drvTermiosTtyGpibConfigureArg1 = { "serial line name",iocshArgString};
static const iocshArg drvTermiosTtyGpibConfigureArg2 = { "open on reset",iocshArgInt};
static const iocshArg *drvTermiosTtyGpibConfigureArgs[3] = {
           &drvTermiosTtyGpibConfigureArg0, &drvTermiosTtyGpibConfigureArg1,
           &drvTermiosTtyGpibConfigureArg2};
static const iocshFuncDef drvTermiosTtyGpibConfigureFuncDef = {
           "drvTermiosTtyGpibConfigure", 3, drvTermiosTtyGpibConfigureArgs};
static void drvTermiosTtyGpibConfigureCallFunc(const iocshArgBuf *args)
{
    drvTermiosTtyGpibConfigure (args[0].ival,
                                args[1].sval,
                                args[2].ival);
}

/*
 * This routine is called before multitasking has started, so there's
 * no race condition in the test/set of firstTime.
 */
static void 
drvTermiosTtyGpibRegisterCommands(void)
{
    static int firstTime = 1;
    if (firstTime) {
#ifndef vxWorks
        extern void registerEpicsIocshSttyCommand(void);
        registerEpicsIocshSttyCommand();
#endif /* vxWorks */
        firstTime = 0;
        iocshRegister(&drvTermiosTtyGpibConfigureFuncDef,drvTermiosTtyGpibConfigureCallFunc);
        epicsSignalInstallSigAlarmIgnore();
    }
}
epicsExportRegistrar(drvTermiosTtyGpibRegisterCommands);
