From: Gerhard Sittig Date: Thu, 24 Sep 2020 19:45:51 +0000 (+0200) Subject: dmm/bm85x: introduce DMM packet parser for Brymen BM850(a/s) X-Git-Url: https://sigrok.org/gitweb/?p=libsigrok.git;a=commitdiff_plain;h=27186edacfe2cf168b0794c80672c422587cf8e8 dmm/bm85x: introduce DMM packet parser for Brymen BM850(a/s) Implement a DMM packet parser for the BM850s protocol. This involves variable length responses, which recently became a common serial-dmm feature. Register the new parser under the "brymen-bm85x" device name. This obsoletes the brymen-dmm driver which announces as "brymen-bm857". This implementation was tested with the BM859s meter. The text to number conversion with precision detection resolves the last remaining issue of bug #1611. --- diff --git a/Makefile.am b/Makefile.am index bdd3c61b..9110cfe0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -166,6 +166,7 @@ libsigrok_la_SOURCES += \ src/dmm/asycii.c \ src/dmm/bm25x.c \ src/dmm/bm52x.c \ + src/dmm/bm85x.c \ src/dmm/bm86x.c \ src/dmm/dtm0660.c \ src/dmm/eev121gw.c \ diff --git a/src/dmm/bm85x.c b/src/dmm/bm85x.c new file mode 100644 index 00000000..f6528885 --- /dev/null +++ b/src/dmm/bm85x.c @@ -0,0 +1,457 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2012 Alexandru Gagniuc + * Copyright (C) 2014 Aurelien Jacobs + * Copyright (C) 2019-2020 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 + * + * Protocol parser for Brymen BM850s DMM packets. 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/PD02BM850s_protocolDL.html + * http://brymen.com/product-html/images/DownloadList/ProtocolList/BM850-BM850a-BM850s_List/BM850-BM850a-BM850s-500000-count-DMM-protocol-BC85X-BC85Xa.zip + * + * Implementor's notes on the protocol: + * - The BM85x devices require a low RTS pulse after COM port open and + * before communication of requests and responses. The vendor doc + * recommends 100ms pulse width including delays around it. Without + * that RTS pulse the meter won't respond to requests. + * - The request has a three byte header (DLE, STX, command code), two + * bytes command arguments, and three bytes tail (checksum, DLE, ETX). + * The checksum spans the area (including) the command code and args. + * The checksum value is the XOR across all payload bytes. Exclusively + * command 0x00 is used (initiate next measurement response) which does + * not need arguments (passes all-zero values). + * - The response has a four byte header (DLE, STX, command code, payload + * size), the respective number of payload data bytes, and a three byte + * tail (checksum, DLE, ETX). The checksum spans the range after the + * length field and before the checksum field. Command 0 response data + * payload consists of a four byte flags field and a text field for + * measurement values (floating point with exponent in ASCII). + * - Special cases of response data: + * - The text field which carries the measurement value also contains + * whitespace which may break simple text to number conversion. Like + * 10 02 00 0f 07 00 00 00 20 30 2e 30 30 33 32 20 45 2b 30 46 10 03 + * which translates to: 07 00 00 00 " 0.0032 E+0". Text for overload + * conditions can be shorter which results in variable packet length. + * Some meter functions provide unexpected text for their values. + * - The reference impedance for decibel measurements looks wrong and + * requires special treatment to isolate the 4..1200R value: + * bfunc 80 20 00 00, text " 0. 800 E+1" (reference, 800R) + * The decibel measurement values use an unexpected scale. + * bfunc 00 20 00 00, text "-0.3702 E-1" (measurement, -37.02dBm) + * The reference value gets sent (sometimes) in a DMM response when + * the meter's function is entered, or the reference value changes. + * The 'bfunc' flags combination allows telling packet types apart. + * - Temperature measurements put the C/F unit between the mantissa + * and the exponent, which needs to get removed: " 0.0217CE+3" + * - Diode measurements appear to exclusively provide the 'Volt' flag + * but no 'Diode' flag. The display shows ".diod" for a moment but + * this information is no longer available when voltage measurements + * are seen. + */ + +#include +#include +#include +#include +#include "libsigrok-internal.h" +#include + +#define LOG_PREFIX "brymen-bm85x" + +#define STX 0x02 +#define ETX 0x03 +#define DLE 0x10 + +#define CMD_GET_READING 0 + +#define PKT_HEAD_LEN 4 +#define PKT_DATA_MAX 15 +#define PKT_TAIL_LEN 3 +#define PKT_BFUNC_LEN 4 + +static uint8_t bm85x_crc(const uint8_t *buf, size_t len) +{ + uint8_t crc; + + crc = 0; + while (len--) + crc ^= *buf++; + + return crc; +} + +#ifdef HAVE_SERIAL_COMM +/** Meter's specific activity after port open and before data exchange. */ +SR_PRIV int brymen_bm85x_after_open(struct sr_serial_dev_inst *serial) +{ + int rts_toggle_delay_us; + + /* + * The device requires an RTS *pulse* before communication. + * The vendor's documentation recommends the following sequence: + * Open the COM port, wait for 100ms, set RTS=1, wait for 100ms, + * set RTS=0, wait for 100ms, set RTS=1, configure bitrate and + * frame format, transmit request data, receive response data. + */ + rts_toggle_delay_us = 100 * 1000; /* 100ms */ + g_usleep(rts_toggle_delay_us); + serial_set_handshake(serial, 1, -1); + g_usleep(rts_toggle_delay_us); + serial_set_handshake(serial, 0, -1); + g_usleep(rts_toggle_delay_us); + serial_set_handshake(serial, 1, -1); + g_usleep(rts_toggle_delay_us); + + return SR_OK; +} + +static int bm85x_send_command(struct sr_serial_dev_inst *serial, + uint8_t cmd, uint8_t arg1, uint8_t arg2) +{ + uint8_t buf[8]; + uint8_t crc, *wrptr, *crcptr; + size_t wrlen; + int ret; + + wrptr = &buf[0]; + write_u8_inc(&wrptr, DLE); + write_u8_inc(&wrptr, STX); + crcptr = wrptr; + write_u8_inc(&wrptr, cmd); + write_u8_inc(&wrptr, arg1); + write_u8_inc(&wrptr, arg2); + crc = bm85x_crc(crcptr, wrptr - crcptr); + write_u8_inc(&wrptr, crc); + write_u8_inc(&wrptr, DLE); + write_u8_inc(&wrptr, ETX); + + wrlen = wrptr - &buf[0]; + ret = serial_write_nonblocking(serial, &buf[0], wrlen); + if (ret < 0) + return ret; + if ((size_t)ret != wrlen) + return SR_ERR_IO; + + return SR_OK; +} + +/** Initiate reception of another meter's reading. */ +SR_PRIV int brymen_bm85x_packet_request(struct sr_serial_dev_inst *serial) +{ + return bm85x_send_command(serial, CMD_GET_READING, 0, 0); +} +#endif + +/** + * Check Brymen BM85x DMM packet for validity. + * + * @param[in] st The DMM driver's internal state. + * @param[in] buf The data bytes received so far. + * @param[in] len The received data's length (byte count). + * @param[out] pkt_len The packet's calculated total size (when valid). + * + * The BM850s protocol uses packets of variable length. A minimum amount + * of RX data provides the packet header, which communicates the payload + * size, which allows to determine the packet's total size. Callers of + * this validity checker can learn how much data will get consumed when + * a valid packet got received and processed. The packet size is not + * known in advance. + * + * @returns SR_OK when the packet is valid. + * @returns SR_ERR* (below zero) when the packet is invalid. + * @returns Greater 0 when packet is incomplete, more data is needed. + */ +SR_PRIV int brymen_bm85x_packet_valid(void *st, + const uint8_t *buf, size_t len, size_t *pkt_len) +{ + size_t plen; + uint8_t cmd, crc; + + (void)st; + + /* Four header bytes: DLE, STX, command, payload length. */ + if (len < PKT_HEAD_LEN) + return SR_PACKET_NEED_RX; + if (read_u8_inc(&buf) != DLE) + return SR_PACKET_INVALID; + if (read_u8_inc(&buf) != STX) + return SR_PACKET_INVALID; + cmd = read_u8_inc(&buf); + /* Non-fatal, happens with OL pending during connect. */ + if (cmd == 0x01) + cmd = 0x00; + if (cmd != CMD_GET_READING) + return SR_PACKET_INVALID; + plen = read_u8_inc(&buf); + if (plen > PKT_DATA_MAX) + return SR_PACKET_INVALID; + len -= PKT_HEAD_LEN; + + /* Checksum spans bfunc and value text. Length according to header. */ + if (len < plen + PKT_TAIL_LEN) + return SR_PACKET_NEED_RX; + crc = bm85x_crc(buf, plen); + buf += plen; + len -= plen; + + /* Three tail bytes: checksum, DLE, ETX. */ + if (len < PKT_TAIL_LEN) + return SR_PACKET_NEED_RX; + if (read_u8_inc(&buf) != crc) + return SR_PACKET_INVALID; + if (read_u8_inc(&buf) != DLE) + return SR_PACKET_INVALID; + if (read_u8_inc(&buf) != ETX) + return SR_PACKET_INVALID; + + /* + * Only return the total packet length when the receive buffer + * was found to be valid. For invalid packets it's preferred to + * have the caller keep trying to sync to the packet stream. + */ + if (pkt_len) + *pkt_len = PKT_HEAD_LEN + plen + PKT_TAIL_LEN; + return SR_PACKET_VALID; +} + +struct bm85x_flags { + gboolean is_batt, is_db, is_perc, is_hz, is_amp, is_beep; + gboolean is_ohm, is_temp_f, is_temp_c, is_diode, is_cap; + gboolean is_volt, is_dc, is_ac; +}; + +static int bm85x_parse_flags(const uint8_t *bfunc, struct bm85x_flags *flags) +{ + if (!bfunc || !flags) + return SR_ERR_ARG; + memset(flags, 0, sizeof(*flags)); + + flags->is_batt = bfunc[3] & (1u << 7); + if ((bfunc[3] & 0x7f) != 0) + return SR_ERR_ARG; + + if ((bfunc[2] & 0xff) != 0) + return SR_ERR_ARG; + + if ((bfunc[1] & 0xc0) != 0) + return SR_ERR_ARG; + flags->is_db = bfunc[1] & (1u << 5); + if ((bfunc[1] & 0x10) != 0) + return SR_ERR_ARG; + flags->is_perc = bfunc[1] & (1u << 3); + flags->is_hz = bfunc[1] & (1u << 2); + flags->is_amp = bfunc[1] & (1u << 1); + flags->is_beep = bfunc[1] & (1u << 0); + + flags->is_ohm = bfunc[0] & (1u << 7); + flags->is_temp_f = bfunc[0] & (1u << 6); + flags->is_temp_c = bfunc[0] & (1u << 5); + flags->is_diode = bfunc[0] & (1u << 4); + flags->is_cap = bfunc[0] & (1u << 3); + flags->is_volt = bfunc[0] & (1u << 2); + flags->is_dc = bfunc[0] & (1u << 1); + flags->is_ac = bfunc[0] & (1u << 0); + + return SR_OK; +} + +static int bm85x_parse_value(char *txt, double *val, int *digits) +{ + char *src, *dst, c; + int ret; + + /* + * See above comment on whitespace in response's number text. + * The caller provides a NUL terminated writable text copy. + * Go for low hanging fruit first (OL condition). Eliminate + * whitespace then and do the number conversion. + */ + if (strstr(txt, "+OL")) { + *val = +INFINITY; + return SR_OK; + } + if (strstr(txt, "-OL")) { + *val = -INFINITY; + return SR_OK; + } + if (strstr(txt, "OL")) { + *val = INFINITY; + return SR_OK; + } + + src = txt; + dst = txt; + while (*src) { + c = *src++; + if (c == ' ') + continue; + *dst++ = c; + } + *dst = '\0'; + + ret = sr_atod_ascii_digits(txt, val, digits); + if (ret != SR_OK) + return ret; + + return SR_OK; +} + +static int bm85x_parse_payload(const uint8_t *p, size_t l, + double *val, struct sr_datafeed_analog *analog) +{ + const uint8_t *bfunc; + char text_buf[PKT_DATA_MAX], *text; + size_t text_len; + int ret; + struct bm85x_flags flags; + int digits; + char *parse; + + /* Get a bfunc bits reference, and a writable value text. */ + bfunc = &p[0]; + text_len = l - PKT_BFUNC_LEN; + memcpy(text_buf, &p[PKT_BFUNC_LEN], text_len); + text_buf[text_len] = '\0'; + text = &text_buf[0]; + sr_dbg("DMM bfunc %02x %02x %02x %02x, text \"%s\"", + bfunc[0], bfunc[1], bfunc[2], bfunc[3], text); + + /* Check 'bfunc' bitfield first, text interpretation depends on it. */ + ret = bm85x_parse_flags(bfunc, &flags); + if (ret != SR_OK) + return ret; + + /* Parse the text after potential normalization/transformation. */ + if (flags.is_db && flags.is_ohm) { + static const char *prefix = " 0."; + static const char *suffix = " E"; + /* See above comment on dBm reference value text. */ + if (strncmp(text, prefix, strlen(prefix)) != 0) + return SR_ERR_DATA; + text += strlen(prefix); + text_len -= strlen(prefix); + parse = strstr(text, suffix); + if (!parse) + return SR_ERR_DATA; + *parse = '\0'; + } + if (flags.is_temp_f || flags.is_temp_c) { + /* See above comment on temperature value text. */ + parse = strchr(text, flags.is_temp_f ? 'F' : 'C'); + if (!parse) + return SR_ERR_DATA; + *parse = ' '; + } + digits = 0; + ret = bm85x_parse_value(text, val, &digits); + if (ret != SR_OK) + return ret; + + /* Fill in MQ and flags result details. */ + analog->meaning->mqflags = 0; + if (flags.is_volt) { + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + } + if (flags.is_amp) { + analog->meaning->mq = SR_MQ_CURRENT; + analog->meaning->unit = SR_UNIT_AMPERE; + } + if (flags.is_ohm) { + if (flags.is_db) + analog->meaning->mq = SR_MQ_RESISTANCE; + else if (flags.is_beep) + analog->meaning->mq = SR_MQ_CONTINUITY; + else + analog->meaning->mq = SR_MQ_RESISTANCE; + analog->meaning->unit = SR_UNIT_OHM; + } + if (flags.is_hz) { + analog->meaning->mq = SR_MQ_FREQUENCY; + analog->meaning->unit = SR_UNIT_HERTZ; + } + if (flags.is_perc) { + analog->meaning->mq = SR_MQ_DUTY_CYCLE; + analog->meaning->unit = SR_UNIT_PERCENTAGE; + } + if (flags.is_cap) { + analog->meaning->mq = SR_MQ_CAPACITANCE; + analog->meaning->unit = SR_UNIT_FARAD; + } + if (flags.is_temp_f) { + analog->meaning->mq = SR_MQ_TEMPERATURE; + analog->meaning->unit = SR_UNIT_FAHRENHEIT; + } + if (flags.is_temp_c) { + analog->meaning->mq = SR_MQ_TEMPERATURE; + analog->meaning->unit = SR_UNIT_CELSIUS; + } + if (flags.is_db && !flags.is_ohm) { + /* See above comment on dBm measurements scale. */ + analog->meaning->mq = SR_MQ_POWER; + analog->meaning->unit = SR_UNIT_DECIBEL_MW; + *val *= 1000; + digits -= 3; + } + + if (flags.is_diode) { + /* See above comment on diode measurement responses. */ + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + analog->meaning->mqflags |= SR_MQFLAG_DIODE; + analog->meaning->mqflags |= SR_MQFLAG_DC; + } + if (flags.is_ac) + analog->meaning->mqflags |= SR_MQFLAG_AC; + if (flags.is_dc) + analog->meaning->mqflags |= SR_MQFLAG_DC; + + analog->encoding->digits = digits; + analog->spec->spec_digits = digits; + + if (flags.is_batt) + sr_warn("Low battery!"); + + return SR_OK; +} + +SR_PRIV int brymen_bm85x_parse(void *st, const uint8_t *buf, size_t len, + double *val, struct sr_datafeed_analog *analog, void *info) +{ + const uint8_t *pl_ptr; + size_t pl_len; + + (void)st; + (void)info; + + if (!buf || !len) + return SR_ERR_DATA; + if (!val || !analog) + return SR_ERR_DATA; + + if (brymen_bm85x_packet_valid(NULL, buf, len, NULL) != SR_PACKET_VALID) + return SR_ERR_DATA; + pl_ptr = &buf[PKT_HEAD_LEN]; + pl_len = len - PKT_HEAD_LEN - PKT_TAIL_LEN; + + return bm85x_parse_payload(pl_ptr, pl_len, val, analog); +} diff --git a/src/hardware/serial-dmm/api.c b/src/hardware/serial-dmm/api.c index 660f2080..684047c6 100644 --- a/src/hardware/serial-dmm/api.c +++ b/src/hardware/serial-dmm/api.c @@ -372,6 +372,17 @@ SR_REGISTER_DEV_DRIVER_LIST(serial_dmm_drivers, NULL ), /* }}} */ + /* bm85x based meters {{{ */ + DMM_LEN( + "brymen-bm85x", brymen_bm85x, "Brymen", "BM85x", + NULL, "9600/8n1/dtr=1/rts=1", + BRYMEN_BM85x_PACKET_SIZE_MIN, 2000, 400, + NULL, NULL, /* INIT/FREE for DMM state */ + brymen_bm85x_after_open, brymen_bm85x_packet_request, + brymen_bm85x_packet_valid, brymen_bm85x_parse, + NULL + ), + /* }}} */ /* bm86x based meters {{{ */ DMM_CONN( "brymen-bm86x", brymen_bm86x, "Brymen", "BM86x", diff --git a/src/libsigrok-internal.h b/src/libsigrok-internal.h index 84e5faa5..beeb716f 100644 --- a/src/libsigrok-internal.h +++ b/src/libsigrok-internal.h @@ -2349,6 +2349,21 @@ SR_PRIV gboolean sr_brymen_bm52x_packet_valid(const uint8_t *buf); SR_PRIV int sr_brymen_bm52x_parse(const uint8_t *buf, float *floatval, struct sr_datafeed_analog *analog, void *info); +/*--- dmm/bm85x.c -----------------------------------------------------------*/ + +#define BRYMEN_BM85x_PACKET_SIZE_MIN 4 + +struct brymen_bm85x_info { int dummy; }; + +#ifdef HAVE_SERIAL_COMM +SR_PRIV int brymen_bm85x_after_open(struct sr_serial_dev_inst *serial); +SR_PRIV int brymen_bm85x_packet_request(struct sr_serial_dev_inst *serial); +#endif +SR_PRIV gboolean brymen_bm85x_packet_valid(void *state, + const uint8_t *buf, size_t len, size_t *pkt_len); +SR_PRIV int brymen_bm85x_parse(void *state, const uint8_t *buf, size_t len, + double *floatval, struct sr_datafeed_analog *analog, void *info); + /*--- dmm/bm86x.c -----------------------------------------------------------*/ #define BRYMEN_BM86X_PACKET_SIZE 24