]> sigrok.org Git - libsigrok.git/blobdiff - src/dmm/bm52x.c
dmm/bm52x: add support for recorded measurements (memory data sets)
[libsigrok.git] / src / dmm / bm52x.c
index b51d91c768abad0a8529ff21c044492d7953d2fc..728b70a690308e6cabb662cbf35dbddf4df67a06 100644 (file)
  * http://brymen.com/product-html/Download2.html
  * http://brymen.com/product-html/PD02BM520s_protocolDL.html
  * http://brymen.com/product-html/images/DownloadList/ProtocolList/BM520-BM520s_List/BM520-BM520s-10000-count-professional-dual-display-mobile-logging-DMMs-protocol.zip
+ *
+ * The parser implementation was tested with a Brymen BM525s meter. Some
+ * of the responses differ from the vendor's documentation:
+ * - Recording session total byte counts don't start after the byte count
+ *   field, but instead include this field and the model ID (spans _every_
+ *   byte in the stream).
+ * - Recording session start/end markers are referred to as DLE, STX,
+ *   and ETX. Observed traffic instead sends 0xee, 0xa0, and 0xc0.
  */
 
 /*
  * TODO
- * - This DMM packet parser exclusively supports live readings (vendor
- *   documentation refers to it as "real-time download" aka RTD). A HID
- *   report is sent which results in three HID reports in response which
- *   carry 24 bytes with LCD indicator bitfields (and a few literals to
- *   synchronize to the byte stream). Reading back previous recordings
- *   ("memory data sets" in the vendor documentation) involve different
- *   types of requests, and several of them, and result in a different
- *   number of response reports while their interpretation differs, too,
- *   of course. None of this fits the serial-dmm approach, and needs to
- *   get addressed later.
- *   - Configurable sample rate, range 20/s rate up to 600s period.
- *   - Multiple sessions, one function per session, up to 999 "session
- *     pages" (recordings with their sequence of measurement values).
- *   - Up to 87000 (single display) or 43500 (dual display) measurements
- *     total on BM525s.
- *   - Request 0x00, 0x00, 0x52, 0x88 to request the HEAD of recordings.
- *     Request 0x00, 0x00, 0x52, 0x89 to request the NEXT memory chunk.
- *     Request 0x00, 0x00, 0x52, 0x8a to re-request the CURR memory chunk
- *     (repetition when transmission failed detectably?).
- *   - All these HID report requests result in four HID responses which
- *     carry 32 bytes (24 bytes of payload data, and a checksum) where
- *     application's fields can cross the boundary of HID reports and
- *     even response chunks.
  * - Some of the meter's functions and indications cannot get expressed
  *   by means of sigrok MQ and flags terms. Some indicator's meaning is
  *   unknown or uncertain, and thus their state is not evaluated.
  *     reference, including min/max/diff when displayed upon activation.
  *   - The "beep jack" displays "InEr" in the secondary display. This is
  *     not caught here, no PC side message gets emitted.
+ * - Support for recordings is mostly untested. It was written to the
+ *   letter of the vendor documentation, but was not verified to work
+ *   for all of the many meter's modes including ranges. Inspection of
+ *   the full byte stream is necessary on one hand since random access
+ *   is not available, and useful on the other hand for consistency
+ *   checks.
+ * - The vendor's shipping box and user manual suggests a similarity of
+ *   BM520s and BM820s meters. Can this DMM packet parser support both?
  */
 
 #include <config.h>
 #include "libsigrok-internal.h"
 #include <math.h>
 #include <string.h>
+#include <strings.h>
 
 #define LOG_PREFIX "brymen-bm52x"
 
+/*
+ * DMM specific device options, and state keeping. All of it is related
+ * to recorded information in contrast to live readings. There also are
+ * four types of requesting HID reports that need to be sent.
+ */
+
+static const uint32_t devopts[] = {
+       SR_CONF_CONTINUOUS,
+       SR_CONF_LIMIT_SAMPLES | SR_CONF_SET,
+       SR_CONF_LIMIT_MSEC | SR_CONF_SET,
+       SR_CONF_DATA_SOURCE | SR_CONF_GET | SR_CONF_SET | SR_CONF_LIST,
+};
+
+struct brymen_bm52x_state {
+       size_t sess_idx;
+       struct {
+               uint8_t buff[2 * 32];
+               size_t fill_pos;
+               size_t read_pos;
+               size_t remain;
+       } rsp;
+       const struct sr_dev_inst *sdi;
+};
+
+enum bm52x_reqtype {
+       REQ_LIVE_READ,
+       REQ_REC_HEAD,
+       REQ_REC_NEXT,
+       REQ_REC_CURR,
+};
+
 #ifdef HAVE_SERIAL_COMM
-SR_PRIV int sr_brymen_bm52x_packet_request(struct sr_serial_dev_inst *serial)
+static int bm52x_send_req(struct sr_serial_dev_inst *serial, enum bm52x_reqtype t)
 {
-       static const uint8_t request[] = { 0x00, 0x00, 0x52, 0x66, };
+       static const uint8_t req_live[] = { 0x00, 0x00, 0x52, 0x66, };
+       static const uint8_t req_head[] = { 0x00, 0x00, 0x52, 0x88, };
+       static const uint8_t req_next[] = { 0x00, 0x00, 0x52, 0x89, };
+       static const uint8_t req_curr[] = { 0x00, 0x00, 0x52, 0x8a, };
+       static const uint8_t *req_bytes[] = {
+               [REQ_LIVE_READ] = req_live,
+               [REQ_REC_HEAD] = req_head,
+               [REQ_REC_NEXT] = req_next,
+               [REQ_REC_CURR] = req_curr,
+       };
+       static const size_t req_len = ARRAY_SIZE(req_live);
+
+       const uint8_t *p;
+       size_t l;
+       int ret;
 
-       serial_write_nonblocking(serial, request, sizeof(request));
+       if (t >= ARRAY_SIZE(req_bytes))
+               return SR_ERR_ARG;
+       p = req_bytes[t];
+       l = req_len;
+       ret = serial_write_nonblocking(serial, p, l);
+       if (ret < 0)
+               return ret;
+       if ((size_t)ret != l)
+               return SR_ERR_IO;
 
        return SR_OK;
 }
+
+SR_PRIV int sr_brymen_bm52x_packet_request(struct sr_serial_dev_inst *serial)
+{
+       return bm52x_send_req(serial, REQ_LIVE_READ);
+}
 #endif
 
+/*
+ * The following code interprets live readings ("real-time download")
+ * which arrive in the "traditional" bitmap for LCD segments. Reading
+ * previously recorded measurements ("memory data sets") differs a lot
+ * and is handled in other code paths.
+ */
+
 SR_PRIV gboolean sr_brymen_bm52x_packet_valid(const uint8_t *buf)
 {
        if (buf[16] != 0x52)
@@ -456,3 +515,1022 @@ SR_PRIV int sr_brymen_bm52x_parse(const uint8_t *buf, float *val,
 
        return SR_OK;
 }
+
+/*
+ * The above code paths support live readings ("real-time download").
+ * The below code paths support recordings ("memory data sets") which
+ * use different requests and responses and measurement representation
+ * which feels like "a different meter".
+ */
+
+/*
+ * Developer notes, example data for recorded sessions.
+ *
+ * model
+ * 01
+ *    total bytes
+ *    e6 02 00
+ *             session count
+ *             01 00
+ *                   "DLE/STX" marker
+ *                   ee a0
+ *                         PS/NS addresses
+ *                         8a 03 a0 60 03 a0
+ *                                           func/sel/stat (DC-V, single display)
+ *                                           02 00 00
+ *                                                    session page length in bytes (3 * 240)
+ *                                                    d0 02 00
+ *                                                             main[/secondary] display data
+ *                                                             00 00 00 00
+ *                                                                          checksums and padding
+ *                                                                          7c 05 00 00 00 00 00 00
+ * 00 00 80 00 00 80 00 00 80 00 00 80 00 00 00 00 00 80 00 00 80 00 00 80  80 03 00 00 00 00 00 00
+ * 00 00 00 00 00 00 00 00 80 00 00 80 00 00 80 00 00 80 00 00 80 00 00 80  00 03 00 00 00 00 00 00
+ * ...
+ * 00 00 80 00 00 00 00 00 00 00 00 80 00 00 80 00 00 80 00 00 80 00 00 80  00 03 00 00 00 00 00 00
+ * 00 00 80 00 00 80 00 00 80 00 00 80 00 00 80 00 00 80 00 00
+ *                                                             "DLE/ETX" marker
+ *                                                             ee c0
+ *                                                                          ae 04 00 00 00 00 00 00
+ *
+ * - Checksum in bytes[25:24] is the mere sum of bytes[0:23].
+ * - Model ID is 0 or 1 -- does this translate to BM521s and BM525s?
+ * - Total byte count _includes_ everything starting at model ID.
+ * - There is no measurements count for a session page, but its length
+ *   in bytes, and a dual display flag, which lets us derive the count.
+ * - STX/ETX/DLE markers don't use the expected ASCII codes.
+ */
+
+/*
+ * See vendor doc table 3.1 "Logging interval". Includes sub-1Hz rates,
+ * but also sub-1s intervals. Let's keep both presentations at hand.
+ */
+static const struct {
+       unsigned int ival_secs;
+       unsigned int freq_rate;
+} bm52x_rec_ivals[] = {
+       [ 0] = {   0, 20, },
+       [ 1] = {   0, 10, },
+       [ 2] = {   0,  2, },
+       [ 3] = {   1,  1, },
+       [ 4] = {   2,  0, },
+       [ 5] = {   3,  0, },
+       [ 6] = {   4,  0, },
+       [ 7] = {   5,  0, },
+       [ 8] = {  10,  0, },
+       [ 9] = {  15,  0, },
+       [10] = {  30,  0, },
+       [11] = {  60,  0, },
+       [12] = { 120,  0, },
+       [13] = { 180,  0, },
+       [14] = { 300,  0, },
+       [15] = { 600,  0, },
+};
+
+/*
+ * See vendor doc table 6 "Range bits". Temperature is not listed there
+ * but keeping it here unifies the processing code paths.
+ */
+static const int bm52x_ranges_volt[16] = { 3, 2, 1, 0, };
+static const int bm52x_ranges_millivolt[16] = { 5, 4, };
+static const int bm52x_ranges_freq[16] = { 3, 2, 1, 0, -1, -2, -3, };
+static const int bm52x_ranges_duty[16] = { 2, 1, };
+static const int bm52x_ranges_ohm[16] = { 1, 0, -1, -2, -3, -4, };
+static const int bm52x_ranges_cond[16] = { 11, };
+static const int bm52x_ranges_cap[16] = { 11, 10, 9, 8, 7, 6, 5, };
+static const int bm52x_ranges_diode[16] = { 3, };
+static const int bm52x_ranges_temp[16] = { 0, };
+static const int bm52x_ranges_amp[16] = { 3, 2, };
+static const int bm52x_ranges_milliamp[16] = { 5, 4, };
+static const int bm52x_ranges_microamp[16] = { 7, 6, };
+
+/** Calculate checksum of four-HID-report responses (recordings). */
+static uint16_t bm52x_rec_checksum(const uint8_t *b, size_t l)
+{
+       uint16_t cs;
+
+       cs = 0;
+       while (l--)
+               cs += *b++;
+
+       return cs;
+}
+
+/**
+ * Retrieve the first/next chunk of recording information.
+ * Support for live readings is theoretical, and unused/untested.
+ */
+static int bm52x_rec_next_rsp(struct sr_serial_dev_inst *serial,
+       enum bm52x_reqtype req, struct brymen_bm52x_state *state)
+{
+       uint8_t *b;
+       size_t l;
+       int ret;
+
+       /* Seed internal state when sending the HEAD request. */
+       if (req == REQ_REC_HEAD || req == REQ_LIVE_READ)
+               memset(&state->rsp, 0, sizeof(state->rsp));
+
+       /* Move unprocessed content to the front. */
+       if (state->rsp.read_pos) {
+               b = &state->rsp.buff[0];
+               l = state->rsp.fill_pos - state->rsp.read_pos;
+               if (l)
+                       memmove(&b[0], &b[state->rsp.read_pos], l);
+               state->rsp.fill_pos -= state->rsp.read_pos;
+               state->rsp.read_pos = 0;
+       }
+
+       /* Avoid queries for non-existing data. Limit NEXT requests. */
+       if (req == REQ_REC_NEXT && !state->rsp.remain)
+               return SR_ERR_IO;
+
+       /* Add another response chunk to the read buffer. */
+       b = &state->rsp.buff[state->rsp.fill_pos];
+       l = req == REQ_LIVE_READ ? 24 : 32;
+       if (sizeof(state->rsp.buff) - state->rsp.fill_pos < l)
+               return SR_ERR_BUG;
+       ret = bm52x_send_req(serial, req);
+       if (ret != SR_OK)
+               return ret;
+       ret = serial_read_blocking(serial, b, l, 1000);
+       if (ret < 0)
+               return ret;
+       if ((size_t)ret != l)
+               return SR_ERR_IO;
+       state->rsp.fill_pos += l;
+
+       /* Devel support: dump the new receive data. */
+       if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
+               GString *text;
+               const char *req_text;
+
+               req_text = (req == REQ_LIVE_READ) ? "LIVE" :
+                       (req == REQ_REC_HEAD) ? "MEM HEAD" :
+                       (req == REQ_REC_NEXT) ? "MEM NEXT" :
+                       (req == REQ_REC_CURR) ? "MEM CURR" :
+                       "<inv>";
+               text = sr_hexdump_new(b, l);
+               sr_spew("%s: %s", req_text, text->str);
+               sr_hexdump_free(text);
+       }
+
+       /* Verify checksum. No CURR repetition is attempted here. */
+       if (l > 24) {
+               uint16_t calc, rcvd;
+
+               calc = bm52x_rec_checksum(b, 24);
+               rcvd = read_u16le(&b[24]);
+               if (calc != rcvd)
+                       return SR_ERR_DATA;
+               state->rsp.fill_pos -= 32 - 24;
+       }
+
+       /* Seed amount of total available data from HEAD response. */
+       if (req == REQ_REC_HEAD) {
+               const uint8_t *rdptr;
+
+               rdptr = &state->rsp.buff[0];
+               (void)read_u8_inc(&rdptr); /* model ID */
+               state->rsp.remain = read_u24le_inc(&rdptr); /* byte count */
+       }
+
+       return SR_OK;
+}
+
+/** Make sure a minimum amount of response data is available. */
+static const uint8_t *bm52x_rec_ensure(struct sr_serial_dev_inst *serial,
+       size_t min_count, struct brymen_bm52x_state *state)
+{
+       size_t got;
+       const uint8_t *read_ptr;
+       int ret;
+
+       got = state->rsp.fill_pos - state->rsp.read_pos;
+       if (got >= min_count) {
+               read_ptr = &state->rsp.buff[state->rsp.read_pos];
+               return read_ptr;
+       }
+       ret = bm52x_rec_next_rsp(serial, REQ_REC_NEXT, state);
+       if (ret < 0)
+               return NULL;
+       read_ptr = &state->rsp.buff[state->rsp.read_pos];
+       return read_ptr;
+}
+
+/** Get a u8 quantity of response data, with auto-fetch and position increment. */
+static uint8_t bm52x_rec_get_u8(struct sr_serial_dev_inst *serial,
+       struct brymen_bm52x_state *state)
+{
+       const uint8_t *read_ptr;
+       uint8_t value;
+       size_t length;
+
+       length = sizeof(value);
+       if (length > state->rsp.remain) {
+               state->rsp.remain = 0;
+               return 0;
+       }
+       read_ptr = bm52x_rec_ensure(serial, length, state);
+       if (!read_ptr)
+               return 0;
+       value = read_u8(read_ptr);
+       state->rsp.read_pos += length;
+       state->rsp.remain -= length;
+       return value;
+}
+
+/** Get a u16 quantity of response data, with auto-fetch and position increment. */
+static uint16_t bm52x_rec_get_u16(struct sr_serial_dev_inst *serial,
+       struct brymen_bm52x_state *state)
+{
+       const uint8_t *read_ptr;
+       uint16_t value;
+       size_t length;
+
+       length = sizeof(value);
+       if (length > state->rsp.remain) {
+               state->rsp.remain = 0;
+               return 0;
+       }
+       read_ptr = bm52x_rec_ensure(serial, length, state);
+       if (!read_ptr)
+               return 0;
+       value = read_u16le(read_ptr);
+       state->rsp.read_pos += length;
+       state->rsp.remain -= length;
+       return value;
+}
+
+/** Get a u24 quantity of response data, with auto-fetch and position increment. */
+static uint32_t bm52x_rec_get_u24(struct sr_serial_dev_inst *serial,
+       struct brymen_bm52x_state *state)
+{
+       const uint8_t *read_ptr;
+       uint32_t value;
+       size_t length;
+
+       length = 24 / sizeof(uint8_t) / 8;
+       if (length > state->rsp.remain) {
+               state->rsp.remain = 0;
+               return 0;
+       }
+       read_ptr = bm52x_rec_ensure(serial, length, state);
+       if (!read_ptr)
+               return 0;
+       value = read_u24le(read_ptr);
+       state->rsp.read_pos += length;
+       state->rsp.remain -= length;
+       return value;
+}
+
+/** Get the HEAD chunk of recording data, determine session page count. */
+static int bm52x_rec_get_count(struct brymen_bm52x_state *state,
+       struct sr_serial_dev_inst *serial)
+{
+       int ret;
+       size_t byte_count, sess_count;
+
+       memset(&state->rsp, 0, sizeof(state->rsp));
+       ret = bm52x_rec_next_rsp(serial, REQ_REC_HEAD, state);
+       if (ret != SR_OK)
+               return ret;
+
+       (void)bm52x_rec_get_u8(serial, state); /* model ID */
+       byte_count = bm52x_rec_get_u24(serial, state); /* total bytes count */
+       sess_count = bm52x_rec_get_u16(serial, state); /* session count */
+       sr_dbg("bytes %zu, sessions %zu", byte_count, sess_count);
+
+       return sess_count;
+}
+
+static double bm52x_rec_get_value(uint32_t raw, const int *ranges, int *digits)
+{
+       uint16_t val_digs;
+       gboolean is_neg, is_ol, low_batt;
+       uint8_t range;
+       double value;
+       int decimals;
+
+       val_digs = raw >> 8;
+       is_neg = raw & (1u << 7);
+       is_ol = raw & (1u << 6);
+       low_batt = raw & (1u << 5);
+       range = raw & 0x0f;
+       sr_dbg("item: %s%u, %s %s, range %01x",
+               is_neg ? "-" : "+", val_digs,
+               is_ol ? "OL" : "ol", low_batt ? "BATT" : "batt",
+               range);
+
+       /* Convert to number. OL takes precedence. */
+       *digits = 0;
+       value = val_digs;
+       if (ranges && ranges[range]) {
+               decimals = ranges[range];
+               value /= pow(10, decimals);
+               *digits = decimals;
+       }
+       if (is_ol)
+               value = INFINITY;
+       if (is_neg)
+               value *= -1;
+
+       /*
+        * Implementor's note: "Low battery" conditions are worth a
+        * warning since the reading could be incorrect. Rate limiting
+        * is not needed since the Brymen DMM will stop recording in
+        * that case, so at most the last sample in the session page
+        * could be affected.
+        */
+       if (low_batt)
+               sr_warn("Recording was taken when battery was low.");
+
+       return value;
+}
+
+static int bm52x_rec_prep_feed(uint8_t bfunc, uint8_t bsel, uint8_t bstat,
+       struct sr_datafeed_analog *analog1, struct sr_datafeed_analog *analog2,
+       double *value1, double *value2, const int **ranges1, const int **ranges2,
+       const struct sr_dev_inst *sdi)
+{
+       struct sr_channel *ch;
+       gboolean is_amp, is_deg_f;
+       enum sr_mq *mq1, *mq2;
+       enum sr_unit *unit1, *unit2;
+       enum sr_mqflag *mqf1, *mqf2;
+       enum sr_unit unit_c_f;
+       const int *r_a_ma;
+
+       /* Prepare general submission on first channel. */
+       analog1->data = value1;
+       analog1->encoding->unitsize = sizeof(*value1);
+       analog1->num_samples = 1;
+       ch = g_slist_nth_data(sdi->channels, 0);
+       analog1->meaning->channels = g_slist_append(NULL, ch);
+       *ranges1 = NULL;
+       mq1 = &analog1->meaning->mq;
+       mqf1 = &analog1->meaning->mqflags;
+       unit1 = &analog1->meaning->unit;
+
+       /* Prepare general submission on second channel. */
+       analog2->data = value2;
+       analog2->encoding->unitsize = sizeof(*value2);
+       analog2->num_samples = 1;
+       ch = g_slist_nth_data(sdi->channels, 1);
+       analog2->meaning->channels = g_slist_append(NULL, ch);
+       *ranges2 = NULL;
+       mq2 = &analog2->meaning->mq;
+       mqf2 = &analog2->meaning->mqflags;
+       unit2 = &analog2->meaning->unit;
+
+       /* Derive main/secondary display functions from bfunc/bsel/bstat. */
+       is_amp = bstat & (1u << 5);
+       is_deg_f = bstat & (1u << 4);
+       switch (bfunc) {
+       case 1: /* AC V */
+               switch (bsel) {
+               case 0: /* AC volt, Hz */
+                       *ranges1 = bm52x_ranges_volt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_freq;
+                       *mq2 = SR_MQ_FREQUENCY;
+                       *unit2 = SR_UNIT_HERTZ;
+                       break;
+               case 1: /* Hz, AC volt */
+                       *ranges1 = bm52x_ranges_freq;
+                       *mq1 = SR_MQ_FREQUENCY;
+                       *unit1 = SR_UNIT_HERTZ;
+                       *ranges2 = bm52x_ranges_volt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 2: /* DC V */
+               switch (bsel) {
+               case 0: /* DC V, - */
+                       *ranges1 = bm52x_ranges_volt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_VOLT;
+                       break;
+               case 1: /* DC V, AC V */
+                       *ranges1 = bm52x_ranges_volt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_volt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               case 2: /* DC+AC V, AC V */
+                       *ranges1 = bm52x_ranges_volt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_volt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 3: /* DC mV */
+               switch (bsel) {
+               case 0: /* DC mV, - */
+                       *ranges1 = bm52x_ranges_millivolt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_VOLT;
+                       break;
+               case 1: /* DC mV, AC mV */
+                       *ranges1 = bm52x_ranges_millivolt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_millivolt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               case 2: /* DC+AC mV, AC mV */
+                       *ranges1 = bm52x_ranges_millivolt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_millivolt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               case 3: /* Hz, - */
+                       *ranges1 = bm52x_ranges_freq;
+                       *mq1 = SR_MQ_FREQUENCY;
+                       *unit1 = SR_UNIT_HERTZ;
+                       break;
+               case 4: /* %, - */
+                       *ranges1 = bm52x_ranges_duty;
+                       *mq1 = SR_MQ_DUTY_CYCLE;
+                       *unit1 = SR_UNIT_PERCENTAGE;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 4: /* AC mV */
+               switch (bsel) {
+               case 0: /* AC mV, Hz */
+                       *ranges1 = bm52x_ranges_millivolt;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_VOLT;
+                       *ranges2 = bm52x_ranges_freq;
+                       *mq2 = SR_MQ_FREQUENCY;
+                       *unit2 = SR_UNIT_HERTZ;
+                       break;
+               case 1: /* Hz, AC mV */
+                       *ranges1 = bm52x_ranges_freq;
+                       *mq1 = SR_MQ_FREQUENCY;
+                       *unit1 = SR_UNIT_HERTZ;
+                       *ranges2 = bm52x_ranges_millivolt;
+                       *mq2 = SR_MQ_VOLTAGE;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_VOLT;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 5: /* Res/Cond/Cont */
+               switch (bsel) {
+               case 0: /* Resistance */
+                       *ranges1 = bm52x_ranges_ohm;
+                       *mq1 = SR_MQ_RESISTANCE;
+                       *unit1 = SR_UNIT_OHM;
+                       break;
+               case 1: /* Siemens */
+                       *ranges1 = bm52x_ranges_cond;
+                       *mq1 = SR_MQ_CONDUCTANCE;
+                       *unit1 = SR_UNIT_SIEMENS;
+                       break;
+               case 2: /* Continuity */
+                       *ranges1 = bm52x_ranges_ohm;
+                       *mq1 = SR_MQ_CONTINUITY;
+                       *unit1 = SR_UNIT_OHM;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 6: /* Temperature */
+               unit_c_f = is_deg_f ? SR_UNIT_FAHRENHEIT : SR_UNIT_CELSIUS;
+               switch (bsel) {
+               case 0: /* T1, - */
+                       *ranges1 = bm52x_ranges_temp;
+                       *mq1 = SR_MQ_TEMPERATURE;
+                       *unit1 = unit_c_f;
+                       break;
+               case 1: /* T2, - */
+                       *ranges1 = bm52x_ranges_temp;
+                       *mq1 = SR_MQ_TEMPERATURE;
+                       *unit1 = unit_c_f;
+                       break;
+               case 2: /* T1, T2 */
+                       *ranges1 = bm52x_ranges_temp;
+                       *mq1 = SR_MQ_TEMPERATURE;
+                       *unit1 = unit_c_f;
+                       *ranges2 = bm52x_ranges_temp;
+                       *mq2 = SR_MQ_TEMPERATURE;
+                       *unit2 = unit_c_f;
+                       break;
+               case 3: /* T1-T2, T2 */
+                       *ranges1 = bm52x_ranges_temp;
+                       *mq1 = SR_MQ_TEMPERATURE;
+                       *unit1 = unit_c_f;
+                       *ranges2 = bm52x_ranges_temp;
+                       *mq2 = SR_MQ_TEMPERATURE;
+                       *unit2 = unit_c_f;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 7: /* Cap/Diode */
+               switch (bsel) {
+               case 0: /* Capacitance, - */
+                       *ranges1 = bm52x_ranges_cap;
+                       *mq1 = SR_MQ_CAPACITANCE;
+                       *unit1 |= SR_UNIT_FARAD;
+                       break;
+               case 1: /* Diode voltage, - */
+                       *ranges1 = bm52x_ranges_diode;
+                       *mq1 = SR_MQ_VOLTAGE;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *mqf1 |= SR_MQFLAG_DIODE;
+                       *unit1 |= SR_UNIT_VOLT;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 8: /* DC A/mA */
+               r_a_ma = is_amp ? bm52x_ranges_amp : bm52x_ranges_milliamp;
+               switch (bsel) {
+               case 0: /* DC A/mA, - */
+                       *ranges1 = r_a_ma;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       break;
+               case 1: /* DC A/mA, AC A/mA */
+                       *ranges1 = r_a_ma;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = r_a_ma;
+                       *mq2 = SR_MQ_CURRENT;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_AMPERE;
+                       break;
+               case 2: /* DC+AC A/mA, AC A/mA */
+                       *ranges1 = r_a_ma;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = r_a_ma;
+                       *mq2 = SR_MQ_CURRENT;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_AMPERE;
+                       break;
+               case 3: /* AC A/mA, Hz */
+                       *ranges1 = r_a_ma;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = bm52x_ranges_freq;
+                       *mq2 = SR_MQ_FREQUENCY;
+                       *unit2 = SR_UNIT_HERTZ;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       case 9: /* DC uA */
+               switch (bsel) {
+               case 0: /* DC uA, - */
+                       *ranges1 = bm52x_ranges_microamp;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       break;
+               case 1: /* DC uA, AC uA */
+                       *ranges1 = bm52x_ranges_microamp;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = bm52x_ranges_microamp;
+                       *mq2 = SR_MQ_CURRENT;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_AMPERE;
+                       break;
+               case 2: /* DC+AC uA, AC uA */
+                       *ranges1 = bm52x_ranges_microamp;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_DC;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = bm52x_ranges_microamp;
+                       *mq2 = SR_MQ_CURRENT;
+                       *mqf2 |= SR_MQFLAG_AC;
+                       *unit2 = SR_UNIT_AMPERE;
+                       break;
+               case 3: /* AC uA, Hz */
+                       *ranges1 = bm52x_ranges_microamp;
+                       *mq1 = SR_MQ_CURRENT;
+                       *mqf1 |= SR_MQFLAG_AC;
+                       *unit1 = SR_UNIT_AMPERE;
+                       *ranges2 = bm52x_ranges_freq;
+                       *mq2 = SR_MQ_FREQUENCY;
+                       *unit2 = SR_UNIT_HERTZ;
+                       break;
+               default:
+                       return SR_ERR_DATA;
+               }
+               break;
+       default:
+               return SR_ERR_DATA;
+       }
+
+       return SR_OK;
+}
+
+/** Traverse one recorded session page, optionally feed session bus. */
+static int bm52x_rec_read_page_int(const struct sr_dev_inst *sdi,
+       struct brymen_bm52x_state *state, struct sr_serial_dev_inst *serial,
+       gboolean skip)
+{
+       uint8_t bfunc, bsel, bstat;
+       uint8_t ival;
+       gboolean has_sec_disp;
+       size_t page_len, meas_len, meas_count;
+       uint32_t meas_data;
+       struct sr_datafeed_packet packet;
+       struct sr_datafeed_analog analog1, analog2;
+       struct sr_analog_encoding encoding1, encoding2;
+       struct sr_analog_meaning meaning1, meaning2;
+       struct sr_analog_spec spec1, spec2;
+       int digits, ret;
+       double values[2];
+       const int *ranges1, *ranges2;
+       enum sr_configkey key;
+       uint64_t num;
+
+       sr_dbg("progress: %s, %s", __func__, skip ? "skip" : "feed");
+
+       /* Get the header information of the session page (raw). */
+       if (bm52x_rec_get_u8(serial, state) != 0xee) /* "DLE" */
+               return SR_ERR_DATA;
+       if (bm52x_rec_get_u8(serial, state) != 0xa0) /* "STX" */
+               return SR_ERR_DATA;
+       (void)bm52x_rec_get_u24(serial, state); /* prev page addr */
+       (void)bm52x_rec_get_u24(serial, state); /* next page addr */
+       bfunc = bm52x_rec_get_u8(serial, state); /* meter function */
+       bsel = bm52x_rec_get_u8(serial, state); /* fun selection */
+       bstat = bm52x_rec_get_u8(serial, state); /* status */
+       page_len = bm52x_rec_get_u24(serial, state); /* page length */
+       sr_dbg("page head: func/sel/state %02x/%02x/%02x, len %zu",
+               bfunc, bsel, bstat, page_len);
+
+       /* Interpret the header information of the session page. */
+       ival = bstat & 0x0f;
+       has_sec_disp = bstat & (1u << 7);
+       meas_len = (has_sec_disp ? 2 : 1) * 3;
+       if (page_len % meas_len)
+               return SR_ERR_DATA;
+       meas_count = page_len / meas_len;
+       sr_dbg("page head: ival %u, %s, samples %zu",
+               ival, has_sec_disp ? "dual" : "main", meas_count);
+
+       /* Prepare feed to the sigrok session. Send rate/interval. */
+       sr_analog_init(&analog1, &encoding1, &meaning1, &spec1, 0);
+       sr_analog_init(&analog2, &encoding2, &meaning2, &spec2, 0);
+       ret = bm52x_rec_prep_feed(bfunc, bsel, bstat,
+               &analog1, &analog2, &values[0], &values[1],
+               &ranges1, &ranges2, sdi);
+       if (ret != SR_OK)
+               return SR_ERR_DATA;
+       if (!skip) {
+               memset(&packet, 0, sizeof(packet));
+               packet.type = SR_DF_ANALOG;
+
+               if (bm52x_rec_ivals[ival].freq_rate) {
+                       sr_dbg("rate: %u", bm52x_rec_ivals[ival].freq_rate);
+                       key = SR_CONF_SAMPLERATE;
+                       num = bm52x_rec_ivals[ival].freq_rate;
+                       (void)sr_session_send_meta(sdi,
+                               key, g_variant_new_uint64(num));
+               }
+               if (bm52x_rec_ivals[ival].ival_secs) {
+                       sr_dbg("ival: %u", bm52x_rec_ivals[ival].ival_secs);
+                       key = SR_CONF_SAMPLE_INTERVAL;
+                       num = bm52x_rec_ivals[ival].ival_secs * 1000; /* in ms */
+                       (void)sr_session_send_meta(sdi,
+                               key, g_variant_new_uint64(num));
+               }
+       }
+
+       /*
+        * Implementor's note:
+        * Software limits require devc access, which is an internal
+        * detail of the serial-dmm driver, which this bm52x parser
+        * is not aware of. So we always provide the complete set of
+        * recorded samples. Should be acceptable. Duplicating limit
+        * support in local config get/set is considered undesirable.
+        */
+       while (meas_count--) {
+               meas_data = bm52x_rec_get_u24(serial, state);
+               values[0] = bm52x_rec_get_value(meas_data, ranges1, &digits);
+               if (!skip) {
+                       analog1.encoding->digits  = digits;
+                       analog1.spec->spec_digits = digits;
+                       packet.payload = &analog1;
+                       ret = sr_session_send(sdi, &packet);
+                       if (ret != SR_OK)
+                               return ret;
+               }
+
+               if (!has_sec_disp)
+                       continue;
+               meas_data = bm52x_rec_get_u24(serial, state);
+               values[1] = bm52x_rec_get_value(meas_data, ranges2, &digits);
+               if (!skip) {
+                       analog2.encoding->digits  = digits;
+                       analog2.spec->spec_digits = digits;
+                       packet.payload = &analog2;
+                       ret = sr_session_send(sdi, &packet);
+                       if (ret != SR_OK)
+                               return ret;
+               }
+       }
+
+       /* Check termination of the session page. */
+       if (bm52x_rec_get_u8(serial, state) != 0xee) /* "DLE" */
+               return SR_ERR_DATA;
+       if (bm52x_rec_get_u8(serial, state) != 0xc0) /* "ETX" */
+               return SR_ERR_DATA;
+
+       return SR_OK;
+}
+
+/** Skip one recorded session page. */
+static int bm52x_rec_skip_page(const struct sr_dev_inst *sdi,
+       struct brymen_bm52x_state *state, struct sr_serial_dev_inst *serial)
+{
+       return bm52x_rec_read_page_int(sdi, state, serial, TRUE);
+}
+
+/** Forward one recorded session page. */
+static int bm52x_rec_read_page(const struct sr_dev_inst *sdi,
+       struct brymen_bm52x_state *state, struct sr_serial_dev_inst *serial)
+{
+       return bm52x_rec_read_page_int(sdi, state, serial, FALSE);
+}
+
+SR_PRIV void *brymen_bm52x_state_init(void)
+{
+       return g_malloc0(sizeof(struct brymen_bm52x_state));
+}
+
+SR_PRIV void brymen_bm52x_state_free(void *state)
+{
+       g_free(state);
+}
+
+SR_PRIV int brymen_bm52x_config_get(void *st, uint32_t key, GVariant **data,
+       const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
+{
+       struct brymen_bm52x_state *state;
+       char text[20];
+
+       state = st;
+
+       if (!sdi)
+               return SR_ERR_NA;
+       (void)cg;
+
+       switch (key) {
+       case SR_CONF_DATA_SOURCE:
+               if (!state)
+                       return SR_ERR_ARG;
+               if (state->sess_idx == 0)
+                       snprintf(text, sizeof(text), "Live");
+               else
+                       snprintf(text, sizeof(text), "Rec-%zu", state->sess_idx);
+               *data = g_variant_new_string(text);
+               return SR_OK;
+       default:
+               return SR_ERR_NA;
+       }
+}
+
+SR_PRIV int brymen_bm52x_config_set(void *st, uint32_t key, GVariant *data,
+       const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
+{
+       struct brymen_bm52x_state *state;
+       const char *s;
+       int ret, nr;
+
+       state = st;
+
+       if (!sdi)
+               return SR_ERR_NA;
+       (void)cg;
+
+       switch (key) {
+       case SR_CONF_DATA_SOURCE:
+               s = g_variant_get_string(data, NULL);
+               if (!s || !*s)
+                       return SR_ERR_ARG;
+               if (strcasecmp(s, "Live") == 0) {
+                       state->sess_idx = 0;
+                       return SR_OK;
+               }
+               if (strncasecmp(s, "Rec-", strlen("Rec-")) != 0)
+                       return SR_ERR_ARG;
+               s += strlen("Rec-");
+               ret = sr_atoi(s, &nr);
+               if (ret != SR_OK || nr <= 0 || nr > 999)
+                       return SR_ERR_ARG;
+               state->sess_idx = nr;
+               return SR_OK;
+       default:
+               return SR_ERR_NA;
+       }
+}
+
+SR_PRIV int brymen_bm52x_config_list(void *st, uint32_t key, GVariant **data,
+       const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
+{
+       struct brymen_bm52x_state *state;
+       struct sr_serial_dev_inst *serial;
+       int ret;
+       size_t count, idx;
+       GVariantBuilder gvb;
+       char name[20];
+
+       /*
+        * Have common keys handled by caller's common code.
+        * ERR N/A results in the caller's logic handling the request.
+        * Only handle strictly local properties here in this code path.
+        */
+       switch (key) {
+       case SR_CONF_SCAN_OPTIONS:
+               /* Scan options. Common property. */
+               return SR_ERR_NA;
+       case SR_CONF_DEVICE_OPTIONS:
+               if (!sdi)
+                       /* Driver options. Common property. */
+                       return SR_ERR_NA;
+               if (cg)
+                       /* Channel group's devopts. Common error path. */
+                       return SR_ERR_NA;
+               /* List meter's local device options. Overrides common data. */
+               *data = g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32,
+                               devopts, ARRAY_SIZE(devopts), sizeof(uint32_t));
+               return SR_OK;
+       case SR_CONF_DATA_SOURCE:
+               state = st;
+               if (!sdi)
+                       return SR_ERR_ARG;
+               serial = sdi->conn;
+               ret = bm52x_rec_get_count(state, serial);
+               if (ret < 0)
+                       return ret;
+               count = ret;
+               g_variant_builder_init(&gvb, G_VARIANT_TYPE_ARRAY);
+               for (idx = 0; idx <= count; idx++) {
+                       if (idx == 0)
+                               snprintf(name, sizeof(name), "Live");
+                       else
+                               snprintf(name, sizeof(name), "Rec-%zu", idx);
+                       g_variant_builder_add(&gvb, "s", name);
+               }
+               *data = g_variant_builder_end(&gvb);
+               return SR_OK;
+       default:
+               return SR_ERR_NA;
+       }
+}
+
+/**
+ * BM520s specific receive routine for recorded measurements.
+ *
+ * @param[in] fd File descriptor.
+ * @param[in] revents Mask of pending events.
+ * @param[in] cb_data Call back data (receive handler state).
+ *
+ * @return TRUE when the routine needs to get re-invoked, FALSE when the
+ *   routine is done and no further invocations are required.
+ *
+ * @private
+ *
+ * It's an implementation detail that a single invocation will carry out
+ * all the work that is involved in reading back recorded measurements.
+ */
+static int bm52x_rec_receive_data(int fd, int revents, void *cb_data)
+{
+       struct brymen_bm52x_state *state;
+       const struct sr_dev_inst *sdi;
+       struct sr_dev_inst *sdi_rw;
+       struct sr_serial_dev_inst *serial;
+       int ret;
+       size_t count, idx;
+
+       (void)fd;
+       (void)revents;
+       state = cb_data;
+
+       sdi = state->sdi;
+       serial = sdi->conn;
+
+       ret = bm52x_rec_get_count(state, serial);
+       if (ret < 0)
+               return FALSE;
+       count = ret;
+
+       /* Un-const 'sdi' since sr_dev_acquisition_stop() needs that. */
+       sdi_rw = (struct sr_dev_inst *)sdi;
+
+       /*
+        * Immediate (silent, zero data) stop for non-existent sessions.
+        * Early exit is an arbitrary implementation detail, in theory
+        * the loop below would transparently handle the situation when
+        * users request non-existing session pages.
+        */
+       if (state->sess_idx > count) {
+               sr_dev_acquisition_stop(sdi_rw);
+               return FALSE;
+       }
+
+       /* Iterate all session pages, forward the one of interest. */
+       for (idx = 1; idx <= count; idx++) {
+               if (idx == state->sess_idx)
+                       ret = bm52x_rec_read_page(sdi, state, serial);
+               else
+                       ret = bm52x_rec_skip_page(sdi, state, serial);
+               if (ret != SR_OK)
+                       break;
+       }
+
+       sr_dev_acquisition_stop(sdi_rw);
+       return FALSE;
+}
+
+/**
+ * BM520s specific acquisition start callback.
+ *
+ * @param[in] st DMM parser state.
+ * @param[in] sdi Device instance.
+ * @param[out] cb Receive callback for the acquisition.
+ * @param[out] cb_data Callback data for receive callback.
+ *
+ * @returns SR_OK upon success.
+ * @returns SR_ERR* upon failure.
+ *
+ * @private
+ *
+ * The BM520s protocol parser uses common logic and the packet parser
+ * for live acquisition, but runs a different set of requests and a
+ * different response layout interpretation for recorded measurements.
+ */
+SR_PRIV int brymen_bm52x_acquire_start(void *st, const struct sr_dev_inst *sdi,
+       sr_receive_data_callback *cb, void **cb_data)
+{
+       struct brymen_bm52x_state *state;
+
+       if (!sdi || !st)
+               return SR_ERR_ARG;
+       state = st;
+
+       /* Read live measurements. No local override required. */
+       if (state->sess_idx == 0)
+               return SR_OK;
+
+       /* Arrange to read back recorded session. */
+       sr_dbg("session page requested: %zu", state->sess_idx);
+       state->sdi = sdi;
+       *cb = bm52x_rec_receive_data;
+       *cb_data = state;
+       return SR_OK;
+}