/* geiger.c -- 
 * Created: Thu Jan 24 11:54:31 2002 by faith@alephnull.com
 * Revised: Tue Mar 26 15:12:17 2002 by faith@alephnull.com
 * Copyright 2002 Rickard E. Faith (faith@alephnull.com)
 *
 * Adapatation by Peer Janssen (peer@baden-online.de):
 * Jul 02 2005 - Use of calibration factor 1 as default value
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * 
 * $Id$
 * 
 */

/* This code collects data from the Aware Electronics RM-60 geiger
 * counter.  It probably works with other geiger counters from Aware
 * Electronics, but I haven't tried any others (although the output will
 * not be in uR/hr -- please update the gLog function to compute the
 * uR/hr correctly for other models).
 *
 * The pin out of the 4 wires from the geiger counter are as follows:
 *
 * RM-60     RS-232     DB-25
 * Ground    SG         7
 * Signal    RI         22       [goes low when an event occurs]
 * Positive  DTR        20
 * Negative  RTS        4
 *
 * I did all my initial testing with a breakout box with only these 4
 * pins and pin 1 (FG) connected.  Then I plugged in the DB-9 connector
 * to the RM-60 cable and did more tests without the breakout box.  I
 * had the best success with the standard serial port (/dev/ttyS0) and
 * wasn't able to get things to work with a port on an ISA-bus Cyclades
 * Cyclom-Y serial card (perhaps because it doesn't put out enough power
 * to power the device? -- I didn't explore further).
 */

#define DEFAULT_TTY "/dev/geiger"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <termios.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

#define GAVERAGE 5              /* Number of minutes to average over */
#define GWAIT    60             /* Number of seconds to wait (for testing) */

typedef struct gData {
    time_t        t;
    unsigned long count;
} gDataT;

static int           fd;
static double        click[GAVERAGE];
static int           currentClick, maxClick;
static gDataT        d0, d1;
static const char    *logfile;
static char          filename[2048];
static FILE          *str;

static void gLog(gDataT *d0, gDataT *d1)
{
    double        current, average, total;
    int           i;
/*    double        factor = 1000.0 / 1050.0; /* RM-60 */
    double        factor = 1; /* simple counting of pulses */

                                /* Compute uR/hr for previous minute.
                                 * Note that, for the RM-60,
                                 * counts/minute * 1000/1050 = uR/hr
                                 * (see http://www.aw-el.com/specs.htm) */
    time_t seconds = d1->t - d0->t;
    current = (double)(d1->count - d0->count) * factor * 60.0 / seconds;

                                /* Maintain table for computing average */
    click[currentClick++] = current;
    if (currentClick > maxClick) maxClick = currentClick;
    if (currentClick >= GAVERAGE) currentClick = 0;

                                /* Compute average */
    for (total = 0, i = 0; i < maxClick; i++) total += click[i];
    average = maxClick ? (total / maxClick) : 0.0;

    if (logfile) {
        char      tmpfilename[2048];
        time_t    t;
        struct tm *tm;

        time(&t);
        tm = localtime(&t);
        
        strftime(tmpfilename, sizeof(tmpfilename), logfile, tm);
        if (strcmp(tmpfilename, filename)) {
            strcpy(filename, tmpfilename);
            if (str) fclose(str);
            str = fopen(filename, "a");
        }
    }
    fprintf((logfile && str) ? str : stdout, "%lu %.1f %.1f # %24.24s\n",
            d1->t, current, average, ctime(&d1->t));
    fflush(str);
}

static void gRead(gDataT *data)
{
    struct serial_icounter_struct icount;
    
    time(&data->t);
    ioctl(fd, TIOCGICOUNT, &icount);
    data->count = icount.rng;
}

static void gHandler(int sig)
{
    gRead(&d1);
    gLog(&d0, &d1);
    d0 = d1;
    alarm(GWAIT);
}

static void gOpen(const char *port)
{
   struct termios term;
   int            info;

   if ((fd = open(port, O_RDWR|O_NOCTTY)) < 0) {
       perror( __FUNCTION__ );
       fprintf( stderr, "Cannot open \"%s\"\n", port );
       exit(1);
   }

                                /* This prevents two daemons from
                                 * running simultaneously, but it
                                 * otherwise optional. */
   if (lockf(fd, F_TLOCK, 0) < 0) {
       perror(__FUNCTION__);
       fprintf(stderr, "Cannot lock \"%s\"\n", port);
       close(fd);
       exit(1);
   }

   if (tcgetattr(fd, &term) < 0) {
       perror( __FUNCTION__ );
       fprintf( stderr, "tcgetattr failed\n" );
       exit(1);
   }

				/* Raw */
   cfmakeraw(&term);
   term.c_cflag |= HUPCL;

   term.c_cc[VMIN] = 0;
   term.c_cc[VTIME] = 0;

   cfsetospeed(&term, B2400);
   cfsetispeed(&term, B2400);
				/* Set */
   tcsetattr(fd, TCSANOW, &term);

                                /* Turn on power -- DTR high, RTS low */
   info = TIOCM_DTR;
   ioctl(fd, TIOCMBIS, &info);
   info = TIOCM_RTS;
   ioctl(fd, TIOCMBIC, &info);
   sleep(1);
}

static void help( void )
{
   static const char *help_msg[] = {
       "",
       "-t <tty>  specify tty (default = " DEFAULT_TTY ")",
       "-d        increase debugging level",
       "-l        logfile name in strftime(3) format",
       "-h        give this help",
       0 };
   const char        **p = help_msg;

   fprintf( stderr, "geiger 1.0\n");
   fprintf( stderr, "Copyright 2002 Rik Faith (faith@alephnull.com)\n" );
   while (*p) fprintf( stderr, "%s\n", *p++ );
}

int main( int argc, char **argv )
{
    int              debug   = 0;
    const char       *tty    = DEFAULT_TTY;
    int              c;
    struct sigaction sa;
    
    while ((c = getopt( argc, argv, "t:dl:")) != EOF)
        switch (c) {
        case 't': tty = optarg;     break;
        case 'd': ++debug;          break;
        case 'l': logfile = optarg; break;
        default:  help(); exit(0);
        }
    
    gOpen(tty);
    
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = gHandler;
    sigaction(SIGALRM, &sa, NULL);
    
    gRead(&d0);
    alarm(GWAIT);
    
    for (;;) {
        /* If we're debugging, we want the ioctl to return with each
         * change so that we can print something.  This would be useful
         * if we were trying to record the time between events.
         * However, if we aren't debugging, then we can let the kernel
         * service the interruption and keep count -- we don't need to
         * return to user space to have that happen.  See the code in
         * linux/drivers/char/serial.c to see how this works.
         *
         * Note that this ioctl can also return EIO and EINTR, so if
         * you're timing events, be sure to check that an event actually
         * happened.  Note that the delivery of SIGALRM causes the ioctl
         * to return. */
        ioctl(fd, TIOCMIWAIT, debug ? TIOCM_RNG : 0);
        if (debug) {
            time_t t;
            time(&t);
            printf("%lu\n", t);
        }
    }
   return 0;
}

