diff --git a/Makefile.am b/Makefile.am index 8eb3d7a5..8071aac4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/configure.ac b/configure.ac index abd8d056..25166173 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 00000000..eacd17d3 --- /dev/null +++ b/src/hardware/rdtech-tc/api.c @@ -0,0 +1,169 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2020 Andreas Sandberg + * + * 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 +#include +#include +#include +#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 index 00000000..456681ea --- /dev/null +++ b/src/hardware/rdtech-tc/protocol.c @@ -0,0 +1,241 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2020 Andreas Sandberg + * + * 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 +#include +#include +#include +#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 index 00000000..963c7765 --- /dev/null +++ b/src/hardware/rdtech-tc/protocol.h @@ -0,0 +1,50 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2020 Andreas Sandberg + * + * 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_RDTECH_TC_PROTOCOL_H +#define LIBSIGROK_HARDWARE_RDTECH_TC_PROTOCOL_H + +#include +#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