From: Derek Hageman Date: Tue, 19 Feb 2019 23:26:27 +0000 (-0700) Subject: Add support for the Mooshimeter DMM X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=ebcd1aba011ba6e1082e96be3cd7e47374a516b4;p=libsigrok.git Add support for the Mooshimeter DMM This adds support for the Mooshim Engineering BLE based Mooshimeter. Because the meter requires raw BLE packets, the driver uses the BLE layer directly. Since the meter has no physical way of configuring it, the actual configuration is set entirely with sigrok device options. --- diff --git a/Makefile.am b/Makefile.am index 66d5f506..cbb7da2f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -463,6 +463,12 @@ src_libdrivers_la_SOURCES += \ src/hardware/mic-985xx/protocol.c \ src/hardware/mic-985xx/api.c endif +if HW_MOOSHIMETER_DMM +src_libdrivers_la_SOURCES += \ + src/hardware/mooshimeter-dmm/protocol.h \ + src/hardware/mooshimeter-dmm/protocol.c \ + src/hardware/mooshimeter-dmm/api.c +endif if HW_MOTECH_LPS_30X src_libdrivers_la_SOURCES += \ src/hardware/motech-lps-30x/protocol.h \ diff --git a/README.devices b/README.devices index e7fe0429..a7e50a39 100644 --- a/README.devices +++ b/README.devices @@ -107,6 +107,7 @@ The following drivers/devices do not need any firmware upload: - maynuo-m97 - mic-985xx (including all subdrivers) - motech-lps-30x + - mooshimeter-dmm - norma-dmm - openbench-logic-sniffer - pce-322a @@ -475,3 +476,74 @@ Example: $ sigrok-cli --driver ols:conn=/dev/ttyACM0 ... + +Mooshimeter +----------- + +The Mooshim Engineering Mooshimeter is controlled via Bluetooth Low Energy +(sometimes called Bluetooth 4.0), as such it requires a supported Bluetooth +interface available. The 'conn' option is required and must contain the +Bluetooth MAC address of the meter. + +Example: + + $ sigrok-cli --driver mooshimeter-dmm:conn=12-34-56-78-9A-BC ... + +Since the Mooshimeter has no physical interface on the meter itself, the +channel configuration is set with the 'channel_config' option. The format +of this option is 'CH1,CH2' where each channel configuration has the form +'MODE:RANGE:ANALYSIS', with later parts being optional. In addition for +CLI compatibility, the ',' in the channels can also be a '/' and the ':' in +the individual configuration can be a ';'. + +Available channel 1 modes: + + - Current, A: Current in amps + - Temperature, T, K: Internal meter temperature in Kelvin + - Resistance, Ohm, W: Resistance in ohms + - Diode, D: Diode voltage + - Aux, LV: Auxiliary (W input) low voltage sensor (1.2V max) + +Available channel 2 modes: + + - Voltage, V: Voltage + - Temperature, T, K: Internal meter temperature in Kelvin + - Resistance, Ohm, W: Resistance in ohms + - Diode, D: Diode voltage + - Aux, LV: Auxiliary (W input) low voltage sensor (1.2V max) + +Only one channel can use the shared inputs at a time (e.g. if CH1 is measuring +resistance, CH2 cannot measure low voltage). Temperature is excepted from +this, so the meter can measure internal temperature and low voltage at the +same time. + +Additionally, the meter can calculate the real power of both channels. This +generally only makes sense when CH1 is set to current and CH2 is set to a +voltage and so it is disabled by default. It must be enabled by enabling the +'P' channel (the third channel). + +The range of the channel specification sets the maximum input for that channel +and is rounded up to the next value the meter itself supports. For example, +specifying 50 for the voltage will result in the actual maximum of 60. +Specifying 61 would result in 600. If omitted, sigrok will perform +auto-ranging of the channel by selecting the next greater value than the +latest maximum. + +The analysis option sets how the meter reports its internal sampling buffer +to sigrok: + + - Mean, DC: The default is a simple arithmetic mean of the sample buffer + - RMS, AC: The root mean square of the sample buffer + - Buf, Buffer, Samples: Report the entire sample buffer to sigrok. This + results in packets that contain all the samples in the buffer instead + of a single output value. + +The size of the sample buffer is set with the 'avg_samples' option, while +the sampling rate is set with the 'samplerate' option. So the update rate +is avg_samples/samplerate. Both are rounded up to the next supported value +by the meter. + +Example: + + $ sigrok-cli -c channel_config="Aux;0.1/T" --driver mooshimeter-dmm... + $ sigrok-cli -c channel_config="A;;AC/V;;AC" --driver mooshimeter-dmm... diff --git a/configure.ac b/configure.ac index 3ab83af9..cd3346c8 100644 --- a/configure.ac +++ b/configure.ac @@ -136,12 +136,16 @@ SR_ARG_OPT_CHECK([libieee1284], [LIBIEEE1284],, [ AS_IF([test "x$sr_have_libieee1284" = xyes], [SR_PREPEND([SR_EXTRA_LIBS], [-lieee1284])]) +SR_ARG_OPT_PKG([libgio], [LIBGIO], , [gio-2.0 >= 2.24.0]) + # See if any of the (potentially platform specific) libs are available # which provide some means of Bluetooth communication. AS_IF([test "x$sr_have_libbluez" = xyes], sr_have_bluetooth=yes, sr_have_bluetooth=no) AS_IF([test "x$sr_have_bluetooth" = xyes], [AC_DEFINE([HAVE_BLUETOOTH], [1], [Specifies whether Bluetooth communication is supported.])]) +AS_IF([test "x$sr_have_bluetooth" = xyes], + [SR_APPEND([sr_deps_avail], [bluetooth_comm])]) AS_IF([test "x$sr_have_libserialport" = xyes -o "x$sr_have_libhidapi" = xyes -o "x$sr_have_bluetooth" = xyes], sr_have_serial_comm=yes, sr_have_serial_comm=no) @@ -290,6 +294,7 @@ SR_DRIVER([Manson HCS-3xxx], [manson-hcs-3xxx], [serial_comm]) SR_DRIVER([maynuo-m97], [maynuo-m97]) SR_DRIVER([MIC 985xx], [mic-985xx], [serial_comm]) SR_DRIVER([Microchip PICkit2], [microchip-pickit2], [libusb]) +SR_DRIVER([Mooshimeter DMM], [mooshimeter-dmm], [bluetooth_comm libgio]) SR_DRIVER([Motech LPS 30x], [motech-lps-30x], [serial_comm]) SR_DRIVER([Norma DMM], [norma-dmm], [serial_comm]) SR_DRIVER([OpenBench Logic Sniffer], [openbench-logic-sniffer], [serial_comm]) diff --git a/src/hardware/mooshimeter-dmm/api.c b/src/hardware/mooshimeter-dmm/api.c new file mode 100644 index 00000000..08199a68 --- /dev/null +++ b/src/hardware/mooshimeter-dmm/api.c @@ -0,0 +1,1039 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2019 Derek Hageman + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include "protocol.h" + +static struct sr_dev_driver mooshimeter_dmm_driver_info; + +static const uint32_t scanopts[] = { + SR_CONF_CONN, +}; + +static const uint32_t drvopts[] = { + SR_CONF_MULTIMETER, +}; + +static const uint32_t devopts[] = { + SR_CONF_CONTINUOUS, + SR_CONF_LIMIT_SAMPLES | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_LIMIT_MSEC | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_AVG_SAMPLES | SR_CONF_GET | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_SAMPLERATE | SR_CONF_GET | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_CHANNEL_CONFIG | SR_CONF_SET, +}; + +static void init_dev(struct sr_dev_inst *sdi) +{ + struct dev_context *devc; + struct sr_channel *chan; + + devc = g_new0(struct dev_context, 1); + sdi->priv = devc; + sdi->status = SR_ST_INITIALIZING; + sdi->vendor = g_strdup("Mooshim Engineering"); + sdi->model = g_strdup("Mooshimeter"); + + sr_sw_limits_init(&devc->limits); + + chan = sr_channel_new(sdi, 0, SR_CHANNEL_ANALOG, TRUE, "CH1"); + devc->channel_meaning[0].mq = SR_MQ_CURRENT; + devc->channel_meaning[0].unit = SR_UNIT_AMPERE; + devc->channel_meaning[0].mqflags = SR_MQFLAG_DC; + devc->channel_meaning[0].channels = g_slist_prepend(NULL, chan); + + chan = sr_channel_new(sdi, 1, SR_CHANNEL_ANALOG, TRUE, "CH2"); + devc->channel_meaning[1].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[1].unit = SR_UNIT_VOLT; + devc->channel_meaning[1].mqflags = SR_MQFLAG_DC; + devc->channel_meaning[1].channels = g_slist_prepend(NULL, chan); + + chan = sr_channel_new(sdi, 2, SR_CHANNEL_ANALOG, FALSE, "P"); + devc->channel_meaning[2].mq = SR_MQ_POWER; + devc->channel_meaning[2].unit = SR_UNIT_WATT; + devc->channel_meaning[2].mqflags = SR_MQFLAG_RMS; + devc->channel_meaning[2].channels = g_slist_prepend(NULL, chan); +} + +static GSList *scan(struct sr_dev_driver *di, GSList *options) +{ + struct sr_bt_desc *desc; + const char *conn; + struct sr_config *src; + GSList *l; + int ret; + + conn = NULL; + for (l = options; l; l = l->next) { + src = l->data; + switch (src->key) { + case SR_CONF_CONN: + conn = g_variant_get_string(src->data, NULL); + break; + } + } + + if (!conn) + return NULL; + + desc = sr_bt_desc_new(); + if (!desc) + return NULL; + + ret = sr_bt_config_addr_remote(desc, conn); + if (ret < 0) + goto err; + + /* + * These handles where queried with btgatt-client, since the + * documentation specifies them in terms of UUIDs. + * + * service - start: 0x0010, end: 0xffff, type: primary, uuid: 1bc5ffa0-0200-62ab-e411-f254e005dbd4 + * charac - start: 0x0011, value: 0x0012, props: 0x08, ext_props: 0x0000, uuid: 1bc5ffa1-0200-62ab-e411-f254e005dbd4 + * descr - handle: 0x0013, uuid: 00002901-0000-1000-8000-00805f9b34fb + * charac - start: 0x0014, value: 0x0015, props: 0x10, ext_props: 0x0000, uuid: 1bc5ffa2-0200-62ab-e411-f254e005dbd4 + * descr - handle: 0x0016, uuid: 00002902-0000-1000-8000-00805f9b34fb + * descr - handle: 0x0017, uuid: 00002901-0000-1000-8000-00805f9b34fb + */ + ret = sr_bt_config_notify(desc, 0x0015, 0x0012, 0x0016, 0x0001); + if (ret < 0) + goto err; + + struct sr_dev_inst *sdi = g_malloc0(sizeof(struct sr_dev_inst)); + struct dev_context *devc = g_malloc0(sizeof(struct dev_context)); + + sdi->priv = devc; + sdi->inst_type = SR_INST_USER; + sdi->connection_id = g_strdup(conn); + sdi->conn = desc; + + init_dev(sdi); + + return std_scan_complete(di, g_slist_prepend(NULL, sdi)); + +err: + sr_bt_desc_free(desc); + return NULL; +} + +static int dev_clear(const struct sr_dev_driver *di) +{ + struct drv_context *drvc = di->context; + struct sr_dev_inst *sdi; + GSList *l; + + if (drvc) { + for (l = drvc->instances; l; l = l->next) { + sdi = l->data; + struct sr_bt_desc *desc = sdi->conn; + if (desc) + sr_bt_desc_free(desc); + sdi->conn = NULL; + } + } + + return std_dev_clear(di); +} + +static int set_channel1_mean(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_RMS; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_DC; + + return mooshimeter_dmm_set_chooser(sdi, "CH1:ANALYSIS", + "CH1:ANALYSIS:MEAN"); +} + +static int set_channel1_rms(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_DC; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_RMS; + + return mooshimeter_dmm_set_chooser(sdi, "CH1:ANALYSIS", + "CH1:ANALYSIS:RMS"); +} + +static int set_channel1_buffer(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + devc->channel_meaning[0].mqflags &= ~(SR_MQFLAG_DC | SR_MQFLAG_RMS); + + return mooshimeter_dmm_set_chooser(sdi, "CH1:ANALYSIS", + "CH1:ANALYSIS:BUFFER"); +} + +static int set_channel2_mean(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_RMS; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_DC; + + return mooshimeter_dmm_set_chooser(sdi, "CH2:ANALYSIS", + "CH2:ANALYSIS:MEAN"); +} + +static int set_channel2_rms(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_DC; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_RMS; + + return mooshimeter_dmm_set_chooser(sdi, "CH2:ANALYSIS", + "CH2:ANALYSIS:RMS"); +} + +static int set_channel2_buffer(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + devc->channel_meaning[1].mqflags &= ~(SR_MQFLAG_DC | SR_MQFLAG_RMS); + + return mooshimeter_dmm_set_chooser(sdi, "CH2:ANALYSIS", + "CH2:ANALYSIS:BUFFER"); +} + +static void autorange_channel1_current(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH1:RANGE_I", + "CH1:MAPPING:CURRENT", value); +} + +static int configure_channel1_current(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH1:MAPPING", + "CH1:MAPPING:CURRENT"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH1:RANGE_I", + "CH1:MAPPING:CURRENT", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[0] = autorange_channel1_current; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[0] = NULL; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[0].mq = SR_MQ_CURRENT; + devc->channel_meaning[0].unit = SR_UNIT_AMPERE; + + return SR_OK; +} + +static void autorange_channel1_temperature(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH1:RANGE_I", + "CH1:MAPPING:TEMP", value); +} + +static int configure_channel1_temperature(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH1:MAPPING", + "CH1:MAPPING:TEMP"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH1:RANGE_I", + "CH1:MAPPING:TEMP", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[0] = autorange_channel1_temperature; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[0] = NULL; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[0].mq = SR_MQ_TEMPERATURE; + devc->channel_meaning[0].unit = SR_UNIT_KELVIN; + + return SR_OK; +} + +static void autorange_channel1_auxv(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH1:RANGE_I", + "SHARED:AUX_V", value); +} + +static int configure_channel1_auxv(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:AUX_V"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH1:MAPPING", + "CH1:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH1:RANGE_I", + "SHARED:AUX_V", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[0] = autorange_channel1_auxv; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[0] = NULL; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[0].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[0].unit = SR_UNIT_VOLT; + + return SR_OK; +} + +static void autorange_channel1_resistance(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH1:RANGE_I", + "SHARED:RESISTANCE", value); +} + +static int configure_channel1_resistance(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:RESISTANCE"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH1:MAPPING", + "CH1:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH1:RANGE_I", + "SHARED:RESISTANCE", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[0] = autorange_channel1_resistance; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[0] = NULL; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[0].mq = SR_MQ_RESISTANCE; + devc->channel_meaning[0].unit = SR_UNIT_OHM; + + return SR_OK; +} + +static void autorange_channel1_diode(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH1:RANGE_I", + "SHARED:DIODE", value); +} + +static int configure_channel1_diode(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:DIODE"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH1:MAPPING", + "CH1:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH1:RANGE_I", + "SHARED:DIODE", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[0] = autorange_channel1_diode; + devc->channel_meaning[0].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[0] = NULL; + devc->channel_meaning[0].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[0].mqflags |= SR_MQFLAG_DIODE; + devc->channel_meaning[0].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[0].unit = SR_UNIT_VOLT; + + return SR_OK; +} + +static void autorange_channel2_voltage(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH2:RANGE_I", + "CH2:MAPPING:VOLTAGE", value); +} + +static int configure_channel2_voltage(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH2:MAPPING", + "CH2:MAPPING:VOLTAGE"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH2:RANGE_I", + "CH2:MAPPING:VOLTAGE", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[1] = autorange_channel2_voltage; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[1] = NULL; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[1].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[1].unit = SR_UNIT_VOLT; + + return SR_OK; +} + +static void autorange_channel2_temperature(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH2:RANGE_I", + "CH2:MAPPING:TEMP", value); +} + +static int configure_channel2_temperature(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH2:MAPPING", + "CH2:MAPPING:TEMP"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH2:RANGE_I", + "CH2:MAPPING:TEMP", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[1] = autorange_channel2_temperature; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[1] = NULL; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[1].mq = SR_MQ_TEMPERATURE; + devc->channel_meaning[1].unit = SR_UNIT_CELSIUS; + + return SR_OK; +} + +static void autorange_channel2_auxv(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH2:RANGE_I", + "SHARED:AUX_V", value); +} + +static int configure_channel2_auxv(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:AUX_V"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH2:MAPPING", + "CH2:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH2:RANGE_I", + "SHARED:AUX_V", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[1] = autorange_channel2_auxv; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[1] = NULL; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[1].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[1].unit = SR_UNIT_VOLT; + + return SR_OK; +} + +static void autorange_channel2_resistance(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH2:RANGE_I", + "SHARED:RESISTANCE", value); +} + +static int configure_channel2_resistance(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:RESISTANCE"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH2:MAPPING", + "CH2:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH2:RANGE_I", + "SHARED:RESISTANCE", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[1] = autorange_channel2_resistance; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[1] = NULL; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_DIODE; + devc->channel_meaning[1].mq = SR_MQ_RESISTANCE; + devc->channel_meaning[1].unit = SR_UNIT_OHM; + + return SR_OK; +} + +static void autorange_channel2_diode(const struct sr_dev_inst *sdi, + float value) +{ + mooshimeter_dmm_set_autorange(sdi, "CH2:RANGE_I", + "SHARED:DIODE", value); +} + +static int configure_channel2_diode(const struct sr_dev_inst *sdi, + float range) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SHARED", "SHARED:DIODE"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "CH2:MAPPING", + "CH2:MAPPING:SHARED"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "CH2:RANGE_I", + "SHARED:DIODE", range); + if (ret != SR_OK) + return ret; + + if (range <= 0) { + devc->channel_autorange[1] = autorange_channel2_diode; + devc->channel_meaning[1].mqflags |= SR_MQFLAG_AUTORANGE; + } else { + devc->channel_autorange[1] = NULL; + devc->channel_meaning[1].mqflags &= ~SR_MQFLAG_AUTORANGE; + } + + devc->channel_meaning[1].mqflags |= SR_MQFLAG_DIODE; + devc->channel_meaning[1].mq = SR_MQ_VOLTAGE; + devc->channel_meaning[1].unit = SR_UNIT_VOLT; + + return SR_OK; +} + +/* + * Full string: CH1,CH2 + * Each channel: MODE[:RANGE[:ANALYSIS]] + * Channel 1 mode: + * Current, A + * Temperature, T, K + * Resistance, Ohm, W + * Diode, D + * Aux, LV + * Channel 2 mode: + * Voltage, V + * Temperature, T, K + * Resistance, Ohm, W + * Diode, D + * Aux, LV + * Range is the upper bound of the range (e.g. 60 for 0-60 V or 600 for 0-600), + * zero or absent for autoranging + * Analysis: + * Mean, DC + * RMS, AC + * Buffer, Samples + */ +static int apply_channel_config(const struct sr_dev_inst *sdi, + const char *config) +{ + gchar **channel_config; + gchar **parameters; + const gchar *param; + int ret = SR_ERR; + float range; + gboolean shared_in_use = FALSE; + + channel_config = g_strsplit_set(config, ",/", -1); + if (!channel_config[0]) + goto err_free_channel_config; + + parameters = g_strsplit_set(channel_config[0], ":;", -1); + if (parameters[0] && parameters[0][0]) { + range = 0; + if (parameters[1]) + range = g_ascii_strtod(parameters[1], NULL); + + param = parameters[0]; + if (!g_ascii_strncasecmp(param, "Resistance", 10) || + !g_ascii_strncasecmp(param, "Ohm", 3) || + !g_ascii_strncasecmp(param, "W", 1) || + !g_ascii_strncasecmp(param, "R", 1)) { + ret = configure_channel1_resistance(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + shared_in_use = TRUE; + } else if (!g_ascii_strncasecmp(param, "Diode", 5) || + !g_ascii_strncasecmp(param, "D", 1)) { + ret = configure_channel1_diode(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + shared_in_use = TRUE; + } else if (!g_ascii_strncasecmp(param, "Aux", 3) || + !g_ascii_strncasecmp(param, "LV", 2)) { + ret = configure_channel1_auxv(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + shared_in_use = TRUE; + } else if (!g_ascii_strncasecmp(param, "T", 1) || + !g_ascii_strncasecmp(param, "K", 1)) { + ret = configure_channel1_temperature(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strncasecmp(param, "Current", 7) || + !g_ascii_strncasecmp(param, "A", 1) || + *parameters[0]) { + ret = configure_channel1_current(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else { + sr_info("Unrecognized mode for CH1: %s.", param); + ret = configure_channel1_current(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } + + if (parameters[1] && parameters[2]) { + param = parameters[2]; + if (!g_ascii_strcasecmp(param, "RMS") || + !g_ascii_strcasecmp(param, "AC")) { + ret = set_channel1_rms(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strcasecmp(param, "Buffer") || + !g_ascii_strcasecmp(param, "Samples")) { + ret = set_channel1_buffer(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } else { + ret = set_channel1_mean(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } + } + } + g_strfreev(parameters); + + if (!channel_config[1]) { + g_strfreev(channel_config); + return SR_OK; + } + + parameters = g_strsplit_set(channel_config[1], ":;", -1); + if (parameters[0] && parameters[0][0]) { + range = 0; + if (parameters[1]) + range = g_ascii_strtod(parameters[1], NULL); + + param = parameters[0]; + if (!g_ascii_strncasecmp(param, "Resistance", 10) || + !g_ascii_strncasecmp(param, "Ohm", 3) || + !g_ascii_strncasecmp(param, "W", 1) || + !g_ascii_strncasecmp(param, "R", 1)) { + if (shared_in_use) { + ret = SR_ERR; + goto err_free_parameters; + } + ret = configure_channel2_resistance(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strncasecmp(param, "Diode", 5) || + !g_ascii_strncasecmp(param, "D", 1)) { + if (shared_in_use) { + ret = SR_ERR; + goto err_free_parameters; + } + ret = configure_channel2_diode(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strncasecmp(param, "Aux", 3) || + !g_ascii_strncasecmp(param, "LV", 2)) { + if (shared_in_use) { + ret = SR_ERR; + goto err_free_parameters; + } + ret = configure_channel2_auxv(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strncasecmp(param, "T", 1) || + !g_ascii_strncasecmp(param, "K", 1)) { + ret = configure_channel2_temperature(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strncasecmp(param, "V", 1) || + !param[0]) { + ret = configure_channel2_voltage(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } else { + sr_info("Unrecognized mode for CH2: %s.", param); + ret = configure_channel2_voltage(sdi, range); + if (ret != SR_OK) + goto err_free_parameters; + } + + if (parameters[1] && parameters[2]) { + param = parameters[2]; + if (!g_ascii_strcasecmp(param, "RMS") || + !g_ascii_strcasecmp(param, "AC")) { + ret = set_channel2_rms(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } else if (!g_ascii_strcasecmp(param, "Buffer") || + !g_ascii_strcasecmp(param, "Samples")) { + ret = set_channel2_buffer(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } else { + ret = set_channel2_mean(sdi); + if (ret != SR_OK) + goto err_free_parameters; + } + } + } + g_strfreev(parameters); + + g_strfreev(channel_config); + return SR_OK; + +err_free_parameters: + g_strfreev(parameters); +err_free_channel_config: + g_strfreev(channel_config); + return ret; +} + +static int dev_open(struct sr_dev_inst *sdi) +{ + int ret; + + ret = mooshimeter_dmm_open(sdi); + if (ret != SR_OK) + return ret; + + sdi->status = SR_ST_INACTIVE; + + ret = mooshimeter_dmm_set_chooser(sdi, "SAMPLING:TRIGGER", + "SAMPLING:TRIGGER:OFF"); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "SAMPLING:RATE", + "SAMPLING:RATE", 125); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_larger_number(sdi, "SAMPLING:DEPTH", + "SAMPLING:DEPTH", 64); + if (ret != SR_OK) + return ret; + + /* Looks like these sometimes get set to 8, somehow? */ + ret = mooshimeter_dmm_set_integer(sdi, "CH1:BUF_BPS", 24); + if (ret != SR_OK) + return ret; + + ret = mooshimeter_dmm_set_integer(sdi, "CH2:BUF_BPS", 24); + if (ret != SR_OK) + return ret; + + ret = configure_channel1_current(sdi, 0); + if (ret != SR_OK) + return ret; + + ret = set_channel1_mean(sdi); + if (ret != SR_OK) + return ret; + + ret = configure_channel2_voltage(sdi, 0); + if (ret != SR_OK) + return ret; + + ret = set_channel2_mean(sdi); + if (ret != SR_OK) + return ret; + + sdi->status = SR_ST_ACTIVE; + + return SR_OK; +} + +static int dev_close(struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + sdi->status = SR_ST_INACTIVE; + + g_slist_free(devc->channel_meaning[0].channels); + devc->channel_meaning[0].channels = NULL; + + g_slist_free(devc->channel_meaning[1].channels); + devc->channel_meaning[1].channels = NULL; + + g_slist_free(devc->channel_meaning[2].channels); + devc->channel_meaning[2].channels = NULL; + + return mooshimeter_dmm_close(sdi); +} + +static int config_get(uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + struct dev_context *devc = sdi->priv; + int ret; + float value; + + (void)cg; + + switch (key) { + case SR_CONF_SAMPLERATE: + ret = mooshimeter_dmm_get_chosen_number(sdi, "SAMPLING:RATE", + "SAMPLING:RATE", &value); + if (ret != SR_OK) + return ret; + *data = g_variant_new_uint64((guint64)value); + return SR_OK; + case SR_CONF_AVG_SAMPLES: + ret = mooshimeter_dmm_get_chosen_number(sdi, "SAMPLING:DEPTH", + "SAMPLING:DEPTH", &value); + if (ret != SR_OK) + return ret; + *data = g_variant_new_uint64((guint64)value); + return SR_OK; + case SR_CONF_CHANNEL_CONFIG: + return SR_ERR_NA; + default: + break; + } + + return sr_sw_limits_config_get(&devc->limits, key, data); +} + +static int config_set(uint32_t key, GVariant *data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + struct dev_context *devc = sdi->priv; + + (void)cg; + + switch (key) { + case SR_CONF_SAMPLERATE: + return mooshimeter_dmm_set_larger_number(sdi, "SAMPLING:RATE", + "SAMPLING:RATE", g_variant_get_uint64(data)); + case SR_CONF_AVG_SAMPLES: + return mooshimeter_dmm_set_larger_number(sdi, "SAMPLING:DEPTH", + "SAMPLING:DEPTH", g_variant_get_uint64(data)); + case SR_CONF_CHANNEL_CONFIG: + return apply_channel_config(sdi, g_variant_get_string(data, NULL)); + default: + break; + } + + return sr_sw_limits_config_set(&devc->limits, key, data); +} + +static int config_list(uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + int ret; + float *values; + size_t count; + uint64_t *integers; + + switch (key) { + case SR_CONF_SAMPLERATE: + ret = mooshimeter_dmm_get_available_number_choices(sdi, + "SAMPLING:RATE", &values, &count); + if (ret != SR_OK) + return ret; + integers = g_malloc(sizeof(uint64_t) * count); + for (size_t i = 0; i < count; i++) + integers[i] = (uint64_t)values[i]; + g_free(values); + *data = std_gvar_samplerates(integers, count); + g_free(integers); + return SR_OK; + case SR_CONF_AVG_SAMPLES: + ret = mooshimeter_dmm_get_available_number_choices(sdi, + "SAMPLING:DEPTH", &values, &count); + if (ret != SR_OK) + return ret; + integers = g_malloc(sizeof(uint64_t) * count); + for (size_t i = 0; i < count; i++) + integers[i] = (uint64_t)values[i]; + g_free(values); + *data = std_gvar_array_u64(integers, count); + g_free(integers); + return SR_OK; + case SR_CONF_CHANNEL_CONFIG: + return SR_ERR_NA; + default: + break; + } + + return STD_CONFIG_LIST(key, data, sdi, cg, scanopts, drvopts, devopts); +} + +static int dev_acquisition_start(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + int ret; + + ret = mooshimeter_dmm_set_chooser(sdi, "SAMPLING:TRIGGER", + "SAMPLING:TRIGGER:CONTINUOUS"); + if (ret) + return ret; + + sr_sw_limits_acquisition_start(&devc->limits); + std_session_send_df_header(sdi); + + sr_session_source_add(sdi->session, -1, 0, 10000, + mooshimeter_dmm_heartbeat, (void *)sdi); + + /* The Bluetooth socket isn't exposed, so just poll for data. */ + sr_session_source_add(sdi->session, -2, 0, 50, + mooshimeter_dmm_poll, (void *)sdi); + + devc->enable_value_stream = TRUE; + + return SR_OK; +} + +static int dev_acquisition_stop(struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + sr_session_source_remove(sdi->session, -1); + sr_session_source_remove(sdi->session, -2); + devc->enable_value_stream = FALSE; + + mooshimeter_dmm_set_chooser(sdi, "SAMPLING:TRIGGER", + "SAMPLING:TRIGGER:OFF"); + + return SR_OK; +} + +static struct sr_dev_driver mooshimeter_dmm_driver_info = { + .name = "mooshimeter-dmm", + .longname = "Mooshimeter DMM", + .api_version = 1, + .init = std_init, + .cleanup = std_cleanup, + .scan = scan, + .dev_list = std_dev_list, + .dev_clear = dev_clear, + .config_get = config_get, + .config_set = config_set, + .config_list = config_list, + .dev_open = dev_open, + .dev_close = dev_close, + .dev_acquisition_start = dev_acquisition_start, + .dev_acquisition_stop = dev_acquisition_stop, + .context = NULL, +}; +SR_REGISTER_DEV_DRIVER(mooshimeter_dmm_driver_info); diff --git a/src/hardware/mooshimeter-dmm/protocol.c b/src/hardware/mooshimeter-dmm/protocol.c new file mode 100644 index 00000000..38fd3fcc --- /dev/null +++ b/src/hardware/mooshimeter-dmm/protocol.c @@ -0,0 +1,1569 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2019 Derek Hageman + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include "protocol.h" + +/* + * The Mooshimeter protocol is broken down into several layers in a + * communication stack. + * + * The lowest layer is the BLE GATT stack, which provides two characteristics: + * one to write packets to the meter and one to receive them from it. The + * MTU for a packet in either direction is 20 bytes. This is implemented + * in the GATT abstraction, so we can talk to it via simple write commands + * and a read callback. + * + * + * The next layer is the serial stream: each BLE packet in either direction + * has a 1-byte header of a sequence number. Despite what the documentation + * says, this is present in both directions (not just meter output) and is + * NOT reset on the meter output on BLE connection. So the implementation + * here needs to provide an output sequence number and incoming reassembly + * for out of order packets (I haven't actually observed this, but + * supposedly it happens, which is why the sequence number is present). + * So the structure of packets received looks like: + * + * | 1 byte | 1-19 bytes | + * |--------|-------------| + * | SeqNum | Serial Data | + * + * + * On top of the serial layer is the "config tree" layer. This is how + * the meter actually exposes data and configuration. The tree itself + * is composed of nodes, each with a string name, data type, and a list + * of children (zero or more). For value containing (non-informational) + * nodes, they also contain a 7-bit unique identifier. Access to the + * config tree is provided by packets on the serial stream, each packet + * has a 1-byte header, where the uppermost bit (0x80) is set when writing + * (i.e. never by the meter) and the remaining 7 bits are the node identifier. + * The length of the packets varies based on the datatype of the tree node. + * This means that any lost/dropped packets can make the stream unrecoverable + * (i.e. there's no defined sync method other than reconnection). Packets + * are emitted by the meter in response to a read or write command (write + * commands simply back the value) and at unsolicited times by the meter + * (e.g. continuous sampling and periodic battery voltage). A read packet + * send to the meter looks like: + * + * | 1 bit | 7 bits | + * |-------|--------| + * | 0 | NodeID | + * + * In response to the read, the meter will send: + * + * | 1 bit | 7 bits | 1-N bytes | + * |-------|--------|-----------| + * | 0 | NodeID | NodeValue | + * + * A write packet sent to the meter: + * + * | 1 bit | 7 bits | 1-N bytes | + * |-------|--------|-----------| + * | 1 | NodeID | NodeValue | + * + * In response to the write, the meter will send a read response: + * + * | 1 bit | 7 bits | 1-N bytes | + * |-------|--------|-----------| + * | 0 | NodeID | NodeValue | + * + * + * For the data in the tree, all values are little endian (least significant + * bytes first). The supported type codes are: + * + * | Code | Description | Wire Format | + * |------|-------------|----------------------------------------| + * | 0 | Plain | | + * | 1 | Link | | + * | 2 | Chooser | uint8_t | + * | 3 | U8 | uint8_t | + * | 4 | U16 | uint16_t | + * | 5 | U32 | uint32_t | + * | 6 | S8 | int8_t | + * | 7 | S16 | int16_t | + * | 8 | S32 | int32_t | + * | 9 | String | uint16_t length; char value[length] | + * | 10 | Binary | uint16_t length; uint8_t value[length] | + * | 11 | Float | float | + * + * Plain and Link nodes are present to provide information and/or choices + * but do not provide commands codes for direct access (see serialization + * below). Chooser nodes are written with indices described by their Plain + * type children (e.g. to select a choice identified by the second child + * of a chooser, write 1 to the chooser node itself). + * + * On initial connection only three nodes at fixed identifiers are available: + * + * | Node | ID | Type | + * |------------------|----|--------| + * | ADMIN:CRC32 | 0 | U32 | + * | ADMIN:TREE | 1 | Binary | + * | ADMIN:DIAGNOSTIC | 2 | String | + * + * + * The handshake sequence is to read the contents of ADMIN:TREE, which contains + * the zlib compressed tree serialization, then write the CRC of the compressed + * data back to ADMIN:CRC32 (which the meter will echo back). Only after + * that is done will the meter accept access to the rest of the tree. + * + * After zlib decompression the tree serialization is as follows: + * + * | Type | Description | + * |--------------|-------------------------------------| + * | uint8_t | The node data type code from above | + * | uint8_t | Name length | + * | char[length] | Node name (e.g. "ADMIN" or "CRC32") | + * | uint8_t | Number of children | + * | Node[count] | Child serialization (length varies) | + * + * Once the tree has been deserialized, each node needs its identifier + * assigned. This is a depth first tree walk, assigning sequential identifiers + * first the the current node (if it needs one), then repeating recursively + * for each of its children. Plain and Link nodes are skipped in assignment + * but not the walk (so the recursion still happens, but the identifier + * is not incremented). + * + * + * So, for example a write to the ADMIN:CRC32 as part of the handshake would + * be a write by us (the host): + * + * | SerSeq | NodeID | U32 (CRC) | + * | 1 byte | 1 byte | 4 bytes | + * ---------|--------|------------| + * | 0x01 | 0x80 | 0xDEADBEEF | + * + * The meter will respond with a packet like: + * + * | SerSeq | NodeID | U32 (CRC) | + * | 1 byte | 1 byte | 4 bytes | + * ---------|--------|------------| + * | 0x42 | 0x00 | 0xDEADBEEF | + * + * A spontaneous error from the meter (e.g. in response to a bad packet) + * can be emitted like: + * + * | SerSeq | NodeID | U16 (len) | String | + * | 1 byte | 1 byte | 2 bytes | len (=8) bytes | + * ---------|--------|------------|------------------| + * | 0xAB | 0x20 | 0x0008 | BAD\x20DATA | + * + * + * The config tree at the time of writing looks like: + * + * (PLAIN) + * ADMIN (PLAIN) + * CRC32 (U32) = 0 + * TREE (BIN) = 1 + * DIAGNOSTIC (STR) = 2 + * PCB_VERSION (U8) = 3 + * NAME (STR) = 4 + * TIME_UTC (U32) = 5 + * TIME_UTC_MS (U16) = 6 + * BAT_V (FLT) = 7 + * REBOOT (CHOOSER) = 8 + * NORMAL (PLAIN) + * SHIPMODE (PLAIN) + * SAMPLING (PLAIN) + * RATE (CHOOSER) = 9 + * 125 (PLAIN) + * 250 (PLAIN) + * 500 (PLAIN) + * 1000 (PLAIN) + * 2000 (PLAIN) + * 4000 (PLAIN) + * 8000 (PLAIN) + * DEPTH (CHOOSER) = 10 + * 32 (PLAIN) + * 64 (PLAIN) + * 128 (PLAIN) + * 256 (PLAIN) + * TRIGGER (CHOOSER) = 11 + * OFF (PLAIN) + * SINGLE (PLAIN) + * CONTINUOUS (PLAIN) + * LOG (PLAIN) + * ON (U8) = 12 + * INTERVAL (U16) = 13 + * STATUS (U8) = 14 + * POLLDIR (U8) = 15 + * INFO (PLAIN) + * INDEX (U16) = 16 + * END_TIME (U32) = 17 + * N_BYTES (U32) = 18 + * STREAM (PLAIN) + * INDEX (U16) = 19 + * OFFSET (U32) = 20 + * DATA (BIN) = 21 + * CH1 (PLAIN) + * MAPPING (CHOOSER) = 22 + * CURRENT (PLAIN) + * 10 (PLAIN) + * TEMP (PLAIN) + * 350 (PLAIN) + * SHARED (LINK) + * RANGE_I (U8) = 23 + * ANALYSIS (CHOOSER) = 24 + * MEAN (PLAIN) + * RMS (PLAIN) + * BUFFER (PLAIN) + * VALUE (FLT) = 25 + * OFFSET (FLT) = 26 + * BUF (BIN) = 27 + * BUF_BPS (U8) = 28 + * BUF_LSB2NATIVE (FLT) = 29 + * CH2 (PLAIN) + * MAPPING (CHOOSER) = 30 + * VOLTAGE (PLAIN) + * 60 (PLAIN) + * 600 (PLAIN) + * TEMP (PLAIN) + * 350 (PLAIN) + * SHARED (LINK) + * RANGE_I (U8) = 31 + * ANALYSIS (CHOOSER) = 32 + * MEAN (PLAIN) + * RMS (PLAIN) + * BUFFER (PLAIN) + * VALUE (FLT) = 33 + * OFFSET (FLT) = 34 + * BUF (BIN) = 35 + * BUF_BPS (U8) = 36 + * BUF_LSB2NATIVE (FLT) = 37 + * SHARED (CHOOSER) = 38 + * AUX_V (PLAIN) + * 0.1 (PLAIN) + * 0.3 (PLAIN) + * 1.2 (PLAIN) + * RESISTANCE (PLAIN) + * 1000.0 (PLAIN) + * 10000.0 (PLAIN) + * 100000.0 (PLAIN) + * 1000000.0 (PLAIN) + * 10000000.0 (PLAIN) + * DIODE (PLAIN) + * 1.2 (PLAIN) + * REAL_PWR (FLT) = 39 + */ + +static struct config_tree_node *lookup_tree_path(struct dev_context *devc, + const char *path) +{ + struct config_tree_node *current = &devc->tree_root; + struct config_tree_node *next; + const char *end; + size_t length; + + for (;;) { + end = strchr(path, ':'); + if (!end) + length = strlen(path); + else + length = end - path; + + next = NULL; + for (size_t i = 0; i < current->count_children; i++) { + if (!current->children[i].name) + continue; + if (strlen(current->children[i].name) != length) + continue; + if (g_ascii_strncasecmp(path, + current->children[i].name, + length)) { + continue; + } + + next = ¤t->children[i]; + } + if (!next) + return NULL; + if (!end) + return next; + + path = end + 1; + current = next; + } +} + +static int lookup_chooser_index(struct dev_context *devc, const char *path) +{ + struct config_tree_node *node; + + node = lookup_tree_path(devc, path); + if (!node) + return -1; + + return (int)node->index_in_parent; +} + +static gboolean update_tree_data(struct config_tree_node *node, + GByteArray *contents) +{ + guint len; + switch (node->type) { + case TREE_NODE_DATATYPE_PLAIN: + case TREE_NODE_DATATYPE_LINK: + sr_err("Update for dataless node."); + g_byte_array_remove_range(contents, 0, 2); + return TRUE; + case TREE_NODE_DATATYPE_CHOOSER: + case TREE_NODE_DATATYPE_U8: + node->value.i = R8(contents->data + 1); + g_byte_array_remove_range(contents, 0, 2); + break; + case TREE_NODE_DATATYPE_U16: + if (contents->len < 3) + return FALSE; + node->value.i = RL16(contents->data + 1); + g_byte_array_remove_range(contents, 0, 3); + break; + case TREE_NODE_DATATYPE_U32: + if (contents->len < 5) + return FALSE; + node->value.i = RL32(contents->data + 1); + g_byte_array_remove_range(contents, 0, 5); + break; + case TREE_NODE_DATATYPE_S8: + node->value.i = (int8_t)R8(contents->data + 1); + g_byte_array_remove_range(contents, 0, 2); + break; + case TREE_NODE_DATATYPE_S16: + if (contents->len < 3) + return FALSE; + node->value.i = RL16S(contents->data + 1); + g_byte_array_remove_range(contents, 0, 3); + break; + case TREE_NODE_DATATYPE_S32: + if (contents->len < 5) + return FALSE; + node->value.i = RL32S(contents->data + 1); + g_byte_array_remove_range(contents, 0, 5); + break; + case TREE_NODE_DATATYPE_STRING: + case TREE_NODE_DATATYPE_BINARY: + if (contents->len < 3) + return FALSE; + len = RL16(contents->data + 1); + if (contents->len < 3 + len) + return FALSE; + g_byte_array_set_size(node->value.b, len); + memcpy(node->value.b->data, contents->data + 3, len); + g_byte_array_remove_range(contents, 0, 3 + len); + break; + case TREE_NODE_DATATYPE_FLOAT: + if (contents->len < 5) + return FALSE; + node->value.f = RLFL(contents->data + 1); + g_byte_array_remove_range(contents, 0, 5); + break; + } + + node->update_number++; + + if (node->on_update) + (*node->on_update)(node, node->on_update_param); + + return TRUE; +} + +static gboolean incoming_frame(struct packet_rx *rx, + const void *data, guint count) +{ + const guint8 *bytes = data; + int seq, ahead; + GByteArray *ba; + GSList *target = NULL; + + if (!count) + return FALSE; + + seq = bytes[0]; + if (rx->sequence_number < 0) { + rx->sequence_number = (seq + 1) & 0xFF; + g_byte_array_append(rx->contents, bytes + 1, count - 1); + return TRUE; + } else if (rx->sequence_number == seq) { + rx->sequence_number = (seq + 1) & 0xFF; + g_byte_array_append(rx->contents, data + 1, count - 1); + + while (rx->reorder_buffer && rx->reorder_buffer->data) { + rx->sequence_number = (rx->sequence_number + 1) & 0xFF; + + ba = rx->reorder_buffer->data; + g_byte_array_append(rx->contents, ba->data, ba->len); + g_byte_array_free(ba, TRUE); + target = rx->reorder_buffer; + rx->reorder_buffer = rx->reorder_buffer->next; + g_slist_free_1(target); + } + return TRUE; + } else { + ahead = seq - rx->sequence_number; + if (ahead < 0) + ahead += 256; + if (!rx->reorder_buffer) + rx->reorder_buffer = g_slist_alloc(); + target = rx->reorder_buffer; + for (--ahead; ahead > 0; --ahead) { + if (!target->next) + target->next = g_slist_alloc(); + target = target->next; + } + if (target->data) + g_byte_array_free(target->data, TRUE); + target->data = g_byte_array_sized_new(count); + g_byte_array_append(target->data, data + 1, count - 1); + return TRUE; + } +} + +static void consume_packets(struct dev_context *devc) +{ + uint8_t id; + struct config_tree_node *target; + + if (devc->rx.contents->len < 2) + return; + + id = devc->rx.contents->data[0]; + id &= 0x7F; + target = devc->tree_id_lookup[id]; + + if (!target) { + sr_err("Command %hhu code does not map to a known node.", id); + g_byte_array_remove_index(devc->rx.contents, 0); + return consume_packets(devc); + } + + if (!update_tree_data(target, devc->rx.contents)) + return; + + return consume_packets(devc); +} + +static int notify_cb(void *cb_data, uint8_t *data, size_t dlen) +{ + const struct sr_dev_inst *sdi = cb_data; + struct dev_context *devc = sdi->priv; + + if (!incoming_frame(&devc->rx, data, (guint)dlen)) + return -1; + + consume_packets(devc); + + return 0; +} + +static int write_frame(const struct sr_dev_inst *sdi, + const void *frame, size_t length) +{ + struct sr_bt_desc *desc = sdi->conn; + + if (sr_bt_write(desc, frame, length) != (ssize_t)length) + return SR_ERR; + + return SR_OK; +} + +static int poll_tree_value(const struct sr_dev_inst *sdi, + struct config_tree_node *node) +{ + struct dev_context *devc = sdi->priv; + + uint8_t frame[2]; + W8(&frame[0], devc->tx.sequence_number); + W8(&frame[1], node->id); + + devc->tx.sequence_number = (devc->tx.sequence_number + 1) & 0xFF; + + return write_frame(sdi, frame, 2); +} + +static void set_tree_integer(const struct sr_dev_inst *sdi, + struct config_tree_node *node, int32_t value) +{ + struct dev_context *devc = sdi->priv; + uint8_t frame[20]; + size_t length; + + W8(&frame[0], devc->tx.sequence_number); + W8(&frame[1], 0x80 | node->id); + + length = 2; + + switch (node->type) { + case TREE_NODE_DATATYPE_PLAIN: + case TREE_NODE_DATATYPE_LINK: + sr_err("Set attempted for dataless node."); + return; + case TREE_NODE_DATATYPE_CHOOSER: + case TREE_NODE_DATATYPE_U8: + node->value.i = value; + W8(&frame[length], value); + length += 1; + break; + case TREE_NODE_DATATYPE_U16: + node->value.i = value; + WL16(&frame[length], value); + length += 2; + break; + case TREE_NODE_DATATYPE_U32: + node->value.i = value; + WL32(&frame[length], value); + length += 4; + break; + case TREE_NODE_DATATYPE_S8: + node->value.i = value; + W8(&frame[length], value); + length += 1; + break; + case TREE_NODE_DATATYPE_S16: + node->value.i = value; + WL16(&frame[length], value); + length += 2; + break; + case TREE_NODE_DATATYPE_S32: + node->value.i = value; + WL32(&frame[length], value); + length += 4; + break; + case TREE_NODE_DATATYPE_STRING: + case TREE_NODE_DATATYPE_BINARY: + case TREE_NODE_DATATYPE_FLOAT: + return; + } + + devc->tx.sequence_number = (devc->tx.sequence_number + 1) & 0xFF; + write_frame(sdi, frame, length); +} + +static int32_t get_tree_integer(struct config_tree_node *node) +{ + switch (node->type) { + case TREE_NODE_DATATYPE_PLAIN: + case TREE_NODE_DATATYPE_LINK: + sr_err("Read attempted for dataless node."); + return 0; + case TREE_NODE_DATATYPE_CHOOSER: + case TREE_NODE_DATATYPE_U8: + case TREE_NODE_DATATYPE_U16: + case TREE_NODE_DATATYPE_U32: + case TREE_NODE_DATATYPE_S8: + case TREE_NODE_DATATYPE_S16: + case TREE_NODE_DATATYPE_S32: + return node->value.i; + case TREE_NODE_DATATYPE_FLOAT: + return (int)node->value.f; + default: + break; + } + + return 0; +} + +static void tree_diagnostic_updated(struct config_tree_node *node, void *param) +{ + (void)param; + + if (!node->value.b->len) { + sr_warn("Mooshimeter error with no information."); + return; + } + + if (node->value.b->data[node->value.b->len]) { + g_byte_array_set_size(node->value.b, node->value.b->len + 1); + node->value.b->data[node->value.b->len - 1] = 0; + } + + sr_warn("Mooshimeter error: %s.", node->value.b->data); +} + +static void chX_value_update(struct config_tree_node *node, + struct sr_dev_inst *sdi, int channel) +{ + struct dev_context *devc = sdi->priv; + float value; + struct sr_datafeed_packet packet; + struct sr_datafeed_analog analog; + struct sr_analog_encoding encoding; + struct sr_analog_meaning meaning; + struct sr_analog_spec spec; + + if (!devc->enable_value_stream) + return; + + if (!((struct sr_channel *)devc->channel_meaning[channel]. + channels->data)->enabled) { + return; + } + + if (node->type != TREE_NODE_DATATYPE_FLOAT) + return; + value = node->value.f; + + sr_spew("Received value for channel %d = %g.", channel, value); + + /* + * Could do significant digit calculations based on the + * effective number of effective bits (sample rate, buffer size, etc), + * but does it matter? + * (see https://github.com/mooshim/Mooshimeter-AndroidApp/blob/94a20a2d42f6af9975ad48591caa6a17130ca53b/app/src/main/java/com/mooshim/mooshimeter/devices/MooshimeterDevice.java#L691 ) + */ + sr_analog_init(&analog, &encoding, &meaning, &spec, 2); + + memcpy(analog.meaning, &devc->channel_meaning[channel], + sizeof(struct sr_analog_meaning)); + analog.num_samples = 1; + analog.data = &value; + packet.type = SR_DF_ANALOG; + packet.payload = &analog; + sr_session_send(sdi, &packet); + + if (devc->channel_autorange[channel]) + (*devc->channel_autorange[channel])(sdi, value); + + sr_sw_limits_update_samples_read(&devc->limits, 1); + if (sr_sw_limits_check(&devc->limits)) + sr_dev_acquisition_stop(sdi); +} + +static void chX_buffer_update(struct config_tree_node *node, + struct sr_dev_inst *sdi, int channel) +{ + struct dev_context *devc = sdi->priv; + uint32_t bits_per_sample = devc->buffer_bps[channel]; + float output_scalar = devc->buffer_lsb2native[channel]; + uint32_t bytes_per_sample; + const uint8_t *raw; + size_t size; + size_t number_of_samples; + int32_t unscaled = 0; + int32_t sign_bit; + int32_t sign_mask; + float converted_value = 0; + float maximum_value = 0; + float *values; + float *output_value; + struct sr_datafeed_packet packet; + struct sr_datafeed_analog analog; + struct sr_analog_encoding encoding; + struct sr_analog_meaning meaning; + struct sr_analog_spec spec; + + if (!devc->enable_value_stream) + return; + + if (!((struct sr_channel *)devc->channel_meaning[channel]. + channels->data)->enabled) { + return; + } + + if (!bits_per_sample) + return; + if (node->type != TREE_NODE_DATATYPE_BINARY) + return; + raw = node->value.b->data; + size = node->value.b->len; + if (!size) + return; + + bytes_per_sample = bits_per_sample / 8; + if (bits_per_sample % 8 != 0) + bytes_per_sample++; + if (bytes_per_sample > 4) + return; + number_of_samples = size / bytes_per_sample; + if (!number_of_samples) + return; + + sr_analog_init(&analog, &encoding, &meaning, &spec, 0); + + values = g_new0(float, number_of_samples); + output_value = values; + + memcpy(analog.meaning, &devc->channel_meaning[channel], + sizeof(struct sr_analog_meaning)); + analog.num_samples = number_of_samples; + analog.data = output_value; + packet.type = SR_DF_ANALOG; + packet.payload = &analog; + + sr_spew("Received buffer for channel %d with %u bytes (%u samples).", + channel, (unsigned int)size, (unsigned int)number_of_samples); + + sign_bit = 1 << (bits_per_sample - 1); + sign_mask = sign_bit - 1; + for (; size >= bytes_per_sample; size -= bytes_per_sample, + raw += bytes_per_sample, output_value++) { + switch (bytes_per_sample) { + case 1: + unscaled = R8(raw); + break; + case 2: + unscaled = RL16(raw); + break; + case 3: + unscaled = ((uint32_t)raw[0]) | + (((uint32_t)raw[1]) << 8) | + (((uint32_t)raw[2]) << 16); + break; + case 4: + unscaled = RL32(raw); + break; + default: + break; + } + + unscaled = (unscaled & sign_mask) - (unscaled & sign_bit); + converted_value = (float)unscaled * output_scalar; + *output_value = converted_value; + if (fabsf(converted_value) > maximum_value) + maximum_value = fabsf(maximum_value); + } + + sr_session_send(sdi, &packet); + + g_free(values); + + if (devc->channel_autorange[channel]) + (*devc->channel_autorange[channel])(sdi, maximum_value); + + sr_sw_limits_update_samples_read(&devc->limits, number_of_samples); + if (sr_sw_limits_check(&devc->limits)) + sr_dev_acquisition_stop(sdi); +} + +static void ch1_value_update(struct config_tree_node *node, void *param) +{ + chX_value_update(node, param, 0); +} + +static void ch2_value_update(struct config_tree_node *node, void *param) +{ + chX_value_update(node, param, 1); +} + +static void power_value_update(struct config_tree_node *node, void *param) +{ + chX_value_update(node, param, 2); +} + +static void ch1_buffer_update(struct config_tree_node *node, void *param) +{ + chX_buffer_update(node, param, 0); +} + +static void ch2_buffer_update(struct config_tree_node *node, void *param) +{ + chX_buffer_update(node, param, 1); +} + +static void ch1_buffer_bps_update(struct config_tree_node *node, void *param) +{ + const struct sr_dev_inst *sdi = param; + struct dev_context *devc = sdi->priv; + devc->buffer_bps[0] = (uint32_t)get_tree_integer(node); +} + +static void ch2_buffer_bps_update(struct config_tree_node *node, void *param) +{ + const struct sr_dev_inst *sdi = param; + struct dev_context *devc = sdi->priv; + devc->buffer_bps[1] = (uint32_t)get_tree_integer(node); +} + +static void ch1_buffer_lsb2native_update(struct config_tree_node *node, + void *param) +{ + const struct sr_dev_inst *sdi = param; + struct dev_context *devc = sdi->priv; + if (node->type != TREE_NODE_DATATYPE_BINARY) + return; + devc->buffer_lsb2native[0] = node->value.f; +} + +static void ch2_buffer_lsb2native_update(struct config_tree_node *node, + void *param) +{ + const struct sr_dev_inst *sdi = param; + struct dev_context *devc = sdi->priv; + if (node->type != TREE_NODE_DATATYPE_BINARY) + return; + devc->buffer_lsb2native[1] = node->value.f; +} + +static void release_tree_node(struct config_tree_node *node) +{ + g_free(node->name); + + switch (node->type) { + case TREE_NODE_DATATYPE_STRING: + case TREE_NODE_DATATYPE_BINARY: + g_byte_array_free(node->value.b, TRUE); + break; + default: + break; + } + + for (size_t i = 0; i < node->count_children; i++) + release_tree_node(node->children + i); + g_free(node->children); +} + +static void allocate_startup_tree(struct dev_context *devc) +{ + struct config_tree_node *node; + + node = &devc->tree_root; + node->name = g_strdup("ADMIN"); + node->type = TREE_NODE_DATATYPE_PLAIN; + node->count_children = 3; + node->children = g_new0(struct config_tree_node, node->count_children); + + node = &devc->tree_root.children[0]; + node->name = g_strdup("CRC"); + node->type = TREE_NODE_DATATYPE_U32; + node->id = 0; + devc->tree_id_lookup[node->id] = node; + + node = &devc->tree_root.children[1]; + node->name = g_strdup("TREE"); + node->type = TREE_NODE_DATATYPE_BINARY; + node->value.b = g_byte_array_new(); + node->id = 1; + devc->tree_id_lookup[node->id] = node; + + node = &devc->tree_root.children[2]; + node->name = g_strdup("DIAGNOSTIC"); + node->type = TREE_NODE_DATATYPE_STRING; + node->value.b = g_byte_array_new(); + node->id = 2; + devc->tree_id_lookup[node->id] = node; +} + +static gboolean tree_node_has_id(struct config_tree_node *node) +{ + switch (node->type) { + case TREE_NODE_DATATYPE_PLAIN: + case TREE_NODE_DATATYPE_LINK: + return FALSE; + default: + break; + } + + return TRUE; +} + +static int deserialize_tree(struct dev_context *devc, + struct config_tree_node *node, + int *id, const uint8_t **data, size_t *size) +{ + size_t n; + int res; + + if (*size < 2) + return SR_ERR_DATA; + + n = R8(*data); + *data += 1; + *size -= 1; + if (n > TREE_NODE_DATATYPE_FLOAT) + return SR_ERR_DATA; + node->type = n; + + switch (node->type) { + case TREE_NODE_DATATYPE_STRING: + case TREE_NODE_DATATYPE_BINARY: + node->value.b = g_byte_array_new(); + break; + default: + break; + } + + n = R8(*data); + *data += 1; + *size -= 1; + if (n > *size) + return SR_ERR_DATA; + node->name = g_strndup((const char *)(*data), n); + *data += n; + *size -= n; + + if (!(*size)) + return SR_ERR_DATA; + + if (tree_node_has_id(node)) { + node->id = *id; + (*id)++; + devc->tree_id_lookup[node->id] = node; + } + + n = R8(*data); + *data += 1; + *size -= 1; + + if (n) { + node->count_children = n; + node->children = g_new0(struct config_tree_node, n); + + for (size_t i = 0; i < n; i++) { + if ((res = deserialize_tree(devc, + node->children + i, id, + data, size)) != SR_OK) { + return res; + } + node->children[i].index_in_parent = i; + } + } + + return SR_OK; +} + +static int wait_for_update(const struct sr_dev_inst *sdi, + struct config_tree_node *node, + uint32_t original_update_number) +{ + struct sr_bt_desc *desc = sdi->conn; + int ret; + gint64 start_time; + + start_time = g_get_monotonic_time(); + for (;;) { + ret = sr_bt_check_notify(desc); + if (ret < 0) + return SR_ERR; + + if (node->update_number != original_update_number) + return SR_OK; + + if (g_get_monotonic_time() - start_time > 5 * 1000 * 1000) + break; + + if (ret > 0) + continue; + + /* Nothing pollable, so just sleep a bit and try again. */ + g_usleep(50 * 1000); + } + + return SR_ERR_TIMEOUT; +} + +static void install_update_handlers(struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *target; + + target = lookup_tree_path(devc, "CH1:VALUE"); + if (target) { + target->on_update = ch1_value_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 1 values."); + } + + target = lookup_tree_path(devc, "CH1:BUF"); + if (target) { + target->on_update = ch1_buffer_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 1 buffer."); + } + + target = lookup_tree_path(devc, "CH1:BUF_BPS"); + if (target) { + target->on_update = ch1_buffer_bps_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 1 buffer BPS."); + } + + target = lookup_tree_path(devc, "CH1:BUF_LSB2NATIVE"); + if (target) { + target->on_update = ch1_buffer_lsb2native_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 1 buffer conversion factor."); + } + + target = lookup_tree_path(devc, "CH2:VALUE"); + if (target) { + target->on_update = ch2_value_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 2 values."); + } + + target = lookup_tree_path(devc, "CH2:BUF"); + if (target) { + target->on_update = ch2_buffer_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 2 buffer."); + } + + target = lookup_tree_path(devc, "CH2:BUF_BPS"); + if (target) { + target->on_update = ch2_buffer_bps_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 2 buffer BPS."); + } + + target = lookup_tree_path(devc, "CH2:BUF_LSB2NATIVE"); + if (target) { + target->on_update = ch2_buffer_lsb2native_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for channel 2 buffer conversion factor."); + } + + target = lookup_tree_path(devc, "REAL_PWR"); + if (target) { + target->on_update = power_value_update; + target->on_update_param = sdi; + } else { + sr_warn("No tree path for real power."); + } +} + +struct startup_context { + struct sr_dev_inst *sdi; + uint32_t crc; + int result; + gboolean running; +}; + +static void startup_failed(struct startup_context *ctx, int err) +{ + sr_dbg("Startup handshake failed: %s.", sr_strerror(err)); + + ctx->result = err; + ctx->running = FALSE; +} + +static void startup_complete(struct startup_context *ctx) +{ + sr_dbg("Startup handshake completed."); + + install_update_handlers(ctx->sdi); + + ctx->running = FALSE; +} + +static int startup_run(struct startup_context *ctx) +{ + struct sr_bt_desc *desc = ctx->sdi->conn; + int ret; + gint64 start_time; + + ctx->result = SR_OK; + ctx->running = TRUE; + + start_time = g_get_monotonic_time(); + for (;;) { + ret = sr_bt_check_notify(desc); + if (ret < 0) + return SR_ERR; + + if (!ctx->running) + return ctx->result; + + if (g_get_monotonic_time() - start_time > 30 * 1000 * 1000) + break; + + if (ret > 0) + continue; + + /* Nothing pollable, so just sleep a bit and try again. */ + g_usleep(50 * 1000); + } + + return SR_ERR_TIMEOUT; +} + +static void startup_tree_crc_updated(struct config_tree_node *node, void *param) +{ + struct startup_context *ctx = param; + uint32_t result; + + node->on_update = NULL; + + result = (uint32_t)get_tree_integer(node); + if (result != ctx->crc) { + sr_err("Tree CRC mismatch, expected %08X but received %08X.", + ctx->crc, result); + startup_failed(ctx, SR_ERR_DATA); + return; + } + + startup_complete(ctx); +} + +static void startup_send_tree_crc(struct startup_context *ctx) +{ + struct dev_context *devc = ctx->sdi->priv; + struct config_tree_node *target; + + if (!(target = lookup_tree_path(devc, "ADMIN:CRC32"))) { + sr_err("ADMIN:CRC32 node not found in received startup tree."); + startup_failed(ctx, SR_ERR_DATA); + return; + } + + target->on_update = startup_tree_crc_updated; + target->on_update_param = ctx; + + set_tree_integer(ctx->sdi, target, ctx->crc); +} + +static uint32_t crc32(const uint8_t *ptr, size_t size) +{ + uint32_t result = 0xFFFFFFFF; + uint32_t t; + for (; size; size--, ptr++) { + result ^= *ptr; + for (int i = 0; i < 8; i++) { + t = result & 1; + result >>= 1; + if (t) + result ^= 0xEDB88320; + } + } + + return ~result; +} + +static void startup_tree_updated(struct config_tree_node *node, void *param) +{ + struct startup_context *ctx = param; + struct dev_context *devc = ctx->sdi->priv; + + GConverter *decompressor; + GConverterResult decompress_result; + GByteArray *tree_data; + gsize input_read; + gsize output_size; + GError *err = NULL; + int res; + int id; + const uint8_t *data; + size_t size; + struct config_tree_node *target; + + ctx->crc = crc32(node->value.b->data, node->value.b->len); + + tree_data = g_byte_array_new(); + g_byte_array_set_size(tree_data, 4096); + decompressor = (GConverter *)g_zlib_decompressor_new( + G_ZLIB_COMPRESSOR_FORMAT_ZLIB); + for (;;) { + g_converter_reset(decompressor); + decompress_result = g_converter_convert(decompressor, + node->value.b->data, + node->value.b->len, + tree_data->data, + tree_data->len, + G_CONVERTER_INPUT_AT_END, + &input_read, + &output_size, + &err); + if (decompress_result == G_CONVERTER_FINISHED) + break; + if (decompress_result == G_CONVERTER_ERROR) { + if (err->code == G_IO_ERROR_NO_SPACE && + tree_data->len < 1024 * 1024) { + g_byte_array_set_size(tree_data, + tree_data->len * 2); + continue; + } + sr_err("Tree decompression failed: %s.", err->message); + } else { + sr_err("Tree decompression error %d.", + (int)decompress_result); + } + startup_failed(ctx, SR_ERR_DATA); + return; + } + g_object_unref(decompressor); + + sr_dbg("Config tree received (%d -> %d bytes) with CRC %08X.", + node->value.b->len, (int)output_size, + ctx->crc); + + release_tree_node(&devc->tree_root); + memset(&devc->tree_root, 0, sizeof(struct config_tree_node)); + memset(devc->tree_id_lookup, 0, sizeof(devc->tree_id_lookup)); + + id = 0; + data = tree_data->data; + size = output_size; + res = deserialize_tree(devc, &devc->tree_root, &id, &data, &size); + g_byte_array_free(tree_data, TRUE); + + if (res != SR_OK) { + sr_err("Tree deserialization failed."); + startup_failed(ctx, res); + return; + } + + if ((target = lookup_tree_path(devc, "ADMIN:DIAGNOSTIC"))) { + target->on_update = tree_diagnostic_updated; + target->on_update_param = ctx->sdi; + } + + startup_send_tree_crc(ctx); +} + +static void release_rx_buffer(void *data) +{ + GByteArray *ba = data; + if (!ba) + return; + g_byte_array_free(ba, TRUE); +} + +SR_PRIV int mooshimeter_dmm_open(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + struct sr_bt_desc *desc = sdi->conn; + struct startup_context ctx; + int ret; + + release_tree_node(&devc->tree_root); + memset(&devc->tree_root, 0, sizeof(struct config_tree_node)); + memset(devc->tree_id_lookup, 0, sizeof(devc->tree_id_lookup)); + + g_slist_free_full(devc->rx.reorder_buffer, release_rx_buffer); + devc->rx.reorder_buffer = NULL; + if (devc->rx.contents) + devc->rx.contents->len = 0; + else + devc->rx.contents = g_byte_array_new(); + devc->rx.sequence_number = -1; + devc->tx.sequence_number = 0; + + ret = sr_bt_config_cb_data(desc, notify_cb, (void *)sdi); + if (ret < 0) + return SR_ERR; + + ret = sr_bt_connect_ble(desc); + if (ret < 0) + return SR_ERR; + + ret = sr_bt_start_notify(desc); + if (ret < 0) + return SR_ERR; + + memset(&ctx, 0, sizeof(ctx)); + ctx.sdi = (struct sr_dev_inst *)sdi; + + allocate_startup_tree(devc); + devc->tree_id_lookup[1]->on_update = startup_tree_updated; + devc->tree_id_lookup[1]->on_update_param = &ctx; + devc->tree_id_lookup[2]->on_update = tree_diagnostic_updated; + devc->tree_id_lookup[2]->on_update_param = (struct sr_dev_inst *)sdi; + + sr_spew("Initiating startup handshake."); + + ret = poll_tree_value(sdi, devc->tree_id_lookup[1]); + if (ret != SR_OK) + return ret; + + return startup_run(&ctx); +} + +SR_PRIV int mooshimeter_dmm_close(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + struct sr_bt_desc *desc = sdi->conn; + + sr_bt_disconnect(desc); + + release_tree_node(&devc->tree_root); + memset(&devc->tree_root, 0, sizeof(struct config_tree_node)); + memset(devc->tree_id_lookup, 0, sizeof(devc->tree_id_lookup)); + + g_slist_free_full(devc->rx.reorder_buffer, release_rx_buffer); + devc->rx.reorder_buffer = NULL; + if (devc->rx.contents) + g_byte_array_free(devc->rx.contents, TRUE); + devc->rx.contents = NULL; + + return SR_OK; +} + +SR_PRIV int mooshimeter_dmm_set_chooser(const struct sr_dev_inst *sdi, + const char *path, const char *choice) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *target; + int value; + uint32_t original_update_number; + + value = lookup_chooser_index(devc, choice); + if (value == -1) { + sr_err("Value %s not found for chooser %s.", choice, path); + return SR_ERR_DATA; + } + + target = lookup_tree_path(devc, path); + if (!target) { + sr_err("Tree path %s not found.", path); + return SR_ERR_DATA; + } + + sr_spew("Setting chooser %s to %s (%d).", path, choice, value); + + original_update_number = target->update_number; + set_tree_integer(sdi, target, value); + return wait_for_update(sdi, target, original_update_number); +} + +SR_PRIV int mooshimeter_dmm_set_integer(const struct sr_dev_inst *sdi, + const char *path, int value) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *target; + uint32_t original_update_number; + + target = lookup_tree_path(devc, path); + if (!target) { + sr_err("Tree path %s not found.", path); + return SR_ERR_DATA; + } + + sr_spew("Setting integer %s to %d.", path, value); + + original_update_number = target->update_number; + set_tree_integer(sdi, target, value); + return wait_for_update(sdi, target, original_update_number); +} + +static struct config_tree_node *select_next_largest_in_tree( + struct dev_context *devc, + const char *parent, float number) +{ + float node_value; + float distance; + float best_distance = 0; + struct config_tree_node *choice_parent; + struct config_tree_node *selected_choice = NULL; + + choice_parent = lookup_tree_path(devc, parent); + if (!choice_parent) { + sr_err("Tree path %s not found.", parent); + return NULL; + } + if (!choice_parent->count_children) { + sr_err("Tree path %s has no children.", parent); + return NULL; + } + + for (size_t i = 0; i < choice_parent->count_children; i++) { + node_value = strtof(choice_parent->children[i].name, NULL); + if (node_value <= 0) + continue; + distance = node_value - number; + if (!selected_choice) { + selected_choice = &choice_parent->children[i]; + best_distance = distance; + continue; + } + /* Select the one that's the least below it, if all + * are below the target */ + if (distance < 0) { + if (best_distance > 0) + continue; + if (distance > best_distance) { + selected_choice = &choice_parent->children[i]; + best_distance = distance; + } + continue; + } + if (best_distance < 0 || distance < best_distance) { + selected_choice = &choice_parent->children[i]; + best_distance = distance; + } + } + + return selected_choice; +} + +SR_PRIV int mooshimeter_dmm_set_larger_number(const struct sr_dev_inst *sdi, + const char *path, const char *parent, float number) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *selected_choice; + struct config_tree_node *target; + uint32_t original_update_number; + + selected_choice = select_next_largest_in_tree(devc, parent, number); + if (!selected_choice) { + sr_err("No choice available for %f at %s.", number, parent); + return SR_ERR_NA; + } + + target = lookup_tree_path(devc, path); + if (!target) { + sr_err("Tree path %s not found.", path); + return SR_ERR_DATA; + } + + sr_spew("Setting number choice %s to index %d for requested %g.", path, + (int)selected_choice->index_in_parent, number); + + original_update_number = target->update_number; + set_tree_integer(sdi, target, selected_choice->index_in_parent); + return wait_for_update(sdi, target, original_update_number); +} + +SR_PRIV gboolean mooshimeter_dmm_set_autorange(const struct sr_dev_inst *sdi, + const char *path, const char *parent, float latest) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *selected_choice; + struct config_tree_node *target; + + selected_choice = select_next_largest_in_tree(devc, parent, + fabsf(latest)); + if (!selected_choice) { + sr_err("No choice available for %f at %s.", latest, parent); + return FALSE; + } + + target = lookup_tree_path(devc, path); + if (!target) { + sr_err("Tree path %s not found.", path); + return FALSE; + } + + if (get_tree_integer(target) == (int)selected_choice->index_in_parent) + return FALSE; + + sr_spew("Changing autorange %s to index %d for %g.", path, + (int)selected_choice->index_in_parent, latest); + + set_tree_integer(sdi, target, selected_choice->index_in_parent); + + return TRUE; +} + +SR_PRIV int mooshimeter_dmm_get_chosen_number(const struct sr_dev_inst *sdi, + const char *path, const char *parent, float *number) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *value_node; + struct config_tree_node *available; + int32_t selected; + + value_node = lookup_tree_path(devc, path); + if (!value_node) { + sr_err("Tree path %s not found.", path); + return SR_ERR_DATA; + } + + available = lookup_tree_path(devc, parent); + if (!available) { + sr_err("Tree path %s not found.", path); + return SR_ERR_DATA; + } + + selected = get_tree_integer(value_node); + if (selected < 0 || selected >= (int32_t)available->count_children) + return SR_ERR_DATA; + + *number = g_ascii_strtod(available->children[selected].name, NULL); + + return SR_OK; +} + +SR_PRIV int mooshimeter_dmm_get_available_number_choices( + const struct sr_dev_inst *sdi, const char *path, + float **numbers, size_t *count) +{ + struct dev_context *devc = sdi->priv; + struct config_tree_node *available; + + available = lookup_tree_path(devc, path); + if (!available) { + sr_err("Tree path %s not found.", path); + return SR_ERR_NA; + } + + *numbers = g_malloc(sizeof(float) * available->count_children); + *count = available->count_children; + + for (size_t i = 0; i < available->count_children; i++) { + (*numbers)[i] = g_ascii_strtod(available->children[i].name, + NULL); + } + + return SR_OK; +} + +SR_PRIV int mooshimeter_dmm_poll(int fd, int revents, void *cb_data) +{ + struct sr_dev_inst *sdi; + struct sr_bt_desc *desc; + + (void)fd; + (void)revents; + + if (!(sdi = cb_data)) + return TRUE; + + desc = sdi->conn; + + while (sr_bt_check_notify(desc) > 0); + + return TRUE; +} + +/* + * The meter will disconnect if it doesn't receive a host command for 30 (?) + * seconds, so periodically poll a trivial value to keep it alive. + */ +SR_PRIV int mooshimeter_dmm_heartbeat(int fd, int revents, void *cb_data) +{ + struct sr_dev_inst *sdi; + struct dev_context *devc; + struct config_tree_node *target; + + (void)fd; + (void)revents; + + if (!(sdi = cb_data)) + return TRUE; + + if (!(devc = sdi->priv)) + return TRUE; + + target = lookup_tree_path(devc, "PCB_VERSION"); + if (!target) { + sr_err("Tree for PCB_VERSION not found."); + return FALSE; + } + + sr_spew("Sending heartbeat request."); + poll_tree_value(sdi, target); + + return TRUE; +} diff --git a/src/hardware/mooshimeter-dmm/protocol.h b/src/hardware/mooshimeter-dmm/protocol.h new file mode 100644 index 00000000..9670a8cb --- /dev/null +++ b/src/hardware/mooshimeter-dmm/protocol.h @@ -0,0 +1,104 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2019 Derek Hageman + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIBSIGROK_HARDWARE_MOOSHIMETER_DMM_PROTOCOL_H +#define LIBSIGROK_HARDWARE_MOOSHIMETER_DMM_PROTOCOL_H + +#include +#include +#include +#include "libsigrok-internal.h" + +#define LOG_PREFIX "mooshimeter-dmm" + +struct packet_rx { + int sequence_number; + GSList *reorder_buffer; + GByteArray *contents; +}; + +struct packet_tx { + int sequence_number; +}; + +enum tree_node_datatype { + TREE_NODE_DATATYPE_PLAIN = 0, + TREE_NODE_DATATYPE_LINK, + TREE_NODE_DATATYPE_CHOOSER, + TREE_NODE_DATATYPE_U8, + TREE_NODE_DATATYPE_U16, + TREE_NODE_DATATYPE_U32, + TREE_NODE_DATATYPE_S8, + TREE_NODE_DATATYPE_S16, + TREE_NODE_DATATYPE_S32, + TREE_NODE_DATATYPE_STRING, + TREE_NODE_DATATYPE_BINARY, + TREE_NODE_DATATYPE_FLOAT, +}; + +union tree_value { + int32_t i; + float f; + GByteArray *b; +}; + +struct config_tree_node { + char *name; + int id; + size_t index_in_parent; + + enum tree_node_datatype type; + union tree_value value; + + size_t count_children; + struct config_tree_node *children; + + uint32_t update_number; + void (*on_update)(struct config_tree_node *node, void *param); + void *on_update_param; +}; + +struct dev_context { + struct packet_rx rx; + struct packet_tx tx; + struct config_tree_node tree_root; + struct config_tree_node *tree_id_lookup[0x7F]; + uint32_t buffer_bps[2]; + float buffer_lsb2native[2]; + + void (*channel_autorange[3])(const struct sr_dev_inst *sdi, float value); + + struct sr_sw_limits limits; + struct sr_analog_meaning channel_meaning[3]; + + gboolean enable_value_stream; +}; + +SR_PRIV int mooshimeter_dmm_open(const struct sr_dev_inst *sdi); +SR_PRIV int mooshimeter_dmm_close(const struct sr_dev_inst *sdi); +SR_PRIV int mooshimeter_dmm_set_chooser(const struct sr_dev_inst *sdi, const char *path, const char *choice); +SR_PRIV int mooshimeter_dmm_set_integer(const struct sr_dev_inst *sdi, const char *path, int value); +SR_PRIV int mooshimeter_dmm_set_larger_number(const struct sr_dev_inst *sdi, const char *path, const char *parent, float number); +SR_PRIV gboolean mooshimeter_dmm_set_autorange(const struct sr_dev_inst *sdi, const char *path, const char *parent, float latest); +SR_PRIV int mooshimeter_dmm_get_chosen_number(const struct sr_dev_inst *sdi, const char *path, const char *parent, float *number); +SR_PRIV int mooshimeter_dmm_get_available_number_choices(const struct sr_dev_inst *sdi, const char *path, float **numbers, size_t *count); +SR_PRIV int mooshimeter_dmm_poll(int fd, int revents, void *cb_data); +SR_PRIV int mooshimeter_dmm_heartbeat(int fd, int revents, void *cb_data); + +#endif