/* 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 pthread 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 and 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: - Filter-Programm für die Ausgabe um Ausreißer herauszufiltern, die Daten zu glätten und bei 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). 2007-05-11 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // simple version number #define SOFTWARE_VERSION_NUMBER 1.021 // 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)) // see also http://www.gecius.de/linux/vc840/ { 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); // time 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 \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 (;;) // for every data packet { n = 0; timeval0 = 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 } } n += i_retval; // add number of received bytes if (0 == timeval0) // 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); }