--- /dev/null
+/*
+ * LibUSB helpers for LibUCW
+ *
+ * (c) 2022--2023 Martin Mares <mj@ucw.cz>
+ *
+ * Based on USB handling in my USB-to-RS485 switch daemon.
+ */
+
+#include <ucw/lib.h>
+#include <ucw/log.h>
+#include <ucw/clists.h>
+#include <ucw/mainloop.h>
+#include <ucw/stkstring.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "usb-mainloop.h"
+#include "usb-helper.h"
+
+uint log_type_usb;
+
+clist usb_device_list;
+
+struct hotplug_request {
+ cnode n;
+ libusb_device *device; // referenced
+ int bus, dev;
+ char port[USB_PORT_NAME_LEN];
+ bool is_connect;
+};
+
+static clist hotplug_request_list;
+
+#define HR_MSG(hp_req, level, fmt, ...) msg(level, "%s: " fmt, hp_req->port, ##__VA_ARGS__)
+#define HR_DBG(hp_req, fmt, ...) msg(L_DEBUG | log_type_usb, "%s: " fmt, hp_req->port, ##__VA_ARGS__)
+
+void FORMAT_CHECK(printf,2,3) usb_error(struct usb_dev *u, const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ const char *formatted_msg = stk_vprintf(fmt, args);
+ va_end(args);
+
+ USB_MSG(u, L_ERROR, "%s", formatted_msg);
+ u->state = USTATE_BROKEN;
+
+ // Cancel all pending transfers
+ usb_dev_cancel(u);
+}
+
+static void check_if_broken(struct usb_dev *u)
+{
+ // USB devices cannot be closed from libusb callbacks, so we handle it here
+ if (u->state != USTATE_BROKEN)
+ return;
+
+ // If there are still in-flight transfers, wait for them to complete
+ if (usb_dev_in_flight(u))
+ return;
+
+ USB_DBG(u, "Leaving broken device");
+ u->state = USTATE_INIT;
+
+ if (u->bus < 0) {
+ // Device already unplugged, so dismantle the USB context
+ USB_MSG(u, L_INFO, "Disconnected");
+
+ timer_del(&u->connect_timer);
+
+ if (u->devh) {
+ libusb_close(u->devh);
+ u->devh = NULL;
+ }
+
+ usb_dev_cleanup(u);
+ clist_remove(&u->n);
+ xfree(u);
+ } else {
+ // Re-connect later
+ timer_add_rel(&u->connect_timer, 5000);
+ }
+}
+
+static void connect_handler(struct main_timer *timer)
+{
+ struct usb_dev *u = timer->data;
+ int err;
+
+ timer_del(timer);
+
+ libusb_reset_device(u->devh);
+
+ if (err = libusb_claim_interface(u->devh, 0)) {
+ usb_error(u, "Cannot claim interface: error %d", err);
+ return;
+ }
+
+ u->state = USTATE_RUNNING;
+ usb_dev_connect(u);
+}
+
+static void hotplug_connect(struct hotplug_request *hr)
+{
+ HR_DBG(hr, "Connected");
+
+ // We might get duplicate events, so ignore the event if the device is already known
+ CLIST_FOR_EACH(struct usb_dev *, u, usb_device_list) {
+ if (u->bus == hr->bus && u->dev == hr->dev)
+ return;
+ }
+
+ struct usb_dev *u = xmalloc_zero(sizeof(*u));
+ u->bus = hr->bus;
+ u->dev = hr->dev;
+ strcpy(u->port, hr->port);
+ u->state = USTATE_INIT;
+
+ int err;
+
+ if (err = libusb_get_device_descriptor(hr->device, &u->desc)) {
+ USB_MSG(u, L_ERROR, "Cannot read descriptors: error %d", err);
+ goto out_early;
+ }
+
+ if (err = libusb_open(hr->device, &u->devh)) {
+ USB_MSG(u, L_ERROR, "Cannot open device: error %d", err);
+ goto out_early;
+ }
+
+ if (!usb_dev_init(u))
+ goto out;
+
+ u->connect_timer.handler = connect_handler;
+ u->connect_timer.data = u;
+ timer_add_rel(&u->connect_timer, 0);
+ return;
+
+out:
+ libusb_close(u->devh);
+out_early:
+ xfree(u);
+}
+
+static void hotplug_disconnect(struct hotplug_request *hr)
+{
+ HR_DBG(hr, "Disconnected");
+
+ CLIST_FOR_EACH(struct usb_dev *, u, usb_device_list) {
+ if (u->bus == hr->bus && u->dev == hr->dev) {
+ u->state = USTATE_BROKEN;
+ u->bus = u->dev = -1;
+ usb_dev_cancel(u);
+ return;
+ }
+ }
+}
+
+static void handle_hotplug(void)
+{
+ struct hotplug_request *hr;
+ while (hr = clist_remove_head(&hotplug_request_list)) {
+ if (hr->is_connect)
+ hotplug_connect(hr);
+ else
+ hotplug_disconnect(hr);
+ libusb_unref_device(hr->device);
+ xfree(hr);
+ }
+}
+
+static struct main_hook usb_hook;
+
+static int usb_hook_handler(struct main_hook *hook UNUSED)
+{
+ handle_hotplug();
+
+ CLIST_FOR_EACH(struct usb_dev *, u, usb_device_list)
+ check_if_broken(u);
+ return HOOK_IDLE;
+}
+
+static int hotplug_callback(libusb_context *ctx UNUSED, libusb_device *device, libusb_hotplug_event event, void *user_data UNUSED)
+{
+ // Called asynchronously, need to defer to the main loop
+
+ if (event != LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED && event != LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
+ return 0;
+
+ struct hotplug_request *hr = xmalloc_zero(sizeof(*hr));
+ hr->device = libusb_ref_device(device);
+ hr->bus = libusb_get_bus_number(device);
+ hr->dev = libusb_get_device_address(device);
+ snprintf(hr->port, sizeof(hr->port), "usb%d.%d", hr->bus, hr->dev);
+ hr->is_connect = (event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED);
+ clist_add_tail(&hotplug_request_list, &hr->n);
+
+ HR_DBG(hr, "Scheduled hotplug event");
+ return 0;
+}
+
+void usb_helper_init(uint vendor_id, uint device_id)
+{
+ usb_init_mainloop();
+
+ log_type_usb = log_register_type("usb");
+
+ clist_init(&usb_device_list);
+
+ clist_init(&hotplug_request_list);
+ usb_hook.handler = usb_hook_handler;
+ hook_add(&usb_hook);
+
+ int err;
+ if (err = libusb_hotplug_register_callback(usb_ctx,
+ LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
+ LIBUSB_HOTPLUG_ENUMERATE,
+ vendor_id,
+ device_id,
+ LIBUSB_HOTPLUG_MATCH_ANY,
+ hotplug_callback,
+ NULL, // no user data
+ NULL // no need to store callback handle
+ ))
+ die("Cannot register libusb hotplug callback");
+}
--- /dev/null
+/*
+ * LibUSB helpers for LibUCW
+ *
+ * (c) 2022--2023 Martin Mares <mj@ucw.cz>
+ */
+
+#include <libusb-1.0/libusb.h>
+
+#include <ucw/clists.h>
+#include <ucw/mainloop.h>
+
+#define USB_PORT_NAME_LEN 16
+
+struct device; // Provided by the user
+
+enum usb_dev_state {
+ USTATE_INIT,
+ USTATE_RUNNING,
+ USTATE_BROKEN,
+};
+
+struct usb_dev {
+ cnode n;
+
+ int bus, dev; // -1 if device already unplugged
+ char port[USB_PORT_NAME_LEN];
+ enum usb_dev_state state;
+
+ struct main_timer connect_timer;
+
+ struct libusb_device_handle *devh;
+ struct libusb_device_descriptor desc;
+
+ struct device *device;
+};
+
+extern clist usb_dev_list;
+
+extern uint log_type_usb;
+
+#define USB_MSG(usb_dev, level, fmt, ...) msg(level, "%s: " fmt, usb_dev->port, ##__VA_ARGS__)
+#define USB_DBG(usb_dev, fmt, ...) msg(L_DEBUG | log_type_usb, "%s: " fmt, usb_dev->port, ##__VA_ARGS__)
+
+void usb_helper_init(uint vendor_id, uint device_id);
+void FORMAT_CHECK(printf,2,3) usb_error(struct usb_dev *u, const char *fmt, ...);
+
+/*** Callbacks provided by the user ***/
+
+// A new device was detected. Read its configuration and check if it can
+// be handled. Allocate transfers. But do not start them until usb_dev_connect().
+bool usb_dev_init(struct usb_dev *u);
+
+// Device was connected, start transfers.
+void usb_dev_connect(struct usb_dev *u);
+
+// An error has been detected, cancel all pending transfers.
+void usb_dev_cancel(struct usb_dev *u);
+
+// Are there any in-flight transfers?
+bool usb_dev_in_flight(struct usb_dev *u);
+
+// Clean up application state, free all usb transfers.
+void usb_dev_cleanup(struct usb_dev *u);
--- /dev/null
+/*
+ * LibUSB over LibUCW Mainloop
+ *
+ * (c) 2014--2022 Martin Mares <mj@ucw.cz>
+ *
+ * Originally developed as a part of the Ursary project.
+ */
+
+#undef LOCAL_DEBUG
+
+#include <ucw/lib.h>
+#include <ucw/clists.h>
+#include <ucw/gary.h>
+#include <ucw/mainloop.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/poll.h>
+
+#include "usb-mainloop.h"
+
+libusb_context *usb_ctx;
+
+static struct main_file **usb_fds;
+
+static int usb_fd_ready(struct main_file *f UNUSED)
+{
+ DBG("USB: Handling events (ready on fd %d)", f->fd);
+ struct timeval tv = { 0, 0 };
+ int comp = 0;
+ int err = libusb_handle_events_timeout_completed(usb_ctx, &tv, &comp);
+ if (err < 0)
+ msg(L_ERROR, "libusb_handle_events: error %d", err);
+ return HOOK_IDLE;
+}
+
+static void usb_added_fd(int fd, short events, void *user_data UNUSED)
+{
+ if (fd >= (int) GARY_SIZE(usb_fds))
+ GARY_RESIZE(usb_fds, fd + 1);
+
+ struct main_file *f = usb_fds[fd];
+ if (!f) {
+ f = xmalloc_zero(sizeof(*f));
+ usb_fds[fd] = f;
+ } else if (file_is_active(f)) {
+ DBG("USB: Releasing fd %d", fd);
+ file_del(f);
+ }
+
+ DBG("USB: Adding fd %d with event mask %u", fd, events);
+ f->fd = fd;
+ f->read_handler = (events & POLLIN) ? usb_fd_ready : NULL;
+ f->write_handler = (events & POLLOUT) ? usb_fd_ready : NULL;
+ file_add(f);
+}
+
+static void usb_removed_fd(int fd, void *user_data UNUSED)
+{
+ DBG("USB: Releasing fd %d", fd);
+ ASSERT(fd < (int) GARY_SIZE(usb_fds));
+ struct main_file *f = usb_fds[fd];
+ ASSERT(f);
+ ASSERT(file_is_active(f));
+ file_del(f);
+}
+
+void usb_init_mainloop(void)
+{
+ int err;
+
+ // Initialize libusb
+ if ((err = libusb_init(&usb_ctx)) < 0)
+ die("libusb_init failed: error %d", err);
+
+ // Connect libusb to UCW mainloop
+
+ if (!libusb_pollfds_handle_timeouts(usb_ctx))
+ die("Unsupported version of libusb, please fix me");
+
+ GARY_INIT_ZERO(usb_fds, 0);
+ libusb_set_pollfd_notifiers(usb_ctx, usb_added_fd, usb_removed_fd, NULL);
+
+ const struct libusb_pollfd **fds = libusb_get_pollfds(usb_ctx);
+ ASSERT(fds);
+ for (int i=0; fds[i]; i++)
+ usb_added_fd(fds[i]->fd, fds[i]->events, NULL);
+ free(fds);
+}