Home modules.gotpike.org
Username: Password: [Create Account]
[Forgot Password?]

Modules

ADT
Database
GTK2
GUI
IP
PiJAX
Public
Sql
Stdio
Subversion
System
Tools
Xosd
lua
v4l2
wx

Recent Changes

Public.USB 1.0
Public.Parser.XML2 1.50
Public.ZeroMQ 1.1
Public.Template.Mustache 1.0
Public.Protocols.XMPP 1.4

Popular Downloads

Public.Parser.JSON2 1.0
Public.Parser.JSON 0.2
GTK2 2.23
Public.Web.FCGI 1.8
Public.Parser.XML2 1.48


Module Information
Public.USB
Viewing contents of Public_USB-1.0/usb.cmod

/*! @module Public
 */

/*! @module USB
 */

#define _GNU_SOURCE

#include "usb_config.h"
#include "util.h"

#ifdef HAVE_LIBUSB_1_0
#include 
#include 
#endif

DECLARATIONS

#ifdef HAVE_LIBUSB_1_0
/* Device object data structure */
typedef struct
{
    libusb_device *device;
    libusb_device_handle *handle;
    unsigned int claimed_interfaces;  /* Bitset for claimed interfaces */
    int initialized;
} DEVICE_OBJECT_DATA;

/* Forward declaration of static callback */
static void usb_transfer_callback(struct libusb_transfer *transfer);

/* Backend integration - GTK2-style pattern for Pike event loop integration */

/* Forward declarations for pollfd integration */
struct usb_pollfd_node;
struct ctx_node;
static void setup_pollfd_monitoring(struct ctx_node *node);
static void cleanup_pollfd_monitoring(struct ctx_node *node);

/* Context registry node for tracking active libusb contexts */
typedef struct ctx_node {
    libusb_context *ctx;
    int backend_enabled;  /* Flag to track if this context opted in */
    struct usb_pollfd_node *pollfds;  /* List of active file descriptors */
    struct ctx_node *next;
} ctx_node;

/* Per-fd tracking structure (Inotify-style pattern) */
typedef struct usb_pollfd_node {
    struct fd_callback_box box;  /* Pike's fd monitoring structure */
    libusb_context *ctx;         /* Back-reference to owning context */
    struct usb_pollfd_node *next;
} usb_pollfd_node;

/* Module-global backend callback state */
static struct callback *usb_backend_cb = NULL;
static ctx_node *active_ctxs = NULL;

/* Helper: Add context to active registry */
static void register_context(libusb_context *ctx)
{
    ctx_node *node;
    
    /* Check if already registered */
    for (node = active_ctxs; node != NULL; node = node->next) {
        if (node->ctx == ctx) {
            node->backend_enabled = 1;
            return;
        }
    }
    
    /* Add new node */
    node = (ctx_node *)malloc(sizeof(ctx_node));
    if (!node) {
        Pike_error("Out of memory registering USB context\n");
    }
    
    node->ctx = ctx;
    node->backend_enabled = 1;
    node->pollfds = NULL;
    node->next = active_ctxs;
    active_ctxs = node;
    
    /* Setup pollfd monitoring for event-driven fd wakeups */
    setup_pollfd_monitoring(node);
}

/* Helper: Remove context from active registry */
static void unregister_context(libusb_context *ctx)
{
    ctx_node *node, *prev = NULL;
    
    for (node = active_ctxs; node != NULL; prev = node, node = node->next) {
        if (node->ctx == ctx) {
            /* Cleanup pollfd monitoring before removal (Inotify pattern) */
            cleanup_pollfd_monitoring(node);
            
            if (prev) {
                prev->next = node->next;
            } else {
                active_ctxs = node->next;
            }
            free(node);
            return;
        }
    }
}

/* Helper: Check if context is registered */
static int context_is_registered(libusb_context *ctx)
{
    ctx_node *node;
    
    for (node = active_ctxs; node != NULL; node = node->next) {
        if (node->ctx == ctx && node->backend_enabled) {
            return 1;
        }
    }
    return 0;
}

/* Helper: Get time in microseconds from timeval */
static long timeval_to_usec(struct timeval *tv)
{
    return tv->tv_sec * 1000000 + tv->tv_usec;
}

/* Helper: Add microseconds to current time */
static void get_time_plus_usec(struct timeval *result, long usec)
{
    struct timeval now;
    INACCURATE_GETTIMEOFDAY(&now);
    
    result->tv_sec = now.tv_sec + (usec / 1000000);
    result->tv_usec = now.tv_usec + (usec % 1000000);
    
    /* Normalize */
    if (result->tv_usec >= 1000000) {
        result->tv_sec += 1;
        result->tv_usec -= 1000000;
    }
}

/* Pollfd Integration - Event-driven fd monitoring (Inotify pattern) */

/* fd_callback_box callback - called by Pike when USB fd has I/O ready */
static int usb_fd_callback(struct fd_callback_box *box, int event)
{
    usb_pollfd_node *node = (usb_pollfd_node *)box;
    struct timeval tv_zero = {0, 0};
    
    /* Handle events on this fd's context (non-blocking) */
    if (node->ctx) {
        libusb_handle_events_timeout(node->ctx, &tv_zero);
    }
    
    return 0;  /* Keep fd registered */
}

/* libusb pollfd added callback - called when libusb adds a new fd to monitor */
static void usb_pollfd_added_cb(int fd, short events, void *user_data)
{
    ctx_node *ctx_node_ptr = (ctx_node *)user_data;
    usb_pollfd_node *node;
    int pike_events = 0;
    
    if (!ctx_node_ptr) return;
    
    /* Convert libusb poll events to Pike events */
    if (events & POLLIN)  pike_events |= PIKE_BIT_FD_READ;
    if (events & POLLOUT) pike_events |= PIKE_BIT_FD_WRITE;
    
    /* Allocate new pollfd tracking node */
    node = (usb_pollfd_node *)malloc(sizeof(usb_pollfd_node));
    if (!node) return;  /* Out of memory - fall back to periodic polling */
    
    /* Initialize fd_callback_box using Pike's macro (CRITICAL - like Inotify does) */
    INIT_FD_CALLBACK_BOX(&node->box,
                         default_backend,      /* Use default backend, NOT NULL! */
                         NULL,                 /* No ref_obj for module-level fds */
                         fd,                   /* File descriptor to monitor */
                         pike_events,          /* Events to watch for */
                         usb_fd_callback,      /* Callback function */
                         0);                   /* Flags */
    
    node->ctx = ctx_node_ptr->ctx;
    
    /* Add to context's pollfd list */
    node->next = ctx_node_ptr->pollfds;
    ctx_node_ptr->pollfds = node;
    
    /* Register with Pike backend (now properly initialized) */
    hook_fd_callback_box(&node->box);
}

/* libusb pollfd removed callback - called when libusb removes an fd */
static void usb_pollfd_removed_cb(int fd, void *user_data)
{
    ctx_node *ctx_node_ptr = (ctx_node *)user_data;
    usb_pollfd_node *node, *prev = NULL;
    
    if (!ctx_node_ptr) return;
    
    /* Find the pollfd node */
    for (node = ctx_node_ptr->pollfds; node != NULL; prev = node, node = node->next) {
        if (node->box.fd == fd) {
            /* Cleanup using Inotify pattern */
            set_fd_callback_events(&node->box, 0, 0);  /* Disable events first */
            change_fd_for_box(&node->box, -1);          /* Invalidate fd */
            unhook_fd_callback_box(&node->box);         /* Then unhook */
            
            /* Remove from list */
            if (prev) {
                prev->next = node->next;
            } else {
                ctx_node_ptr->pollfds = node->next;
            }
            
            free(node);
            return;
        }
    }
}

/* Helper: Setup pollfd monitoring for a context */
static void setup_pollfd_monitoring(ctx_node *node)
{
    const struct libusb_pollfd **pollfds;
    int i;
    
    if (!node || !node->ctx) return;
    
    /* Set up notifiers for future fd add/remove */
    libusb_set_pollfd_notifiers(node->ctx, 
                                 usb_pollfd_added_cb, 
                                 usb_pollfd_removed_cb, 
                                 node);
    
    /* Register existing pollfds */
    pollfds = libusb_get_pollfds(node->ctx);
    if (pollfds) {
        for (i = 0; pollfds[i] != NULL; i++) {
            usb_pollfd_added_cb(pollfds[i]->fd, pollfds[i]->events, node);
        }
        libusb_free_pollfds(pollfds);
    }
}

/* Helper: Cleanup pollfd monitoring for a context */
static void cleanup_pollfd_monitoring(ctx_node *node)
{
    usb_pollfd_node *pollfd_node, *next;
    
    if (!node) return;
    
    /* Remove libusb pollfd notifiers */
    if (node->ctx) {
        libusb_set_pollfd_notifiers(node->ctx, NULL, NULL, NULL);
    }
    
    /* Cleanup all registered fds using Inotify pattern */
    pollfd_node = node->pollfds;
    while (pollfd_node) {
        next = pollfd_node->next;
        
        /* Clean unhook sequence */
        set_fd_callback_events(&pollfd_node->box, 0, 0);
        change_fd_for_box(&pollfd_node->box, -1);
        unhook_fd_callback_box(&pollfd_node->box);
        
        free(pollfd_node);
        pollfd_node = next;
    }
    node->pollfds = NULL;
}

/* Backend callback - called periodically by Pike's event loop (GTK2 pattern) */
static void usb_backend_callback(struct callback *_cb, void *arg, void *backend)
{
    ctx_node *node;
    struct timeval tv_zero = {0, 0};
    
    /* Non-blocking event pump for all registered contexts */
    for (node = active_ctxs; node != NULL; node = node->next) {
        if (node->backend_enabled) {
            /* Non-blocking call: process ready events only */
            libusb_handle_events_timeout(node->ctx, &tv_zero);
        }
    }
    
    /* If backend==NULL: drain mode (called during shutdown) */
    if (!backend) {
        return;
    }
    
    /* Compute minimum timeout across all contexts for dynamic scheduling */
    {
        long min_timeout_usec = 20000;  /* Default: 20ms like GTK2 */
        
        for (node = active_ctxs; node != NULL; node = node->next) {
            if (node->backend_enabled && node->ctx) {
                struct timeval tv;
                int r = libusb_get_next_timeout(node->ctx, &tv);
                
                if (r == 1 && (tv.tv_sec > 0 || tv.tv_usec > 0)) {
                    /* Transfer has a timeout - compute microseconds */
                    long ctx_timeout_usec = tv.tv_sec * 1000000 + tv.tv_usec;
                    
                    /* Use minimum timeout (but don't exceed 20ms for responsiveness) */
                    if (ctx_timeout_usec > 0 && ctx_timeout_usec < min_timeout_usec) {
                        min_timeout_usec = ctx_timeout_usec;
                    }
                }
                /* r == 0: no pending transfers with timeout (keep default 20ms)
                 * r < 0: error (keep default 20ms) */
            }
        }
        
        /* Ensure minimum of 1ms to avoid busy-looping */
        if (min_timeout_usec < 1000) {
            min_timeout_usec = 1000;  /* 1ms minimum */
        }
        
        /* Reschedule next callback with computed timeout */
        struct timeval next_run;
        get_time_plus_usec(&next_run, min_timeout_usec);
        
        backend_lower_timeout(backend, &next_run);
    }
}

#endif

/*! @class Device
 *!   Represents a single USB device and provides methods for communicating
 *!   with it.
 *!
 *!   A Device wraps a @tt{libusb_device@} and optionally a
 *!   @tt{libusb_device_handle@}, providing access to device descriptors,
 *!   interface management, and data transfers.
 *!
 *!   @b{Lifecycle@}
 *!
 *!   Device objects are obtained from @[USB()->get_device_list()].
 *!   A typical usage sequence is:
 *!
 *!   @ol
 *!     @item
 *!       Obtain a Device from @[USB()->get_device_list()]
 *!     @item
 *!       Read descriptors with @[get_descriptor()] and
 *!       @[get_config_descriptor()]
 *!     @item
 *!       Call @[open()] to open a connection to the hardware
 *!     @item
 *!       Check and detach kernel driver with @[kernel_driver_active()]
 *!       and @[detach_kernel_driver()]
 *!     @item
 *!       Claim an interface with @[claim_interface()]
 *!     @item
 *!       Perform I/O with @[bulk_transfer()] or
 *!       @[submit_bulk_transfer()]
 *!     @item
 *!       Release with @[release_interface()] and @[close()]
 *!   @endol
 *!
 *!   @note
 *!     Device objects should NOT be instantiated directly by users.
 *!     They are created and returned by @[USB()->get_device_list()].
 *!
 *!   @note
 *!     Descriptor methods (@[get_descriptor()],
 *!     @[get_config_descriptor()], @[get_bus_number()],
 *!     @[get_device_address()], @[get_speed()]) do not require the
 *!     device to be opened. All other operations require a prior call
 *!     to @[open()].
 *!
 *!   @note
 *!     When the Device object is destroyed, any claimed interfaces are
 *!     automatically released and the device handle is closed.
 *!
 *!   @seealso
 *!     @[USB()->get_device_list()], @[USB]
 */
PIKECLASS Device {

#ifdef HAVE_LIBUSB_1_0
CVAR DEVICE_OBJECT_DATA *object_data;
#else
CVAR void *object_data;
#endif

/* Pike callback function for async transfers */
PIKEVAR function transfer_callback;

DECLARE_STORAGE;

INIT
{
#ifdef HAVE_LIBUSB_1_0
    /* INIT macro allocates object storage - runs when object is created */
    THIS->object_data = (DEVICE_OBJECT_DATA*)malloc(sizeof(DEVICE_OBJECT_DATA));
    if (!THIS->object_data)
        Pike_error("Out of memory!\n");
    
    THIS->object_data->initialized = 0;
    THIS->object_data->device = NULL;
    THIS->object_data->handle = NULL;
    THIS->object_data->claimed_interfaces = 0;
#else
    THIS->object_data = NULL;
#endif
}

EXIT
{
    /* EXIT macro frees object storage - runs when object is destroyed */
    if (THIS->object_data)
    {
#ifdef HAVE_LIBUSB_1_0
        DEVICE_OBJECT_DATA *dta = THIS->object_data;
        
        /* Release all claimed interfaces */
        if (dta->handle) {
            for (int i = 0; i < 32; i++) {
                if (dta->claimed_interfaces & (1 << i)) {
                    libusb_release_interface(dta->handle, i);
                }
            }
        }
        
        /* Close device handle if open */
        if (dta->handle) {
            libusb_close(dta->handle);
            dta->handle = NULL;
        }
        
        /* Unreference the device */
        if (dta->device) {
            libusb_unref_device(dta->device);
            dta->device = NULL;
        }
#endif
        free(THIS->object_data);
        THIS->object_data = NULL;
    }
    
    /* PIKEVAR transfer_callback is automatically freed by Pike */
}

/*! @decl private void create()
 *!   Creates a new Device object.
 *!
 *!   @note
 *!     This constructor is PRIVATE and should not be called by user code.
 *!     Device objects are created internally by the USB class.
 *!     Use methods from the USB class to obtain Device objects.
 */
PIKEFUN void create()
flags ID_PRIVATE;
{
#ifdef HAVE_LIBUSB_1_0
    /* Device object starts in an uninitialized state */
    /* It will be populated when assigned a libusb_device */
    THIS->object_data->initialized = 1;
#else
    Pike_error("USB support not available - libusb-1.0 was not found during compilation\n");
#endif
    
    pop_n_elems(args);
}

/*! @decl int open()
 *!   Opens a connection to the USB device hardware.
 *!
 *!   This must be called before performing any I/O operations,
 *!   claiming interfaces, or managing kernel drivers. Descriptor
 *!   queries (@[get_descriptor()], @[get_config_descriptor()]) do
 *!   not require the device to be open.
 *!
 *!   If the device is already open, this is a no-op and returns 0.
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code on failure.
 *!     Common error codes:
 *!     @int
 *!       @value 0
 *!         Success.
 *!       @value -3
 *!         @tt{LIBUSB_ERROR_ACCESS@} - Insufficient permissions.
 *!         Run with elevated privileges (sudo).
 *!       @value -4
 *!         @tt{LIBUSB_ERROR_NO_DEVICE@} - Device has been disconnected.
 *!     @endint
 *!
 *!   @throws
 *!     Throws an error if the device has not been initialized.
 *!
 *!   @seealso
 *!     @[close()]
 */
PIKEFUN int open()
{
#ifdef HAVE_LIBUSB_1_0
    int r;
    
    if (!THIS->object_data->device) {
        Pike_error("Device not initialized\n");
    }
    
    if (THIS->object_data->handle) {
        /* Already open */
        pop_n_elems(args);
        push_int(0);
        return;
    }
    
    r = libusb_open(THIS->object_data->device, &THIS->object_data->handle);
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int close()
 *!   Closes the connection to the USB device.
 *!
 *!   All claimed interfaces are automatically released before the
 *!   device handle is closed. It is safe to call this on a device
 *!   that is not open.
 *!
 *!   @returns
 *!     0 on success.
 *!
 *!   @note
 *!     After closing, the Device object can be re-opened with
 *!     @[open()].
 *!
 *!   @seealso
 *!     @[open()], @[release_interface()]
 */
PIKEFUN int close()
{
#ifdef HAVE_LIBUSB_1_0
    if (THIS->object_data->handle) {
        /* Release all claimed interfaces first */
        for (int i = 0; i < 32; i++) {
            if (THIS->object_data->claimed_interfaces & (1 << i)) {
                libusb_release_interface(THIS->object_data->handle, i);
                THIS->object_data->claimed_interfaces &= ~(1 << i);
            }
        }
        
        libusb_close(THIS->object_data->handle);
        THIS->object_data->handle = NULL;
    }
    
    pop_n_elems(args);
    push_int(0);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int get_bus_number()
 *!   Returns the bus number of the device.
 *!
 *!   Does not require the device to be opened.
 *!
 *!   @returns
 *!     The number of the bus the device is connected to.
 *!
 *!   @seealso
 *!     @[get_device_address()]
 */
PIKEFUN int get_bus_number()
{
#ifdef HAVE_LIBUSB_1_0
    int bus_num = 0;
    
    if (THIS->object_data->device) {
        bus_num = libusb_get_bus_number(THIS->object_data->device);
    }
    
    pop_n_elems(args);
    push_int(bus_num);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int get_device_address()
 *!   Returns the address of the device on its bus.
 *!
 *!   Does not require the device to be opened. Together with
 *!   @[get_bus_number()], this uniquely identifies a device on the
 *!   system.
 *!
 *!   @returns
 *!     The device address on the bus.
 *!
 *!   @seealso
 *!     @[get_bus_number()]
 */
PIKEFUN int get_device_address()
{
#ifdef HAVE_LIBUSB_1_0
    int addr = 0;
    
    if (THIS->object_data->device) {
        addr = libusb_get_device_address(THIS->object_data->device);
    }
    
    pop_n_elems(args);
    push_int(addr);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int get_speed()
 *!   Returns the negotiated connection speed of the device.
 *!
 *!   Does not require the device to be opened.
 *!
 *!   @returns
 *!     One of the following speed constants:
 *!     @int
 *!       @value 0
 *!         @tt{SPEED_UNKNOWN@} - Unknown speed (OS doesn't know or error).
 *!       @value 1
 *!         @tt{SPEED_LOW@} - Low speed (1.5 Mbit/s, USB 1.0).
 *!       @value 2
 *!         @tt{SPEED_FULL@} - Full speed (12 Mbit/s, USB 1.1).
 *!       @value 3
 *!         @tt{SPEED_HIGH@} - High speed (480 Mbit/s, USB 2.0).
 *!       @value 4
 *!         @tt{SPEED_SUPER@} - Super speed (5 Gbit/s, USB 3.0).
 *!       @value 5
 *!         @tt{SPEED_SUPER_PLUS@} - Super speed+ (10 Gbit/s, USB 3.1).
 *!     @endint
 *!
 *!   @seealso
 *!     @[get_descriptor()]
 */
PIKEFUN int get_speed()
{
#ifdef HAVE_LIBUSB_1_0
    int speed = LIBUSB_SPEED_UNKNOWN;
    
    if (THIS->object_data->device) {
        speed = libusb_get_device_speed(THIS->object_data->device);
    }
    
    pop_n_elems(args);
    push_int(speed);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl mapping get_descriptor()
 *!   Get the device descriptor for this USB device.
 *!
 *!   @returns
 *!     A mapping with device descriptor information:
 *!     @mapping
 *!       @member int "idVendor"
 *!         USB vendor ID (VID)
 *!       @member int "idProduct"
 *!         USB product ID (PID)
 *!       @member int "bDeviceClass"
 *!         Device class code
 *!       @member int "bDeviceSubClass"
 *!         Device subclass code
 *!       @member int "bDeviceProtocol"
 *!         Device protocol code
 *!       @member int "bcdUSB"
 *!         USB specification release number
 *!       @member int "bMaxPacketSize0"
 *!         Maximum packet size for endpoint zero
 *!       @member int "bNumConfigurations"
 *!         Number of possible configurations
 *!     @endmapping
 */
PIKEFUN mapping get_descriptor()
{
#ifdef HAVE_LIBUSB_1_0
    struct libusb_device_descriptor desc;
    int r;
    
    if (!THIS->object_data->device) {
        Pike_error("Device not initialized\n");
    }
    
    r = libusb_get_device_descriptor(THIS->object_data->device, &desc);
    if (r < 0) {
        Pike_error("Failed to get device descriptor: %s\n", libusb_error_name(r));
    }
    
    /* Create a mapping with device descriptor information */
    push_text("idVendor");
    push_int(desc.idVendor);
    push_text("idProduct");
    push_int(desc.idProduct);
    push_text("bDeviceClass");
    push_int(desc.bDeviceClass);
    push_text("bDeviceSubClass");
    push_int(desc.bDeviceSubClass);
    push_text("bDeviceProtocol");
    push_int(desc.bDeviceProtocol);
    push_text("bcdUSB");
    push_int(desc.bcdUSB);
    push_text("bMaxPacketSize0");
    push_int(desc.bMaxPacketSize0);
    push_text("bNumConfigurations");
    push_int(desc.bNumConfigurations);
    
    /* Create mapping from the key-value pairs on stack (8 pairs = 16 elements) */
    f_aggregate_mapping(16);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl mapping get_config_descriptor(int config_index)
 *!   Retrieves a USB configuration descriptor.
 *!
 *!   Returns a mapping containing configuration fields plus nested
 *!   arrays of interface and endpoint descriptors. This provides the
 *!   full device topology needed to find interfaces, endpoints, and
 *!   their properties.
 *!
 *!   Does not require the device to be opened.
 *!
 *!   @param config_index
 *!     Configuration index (usually 0 for the first/default
 *!     configuration). Valid range is 0 to
 *!     @expr{get_descriptor()->bNumConfigurations - 1@}.
 *!
 *!   @returns
 *!     A mapping with the following structure:
 *!     @mapping
 *!       @member int "bNumInterfaces"
 *!         Number of interfaces in this configuration.
 *!       @member int "bConfigurationValue"
 *!         Value to pass to SetConfiguration to select this config.
 *!       @member int "bmAttributes"
 *!         Configuration characteristics (bit 6 = self-powered,
 *!         bit 5 = remote wakeup).
 *!       @member int "MaxPower"
 *!         Maximum power consumption in 2mA units (USB 2.0) or
 *!         8mA units (USB 3.0).
 *!       @member array(mapping) "interface"
 *!         Array of interface groupings, one per interface.
 *!         Each element is a mapping:
 *!         @mapping
 *!           @member array(mapping) "altsetting"
 *!             Array of alternate settings. Each element is a mapping:
 *!             @mapping
 *!               @member int "bInterfaceNumber"
 *!                 Interface number.
 *!               @member int "bAlternateSetting"
 *!                 Alternate setting index.
 *!               @member int "bInterfaceClass"
 *!                 USB class code (e.g. @expr{0x02@} for CDC,
 *!                 @expr{0x0A@} for CDC-Data).
 *!               @member int "bInterfaceSubClass"
 *!                 USB subclass code.
 *!               @member int "bInterfaceProtocol"
 *!                 USB protocol code.
 *!               @member int "bNumEndpoints"
 *!                 Number of endpoints (excluding EP0).
 *!               @member array(mapping) "endpoint"
 *!                 Array of endpoint descriptors. Each element:
 *!                 @mapping
 *!                   @member int "bEndpointAddress"
 *!                     Endpoint address. Bit 7 indicates direction:
 *!                     @expr{0x80@} = IN (device to host),
 *!                     @expr{0x00@} = OUT (host to device).
 *!                   @member int "bmAttributes"
 *!                     Transfer type in bits 0-1:
 *!                     @expr{0x00@} = Control, @expr{0x01@} = Isochronous,
 *!                     @expr{0x02@} = Bulk, @expr{0x03@} = Interrupt.
 *!                   @member int "wMaxPacketSize"
 *!                     Maximum packet size for this endpoint.
 *!                     Use this as the buffer size for IN transfers
 *!                     to avoid @tt{LIBUSB_ERROR_OVERFLOW@}.
 *!                   @member int "bInterval"
 *!                     Polling interval (for interrupt/isochronous).
 *!                 @endmapping
 *!             @endmapping
 *!         @endmapping
 *!     @endmapping
 *!
 *!   @throws
 *!     Throws an error if the device is not initialized or if the
 *!     config index is invalid.
 *!
 *!   @example
 *!     // Find CDC-Data interface and its bulk endpoints
 *!     mapping config = device->get_config_descriptor(0);
 *!     foreach(config->interface, mapping iface) {
 *!       foreach(iface->altsetting, mapping alt) {
 *!         if (alt->bInterfaceClass == 0x0A) {
 *!           write("CDC-Data interface %d\n", alt->bInterfaceNumber);
 *!           foreach(alt->endpoint, mapping ep) {
 *!             write("  Endpoint 0x%02x: %s, max %d bytes\n",
 *!                   ep->bEndpointAddress,
 *!                   (ep->bEndpointAddress & 0x80) ? "IN" : "OUT",
 *!                   ep->wMaxPacketSize);
 *!           }
 *!         }
 *!       }
 *!     }
 *!
 *!   @seealso
 *!     @[get_descriptor()]
 */
PIKEFUN mapping get_config_descriptor(int config_index)
{
#ifdef HAVE_LIBUSB_1_0
    struct libusb_config_descriptor *config;
    int r, i, j, k;
    
    if (!THIS->object_data->device) {
        Pike_error("Device not initialized\n");
    }
    
    r = libusb_get_config_descriptor(THIS->object_data->device, config_index, &config);
    if (r < 0) {
        Pike_error("Failed to get config descriptor: %s\n", libusb_error_name(r));
    }
    
    /* Build configuration mapping */
    push_text("bNumInterfaces");
    push_int(config->bNumInterfaces);
    push_text("bConfigurationValue");
    push_int(config->bConfigurationValue);
    push_text("bmAttributes");
    push_int(config->bmAttributes);
    push_text("MaxPower");
    push_int(config->MaxPower);
    
    /* Build interface array */
    push_text("interface");
    
    for (i = 0; i < config->bNumInterfaces; i++) {
        const struct libusb_interface *iface = &config->interface[i];
        
        /* Build altsetting array for this interface */
        push_text("altsetting");
        
        for (j = 0; j < iface->num_altsetting; j++) {
            const struct libusb_interface_descriptor *alt = &iface->altsetting[j];
            
            /* Build interface descriptor mapping */
            push_text("bInterfaceNumber");
            push_int(alt->bInterfaceNumber);
            push_text("bAlternateSetting");
            push_int(alt->bAlternateSetting);
            push_text("bInterfaceClass");
            push_int(alt->bInterfaceClass);
            push_text("bInterfaceSubClass");
            push_int(alt->bInterfaceSubClass);
            push_text("bInterfaceProtocol");
            push_int(alt->bInterfaceProtocol);
            push_text("bNumEndpoints");
            push_int(alt->bNumEndpoints);
            
            /* Build endpoint array */
            push_text("endpoint");
            
            for (k = 0; k < alt->bNumEndpoints; k++) {
                const struct libusb_endpoint_descriptor *ep = &alt->endpoint[k];
                
                /* Build endpoint descriptor mapping */
                push_text("bEndpointAddress");
                push_int(ep->bEndpointAddress);
                push_text("bmAttributes");
                push_int(ep->bmAttributes);
                push_text("wMaxPacketSize");
                push_int(ep->wMaxPacketSize);
                push_text("bInterval");
                push_int(ep->bInterval);
                
                /* Create endpoint mapping (4 pairs = 8 elements) */
                f_aggregate_mapping(8);
            }
            
            /* Create endpoint array */
            f_aggregate(alt->bNumEndpoints);
            
            /* Create interface descriptor mapping (7 pairs = 14 elements) */
            f_aggregate_mapping(14);
        }
        
        /* Create altsetting array */
        f_aggregate(iface->num_altsetting);
        
        /* Create interface mapping with altsetting array (1 pair = 2 elements) */
        f_aggregate_mapping(2);
    }
    
    /* Create interface array */
    f_aggregate(config->bNumInterfaces);
    
    /* Create final configuration mapping (5 pairs = 10 elements) */
    f_aggregate_mapping(10);
    
    /* Free the config descriptor */
    libusb_free_config_descriptor(config);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int get_max_iso_packet_size(int endpoint)
 *!   Returns the maximum isochronous packet size for a given endpoint.
 *!
 *!   This is useful for allocating appropriately sized buffers for
 *!   isochronous transfers.
 *!
 *!   Does not require the device to be opened.
 *!
 *!   @param endpoint
 *!     The endpoint address.
 *!
 *!   @returns
 *!     The maximum packet size in bytes, or a negative libusb error
 *!     code on failure.
 *!
 *!   @seealso
 *!     @[get_config_descriptor()]
 */
PIKEFUN int get_max_iso_packet_size(int endpoint)
{
#ifdef HAVE_LIBUSB_1_0
    int size = 0;
    
    if (THIS->object_data->device) {
        size = libusb_get_max_iso_packet_size(THIS->object_data->device, endpoint);
    }
    
    pop_n_elems(args);
    push_int(size);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int kernel_driver_active(int interface)
 *!   Checks if a kernel driver is active on the given interface.
 *!
 *!   On Linux and macOS, the operating system may claim USB interfaces
 *!   with kernel drivers (e.g. @tt{cdc_acm@}, @tt{AppleUSBCDC@}).
 *!   You must detach the kernel driver before you can claim the
 *!   interface for your own use.
 *!
 *!   Requires the device to be opened.
 *!
 *!   @param interface
 *!     The interface number (from @[get_config_descriptor()]).
 *!
 *!   @returns
 *!     @int
 *!       @value 1
 *!         A kernel driver is active.
 *!       @value 0
 *!         No kernel driver is active.
 *!     @endint
 *!     A negative value indicates an error.
 *!
 *!   @seealso
 *!     @[detach_kernel_driver()], @[attach_kernel_driver()],
 *!     @[claim_interface()]
 */
PIKEFUN int kernel_driver_active(int interface)
{
#ifdef HAVE_LIBUSB_1_0
    int r = 0;
    
    if (THIS->object_data->handle) {
        r = libusb_kernel_driver_active(THIS->object_data->handle, interface);
    }
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int detach_kernel_driver(int interface)
 *!   Detaches the kernel driver from the given interface.
 *!
 *!   This must be called before @[claim_interface()] if a kernel
 *!   driver is active. Use @[kernel_driver_active()] to check first.
 *!
 *!   Requires the device to be opened.
 *!
 *!   @param interface
 *!     The interface number.
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code:
 *!     @int
 *!       @value 0
 *!         Success.
 *!       @value -5
 *!         @tt{LIBUSB_ERROR_NOT_FOUND@} - No kernel driver was active.
 *!       @value -3
 *!         @tt{LIBUSB_ERROR_ACCESS@} - Insufficient permissions.
 *!     @endint
 *!
 *!   @note
 *!     On macOS, reattaching the kernel driver after detaching may
 *!     fail and cause the device to disappear from the USB bus.
 *!     Consider using @[attach_kernel_driver()] cautiously, or skip
 *!     reattachment entirely.
 *!
 *!   @seealso
 *!     @[kernel_driver_active()], @[attach_kernel_driver()],
 *!     @[claim_interface()]
 */
PIKEFUN int detach_kernel_driver(int interface)
{
#ifdef HAVE_LIBUSB_1_0
    int r = 0;
    
    if (THIS->object_data->handle) {
        r = libusb_detach_kernel_driver(THIS->object_data->handle, interface);
    }
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int attach_kernel_driver(int interface)
 *!   Re-attaches the kernel driver to the given interface.
 *!
 *!   Call this after @[release_interface()] to return the interface
 *!   to the operating system's control.
 *!
 *!   Requires the device to be opened.
 *!
 *!   @param interface
 *!     The interface number.
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code:
 *!     @int
 *!       @value 0
 *!         Success.
 *!       @value -5
 *!         @tt{LIBUSB_ERROR_NOT_FOUND@} - No kernel driver available.
 *!       @value -6
 *!         @tt{LIBUSB_ERROR_BUSY@} - Interface is claimed by another
 *!         program.
 *!     @endint
 *!
 *!   @note
 *!     On macOS, this call often fails and may cause the device to
 *!     disappear from the USB bus entirely, requiring a physical
 *!     unplug/replug. Consider skipping reattachment on macOS.
 *!
 *!   @seealso
 *!     @[detach_kernel_driver()], @[kernel_driver_active()],
 *!     @[release_interface()]
 */
PIKEFUN int attach_kernel_driver(int interface)
{
#ifdef HAVE_LIBUSB_1_0
    int r = 0;
    
    if (THIS->object_data->handle) {
        r = libusb_attach_kernel_driver(THIS->object_data->handle, interface);
    }
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int claim_interface(int interface)
 *!   Claims exclusive access to a USB interface.
 *!
 *!   You must claim an interface before performing any I/O on its
 *!   endpoints. If a kernel driver is active on the interface, you
 *!   must call @[detach_kernel_driver()] first.
 *!
 *!   Requires the device to be opened.
 *!
 *!   @param interface
 *!     The interface number (from @[get_config_descriptor()]).
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code:
 *!     @int
 *!       @value 0
 *!         Success.
 *!       @value -6
 *!         @tt{LIBUSB_ERROR_BUSY@} - Interface already claimed
 *!         (by another program or kernel driver).
 *!       @value -4
 *!         @tt{LIBUSB_ERROR_NO_DEVICE@} - Device disconnected.
 *!     @endint
 *!
 *!   @throws
 *!     Throws an error if the device is not open or the interface
 *!     number is out of range (0-31).
 *!
 *!   @note
 *!     Claimed interfaces are tracked and will be automatically
 *!     released when the device is closed or the Device object is
 *!     destroyed.
 *!
 *!   @seealso
 *!     @[release_interface()], @[detach_kernel_driver()]
 */
PIKEFUN int claim_interface(int interface)
{
#ifdef HAVE_LIBUSB_1_0
    int r = 0;
    
    if (!THIS->object_data->handle) {
        Pike_error("Device not open\n");
    }
    
    if (interface < 0 || interface >= 32) {
        Pike_error("Invalid interface number\n");
    }
    
    r = libusb_claim_interface(THIS->object_data->handle, interface);
    if (r == 0) {
        THIS->object_data->claimed_interfaces |= (1 << interface);
    }
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int release_interface(int interface)
 *!   Releases a previously claimed interface.
 *!
 *!   This should be called before closing the device to cleanly
 *!   release the interface back to the operating system.
 *!
 *!   Requires the device to be opened.
 *!
 *!   @param interface
 *!     The interface number previously passed to @[claim_interface()].
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code.
 *!
 *!   @throws
 *!     Throws an error if the device is not open or the interface
 *!     number is out of range.
 *!
 *!   @seealso
 *!     @[claim_interface()], @[close()]
 */
PIKEFUN int release_interface(int interface)
{
#ifdef HAVE_LIBUSB_1_0
    int r = 0;
    
    if (!THIS->object_data->handle) {
        Pike_error("Device not open\n");
    }
    
    if (interface < 0 || interface >= 32) {
        Pike_error("Invalid interface number\n");
    }
    
    r = libusb_release_interface(THIS->object_data->handle, interface);
    if (r == 0) {
        THIS->object_data->claimed_interfaces &= ~(1 << interface);
    }
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl void set_transfer_callback(function(mapping:void) callback)
 *!   Sets the callback function for asynchronous USB transfers.
 *!
 *!   The callback is invoked each time an asynchronous transfer
 *!   submitted via @[submit_bulk_transfer()] or
 *!   @[submit_interrupt_transfer()] completes (or fails/times out).
 *!
 *!   The callback receives a single mapping argument with the
 *!   following fields:
 *!   @mapping
 *!     @member int "status"
 *!       Transfer status:
 *!       @int
 *!         @value 0
 *!           @tt{TRANSFER_COMPLETED@} - Success.
 *!         @value 1
 *!           @tt{TRANSFER_ERROR@} - Transfer failed.
 *!         @value 2
 *!           @tt{TRANSFER_TIMED_OUT@} - Transfer timed out.
 *!         @value 3
 *!           @tt{TRANSFER_CANCELLED@} - Transfer was cancelled.
 *!         @value 4
 *!           @tt{TRANSFER_STALL@} - Endpoint stalled.
 *!         @value 5
 *!           @tt{TRANSFER_NO_DEVICE@} - Device disconnected.
 *!         @value 6
 *!           @tt{TRANSFER_OVERFLOW@} - Device sent more data than
 *!           expected.
 *!       @endint
 *!     @member int "actual_length"
 *!       Number of bytes actually transferred.
 *!     @member string "data"
 *!       For IN (read) transfers: the received data as a string.
 *!       For OUT (write) transfers: @expr{0@}.
 *!     @member int "endpoint"
 *!       The endpoint address the transfer was on.
 *!     @member int "type"
 *!       The transfer type (bulk, interrupt, etc.).
 *!   @endmapping
 *!
 *!   @param callback
 *!     A function to be called when transfers complete.
 *!
 *!   @note
 *!     You must call this before using @[submit_bulk_transfer()] or
 *!     @[submit_interrupt_transfer()].
 *!
 *!   @note
 *!     For the callback to fire, USB events must be processed.
 *!     Either call @[USB()->handle_events_timeout()] manually, or
 *!     enable automatic processing with
 *!     @[USB()->enable_backend_events()] and run Pike in backend
 *!     mode (@expr{return -1@} from @tt{main()@}).
 *!
 *!   @seealso
 *!     @[submit_bulk_transfer()], @[submit_interrupt_transfer()],
 *!     @[USB()->enable_backend_events()]
 */
PIKEFUN void set_transfer_callback(function callback)
{
    /* Store the Pike function for later invocation */
    /* assign_svalue copies the svalue, add_ref_svalue increments refcount */
    assign_svalue(&(THIS->transfer_callback), callback);
    add_ref_svalue(&THIS->transfer_callback);
    
    pop_n_elems(args);
}

/*! @decl int|string bulk_transfer(int endpoint, int|string data_or_length, int timeout)
 *!   Perform a synchronous (blocking) bulk transfer.
 *!
 *!   This is the simplest way to read from or write to a USB device.
 *!   The call blocks until the transfer completes or the timeout
 *!   expires.
 *!
 *!   The direction (read or write) is determined by the endpoint
 *!   address: endpoints with bit 7 set (@expr{0x80@}) are IN (read),
 *!   otherwise OUT (write).
 *!
 *!   Requires the device to be opened and the interface claimed.
 *!
 *!   @param endpoint
 *!     Endpoint address. Use @expr{0x8X@} for IN (read from device),
 *!     @expr{0x0X@} for OUT (write to device). The endpoint number
 *!     and direction are obtained from @[get_config_descriptor()].
 *!
 *!   @param data_or_length
 *!     For OUT (write) transfers: a @expr{string@} containing the
 *!     data to send.
 *!     For IN (read) transfers: an @expr{int@} specifying the maximum
 *!     number of bytes to read. This should be at least the
 *!     endpoint's @tt{wMaxPacketSize@} to avoid
 *!     @tt{LIBUSB_ERROR_OVERFLOW@}.
 *!
 *!   @param timeout
 *!     Transfer timeout in milliseconds. Use 0 for no timeout
 *!     (wait indefinitely).
 *!
 *!   @returns
 *!     For OUT transfers: an @expr{int@} with the number of bytes
 *!     actually sent.
 *!     For IN transfers: a @expr{string@} containing the received
 *!     data. Returns an empty string on timeout with no data.
 *!
 *!   @throws
 *!     Throws an error if the device is not open, the argument types
 *!     are wrong, or the transfer fails with an error other than
 *!     timeout.
 *!
 *!   @example
 *!     // Write data to endpoint 0x03
 *!     int sent = device->bulk_transfer(0x03, "Hello\r\n", 1000);
 *!     write("Sent %d bytes\n", sent);
 *!
 *!     // Read up to 512 bytes from endpoint 0x84, 1 second timeout
 *!     string data = device->bulk_transfer(0x84, 512, 1000);
 *!     if (sizeof(data))
 *!       write("Received: %s\n", data);
 *!
 *!   @seealso
 *!     @[submit_bulk_transfer()], @[get_config_descriptor()],
 *!     @[claim_interface()]
 */
PIKEFUN int|string bulk_transfer(int endpoint, int|string data_or_length, int timeout)
{
#ifdef HAVE_LIBUSB_1_0
    int is_in = (endpoint & LIBUSB_ENDPOINT_IN) != 0;
    int transferred = 0;
    int r;
    
    if (!THIS->object_data->handle) {
        Pike_error("Device not open\n");
    }
    
    if (is_in) {
        /* IN transfer (read) - data_or_length should be an int */
        int length;
        unsigned char *buffer;
        
        if (TYPEOF(Pike_sp[-args+1]) != T_INT) {
            Pike_error("For IN transfers, second argument must be int (buffer size)\n");
        }
        
        length = Pike_sp[-args+1].u.integer;
        if (length <= 0 || length > 1048576) {  /* 1MB safety limit */
            Pike_error("Invalid buffer size: %d\n", length);
        }
        
        buffer = (unsigned char *)malloc(length);
        if (!buffer) {
            Pike_error("Out of memory allocating %d byte buffer\n", length);
        }
        
        r = libusb_bulk_transfer(THIS->object_data->handle, endpoint,
                                  buffer, length, &transferred, timeout);
        
        if (r < 0 && r != LIBUSB_ERROR_TIMEOUT) {
            free(buffer);
            Pike_error("Bulk IN transfer failed: %s\n", libusb_error_name(r));
        }
        
        pop_n_elems(args);
        
        /* Return received data as string */
        if (transferred > 0) {
            push_string(make_shared_binary_string((char *)buffer, transferred));
        } else {
            push_empty_string();
        }
        
        free(buffer);
        
    } else {
        /* OUT transfer (write) - data_or_length should be a string */
        struct pike_string *data;
        
        if (TYPEOF(Pike_sp[-args+1]) != T_STRING) {
            Pike_error("For OUT transfers, second argument must be string (data to send)\n");
        }
        
        data = Pike_sp[-args+1].u.string;
        
        r = libusb_bulk_transfer(THIS->object_data->handle, endpoint,
                                  (unsigned char *)data->str, data->len,
                                  &transferred, timeout);
        
        if (r < 0 && r != LIBUSB_ERROR_TIMEOUT) {
            Pike_error("Bulk OUT transfer failed: %s\n", libusb_error_name(r));
        }
        
        pop_n_elems(args);
        push_int(transferred);
    }
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl void submit_bulk_transfer(int endpoint, string data, int timeout)
 *!   Submit an asynchronous (non-blocking) bulk transfer.
 *!
 *!   Unlike @[bulk_transfer()], this returns immediately after
 *!   queuing the transfer. When the transfer completes (or fails),
 *!   the callback registered via @[set_transfer_callback()] is
 *!   invoked with a result mapping.
 *!
 *!   For IN (read) transfers, pass a string of NUL bytes sized to
 *!   the endpoint's @tt{wMaxPacketSize@}. The actual received data
 *!   will be delivered in the callback's @expr{"data"@} field.
 *!
 *!   Requires the device to be opened and the interface claimed.
 *!
 *!   @param endpoint
 *!     Endpoint address. Use @expr{0x8X@} for IN (read),
 *!     @expr{0x0X@} for OUT (write).
 *!
 *!   @param data
 *!     For OUT transfers: the data to send.
 *!     For IN transfers: a buffer string (e.g.
 *!     @expr{"\0" * max_packet_size@}) whose length determines the
 *!     maximum read size.
 *!
 *!   @param timeout
 *!     Transfer timeout in milliseconds (0 for no timeout).
 *!
 *!   @throws
 *!     Throws an error if the device is not open, no transfer
 *!     callback has been set, or the transfer submission fails.
 *!
 *!   @note
 *!     You must call @[set_transfer_callback()] before using this
 *!     method.
 *!
 *!   @note
 *!     For the transfer to complete, USB events must be processed.
 *!     Use @[USB()->enable_backend_events()] for automatic
 *!     processing, or call @[USB()->handle_events_timeout()]
 *!     manually.
 *!
 *!   @example
 *!     // Set up async callback
 *!     device->set_transfer_callback(lambda(mapping result) {
 *!       if (result->status == 0 && result->data)
 *!         write("Got: %s\n", result->data);
 *!     });
 *!
 *!     // Submit async read (512 byte buffer)
 *!     device->submit_bulk_transfer(0x84, "\0" * 512, 2000);
 *!
 *!     // Enable backend for automatic event processing
 *!     usb->enable_backend_events(1);
 *!     return -1;  // Enter Pike backend
 *!
 *!   @seealso
 *!     @[bulk_transfer()], @[set_transfer_callback()],
 *!     @[submit_interrupt_transfer()],
 *!     @[USB()->enable_backend_events()]
 */
PIKEFUN void submit_bulk_transfer(int endpoint, string data, int timeout)
{
#ifdef HAVE_LIBUSB_1_0
    struct libusb_transfer *transfer;
    unsigned char *buffer;
    int r;
    
    if (!THIS->object_data->handle) {
        Pike_error("Device not open\n");
    }
    
    if (TYPEOF(THIS->transfer_callback) != T_FUNCTION) {
        Pike_error("No transfer callback set - call set_transfer_callback() first\n");
    }
    
    /* Allocate transfer structure */
    transfer = libusb_alloc_transfer(0);
    if (!transfer) {
        Pike_error("Failed to allocate transfer\n");
    }
    
    /* Allocate buffer and copy data */
    buffer = (unsigned char *)malloc(data->len);
    if (!buffer) {
        libusb_free_transfer(transfer);
        Pike_error("Out of memory\n");
    }
    memcpy(buffer, data->str, data->len);
    
    /* Fill the transfer structure */
    libusb_fill_bulk_transfer(transfer,
                               THIS->object_data->handle,
                               endpoint,
                               buffer,
                               data->len,
                               usb_transfer_callback,
                               Pike_fp->current_object,  /* Pass Device object as user_data */
                               timeout);
    
    /* Submit the transfer */
    r = libusb_submit_transfer(transfer);
    if (r < 0) {
        free(buffer);
        libusb_free_transfer(transfer);
        Pike_error("Failed to submit transfer: %s\n", libusb_error_name(r));
    }
    
    /* Transfer is now in flight - buffer and transfer will be freed in callback */
#else
    Pike_error("USB support not available\n");
#endif
    
    pop_n_elems(args);
}

/*! @decl void submit_interrupt_transfer(int endpoint, string data, int timeout)
 *!   Submit an asynchronous (non-blocking) interrupt transfer.
 *!
 *!   Interrupt transfers are typically used for small, time-sensitive
 *!   data such as HID events or CDC control notifications.
 *!
 *!   Works identically to @[submit_bulk_transfer()] but uses the
 *!   interrupt transfer type.
 *!
 *!   Requires the device to be opened and the interface claimed.
 *!
 *!   @param endpoint
 *!     Endpoint address. Use @expr{0x8X@} for IN (read),
 *!     @expr{0x0X@} for OUT (write).
 *!
 *!   @param data
 *!     For OUT transfers: the data to send.
 *!     For IN transfers: a buffer string whose length determines the
 *!     maximum read size.
 *!
 *!   @param timeout
 *!     Transfer timeout in milliseconds (0 for no timeout).
 *!
 *!   @throws
 *!     Throws an error if the device is not open, no transfer
 *!     callback has been set, or the transfer submission fails.
 *!
 *!   @note
 *!     You must call @[set_transfer_callback()] before using this
 *!     method.
 *!
 *!   @seealso
 *!     @[submit_bulk_transfer()], @[set_transfer_callback()]
 */
PIKEFUN void submit_interrupt_transfer(int endpoint, string data, int timeout)
{
#ifdef HAVE_LIBUSB_1_0
    struct libusb_transfer *transfer;
    unsigned char *buffer;
    int r;
    
    if (!THIS->object_data->handle) {
        Pike_error("Device not open\n");
    }
    
    if (TYPEOF(THIS->transfer_callback) != T_FUNCTION) {
        Pike_error("No transfer callback set - call set_transfer_callback() first\n");
    }
    
    /* Allocate transfer structure */
    transfer = libusb_alloc_transfer(0);
    if (!transfer) {
        Pike_error("Failed to allocate transfer\n");
    }
    
    /* Allocate buffer and copy data */
    buffer = (unsigned char *)malloc(data->len);
    if (!buffer) {
        libusb_free_transfer(transfer);
        Pike_error("Out of memory\n");
    }
    memcpy(buffer, data->str, data->len);
    
    /* Fill the transfer structure */
    libusb_fill_interrupt_transfer(transfer,
                                    THIS->object_data->handle,
                                    endpoint,
                                    buffer,
                                    data->len,
                                    usb_transfer_callback,
                                    Pike_fp->current_object,  /* Pass Device object */
                                    timeout);
    
    /* Submit the transfer */
    r = libusb_submit_transfer(transfer);
    if (r < 0) {
        free(buffer);
        libusb_free_transfer(transfer);
        Pike_error("Failed to submit transfer: %s\n", libusb_error_name(r));
    }
#else
    Pike_error("USB support not available\n");
#endif
    
    pop_n_elems(args);
}

/* Static callback function for async transfers */
#ifdef HAVE_LIBUSB_1_0
static void usb_transfer_callback(struct libusb_transfer *transfer)
{
    struct object *device_obj;
    
    if (!transfer || !transfer->user_data) {
        /* Free resources even if we can't call the callback */
        if (transfer) {
            if (transfer->buffer) free(transfer->buffer);
            libusb_free_transfer(transfer);
        }
        return;
    }
    
    /* The user_data field contains our Device object */
    device_obj = (struct object *)transfer->user_data;
    
    /* Check if a Pike callback function is set */
    if (TYPEOF(OBJ2_USB_DEVICE(device_obj)->transfer_callback) != T_FUNCTION) {
        /* Free resources - no callback to invoke */
        if (transfer->buffer) free(transfer->buffer);
        libusb_free_transfer(transfer);
        return;
    }
    
    /* Build a mapping with transfer information to pass to Pike callback */
    push_text("status");
    push_int(transfer->status);
    push_text("actual_length");
    push_int(transfer->actual_length);
    
    /* Include transferred data for IN transfers */
    if (transfer->endpoint & LIBUSB_ENDPOINT_IN) {
        push_text("data");
        push_string(make_shared_binary_string((char *)transfer->buffer, 
                                               transfer->actual_length));
    } else {
        push_text("data");
        push_int(0);  /* No data for OUT transfers */
    }
    
    push_text("endpoint");
    push_int(transfer->endpoint);
    push_text("type");
    push_int(transfer->type);
    
    /* Create mapping from key-value pairs (5 pairs = 10 elements) */
    f_aggregate_mapping(10);
    
    /* Free the transfer buffer and struct now that data is copied to Pike */
    if (transfer->buffer) free(transfer->buffer);
    libusb_free_transfer(transfer);
    
    /* Push the callback function onto the stack */
    /* push_svalue adds a reference to the stack */
    push_svalue(&(OBJ2_USB_DEVICE(device_obj)->transfer_callback));
    
    /* Stack now has: [mapping, function] */
    /* Swap so function is first: [function, mapping] */
    stack_swap();
    
    /* Call the Pike function with the mapping as argument */
    /* apply_svalue will pop the function and arguments from stack */
    apply_svalue(Pike_sp - 2, 1);
    
    /* Pop the return value from the callback */
    pop_stack();
}
#endif

}

/*! @endclass
 */

/*! @class USB
 *!   USB context manager and device enumerator.
 *!
 *!   Each USB instance manages its own @tt{libusb@} context and
 *!   provides methods for discovering connected devices and
 *!   controlling USB event processing.
 *!
 *!   @b{Basic Usage@}
 *!
 *!   @example
 *!     // Create context and enumerate devices
 *!     object usb = Public.USB.USB();
 *!     array devices = usb->get_device_list();
 *!
 *!     foreach(devices, object dev) {
 *!       mapping desc = dev->get_descriptor();
 *!       write("Device: %04x:%04x\n",
 *!             desc->idVendor, desc->idProduct);
 *!     }
 *!
 *!   @b{Synchronous vs. Asynchronous I/O@}
 *!
 *!   For synchronous (blocking) communication, use
 *!   @[Device()->bulk_transfer()] directly.
 *!
 *!   For asynchronous (non-blocking) communication:
 *!   @ol
 *!     @item
 *!       Call @[enable_backend_events()] to integrate with Pike's
 *!       event loop.
 *!     @item
 *!       Set a callback on the device with
 *!       @[Device()->set_transfer_callback()].
 *!     @item
 *!       Submit transfers with @[Device()->submit_bulk_transfer()].
 *!     @item
 *!       Return @expr{-1@} from @tt{main()@} to enter Pike's
 *!       backend.
 *!   @endol
 *!
 *!   Alternatively, call @[handle_events_timeout()] in a loop
 *!   to process events manually without Pike's backend.
 *!
 *!   @b{Constants@}
 *!
 *!   USB speed constants are available as class members:
 *!   @int
 *!     @value 0
 *!       @tt{SPEED_UNKNOWN@} - Speed is unknown.
 *!     @value 1
 *!       @tt{SPEED_LOW@} - Low speed (1.5 Mbit/s, USB 1.0).
 *!     @value 2
 *!       @tt{SPEED_FULL@} - Full speed (12 Mbit/s, USB 1.1).
 *!     @value 3
 *!       @tt{SPEED_HIGH@} - High speed (480 Mbit/s, USB 2.0).
 *!     @value 4
 *!       @tt{SPEED_SUPER@} - Super speed (5 Gbit/s, USB 3.0).
 *!     @value 5
 *!       @tt{SPEED_SUPER_PLUS@} - Super speed+ (10 Gbit/s, USB 3.1).
 *!   @endint
 *!
 *!   @note
 *!     Multiple USB instances can coexist, each with their own
 *!     independent libusb context. Backend event processing handles
 *!     all registered contexts automatically.
 *!
 *!   @seealso
 *!     @[Device], @[Public.USB.get_vendor_name()]
 */
PIKECLASS USB {

#ifdef HAVE_LIBUSB_1_0
/* USB speed constants from libusb */
#define USB_SPEED_UNKNOWN      LIBUSB_SPEED_UNKNOWN
#define USB_SPEED_LOW          LIBUSB_SPEED_LOW
#define USB_SPEED_FULL         LIBUSB_SPEED_FULL
#define USB_SPEED_HIGH         LIBUSB_SPEED_HIGH
#define USB_SPEED_SUPER        LIBUSB_SPEED_SUPER
#define USB_SPEED_SUPER_PLUS   LIBUSB_SPEED_SUPER_PLUS
#endif

typedef struct
{
#ifdef HAVE_LIBUSB_1_0
    libusb_context *ctx;
#endif
    int initialized;
} USB_OBJECT_DATA;

CVAR USB_OBJECT_DATA *object_data;

EXTRA
{
#ifdef HAVE_LIBUSB_1_0
    /* Add USB speed constants to the USB class */
    add_integer_constant("SPEED_UNKNOWN", USB_SPEED_UNKNOWN, 0);
    add_integer_constant("SPEED_LOW", USB_SPEED_LOW, 0);
    add_integer_constant("SPEED_FULL", USB_SPEED_FULL, 0);
    add_integer_constant("SPEED_HIGH", USB_SPEED_HIGH, 0);
    add_integer_constant("SPEED_SUPER", USB_SPEED_SUPER, 0);
    add_integer_constant("SPEED_SUPER_PLUS", USB_SPEED_SUPER_PLUS, 0);
#endif
}

INIT
{
    /* INIT macro allocates object storage - runs when object is created */
    THIS->object_data = (USB_OBJECT_DATA*)malloc(sizeof(USB_OBJECT_DATA));
    if (!THIS->object_data)
        Pike_error("Out of memory!\n");
    
    THIS->object_data->initialized = 0;
#ifdef HAVE_LIBUSB_1_0
    THIS->object_data->ctx = NULL;
#endif
}

EXIT
{
    /* EXIT macro frees object storage - runs when object is destroyed */
    if (THIS->object_data)
    {
#ifdef HAVE_LIBUSB_1_0
        USB_OBJECT_DATA *dta = THIS->object_data;
        
        /* Unregister from backend if backend events were enabled */
        if (dta->initialized && dta->ctx) {
            if (context_is_registered(dta->ctx)) {
                unregister_context(dta->ctx);
                
                /* Remove module-level callback if no contexts remain */
                if (active_ctxs == NULL && usb_backend_cb) {
                    remove_callback(usb_backend_cb);
                    usb_backend_cb = NULL;
                }
            }
            
            /* Exit libusb context */
            libusb_exit(dta->ctx);
        }
#endif
        free(THIS->object_data);
        THIS->object_data = NULL;
    }
}

PIKEFUN void create()
{
#ifdef HAVE_LIBUSB_1_0
    int r;
    
    r = libusb_init(&THIS->object_data->ctx);
    if (r < 0) {
        Pike_error("Failed to initialize libusb: %s\n", libusb_error_name(r));
    }
    
    THIS->object_data->initialized = 1;
#else
    Pike_error("USB support not available - libusb-1.0 was not found during compilation\n");
#endif
    
    pop_n_elems(args);
}

/*! @decl array(Device) get_device_list()
 *!   Enumerates all USB devices currently connected to the system.
 *!
 *!   Returns an array of @[Device] objects, one per connected device.
 *!   The returned devices are not opened — use @[Device()->open()]
 *!   to open a connection before performing I/O.
 *!
 *!   Descriptor methods such as @[Device()->get_descriptor()] and
 *!   @[Device()->get_config_descriptor()] can be called on the
 *!   returned devices without opening them first.
 *!
 *!   @returns
 *!     @expr{array(Device)@} — One @[Device] object per connected
 *!     USB device. May be empty if no devices are found.
 *!
 *!   @throws
 *!     Throws an error if the USB context is not initialized or
 *!     if libusb device enumeration fails.
 *!
 *!   @example
 *!     object usb = Public.USB.USB();
 *!     foreach(usb->get_device_list(), object dev) {
 *!       mapping d = dev->get_descriptor();
 *!       string vendor = Public.USB.get_vendor_name(d->idVendor);
 *!       write("%04x:%04x %s\n",
 *!             d->idVendor, d->idProduct, vendor || "Unknown");
 *!     }
 *!
 *!   @seealso
 *!     @[Device], @[Device()->get_descriptor()]
 */
PIKEFUN array get_device_list()
{
#ifdef HAVE_LIBUSB_1_0
    libusb_device **devices;
    ssize_t cnt;
    int i;
    
    if (!THIS->object_data->initialized) {
        Pike_error("USB context not initialized\n");
    }
    
    /* Get list of devices */
    cnt = libusb_get_device_list(THIS->object_data->ctx, &devices);
    if (cnt < 0) {
        Pike_error("Failed to get device list: %s\n", libusb_error_name(cnt));
    }
    
    /* Create Pike array to hold Device objects */
    {
        int device_count = 0;
        for (i = 0; i < cnt; i++) {
            struct libusb_device_descriptor desc;
            int r;
            
            r = libusb_get_device_descriptor(devices[i], &desc);
            if (r < 0) {
                /* Skip devices we can't read */
                continue;
            }
            
            /* Create a new Device object */
            /* clone_object creates an instance from a program */
            {
                struct object *device_obj;
                DEVICE_OBJECT_DATA *obj_data;
                
                /* Clone the Device program to create a new instance */
                device_obj = clone_object(USB_Device_program, 0);
                
                /* Push the new object onto the Pike stack */
                push_object(device_obj);
                
                /* Access the Device object's storage using OBJ2_USB_DEVICE macro */
                obj_data = OBJ2_USB_DEVICE(device_obj)->object_data;
                
                /* Store the libusb_device pointer in the Device object */
                /* Increment reference count since we're storing a reference */
                obj_data->device = libusb_ref_device(devices[i]);
                obj_data->handle = NULL;
                obj_data->claimed_interfaces = 0;
                obj_data->initialized = 1;
                
                /* PIKEVAR transfer_callback is auto-initialized to 0 by Pike */
            }
            
            /* Device object is already on stack, increment count */
            device_count++;
        }
        
        /* Free the device list (with unref=1 to decrement reference counts) */
        /* Note: We called libusb_ref_device() for each device we stored,
         * so libusb_free_device_list() won't destroy the devices we're using */
        libusb_free_device_list(devices, 1);
        
        /* Create array from Device objects on stack */
        f_aggregate(device_count);
    }
#else
    Pike_error("USB support not available - libusb-1.0 was not found during compilation\n");
#endif
    /* Result array is already on stack, no need to pop_n_elems */
}

/*! @decl int handle_events()
 *!   Process pending USB events, blocking until an event occurs.
 *!
 *!   This is required to complete asynchronous transfers submitted
 *!   via @[Device()->submit_bulk_transfer()]. When an async transfer
 *!   finishes, the callback set with
 *!   @[Device()->set_transfer_callback()] is invoked during this
 *!   call.
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code on failure.
 *!
 *!   @throws
 *!     Throws an error if the USB context is not initialized.
 *!
 *!   @note
 *!     This call @b{blocks@} until at least one event is processed.
 *!     Use @[handle_events_timeout()] for non-blocking operation, or
 *!     @[enable_backend_events()] to have Pike's backend process
 *!     events automatically.
 *!
 *!   @seealso
 *!     @[handle_events_timeout()], @[enable_backend_events()]
 */
PIKEFUN int handle_events()
{
#ifdef HAVE_LIBUSB_1_0
    int r;
    
    if (!THIS->object_data->initialized) {
        Pike_error("USB context not initialized\n");
    }
    
    r = libusb_handle_events(THIS->object_data->ctx);
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl int handle_events_timeout(int timeout_ms)
 *!   Process pending USB events with a timeout.
 *!
 *!   Like @[handle_events()], but returns after the specified timeout
 *!   even if no events were processed. This is the preferred method
 *!   for manual event polling, as it avoids blocking indefinitely.
 *!
 *!   @param timeout_ms
 *!     Maximum time to wait in milliseconds.
 *!     @int
 *!       @value 0
 *!         Non-blocking: process any ready events and return
 *!         immediately.
 *!     @endint
 *!     Positive values specify the maximum wait time.
 *!
 *!   @returns
 *!     0 on success, or a negative libusb error code on failure.
 *!
 *!   @throws
 *!     Throws an error if the USB context is not initialized.
 *!
 *!   @example
 *!     // Manual event processing loop
 *!     while (running) {
 *!       usb->handle_events_timeout(100);
 *!     }
 *!
 *!   @note
 *!     For most applications, @[enable_backend_events()] is simpler
 *!     and more efficient than calling this method in a loop.
 *!
 *!   @seealso
 *!     @[handle_events()], @[enable_backend_events()]
 */
PIKEFUN int handle_events_timeout(int timeout_ms)
{
#ifdef HAVE_LIBUSB_1_0
    struct timeval tv;
    int r;
    
    if (!THIS->object_data->initialized) {
        Pike_error("USB context not initialized\n");
    }
    
    /* Convert milliseconds to timeval */
    tv.tv_sec = timeout_ms / 1000;
    tv.tv_usec = (timeout_ms % 1000) * 1000;
    
    r = libusb_handle_events_timeout(THIS->object_data->ctx, &tv);
    
    pop_n_elems(args);
    push_int(r);
#else
    Pike_error("USB support not available\n");
#endif
}

/*! @decl void enable_backend_events(int enabled)
 *!   Enable or disable automatic USB event processing via Pike's
 *!   backend.
 *!
 *!   When enabled, this USB context is registered with Pike's
 *!   backend system. The backend will automatically process USB
 *!   events, including completing async transfers and invoking
 *!   their callbacks, without any manual polling.
 *!
 *!   This uses a hybrid event model:
 *!   @ul
 *!     @item
 *!       @b{File descriptor monitoring@}: Wakes instantly when USB
 *!       I/O is ready (via @tt{libusb_get_pollfds@}).
 *!     @item
 *!       @b{Periodic timeout checks@}: Ensures transfer timeouts
 *!       are honored (dynamically scheduled every 1-20ms).
 *!   @endul
 *!
 *!   @param enabled
 *!     @int
 *!       @value 1
 *!         Enable backend integration for this context.
 *!       @value 0
 *!         Disable backend integration for this context.
 *!     @endint
 *!
 *!   @throws
 *!     Throws an error if the USB context is not initialized.
 *!
 *!   @note
 *!     Backend events are @b{disabled@} by default. You must
 *!     explicitly enable them.
 *!
 *!   @note
 *!     Pike must be running in backend mode for this to work.
 *!     Return @expr{-1@} from @tt{main()@} to enter the backend,
 *!     or use @expr{Pike.DefaultBackend@} explicitly.
 *!
 *!   @note
 *!     When disabled (or if Pike is not in backend mode), you must
 *!     call @[handle_events_timeout()] manually to process USB
 *!     events and trigger transfer callbacks.
 *!
 *!   @note
 *!     Multiple USB instances can enable backend events
 *!     simultaneously. A single backend callback serves all
 *!     registered contexts efficiently.
 *!
 *!   @example
 *!     object usb = Public.USB.USB();
 *!     object dev = usb->get_device_list()[0];
 *!     dev->open();
 *!     dev->claim_interface(1);
 *!
 *!     // Enable automatic event processing
 *!     usb->enable_backend_events(1);
 *!
 *!     // Set up async callback and submit transfer
 *!     dev->set_transfer_callback(lambda(mapping r) {
 *!       write("Got %d bytes\n", r->actual_length);
 *!     });
 *!     dev->submit_bulk_transfer(0x84, "\0" * 512, 2000);
 *!
 *!     // Enter backend - callbacks fire automatically
 *!     return -1;
 *!
 *!   @seealso
 *!     @[handle_events_timeout()], @[handle_events()],
 *!     @[Device()->set_transfer_callback()],
 *!     @[Device()->submit_bulk_transfer()]
 */
PIKEFUN void enable_backend_events(int enabled)
{
#ifdef HAVE_LIBUSB_1_0
    INT_TYPE enable_flag;
    
    get_all_args("enable_backend_events", args, "%i", &enable_flag);
    
    if (!THIS->object_data->initialized) {
        Pike_error("USB context not initialized\n");
    }
    
    if (enable_flag) {
        /* Enable: Register this context with the backend */
        register_context(THIS->object_data->ctx);
        
        /* Install module-level backend callback if this is the first registration */
        if (!usb_backend_cb) {
            usb_backend_cb = (struct callback *)add_backend_callback(usb_backend_callback, 0, 0);
        }
    } else {
        /* Disable: Unregister this context */
        unregister_context(THIS->object_data->ctx);
        
        /* Remove module-level callback if no contexts remain */
        if (active_ctxs == NULL && usb_backend_cb) {
            remove_callback(usb_backend_cb);
            usb_backend_cb = NULL;
        }
    }
    
    pop_n_elems(args);
    push_int(0);
#else
    Pike_error("USB support not available\n");
#endif
}

}

/*! @endclass
 */

/*! @endmodule USB
 */

/*! @endmodule Public
 */


gotpike.org | Copyright © 2004 - 2019 | Pike is a trademark of Department of Computer and Information Science, Linköping University