--- /dev/null
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2012-2013 Uwe Hermann <uwe@hermann-uwe.de>
+ * Copyright (C) 2016 Gerhard Sittig <gerhard.sittig@gmx.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * 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 <config.h>
+#include <ctype.h>
+#include <glib.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <libsigrok/libsigrok.h>
+#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;
+}