X-Git-Url: https://sigrok.org/gitweb/?p=libsigrok.git;a=blobdiff_plain;f=src%2Fdmm%2Fbm52x.c;h=728b70a690308e6cabb662cbf35dbddf4df67a06;hp=b51d91c768abad0a8529ff21c044492d7953d2fc;hb=0931639a12e388aa5206c64d4d937f070d7453b2;hpb=9a1a7dc2832b0c51baff94ef551518d91f47ff31 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; +}