/* A program to read bytes from the Radio Shack ProbeScope and */
/* display the values on an X terminal                         */
/* compile using:                                              */
/*    gcc -I/usr/include/X11 -L/usr/X11R6/lib -lX11 \          */
/*         -o xProbeScope  xProbeScope.c                       */
/*                                                             */
/* Updated for later ProbeScopes by Scott Anderson, Aug 5,2004 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <sys/fcntl.h>
#include <sys/signal.h>
#include <sys/errno.h>
#include <sys/time.h>
#include <sys/types.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

/****************** Global Variables for X *********************/
int  ps_xinit(int argc, char *argv[]); /* set up X display */
void ps_x_events();    /* process X events */
void ps_x_settings();  /* display scope settings */
void ps_x_reticule();  /* display grid on scope face */
void ps_x_plots();     /* display the sample data points */

/****************** Global Variables for X *********************/
Display *display;
Window  window;
XSetWindowAttributes attributes;
XGCValues gr_values;
XFontStruct *fontinfo;
GC gr_context;
Visual *visual;
int depth;
int screen;
XEvent event;
XColor    colorWhite, colorBlack, colorRed, dummy;


/************** Global Variables for ProbeScope ************/
#define PS_BYTES  138
#define SET_LEN   20
int    psfd;   /* file desc. for serial port to the ProbeScope */
struct PS_SMPL {
    int  state;                /* wait4sync, insync, insample */
    char mode[SET_LEN];
    char coup[SET_LEN];
    char vert[SET_LEN];
    char timd[SET_LEN];
    char trig[SET_LEN];
    char tlvl[SET_LEN];
    char  bytes[PS_BYTES];     /* PS settings and samples */
} smpl;





/***************** How this program works ******************
   Open the serial port to the ProbeScope
   Initialize the X display
   Select loop {
      Get characters from the ProbeScope
      IF full sample
         display sample data
      Handle X events
   }

   The X window for this application is divided into two
   areas.  The left hand area has the probe settings and
   the right hand area has the plot of the data points.
   The settings area is SET_WD pixels wide and SET_LN
   pixels long.  The corresponding values for the data
   area are DAT_WD and DAT_LN.  There are always 128
   samples and the maximum value for any sample is 64.
   The data area is surrounded by a blank border BORDER
   pixels wide.  The data display area is a multiple of
   64 pixels high and a multiple of 140 pixels wide.  We
   round the 128 sample size to 140 in order to get an
   integer number of vertical grid lines (7).  Thus the
   number _samples_ per division is 20.
   SCALE_X and SCALE_Y specify how many pixels in each
   direction are used for each sample point.
   The number of pixels per vertical division is the total
   number of vertical pixels divided by 10, or 64*SCALE_Y/10.
   The number of pixels per horizontal division (PIX_PER_Y_DIV)
   is the total number of horizontal pixels divided by 7, or
   140*SCALE_X/7.
************************************************************/
#define X_TOPLEFT 200
#define Y_TOPLEFT 200
#define BORDER    10
/* Scales of 2,3 and 3,5 give nice looking displays */
#define SCALE_X   3
#define SCALE_Y   5
#define DAT_WD    ((2 * BORDER) + (SCALE_X * 140))
#define DAT_LN    ((2 * BORDER) + (SCALE_Y * 64))
#define NUM_X_DIV 7
#define NUM_Y_DIV 10
#define PIX_PER_X_DIV  ((140*SCALE_X)/NUM_X_DIV)
#define PIX_PER_Y_DIV  ((64*SCALE_Y)/NUM_Y_DIV)
#define SET_WD  200
#define SET_LN  DAT_LN


/*************** The Sample State Machine *******************
   The ProbeScope does not use any flow control on the serial
   line.  When it powers up, it starts sending.  So when this
   program starts we do not know where we are in a sample cycle.
   We assume we are in the middle of sample data and wait for
   a SYNC character.  Once we get a SYNC we start to wait for
   a non-SYNC character which is the start of a sample.  We
   collect the 137 bytes of the sample and display the plot.
   We are then back in the SYNCWAIT state and start to look
   for another SYNC.
************************************************************/
#define ST_SYNCWAIT     1
#define ST_INSYNC       2
#define ST_INSMPL       3


int ps_open(char *serport);
int ps_x_init(int argc, char *argv[]);
int ps_sample(void);

int main (int argc, char *argv[])
{
    fd_set rfds;  /* bit masks for select statement */
    int    mxfd;  /* Maximum FD for the select statement */
    int    xfd;   /* File Descriptor for the X display */

    /* Open the serial port to the ProbeScope */
    psfd = ps_open(argv[1]);

    /* Initialize the X display */
    xfd = ps_x_init(argc, argv);

    /* Initialize the sample state machine */
    smpl.state = ST_SYNCWAIT;

    /* Set a flag so the X Expose event does not try to plot a sample */
    strncpy(smpl.mode, "Awaiting sample", SET_LEN);

    /* Get sets of samples and display */
    mxfd = (psfd > xfd) ? (psfd+1) : (xfd+1);
    while (1) {
        FD_ZERO(&rfds);
        FD_SET(psfd, &rfds);
        FD_SET(xfd, &rfds);

        (void) select(mxfd, &rfds, (fd_set *)NULL,
                (fd_set *)NULL, (struct timeval *) NULL);
        if (FD_ISSET(psfd, &rfds)) {
            if (ps_sample()) {
                ps_x_settings();  /* put up settings */
                ps_x_reticule();  /* put up reticule */
                ps_x_plots();
                XFlush(display);
            }
        }

        if (FD_ISSET(xfd, &rfds)) {
            ps_x_events();
        }
    }
}



/* Initialize the X display                                   */
/* No inputs and the output is the file descriptor for the    */
/* X display.                                                 */
int ps_x_init(int argc, char *argv[])
{
    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Unable to open X display\n");
        exit(1);
    }
    screen = DefaultScreen(display);
    visual = DefaultVisual(display,screen);
    depth  = DefaultDepth(display,screen);
    attributes.background_pixel = XBlackPixel(display,screen);
 
    window = XCreateWindow( display,XRootWindow(display,screen),
                 X_TOPLEFT, Y_TOPLEFT, (SET_WD+DAT_WD), (SET_LN), 5,
                 depth,  InputOutput, visual ,CWBackPixel, &attributes);

    XSelectInput(display,window,ExposureMask | KeyPressMask) ;
    fontinfo = XLoadQueryFont(display,"9x15");
     
    XAllocNamedColor(display, DefaultColormap(display, screen),"white",
                      &colorWhite,&dummy);
    XAllocNamedColor(display, DefaultColormap(display, screen),"red",
                      &colorRed,&dummy);
 
    gr_values.font = fontinfo->fid;
    gr_values.foreground = colorWhite.pixel;
    gr_context=XCreateGC(display,window,GCFont+GCForeground, &gr_values);
    XSetStandardProperties(display,window, "xProbeScope", "xProbeScope",
                      (Pixmap)NULL, argv, argc, NULL);
    XMapWindow(display,window);
    XFlush(display);

    return(ConnectionNumber(display));
}


/* Handle X events                                            */
/* This routine is called just after the select() when there  */
/* is some activity on the X display.  We switch on the event */
/* and perform the processing necessary.                      */
void ps_x_events()
{
    XNextEvent(display,&event);

    switch(event.type){
    case Expose:
        ps_x_settings();  /* put up settings */
        ps_x_reticule();  /* put up reticule */
        if (smpl.mode[0] != 'A')  /* don't plot if 'Awaiting data' */
            ps_x_plots();
        XFlush(display);
        break;
    case KeyPress: 
        XCloseDisplay(display); 
        exit(0);
    }
}


/* ps_x_settings:  Display the strings in the setting  */
/* returns nothing.                                    */
void ps_x_settings()
{
    /* Note that all the strings are SET_LEN characters long */
    /* We could (should?) detect the font height and adjust  */
    /* the line spacing accordingly.  Maybe next release.    */
    XDrawImageString(display,window,gr_context,10,20,smpl.mode, SET_LEN);
    XDrawImageString(display,window,gr_context,10,40,smpl.coup, SET_LEN);
    XDrawImageString(display,window,gr_context,10,60,smpl.vert, SET_LEN);
    XDrawImageString(display,window,gr_context,10,80,smpl.timd, SET_LEN);
    XDrawImageString(display,window,gr_context,10,100,smpl.trig, SET_LEN);
    XDrawImageString(display,window,gr_context,10,120,smpl.tlvl, SET_LEN);
}


/* ps_x_reticule:  Display the grid pattern on the face of the scope */
/* returns nothing.                                                  */
void ps_x_reticule()
{
    int i,j;


    /* Draw the separator between the settings and the plot areas */
    XDrawLine(display,window,gr_context, SET_WD, 0, SET_WD, SET_LN);

    /* Let's use a different color for the reticule */
    XSetForeground(display, gr_context, colorRed.pixel);

    /* Draw the Vertical lines */
    for (i=SET_WD + BORDER; i <= SET_WD + DAT_WD - BORDER; i=i + PIX_PER_X_DIV) {
        XDrawLine(display,window,gr_context,
             i, DAT_LN - (NUM_Y_DIV * PIX_PER_Y_DIV) - BORDER,  /* start x and y */
             /*i, DAT_LN - BORDER); *//* end x,y */
             i, BORDER + NUM_Y_DIV * PIX_PER_Y_DIV); /* end x,y */
    }

    /* Draw the Horizontal lines */
    for (j=DAT_LN - BORDER; j >=BORDER; j=j - PIX_PER_Y_DIV) {
        XDrawLine(display,window,gr_context,
             SET_WD + BORDER, j,
             SET_WD + DAT_WD - BORDER, j); /* end x,y */
    }
    XSetForeground(display, gr_context, colorWhite.pixel);

    /* We could clear the whole display area before we start drawing */
    /* the reticule (grid) lines but this causes an annoying flicker */
    /* since the X display can update the screen after the clear but */
    /* before the new lines are drawn.                               */
    /* We circumvent this problem by drawing the lines then clearing */
    /* the 70 individual boxes formed by the grid.                   */
    /* (Warning: this code assumes a line width of 1 !)              */
    for (i=SET_WD + BORDER; i <= SET_WD + DAT_WD - BORDER; i=i + PIX_PER_X_DIV) {
        for (j=DAT_LN - BORDER; j >=BORDER; j=j - PIX_PER_Y_DIV) {
            XClearArea(display,window,i+1,j+1, PIX_PER_X_DIV-1, PIX_PER_Y_DIV-1,0);
        }
    }
}



/* ps_x_plots:  Plot the sample data                        */
/* returns nothing.                                         */
void ps_x_plots()
{
    /* The data to plot is in the 'bytes' part of the smpl    */
    /* structure starting at byte #5.  We plot the 128 points */
    /* as 127 line segments.                                  */
    int  i;   /* index into array of sample points */

    for (i=5; i<132; i++) {
        XDrawLine(display,window,gr_context,
            SET_WD + BORDER + ((i-5)*SCALE_X),            /* start point x value */
            SET_LN - BORDER - (SCALE_Y * smpl.bytes[i]),  /* start point y value */
            SET_WD + BORDER + ((i-4)*SCALE_X),              /* end point x value */
            SET_LN - BORDER - (SCALE_Y * smpl.bytes[i+1])); /* end point y value */
    }
}


/* ps_open:  Open the serial port specified */
/* returns:  The opened file descriptor */
int ps_open(char *serport)
{
    struct  termios    tbuf;
    int     fd_dev;

    if (( fd_dev = open(serport, (O_RDWR | O_NDELAY), 0)) < 0 ) {
        fprintf(stderr,"Unable to open tty port specified\n");
        exit(1);
    }

    tbuf.c_cflag = CS7|CREAD|CLOCAL;
    if (cfsetspeed(&tbuf, B19200)) {
        perror("cfsetspeed");
        exit(1);
    }
    tbuf.c_iflag = IGNBRK;
    tbuf.c_oflag = 0;
    tbuf.c_lflag = 0;
    tbuf.c_cc[VMIN] = 1; /* character-by-character input */
    tbuf.c_cc[VTIME]= 0; /* no delay waiting for characters */
    if (tcsetattr(fd_dev, TCSANOW, &tbuf) < 0) {
        fprintf(stderr, "pscope: Unable to set device '%s' parameters\n", serport);
        exit(1);
    }
    if (( fd_dev = open(serport, (O_RDWR), 0)) < 0 ) {
        fprintf(stderr, "Unable to open tty port specified\n");
        exit(1);
    }

    return(fd_dev);  /* return the serial port file descriptor */
}


/* Get Sample:  get a set of samples from the ProbeScope */
int ps_sample(void)
{
    static int  cnt=0;   /* byte counter (smpl[0] is the sync byte) */

    /* Switch on the state of sample process */
    switch (smpl.state) {
        case ST_SYNCWAIT:  /* Waiting for a sync byte */
            read(psfd, &smpl.bytes[cnt], 1);
            if ((0x7c & smpl.bytes[cnt]) == 0x7c) {
                smpl.state = ST_INSYNC;
                cnt = 1;
            }
            return(0);
            break;

        case ST_INSYNC:    /* Waiting for sample to start */
            read(psfd, &smpl.bytes[cnt], 1);
            if ((0x7c & smpl.bytes[cnt]) != 0x7c) {
                smpl.state = ST_INSMPL;
                cnt = 2;  /* just read byte 1, the first smpl byte */
            }
            return(0);
            break;

        case ST_INSMPL:    /* Read the setting bytes and the sample bytes */
            cnt += read(psfd, &smpl.bytes[cnt], PS_BYTES-cnt);
            if (cnt < PS_BYTES)
                return(0);   /* need more bytes for the sample */
            break;
    }

    /* At this point we have all 138 bytes of the sample.  We now */
    /* analyze the first few bytes to get the 'settings' strings. */

    /* A zero bit indicates coupling */
    switch(smpl.bytes[1] & 0x30) {
        case 0x10: strncpy(smpl.coup, "Coupling:  AC       ", SET_LEN); break;
        case 0x20: strncpy(smpl.coup, "Coupling:  DC       ", SET_LEN); break;
        case 0x00: strncpy(smpl.coup, "Coupling:  GND      ", SET_LEN); break;
    }

    /* A set bit indicates voltage range */
    switch(smpl.bytes[1] & 0x0c) {
        case 0x00: strncpy(smpl.vert, "Volt/div:  0.1      ", SET_LEN); break;
        case 0x04: strncpy(smpl.vert, "Volt/div:  1.0      ", SET_LEN); break;
        case 0x08: strncpy(smpl.vert, "Volt/div:  10       ", SET_LEN); break;
    }

    /* An integer indicated the time base */
    /* There are 20 samples per horizontal division so the time per division */
    /* is 20 times the sample rate. */
    switch(smpl.bytes[2]) {
        case 0: strncpy(smpl.timd, "Time/div:  1 us     ", SET_LEN); break;
        case 1: strncpy(smpl.timd, "Time/div:  2 us     ", SET_LEN); break;
        case 2: strncpy(smpl.timd, "Time/div:  10 us    ", SET_LEN); break;
        case 3: strncpy(smpl.timd, "Time/div:  20 us    ", SET_LEN); break;
        case 4: strncpy(smpl.timd, "Time/div:  100 us   ", SET_LEN); break;
        case 5: strncpy(smpl.timd, "Time/div:  200 us   ", SET_LEN); break;
        case 6: strncpy(smpl.timd, "Time/div:  1 ms     ", SET_LEN); break;
        case 7: strncpy(smpl.timd, "Time/div:  2 ms     ", SET_LEN); break;
        case 8: strncpy(smpl.timd, "Time/div:  10 ms    ", SET_LEN); break;
        case 9: strncpy(smpl.timd, "Time/div:  20 ms    ", SET_LEN); break;
    }

    /* A set LSB indicates SINGLE mode */
    switch(smpl.bytes[3] & 0x01) {
        case 0x00: strncpy(smpl.mode, "Mode:      Run      ", SET_LEN); break;
        case 0x01: strncpy(smpl.mode, "Mode:      Single   ", SET_LEN); break;
    }

    /* A set bit indicates the triggering */
    switch(smpl.bytes[3] & 0x78) {
        case 0x00: strncpy(smpl.trig, "Trigger:   Auto     ", SET_LEN); break;
        case 0x08: strncpy(smpl.trig, "Trigger:   -Ext     ", SET_LEN); break;
        case 0x10: strncpy(smpl.trig, "Trigger:   +Ext     ", SET_LEN); break;
        case 0x20: strncpy(smpl.trig, "Trigger:   -Int     ", SET_LEN); break;
        case 0x40: strncpy(smpl.trig, "Trigger:   +Int     ", SET_LEN); break;
    }

    /* A set bit indicates the trigger level */
    switch(smpl.bytes[4] & 0x01) {
        case 0x00: strncpy(smpl.tlvl, "Trig Lvl:  -0.5 Volt", SET_LEN); break;
        case 0x02: strncpy(smpl.tlvl, "Trig Lvl:  +0.5 Volt", SET_LEN); break;
        case 0x04: strncpy(smpl.tlvl, "Trig Lvl:  +0.3 Volt", SET_LEN); break;
        case 0x08: strncpy(smpl.tlvl, "Trig Lvl:  +0.1 Volt", SET_LEN); break;
        case 0x10: strncpy(smpl.tlvl, "Trig Lvl:  -0.1 Volt", SET_LEN); break;
        case 0x20: strncpy(smpl.tlvl, "Trig Lvl:  -0.3 Volt", SET_LEN); break;

    }

    /* We are done reading the settings.  The next 128 bytes are samples. */
    /* The samples start at byte 5.  Return and let the display           */
    /* subroutine plot it. */
    smpl.state = ST_SYNCWAIT; /* next byte should be a sync */
    cnt = 0;                  /* count starts at 0 since byte 0 is sync */
    return(1);                /* completed sample */
}
