From: Gerhard Sittig Date: Thu, 24 Sep 2020 21:01:26 +0000 (+0200) Subject: dmm/bm52x: add support for recorded measurements (memory data sets) X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=0931639a12e388aa5206c64d4d937f070d7453b2;p=libsigrok.git dmm/bm52x: add support for recorded measurements (memory data sets) Extend the BM52x packet parser, add config get/set/list code to handle the data source property. Either let the common serial-dmm code run the acquisition of live readings, or locally retrieve the selected "session page" and forward its measurements to the session bus. These separate code paths are required because the protocol differs a lot between these modes, a totally different set of requests and responses is involved, response interpretation logic is impossible to share between live and recorded measurements. --- diff --git a/src/dmm/bm52x.c b/src/dmm/bm52x.c index b51d91c7..728b70a6 100644 --- a/src/dmm/bm52x.c +++ b/src/dmm/bm52x.c @@ -26,33 +26,18 @@ * 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. @@ -73,6 +58,14 @@ * 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 @@ -80,20 +73,86 @@ #include "libsigrok-internal.h" #include #include +#include #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" : + ""; + 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; +} diff --git a/src/hardware/serial-dmm/api.c b/src/hardware/serial-dmm/api.c index 684047c6..1b9a810b 100644 --- a/src/hardware/serial-dmm/api.c +++ b/src/hardware/serial-dmm/api.c @@ -127,6 +127,14 @@ static GSList *scan(struct sr_dev_driver *di, GSList *options) * happen to provide them. (This is a compromise to do it here, * and not extend the DMM_CONN() et al set of macros.) */ + if (dmm->packet_parse == sr_brymen_bm52x_parse) { + dmm->dmm_state_init = brymen_bm52x_state_init; + dmm->dmm_state_free = brymen_bm52x_state_free; + dmm->config_get = brymen_bm52x_config_get; + dmm->config_set = brymen_bm52x_config_set; + dmm->config_list = brymen_bm52x_config_list; + dmm->acquire_start = brymen_bm52x_acquire_start; + } if (dmm->dmm_state_init) dmm->dmm_state = dmm->dmm_state_init(); diff --git a/src/libsigrok-internal.h b/src/libsigrok-internal.h index beeb716f..6a85d694 100644 --- a/src/libsigrok-internal.h +++ b/src/libsigrok-internal.h @@ -2349,6 +2349,20 @@ 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); +struct brymen_bm52x_state; + +SR_PRIV void *brymen_bm52x_state_init(void); +SR_PRIV void brymen_bm52x_state_free(void *state); +SR_PRIV int brymen_bm52x_config_get(void *state, uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, const struct sr_channel_group *cg); +SR_PRIV int brymen_bm52x_config_set(void *state, uint32_t key, GVariant *data, + const struct sr_dev_inst *sdi, const struct sr_channel_group *cg); +SR_PRIV int brymen_bm52x_config_list(void *state, uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, const struct sr_channel_group *cg); +SR_PRIV int brymen_bm52x_acquire_start(void *state, + const struct sr_dev_inst *sdi, + sr_receive_data_callback *cb, void **cb_data); + /*--- dmm/bm85x.c -----------------------------------------------------------*/ #define BRYMEN_BM85x_PACKET_SIZE_MIN 4