/*
  Ausleseprogram für die billigen Conrad Digitalmultimeter Voltcraft VC 820 und VC 840.

  Reverse-Engineered von Georg Acher, Nov. 2001

  WWW:    http://www.acher.org
          http://wwwbode.cs.tum.edu/~acher/vc840/index.html
  e-mail: georg at acher dot org

  Das VC820/840 sendet 14 Bytes mit 2400Baud 8N1
  Das High-Nibble der Bytes entspricht der Position (1...e)
  Nur das Low-Nibble ist interessant!
  Die Low-Nibbles codieren LCD-Segmente:
  Nibble 0: AUTO|AC|TRUE_RMS|RS232
  Nibble 1-2: Minus, Digit 0
  Nibble 3-4: Dot, Digit 1
  Nibble 5-6: Dot, Digit 2
  Nibble 7-8: Dot, Digit 3
  Nibble 8-14: numkM A V Hz F Ohm °C % Rel Diode Beep Hold

------------------------------------------------------------------------------------------------------

From Rolf Fretag, 2004:
- timeouts with pthead and select
- consistence checks with receive time check
- consistence check with leap time check
- no wrap around of get_time
- check of minimum data packet length
- exit with pthread_kill, pthread_exit
- exclusive open
- option for the device file (e. g. /dev/ttyS0)

Im Gegensatz zu früher werden die empfangenen Datenpakete dadurch immer richtig erkannt
und defekte Datenpakete werden nun korrekt verworfen.
Dies ist wichtig wenn die Übertragung durch ein sehr schwaches Signal oder einen Wackelkontakt
stark gestört ist.

Mit USB-RS232-Adapter funktioniert es auch, aber bei einem zwischengeschalteten USB1-Hub gibt es häufig
Aussetzer.


TODO:
- Herausfinden, warum (unter Linux Kernel 2.6.8) ein Killen der Original-Version des Programms mit Ctrl-C
  zur ca. 5 % dazu führt, dass das Device danach (bis zum reboot) nicht mehr geöffnet werden kann, obwohl
  kein Prozess mehr darauf zugreift!
- Filter-Programm für die Ausgabe um Ausreißer herauszufiltern, die Daten zu glätten und beim schwacher
  Batterie oder Anstecken der seriellen Schnittstelle auftretende Fehler wie Daten einer Ampere-Messung
  bei einer Volt-Messung herauszufiltern.

Es können bei passendem ioctl auch mehrere dieser Programme eine Schnittstelle gleichzeitig auslesen, aber
da die Daten nicht dupliziert werden, also einzelne Bytes immer von nur einem Prozess gelesen werden, gibt
das etwas Datenmüll und einige timeouts, funktioniert aber.

compile e. g. with
gcc -O3 -lpthread -o vc840 vc840.c

Licence: GPL (GNU PUBLIC LICENCE).

2005-09-06

*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <iso646.h>
#include <string.h>
#include <stdint.h>
#include <math.h>

// simple version number
#define SOFTWARE_VERSION_NUMBER 1.01

// standard flags: 8 bit per byte, enable receiver, lower  modem  control lines after last process closes the device
// #define STD_FLG (CS8|CREAD|HUPCL) 

// Dividing of (unsigned) integer numbers with good rounding (with minimized quantisation error).
// Integer division (of positive numbers) ignores everything behind the point while good rounding
// rounds up >= .5 and rounds down < .5 (kaufmännische Rundung).
#define mc_POS_DIV(a, b)  ( (a)/(b) +  ( ( (a) % (b) >= (b)/2 ) ? 1 : 0 ) )


static int fd;                  // file descriptor
static long long int g_t;       // start time

// time with microseconds
inline signed long long int
get_time ()
{
  static struct timeval ti;
  static struct timezone tzp;

  gettimeofday (&ti, &tzp);
  return (ti.tv_usec + 1000000 * ((long long int) ti.tv_sec));
}


/*---------------------------------------------------------------------*/
int
open_serial (const char *device, int baudrate)
{
  int fd = 0;
  speed_t bdflag = 0;
  static struct termios tty;
  int modelines = 0;

  fprintf (stderr, "Using serial output to %s, baudrate %i\n", device, baudrate);
  switch (baudrate)
  {
    case 0:
      bdflag = B0;              // is used to terminate the connection
      break;
    case 50:
      bdflag = B50;
      break;
    case 75:
      bdflag = B75;
      break;
    case 110:
      bdflag = B110;
      break;
    case 134:
      bdflag = B134;
      break;
    case 150:
      bdflag = B150;
      break;
    case 200:
      bdflag = B200;
      break;
    case 300:
      bdflag = B300;
      break;
    case 600:
      bdflag = B600;
      break;
    case 1200:
      bdflag = B1200;
      break;
    case 1800:
      bdflag = B1800;
      break;
    case 2400:                 // vc820/vc840
      bdflag = B2400;
      break;
    case 4800:
      bdflag = B4800;
      break;
    case 9600:
      bdflag = B9600;
      break;
    case 19200:
      bdflag = B19200;
      break;
    case 38400:
      bdflag = B38400;
      break;
    case 57600:
      bdflag = B57600;
      break;
    case 115200:
      bdflag = B115200;
      break;
    case 230400:
      bdflag = B230400;
      break;
    case 460800:
      bdflag = B460800;
      break;
    case 500000:
      bdflag = B500000;
      break;
    case 576000:
      bdflag = B576000;
      break;
    case 921600:
      bdflag = B921600;
      break;
    case 1000000:
      bdflag = B1000000;
      break;
    case 1152000:
      bdflag = B1152000;
      break;
    case 1500000:
      bdflag = B1500000;
      break;
    case 2000000:
      bdflag = B2000000;
      break;
    case 2500000:
      bdflag = B2500000;
      break;
    case 3000000:
      bdflag = B3000000;
      break;
    case 3500000:
      bdflag = B3500000;
      break;
    case 4000000:
      bdflag = B4000000;
      break;
    default:
      fprintf (stderr, "Unsupported baudrate %i\n", baudrate);
      fprintf (stderr, "Supported are: 0, 50, 70, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, 9600, 19200,\n");
      fprintf (stderr, "38400, 57600, 115200, 230400, 460800, 500 k, 576 k, 921.6 k, 1 M, 1.152 M, 1.5 M, 2 M, 2.5 M,\n");
      fprintf (stderr, "3 M, 3.5 M and 4 M.\n");
      exit (-1);
  }
  /* Öffnen und Überprüfen des Filedeskriptors with the read+write mode and waiting mode */
  fd = open (device, O_RDWR | O_NOCTTY | O_NDELAY);
/* The O_NOCTTY flag tells UNIX that this program doesn't want to be the "controlling terminal"
   for that port. If you don't specify this then any input (such as keyboard abort signals and
   so forth) will affect your process. Programs like getty(1M/8) use this feature when starting
   the login process, but normally a user program does not want this behavior.
   The O_NDELAY flag tells UNIX that this program doesn't care what state the DCD signal line is
   in - whether the other end of the port is up and running. If you do not specify this flag, your
   process will be put to sleep until the DCD signal line is the space voltage.
*/
  if (0 >= fd)                  //  == -1)
  {
    perror ("open failed");
    exit (-1);
  }
  if (!isatty (fd))
  {
    fprintf (stderr, "The Device %s is no Terminal-Device !\n", device);
    return (-1);
  }
  fprintf (stderr, "open %s done.\n", device);
  tcgetattr (fd, &tty);
  tty.c_iflag = IGNBRK | IGNPAR;        /*Ignoriere BREAKS beim Input *//*Keine Parität */
  tty.c_oflag = 0;              // raw output
  tty.c_lflag = 0;              // Keine besonderen Angaben nötig
  tty.c_line = 0;
  tty.c_cc[VTIME] = 0;          // no waiting
  tty.c_cc[VMIN] = 1;           // minimum of reading bytes, was 1
  tty.c_cflag = CS8 | CREAD | CLOCAL | HUPCL;   // was STD_FLG; 8N1, read, no modem status,
  cfsetispeed (&tty, bdflag);   // input
  cfsetospeed (&tty, bdflag);   // output
  fprintf (stderr, "cfsetispeed and cfsetospeed done.\n");
  if (-1 == tcsetattr (fd, TCSAFLUSH, &tty))
  {
    fprintf (stderr, "Error: Could not set terminal attributes for %s !\n", device);
  }
  // Set IO-Control-Parameter and check the device.
  if (-1 == ioctl (fd, TIOCEXCL, &modelines))   // Put the tty into exclusive mode.
  {
    fprintf (stderr, "Error at ioctl TIOCEXCL on %s!\n", device);
    perror ("ioctl()");
    return (-1);
  }
  fprintf (stderr, "ioctl done.\n");
  return (fd);
}


/*- set ready to sent and data terminal ready; seems to be necessary for the opto coupler -*/
void
set_rts_dtr (int fd)
{
  int arg = TIOCM_RTS | TIOCM_DTR;
  ioctl (fd, TIOCMBIS, &arg);
  arg = TIOCM_RTS;
  ioctl (fd, TIOCMBIC, &arg);
  return;
}


/*---------------------------------------------------------------------*/
int
digit (int x)
{
  int dig[10] = { 0xd7, 0x05, 0x5b, 0x1f, 0x27, 0x3e, 0x7e, 0x15, 0x7f, 0x3f };
  int n = 0;
  x = x & 0x7f;
  for (n = 0; n < 10; n++)
  {
    if (x == dig[n])
      return n;
  }
  return 0;
}


/*---------------------------------------------------------------------*/
void
dvm_unit (int y, int x, unsigned char c_z, char *s)
{
  char *prefix = "";
  char *unit = "";
  char *ext = "";
  char *ext1 = "";

  if (c_z bitand 0x01)
    ext1 = "HOLD";
  if (x & 0x2000)
    ext = "delta";
  else
  {
    if (x & 0x100000)
      ext = "Diode";
    else
    {
      if (x & 0x10000)
        ext = "Beep";
    }
  }
  // prefix: m or u or n ...
  if (x & 0x080000)
    prefix = "m";
  else
  {
    if (x & 0x800000)
      prefix = "u";
    else
    {
      if (x & 0x400000)
        prefix = "n";
      else
      {
        if (x & 0x020000)
          prefix = "M";
        else
        {
          if (x & 0x200000)
            prefix = "k";
        }
      }
    }
  }
  // unit: A or V or Hz or ...
  if (x & 0x0800)
    unit = "A";
  else
  {
    if (x & 0x0200)
      unit = "Hz";
    else
    {
      if (x & 0x40000)
        unit = "%";
      else
      {
        if (x & 0x10)
          unit = "°C";
        else
        {
          if (x & 0x4000)
            unit = "Ohm";
          else
          {
            if (x & 0x0400)
              unit = "V";
            else
            {
              if (x & 0x8000)
                unit = "F";
            }
          }
        }
      }
    }
  }
  snprintf (s, 128, "%s%s %s (%s) %s", prefix, unit, (y & 0x8 ? "AC" : ""), ext, ext1);
  return;
}


// simple timeout
void *
func_timeout (void *threadid)
{
  sleep (5);                    // 5 s timeout
  if (0 >= fd)                  // device file could not be opened
  {
    fprintf (stderr, "Error: Timeout, device file could not be opened, exiting.\n");
    exit (-1);
  }
  pthread_exit (NULL);
}


// Cleartext and maybe raw output of the input buffer. Parameter: Flag for
// stdout/stderr as standard out.
void
dump0 (unsigned char *buffer, _Bool b_channel)
{
  int n;
  unsigned char buffer1[9] = { 0 };
  float it = 0.;
  long long int tf = 0;
  char units[20] = { 0 };

#ifdef DEBUG
  // Raw output
  for (n = 0; n < 16; n++)
    fprintf (stderr, "%02x ", buffer[n]);
#endif
  buffer1[0] = buffer[0] & 15;
  for (n = 0; n < 8; n++)
    buffer1[1 + n] = ((buffer[2 * n + 1] & 15) << 4) | (buffer[2 * n + 2] & 15);
#ifdef DEBUG
  // Nibble compacted data
  for (n = 0; n < 8; n++)
    fprintf (stderr, "%02x ", buffer1[n]);
  fprintf (stderr, "%i%i%i%i\n", digit (buffer1[1]), digit (buffer1[2]), digit (buffer1[3]), digit (buffer1[4]));
#endif
  if ((buffer1[3] & 0x7f) == 0x68)
    it = 9999999;
  else
    it = 1000.0 * digit (buffer1[1]) + 100.0 * digit (buffer1[2])       //
      + 10 * digit (buffer1[3]) + 1 * digit (buffer1[4]);
  // Dezimalpunkt
  if (buffer1[4] & 0x80)
    it = it / 10.0;
  if (buffer1[3] & 0x80)
    it = it / 100.0;
  if (buffer1[2] & 0x80)
    it = it / 1000.0;
  if (buffer1[1] & 0x80)
    it = -it;
  tf = mc_POS_DIV ((get_time (NULL) - g_t), 100000);    // tf in 100 ms
  dvm_unit (buffer1[0], (buffer1[5] << 16) | (buffer1[6] << 8) | buffer1[7], buffer[11], units);
  fprintf ((b_channel ? stderr : stdout), "%0.1f %0.3f %s\n", tf / 10., it, units);
  fflush (stdout);
  return;
}


// print the actual times and date to stderr
void
write_times (void)
{
  struct tm *tm;
  long long int tf;
  time_t t1 = time (NULL);

  tm = localtime (&t1);
  tf = mc_POS_DIV ((get_time (NULL) - g_t), 100000);    // tf in 100 ms
  fprintf (stderr, " Time %0.3f, local date and time:  %d-%d-%d, %d:%d:%d.\n", tf / 10., tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
           tm->tm_min, tm->tm_sec);
  return;
}


/*---------------------------------------------------------------------*/
int
main (int argc, char **argv)
{
  const char *device = argv[1]; // was "/dev/ttyS0", "/dev/ttyUSB0", ..., serial port
  // Caution: Some onboard ports do have huge delays of 47000 µs which causes timeouts!
  static unsigned char buffer[132];
  signed int i, n;
  // calculate deadline (timeout) in µs for receiving 1 Byte: 3 times transmission time of a byte (+stop bit +start bit: 10 bit)
  // (3 * 10 * 1000000) / 2400 = 12500 for fast USB-RS232-Adapters,
  // 25000 for slow USB-RS232-Adapters,
  // 50000 for slow onboard-ports
  const int i_deadline = 50000;
  long long int t = 0;
  int maxfd = 0, id = 0, i_ret = 0, i_retval;
  static struct timeval timeout;
  static fd_set readfs;         // file descriptor set
  static pthread_t thread;
  signed long long int lli_rec_time = 0, timeval0 = 0, timeval1 = 0;
  struct tm *tm;
  time_t t1 = 0;

  if ( (argc>1) and not strncmp (argv[1], "-V", 123) )
  {
    fprintf (stdout, "%s version %.1f\n", argv[0], SOFTWARE_VERSION_NUMBER);
    exit ( (2 == argc) ? 0 : -1);
  }
  if (argc < 2)
  {
    fprintf (stderr, "Usage: %s <device where the VC840 or VC820 is connected>\n", argv[0]);
    fprintf (stderr, "\aor: %s -V \n", argv[0]);
    fprintf (stderr, "[] = Optional <> = Parameter Requirement. 1 parameter required.\n");
    exit (-1);
  }
  // create thread for timeout (to avoid hangup in open_serial, e. g. when the device can not be opened)
  i_ret = pthread_create (&thread, NULL, func_timeout, (void *) &id);   //(void *(*)(void *))
  if (i_ret)
  {
    fprintf (stderr, "ERROR; return code from pthread_create() is %d\n", i_ret);
    exit (-1);
  }
  fd = open_serial (device, 2400);
  maxfd = fd + 1;               // maximum entry
  if (fd <= 0)
  {
    fprintf (stderr, "Error: Could not open port, exiting.\n");
    exit (-1);
  }
  set_rts_dtr (fd);             // DTR/RTS setzen
  tcflush (fd, TCIOFLUSH);      // flush buffers
  write_times ();
  // print time/date
  t1 = time (NULL);
  tm = localtime (&t1);
  fprintf (stdout, "# start date and time:  %d-%d-%d, %d:%d:%d.\n", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
  t = get_time (NULL);      
  g_t = t;
  for (;;)
  {
    n = 0;
    memset (buffer, 0, sizeof (buffer));
    for (;;)                    // receive the bytes one by one
    {
      memset (&timeout, 0, sizeof (timeout));
      timeout.tv_sec = 5;       // seconds
      FD_ZERO (&readfs);
      FD_SET (fd, &readfs);
      i_retval = select (maxfd, &readfs, NULL, NULL, &timeout);
      if (0 != i_retval)
      {
        if (-1 == i_retval)
        {
          perror ("select()");
          usleep (50000);       // wait 50 ms
          tcflush (fd, TCIOFLUSH);      // flush buffers
          continue;
        }
        i_retval = read (fd, &buffer[n], 1); // number of received bytes
	if (-1 == i_retval) // read error
	{
	    fprintf(stderr, "Error: read returned -1.\n");
	    continue;
	}
        if ((buffer[n] & 0xf0) == 0xe0 || (16 <= n))
          break;
        // check if the time from Byte n-1 to n is not too great; the data packet must be defragmented
        if (n++)
        {
          i = timeout.tv_usec + timeout.tv_sec * 1000000;       // time left
          i = 5000000 - i;      // receive time
          if (i > i_deadline)
          {
            fprintf (stderr, "Received data packet fragment: Receive time (in µs) %d > deadline %d, Byte %d.", i, i_deadline, n);
            write_times ();
            dump0 (buffer, 1);
            n = 0;              // nothing valid received
            usleep (50000);     // wait 50 ms
            tcflush (fd, TCIOFLUSH);    // flush buffers
            continue;           // restart receiving
          }
        }
        if (1 == n)             // start time counter for reading data
        {
          timeval0 = get_time (NULL);   // store the time of the actual Byte
          // check if the leap to the last data packet was long enough
          lli_rec_time = timeval0 - timeval1;
          if (lli_rec_time < 100000)    // less than 100 ms leap
          {
            fprintf (stderr, "Too small leap between the data packets: Only %lld µs.", lli_rec_time);
            write_times ();
            dump0 (buffer, 1);
            n = 0;              // nothing valid received
            usleep (50000);     // wait 50 ms
            tcflush (fd, TCIOFLUSH);    // flush buffers
          }
        }
      }
      else
      {
        fprintf (stderr, "Connection timeout (select failed).");
        write_times ();
      }
    }
    if (0 == n)                 // nothing received
      continue;
    timeval1 = get_time (NULL); // store the actual time of the actual data packet end
    lli_rec_time = timeval1 - timeval0;
    if ((lli_rec_time > 100000) or (lli_rec_time < 25000))      // more than 100 ms or less than 25 ms: invalid data
    {
      fprintf (stderr, "Receive timeout, invalid data packet (took %lld microseconds and must be between 25000 and 100000).", lli_rec_time);
      write_times ();
      dump0 (buffer, 1);
      continue;
    }
    if (n < 13)                 // no full packet received
    {
      fprintf (stderr, "Error: Only %d Bytes in the actual data packet. Ignoring.", n);
      write_times ();
      dump0 (buffer, 1);
      continue;
    }
    dump0 (buffer, 0);
    // usleep(100000); // wait 100 ms for next reading/writing (not really nessisary with VC820/VC840)
  }
  pthread_kill (thread, 15);    // terminate timeout thread
  pthread_exit (NULL);          // exit (0);
}
