From: Gerhard Sittig Date: Sun, 9 Jun 2019 07:03:22 +0000 (+0200) Subject: dmm/bm86x: add Brymen BM86x packet parser, register with serial-dmm X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=0759e2f55b5e45ff283f396c38c08eb180ba83b7;p=libsigrok.git dmm/bm86x: add Brymen BM86x packet parser, register with serial-dmm Move Brymen BM86x specific packet parse logic to a new src/dmm/bm86x.c source file, and register the routines with the serial-dmm driver's list of supported devices. Which obsoletes the src/hardware/brymen-bm86x/ hierarchy. This implementation differs from the previous version: The parse routine gets called multiple times after one DMM packet was received. Values for the displays get extracted in separate invocations, the received packet is considered read-only. Unsupported LCD segment combinations for digits get logged. Low battery gets logged with higher severity -- the validity of measurement values is uncertain after all. The parse routine uses longer identifiers. Packet reception uses whichever serial transport is available (need no longer be strictly USB HID nor libusb based). All features of the previous implementation are believed to still be present in this version. This configuration queries measurement values each 0.5 seconds and re-sends a not responded to request after 1.5 seconds. Which follows the combination of the vendor's suggested flow (frequency) and the previous implementation's timeout (3x 500ms). This implementation does not try to re-connect to the HID device for each measurement, and neither checks for the 4.0 seconds timeout (vendor's suggested flow). Local experiments work without these. --- diff --git a/Makefile.am b/Makefile.am index cf0136f9..0f748f3e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -158,6 +158,7 @@ endif libsigrok_la_SOURCES += \ src/dmm/asycii.c \ src/dmm/bm25x.c \ + src/dmm/bm86x.c \ src/dmm/dtm0660.c \ src/dmm/eev121gw.c \ src/dmm/es519xx.c \ diff --git a/src/dmm/bm86x.c b/src/dmm/bm86x.c new file mode 100644 index 00000000..318d4af0 --- /dev/null +++ b/src/dmm/bm86x.c @@ -0,0 +1,359 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2014 Aurelien Jacobs + * Copyright (C) 2019 Gerhard Sittig + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file + * + * Brymen BM86x serial protocol parser. The USB protocol (for the cable) + * and the packet description (for the meter) were retrieved from: + * http://brymen.com/product-html/Download2.html + * http://brymen.com/product-html/PD02BM860s_protocolDL.html + * http://brymen.com/product-html/images/DownloadList/ProtocolList/BM860-BM860s_List/BM860-BM860s-500000-count-dual-display-DMMs-protocol.pdf + */ + +#include +#include +#include +#include "libsigrok-internal.h" +#include + +#define LOG_PREFIX "brymen-bm86x" + +#ifdef HAVE_SERIAL_COMM +SR_PRIV int sr_brymen_bm86x_packet_request(struct sr_serial_dev_inst *serial) +{ + static const uint8_t request[] = { 0x00, 0x00, 0x86, 0x66, }; + + sr_spew("%s() sending request", __func__); + serial_write_nonblocking(serial, request, sizeof(request)); + + return SR_OK; +} +#endif + +SR_PRIV gboolean sr_brymen_bm86x_packet_valid(const uint8_t *buf) +{ + GString *text; + + if (sr_log_loglevel_get() >= SR_LOG_SPEW) { + text = sr_hexdump_new(buf, BRYMEN_BM86X_PACKET_SIZE); + sr_spew("%s() checking DMM packet: %s", __func__, text->str); + sr_hexdump_free(text); + } + + /* + * "Model ID3" (3rd HID report, byte 3) is the only documented + * fixed value, and must be 0x86. All other positions either depend + * on the meter's function, or the measurement's value, or are not + * documented by the vendor (are marked as "don't care", no fixed + * values are listed). There is nothing else we can check reliably. + */ + if (buf[19] != 0x86) + return FALSE; + + return TRUE; +} + +/* + * Data bytes in the DMM packet encode LCD segments in an unusual order + * (bgcdafe) and in an unusual position (bits 7:1 within the byte). The + * decimal point (bit 0) for one digit resides in the _next_ digit's byte. + * + * These routines convert LCD segments to characters, and a section of the + * DMM packet (which corresponds to the primary or secondary display) to + * the text representation of the measurement's value, before regular text + * to number conversion is applied. The first byte of the passed in block + * contains indicators, the value's digits start at the second byte. + */ + +static char brymen_bm86x_parse_digit(uint8_t b) +{ + switch (b >> 1) { + /* Sign. */ + case 0x20: return '-'; + /* Decimal digits. */ + case 0x5f: return '0'; + case 0x50: return '1'; + case 0x6d: return '2'; + case 0x7c: return '3'; + case 0x72: return '4'; + case 0x3e: return '5'; + case 0x3f: return '6'; + case 0x54: return '7'; + case 0x7f: return '8'; + case 0x7e: return '9'; + /* Temperature units. */ + case 0x0f: return 'C'; + case 0x27: return 'F'; + /* OL condition, and diode mode. */ + case 0x0b: return 'L'; + case 0x79: return 'd'; + case 0x10: return 'i'; + case 0x39: return 'o'; + /* Blank digit. */ + case 0x00: return '\0'; + /* Invalid or unknown segment combination. */ + default: + sr_warn("Unknown encoding for digit: 0x%02x.", b); + return '\0'; + } +} + +static int brymen_bm86x_parse_digits(const uint8_t *pkt, size_t pktlen, + char *txtbuf, float *value, char *temp_unit, int *digits, int signflag) +{ + uint8_t byte; + char *txtptr, txtchar; + size_t pos; + int ret; + + txtptr = txtbuf; + if (digits) + *digits = INT_MIN; + + if (pkt[0] & signflag) + *txtptr++ = '-'; + for (pos = 0; pos < pktlen; pos++) { + byte = pkt[1 + pos]; + if (pos && pos < 5 && (byte & 0x01)) { + *txtptr++ = '.'; + if (digits) + *digits = 0; + } + txtchar = brymen_bm86x_parse_digit(byte); + if (pos == 5 && (txtchar == 'C' || txtchar == 'F')) { + if (temp_unit) + *temp_unit = txtchar; + } else if (txtchar) { + *txtptr++ = txtchar; + if (digits) + (*digits)++; + } + } + *txtptr = '\0'; + + if (digits && *digits < 0) + *digits = 0; + + ret = value ? sr_atof_ascii(txtbuf, value) : SR_OK; + if (ret != SR_OK) { + sr_dbg("invalid float string: '%s'", txtbuf); + return ret; + } + + return SR_OK; +} + +/* + * Extract the measurement value and its properties for one of the + * meter's displays from the DMM packet. + */ +static void brymen_bm86x_parse(const uint8_t *buf, float *floatval, + struct sr_datafeed_analog *analog, size_t ch_idx) +{ + char txtbuf[16], temp_unit; + int ret, digits, is_diode, over_limit, scale; + uint8_t ind1, ind15; + + if (ch_idx == 0) { + /* + * Main display. Note that _some_ of the second display's + * indicators are involved in the inspection of the _first_ + * display's measurement value. So we have to get the + * second display's text buffer here, too. + */ + (void)brymen_bm86x_parse_digits(&buf[9], 4, txtbuf, + NULL, NULL, NULL, 0); + is_diode = strcmp(txtbuf, "diod") == 0; + ret = brymen_bm86x_parse_digits(&buf[2], 6, txtbuf, + floatval, &temp_unit, &digits, 0x80); + over_limit = strstr(txtbuf, "0L") || strstr(txtbuf, "0.L"); + if (ret != SR_OK && !over_limit) + return; + + /* SI unit. */ + if (buf[8] & 0x01) { + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + if (is_diode) { + analog->meaning->mqflags |= SR_MQFLAG_DIODE; + analog->meaning->mqflags |= SR_MQFLAG_DC; + } + } else if (buf[14] & 0x80) { + analog->meaning->mq = SR_MQ_CURRENT; + analog->meaning->unit = SR_UNIT_AMPERE; + } else if (buf[14] & 0x20) { + analog->meaning->mq = SR_MQ_CAPACITANCE; + analog->meaning->unit = SR_UNIT_FARAD; + } else if (buf[14] & 0x10) { + analog->meaning->mq = SR_MQ_CONDUCTANCE; + analog->meaning->unit = SR_UNIT_SIEMENS; + } else if (buf[15] & 0x01) { + analog->meaning->mq = SR_MQ_FREQUENCY; + analog->meaning->unit = SR_UNIT_HERTZ; + } else if (buf[10] & 0x01) { + analog->meaning->mq = SR_MQ_CONTINUITY; + analog->meaning->unit = SR_UNIT_OHM; + } else if (buf[15] & 0x10) { + analog->meaning->mq = SR_MQ_RESISTANCE; + analog->meaning->unit = SR_UNIT_OHM; + } else if (buf[15] & 0x02) { + analog->meaning->mq = SR_MQ_POWER; + analog->meaning->unit = SR_UNIT_DECIBEL_MW; + } else if (buf[15] & 0x80) { + analog->meaning->mq = SR_MQ_DUTY_CYCLE; + analog->meaning->unit = SR_UNIT_PERCENTAGE; + } else if (buf[ 2] & 0x0a) { + analog->meaning->mq = SR_MQ_TEMPERATURE; + if (temp_unit == 'F') + analog->meaning->unit = SR_UNIT_FAHRENHEIT; + else + analog->meaning->unit = SR_UNIT_CELSIUS; + } + + /* + * Remove the MIN/MAX/AVG indicators when all of them + * are shown at the same time. + */ + ind1 = buf[1]; + if ((ind1 & 0xe0) == 0xe0) + ind1 &= ~0xe0; + + /* AC/DC/Auto flags. */ + if (buf[1] & 0x10) + analog->meaning->mqflags |= SR_MQFLAG_DC; + if (buf[2] & 0x01) + analog->meaning->mqflags |= SR_MQFLAG_AC; + if (buf[1] & 0x01) + analog->meaning->mqflags |= SR_MQFLAG_AUTORANGE; + if (buf[1] & 0x08) + analog->meaning->mqflags |= SR_MQFLAG_HOLD; + if (ind1 & 0x20) + analog->meaning->mqflags |= SR_MQFLAG_MAX; + if (ind1 & 0x40) + analog->meaning->mqflags |= SR_MQFLAG_MIN; + if (ind1 & 0x80) + analog->meaning->mqflags |= SR_MQFLAG_AVG; + if (buf[3] & 0x01) + analog->meaning->mqflags |= SR_MQFLAG_RELATIVE; + + /* + * Remove the "dBm" indication's "m" indicator before the + * SI unit's prefixes get inspected. To avoid an interaction + * with the "milli" prefix. + */ + ind15 = buf[15]; + if (ind15 & 0x02) + ind15 &= ~0x04; + + /* SI prefix. */ + scale = 0; + if (buf[14] & 0x40) /* n */ + scale = -9; + if (buf[15] & 0x08) /* u */ + scale = -6; + if (ind15 & 0x04) /* m */ + scale = -3; + if (buf[15] & 0x40) /* k */ + scale = +3; + if (buf[15] & 0x20) /* M */ + scale = +6; + if (scale) { + *floatval *= pow(10, scale); + digits += -scale; + } + + if (over_limit) + *floatval = INFINITY; + + analog->encoding->digits = digits; + analog->spec->spec_digits = digits; + } else if (ch_idx == 1) { + /* Secondary display. */ + ret = brymen_bm86x_parse_digits(&buf[9], 4, txtbuf, + floatval, &temp_unit, &digits, 0x10); + + /* SI unit. */ + if (buf[14] & 0x08) { + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + } else if (buf[9] & 0x04) { + analog->meaning->mq = SR_MQ_CURRENT; + analog->meaning->unit = SR_UNIT_AMPERE; + } else if (buf[9] & 0x08) { + analog->meaning->mq = SR_MQ_CURRENT; + analog->meaning->unit = SR_UNIT_PERCENTAGE; + } else if (buf[14] & 0x04) { + analog->meaning->mq = SR_MQ_FREQUENCY; + analog->meaning->unit = SR_UNIT_HERTZ; + } else if (buf[9] & 0x40) { + analog->meaning->mq = SR_MQ_TEMPERATURE; + if (temp_unit == 'F') + analog->meaning->unit = SR_UNIT_FAHRENHEIT; + else + analog->meaning->unit = SR_UNIT_CELSIUS; + } + + /* AC flag. */ + if (buf[9] & 0x20) + analog->meaning->mqflags |= SR_MQFLAG_AC; + + /* SI prefix. */ + scale = 0; + if (buf[ 9] & 0x01) /* u */ + scale = -6; + if (buf[ 9] & 0x02) /* m */ + scale = -3; + if (buf[14] & 0x02) /* k */ + scale = +3; + if (buf[14] & 0x01) /* M */ + scale = +6; + if (scale) { + *floatval *= pow(10, scale); + digits += -scale; + } + + analog->encoding->digits = digits; + analog->spec->spec_digits = digits; + } + + if (buf[9] & 0x80) + sr_warn("Battery is low."); +} + +SR_PRIV int sr_brymen_bm86x_parse(const uint8_t *buf, float *val, + struct sr_datafeed_analog *analog, void *info) +{ + struct brymen_bm86x_info *info_local; + size_t ch_idx; + + /* + * Scan a portion of the received DMM packet which corresponds + * to the caller's specified display. Then prepare to scan a + * different portion of the packet for another display. This + * routine gets called multiple times for one received packet. + */ + info_local = info; + ch_idx = info_local->ch_idx; + brymen_bm86x_parse(buf, val, analog, ch_idx); + info_local->ch_idx = ch_idx + 1; + + return SR_OK; +} diff --git a/src/hardware/serial-dmm/api.c b/src/hardware/serial-dmm/api.c index 1f724c71..f0418175 100644 --- a/src/hardware/serial-dmm/api.c +++ b/src/hardware/serial-dmm/api.c @@ -131,6 +131,8 @@ static GSList *scan(struct sr_dev_driver *di, GSList *options) sdi->conn = serial; sdi->priv = devc; dmm->channel_count = 1; + if (dmm->packet_parse == sr_brymen_bm86x_parse) + dmm->channel_count = BRYMEN_BM86X_DISPLAY_COUNT; if (dmm->packet_parse == sr_eev121gw_3displays_parse) { dmm->channel_count = EEV121GW_DISPLAY_COUNT; dmm->channel_formats = eev121gw_channel_formats; @@ -254,6 +256,15 @@ SR_REGISTER_DEV_DRIVER_LIST(serial_dmm_drivers, NULL ), /* }}} */ + /* bm86x based meters {{{ */ + DMM_CONN( + "brymen-bm86x", brymen_bm86x, "Brymen", "BM86x", + "hid/bu86x", NULL, BRYMEN_BM86X_PACKET_SIZE, 1500, 500, + sr_brymen_bm86x_packet_request, + sr_brymen_bm86x_packet_valid, sr_brymen_bm86x_parse, + NULL + ), + /* }}} */ /* dtm0660 based meters {{{ */ DMM( "peaktech-3415", dtm0660, diff --git a/src/libsigrok-internal.h b/src/libsigrok-internal.h index a6c113a5..ea444823 100644 --- a/src/libsigrok-internal.h +++ b/src/libsigrok-internal.h @@ -1568,6 +1568,20 @@ SR_PRIV gboolean sr_brymen_bm25x_packet_valid(const uint8_t *buf); SR_PRIV int sr_brymen_bm25x_parse(const uint8_t *buf, float *floatval, struct sr_datafeed_analog *analog, void *info); +/*--- dmm/bm86x.c -----------------------------------------------------------*/ + +#define BRYMEN_BM86X_PACKET_SIZE 24 +#define BRYMEN_BM86X_DISPLAY_COUNT 2 + +struct brymen_bm86x_info { size_t ch_idx; }; + +#ifdef HAVE_SERIAL_COMM +SR_PRIV int sr_brymen_bm86x_packet_request(struct sr_serial_dev_inst *serial); +#endif +SR_PRIV gboolean sr_brymen_bm86x_packet_valid(const uint8_t *buf); +SR_PRIV int sr_brymen_bm86x_parse(const uint8_t *buf, float *floatval, + struct sr_datafeed_analog *analog, void *info); + /*--- dmm/ut71x.c -----------------------------------------------------------*/ #define UT71X_PACKET_SIZE 11