]> sigrok.org Git - libsigrok.git/commitdiff
rdtech-tc: Add initial support for the RDTech TC66C
authorAndreas Sandberg <redacted>
Fri, 6 Mar 2020 14:44:41 +0000 (14:44 +0000)
committerUwe Hermann <redacted>
Thu, 4 Jun 2020 22:25:26 +0000 (00:25 +0200)
This changeset adds support for the RDTech TC66C USB power meter.

Currently, the driver reports the following channels:
  * V: VBus voltage
  * I: VBus current
  * D+: D+ voltage
  * D-: D- voltage
  * E: Energy consumed in threshold-based recording mode.

The number of significant digits shown for each channel has been set
to match the number of digits shown on the device.

Usage example:

sigrok-cli -d rdtech-tc:conn=/dev/ttyACM0 --scan

Known issues:

  * BLE support is currently unimplemented. This uses a different
    command set, but the same poll data format.

Kudos to Ben V. Brown for reverse engineering some of the protocol and
documenting the encryption key used for poll data.

Signed-off-by: Andreas Sandberg <redacted>
Makefile.am
configure.ac
src/hardware/rdtech-tc/api.c [new file with mode: 0644]
src/hardware/rdtech-tc/protocol.c [new file with mode: 0644]
src/hardware/rdtech-tc/protocol.h [new file with mode: 0644]

index 8eb3d7a5ffc535e08f97fb53de3bd274164848a9..8071aac47c6bfe41e021854e89b735abb5d08fe3 100644 (file)
@@ -521,6 +521,12 @@ src_libdrivers_la_SOURCES += \
        src/hardware/rdtech-um/protocol.c \
        src/hardware/rdtech-um/api.c
 endif
+if HW_RDTECH_TC
+src_libdrivers_la_SOURCES += \
+       src/hardware/rdtech-tc/protocol.h \
+       src/hardware/rdtech-tc/protocol.c \
+       src/hardware/rdtech-tc/api.c
+endif
 if HW_RIGOL_DS
 src_libdrivers_la_SOURCES += \
        src/hardware/rigol-ds/protocol.h \
index abd8d056affbb877583986e0a20aa475477ab786..2516617373c94feb680506c8ec5c69c12715eb08 100644 (file)
@@ -107,6 +107,8 @@ SR_ARG_OPT_PKG([libhidapi], [LIBHIDAPI], ,
 
 SR_ARG_OPT_PKG([libbluez], [LIBBLUEZ], , [bluez >= 4.0])
 
+SR_ARG_OPT_PKG([libnettle], [LIBNETTLE], , [nettle])
+
 # FreeBSD comes with an "integrated" libusb-1.0-style USB API.
 # This means libusb-1.0 is always available; no need to check for it.
 # On Windows, require the latest version we can get our hands on,
@@ -303,6 +305,7 @@ SR_DRIVER([PCE PCE-322A], [pce-322a], [serial_comm])
 SR_DRIVER([Pipistrello-OLS], [pipistrello-ols], [libftdi])
 SR_DRIVER([RDTech DPSxxxx/DPHxxxx], [rdtech-dps], [serial_comm])
 SR_DRIVER([RDTech UMXX], [rdtech-um], [serial_comm])
+SR_DRIVER([RDTech TCXX], [rdtech-tc], [serial_comm libnettle])
 SR_DRIVER([Rigol DS], [rigol-ds])
 SR_DRIVER([Rohde&Schwarz SME-0x], [rohde-schwarz-sme-0x], [serial_comm])
 SR_DRIVER([Saleae Logic16], [saleae-logic16], [libusb])
diff --git a/src/hardware/rdtech-tc/api.c b/src/hardware/rdtech-tc/api.c
new file mode 100644 (file)
index 0000000..eacd17d
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2020 Andreas Sandberg <andreas@sandberg.pp.se>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <string.h>
+#include <libsigrok/libsigrok.h>
+#include "libsigrok-internal.h"
+#include "protocol.h"
+
+#define RDTECH_TC_SERIALCOMM "115200/8n1"
+
+static const uint32_t scanopts[] = {
+       SR_CONF_CONN,
+       SR_CONF_SERIALCOMM,
+};
+
+static const uint32_t drvopts[] = {
+       SR_CONF_ENERGYMETER,
+};
+
+static const uint32_t devopts[] = {
+       SR_CONF_CONTINUOUS,
+       SR_CONF_LIMIT_SAMPLES | SR_CONF_SET,
+       SR_CONF_LIMIT_MSEC | SR_CONF_SET,
+};
+
+static GSList *rdtech_tc_scan(struct sr_dev_driver *di, const char *conn,
+                             const char *serialcomm)
+{
+       struct sr_serial_dev_inst *serial;
+       GSList *devices = NULL;
+       struct dev_context *devc = NULL;
+       struct sr_dev_inst *sdi = NULL;
+
+       serial = sr_serial_dev_inst_new(conn, serialcomm);
+       if (serial_open(serial, SERIAL_RDWR) != SR_OK)
+               goto err_out;
+
+       devc = g_malloc0(sizeof(struct dev_context));
+       sr_sw_limits_init(&devc->limits);
+
+       if (rdtech_tc_probe(serial, devc) != SR_OK) {
+               sr_err("Failed to find a supported RDTech TC device.");
+               goto err_out_serial;
+       }
+
+       sdi = g_malloc0(sizeof(struct sr_dev_inst));
+       sdi->status = SR_ST_INACTIVE;
+       sdi->vendor = g_strdup("RDTech");
+       sdi->model = g_strdup(devc->dev_info.model_name);
+       sdi->version = g_strdup(devc->dev_info.fw_ver);
+       sdi->serial_num = g_strdup_printf("%08" PRIu32, devc->dev_info.serial_num);
+       sdi->inst_type = SR_INST_SERIAL;
+       sdi->conn = serial;
+       sdi->priv = devc;
+
+       for (int i = 0; devc->channels[i].name; i++)
+               sr_channel_new(sdi, i, SR_CHANNEL_ANALOG, TRUE, devc->channels[i].name);
+
+       devices = g_slist_append(devices, sdi);
+       serial_close(serial);
+       if (!devices)
+               sr_serial_dev_inst_free(serial);
+
+       return std_scan_complete(di, devices);
+
+err_out_serial:
+       g_free(devc);
+       serial_close(serial);
+err_out:
+       sr_serial_dev_inst_free(serial);
+
+       return NULL;
+}
+
+static GSList *scan(struct sr_dev_driver *di, GSList *options)
+{
+       struct sr_config *src;
+       const char *conn = NULL;
+       const char *serialcomm = RDTECH_TC_SERIALCOMM;
+
+       for (GSList *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;
+               case SR_CONF_SERIALCOMM:
+                       serialcomm = g_variant_get_string(src->data, NULL);
+                       break;
+               }
+       }
+       if (!conn)
+               return NULL;
+
+       return rdtech_tc_scan(di, conn, serialcomm);
+}
+
+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;
+
+       (void)cg;
+
+       devc = sdi->priv;
+
+       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)
+{
+       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;
+       struct sr_serial_dev_inst *serial = sdi->conn;
+
+       sr_sw_limits_acquisition_start(&devc->limits);
+       std_session_send_df_header(sdi);
+
+       serial_source_add(sdi->session, serial, G_IO_IN, 50,
+                         rdtech_tc_receive_data, (void *)sdi);
+
+       return rdtech_tc_poll(sdi);
+}
+
+static struct sr_dev_driver rdtech_tc_driver_info = {
+       .name = "rdtech-tc",
+       .longname = "RDTech TC66C USB power meter",
+       .api_version = 1,
+       .init = std_init,
+       .cleanup = std_cleanup,
+       .scan = scan,
+       .dev_list = std_dev_list,
+       .dev_clear = std_dev_clear,
+       .config_get = NULL,
+       .config_set = config_set,
+       .config_list = config_list,
+       .dev_open = std_serial_dev_open,
+       .dev_close = std_serial_dev_close,
+       .dev_acquisition_start = dev_acquisition_start,
+       .dev_acquisition_stop = std_serial_dev_acquisition_stop,
+       .context = NULL,
+};
+SR_REGISTER_DEV_DRIVER(rdtech_tc_driver_info);
diff --git a/src/hardware/rdtech-tc/protocol.c b/src/hardware/rdtech-tc/protocol.c
new file mode 100644 (file)
index 0000000..456681e
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2020 Andreas Sandberg <andreas@sandberg.pp.se>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdlib.h>
+#include <math.h>
+#include <string.h>
+#include <glib.h>
+#include <nettle/aes.h>
+#include <libsigrok/libsigrok.h>
+#include "libsigrok-internal.h"
+#include "protocol.h"
+
+#define SERIAL_WRITE_TIMEOUT_MS 1
+
+#define TC_POLL_LEN 192
+#define TC_POLL_PERIOD_MS 100
+#define TC_TIMEOUT_MS 1000
+
+static const char POLL_CMD[] = "getva";
+
+#define MAGIC_PAC1 0x31636170UL
+#define MAGIC_PAC2 0x32636170UL
+#define MAGIC_PAC3 0x33636170UL
+
+/* Length of PAC block excluding CRC */
+#define PAC_DATA_LEN 60
+/* Length of PAC block including CRC */
+#define PAC_LEN 64
+
+/* Offset to PAC block from start of poll data */
+#define OFF_PAC1 (0 * PAC_LEN)
+#define OFF_PAC2 (1 * PAC_LEN)
+#define OFF_PAC3 (2 * PAC_LEN)
+
+#define OFF_MODEL 4
+#define LEN_MODEL 4
+
+#define OFF_FW_VER 8
+#define LEN_FW_VER 4
+
+#define OFF_SERIAL 12
+
+static const uint8_t AES_KEY[] = {
+       0x58, 0x21, 0xfa, 0x56, 0x01, 0xb2, 0xf0, 0x26,
+       0x87, 0xff, 0x12, 0x04, 0x62, 0x2a, 0x4f, 0xb0,
+       0x86, 0xf4, 0x02, 0x60, 0x81, 0x6f, 0x9a, 0x0b,
+       0xa7, 0xf1, 0x06, 0x61, 0x9a, 0xb8, 0x72, 0x88,
+};
+
+static const struct binary_analog_channel rdtech_tc_channels[] = {
+       { "V",  {   0 + 48, BVT_LE_UINT32, 1e-4, }, 4, SR_MQ_VOLTAGE, SR_UNIT_VOLT },
+       { "I",  {   0 + 52, BVT_LE_UINT32, 1e-5, }, 5, SR_MQ_CURRENT, SR_UNIT_AMPERE },
+       { "D+", {  64 + 32, BVT_LE_UINT32, 1e-2, }, 2, SR_MQ_VOLTAGE, SR_UNIT_VOLT },
+       { "D-", {  64 + 36, BVT_LE_UINT32, 1e-2, }, 2, SR_MQ_VOLTAGE, SR_UNIT_VOLT },
+       { "E0", {  64 + 12, BVT_LE_UINT32, 1e-3, }, 3, SR_MQ_ENERGY, SR_UNIT_WATT_HOUR },
+       { "E1", {  64 + 20, BVT_LE_UINT32, 1e-3, }, 3, SR_MQ_ENERGY, SR_UNIT_WATT_HOUR },
+       { NULL, },
+};
+
+static int check_pac_crc(uint8_t *data)
+{
+       uint16_t crc;
+       uint32_t crc_field;
+
+       crc = sr_crc16(SR_CRC16_DEFAULT_INIT, data, PAC_DATA_LEN);
+       crc_field = RL32(data + PAC_DATA_LEN);
+
+       if (crc != crc_field) {
+               sr_spew("CRC error. Calculated: %0x" PRIx16 ", expected: %0x" PRIx32,
+                       crc, crc_field);
+               return 0;
+       } else {
+               return 1;
+       }
+}
+
+static int process_poll_pkt(struct dev_context  *devc, uint8_t *dst)
+{
+       struct aes256_ctx ctx;
+
+       aes256_set_decrypt_key(&ctx, AES_KEY);
+       aes256_decrypt(&ctx, TC_POLL_LEN, dst, devc->buf);
+
+       if (RL32(dst + OFF_PAC1) != MAGIC_PAC1 ||
+           RL32(dst + OFF_PAC2) != MAGIC_PAC2 ||
+           RL32(dst + OFF_PAC3) != MAGIC_PAC3) {
+               sr_err("Invalid poll packet magic values!");
+               return SR_ERR;
+       }
+
+       if (!check_pac_crc(dst + OFF_PAC1) ||
+           !check_pac_crc(dst + OFF_PAC2) ||
+           !check_pac_crc(dst + OFF_PAC3)) {
+               sr_err("Invalid poll checksum!");
+               return SR_ERR;
+       }
+
+       return SR_OK;
+}
+
+SR_PRIV int rdtech_tc_probe(struct sr_serial_dev_inst *serial, struct dev_context  *devc)
+{
+       int len;
+       uint8_t poll_pkt[TC_POLL_LEN];
+
+       if (serial_write_blocking(serial, &POLL_CMD, sizeof(POLL_CMD) - 1,
+                                  SERIAL_WRITE_TIMEOUT_MS) < 0) {
+               sr_err("Unable to send probe request.");
+               return SR_ERR;
+       }
+
+       len = serial_read_blocking(serial, devc->buf, TC_POLL_LEN, TC_TIMEOUT_MS);
+       if (len != TC_POLL_LEN) {
+               sr_err("Failed to read probe response.");
+               return SR_ERR;
+       }
+
+       if (process_poll_pkt(devc, poll_pkt) != SR_OK) {
+               sr_err("Unrecognized TC device!");
+               return SR_ERR;
+       }
+
+       devc->channels = rdtech_tc_channels;
+       devc->dev_info.model_name = g_strndup((const char *)poll_pkt + OFF_MODEL, LEN_MODEL);
+       devc->dev_info.fw_ver = g_strndup((const char *)poll_pkt + OFF_FW_VER, LEN_FW_VER);
+       devc->dev_info.serial_num = RL32(poll_pkt + OFF_SERIAL);
+
+       return SR_OK;
+}
+
+SR_PRIV int rdtech_tc_poll(const struct sr_dev_inst *sdi)
+{
+       struct dev_context *devc = sdi->priv;
+       struct sr_serial_dev_inst *serial = sdi->conn;
+
+       if (serial_write_blocking(serial, &POLL_CMD, sizeof(POLL_CMD) - 1,
+                                  SERIAL_WRITE_TIMEOUT_MS) < 0) {
+               sr_err("Unable to send poll request.");
+               return SR_ERR;
+       }
+
+       devc->cmd_sent_at = g_get_monotonic_time() / 1000;
+
+       return SR_OK;
+}
+
+static void handle_poll_data(const struct sr_dev_inst *sdi)
+{
+       struct dev_context *devc = sdi->priv;
+       uint8_t poll_pkt[TC_POLL_LEN];
+       int i;
+       GSList *ch;
+
+       sr_spew("Received poll packet (len: %d).", devc->buflen);
+       if (devc->buflen != TC_POLL_LEN) {
+               sr_err("Unexpected poll packet length: %i", devc->buflen);
+               return;
+       }
+
+       if (process_poll_pkt(devc, poll_pkt) != SR_OK) {
+               sr_err("Failed to process poll packet.");
+               return;
+       }
+
+       for (ch = sdi->channels, i = 0; ch; ch = g_slist_next(ch), i++) {
+               bv_send_analog_channel(sdi, ch->data,
+                                      &devc->channels[i], poll_pkt, TC_POLL_LEN);
+        }
+
+       sr_sw_limits_update_samples_read(&devc->limits, 1);
+}
+
+static void recv_poll_data(struct sr_dev_inst *sdi, struct sr_serial_dev_inst *serial)
+{
+       struct dev_context *devc = sdi->priv;
+       int len;
+
+       /* Serial data arrived. */
+       while (devc->buflen < TC_POLL_LEN) {
+               len = serial_read_nonblocking(serial, devc->buf + devc->buflen, 1);
+               if (len < 1)
+                       return;
+
+               devc->buflen++;
+       }
+
+       if (devc->buflen == TC_POLL_LEN)
+               handle_poll_data(sdi);
+
+       devc->buflen = 0;
+}
+
+SR_PRIV int rdtech_tc_receive_data(int fd, int revents, void *cb_data)
+{
+       struct sr_dev_inst *sdi;
+       struct dev_context *devc;
+       struct sr_serial_dev_inst *serial;
+       int64_t now, elapsed;
+
+       (void)fd;
+
+       if (!(sdi = cb_data))
+               return TRUE;
+
+       if (!(devc = sdi->priv))
+               return TRUE;
+
+       serial = sdi->conn;
+       if (revents == G_IO_IN)
+               recv_poll_data(sdi, serial);
+
+       if (sr_sw_limits_check(&devc->limits)) {
+               sr_dev_acquisition_stop(sdi);
+               return TRUE;
+       }
+
+       now = g_get_monotonic_time() / 1000;
+       elapsed = now - devc->cmd_sent_at;
+
+       if (elapsed > TC_POLL_PERIOD_MS)
+               rdtech_tc_poll(sdi);
+
+       return TRUE;
+}
diff --git a/src/hardware/rdtech-tc/protocol.h b/src/hardware/rdtech-tc/protocol.h
new file mode 100644 (file)
index 0000000..963c776
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2020 Andreas Sandberg <andreas@sandberg.pp.se>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBSIGROK_HARDWARE_RDTECH_TC_PROTOCOL_H
+#define LIBSIGROK_HARDWARE_RDTECH_TC_PROTOCOL_H
+
+#include <libsigrok/libsigrok.h>
+#include "libsigrok-internal.h"
+
+#define LOG_PREFIX "rdtech-tc"
+
+#define RDTECH_TC_BUFSIZE 256
+
+struct rdtech_dev_info {
+       char *model_name;
+       char *fw_ver;
+       uint32_t serial_num;
+};
+
+struct dev_context {
+       struct rdtech_dev_info dev_info;
+       const struct binary_analog_channel *channels;
+       struct sr_sw_limits limits;
+
+       uint8_t buf[RDTECH_TC_BUFSIZE];
+       int buflen;
+       int64_t cmd_sent_at;
+};
+
+SR_PRIV int rdtech_tc_probe(struct sr_serial_dev_inst *serial, struct dev_context  *devc);
+SR_PRIV int rdtech_tc_receive_data(int fd, int revents, void *cb_data);
+SR_PRIV int rdtech_tc_poll(const struct sr_dev_inst *sdi);
+
+#endif