/*
 * serial_test.c
 *
 * Copyright (C) 1999  Michael McCormack
 *
 * A short test program for the TIOCIWAIT ioctl of the 
 * Linux serial port driver. Could be extended to further test the serial
 * driver. It requires a null modem cable of the following flavour:
 *
 *  /dev/cua0       /dev/cua1
 *     RXD <---------> TXD
 *     TXD <---------> RXD
 *     DTR <---------> DSR,RNG
 * DSR,RNG <---------> DTR
 *     CTS <---------> RTS
 *     RTS <---------> CTS
 *     GND <---------> GND
 * 
 * This program is distrubuted under the terms of the GNU Public Licence.
 */

#include<stdio.h>
#include<unistd.h>
#include<termios.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<sys/wait.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include<signal.h>

/* timers in microseconds */
#define A_LITTLE 100000
#define A_LOT    (10*A_LITTLE)

#define TIOCMIWAIT_RNG   TIOCM_RNG
#define TIOCMIWAIT_DSR   TIOCM_DSR
#define TIOCMIWAIT_DCD   TIOCM_CD
#define TIOCMIWAIT_CTS   TIOCM_CTS
#define TIOCMIWAIT_RX    0x1000
#define TIOCMIWAIT_BRK   0x2000
#define TIOCMIWAIT_TX    0x4000
#define TIOCMIWAIT_ERR   0x8000

#define TIOCMIWAIT_RETFLAGS 0x800000

const char *infile="/dev/cua0";
const char *outfile="/dev/cua1";

int rts_out(int fd)  /* toggle RTS */
{
    int modem;
    int r;

    r=ioctl(fd,TIOCMGET,&modem);
    if(r<0)
    {
        fprintf(stderr,"get modem info failed\n");
        return 1;
    }
    usleep(A_LITTLE);
    modem ^= TIOCM_RTS;
    r=ioctl(fd,TIOCMSET,&modem);
    if(r<0)
    {
        fprintf(stderr,"couldn't toggle RTS\n");
        return 1;
    }

    return 0;
}

int cts_in(int fd)  /* wait for a change in CTS */
{
    int r;

    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_CTS|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"CTS Wait failed (%d)\n",r);
        return 1;
    }
    if ( r!=TIOCMIWAIT_CTS)
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int dtr_out(int fd)  /* toggle DTR */
{
    int modem;
    int r;

    r=ioctl(fd,TIOCMGET,&modem);
    if(r<0)
    {
        fprintf(stderr,"get modem info failed\n");
        return 1;
    }
    usleep(A_LITTLE);
    modem ^= TIOCM_DTR;
    r=ioctl(fd,TIOCMSET,&modem);
    if(r>=0)
    {
        usleep(A_LITTLE);
        modem ^= TIOCM_DTR;
        r=ioctl(fd,TIOCMSET,&modem);
    }
    if(r<0)
    {
        fprintf(stderr,"couldn't toggle DTR\n");
        return 1;
    }

    return 0;
}

int dsr_in(int fd) /* wait for a change in DSR */
{
    int r;

    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_DSR|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"DSR Wait failed (%d)\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_DSR))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int brk_out(int fd)  /* send a break */
{
    int modem;
    int r;

    usleep(A_LITTLE);
    r = tcsendbreak(fd,0);
    if(r<0)
    {
        fprintf(stderr,"couldn't generate break condition\n");
        return 1;
    }

    return 0;
}

int brk_in(int fd) /* wait for break */
{
    int r;

    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_BRK|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"break wait failed (%d)\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_BRK))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int rng_in(int fd) /* wait for a change in RNG */
{
    int r;

    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_RNG|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"RNG Wait failed (%d)\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_RNG))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int tx_out(int fd)  /* send one character (0x00) */
{
    char testdataout[1];
    int n;

    n = sizeof testdataout;
    memset(testdataout,0,n);
    usleep(A_LITTLE);
    if(n!=write(fd,testdataout,n))
    {
        fprintf(stderr,"write failed\n");
        return 1;
    }
    return 0;
}

int rx_in(int fd) /* wait for a character to be received */
{
    char testdatain[1];
    int n,r;

    n = sizeof testdatain;
    memset(testdatain,0,n);

    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_RX|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"CTS Wait failed (%d)\n",r);
        return 1;
    }
    if ( r!=TIOCMIWAIT_RX)
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    tcflush(fd,TCIOFLUSH);
    return 0;
}

int any_in(int fd) /* wait for any DELTA */
{
    int r;

    tcflush(fd,TCIOFLUSH);
    /* clear the wait flags */
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_RX|TIOCMIWAIT_DSR|
                          TIOCMIWAIT_CTS|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"CTS Wait failed (%d)\n",r);
        return 1;
    }
    return 0;
}

int pe_out(int fd)  /* send one character (0x00) with the wrong parity */
{
    char testdataout[1]={0xaa};
    int n,r;
    struct termios tios,tios_old;

    tcflush(fd,TCIOFLUSH);
    n = sizeof testdataout;
    usleep(A_LITTLE);
    /* clear the wait flags */
    tcgetattr(fd,&tios);
    memcpy(&tios_old,&tios,sizeof (struct termios));
    tios.c_iflag &= ~(IGNPAR|IGNBRK);
    tios.c_iflag |= INPCK;
    tios.c_cflag |= (PARENB | PARODD );
    if(0>tcsetattr(fd,TCSANOW,&tios))
    {
        fprintf(stderr,"Couldn't set port params\n");
        return 1;
    }
    r=write(fd,testdataout,n);
    if(n!=r)
    {
        fprintf(stderr,"write failed\n");
        return 1;
    }
    return 0;
}

int pe_in(int fd) /* wait for an error - triggered by a tx char with no par  */
{
    char testdatain[1];
    int n,r;
    struct termios tios;

    /* clear the wait flags */
    if(0>tcgetattr(fd,&tios))
    {
        fprintf(stderr,"Couldn't get port params\n");
        return 1;
    }
    tios.c_iflag &= ~IGNPAR;
    tios.c_iflag |= INPCK|PARMRK;
    tios.c_cflag &= ~PARODD;
    tios.c_cflag |= PARENB;
    if(0>tcsetattr(fd,TCSANOW,&tios))
    {
        fprintf(stderr,"Couldn't set port params\n");
        return 1;
    }

    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_ERR|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"parity error wait failed (%d)\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_ERR))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int fe_out(int fd)  /* send two characters at the wrong baud rate */
{
    char testdataout[]={0xa5,0x5a,0xa5};
    int n,r;
    struct termios tios;

    n = sizeof testdataout;
    usleep(A_LITTLE);
    /* clear the wait flags */
    tcgetattr(fd,&tios);
    cfsetispeed(&tios,B19200);
    cfsetospeed(&tios,B19200);
    if(0>tcsetattr(fd,TCSANOW,&tios))
    {
        fprintf(stderr,"Couldn't set port params\n");
        return 1;
    }
    r=write(fd,testdataout,n);
    if(n!=r)
    {
        fprintf(stderr,"write failed\n");
        return 1;
    }
    return 0;
}

int fe_in(int fd) /* wait for an error - triggered by a tx char wrong baud */
{
    char testdatain[1];
    int n,r;
    struct termios tios;

    tcgetattr(fd,&tios);
    tios.c_iflag &= ~IGNPAR; /* detect framing errors */
    tios.c_iflag |= INPCK;   /* check parity framing errors */
    if(0>tcsetattr(fd,TCSANOW,&tios))
    {
        fprintf(stderr,"Couldn't set port params\n");
        return 1;
    }
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_ERR|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"frame error wait failed (%d)\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_ERR))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

int nothing(int fd)
{
    return 0;
}

int tx_in(int fd)
{
    char testdataout[100]={1,2,3,4,5,6,7,8,9,10};
    int n,r,outcount;

    n = sizeof testdataout;
    r = ioctl(fd,TIOCMIWAIT,0);
    if( r < 0 )
    {
        fprintf(stderr,"clear failed (%d)\n",r);
        return 1;
    }
    if(n!=write(fd,testdataout,n))
    {
        fprintf(stderr,"write failed\n");
        return 1;
    }
    r=ioctl(fd,TIOCMIWAIT,TIOCMIWAIT_TX|TIOCMIWAIT_RETFLAGS);
    if( 0 > r )
    {
        fprintf(stderr,"tx holding empty wait failed (%d)\n",r);
        return 1;
    }
    if(0>ioctl(fd,TIOCOUTQ, &outcount))
    {
        fprintf(stderr,"tx buffer size read failed (%d)\n",r);
        return 1;
    }
    if( outcount)
    {
        fprintf(stderr,"tx buffer not really empty!\n",r);
        return 1;
    }
    if ( !(r&TIOCMIWAIT_TX))
    {
        fprintf(stderr,"returned flag incorrect\n");
        return 1;
    }
    return 0;
}

typedef struct tagTest
{
    int (*out)(int) ;
    int (*in)(int) ;
    char *name;
} TestList;

TestList tests[]=
{
    {dtr_out, dsr_in, "DSR delta wait"},
    {dtr_out, rng_in, "RNG delta wait"},
    {rts_out, cts_in, "CTS delta wait"},
    {brk_out, brk_in, "break condition wait"},
    {tx_out,  rx_in,  "RX char wait"},
    {dtr_out, any_in, "any INT, triggered by DSR"},
    {rts_out, any_in, "any INT, triggered by CTS"},
    {tx_out, any_in, "any INT, triggered by RX"},
    {pe_out, pe_in, "parity error wait"},
    {fe_out, fe_in, "frame error wait"},
    {nothing, tx_in, "transmit buffer empty wait"},
};

int init_serial(int fd)
{
    struct termios tios;

    if(fd<0)
    {
        return -1;
    }

    /* initialise child and parent serial ports */
    memset(&tios,0,sizeof (struct termios));
    cfmakeraw(&tios);
    cfsetispeed(&tios,B9600);
    cfsetospeed(&tios,B9600);
    tcsetattr(fd,TCSANOW,&tios);
    tcflush(fd,TCIOFLUSH);
    return fd;
}

void quit_now(int sig) 
{
    exit(1);
}

int dotests(int out,int in) 
{
    int i,rp,rc;
    pid_t child;

    for(i=0; i<(sizeof(tests)/sizeof(TestList)); i++)
    {
        init_serial(in);
        init_serial(out);
        fprintf(stderr,"Test %2d (%30s) ... ",i,tests[i].name);

        tcflush(in,TCIOFLUSH);
        tcflush(out,TCIOFLUSH);

        /* fork into two processes 
         * - the two serial ports should be connected by a 8 wire null modem
         */
        if((child = fork()) < 0)
        {
            fprintf(stderr,"Error - fork failed.\n");
            exit(1);
        }

        if(!child)
        {
            struct itimerval x;

            /* quit in one second if the test blocks */
            signal(SIGALRM,quit_now);
            x.it_value.tv_sec=A_LOT/1000000;
            x.it_value.tv_usec=A_LOT%1000000;
            x.it_interval.tv_sec=A_LOT/1000000;
            x.it_interval.tv_usec=A_LOT%1000000;
            setitimer(ITIMER_REAL,&x,NULL);

            rc = tests[i].in(in);
            exit(rc); /* exit child */
        }
        else
        {
            rp = tests[i].out(out);
            waitpid(child,&rc,0);
        }

        fprintf(stderr,(rc||rp)?"Fail\n":"Pass\n");
    }
    return 0;
}

int main(int argc, char **argv) 
{
    int in,out;

    in = open(infile,O_RDWR);
    if(in<0)
    {
        fprintf(stderr,"open %s failed\n",infile);
        return 1;
    }
    out = open(outfile,O_RDWR);
    if(out<0)
    {
        fprintf(stderr,"open %s failed\n",outfile);
        return 1;
    }

    fprintf(stderr,"testing %s -> %s ... \n",infile,outfile);
    dotests(in,out);

    fprintf(stderr,"testing %s -> %s ... \n",outfile,infile);
    dotests(out,in);

    close(in);
    close(out);
    return 0;
}
