dcttech-usbrelay: implement multiplexer driver for USB relay card
[libsigrok.git] / src / hardware / dcttech-usbrelay / api.c
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);