diff --git a/Makefile.am b/Makefile.am index 7d7d1f9e..d18a674d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -148,6 +148,7 @@ libsigrok_la_SOURCES += \ src/dmm/fs9922.c \ src/dmm/m2110.c \ src/dmm/metex14.c \ + src/dmm/asycii.c \ src/dmm/rs9lcd.c \ src/dmm/bm25x.c \ src/dmm/ut71x.c \ diff --git a/src/dmm/asycii.c b/src/dmm/asycii.c new file mode 100644 index 00000000..8f5770f5 --- /dev/null +++ b/src/dmm/asycii.c @@ -0,0 +1,541 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2012-2013 Uwe Hermann + * Copyright (C) 2016 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 2 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 . + */ + +/* + * Parser for the ASYC-II 16-bytes ASCII protocol (PRINT). + * + * This should work for various multimeters which use this kind of protocol, + * even though there is some variation in which modes each DMM supports. + * + * This implementation was developed for and tested with a Metrix MX56C, + * which is identical to the BK Precision 5390. + * See the metex14.c implementation for the 14-byte protocol used by many + * other models. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "libsigrok-internal.h" + +#define LOG_PREFIX "asycii" + +/** + * Parse sign and value from text buffer, byte 0-6. + * + * The first character always is the sign (' ' or '-'). Subsequent + * positions contain digits, dots, or spaces. Overflow / open inputs + * are signalled with several magic literals that cannot get interpreted + * as a number, either with 'X' characters in them, or with several + * forms of "OL". + * + * @param[in] buf The text buffer received from the DMM. + * @param[out] result A floating point number value. + * @param[out] exponent Augments the number value. + */ +static int parse_value(const char *buf, struct asycii_info *info, + float *result, int *exponent) +{ + char valstr[7 + 1]; + const char *valp; + int i, cnt, is_ol, dot_pos; + char *endp; + + /* + * Strip all spaces from bytes 0-6. By copying all + * non-space characters into a buffer. + */ + cnt = 0; + for (i = 0; i < 7; i++) { + if (buf[i] != ' ') + valstr[cnt++] = buf[i]; + } + valstr[cnt] = '\0'; + valp = &valstr[0]; + sr_spew("%s(), number buffer [%s]", __func__, valp); + + /* + * Check for "over limit" conditions. Depending on the meter's + * selected mode, the textual representation might differ. Test + * all known variations. + */ + is_ol = 0; + is_ol += (g_ascii_strcasecmp(valp, ".OL") == 0) ? 1 : 0; + is_ol += (g_ascii_strcasecmp(valp, "O.L") == 0) ? 1 : 0; + is_ol += (g_ascii_strcasecmp(valp, "-.OL") == 0) ? 1 : 0; + is_ol += (g_ascii_strcasecmp(valp, "-O.L") == 0) ? 1 : 0; + is_ol += (g_ascii_strncasecmp(valp, "X", 1) == 0) ? 1 : 0; + is_ol += (g_ascii_strncasecmp(valp, "-X", 2) == 0) ? 1 : 0; + if (is_ol) { + sr_spew("%s(), over limit", __func__); + *result = INFINITY; + return SR_OK; + } + + /* + * Convert the textual number representation to a float, and + * an exponent. Apply sanity checks (optional sign, digits and + * dot expected here, exclusively). + */ + endp = NULL; + *result = strtof(valp, &endp); + if (endp == NULL || *endp != '\0') { + info->is_invalid = TRUE; + sr_spew("%s(), cannot convert number", __func__); + return SR_ERR_DATA; + } + dot_pos = strcspn(valstr, "."); + if (dot_pos < cnt) + *exponent = -(cnt - dot_pos - 1); + else + *exponent = 0; + sr_spew("%s(), display value is %f", __func__, *result); + return SR_OK; +} + +/** + * Parse unit and flags from text buffer, bytes 7-14. + * + * The unit and flags optionally follow the number value for the + * measurement. Either can be present or absent. The scale factor + * is always at index 7. The unit starts at index 8, and is of + * variable length. Flags immediately follow the unit. The remainder + * of the text buffer is SPACE padded, and terminated with CR. + * + * Notice the implementation detail of case *sensitive* comparison. + * This would break correct operation. It's essential that e.g. "Vac" + * gets split into the "V" unit and the "ac" flag, not into "VA" and + * the unknown "c" flag! In the absence of separators or fixed + * positions and with ambiguous text (when abbreviated), order of + * comparison matters, too. + * + * @param[in] buf The text buffer received from the DMM. + * @param[out] info Broken down measurement details. + */ +static void parse_flags(const char *buf, struct asycii_info *info) +{ + int i, cnt; + char unit[8 + 1]; + const char *u; + + /* Bytes 0-6: Number value, see parse_value(). */ + + /* Strip spaces from bytes 7-14. */ + cnt = 0; + for (i = 7; i < 15; i++) { + if (buf[i] != ' ') + unit[cnt++] = buf[i]; + } + unit[cnt] = '\0'; + u = &unit[0]; + sr_spew("%s(): unit/flag buffer [%s]", __func__, u); + + /* Scan for the scale factor. */ + sr_spew("%s(): scanning factor, buffer [%s]", __func__, u); + if (*u == 'p') { + u++; + info->is_pico = TRUE; + } else if (*u == 'n') { + u++; + info->is_nano = TRUE; + } else if (*u == 'u') { + u++; + info->is_micro = TRUE; + } else if (*u == 'm') { + u++; + info->is_milli = TRUE; + } else if (*u == ' ') { + u++; + } else if (*u == 'k') { + u++; + info->is_kilo = TRUE; + } else if (*u == 'M') { + u++; + info->is_mega = TRUE; + } else { + /* Absence of a scale modifier is perfectly fine. */ + } + + /* Scan for the measurement unit. */ + sr_spew("%s(): scanning unit, buffer [%s]", __func__, u); + if (strncmp(u, "A", strlen("A")) == 0) { + u += strlen("A"); + info->is_ampere = TRUE; + } else if (strncmp(u, "VA", strlen("VA")) == 0) { + u += strlen("VA"); + info->is_volt_ampere = TRUE; + } else if (strncmp(u, "V", strlen("V")) == 0) { + u += strlen("V"); + info->is_volt = TRUE; + } else if (strncmp(u, "ohm", strlen("ohm")) == 0) { + u += strlen("ohm"); + info->is_resistance = TRUE; + info->is_ohm = TRUE; + } else if (strncmp(u, "F", strlen("F")) == 0) { + u += strlen("F"); + info->is_capacitance = TRUE; + info->is_farad = TRUE; + } else if (strncmp(u, "dB", strlen("dB")) == 0) { + u += strlen("dB"); + info->is_gain = TRUE; + info->is_decibel = TRUE; + } else if (strncmp(u, "Hz", strlen("Hz")) == 0) { + u += strlen("Hz"); + info->is_frequency = TRUE; + info->is_hertz = TRUE; + } else if (strncmp(u, "%", strlen("%")) == 0) { + u += strlen("%"); + info->is_duty_cycle = TRUE; + if (*u == '+') { + u++; + info->is_duty_pos = TRUE; + } else if (*u == '-') { + u++; + info->is_duty_neg = TRUE; + } else { + info->is_invalid = TRUE; + } + } else if (strncmp(u, "Cnt", strlen("Cnt")) == 0) { + u += strlen("Cnt"); + info->is_pulse_count = TRUE; + info->is_unitless = TRUE; + if (*u == '+') { + u++; + info->is_count_pos = TRUE; + } else if (*u == '-') { + u++; + info->is_count_neg = TRUE; + } else { + info->is_invalid = TRUE; + } + } else if (strncmp(u, "s", strlen("s")) == 0) { + u += strlen("s"); + info->is_pulse_width = TRUE; + info->is_seconds = TRUE; + if (*u == '+') { + u++; + info->is_period_pos = TRUE; + } else if (*u == '-') { + u++; + info->is_period_neg = TRUE; + } else { + info->is_invalid = TRUE; + } + } else { + /* Not strictly illegal, but unknown/unsupported. */ + sr_spew("%s(): measurement: unsupported", __func__); + info->is_invalid = TRUE; + } + + /* Scan for additional flags. */ + sr_spew("%s(): scanning flags, buffer [%s]", __func__, u); + if (strncmp(u, "ac+dc", strlen("ac+dc")) == 0) { + u += strlen("ac+dc"); + info->is_ac_and_dc = TRUE; + } else if (strncmp(u, "ac", strlen("ac")) == 0) { + u += strlen("ac"); + info->is_ac = TRUE; + } else if (strncmp(u, "dc", strlen("dc")) == 0) { + u += strlen("dc"); + info->is_dc = TRUE; + } else if (strncmp(u, "d", strlen("d")) == 0) { + u += strlen("d"); + info->is_diode = TRUE; + } else if (strncmp(u, "Pk", strlen("Pk")) == 0) { + u += strlen("Pk"); + if (*u == '+') { + u++; + info->is_peak_max = TRUE; + } else if (*u == '-') { + u++; + info->is_peak_min = TRUE; + } else { + info->is_invalid = TRUE; + } + } else if (strcmp(u, "") == 0) { + /* Absence of any flags is acceptable. */ + } else { + /* Presence of unknown flags is not. */ + sr_dbg("%s(): flag: unknown", __func__); + info->is_invalid = TRUE; + } + + /* Was all of the received data consumed? */ + if (*u != '\0') + info->is_invalid = TRUE; + + /* + * Note: + * - The protocol does not distinguish between "resistance" + * and "continuity". + * - Relative measurement and hold cannot get recognized. + */ +} + +/** + * Fill in a datafeed from previously parsed measurement details. + * + * @param[out] analog The datafeed which gets filled in. + * @param[in] floatval The number value of the measurement. + * @param[in] exponent Augments the number value. + * @param[in] info Scale and unit and other attributes. + */ +static void handle_flags(struct sr_datafeed_analog *analog, float *floatval, + int *exponent, const struct asycii_info *info) +{ + int factor = 0; + + /* Factors */ + if (info->is_pico) + factor -= 12; + if (info->is_nano) + factor -= 9; + if (info->is_micro) + factor -= 6; + if (info->is_milli) + factor -= 3; + if (info->is_kilo) + factor += 3; + if (info->is_mega) + factor += 6; + *floatval *= powf(10, factor); + *exponent += factor; + + /* Measurement modes */ + if (info->is_volt) { + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + } + if (info->is_volt_ampere) { + analog->meaning->mq = SR_MQ_POWER; + analog->meaning->unit = SR_UNIT_VOLT_AMPERE; + } + if (info->is_ampere) { + analog->meaning->mq = SR_MQ_CURRENT; + analog->meaning->unit = SR_UNIT_AMPERE; + } + if (info->is_frequency) { + analog->meaning->mq = SR_MQ_FREQUENCY; + analog->meaning->unit = SR_UNIT_HERTZ; + } + if (info->is_duty_cycle) { + analog->meaning->mq = SR_MQ_DUTY_CYCLE; + analog->meaning->unit = SR_UNIT_PERCENTAGE; + } + if (info->is_pulse_width) { + analog->meaning->mq = SR_MQ_PULSE_WIDTH; + analog->meaning->unit = SR_UNIT_SECOND; + } + if (info->is_pulse_count) { + analog->meaning->mq = SR_MQ_COUNT; + analog->meaning->unit = SR_UNIT_UNITLESS; + } + if (info->is_resistance) { + analog->meaning->mq = SR_MQ_RESISTANCE; + analog->meaning->unit = SR_UNIT_OHM; + } + if (info->is_capacitance) { + analog->meaning->mq = SR_MQ_CAPACITANCE; + analog->meaning->unit = SR_UNIT_FARAD; + } + if (info->is_diode) { + analog->meaning->mq = SR_MQ_VOLTAGE; + analog->meaning->unit = SR_UNIT_VOLT; + } + if (info->is_gain) { + analog->meaning->mq = SR_MQ_GAIN; + analog->meaning->unit = SR_UNIT_DECIBEL_VOLT; + } + + /* Measurement related flags */ + if (info->is_ac) + analog->meaning->mqflags |= SR_MQFLAG_AC; + if (info->is_ac_and_dc) + analog->meaning->mqflags |= SR_MQFLAG_AC | SR_MQFLAG_DC; + if (info->is_dc) + analog->meaning->mqflags |= SR_MQFLAG_DC; + if (info->is_diode) + analog->meaning->mqflags |= SR_MQFLAG_DIODE; + if (info->is_peak_max) + analog->meaning->mqflags |= SR_MQFLAG_MAX; + if (info->is_peak_min) + analog->meaning->mqflags |= SR_MQFLAG_MIN; +} + +/** + * Check measurement details for consistency and validity. + * + * @param[in] info The previously parsed details. + * + * @return TRUE on success, FALSE otherwise. + */ +static gboolean flags_valid(const struct asycii_info *info) +{ + int count; + + /* Have previous checks raised the "invalid" flag? */ + if (info->is_invalid) { + sr_dbg("Previous parse raised \"invalid\" flag for packet."); + return FALSE; + } + + /* Does the packet have more than one multiplier? */ + count = 0; + count += (info->is_pico) ? 1 : 0; + count += (info->is_nano) ? 1 : 0; + count += (info->is_micro) ? 1 : 0; + count += (info->is_milli) ? 1 : 0; + count += (info->is_kilo) ? 1 : 0; + count += (info->is_mega) ? 1 : 0; + if (count > 1) { + sr_dbg("More than one multiplier detected in packet."); + return FALSE; + } + + /* Does the packet "measure" more than one type of value? */ + count = 0; + count += (info->is_volt || info->is_diode) ? 1 : 0; + count += (info->is_volt_ampere) ? 1 : 0; + count += (info->is_ampere) ? 1 : 0; + count += (info->is_gain) ? 1 : 0; + count += (info->is_resistance) ? 1 : 0; + count += (info->is_capacitance) ? 1 : 0; + count += (info->is_frequency) ? 1 : 0; + count += (info->is_duty_cycle) ? 1 : 0; + count += (info->is_pulse_width) ? 1 : 0; + count += (info->is_pulse_count) ? 1 : 0; + if (count > 1) { + sr_dbg("More than one measurement type detected in packet."); + return FALSE; + } + + /* Are conflicting AC and DC flags set? */ + count = 0; + count += (info->is_ac) ? 1 : 0; + count += (info->is_ac_and_dc) ? 1 : 0; + count += (info->is_dc) ? 1 : 0; + if (count > 1) { + sr_dbg("Conflicting AC and DC flags detected in packet."); + return FALSE; + } + + return TRUE; +} + +#ifdef HAVE_LIBSERIALPORT +/** + * Arrange for the reception of another measurement from the DMM. + * + * This routine is unused in the currently implemented PRINT mode, + * where the meter sends measurements to the PC in pre-set intervals, + * without the PC's intervention. + * + * @param[in] serial The serial connection. + * + * @private + */ +SR_PRIV int sr_asycii_packet_request(struct sr_serial_dev_inst *serial) +{ + /* + * The current implementation assumes that the user pressed + * the PRINT button. It has no support to query/trigger packet + * reception from the meter. + */ + (void)serial; + sr_spew("NOT requesting DMM packet."); + return SR_OK; +} +#endif + +/** + * Check whether a received frame is valid. + * + * @param[in] buf The text buffer with received data. + * + * @return TRUE upon success, FALSE otherwise. + */ +SR_PRIV gboolean sr_asycii_packet_valid(const uint8_t *buf) +{ + struct asycii_info info; + + /* First check whether we are in sync with the packet stream. */ + if (buf[15] != '\r') + return FALSE; + + /* Have the received packet content parsed. */ + memset(&info, 0x00, sizeof(info)); + parse_flags((const char *)buf, &info); + if (!flags_valid(&info)) + return FALSE; + + return TRUE; +} + +/** + * Parse a protocol packet. + * + * @param[in] buf Buffer containing the protocol packet. Must not be NULL. + * @param[out] floatval Pointer to a float variable. That variable will + * be modified in-place depending on the protocol packet. + * Must not be NULL. + * @param[out] analog Pointer to a struct sr_datafeed_analog. The struct + * will be filled with data according to the protocol packet. + * Must not be NULL. + * @param[out] info Pointer to a struct asycii_info. The struct will be + * filled with data according to the protocol packet. Must + * not be NULL. + * + * @return SR_OK upon success, SR_ERR upon failure. Upon errors, the + * 'analog' variable contents are undefined and should not + * be used. + */ +SR_PRIV int sr_asycii_parse(const uint8_t *buf, float *floatval, + struct sr_datafeed_analog *analog, void *info) +{ + int ret, exponent; + struct asycii_info *info_local; + + info_local = (struct asycii_info *)info; + + /* Don't print byte 15. That one contains the carriage return. */ + sr_dbg("DMM packet: \"%.15s\"", buf); + + memset(info_local, 0x00, sizeof(*info_local)); + + exponent = 0; + ret = parse_value((const char *)buf, info_local, floatval, &exponent); + if (ret != SR_OK) { + sr_dbg("Error parsing value: %d.", ret); + return ret; + } + + parse_flags((const char *)buf, info_local); + handle_flags(analog, floatval, &exponent, info_local); + + analog->encoding->digits = -exponent; + analog->spec->spec_digits = -exponent; + + return SR_OK; +} diff --git a/src/libsigrok-internal.h b/src/libsigrok-internal.h index 0856c480..fa4432fb 100644 --- a/src/libsigrok-internal.h +++ b/src/libsigrok-internal.h @@ -1321,6 +1321,31 @@ SR_PRIV gboolean sr_ut372_packet_valid(const uint8_t *buf); SR_PRIV int sr_ut372_parse(const uint8_t *buf, float *floatval, struct sr_datafeed_analog *analog, void *info); +/*--- hardware/dmm/asycii.c -------------------------------------------------*/ + +#define ASYCII_PACKET_SIZE 16 + +struct asycii_info { + gboolean is_ac, is_dc, is_ac_and_dc; + gboolean is_resistance, is_capacitance, is_diode, is_gain; + gboolean is_frequency, is_duty_cycle, is_duty_pos, is_duty_neg; + gboolean is_pulse_width, is_period_pos, is_period_neg; + gboolean is_pulse_count, is_count_pos, is_count_neg; + gboolean is_ampere, is_volt, is_volt_ampere, is_farad, is_ohm; + gboolean is_hertz, is_percent, is_seconds, is_decibel; + gboolean is_pico, is_nano, is_micro, is_milli, is_kilo, is_mega; + gboolean is_unitless; + gboolean is_peak_min, is_peak_max; + gboolean is_invalid; +}; + +#ifdef HAVE_LIBSERIALPORT +SR_PRIV int sr_asycii_packet_request(struct sr_serial_dev_inst *serial); +#endif +SR_PRIV gboolean sr_asycii_packet_valid(const uint8_t *buf); +SR_PRIV int sr_asycii_parse(const uint8_t *buf, float *floatval, + struct sr_datafeed_analog *analog, void *info); + /*--- hardware/scale/kern.c -------------------------------------------------*/ struct kern_info {