]> sigrok.org Git - libsigrok.git/commitdiff
dmm/eev121gw: introduce parser for EEVblog 121GW 19-byte binary packets
authorGerhard Sittig <redacted>
Fri, 28 Sep 2018 16:39:23 +0000 (18:39 +0200)
committerUwe Hermann <redacted>
Sun, 14 Oct 2018 16:21:56 +0000 (18:21 +0200)
Introduce the dmm/eev121gw.c source file with parse routines for the
EEVblog 121GW meter's 19-bytes binary packets. Get the values and MQ
properties of the device's several displays (main, sub, bar) in several
individual parse calls.

This commit introduces initial support for the device. Some of the modes
and features are untested, as are some of the device's ranges.

Makefile.am
src/dmm/eev121gw.c [new file with mode: 0644]
src/libsigrok-internal.h

index aa950a343f14d9b9771ae53782a61ca358a8f7cc..148de0ff70108628d0230697c754e30ea26bfc5a 100644 (file)
@@ -149,6 +149,7 @@ libsigrok_la_SOURCES += \
        src/dmm/asycii.c \
        src/dmm/bm25x.c \
        src/dmm/dtm0660.c \
+       src/dmm/eev121gw.c \
        src/dmm/es519xx.c \
        src/dmm/fs9721.c \
        src/dmm/fs9922.c \
diff --git a/src/dmm/eev121gw.c b/src/dmm/eev121gw.c
new file mode 100644 (file)
index 0000000..4f13ba8
--- /dev/null
@@ -0,0 +1,1373 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2018 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 2 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/>.
+ */
+
+/**
+ * @file
+ *
+ * EEVblog 121GW 19-bytes binary protocol parser.
+ *
+ * @internal
+ *
+ * Note that this protocol is different from other meters. We need not
+ * decode the LCD presentation (segments a-g and dot of seven segment
+ * displays). Neither need we decode a textual presentation consisting
+ * of number strings with decimals, and scale/quantity suffixes. Instead
+ * a binary packet is received which contains an unsigned mantissa for
+ * the value, and a number of boolean flags as well as bitfields for modes
+ * and ranges.
+ *
+ * But the protocol is also similar to the four-display variant of the
+ * metex14 protocol. A single DMM packet contains information for two
+ * displays and a bargraph, as well as several flags corresponding to
+ * display indicators and global device state. The vendor's documentation
+ * refers to these sections as "main", "sub", "bar", and "icon".
+ *
+ * It's essential to understand that the serial-dmm API is only able to
+ * communicate a single float value (including its precision and quantity
+ * details) in a single parse call. Which is why we keep a channel index
+ * in the 'info' structure, and run the parse routine several times upon
+ * reception of a single packet. This approach is shared with the metex14
+ * parser.
+ *
+ * The parse routine here differs from other DMM parsers which typically
+ * are split into routines which parse a value (get a number and exponent),
+ * parse flags, and handle flags which were parsed before. The 121GW
+ * meter's packets don't fit this separation naturally, getting the value
+ * and related flags heavily depends on which display shall get inspected,
+ * thus should be done at the same time. Filling in an 'info' structure
+ * from packet content first, and mapping this 'info' to the 'analog'
+ * details then still is very useful for maintainability.
+ *
+ * TODO:
+ * - The meter is feature packed. This implementation does support basic
+ *   operation (voltage, current, power, resistance, continuity, diode,
+ *   capacitance, temperature). Support for remaining modes, previously
+ *   untested ranges, and advanced features (DC+AC, VA power, dB gain,
+ *   burden voltage) may be missing or incomplete. Ranges support and
+ *   value scaling should be considered "under development" in general
+ *   until test coverage was increased. Some flags are not evaluated
+ *   correctly yet, or not at all (min/max/avg, memory).
+ * - Test previously untested modes: current, power, gain, sub display
+ *   modes. Test untested ranges (voltage above 30V, temperature above
+ *   30deg (into the hundreds), negative temperatures, large resistors,
+ *   large capacitors). Test untested features (min/max/avg, 1ms peak,
+ *   log memory).
+ * - It's assumed that a continuous data stream was arranged for. This
+ *   implementation does not support the "packet request" API. Also I
+ *   was to understand that once the request was sent (write 0300 to
+ *   handle 9, after connecting) no further request is needed. Only
+ *   the loss of communication may need recovery, which we leave as an
+ *   option for later improvement, or as a feature of an external helper
+ *   which feeds the COM port from Bluetooth communication data, or
+ *   abstracts away the BLE communication.
+ *
+ * Implementation notes:
+ * - Yes some ranges seem duplicate but that's fine. The meter's packets
+ *   do provide multiple range indices for some of the modes which do
+ *   communicate values in the same range of values.
+ * - Some of the packet's bits don't match the available documentation.
+ *   Some of the meter's features are not available to the PC side by
+ *   means of inspecting packets.
+ *   - Bit 5 of "bar value" was seen with value 1 in FREQ and OHM:
+ *     f2  17 84 21 21  08 00 00 00  64 01 01 17  12 37  02 40 00  7d
+ *     So we keep the test around but accept when it fails.
+ *   - The "gotta beep" activity of continuity/break test mode is not
+ *     available in the packets.
+ * - The interpretation of range indices depends on the specific mode
+ *   (meter's function, and range when selectable by the user like mV).
+ *   As does the precision of results.
+ */
+
+#include "config.h"
+#include <ctype.h>
+#include <glib.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include "libsigrok/libsigrok.h"
+#include "libsigrok-internal.h"
+
+#define LOG_PREFIX "eev121gw"
+
+/*
+ * TODO:
+ * When these bit field extraction helpers move to some common location,
+ * their names may need adjustment to reduce the potential for conflicts.
+ */
+// #define BIT(n) (1UL << (n))
+#define MASK(len) ((1UL << (len)) - 1)
+#define FIELD_PL(v, pos, len) (((v) >> (pos)) & MASK(len))
+#define FIELD_NL(v, name) FIELD_PL(v, POS_ ## name, LEN_ ## name)
+#define FIELD_NB(v, name) FIELD_PL(v, POS_ ## name, 1)
+
+/*
+ * Support compile time checks for expected sizeof() results etc, like
+ *   STATIC_ASSERT(sizeof(struct packet) == 19, "packet size");
+ * Probably should go to some common location.
+ * See http://www.pixelbeat.org/programming/gcc/static_assert.html for details.
+ */
+#define ASSERT_CONCAT_(a, b) a ## b
+#define ASSERT_CONCAT(a, b) ASSERT_CONCAT_(a, b)
+/* These can't be used after statements in c89. */
+#ifdef __COUNTER__
+  #define STATIC_ASSERT(e, m) \
+    ; enum { ASSERT_CONCAT(static_assert_, __COUNTER__) = 1 / (int)(!!(e)) }
+#else
+  /*
+   * This can't be used twice on the same line so ensure if using in headers
+   * that the headers are not included twice (by wrapping in #ifndef...#endif).
+   * Note it doesn't cause an issue when used on same line of separate modules
+   * compiled with gcc -combine -fwhole-program.
+   */
+  #define STATIC_ASSERT(e, m) \
+    ; enum { ASSERT_CONCAT(assert_line_, __LINE__) = 1 / (int)(!!(e)) }
+#endif
+
+/*
+ * Symbolic identifiers for access to the packet's payload. "Offsets"
+ * address bytes within the packet. "Positions" specify the (lowest)
+ * bit number of a field, "lengths" specify the fields' number of bits.
+ * "Values" specify magic values or fixed content (SBZ, RSV, etc).
+ */
+enum eev121gw_packet_offs {
+       OFF_START_CMD,
+#define  VAL_START_CMD         0xf2
+       OFF_SERIAL_3,
+       OFF_SERIAL_2,
+       OFF_SERIAL_1,
+       OFF_SERIAL_0,
+#define  POS_SERIAL_YEAR       24
+#define  LEN_SERIAL_YEAR       8
+#define  POS_SERIAL_MONTH      20
+#define  LEN_SERIAL_MONTH      4
+#define  POS_SERIAL_NUMBER     0
+#define  LEN_SERIAL_NUMBER     20
+       OFF_MAIN_MODE,
+#define  POS_MAIN_MODE_VAL_U   6
+#define  LEN_MAIN_MODE_VAL_U   2
+#define  POS_MAIN_MODE_RSV_5   5
+#define  POS_MAIN_MODE_MODE    0
+#define  LEN_MAIN_MODE_MODE    5
+       OFF_MAIN_RANGE,
+#define  POS_MAIN_RANGE_OFL    7
+#define  POS_MAIN_RANGE_SIGN   6
+#define  POS_MAIN_RANGE_DEGC   5
+#define  POS_MAIN_RANGE_DEGF   4
+#define  POS_MAIN_RANGE_RANGE  0
+#define  LEN_MAIN_RANGE_RANGE  4
+       OFF_MAIN_VAL_H,
+       OFF_MAIN_VAL_L,
+       OFF_SUB_MODE,
+#define  POS_SUB_MODE_MODE     0
+#define  LEN_SUB_MODE_MODE     8
+       OFF_SUB_RANGE,
+#define  POS_SUB_RANGE_OFL     7
+#define  POS_SUB_RANGE_SIGN    6
+#define  POS_SUB_RANGE_K       5
+#define  POS_SUB_RANGE_HZ      4
+#define  POS_SUB_RANGE_RSV_3   3
+#define  POS_SUB_RANGE_POINT   0
+#define  LEN_SUB_RANGE_POINT   3
+       OFF_SUB_VAL_H,
+       OFF_SUB_VAL_L,
+       OFF_BAR_STATUS,
+#define  POS_BAR_STATUS_RSV_5  5
+#define  LEN_BAR_STATUS_RSV_5  3
+#define  POS_BAR_STATUS_USE    4
+#define  POS_BAR_STATUS_150    3
+#define  POS_BAR_STATUS_SIGN   2
+#define  POS_BAR_STATUS_1K_500 0
+#define  LEN_BAR_STATUS_1K_500 2
+       OFF_BAR_VALUE,
+#define  POS_BAR_VALUE_RSV_6   6
+#define  LEN_BAR_VALUE_RSV_6   2
+#define  POS_BAR_VALUE_RSV_5   5
+#define  POS_BAR_VALUE_VALUE   0
+#define  LEN_BAR_VALUE_VALUE   5
+       OFF_ICON_STS_1,
+#define  POS_ICON_STS1_DEGC    7
+#define  POS_ICON_STS1_1KHZ    6
+#define  POS_ICON_STS1_1MSPK   5
+#define  POS_ICON_STS1_DCAC    3
+#define  LEN_ICON_STS1_DCAC    2
+#define  POS_ICON_STS1_AUTO    2
+#define  POS_ICON_STS1_APO     1
+#define  POS_ICON_STS1_BAT     0
+       OFF_ICON_STS_2,
+#define  POS_ICON_STS2_DEGF    7
+#define  POS_ICON_STS2_BT      6
+#define  POS_ICON_STS2_UNK     5 /* TODO: What is this flag? 20mA loop current? */
+#define  POS_ICON_STS2_REL     4
+#define  POS_ICON_STS2_DBM     3
+#define  POS_ICON_STS2_MINMAX  0 /* TODO: How to interpret the 3-bit field? */
+#define  LEN_ICON_STS2_MINMAX  3
+       OFF_ICON_STS_3,
+#define  POS_ICON_STS3_RSV_7   7
+#define  POS_ICON_STS3_TEST    6
+#define  POS_ICON_STS3_MEM     4 /* TODO: How to interpret the 2-bit field? */
+#define  LEN_ICON_STS3_MEM     2
+#define  POS_ICON_STS3_AHOLD   2
+#define  LEN_ICON_STS3_AHOLD   2
+#define  POS_ICON_STS3_AC      1
+#define  POS_ICON_STS3_DC      0
+       OFF_CHECKSUM,
+       /* This is not an offset, but the packet's "byte count". */
+       PACKET_LAST_OFF,
+};
+
+STATIC_ASSERT(PACKET_LAST_OFF == EEV121GW_PACKET_SIZE,
+       "byte offsets vs packet length mismatch");
+
+enum mode_codes {
+       /* Modes for 'main' and 'sub' displays. */
+       MODE_LOW_Z = 0,
+       MODE_DC_V = 1,
+       MODE_AC_V = 2,
+       MODE_DC_MV = 3,
+       MODE_AC_MV = 4,
+       MODE_TEMP = 5,
+       MODE_FREQ = 6,
+       MODE_PERIOD = 7,
+       MODE_DUTY = 8,
+       MODE_RES = 9,
+       MODE_CONT = 10,
+       MODE_DIODE = 11,
+       MODE_CAP = 12,
+       MODE_AC_UVA = 13,
+       MODE_AC_MVA = 14,
+       MODE_AC_VA = 15,
+       MODE_AC_UA = 16,
+       MODE_DC_UA = 17,
+       MODE_AC_MA = 18,
+       MODE_DC_MA = 19,
+       MODE_AC_A = 20,
+       MODE_DC_A = 21,
+       MODE_DC_UVA = 22,
+       MODE_DC_MVA = 23,
+       MODE_DC_VA = 24,
+       /* More modes for 'sub' display. */
+       MODE_SUB_TEMPC = 100,
+       MODE_SUB_TEMPF = 105,
+       MODE_SUB_BATT = 110,
+       MODE_SUB_APO_ON = 120,
+       MODE_SUB_APO_OFF = 125,
+       MODE_SUB_YEAR = 130,
+       MODE_SUB_DATE = 135,
+       MODE_SUB_TIME = 140,
+       MODE_SUB_B_VOLT = 150,
+       MODE_SUB_LCD = 160,
+       MODE_SUB_CONT_PARM_0 = 170,
+       MODE_SUB_CONT_PARM_1 = 171,
+       MODE_SUB_CONT_PARM_2 = 172,
+       MODE_SUB_CONT_PARM_3 = 173,
+       MODE_SUB_DBM = 180,
+       MODE_SUB_IVAL = 190,
+};
+
+enum range_codes {
+       RANGE_0,
+       RANGE_1,
+       RANGE_2,
+       RANGE_3,
+       RANGE_4,
+       RANGE_5,
+       RANGE_6,
+       RANGE_MAX,
+};
+
+enum bar_range_codes {
+       BAR_RANGE_5,
+       BAR_RANGE_50,
+       BAR_RANGE_500,
+       BAR_RANGE_1000,
+};
+#define BAR_VALUE_MAX 25
+
+enum acdc_codes {
+       ACDC_NONE,
+       ACDC_DC,
+       ACDC_AC,
+       ACDC_ACDC,
+};
+
+SR_PRIV const char *eev121gw_channel_formats[EEV121GW_DISPLAY_COUNT] = {
+       /*
+        * TODO:
+        * The "main", "sub", "bar" names were taken from the packet
+        * description. Will users prefer "primary", "secondary", and
+        * "bargraph" names? Or even-length "pri", "sec", "bar" instead?
+        */
+       "main", "sub", "bar",
+};
+
+/*
+ * See page 69 in the 2018-09-24 manual for a table of modes and their
+ * respective ranges ("Calibration Reference Table"). This is the input
+ * to get the number of significant digits, and the decimal's position.
+ */
+struct mode_range_item {
+       const char *desc; /* Description, for diagnostics. */
+       int digits; /* Number of significant digits, see @ref sr_analog_encoding. */
+       int factor; /* Factor to convert the uint to a float. */
+};
+
+struct mode_range_items {
+       size_t range_count;
+       const struct mode_range_item ranges[RANGE_MAX];
+};
+
+static const struct mode_range_items mode_ranges_lowz = {
+       .range_count = 1,
+       .ranges = {
+               { .desc =  "600.0V", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_volts = {
+       .range_count = 4,
+       .ranges = {
+               { .desc = "5.0000V", .digits = 4, .factor = 4, },
+               { .desc = "50.000V", .digits = 3, .factor = 3, },
+               { .desc = "500.00V", .digits = 2, .factor = 2, },
+               { .desc =  "600.0V", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_millivolts = {
+       .range_count = 2,
+       .ranges = {
+               { .desc = "50.000mV", .digits = 6, .factor = 6, },
+               { .desc = "500.00mV", .digits = 5, .factor = 5, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_temp = {
+       .range_count = 1,
+       .ranges = {
+               { .desc = "-200.0C ~ 1350.0C", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_freq = {
+       .range_count = 5,
+       .ranges = {
+               { .desc = "99.999Hz", .digits = 3, .factor = 3, },
+               { .desc = "999.99Hz", .digits = 2, .factor = 2, },
+               { .desc = "9.9999kHz", .digits = 1, .factor = 1, },
+               { .desc = "99.999kHz", .digits = 0, .factor = 0, },
+               { .desc = "999.99kHz", .digits = -1, .factor = -1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_period = {
+       .range_count = 3,
+       .ranges = {
+               { .desc = "9.9999ms", .digits = 7, .factor = 7, },
+               { .desc = "99.999ms", .digits = 6, .factor = 6, },
+               { .desc = "999.99ms", .digits = 5, .factor = 5, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_duty = {
+       .range_count = 1,
+       .ranges = {
+               { .desc = "99.9%", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_res = {
+       .range_count = 7,
+       .ranges = {
+               { .desc = "50.000R", .digits = 3, .factor = 3, },
+               { .desc = "500.00R", .digits = 2, .factor = 2, },
+               { .desc = "5.0000k", .digits = 1, .factor = 1, },
+               { .desc = "50.000k", .digits = 0, .factor = 0, },
+               { .desc = "500.00k", .digits = -1, .factor = -1, },
+               { .desc = "5.0000M", .digits = -2, .factor = -2, },
+               { .desc = "50.000M", .digits = -3, .factor = -3, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_cont = {
+       .range_count = 1,
+       .ranges = {
+               { .desc = "500.00R", .digits = 2, .factor = 2, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_diode = {
+       .range_count = 2,
+       .ranges = {
+               { .desc = "3.0000V", .digits = 4, .factor = 4, },
+               { .desc = "15.000V", .digits = 3, .factor = 3, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_cap = {
+       .range_count = 6,
+       .ranges = {
+               { .desc =  "10.00n", .digits = 11, .factor = 11, },
+               { .desc =  "100.0n", .digits = 10, .factor = 10, },
+               { .desc =  "1.000u", .digits = 9, .factor = 9, },
+               { .desc =  "10.00u", .digits = 8, .factor = 8, },
+               { .desc =  "100.0u", .digits = 7, .factor = 7, },
+               { .desc =  "10.00m", .digits = 5, .factor = 5, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_pow_va = {
+       .range_count = 4,
+       .ranges = {
+               { .desc = "2500.0mVA", .digits = 4, .factor = 4, },
+               { .desc = "25000.mVA", .digits = 3, .factor = 3, },
+               { .desc = "25.000VA", .digits = 3, .factor = 3, },
+               { .desc = "500.00VA", .digits = 2, .factor = 2, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_pow_mva = {
+       .range_count = 4,
+       .ranges = {
+               { .desc = "25.000mVA", .digits = 6, .factor = 6, },
+               { .desc = "250.00mVA", .digits = 5, .factor = 5, },
+               { .desc = "250.00mVA", .digits = 5, .factor = 5, },
+               { .desc = "2500.0mVA", .digits = 4, .factor = 4, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_pow_uva = {
+       .range_count = 4,
+       .ranges = {
+               { .desc = "250.00uVA", .digits = 8, .factor = 8, },
+               { .desc = "2500.0uVA", .digits = 7, .factor = 7, },
+               { .desc = "2500.0uVA", .digits = 7, .factor = 7, },
+               { .desc = "25000.uVA", .digits = 6, .factor = 6, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_curr_a = {
+       .range_count = 3,
+       .ranges = {
+               { .desc = "500.00mA", .digits = 5, .factor = 5, },
+               { .desc = "5.0000A", .digits = 4, .factor = 4, },
+               { .desc = "10.000A", .digits = 3, .factor = 3, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_curr_ma = {
+       .range_count = 2,
+       .ranges = {
+               { .desc = "5.0000mA", .digits = 7, .factor = 7, },
+               { .desc = "50.000mA", .digits = 6, .factor = 6, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_curr_ua = {
+       .range_count = 2,
+       .ranges = {
+               { .desc = "50.000uA", .digits = 9, .factor = 9, },
+               { .desc = "500.00uA", .digits = 8, .factor = 8, },
+       },
+};
+
+static const struct mode_range_items *mode_ranges_main[] = {
+       [MODE_LOW_Z] = &mode_ranges_lowz,
+       [MODE_DC_V] = &mode_ranges_volts,
+       [MODE_AC_V] = &mode_ranges_volts,
+       [MODE_DC_MV] = &mode_ranges_millivolts,
+       [MODE_AC_MV] = &mode_ranges_millivolts,
+       [MODE_TEMP] = &mode_ranges_temp,
+       [MODE_FREQ] = &mode_ranges_freq,
+       [MODE_PERIOD] = &mode_ranges_period,
+       [MODE_DUTY] = &mode_ranges_duty,
+       [MODE_RES] = &mode_ranges_res,
+       [MODE_CONT] = &mode_ranges_cont,
+       [MODE_DIODE] = &mode_ranges_diode,
+       [MODE_CAP] = &mode_ranges_cap,
+       [MODE_DC_VA] = &mode_ranges_pow_va,
+       [MODE_AC_VA] = &mode_ranges_pow_va,
+       [MODE_DC_MVA] = &mode_ranges_pow_mva,
+       [MODE_AC_MVA] = &mode_ranges_pow_mva,
+       [MODE_DC_UVA] = &mode_ranges_pow_uva,
+       [MODE_AC_UVA] = &mode_ranges_pow_uva,
+       [MODE_DC_A] = &mode_ranges_curr_a,
+       [MODE_AC_A] = &mode_ranges_curr_a,
+       [MODE_DC_MA] = &mode_ranges_curr_ma,
+       [MODE_AC_MA] = &mode_ranges_curr_ma,
+       [MODE_DC_UA] = &mode_ranges_curr_ua,
+       [MODE_AC_UA] = &mode_ranges_curr_ua,
+};
+
+/*
+ * The secondary display encodes SI units / scaling differently from the
+ * main display, and fewer ranges are available. So we share logic between
+ * displays for scaling, but have to keep separate tables for the displays.
+ */
+
+static const struct mode_range_items mode_ranges_temp_sub = {
+       .range_count = 2,
+       .ranges = {
+               [1] = { .desc = "sub 100.0C", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_freq_sub = {
+       .range_count = 4,
+       .ranges = {
+               [1] = { .desc = "999.9Hz", .digits = 1, .factor = 1, },
+               [2] = { .desc = "99.99Hz", .digits = 2, .factor = 2, },
+               [3] = { .desc = "9.999kHz", .digits = 3, .factor = 3, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_batt_sub = {
+       .range_count = 2,
+       .ranges = {
+               [1] = { .desc = "sub 10.0V", .digits = 1, .factor = 1, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_gain_sub = {
+       .range_count = 4,
+       .ranges = {
+               [1] = { .desc = "dbm 5000.0dBm", .digits = 1, .factor = 1, },
+               [2] = { .desc = "dbm 500.00dBm", .digits = 2, .factor = 2, },
+               [3] = { .desc = "dbm 50.000dBm", .digits = 3, .factor = 3, },
+       },
+};
+
+static const struct mode_range_items mode_ranges_diode_sub = {
+       .range_count = 1,
+       .ranges = {
+               [0] = { .desc = "diode 15.0V", .digits = 0, .factor = 0, },
+       },
+};
+
+/* TODO: Complete the list of ranges. Only tested with low voltages so far. */
+static const struct mode_range_items mode_ranges_volts_sub = {
+       .range_count = 5,
+       .ranges = {
+               [4] = { .desc = "5.0000V", .digits = 4, .factor = 4, },
+       },
+};
+
+/* TODO: Complete the list of ranges. Only tested with low voltages so far. */
+static const struct mode_range_items mode_ranges_mamps_sub = {
+       .range_count = 3,
+       .ranges = {
+               [2] = { .desc = "500.00mA", .digits = 5, .factor = 5, },
+       },
+};
+
+static const struct mode_range_items *mode_ranges_sub[] = {
+       [MODE_DC_V] = &mode_ranges_volts_sub,
+       [MODE_AC_V] = &mode_ranges_volts_sub,
+       [MODE_DC_A] = &mode_ranges_mamps_sub,
+       [MODE_AC_A] = &mode_ranges_mamps_sub,
+       [MODE_FREQ] = &mode_ranges_freq_sub,
+       [MODE_DIODE] = &mode_ranges_diode_sub,
+       [MODE_SUB_TEMPC] = &mode_ranges_temp_sub,
+       [MODE_SUB_TEMPF] = &mode_ranges_temp_sub,
+       [MODE_SUB_BATT] = &mode_ranges_batt_sub,
+       [MODE_SUB_DBM] = &mode_ranges_gain_sub,
+};
+
+static const struct mode_range_item *mode_range_get_scale(
+       enum eev121gw_display display,
+       enum mode_codes mode, enum range_codes range)
+{
+       const struct mode_range_items *items;
+       const struct mode_range_item *item;
+
+       if (display == EEV121GW_DISPLAY_MAIN) {
+               if (mode >= ARRAY_SIZE(mode_ranges_main))
+                       return NULL;
+               items = mode_ranges_main[mode];
+               if (!items || !items->range_count)
+                       return NULL;
+               if (range >= items->range_count)
+                       return NULL;
+               item = &items->ranges[range];
+               return item;
+       }
+       if (display == EEV121GW_DISPLAY_SUB) {
+               if (mode >= ARRAY_SIZE(mode_ranges_sub))
+                       return NULL;
+               items = mode_ranges_sub[mode];
+               if (!items || !items->range_count)
+                       return NULL;
+               if (range >= items->range_count)
+                       return NULL;
+               item = &items->ranges[range];
+               if (!item->desc || !*item->desc)
+                       return NULL;
+               return item;
+       }
+
+       return NULL;
+}
+
+SR_PRIV gboolean sr_eev121gw_packet_valid(const uint8_t *buf)
+{
+       uint8_t csum;
+       size_t idx;
+
+       /* Leading byte, literal / fixed value. */
+       if (buf[OFF_START_CMD] != VAL_START_CMD)
+               return FALSE;
+
+       /* Check some always-zero bits in reserved locations. */
+       if (FIELD_NB(buf[OFF_MAIN_MODE], MAIN_MODE_RSV_5))
+               return FALSE;
+       if (FIELD_NB(buf[OFF_SUB_RANGE], SUB_RANGE_RSV_3))
+               return FALSE;
+       if (FIELD_NL(buf[OFF_BAR_STATUS], BAR_STATUS_RSV_5))
+               return FALSE;
+       if (FIELD_NL(buf[OFF_BAR_VALUE], BAR_VALUE_RSV_6))
+               return FALSE;
+       /* See TODO for bit 5 of "bar value" not always being 0. */
+       if (0 && FIELD_NB(buf[OFF_BAR_VALUE], BAR_VALUE_RSV_5))
+               return FALSE;
+       if (FIELD_NB(buf[OFF_ICON_STS_3], ICON_STS3_RSV_7))
+               return FALSE;
+
+       /* Checksum, XOR over all previous bytes. */
+       csum = 0x00;
+       for (idx = OFF_START_CMD; idx < OFF_CHECKSUM; idx++)
+               csum ^= buf[idx];
+       if (csum != buf[OFF_CHECKSUM]) {
+               /* Non-critical condition, almost expected to see invalid data. */
+               sr_spew("Packet csum: want %02x, got %02x.", csum, buf[OFF_CHECKSUM]);
+               return FALSE;
+       }
+
+       sr_spew("Packet valid.");
+
+       return TRUE;
+}
+
+/**
+ * Parse a protocol packet.
+ *
+ * @param[in] buf Buffer containing the protocol packet. Must not be NULL.
+ * @param[out] floatval Pointer to a float variable. That variable will be
+ *             modified in-place depending on the protocol packet.
+ *             Must not be NULL.
+ * @param[out] analog Pointer to a struct sr_datafeed_analog. The struct will
+ *             be filled with data according to the protocol packet.
+ *             Must not be NULL.
+ * @param[out] info Pointer to a struct eevblog_121gw_info. The struct will be
+ *             filled with data according to the protocol packet.
+ *             Must not be NULL.
+ *
+ * @return SR_OK upon success, SR_ERR upon failure. Upon errors, the
+ *         'analog' variable contents are undefined and should not be used.
+ */
+SR_PRIV int sr_eev121gw_parse(const uint8_t *buf, float *floatval,
+               struct sr_datafeed_analog *analog, void *info)
+{
+       struct eev121gw_info *info_local;
+       enum eev121gw_display display;
+       const char *channel_name;
+       uint32_t raw_serial;
+       uint8_t raw_main_mode, raw_main_range;
+       uint16_t raw_main_value;
+       uint8_t raw_sub_mode, raw_sub_range;
+       uint16_t raw_sub_value;
+       uint8_t raw_bar_status, raw_bar_value;
+       uint8_t raw_icon_stat_1, raw_icon_stat_2, raw_icon_stat_3;
+       uint32_t uint_value;
+       enum mode_codes main_mode;
+       enum range_codes main_range;
+       enum mode_codes sub_mode;
+       enum range_codes sub_range;
+       const struct mode_range_item *scale;
+       gboolean is_dc, is_sign, use_sign;
+       gboolean is_k;
+       unsigned int cont_code;
+
+       info_local = info;
+       display = info_local->ch_idx;
+       channel_name = eev121gw_channel_formats[display];
+       memset(info_local, 0, sizeof(*info_local));
+       *floatval = 0.0f;
+
+       /*
+        * Get the packet's bytes into native C language typed variables.
+        * This keeps byte position references out of logic/calculations.
+        * The start command and CRC were verified before we get here.
+        */
+       raw_serial = RB32(&buf[OFF_SERIAL_3]);
+       raw_main_mode = R8(&buf[OFF_MAIN_MODE]);
+       raw_main_range = R8(&buf[OFF_MAIN_RANGE]);
+       raw_main_value = RB16(&buf[OFF_MAIN_VAL_H]);
+       raw_sub_mode = R8(&buf[OFF_SUB_MODE]);
+       raw_sub_range = R8(&buf[OFF_SUB_RANGE]);
+       raw_sub_value = RB16(&buf[OFF_SUB_VAL_H]);
+       raw_bar_status = R8(&buf[OFF_BAR_STATUS]);
+       raw_bar_value = R8(&buf[OFF_BAR_VALUE]);
+       raw_icon_stat_1 = R8(&buf[OFF_ICON_STS_1]);
+       raw_icon_stat_2 = R8(&buf[OFF_ICON_STS_2]);
+       raw_icon_stat_3 = R8(&buf[OFF_ICON_STS_3]);
+
+       /*
+        * Packets contain a YEAR-MONTH date spec. It's uncertain how
+        * this data relates to the device's production or the firmware
+        * version. It certainly is not the current date either. Only
+        * optionally log this information, it's consistent across all
+        * packets (won't change within a session), and will be noisy if
+        * always enabled.
+        *
+        * Packets also contain a user adjustable device identification
+        * number (see the SETUP options). This is motivated by support
+        * for multiple devices, but won't change here within a session.
+        * The user chose to communicate to one specific device when the
+        * session started, by means of the conn= spec.
+        *
+        * It was suggested that this 'serial' field might be used as an
+        * additional means to check for a packet's validity (or absence
+        * of communication errors). This remains as an option for future
+        * improvement.
+        */
+       if (0) {
+               unsigned int ser_year, ser_mon, ser_nr;
+
+               ser_year = FIELD_NL(raw_serial, SERIAL_YEAR);
+               ser_mon = FIELD_NL(raw_serial, SERIAL_MONTH);
+               ser_nr = FIELD_NL(raw_serial, SERIAL_NUMBER);
+               sr_spew("Packet: Y-M %x-%x, nr %x.", ser_year, ser_mon, ser_nr);
+       }
+
+       switch (display) {
+
+       case EEV121GW_DISPLAY_MAIN:
+               /*
+                * Get those fields which correspond to the main display.
+                * The value's mantissa has 18 bits. The sign is separate
+                * (and is not universally applicable, mode needs to get
+                * inspected). The range's scaling and precision also
+                * depend on the mode.
+                */
+               main_mode = FIELD_NL(raw_main_mode, MAIN_MODE_MODE);
+               main_range = FIELD_NL(raw_main_range, MAIN_RANGE_RANGE);
+               scale = mode_range_get_scale(EEV121GW_DISPLAY_MAIN,
+                       main_mode, main_range);
+               if (!scale)
+                       return SR_ERR_NA;
+               info_local->factor = scale->factor;
+               info_local->digits = scale->digits;
+
+               uint_value = raw_main_value;
+               uint_value |= FIELD_NL(raw_main_mode, MAIN_MODE_VAL_U) << 16;
+               info_local->uint_value = uint_value;
+               info_local->is_ofl = FIELD_NB(raw_main_range, MAIN_RANGE_OFL);
+
+               switch (main_mode) {
+               case MODE_LOW_Z:
+                       is_dc = FALSE;
+                       if (FIELD_NB(raw_icon_stat_3, ICON_STS3_DC))
+                               is_dc = TRUE;
+                       if (FIELD_NB(raw_icon_stat_3, ICON_STS3_AC))
+                               is_dc = FALSE;
+                       use_sign = is_dc;
+                       break;
+               case MODE_DC_V:
+               case MODE_DC_MV:
+               case MODE_TEMP:
+               case MODE_DC_UVA:
+               case MODE_DC_MVA:
+               case MODE_DC_VA:
+               case MODE_DC_UA:
+               case MODE_DC_MA:
+               case MODE_DC_A:
+                       use_sign = TRUE;
+                       break;
+               default:
+                       use_sign = FALSE;
+                       break;
+               }
+               if (use_sign) {
+                       is_sign = FIELD_NB(raw_main_range, MAIN_RANGE_SIGN);
+                       info_local->is_neg = is_sign;
+               }
+
+               switch (main_mode) {
+               case MODE_LOW_Z:
+                       info_local->is_voltage = TRUE;
+                       /* TODO: Need to determine AC/DC here? */
+                       info_local->is_volt = TRUE;
+                       info_local->is_low_pass = TRUE;
+                       break;
+               case MODE_DC_V:
+                       info_local->is_voltage = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+               case MODE_AC_V:
+                       info_local->is_voltage = TRUE;
+                       info_local->is_volt = TRUE;
+                       info_local->is_ac = TRUE;
+                       break;
+               case MODE_DC_MV:
+                       info_local->is_voltage = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+               case MODE_AC_MV:
+                       info_local->is_voltage = TRUE;
+                       info_local->is_volt = TRUE;
+                       info_local->is_ac = TRUE;
+                       break;
+               case MODE_TEMP:
+                       info_local->is_temperature = TRUE;
+                       if (FIELD_NB(raw_main_range, MAIN_RANGE_DEGC))
+                               info_local->is_celsius = TRUE;
+                       if (FIELD_NB(raw_main_range, MAIN_RANGE_DEGF))
+                               info_local->is_fahrenheit = TRUE;
+                       break;
+               case MODE_FREQ:
+                       info_local->is_frequency = TRUE;
+                       info_local->is_hertz = TRUE;
+                       break;
+               case MODE_PERIOD:
+                       info_local->is_period = TRUE;
+                       info_local->is_seconds = TRUE;
+                       break;
+               case MODE_DUTY:
+                       info_local->is_duty_cycle = TRUE;
+                       info_local->is_percent = TRUE;
+                       break;
+               case MODE_RES:
+                       info_local->is_resistance = TRUE;
+                       info_local->is_ohm = TRUE;
+                       break;
+               case MODE_CONT:
+                       info_local->is_continuity = TRUE;
+                       info_local->is_ohm = TRUE;
+                       /*
+                        * In continuity mode the packet provides the
+                        * resistance in ohms (500R range), but seems to
+                        * _not_ carry the "boolean" open/closed state
+                        * which controls the beeper. Users can select
+                        * whether to trigger at 30R or 300R, and whether
+                        * to trigger on values below (continuity) or
+                        * above (cable break) the limit, but we cannot
+                        * tell what the currently used setting is. So
+                        * we neither get the beeper's state, nor can we
+                        * derive it from other information.
+                        */
+                       break;
+               case MODE_DIODE:
+                       info_local->is_diode = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+               case MODE_CAP:
+                       info_local->is_capacitance = TRUE;
+                       info_local->is_farad = TRUE;
+                       break;
+               case MODE_AC_UVA:
+                       info_local->is_power = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               case MODE_AC_MVA:
+                       info_local->is_power = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               case MODE_AC_VA:
+                       info_local->is_power = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               case MODE_AC_UA:
+                       info_local->is_current = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_DC_UA:
+                       info_local->is_current = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_AC_MA:
+                       info_local->is_current = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_DC_MA:
+                       info_local->is_current = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_AC_A:
+                       info_local->is_current = TRUE;
+                       info_local->is_ac = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_DC_A:
+                       info_local->is_current = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_DC_UVA:
+                       info_local->is_power = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               case MODE_DC_MVA:
+                       info_local->is_power = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               case MODE_DC_VA:
+                       info_local->is_power = TRUE;
+                       info_local->is_dc = TRUE;
+                       info_local->is_volt_ampere = TRUE;
+                       break;
+               /* Modes 100-199 only apply to the secondary display. */
+               default:
+                       return SR_ERR_NA;
+               }
+
+               /*
+                * Inspect the "icons" section, since it is associated
+                * with the primary display and global device state.
+                */
+               if (FIELD_NB(raw_icon_stat_1, ICON_STS1_1KHZ))
+                       info_local->is_low_pass = TRUE;
+               if (FIELD_NB(raw_icon_stat_1, ICON_STS1_1MSPK))
+                       info_local->is_1ms_peak = TRUE;
+               switch (FIELD_NL(raw_icon_stat_1, ICON_STS1_DCAC)) {
+               case ACDC_ACDC:
+                       info_local->is_ac = TRUE;
+                       info_local->is_dc = TRUE;
+                       break;
+               case ACDC_AC:
+                       info_local->is_ac = TRUE;
+                       break;
+               case ACDC_DC:
+                       info_local->is_dc = TRUE;
+                       break;
+               case ACDC_NONE:
+                       /* EMPTY */
+                       break;
+               }
+               if (FIELD_NB(raw_icon_stat_1, ICON_STS1_AUTO))
+                       info_local->is_auto_range = TRUE;
+               if (FIELD_NB(raw_icon_stat_1, ICON_STS1_APO))
+                       info_local->is_auto_poweroff = TRUE;
+               if (FIELD_NB(raw_icon_stat_1, ICON_STS1_BAT))
+                       info_local->is_low_batt = TRUE;
+               if (FIELD_NB(raw_icon_stat_2, ICON_STS2_BT))
+                       info_local->is_bt = TRUE;
+               /* TODO: Is this the "20mA loop current" flag? */
+               if (FIELD_NB(raw_icon_stat_2, ICON_STS2_UNK))
+                       info_local->is_loop_current = TRUE;
+               if (FIELD_NB(raw_icon_stat_2, ICON_STS2_REL))
+                       info_local->is_rel = TRUE;
+               /* dBm only applies to secondary display, not main. */
+               switch (FIELD_NL(raw_icon_stat_2, ICON_STS2_MINMAX)) {
+               /* TODO: Do inspect the min/max/avg flags. */
+               default:
+                       /* EMPTY */
+                       break;
+               }
+               if (FIELD_NB(raw_icon_stat_3, ICON_STS3_TEST))
+                       info_local->is_test = TRUE;
+               /* TODO: How to interpret the 2-bit MEM field? */
+               if (FIELD_NL(raw_icon_stat_3, ICON_STS3_MEM))
+                       info_local->is_mem = TRUE;
+               if (FIELD_NL(raw_icon_stat_3, ICON_STS3_AHOLD))
+                       info_local->is_hold = TRUE;
+               /* TODO: Are these for the secondary display? See status-2 ACDC. */
+               if (FIELD_NB(raw_icon_stat_3, ICON_STS3_AC))
+                       info_local->is_ac = TRUE;
+               if (FIELD_NB(raw_icon_stat_3, ICON_STS3_DC))
+                       info_local->is_dc = TRUE;
+
+               sr_spew("Disp '%s', value: %lu (ov %d, neg %d), mode %d, range %d.",
+                       channel_name,
+                       (unsigned long)info_local->uint_value,
+                       info_local->is_ofl, info_local->is_neg,
+                       (int)main_mode, (int)main_range);
+               /* Advance to the number and units conversion below. */
+               break;
+
+       case EEV121GW_DISPLAY_SUB:
+               /*
+                * Get those fields which correspond to the secondary
+                * display. The value's mantissa has 16 bits. The sign
+                * is separate is only applies to some of the modes.
+                * Scaling and precision also depend on the mode. The
+                * interpretation of the secondary display is different
+                * from the main display: The 'range' is not an index
+                * into ranges, instead it's the decimal's position.
+                * Yet more scaling depends on the mode, to complicate
+                * matters. The secondary display uses modes 100-199,
+                * and some of the 0-24 modes as well.
+                */
+               sub_mode = FIELD_NL(raw_sub_mode, SUB_MODE_MODE);
+               sub_range = FIELD_NL(raw_sub_range, SUB_RANGE_POINT);
+               scale = mode_range_get_scale(EEV121GW_DISPLAY_SUB,
+                       sub_mode, sub_range);
+               if (!scale)
+                       return SR_ERR_NA;
+               info_local->factor = scale->factor;
+               info_local->digits = scale->digits;
+
+               info_local->uint_value = raw_sub_value;
+               info_local->is_ofl = FIELD_NB(raw_sub_range, SUB_RANGE_OFL);
+
+               switch (sub_mode) {
+               case MODE_DC_V:
+               case MODE_AC_V:
+               case MODE_DC_A:
+               case MODE_AC_A:
+               case MODE_SUB_TEMPC:
+               case MODE_SUB_TEMPF:
+               case MODE_SUB_B_VOLT:
+               case MODE_SUB_DBM:
+                       use_sign = TRUE;
+                       break;
+               default:
+                       use_sign = FALSE;
+                       break;
+               }
+               if (use_sign) {
+                       is_sign = FIELD_NB(raw_sub_range, SUB_RANGE_SIGN);
+                       info_local->is_neg = is_sign;
+               }
+               is_k = FIELD_NB(raw_sub_range, SUB_RANGE_K);
+
+               /*
+                * TODO: Re-check the power mode display as more data becomes
+                * available.
+                *
+                * The interpretation of the secondary display in power (VA)
+                * modes is uncertain. The mode suggests A or uA units but the
+                * value is supposed to be mA without a reliable condition
+                * for us to check...
+                *
+                * f2  17 84 21 21  18 02 00 00  01 04 00 0b  00 00  0a 40 00  3f
+                * f2  17 84 21 21  18 02 00 00  15 03 00 00  00 00  0a 40 00  27
+                *                  DC VA        DC V / DC A
+                *                  25.000VA     dot 4 / dot 3
+                *
+                * f2  17 84 21 21  18 00 00 26  01 04 4c 57  00 00  0e 40 00  0f
+                * f2  17 84 21 21  18 00 00 26  15 02 00 c7  00 00  0e 40 00  c1
+                *                  3.8mVA DC    1.9543V
+                *                                 1.98mA (!) DC A + dot 2 -> milli(!) amps?
+                *
+                * f2  17 84 21 21  17 00 07 85  01 04 4c 5a  00 00  0e 40 00  a9
+                * f2  17 84 21 21  17 00 07 85  13 04 26 7b  00 00  0e 40 00  f0
+                *                  1.925mVA DC  1.9546V
+                *                               0.9852mA
+                *
+                * f2  17 84 21 21  16 02 11 e0  01 04 26 39  00 02  0e 40 00  d2
+                * f2  17 84 21 21  16 02 11 e0  11 04 12 44  00 02  0e 40 00  8b
+                *                  457.6uVA DC  0.9785V
+                *                               0.4676mA (!) DC uA + dot 4 -> milli(!) amps?
+                */
+
+               switch (sub_mode) {
+               case MODE_DC_V:
+                       info_local->is_voltage = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+               case MODE_DC_A:
+                       info_local->is_current = TRUE;
+                       info_local->is_ampere = TRUE;
+                       break;
+               case MODE_FREQ:
+                       info_local->is_frequency = TRUE;
+                       info_local->is_hertz = TRUE;
+                       if (is_k) {
+                               info_local->factor -= 3;
+                               info_local->digits -= 3;
+                       }
+                       info_local->is_ofl = FALSE;
+                       break;
+               case MODE_SUB_TEMPC:
+                       info_local->is_temperature = TRUE;
+                       info_local->is_celsius = TRUE;
+                       break;
+               case MODE_SUB_TEMPF:
+                       info_local->is_temperature = TRUE;
+                       info_local->is_fahrenheit = TRUE;
+                       break;
+               case MODE_SUB_BATT:
+                       /* TODO: How to communicate it's the *battery* voltage? */
+                       info_local->is_voltage = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+               case MODE_SUB_DBM:
+                       info_local->is_gain = TRUE;
+                       info_local->is_dbm = TRUE;
+                       break;
+               case MODE_SUB_CONT_PARM_0:
+               case MODE_SUB_CONT_PARM_1:
+               case MODE_SUB_CONT_PARM_2:
+               case MODE_SUB_CONT_PARM_3:
+                       /*
+                        * These "continuity parameters" are special. The
+                        * least significant bits represent the options:
+                        *
+                        * 0xaa = 170 => down 30
+                        * 0xab = 171 => up 30
+                        * 0xac = 172 => down 300
+                        * 0xad = 173 => up 300
+                        *
+                        * bit 0 value 0 -> close (cont)
+                        * bit 0 value 1 -> open (break)
+                        * bit 1 value 0 -> 30R limit
+                        * bit 1 value 1 -> 300R limit
+                        *
+                        * This "display value" is only seen during setup
+                        * but not during regular operation of continuity
+                        * mode. :( In theory we could somehow pass the
+                        * 30/300 ohm limit to sigrok, but that'd be of
+                        * somewhat limited use.
+                        */
+                       cont_code = sub_mode - MODE_SUB_CONT_PARM_0;
+                       info_local->is_resistance = TRUE;
+                       info_local->is_ohm = TRUE;
+                       info_local->uint_value = (cont_code & 0x02) ? 300 : 30;
+                       info_local->is_neg = FALSE;
+                       info_local->is_ofl = FALSE;
+                       info_local->factor = 0;
+                       info_local->digits = 0;
+                       break;
+               case MODE_DIODE:
+                       /* Displays configured diode test voltage. */
+                       info_local->is_voltage = TRUE;
+                       info_local->is_volt = TRUE;
+                       break;
+
+               /* Reflecting these to users seems pointless, ignore them. */
+               case MODE_SUB_APO_ON:
+               case MODE_SUB_APO_OFF:
+               case MODE_SUB_LCD:
+               case MODE_SUB_YEAR:
+               case MODE_SUB_DATE:
+               case MODE_SUB_TIME:
+                       return SR_ERR_NA;
+
+               /* Unknown / unsupported sub display mode. */
+               default:
+                       return SR_ERR_NA;
+               }
+
+               sr_spew("disp '%s', value: %lu (ov %d, neg %d), mode %d, range %d",
+                       channel_name,
+                       (unsigned long)info_local->uint_value,
+                       info_local->is_ofl, info_local->is_neg,
+                       (int)sub_mode, (int)sub_range);
+               /* Advance to the number and units conversion below. */
+               break;
+
+       case EEV121GW_DISPLAY_BAR:
+               /*
+                * Get those fields which correspond to the bargraph.
+                * There are 26 segments (ticks 0-25), several ranges
+                * apply (up to 5, or up to 10, several decades). The
+                * bargraph does not apply to all modes and ranges,
+                * hence there is a "use" flag (negative logic, blank
+                * signal). Bit 5 was also found to have undocumented
+                * values, we refuse to use the bargraph value then.
+                */
+               if (FIELD_NB(raw_bar_status, BAR_STATUS_USE))
+                       return SR_ERR_NA;
+               if (FIELD_NB(raw_bar_value, BAR_VALUE_RSV_5))
+                       return SR_ERR_NA;
+               uint_value = FIELD_NL(raw_bar_value, BAR_VALUE_VALUE);
+               if (uint_value > BAR_VALUE_MAX)
+                       uint_value = BAR_VALUE_MAX;
+               info_local->is_neg = FIELD_NB(raw_bar_status, BAR_STATUS_SIGN);
+               switch (FIELD_NL(raw_bar_status, BAR_STATUS_1K_500)) {
+               case BAR_RANGE_5:
+                       /* Full range 5.0, in steps of 0.2 each. */
+                       uint_value *= 5000 / BAR_VALUE_MAX;
+                       info_local->factor = 3;
+                       info_local->digits = 1;
+                       break;
+               case BAR_RANGE_50:
+                       /* Full range 50, in steps of 2 each. */
+                       uint_value *= 50 / BAR_VALUE_MAX;
+                       info_local->factor = 0;
+                       info_local->digits = 0;
+                       break;
+               case BAR_RANGE_500:
+                       /* Full range 500, in steps of 20 each. */
+                       uint_value *= 500 / BAR_VALUE_MAX;
+                       info_local->factor = 0;
+                       info_local->digits = -1;
+                       break;
+               case BAR_RANGE_1000:
+                       /* Full range 1000, in steps of 40 each. */
+                       uint_value *= 1000 / BAR_VALUE_MAX;
+                       info_local->factor = 0;
+                       info_local->digits = -1;
+                       break;
+               default:
+                       return SR_ERR_NA;
+               }
+               info_local->uint_value = uint_value;
+               info_local->is_unitless = TRUE;
+               sr_spew("Disp '%s', value: %u.", channel_name,
+                       (unsigned int)info_local->uint_value);
+               /* Advance to the number and units conversion below. */
+               break;
+
+       default:
+               /* Unknown display, programmer's error, ShouldNotHappen(TM). */
+               sr_err("Disp '-?-'.");
+               return SR_ERR_ARG;
+       }
+
+       /*
+        * Convert the unsigned mantissa and its modifiers to a float
+        * analog value, including scale and quantity. Do the conversion
+        * first, and optionally override the result with 'inf' later.
+        * Apply the sign last so that +inf and -inf are supported.
+        */
+       *floatval = info_local->uint_value;
+       if (info_local->factor)
+               *floatval *= powf(10, -info_local->factor);
+       if (info_local->is_ofl)
+               *floatval = INFINITY;
+       if (info_local->is_neg)
+               *floatval = -*floatval;
+
+       analog->encoding->digits = info_local->digits;
+       analog->spec->spec_digits = info_local->digits;
+
+       /*
+        * Communicate the measured quantity.
+        */
+       /* Determine the quantity itself. */
+       if (info_local->is_voltage)
+               analog->meaning->mq = SR_MQ_VOLTAGE;
+       if (info_local->is_current)
+               analog->meaning->mq = SR_MQ_CURRENT;
+       if (info_local->is_power)
+               analog->meaning->mq = SR_MQ_POWER;
+       if (info_local->is_gain)
+               analog->meaning->mq = SR_MQ_GAIN;
+       if (info_local->is_resistance)
+               analog->meaning->mq = SR_MQ_RESISTANCE;
+       if (info_local->is_capacitance)
+               analog->meaning->mq = SR_MQ_CAPACITANCE;
+       if (info_local->is_diode)
+               analog->meaning->mq = SR_MQ_VOLTAGE;
+       if (info_local->is_temperature)
+               analog->meaning->mq = SR_MQ_TEMPERATURE;
+       if (info_local->is_continuity)
+               analog->meaning->mq = SR_MQ_CONTINUITY;
+       if (info_local->is_frequency)
+               analog->meaning->mq = SR_MQ_FREQUENCY;
+       if (info_local->is_period)
+               analog->meaning->mq = SR_MQ_TIME;
+       if (info_local->is_duty_cycle)
+               analog->meaning->mq = SR_MQ_DUTY_CYCLE;
+       if (info_local->is_unitless)
+               analog->meaning->mq = SR_MQ_COUNT;
+       /* Add AC / DC / DC+AC flags. */
+       if (info_local->is_ac)
+               analog->meaning->mqflags |= SR_MQFLAG_AC;
+       if (info_local->is_dc)
+               analog->meaning->mqflags |= SR_MQFLAG_DC;
+       /* Specify units. */
+       if (info_local->is_ampere)
+               analog->meaning->unit = SR_UNIT_AMPERE;
+       if (info_local->is_volt)
+               analog->meaning->unit = SR_UNIT_VOLT;
+       if (info_local->is_volt_ampere)
+               analog->meaning->unit = SR_UNIT_VOLT_AMPERE;
+       if (info_local->is_dbm)
+               analog->meaning->unit = SR_UNIT_DECIBEL_VOLT;
+       if (info_local->is_ohm)
+               analog->meaning->unit = SR_UNIT_OHM;
+       if (info_local->is_farad)
+               analog->meaning->unit = SR_UNIT_FARAD;
+       if (info_local->is_celsius)
+               analog->meaning->unit = SR_UNIT_CELSIUS;
+       if (info_local->is_fahrenheit)
+               analog->meaning->unit = SR_UNIT_FAHRENHEIT;
+       if (info_local->is_hertz)
+               analog->meaning->unit = SR_UNIT_HERTZ;
+       if (info_local->is_seconds)
+               analog->meaning->unit = SR_UNIT_SECOND;
+       if (info_local->is_percent)
+               analog->meaning->unit = SR_UNIT_PERCENTAGE;
+       if (info_local->is_loop_current)
+               analog->meaning->unit = SR_UNIT_PERCENTAGE;
+       if (info_local->is_unitless)
+               analog->meaning->unit = SR_UNIT_UNITLESS;
+       if (info_local->is_logic)
+               analog->meaning->unit = SR_UNIT_UNITLESS;
+       /* Add other indicator flags. */
+       if (info_local->is_diode) {
+               analog->meaning->mqflags |= SR_MQFLAG_DIODE;
+               analog->meaning->mqflags |= SR_MQFLAG_DC;
+       }
+       if (info_local->is_min)
+               analog->meaning->mqflags |= SR_MQFLAG_MIN;
+       if (info_local->is_max)
+               analog->meaning->mqflags |= SR_MQFLAG_MAX;
+       if (info_local->is_avg)
+               analog->meaning->mqflags |= SR_MQFLAG_AVG;
+       /* TODO: How to communicate info_local->is_1ms_peak? */
+       if (info_local->is_rel)
+               analog->meaning->mqflags |= SR_MQFLAG_RELATIVE;
+       if (info_local->is_hold)
+               analog->meaning->mqflags |= SR_MQFLAG_HOLD;
+       /* TODO: How to communicate info_local->is_low_pass? */
+       if (info_local->is_mem) /* XXX Is REF appropriate here? */
+               analog->meaning->mqflags |= SR_MQFLAG_REFERENCE;
+       if (info_local->is_auto_range)
+               analog->meaning->mqflags |= SR_MQFLAG_AUTORANGE;
+       /* TODO: How to communicate info->is_test? What's its meaning at all? */
+       /* TODO: How to communicate info->is_auto_poweroff? */
+       /* TODO: How to communicate info->is_low_batt? */
+
+       return SR_OK;
+}
+
+/*
+ * Parse the same packet multiple times, to extract individual analog
+ * values which correspond to several displays of the device. Make sure
+ * to keep the channel index in place, even if the parse routine will
+ * clear the info structure.
+ */
+SR_PRIV int sr_eev121gw_3displays_parse(const uint8_t *buf, float *floatval,
+               struct sr_datafeed_analog *analog, void *info)
+{
+       struct eev121gw_info *info_local;
+       size_t ch_idx;
+       int rc;
+
+       info_local = info;
+       ch_idx = info_local->ch_idx;
+       rc = sr_eev121gw_parse(buf, floatval, analog, info);
+       info_local->ch_idx = ch_idx + 1;
+
+       return rc;
+}
index a567d14baccc0df9b3612ed0daa414d117e17b41..1dcdc992fd9cb7e2c64a68b26697657129ce50b3 100644 (file)
@@ -1451,6 +1451,48 @@ SR_PRIV gboolean sr_asycii_packet_valid(const uint8_t *buf);
 SR_PRIV int sr_asycii_parse(const uint8_t *buf, float *floatval,
                            struct sr_datafeed_analog *analog, void *info);
 
+/*--- src/dmm/eev121gw.c ----------------------------------------------------*/
+
+#define EEV121GW_PACKET_SIZE 19
+
+enum eev121gw_display {
+       EEV121GW_DISPLAY_MAIN,
+       EEV121GW_DISPLAY_SUB,
+       EEV121GW_DISPLAY_BAR,
+       EEV121GW_DISPLAY_COUNT,
+};
+
+struct eev121gw_info {
+       /* Selected channel. */
+       size_t ch_idx;
+       /*
+        * Measured value, number and sign/overflow flags, scale factor
+        * and significant digits.
+        */
+       uint32_t uint_value;
+       gboolean is_ofl, is_neg;
+       int factor, digits;
+       /* Currently active mode (meter's function). */
+       gboolean is_ac, is_dc, is_voltage, is_current, is_power, is_gain;
+       gboolean is_resistance, is_capacitance, is_diode, is_temperature;
+       gboolean is_continuity, is_frequency, is_period, is_duty_cycle;
+       /* Quantities associated with mode/function. */
+       gboolean is_ampere, is_volt, is_volt_ampere, is_dbm;
+       gboolean is_ohm, is_farad, is_celsius, is_fahrenheit;
+       gboolean is_hertz, is_seconds, is_percent, is_loop_current;
+       gboolean is_unitless, is_logic;
+       /* Other indicators. */
+       gboolean is_min, is_max, is_avg, is_1ms_peak, is_rel, is_hold;
+       gboolean is_low_pass, is_mem, is_bt, is_auto_range, is_test;
+       gboolean is_auto_poweroff, is_low_batt;
+};
+
+SR_PRIV gboolean sr_eev121gw_packet_valid(const uint8_t *buf);
+SR_PRIV int sr_eev121gw_parse(const uint8_t *buf, float *floatval,
+                            struct sr_datafeed_analog *analog, void *info);
+SR_PRIV int sr_eev121gw_3displays_parse(const uint8_t *buf, float *floatval,
+                            struct sr_datafeed_analog *analog, void *info);
+
 /*--- hardware/scale/kern.c -------------------------------------------------*/
 
 struct kern_info {