]> sigrok.org Git - libsigrok.git/commitdiff
lcr/vc4080: introduce LCR packet parser for Voltcraft 4080 (and PeakTech 2165)
authorGerhard Sittig <redacted>
Sat, 15 Jun 2019 20:38:37 +0000 (22:38 +0200)
committerUwe Hermann <redacted>
Wed, 31 Jul 2019 20:40:44 +0000 (22:40 +0200)
Introduce the lcr/vc4080.c source file which implements the parser for
the serial data stream of the Voltcraft 4080 LCR meter. Add the meter to
the list of supported devices in the serial-lcr driver, as well as the
PeakTech 2165 LCR meter which is another compatible device.

This implementation contains a workaround for USB based serial cables
which seem to suffer from incomplete parity handling (observed with the
FT232R based PeakTech cable). Similar approaches were seen in existing
DMM drivers.

This implementation supports the main and secondary displays. The D and Q
"displays" which are communicated in the serial packets appear unreliable
and redundant, users can have the D and Q values shown in the supported
displays.

Makefile.am
src/hardware/serial-lcr/api.c
src/lcr/vc4080.c [new file with mode: 0644]
src/libsigrok-internal.h

index 5273a27f7f70307b2ee4ad4ec3818e1b3704cf32..66d5f5062c1919063fb06271cd1adf4c8114909e 100644 (file)
@@ -178,7 +178,8 @@ libsigrok_la_SOURCES += \
 # Hardware (LCR chip parsers)
 if NEED_SERIAL
 libsigrok_la_SOURCES += \
-       src/lcr/es51919.c
+       src/lcr/es51919.c \
+       src/lcr/vc4080.c
 endif
 
 # Hardware (Scale protocol parsers)
index 035d1b506329159df4ca1cf20facc3d57016f7a0..dd8f83081e81cb6e299db02745761594dde89be3 100644 (file)
@@ -399,3 +399,36 @@ SR_REGISTER_DEV_DRIVER_LIST(lcr_es51919_drivers,
        LCR_ES51919("peaktech-2170", "PeakTech", "2170"),
        LCR_ES51919("uni-t-ut612", "UNI-T", "UT612"),
 );
+
+#define LCR_VC4080(id, vendor, model) \
+       &((struct lcr_info) { \
+               { \
+                       .name = id, \
+                       .longname = vendor " " model, \
+                       .api_version = 1, \
+                       .init = std_init, \
+                       .cleanup = std_cleanup, \
+                       .scan = scan, \
+                       .dev_list = std_dev_list, \
+                       .dev_clear = std_dev_clear, \
+                       .config_get = config_get, \
+                       .config_set = config_set, \
+                       .config_list = config_list, \
+                       .dev_open = std_serial_dev_open, \
+                       .dev_close = std_serial_dev_close, \
+                       .dev_acquisition_start = dev_acquisition_start, \
+                       .dev_acquisition_stop = std_serial_dev_acquisition_stop, \
+                       .context = NULL, \
+               }, \
+               vendor, model, \
+               VC4080_CHANNEL_COUNT, vc4080_channel_formats, \
+               VC4080_COMM_PARAM, VC4080_PACKET_SIZE, \
+               500, vc4080_packet_request, \
+               vc4080_packet_valid, vc4080_packet_parse, \
+               NULL, NULL, vc4080_config_list, \
+       }).di
+
+SR_REGISTER_DEV_DRIVER_LIST(lcr_vc4080_drivers,
+       LCR_VC4080("peaktech-2165", "PeakTech", "2165"),
+       LCR_VC4080("voltcraft-4080", "Voltcraft", "4080"),
+);
diff --git a/src/lcr/vc4080.c b/src/lcr/vc4080.c
new file mode 100644 (file)
index 0000000..6083d5d
--- /dev/null
@@ -0,0 +1,701 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2019 Gerhard Sittig <gerhard.sittig@gmx.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <libsigrok/libsigrok.h>
+#include "libsigrok-internal.h"
+#include <math.h>
+#include <stdint.h>
+#include <string.h>
+
+#define LOG_PREFIX "vc4080"
+
+#ifdef HAVE_SERIAL_COMM
+
+/**
+ * @file Packet parser for Voltcraft 4080 LCR meters.
+ */
+
+/*
+ * Developer notes on the protocol and the implementation:
+ *
+ * The LCR meter is connected to a serial port (1200/7e1). The protocol
+ * is text based (printables plus some line termination), is accessible
+ * to interactive exploration in a terminal. Requests differ in length
+ * (single character, or sequence of seven characters in brackets).
+ * Responses either have 14 (setup) or 39 (measurement) characters.
+ * Thus the protocol lends itself to integration with the serial-lcr
+ * driver. Setup is handled outside of the acquisition loop, and all
+ * measurement results are of equal length and end in a termination
+ * that we can synchronize to. Requesting packets from the meter is
+ * similar to serial-dmm operation.
+ *
+ * Quick notes for our parser's purposes:
+ *
+ *   pkt[0] 'L'/'C'/'R'
+ *   pkt[1] 'Q'/'D'/'R'
+ *   pkt[2] 'A'/'B' output frequency
+ *   pkt[3] 'P'/'S' circuit model
+ *   pkt[4] 'A'/'M' auto/manual
+ *
+ *   pkt[5:9] main display value in text format, '8' switching range, '9' OL
+ *   pkt[10] main display range, '0'-'6', depends on RLC and freq and ser/par
+ *
+ *   pkt[11:14] secondary display value in text format, '9' OL
+ *   pkt[15] secondary display range, '1'-'5', depends on QDR and Rs value
+ *
+ *   pkt[16] packet sequence counter, cycling through '0'-'9'
+ *
+ *   pkt[17:20] D value in text form, '9' OL
+ *   pkt[21] D range
+ *
+ *   pkt[22:25] Q value in text form, '9' OL
+ *   pkt[26] Q range
+ *
+ *   pkt[27] 'S'/'_', SETup(?)
+ *   pkt[28] 'F'/'_', FUSE
+ *   pkt[29] 'H'/'_', HOLD
+ *   pkt[30] 'R' (present value), 'M' (max), 'I' (min), 'A' (avg),
+ *           'X' (max - min), '_' (normal)
+ *   pkt[31] 'R' (REL), 'S' (REL SET), '_' (normal)
+ *   pkt[32] 'L' (LIMITS), '_' (normal)
+ *   pkt[33] 'T' (TOL), 'S' (TOL SET), '_' (normal)
+ *   pkt[34] 'B' (backlight), '_' (normal)
+ *   pkt[35] 'A' (adapter inserted(?)), '_' (normal)
+ *   pkt[36] 'B' (low battery), '_' (normal)
+ *
+ *   pkt[37] always CR (\r)
+ *   pkt[38] always LF (\n)
+ *
+ * Example packet, PeakTech 2165, 1200/8n1 and parity bit stripped:
+ *
+ *   L Q A P A 9 0 0 0 0 6 1 4 0 6 2 1 0 7 1 1 4 1 4 0 6 2 _ _ _ _ _ _ _ _ _ _ CR LF
+ *   0         5         10        15        20        25        30        35     38
+ *
+ * Another example, resistance mode, 1k probed:
+ *
+ *   52 5f 42 5f 41 30 39 39 33 30 32 30 30 30 30 39 33 37 34 35 36 31 30 30 31 33 34 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 0d 0a
+ *   R _ B _ A 09930 2 00009 3 7456 1 0013 4 __________  CR/LF
+ *
+ * Another example, C mode:
+ *
+ *   43 51 42 53 4d 30 39 38 39 31 35 30 30 31 33 34 31 37 35 38 33 31 30 30 31 33 34 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 0d 0a
+ *   C  Q  B  S  M  09891 5           00134 1           7583 1         0013 4         ____...
+ *   C, Q, 120, ser, man, 09891 @2000uF -> C = 989.1uF, 00134 -> Q = 13.4
+ *
+ *   43 51 42 53 4d 30 39 38 38 30 35 30 30 31 33 34 34 37 35 37 34 31 30 30 31 33 34 5f 5f 5f 5f 5f 5f 5f 42 5f 5f 0d 0a
+ *   900uF (main)
+ *
+ * For more details see Conrad's summary document and PeakTech's manual:
+ * http://www.produktinfo.conrad.com/datenblaetter/100000-124999/121064-da-01-en-Schnittstellenbeschr_LCR_4080_Handmessg.pdf
+ * http://peaktech.de/productdetail/kategorie/lcr-messer/produkt/p-2165.html?file=tl_files/downloads/2001%20-%203000/PeakTech_2165_USB.pdf
+ *
+ * TODO
+ * - Check response lengths. Are line terminators involved during setup?
+ * - Check parity. Does FT232R not handle parity correctly? Neither 7e1 (as
+ *   documented) nor 7o1 (for fun) worked. 8n1 provided data but contained
+ *   garbage (LCR driver needs to strip off the parity bit?).
+ * - Determine whether the D and Q channels are required. It seems that
+ *   every LCR packet has space to provide these values, but we may as well
+ *   get away with just two channels, since users can select D and Q to be
+ *   shown in the secondary display. It's yet uncertain whether the D and Q
+ *   values in the packets are meaningful when the meter is not in the D/Q
+ *   measurement mode.
+ */
+
+/*
+ * Supported output frequencies and equivalent circuit models. A helper
+ * for the packet parser (accepting a "code" for the property, regardless
+ * of its position in the LCR packet), and a list for capability queries.
+ * Concentrated in a single spot to remain aware duing maintenance.
+ */
+
+static const double frequencies[] = {
+       SR_HZ(120), SR_KHZ(1),
+};
+
+static uint64_t get_frequency(char code)
+{
+       switch (code) {
+       case 'A': return SR_KHZ(1);
+       case 'B': return SR_HZ(120);
+       default: return 0;
+       }
+}
+
+enum equiv_model { MODEL_PAR, MODEL_SER, MODEL_NONE, };
+
+static const char *const circuit_models[] = {
+       "PARALLEL", "SERIES", "NONE",
+};
+
+static enum equiv_model get_equiv_model(char lcr_code, char model_code)
+{
+       switch (lcr_code) {
+       case 'L': /* EMPTY */ break;
+       case 'C': /* EMPTY */ break;
+       case 'R': return MODEL_NONE;
+       default: return MODEL_NONE;
+       }
+       switch (model_code) {
+       case 'P': return MODEL_PAR;
+       case 'S': return MODEL_SER;
+       default: return MODEL_NONE;
+       }
+}
+
+static const char *get_equiv_model_text(enum equiv_model model)
+{
+       return circuit_models[model];
+}
+
+/*
+ * Packet parse routine and its helpers. Depending on the specific layout
+ * of the meter's packet which communicates measurement results. Some of
+ * them are also used outside of strict packet parsing for value extraction.
+ */
+
+static uint64_t parse_freq(const uint8_t *pkt)
+{
+       return get_frequency(pkt[2]);
+}
+
+static const char *parse_model(const uint8_t *pkt)
+{
+       return get_equiv_model_text(get_equiv_model(pkt[0], pkt[3]));
+}
+
+static float parse_number(const uint8_t *digits, size_t length)
+{
+       char value_text[8];
+       float number;
+       int ret;
+
+       memcpy(value_text, digits, length);
+       value_text[length] = '\0';
+       ret = sr_atof_ascii(value_text, &number);
+
+       return (ret == SR_OK) ? number : 0;
+}
+
+/*
+ * Conrad's protocol description suggests that:
+ * - The main display's LCR selection, output frequency, and range
+ *   result in an Rs value in the 100R to 100k range, in addition to
+ *   the main display's scale for the value.
+ * - The secondary display's DQR selection, the above determined Rs
+ *   value, and range result in the value's scale.
+ * - The D and Q values' range seems to follow the secondary display's
+ *   logic.
+ */
+
+enum lcr_kind { LCR_NONE, LCR_IS_L, LCR_IS_C, LCR_IS_R, };
+enum dqr_kind { DQR_NONE, DQR_IS_D, DQR_IS_Q, DQR_IS_R, };
+
+static int get_main_scale_rs(int *digits, int *rs,
+       uint8_t range, enum lcr_kind lcr, uint64_t freq)
+{
+       /*
+        * Scaling factors for values. Digits count for 20000 full scale.
+        * Full scale values for different modes are:
+        *   R: 20R, 200R, 2k, 20k, 200k, 2M, 10M
+        *   L 1kHz: 2mH, 20mH, 200mH, 2H, 20H, 200H, 1000H
+        *   L 120Hz: 20mH, 200mH, 2H, 20H, 200H, 2kH, 10kH
+        *   C 1kHz: 2nF, 20nF, 200nF, 2uF, 20uF, 200uF, 2mF
+        *   C 120Hz: 20nF, 200nF, 2unF, 20uF, 200uF, 2muF, 20mF
+        */
+       static const int dig_r[] = { -3, -2, -1, +0, +1, +2, +3, };
+       static const int dig_l_1k[] = { -7, -6, -5, -4, -3, -2, -1, };
+       static const int dig_l_120[] = { -6, -5, -4, -3, -2, -1, 0, };
+       static const int dig_c_1k[] = { -13, -12, -11, -10, -9, -8, -7, };
+       static const int dig_c_120[] = { -12, -11, -10, -9, -8, -7, -6, };
+       /*
+        * Rs values for the scale, depending on LCR mode.
+        * Values for R/L: 100R, 100R, 100R, 1k, 10k, 100k, 100k
+        * Values for C: 100k, 100k, 10k, 1k, 100R, 100R, 100R
+        */
+       static const int rs_r_l[] = {
+               100, 100, 100, 1000, 10000, 100000, 100000,
+       };
+       static const int rs_c[] = {
+               100000, 100000, 10000, 1000, 100, 100, 100,
+       };
+
+       const int *digits_table, *rs_table;
+
+       /* The 'range' input value is only valid between 0..6. */
+       if (range > 6)
+               return SR_ERR_DATA;
+
+       if (lcr == LCR_IS_R) {
+               digits_table = dig_r;
+               rs_table = rs_r_l;
+       } else if (lcr == LCR_IS_L && freq == SR_KHZ(1)) {
+               digits_table = dig_l_1k;
+               rs_table = rs_r_l;
+       } else if (lcr == LCR_IS_L && freq == SR_HZ(120)) {
+               digits_table = dig_l_120;
+               rs_table = rs_r_l;
+       } else if (lcr == LCR_IS_C && freq == SR_KHZ(1)) {
+               digits_table = dig_c_1k;
+               rs_table = rs_c;
+       } else if (lcr == LCR_IS_C && freq == SR_HZ(120)) {
+               digits_table = dig_c_120;
+               rs_table = rs_c;
+       } else {
+               return SR_ERR_DATA;
+       }
+
+       if (digits)
+               *digits = digits_table[range];
+       if (rs)
+               *rs = rs_table[range];
+
+       return SR_OK;
+}
+
+static int get_sec_scale(int *digits, uint8_t range, enum dqr_kind dqr, int rs)
+{
+       static const int dig_d_q[] = { 0, -1, -2, -3, -4, 0, };
+       static const int dig_r_100[] = { 0, -2, -1, +0, +1, 0, };
+       static const int dig_r_1k_10k[] = { 0, -2, -1, +0, +1, +2, };
+       static const int dig_r_100k[] = { 0, 0, -1, +0, +1, +2, };
+
+       const int *digits_table;
+
+       /*
+        * Absolute 'range' limits are 1..5, some modes have additional
+        * invalid positions (these get checked below).
+        */
+       if (range < 1 || range > 5)
+               return SR_ERR_DATA;
+
+       if (dqr == DQR_IS_D || dqr == DQR_IS_Q) {
+               if (range > 4)
+                       return SR_ERR_DATA;
+               digits_table = dig_d_q;
+       } else if (dqr == DQR_IS_R && rs == 100) {
+               if (range > 4)
+                       return SR_ERR_DATA;
+               digits_table = dig_r_100;
+       } else if (dqr == DQR_IS_R && (rs == 1000 || rs == 10000)) {
+               digits_table = dig_r_1k_10k;
+       } else if (dqr == DQR_IS_R && rs == 100000) {
+               if (range < 2)
+                       return SR_ERR_DATA;
+               digits_table = dig_r_100k;
+       } else {
+               return SR_ERR_DATA;
+       }
+
+       if (digits)
+               *digits = digits_table[range];
+
+       return SR_OK;
+}
+
+static void parse_measurement(const uint8_t *pkt, float *floatval,
+       struct sr_datafeed_analog *analog, size_t disp_idx)
+{
+       enum lcr_kind lcr;
+       enum dqr_kind dqr;
+       uint64_t freq;
+       enum equiv_model model;
+       gboolean is_auto, main_ranging, main_ol, sec_ol, d_ol, q_ol;
+       float main_value, sec_value, d_value, q_value;
+       char main_range, sec_range, d_range, q_range;
+       gboolean is_hold, is_relative, has_adapter, is_lowbatt;
+       enum minmax_kind {
+               MINMAX_MAX, MINMAX_MIN, MINMAX_SPAN,
+               MINMAX_AVG, MINMAX_CURR, MINMAX_NONE,
+       } minmax;
+       gboolean is_parallel;
+       int mq, mqflags, unit;
+       float value;
+       int digits, exponent;
+       gboolean ol, invalid;
+       int ret, rs, main_digits, sec_digits, d_digits, q_digits;
+       int main_invalid, sec_invalid, d_invalid, q_invalid;
+
+       /* Prepare void return values for error paths. */
+       analog->meaning->mq = 0;
+       analog->meaning->mqflags = 0;
+       if (disp_idx >= VC4080_CHANNEL_COUNT)
+               return;
+
+       /*
+        * The interpretation of secondary displays may depend not only
+        * on the meter's status (indicator flags), but also on the main
+        * display's current value (ranges, scaling). Unconditionally
+        * inspect most of the packet's content, regardless of which
+        * display we are supposed to extract the value for in this
+        * invocation.
+        *
+        * While we are converting the input text, check a few "fatal"
+        * conditions early, cease further packet inspection when the
+        * value is unstable or not yet available, or when the meter's
+        * current mode/function is not supported by this LCR parser.
+        */
+       switch (pkt[0]) {
+       case 'L': lcr = LCR_IS_L; break;
+       case 'R': lcr = LCR_IS_R; break;
+       case 'C': lcr = LCR_IS_C; break;
+       default: return;
+       }
+       switch (pkt[1]) {
+       case 'D': dqr = DQR_IS_D; break;
+       case 'Q': dqr = DQR_IS_Q; break;
+       case 'R': dqr = DQR_IS_R; break;
+       case '_': dqr = DQR_NONE; break; /* Can be valid, like in R mode. */
+       default: return;
+       }
+       freq = get_frequency(pkt[2]);
+       model = get_equiv_model(pkt[0], pkt[3]);
+       is_auto = pkt[4] == 'A';
+       main_ranging = pkt[5] == '8';
+       if (main_ranging)       /* Switching ranges. */
+               return;
+       main_ol = pkt[5] == '9';
+       main_value = parse_number(&pkt[5], 5);
+       main_range = pkt[10];
+       if (main_range < '0' || main_range > '6')
+               main_range = '9';
+       main_range -= '0';
+       /*
+        * Contrary to the documentation, there have been valid four-digit
+        * values in the secondary display which start with '9'. Let's not
+        * consider these as overflown. Out-of-range 'range' specs for the
+        * secondary display will also invalidate these values.
+        */
+       sec_ol = 0 && pkt[11] == '9';
+       sec_value = parse_number(&pkt[11], 4);
+       sec_range = pkt[15];
+       if (sec_range < '0' || sec_range > '6')
+               sec_range = '9';
+       sec_range -= '0';
+       d_ol = pkt[17] == '9';
+       d_value = parse_number(&pkt[17], 4);
+       d_range = pkt[21];
+       if (d_range < '0' || d_range > '6')
+               d_range = '9';
+       d_range -= '0';
+       q_ol = pkt[22] == '9';
+       q_value = parse_number(&pkt[22], 4);
+       q_range = pkt[26];
+       if (q_range < '0' || q_range > '6')
+               q_range = '9';
+       d_range -= '0';
+       switch (pkt[27]) {
+       case 'S': return;       /* Setup mode. Not supported. */
+       case '_': /* EMPTY */ break;
+       default: return;        /* Unknown. */
+       }
+       is_hold = pkt[29] == 'H';
+       switch (pkt[30]) {      /* Min/max modes. */
+       case 'R': minmax = MINMAX_CURR; break;  /* Live reading. */
+       case 'M': minmax = MINMAX_MAX; break;
+       case 'I': minmax = MINMAX_MIN; break;
+       case 'X': minmax = MINMAX_SPAN; break;  /* "Max - min" difference. */
+       case 'A': minmax = MINMAX_AVG; break;
+       case '_': minmax = MINMAX_NONE; break;
+       default: return;        /* Unknown. */
+       }
+       if (minmax == MINMAX_SPAN)      /* Not supported. */
+               return;
+       if (minmax == MINMAX_CURR)      /* Normalize. */
+               minmax = MINMAX_NONE;
+       switch (pkt[31]) {
+       case 'R': is_relative = TRUE; break;
+       case 'S': return;       /* Relative setup. Not supported. */
+                               /* TODO Is this SR_MQFLAG_REFERENCE? */
+       case '_': is_relative = FALSE; break;
+       default: return;        /* Unknown. */
+       }
+       if (pkt[32] != '_')     /* Limits. Not supported. */
+               return;
+       if (pkt[33] != '_')     /* Tolerance. Not supported. */
+               return;
+       has_adapter = pkt[35] == 'A';
+       is_lowbatt = pkt[36] == 'B';
+
+       /*
+        * Always need to inspect the main display's properties, to
+        * determine how to interpret the secondary displays.
+        */
+       rs = main_digits = sec_digits = d_digits = q_digits = 0;
+       main_invalid = sec_invalid = d_invalid = q_invalid = 0;
+       ret = get_main_scale_rs(&main_digits, &rs, main_range, lcr, freq);
+       if (ret != SR_OK)
+               main_invalid = 1;
+       ret = get_sec_scale(&sec_digits, sec_range, dqr, rs);
+       if (ret != SR_OK)
+               sec_invalid = 1;
+       ret = get_sec_scale(&d_digits, d_range, dqr, rs);
+       if (ret != SR_OK)
+               d_invalid = 1;
+       ret = get_sec_scale(&q_digits, q_range, dqr, rs);
+       if (ret != SR_OK)
+               q_invalid = 1;
+
+       /* Determine the measurement value and its units. Apply scaling. */
+       is_parallel = model == MODEL_PAR;
+       mq = 0;
+       mqflags = 0;
+       unit = 0;
+       switch (disp_idx) {
+       case VC4080_DISPLAY_PRIMARY:
+               invalid = main_invalid;
+               if (invalid)
+                       break;
+               if (lcr == LCR_IS_L) {
+                       mq = is_parallel
+                               ? SR_MQ_PARALLEL_INDUCTANCE
+                               : SR_MQ_SERIES_INDUCTANCE;
+                       unit = SR_UNIT_HENRY;
+               } else if (lcr == LCR_IS_C) {
+                       mq = is_parallel
+                               ? SR_MQ_PARALLEL_CAPACITANCE
+                               : SR_MQ_SERIES_CAPACITANCE;
+                       unit = SR_UNIT_FARAD;
+               } else if (lcr == LCR_IS_R) {
+                       mq = is_parallel
+                               ? SR_MQ_PARALLEL_RESISTANCE
+                               : SR_MQ_SERIES_RESISTANCE;
+                       unit = SR_UNIT_OHM;
+               }
+               value = main_value;
+               ol = main_ol;
+               digits = 0;
+               exponent = main_digits;
+               break;
+       case VC4080_DISPLAY_SECONDARY:
+               invalid = sec_invalid;
+               if (invalid)
+                       break;
+               if (dqr == DQR_IS_D) {
+                       mq = SR_MQ_DISSIPATION_FACTOR;
+                       unit = SR_UNIT_UNITLESS;
+               } else if (dqr == DQR_IS_Q) {
+                       mq = SR_MQ_QUALITY_FACTOR;
+                       unit = SR_UNIT_UNITLESS;
+               } else if (dqr == DQR_IS_R) {
+                       mq = SR_MQ_RESISTANCE;
+                       unit = SR_UNIT_OHM;
+               }
+               value = sec_value;
+               ol = sec_ol;
+               digits = 0;
+               exponent = sec_digits;
+               break;
+#if VC4080_WITH_DQ_CHANS
+       case VC4080_DISPLAY_D_VALUE:
+               invalid = d_invalid;
+               if (invalid)
+                       break;
+               mq = SR_MQ_DISSIPATION_FACTOR;
+               unit = SR_UNIT_UNITLESS;
+               value = d_value;
+               ol = d_ol;
+               digits = 4;
+               exponent = d_digits;
+               break;
+       case VC4080_DISPLAY_Q_VALUE:
+               invalid = q_invalid;
+               if (invalid)
+                       break;
+               mq = SR_MQ_QUALITY_FACTOR;
+               unit = SR_UNIT_UNITLESS;
+               value = q_value;
+               ol = q_ol;
+               digits = 4;
+               exponent = q_digits;
+               break;
+#else
+       (void)d_invalid;
+       (void)d_value;
+       (void)d_ol;
+       (void)d_digits;
+       (void)q_invalid;
+       (void)q_value;
+       (void)q_ol;
+       (void)q_digits;
+#endif
+       default:
+               /* ShouldNotHappen(TM). Won't harm either. Silences warnings. */
+               return;
+       }
+       if (invalid)
+               return;
+       if (is_auto)
+               mqflags |= SR_MQFLAG_AUTORANGE;
+       if (is_hold)
+               mqflags |= SR_MQFLAG_HOLD;
+       if (is_relative)
+               mqflags |= SR_MQFLAG_RELATIVE;
+       if (has_adapter)
+               mqflags |= SR_MQFLAG_FOUR_WIRE;
+       switch (minmax) {
+       case MINMAX_MAX:
+               mqflags |= SR_MQFLAG_MAX;
+               break;
+       case MINMAX_MIN:
+               mqflags |= SR_MQFLAG_MIN;
+               break;
+       case MINMAX_SPAN:
+               mqflags |= SR_MQFLAG_MAX | SR_MQFLAG_RELATIVE;
+               break;
+       case MINMAX_AVG:
+               mqflags |= SR_MQFLAG_AVG;
+               break;
+       case MINMAX_CURR:
+       case MINMAX_NONE:
+       default:
+               /* EMPTY */
+               break;
+       }
+
+       /* "Commit" the resulting value. */
+       if (ol) {
+               value = INFINITY;
+       } else {
+               value *= powf(10, exponent);
+               digits -= exponent;
+       }
+       *floatval = value;
+       analog->meaning->mq = mq;
+       analog->meaning->mqflags = mqflags;
+       analog->meaning->unit = unit;
+       analog->encoding->digits = digits;
+       analog->spec->spec_digits = digits;
+
+       /* Low battery is rather severe, the measurement could be invalid. */
+       if (is_lowbatt)
+               sr_warn("Low battery.");
+}
+
+/*
+ * Workaround for cables' improper(?) parity handling.
+ * TODO Should this move to serial-lcr or even common libsigrok code?
+ *
+ * Implementor's note: Serial communication is documented to be 1200/7e1.
+ * But practial setups with the shipped FT232R cable received no response
+ * at all with these settings. The 8n1 configuration resulted in responses
+ * while the LCR meter's packet parser then needs to strip the parity bits.
+ *
+ * Let's run this slightly modified setup for now, until more cables and
+ * compatible devices got observed and the proper solution gets determined.
+ * This cheat lets us receive measurement data right now. Stripping the
+ * parity bits off the packet bytes here in the parser is an idempotent
+ * operation that happens to work during stream detect as well as in the
+ * acquisition loop. It helps in the 8n1 configuration, and keeps working
+ * transparently in the 7e1 configuration, too. No harm is done, and the
+ * initial device support is achieved.
+ *
+ * By coincidence, the 'N' command which requests the next measurement
+ * value happens to conform with the 7e1 frame format (0b_0100_1110
+ * byte value). When the SETUP commands are supposed to work with this
+ * LCR meter as well, then the serial-lcr driver's TX data and RX data
+ * probably needs to pass LCR chip specific transformation routines,
+ * if the above mentioned parity support in serial cables issue has not
+ * yet been resolved.
+ */
+
+static void strip_parity_bit(uint8_t *p, size_t l)
+{
+       while (l--)
+               *p++ &= ~0x80;
+}
+
+/* LCR packet parser's public API. */
+
+SR_PRIV const char *vc4080_channel_formats[VC4080_CHANNEL_COUNT] = {
+       "P1", "P2",
+#if VC4080_WITH_DQ_CHANS
+       "D", "Q",
+#endif
+};
+
+SR_PRIV int vc4080_packet_request(struct sr_serial_dev_inst *serial)
+{
+       static const char *command = "N";
+
+       serial_write_blocking(serial, command, strlen(command), 0);
+
+       return SR_OK;
+}
+
+SR_PRIV gboolean vc4080_packet_valid(const uint8_t *pkt)
+{
+       /* Workaround for funny serial cables. */
+       strip_parity_bit((void *)pkt, VC4080_PACKET_SIZE);
+
+       /* Fixed CR/LF terminator. */
+       if (pkt[37] != '\r' || pkt[38] != '\n')
+               return FALSE;
+
+       return TRUE;
+}
+
+SR_PRIV int vc4080_packet_parse(const uint8_t *pkt, float *val,
+       struct sr_datafeed_analog *analog, void *info)
+{
+       struct lcr_parse_info *parse_info;
+
+       /* Workaround for funny serial cables. */
+       strip_parity_bit((void *)pkt, VC4080_PACKET_SIZE);
+
+       parse_info = info;
+       if (!parse_info->ch_idx) {
+               parse_info->output_freq = parse_freq(pkt);
+               parse_info->circuit_model = parse_model(pkt);
+       }
+       if (val && analog)
+               parse_measurement(pkt, val, analog, parse_info->ch_idx);
+
+       return SR_OK;
+}
+
+/*
+ * These are the get/set/list routines for the _chip_ specific parameters,
+ * the _device_ driver resides in src/hardware/serial-lcr/ instead.
+ */
+
+SR_PRIV int vc4080_config_list(uint32_t key, GVariant **data,
+       const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
+{
+
+       (void)sdi;
+       (void)cg;
+
+       switch (key) {
+       case SR_CONF_OUTPUT_FREQUENCY:
+               *data = g_variant_new_fixed_array(G_VARIANT_TYPE_DOUBLE,
+                       ARRAY_AND_SIZE(frequencies), sizeof(frequencies[0]));
+               return SR_OK;
+       case SR_CONF_EQUIV_CIRCUIT_MODEL:
+               *data = g_variant_new_strv(ARRAY_AND_SIZE(circuit_models));
+               return SR_OK;
+       default:
+               return SR_ERR_NA;
+       }
+       /* UNREACH */
+}
+
+#endif
index adbc79f26c75b0fdec77247c4917a8c560adc06c..a0f00b247c5feceb54e2230fcd69cb2f169bab1e 100644 (file)
@@ -1661,6 +1661,33 @@ SR_PRIV gboolean es51919_packet_valid(const uint8_t *pkt);
 SR_PRIV int es51919_packet_parse(const uint8_t *pkt, float *floatval,
        struct sr_datafeed_analog *analog, void *info);
 
+/*--- lcr/vc4080.c ----------------------------------------------------------*/
+
+/* Note: Also uses 'struct lcr_parse_info' from es51919 above. */
+
+#define VC4080_PACKET_SIZE     39
+#define VC4080_COMM_PARAM      "1200/8n1"
+#define VC4080_WITH_DQ_CHANS   0 /* Enable separate D/Q channels? */
+
+enum vc4080_display {
+       VC4080_DISPLAY_PRIMARY,
+       VC4080_DISPLAY_SECONDARY,
+#if VC4080_WITH_DQ_CHANS
+       VC4080_DISPLAY_D_VALUE,
+       VC4080_DISPLAY_Q_VALUE,
+#endif
+       VC4080_CHANNEL_COUNT,
+};
+
+extern SR_PRIV const char *vc4080_channel_formats[VC4080_CHANNEL_COUNT];
+
+SR_PRIV int vc4080_config_list(uint32_t key, GVariant **data,
+       const struct sr_dev_inst *sdi, const struct sr_channel_group *cg);
+SR_PRIV int vc4080_packet_request(struct sr_serial_dev_inst *serial);
+SR_PRIV gboolean vc4080_packet_valid(const uint8_t *pkt);
+SR_PRIV int vc4080_packet_parse(const uint8_t *pkt, float *floatval,
+       struct sr_datafeed_analog *analog, void *info);
+
 /*--- dmm/ut372.c -----------------------------------------------------------*/
 
 #define UT372_PACKET_SIZE 27