dcttech-usbrelay: implement multiplexer driver for USB relay card
authorGerhard Sittig <gerhard.sittig@gmx.net>
Wed, 21 Jul 2021 18:30:29 +0000 (20:30 +0200)
committerGerhard Sittig <gerhard.sittig@gmx.net>
Wed, 21 Jul 2021 20:03:18 +0000 (22:03 +0200)
Implement support for the "www.dcttech.com USBRelay<n>" card. This V-USB
based HID device communicates HID reports to control up to 8 relays. The
driver depends on the HIDAPI external dependency for maximum portability.

Support for the conn= parameters is limited. A path that HIDAPI can open
is expected, which depends on the platform and HIDAPI implementation,
and may not always be expressed by means of sigrok command line options.
See README.devices for a discussion.

The USB serial number is not considered. This is an implementation
detail of the relay card's firmware. HID reports carry a five character
identifier for the board.

Relay state is cached in the driver. It's assumed that relay state won't
change outside of libsigrok control as long as the device is opened. The
single request to activate/deactivate all relays at once is supported.

configure.ac
src/hardware/dcttech-usbrelay/api.c
src/hardware/dcttech-usbrelay/protocol.c
src/hardware/dcttech-usbrelay/protocol.h

index 4285ced2e9e5b6d0b8cd601dc8754a3daa9e9b02..85a1af41ea993ce93f3d9f1bf3ecd2d5eae7cacd 100644 (file)
@@ -269,7 +269,7 @@ SR_DRIVER([Center 3xx], [center-3xx], [serial_comm])
 SR_DRIVER([ChronoVu LA], [chronovu-la], [libusb libftdi])
 SR_DRIVER([Colead SLM], [colead-slm], [serial_comm])
 SR_DRIVER([Conrad DIGI 35 CPU], [conrad-digi-35-cpu], [serial_comm])
-SR_DRIVER([dcttech usbrelay], [dcttech-usbrelay])
+SR_DRIVER([dcttech usbrelay], [dcttech-usbrelay], [libhidapi])
 SR_DRIVER([demo], [demo])
 SR_DRIVER([DreamSourceLab DSLogic], [dreamsourcelab-dslogic], [libusb])
 SR_DRIVER([Fluke 45], [fluke-45])
index 9751ef5b022f5f494036bf516580e7cf997f9a0c..67de5d1e7dfac3ad6daf889ff6f67b2a96e31d4b 100644 (file)
  */
 
 #include <config.h>
+
+#include <hidapi.h>
+#include <string.h>
+
 #include "protocol.h"
 
+static const uint32_t scanopts[] = {
+       SR_CONF_CONN,
+};
+
+static const uint32_t drvopts[] = {
+       SR_CONF_MULTIPLEXER,
+};
+
+static const uint32_t devopts[] = {
+       SR_CONF_CONN | SR_CONF_GET,
+       SR_CONF_ENABLED | SR_CONF_SET, /* Enable/disable all relays at once. */
+};
+
+static const uint32_t devopts_cg[] = {
+       SR_CONF_ENABLED | SR_CONF_GET | SR_CONF_SET,
+};
+
 static struct sr_dev_driver dcttech_usbrelay_driver_info;
 
+static struct sr_dev_inst *probe_device(const char *path, size_t relay_count)
+{
+       hid_device *hid;
+       int ret;
+       char serno[SERNO_LENGTH + 1];
+       uint8_t curr_state;
+       uint8_t report[1 + REPORT_BYTECOUNT];
+       GString *txt;
+       size_t snr_pos;
+       char c;
+       struct sr_dev_inst *sdi;
+       struct dev_context *devc;
+       struct channel_group_context *cgc;
+       size_t idx, nr;
+       struct sr_channel_group *cg;
+
+       /* Open device, need to communicate to identify. */
+       hid = hid_open_path(path);
+       if (!hid)
+               return NULL;
+
+       /* Get an HID report. */
+       hid_set_nonblocking(hid, 0);
+       memset(&report, 0, sizeof(report));
+       report[0] = REPORT_NUMBER;
+       ret = hid_get_feature_report(hid, report, sizeof(report));
+       hid_close(hid);
+       if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
+               txt = sr_hexdump_new(report, sizeof(report));
+               sr_spew("raw report, rc %d, bytes %s", ret, txt->str);
+               sr_hexdump_free(txt);
+       }
+       if (ret != sizeof(report))
+               return NULL;
+
+       /*
+        * Serial number must be all printable characters. Relay state
+        * is for information only, gets re-retrieved before configure
+        * API calls (get/set).
+        */
+       memset(serno, 0, sizeof(serno));
+       for (snr_pos = 0; snr_pos < SERNO_LENGTH; snr_pos++) {
+               c = report[1 + snr_pos];
+               serno[snr_pos] = c;
+               if (c < 0x20 || c > 0x7e)
+                       return NULL;
+       }
+       curr_state = report[1 + STATE_INDEX];
+       sr_spew("report data, serno[%s], relays 0x%02x.", serno, curr_state);
+
+       /*
+        * Create a device instance, create channels (groups). The
+        * caller fills in vendor, model, conn from USB enum details.
+        */
+       sdi = g_malloc0(sizeof(*sdi));
+       devc = g_malloc0(sizeof(*devc));
+       sdi->priv = devc;
+       devc->hid_path = g_strdup(path);
+       devc->relay_count = relay_count;
+       devc->relay_mask = (1U << relay_count) - 1;
+       for (idx = 0; idx < devc->relay_count; idx++) {
+               nr = idx + 1;
+               cg = g_malloc0(sizeof(*cg));
+               cg->name = g_strdup_printf("R%zu", nr);
+               cgc = g_malloc0(sizeof(*cgc));
+               cg->priv = cgc;
+               cgc->number = nr;
+               sdi->channel_groups = g_slist_append(sdi->channel_groups, cg);
+       }
+
+       return sdi;
+}
+
 static GSList *scan(struct sr_dev_driver *di, GSList *options)
 {
-       struct drv_context *drvc;
+       const char *conn;
        GSList *devices;
-
-       (void)options;
+       struct drv_context *drvc;
+       struct hid_device_info *devs, *curdev;
+       int ret;
+       wchar_t *ws;
+       char nonws[32];
+       char *s, *endp;
+       unsigned long relay_count;
+       struct sr_dev_inst *sdi;
+
+       /* Get optional conn= spec when provided. */
+       conn = NULL;
+       (void)sr_serial_extract_options(options, &conn, NULL);
+       if (conn && !*conn)
+               conn = NULL;
+       /*
+        * TODO Accept different types of conn= specs? Either paths that
+        * hidapi(3) can open. Or bus.addr specs that we can check for
+        * during USB enumeration. Derive want_path, want_bus, want_addr
+        * here from the optional conn= spec.
+        */
 
        devices = NULL;
        drvc = di->context;
        drvc->instances = NULL;
 
-       /* TODO: scan for devices, either based on a SR_CONF_CONN option
-        * or on a USB scan. */
+       /*
+        * The firmware is V-USB based. The USB VID:PID identification
+        * is shared across several projects. Need to inspect the vendor
+        * and product _strings_ to actually identify the device.
+        *
+        * The USB serial number need not be present nor reliable. The
+        * HID report contains a five character string which may serve
+        * as an identification for boards (is said to differ between
+        * boards). The last byte encodes the current relays state.
+        */
+       devs = hid_enumerate(VENDOR_ID, PRODUCT_ID);
+       for (curdev = devs; curdev; curdev = curdev->next) {
+               if (!curdev->vendor_id || !curdev->product_id)
+                       continue;
+               if (!curdev->manufacturer_string || !curdev->product_string)
+                       continue;
+               if (!*curdev->manufacturer_string || !*curdev->product_string)
+                       continue;
+               if (conn && strcmp(curdev->path, conn) != 0) {
+                       sr_dbg("skipping %s, conn= mismatch", curdev->path);
+                       continue;
+               }
+               sr_dbg("checking %04hx:%04hx, vendor %ls, product %ls.",
+                       curdev->vendor_id, curdev->product_id,
+                       curdev->manufacturer_string, curdev->product_string);
+
+               /* Check USB details retrieved by enumeration. */
+               ws = curdev->manufacturer_string;
+               if (!ws || !wcslen(ws))
+                       continue;
+               snprintf(nonws, sizeof(nonws), "%ls", ws);
+               if (strcmp(nonws, VENDOR_STRING) != 0)
+                       continue;
+               ws = curdev->product_string;
+               if (!ws || !wcslen(ws))
+                       continue;
+               snprintf(nonws, sizeof(nonws), "%ls", ws);
+               s = nonws;
+               if (!g_str_has_prefix(s, PRODUCT_STRING_PREFIX))
+                       continue;
+               s += strlen(PRODUCT_STRING_PREFIX);
+               ret = sr_atoul_base(s, &relay_count, &endp, 10);
+               if (ret != SR_OK || !endp || *endp)
+                       continue;
+               sr_info("Found: HID path %s, relay count %lu.",
+                       curdev->path, relay_count);
+
+               /* Identify device by communicating to it. */
+               sdi = probe_device(curdev->path, relay_count);
+               if (!sdi) {
+                       sr_warn("Failed to communicate to %s.", curdev->path);
+                       continue;
+               }
+
+               /* Amend driver instance from USB enumeration details. */
+               sdi->vendor = g_strdup_printf("%ls", curdev->manufacturer_string);
+               sdi->model = g_strdup_printf("%ls", curdev->product_string);
+               sdi->conn = g_strdup(curdev->path);
+               sdi->driver = &dcttech_usbrelay_driver_info;
+               sdi->inst_type = SR_INST_USB;
+
+               devices = g_slist_append(devices, sdi);
+       }
+       hid_free_enumeration(devs);
 
        return devices;
 }
 
 static int dev_open(struct sr_dev_inst *sdi)
 {
-       (void)sdi;
+       struct dev_context *devc;
+
+       devc = sdi->priv;
+
+       if (devc->hid_dev) {
+               hid_close(devc->hid_dev);
+               devc->hid_dev = NULL;
+       }
 
-       /* TODO: get handle from sdi->conn and open it. */
+       devc->hid_dev = hid_open_path(devc->hid_path);
+       if (!devc->hid_dev)
+               return SR_ERR_IO;
+
+       (void)dcttech_usbrelay_update_state(sdi);
 
        return SR_OK;
 }
 
 static int dev_close(struct sr_dev_inst *sdi)
 {
-       (void)sdi;
+       struct dev_context *devc;
+
+       devc = sdi->priv;
 
-       /* TODO: get handle from sdi->conn and close it. */
+       if (devc->hid_dev) {
+               hid_close(devc->hid_dev);
+               devc->hid_dev = NULL;
+       }
 
        return SR_OK;
 }
@@ -60,77 +250,82 @@ static int dev_close(struct sr_dev_inst *sdi)
 static int config_get(uint32_t key, GVariant **data,
        const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
 {
+       gboolean on;
        int ret;
 
-       (void)sdi;
-       (void)data;
-       (void)cg;
+       if (!cg) {
+               switch (key) {
+               case SR_CONF_CONN:
+                       if (!sdi->conn)
+                               return SR_ERR_NA;
+                       *data = g_variant_new_string(sdi->conn);
+                       return SR_OK;
+               default:
+                       return SR_ERR_NA;
+               }
+       }
 
-       ret = SR_OK;
        switch (key) {
-       /* TODO */
+       case SR_CONF_ENABLED:
+               ret = dcttech_usbrelay_query_cg(sdi, cg, &on);
+               if (ret != SR_OK)
+                       return ret;
+               *data = g_variant_new_boolean(on);
+               return SR_OK;
        default:
                return SR_ERR_NA;
        }
-
-       return ret;
 }
 
 static int config_set(uint32_t key, GVariant *data,
        const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
 {
-       int ret;
-
-       (void)sdi;
-       (void)data;
-       (void)cg;
-
-       ret = SR_OK;
-       switch (key) {
-       /* TODO */
-       default:
-               ret = SR_ERR_NA;
+       gboolean on;
+
+       if (!cg) {
+               switch (key) {
+               case SR_CONF_ENABLED:
+                       /* Enable/disable all channels at the same time. */
+                       on = g_variant_get_boolean(data);
+                       return dcttech_usbrelay_switch_cg(sdi, cg, on);
+               default:
+                       return SR_ERR_NA;
+               }
+       } else {
+               switch (key) {
+               case SR_CONF_ENABLED:
+                       on = g_variant_get_boolean(data);
+                       return dcttech_usbrelay_switch_cg(sdi, cg, on);
+               default:
+                       return SR_ERR_NA;
+               }
        }
 
-       return ret;
+       return SR_OK;
 }
 
 static int config_list(uint32_t key, GVariant **data,
        const struct sr_dev_inst *sdi, const struct sr_channel_group *cg)
 {
-       int ret;
 
-       (void)sdi;
-       (void)data;
-       (void)cg;
+       if (!cg) {
+               switch (key) {
+               case SR_CONF_SCAN_OPTIONS:
+               case SR_CONF_DEVICE_OPTIONS:
+                       return STD_CONFIG_LIST(key, data, sdi, cg,
+                               scanopts, drvopts, devopts);
+               default:
+                       return SR_ERR_NA;
+               }
+       }
 
-       ret = SR_OK;
        switch (key) {
-       /* TODO */
+       case SR_CONF_DEVICE_OPTIONS:
+               *data = std_gvar_array_u32(ARRAY_AND_SIZE(devopts_cg));
+               return SR_OK;
        default:
                return SR_ERR_NA;
        }
-
-       return ret;
-}
-
-static int dev_acquisition_start(const struct sr_dev_inst *sdi)
-{
-       /* TODO: configure hardware, reset acquisition state, set up
-        * callbacks and send header packet. */
-
-       (void)sdi;
-
-       return SR_OK;
-}
-
-static int dev_acquisition_stop(struct sr_dev_inst *sdi)
-{
-       /* TODO: stop acquisition. */
-
-       (void)sdi;
-
-       return SR_OK;
 }
 
 static struct sr_dev_driver dcttech_usbrelay_driver_info = {
@@ -147,8 +342,8 @@ static struct sr_dev_driver dcttech_usbrelay_driver_info = {
        .config_list = config_list,
        .dev_open = dev_open,
        .dev_close = dev_close,
-       .dev_acquisition_start = dev_acquisition_start,
-       .dev_acquisition_stop = dev_acquisition_stop,
+       .dev_acquisition_start = std_dummy_dev_acquisition_start,
+       .dev_acquisition_stop = std_dummy_dev_acquisition_stop,
        .context = NULL,
 };
 SR_REGISTER_DEV_DRIVER(dcttech_usbrelay_driver_info);
index 111e4ef1206c2df87cc648b2279e4c19abbcd7e3..d1290f48c9a548996a834fa99421dd7d7cd27b5f 100644 (file)
  */
 
 #include <config.h>
+
+#include <string.h>
+
 #include "protocol.h"
 
-SR_PRIV int dcttech_usbrelay_receive_data(int fd, int revents, void *cb_data)
+SR_PRIV int dcttech_usbrelay_update_state(const struct sr_dev_inst *sdi)
 {
-       const struct sr_dev_inst *sdi;
        struct dev_context *devc;
+       uint8_t report[1 + REPORT_BYTECOUNT];
+       int ret;
+       GString *txt;
+
+       devc = sdi->priv;
+
+       /* Get another HID report. */
+       memset(report, 0, sizeof(report));
+       report[0] = REPORT_NUMBER;
+       ret = hid_get_feature_report(devc->hid_dev, report, sizeof(report));
+       if (ret != sizeof(report))
+               return SR_ERR_IO;
+       if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
+               txt = sr_hexdump_new(report, sizeof(report));
+               sr_spew("got report bytes: %s", txt->str);
+               sr_hexdump_free(txt);
+       }
 
-       (void)fd;
+       /* Update relay state cache from HID report content. */
+       devc->relay_state = report[1 + STATE_INDEX];
+       devc->relay_state &= devc->relay_mask;
+
+       return SR_OK;
+}
+
+SR_PRIV int dcttech_usbrelay_switch_cg(const struct sr_dev_inst *sdi,
+       const struct sr_channel_group *cg, gboolean on)
+{
+       struct dev_context *devc;
+       struct channel_group_context *cgc;
+       gboolean is_all;
+       size_t relay_idx;
+       uint8_t report[1 + REPORT_BYTECOUNT];
+       int ret;
+       GString *txt;
 
-       if (!(sdi = cb_data))
-               return TRUE;
+       devc = sdi->priv;
 
-       if (!(devc = sdi->priv))
-               return TRUE;
+       /* Determine if all or a single relay should be turned off or on. */
+       is_all = !cg ? TRUE : FALSE;
+       if (is_all) {
+               relay_idx = 0;
+       } else {
+               cgc = cg->priv;
+               relay_idx = cgc->number;
+       }
 
-       if (revents == G_IO_IN) {
-               /* TODO */
+       /*
+        * Construct and send the HID report. Notice the weird(?) bit
+        * pattern. Bit 1 is low when all relays are affected at once,
+        * and high to control an individual relay? Bit 0 communicates
+        * whether the relay(s) should be on or off? And all other bits
+        * are always set? It's assumed that the explicit assignment of
+        * full byte values simplifies future maintenance.
+        */
+       memset(report, 0, sizeof(report));
+       report[0] = REPORT_NUMBER;
+       if (is_all) {
+               if (on) {
+                       report[1] = 0xfe;
+               } else {
+                       report[1] = 0xfc;
+               }
+       } else {
+               if (on) {
+                       report[1] = 0xff;
+                       report[2] = relay_idx;
+               } else {
+                       report[1] = 0xfd;
+                       report[2] = relay_idx;
+               }
+       }
+       if (sr_log_loglevel_get() >= SR_LOG_SPEW) {
+               txt = sr_hexdump_new(report, sizeof(report));
+               sr_spew("sending report bytes: %s", txt->str);
+               sr_hexdump_free(txt);
        }
+       ret = hid_send_feature_report(devc->hid_dev, report, sizeof(report));
+       if (ret != sizeof(report))
+               return SR_ERR_IO;
+
+       /* Update relay state cache (non-fatal). */
+       (void)dcttech_usbrelay_update_state(sdi);
+
+       return SR_OK;
+}
+
+/* Answers the query from cached relay state. Beware of 1-based indexing. */
+SR_PRIV int dcttech_usbrelay_query_cg(const struct sr_dev_inst *sdi,
+       const struct sr_channel_group *cg, gboolean *on)
+{
+       struct dev_context *devc;
+       struct channel_group_context *cgc;
+       size_t relay_idx;
+       uint32_t relay_mask;
+
+       devc = sdi->priv;
+       if (!cg)
+               return SR_ERR_ARG;
+       cgc = cg->priv;
+       relay_idx = cgc->number;
+       if (relay_idx < 1 || relay_idx > devc->relay_count)
+               return SR_ERR_ARG;
+       relay_mask = 1U << (relay_idx - 1);
+
+       *on = devc->relay_state & relay_mask;
 
-       return TRUE;
+       return SR_OK;
 }
index 519518426fd69af9960825857fad8a9c446e37c1..45b8ec4bfca0ec7556ffdc6e24e7c53eb9b087bf 100644 (file)
 #ifndef LIBSIGROK_HARDWARE_DCTTECH_USBRELAY_PROTOCOL_H
 #define LIBSIGROK_HARDWARE_DCTTECH_USBRELAY_PROTOCOL_H
 
-#include <stdint.h>
 #include <glib.h>
+#include <hidapi.h>
 #include <libsigrok/libsigrok.h>
+#include <stdint.h>
+
 #include "libsigrok-internal.h"
 
 #define LOG_PREFIX "dcttech-usbrelay"
 
+/* USB identification. */
+#define VENDOR_ID 0x16c0
+#define PRODUCT_ID 0x05df
+#define VENDOR_STRING "www.dcttech.com"
+#define PRODUCT_STRING_PREFIX "USBRelay"
+
+/* HID report layout. */
+#define REPORT_NUMBER 0
+#define REPORT_BYTECOUNT 8
+#define SERNO_LENGTH 5
+#define STATE_INDEX 7
+
 struct dev_context {
+       char *hid_path;
+       hid_device *hid_dev;
+       size_t relay_count;
+       uint32_t relay_mask;
+       uint32_t relay_state;
+};
+
+struct channel_group_context {
+       size_t number;
 };
 
-SR_PRIV int dcttech_usbrelay_receive_data(int fd, int revents, void *cb_data);
+SR_PRIV int dcttech_usbrelay_update_state(const struct sr_dev_inst *sdi);
+SR_PRIV int dcttech_usbrelay_switch_cg(const struct sr_dev_inst *sdi,
+       const struct sr_channel_group *cg, gboolean on);
+SR_PRIV int dcttech_usbrelay_query_cg(const struct sr_dev_inst *sdi,
+       const struct sr_channel_group *cg, gboolean *on);
 
 #endif