# This file is part of Libusb for Ruby.
#
# Libusb for Ruby is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Libusb for Ruby is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Libusb for Ruby. If not, see .
require 'libusb/call'
module LIBUSB
# Class representing a libusb session.
class Context
class Pollfd
include Comparable
def initialize(fd, events=0)
@fd, @events = fd, events
end
def <=>(other)
@fd <=> other.fd
end
# @return [IO] IO object bound to the file descriptor.
def io
rio = IO.new @fd
# autoclose is available in Ruby-1.9+ only
rio.autoclose = false if rio.respond_to?( :autoclose= )
rio
end
# @return [Integer] Numeric file descriptor
attr_reader :fd
# @return [Integer] Event flags to poll for
attr_reader :events
# @return [Boolean] True if the file descriptor has to be observed for incoming/readable data
def pollin?
@events & POLLIN != 0
end
# @return [Boolean] True if the file descriptor has to be observed for outgoing/writeable data
def pollout?
@events & POLLOUT != 0
end
def inspect
"\#<#{self.class} fd:#{@fd}#{' POLLIN' if pollin?}#{' POLLOUT' if pollout?}>"
end
end
class CompletionFlag < FFI::Struct
layout :completed, :int
def completed?
self[:completed] != 0
end
def completed=(flag)
self[:completed] = flag ? 1 : 0
end
end
class HotplugCallback < FFI::Struct
layout :handle, :int
attr_reader :context
# @private
def initialize(context, ctx, callbacks)
super()
@context = context
@ctx = ctx
@callbacks = callbacks
end
# Deregisters the hotplug callback.
#
# Deregister a callback from a {Context}. This function is safe to call from within
# a hotplug callback.
#
# Since libusb version 1.0.16.
def deregister
Call.libusb_hotplug_deregister_callback(@ctx, self[:handle])
@callbacks.delete(self[:handle])
end
end
# Initialize libusb context.
def initialize
m = FFI::MemoryPointer.new :pointer
res = Call.libusb_init(m)
LIBUSB.raise_error res, "in libusb_init" if res!=0
@ctx = m.read_pointer
@on_pollfd_added = nil
@on_pollfd_removed = nil
@hotplug_callbacks = {}
end
# Deinitialize libusb.
#
# Should be called after closing all open devices and before your application terminates.
def exit
Call.libusb_exit(@ctx)
end
# Set message verbosity.
#
# * Level 0: no messages ever printed by the library (default)
# * Level 1: error messages are printed to stderr
# * Level 2: warning and error messages are printed to stderr
# * Level 3: informational messages are printed to stdout, warning and
# error messages are printed to stderr
#
# The default level is 0, which means no messages are ever printed. If you
# choose to increase the message verbosity level, ensure that your
# application does not close the stdout/stderr file descriptors.
#
# You are advised to set level 3. libusb is conservative with its message
# logging and most of the time, will only log messages that explain error
# conditions and other oddities. This will help you debug your software.
#
# If the LIBUSB_DEBUG environment variable was set when libusb was
# initialized, this method does nothing: the message verbosity is
# fixed to the value in the environment variable.
#
# If libusb was compiled without any message logging, this method
# does nothing: you'll never get any messages.
#
# If libusb was compiled with verbose debug message logging, this
# method does nothing: you'll always get messages from all levels.
#
# @param [Fixnum] level debug level to set
def debug=(level)
Call.libusb_set_debug(@ctx, level)
end
def device_list
pppDevs = FFI::MemoryPointer.new :pointer
size = Call.libusb_get_device_list(@ctx, pppDevs)
LIBUSB.raise_error size, "in libusb_get_device_list" if size<0
ppDevs = pppDevs.read_pointer
pDevs = []
size.times do |devi|
pDev = ppDevs.get_pointer(devi*FFI.type_size(:pointer))
pDevs << Device.new(self, pDev)
end
Call.libusb_free_device_list(ppDevs, 1)
pDevs
end
private :device_list
# Handle any pending events in blocking mode.
#
# This method must be called when libusb is running asynchronous transfers.
# This gives libusb the opportunity to reap pending transfers,
# invoke callbacks, etc.
#
# If a zero timeout is passed, this function will handle any already-pending
# events and then immediately return in non-blocking style.
#
# If a non-zero timeout is passed and no events are currently pending, this
# method will block waiting for events to handle up until the specified timeout.
# If an event arrives or a signal is raised, this method will return early.
#
# If the parameter completion_flag is used, then after obtaining the event
# handling lock this function will return immediately if the flag is set to completed.
# This allows for race free waiting for the completion of a specific transfer.
# See source of {Transfer#submit_and_wait} for a use case of completion_flag.
#
# @param [Integer, nil] timeout the maximum time (in millseconds) to block waiting for
# events, or 0 for non-blocking mode
# @param [Context::CompletionFlag, nil] completion_flag CompletionFlag to check
def handle_events(timeout=nil, completion_flag=nil)
if completion_flag && !completion_flag.is_a?(Context::CompletionFlag)
raise ArgumentError, "completion_flag is not a CompletionFlag"
end
if timeout
timeval = Call::Timeval.new
timeval.in_ms = timeout
res = if Call.respond_to?(:libusb_handle_events_timeout_completed)
Call.libusb_handle_events_timeout_completed(@ctx, timeval, completion_flag)
else
Call.libusb_handle_events_timeout(@ctx, timeval)
end
else
res = if Call.respond_to?(:libusb_handle_events_completed)
Call.libusb_handle_events_completed(@ctx, completion_flag )
else
Call.libusb_handle_events(@ctx)
end
end
LIBUSB.raise_error res, "in libusb_handle_events" if res<0
end
# Obtain a list of devices currently attached to the USB system, optionally matching certain criteria.
#
# @param [Hash] filter_hash A number of criteria can be defined in key-value pairs.
# Only devices that equal all given criterions will be returned. If a criterion is
# not specified or its value is +nil+, any device will match that criterion.
# The following criteria can be filtered:
# * :idVendor, :idProduct (+FixNum+) for matching vendor/product ID,
# * :bClass, :bSubClass, :bProtocol (+FixNum+) for the device type -
# Devices using CLASS_PER_INTERFACE will match, if any of the interfaces match.
# * :bcdUSB, :bcdDevice, :bMaxPacketSize0 (+FixNum+) for the
# USB and device release numbers.
# Criteria can also specified as Array of several alternative values.
#
# @example
# # Return all devices of vendor 0x0ab1 where idProduct is 3 or 4:
# context.device :idVendor=>0x0ab1, :idProduct=>[0x0003, 0x0004]
#
# @return [Array]
def devices(filter_hash={})
device_list.select do |dev|
( !filter_hash[:bClass] || (dev.bDeviceClass==CLASS_PER_INTERFACE ?
dev.settings.map(&:bInterfaceClass).&([filter_hash[:bClass]].flatten).any? :
[filter_hash[:bClass]].flatten.include?(dev.bDeviceClass))) &&
( !filter_hash[:bSubClass] || (dev.bDeviceClass==CLASS_PER_INTERFACE ?
dev.settings.map(&:bInterfaceSubClass).&([filter_hash[:bSubClass]].flatten).any? :
[filter_hash[:bSubClass]].flatten.include?(dev.bDeviceSubClass))) &&
( !filter_hash[:bProtocol] || (dev.bDeviceClass==CLASS_PER_INTERFACE ?
dev.settings.map(&:bInterfaceProtocol).&([filter_hash[:bProtocol]].flatten).any? :
[filter_hash[:bProtocol]].flatten.include?(dev.bDeviceProtocol))) &&
( !filter_hash[:bMaxPacketSize0] || [filter_hash[:bMaxPacketSize0]].flatten.include?(dev.bMaxPacketSize0) ) &&
( !filter_hash[:idVendor] || [filter_hash[:idVendor]].flatten.include?(dev.idVendor) ) &&
( !filter_hash[:idProduct] || [filter_hash[:idProduct]].flatten.include?(dev.idProduct) ) &&
( !filter_hash[:bcdUSB] || [filter_hash[:bcdUSB]].flatten.include?(dev.bcdUSB) ) &&
( !filter_hash[:bcdDevice] || [filter_hash[:bcdDevice]].flatten.include?(dev.bcdDevice) )
end
end
# Retrieve a list of file descriptors that should be polled by your main
# loop as libusb event sources.
#
# As file descriptors are a Unix-specific concept, this function is not
# available on Windows and will always return +nil+.
#
# @return [Array] list of Pollfd objects,
# +nil+ on error,
# +nil+ on platforms where the functionality is not available
def pollfds
ppPollfds = Call.libusb_get_pollfds(@ctx)
return nil if ppPollfds.null?
offs = 0
pollfds = []
while !(pPollfd=ppPollfds.get_pointer(offs)).null?
pollfd = Call::Pollfd.new pPollfd
pollfds << Pollfd.new(pollfd[:fd], pollfd[:events])
offs += FFI.type_size :pointer
end
if Call.respond_to?(:libusb_free_pollfds)
Call.libusb_free_pollfds(ppPollfds)
else
Stdio.free(ppPollfds)
end
pollfds
end
# Determine the next internal timeout that libusb needs to handle.
#
# You only need to use this function if you are calling poll() or select() or
# similar on libusb's file descriptors yourself - you do not need to use it if
# you are calling {#handle_events} directly.
#
# You should call this function in your main loop in order to determine how long
# to wait for select() or poll() to return results. libusb needs to be called
# into at this timeout, so you should use it as an upper bound on your select() or
# poll() call.
#
# When the timeout has expired, call into {#handle_events} (perhaps
# in non-blocking mode) so that libusb can handle the timeout.
#
# This function may return zero. If this is the
# case, it indicates that libusb has a timeout that has already expired so you
# should call {#handle_events} immediately. A return code
# of +nil+ indicates that there are no pending timeouts.
#
# On some platforms, this function will always returns +nil+ (no pending timeouts).
# See libusb's notes on time-based events.
#
# @return [Float, nil] the timeout in seconds
def next_timeout
timeval = Call::Timeval.new
res = Call.libusb_get_next_timeout @ctx, timeval
LIBUSB.raise_error res, "in libusb_get_next_timeout" if res<0
res == 1 ? timeval.in_s : nil
end
# Register a notification block for file descriptor additions.
#
# This block will be invoked for every new file descriptor that
# libusb uses as an event source.
#
# Note that file descriptors may have been added even before you register these
# notifiers (e.g. at {Context#initialize} time).
#
# @yieldparam [Pollfd] pollfd The added file descriptor is yielded to the block
def on_pollfd_added &block
@on_pollfd_added = proc do |fd, events, _|
pollfd = Pollfd.new fd, events
block.call pollfd
end
Call.libusb_set_pollfd_notifiers @ctx, @on_pollfd_added, @on_pollfd_removed, nil
end
# Register a notification block for file descriptor removals.
#
# This block will be invoked for every removed file descriptor that
# libusb uses as an event source.
#
# Note that the removal notifier may be called during {Context#exit}
# (e.g. when it is closing file descriptors that were opened and added to the poll
# set at {Context#initialize} time). If you don't want this, overwrite the notifier
# immediately before calling {Context#exit}.
#
# @yieldparam [Pollfd] pollfd The removed file descriptor is yielded to the block
def on_pollfd_removed &block
@on_pollfd_removed = proc do |fd, _|
pollfd = Pollfd.new fd
block.call pollfd
end
Call.libusb_set_pollfd_notifiers @ctx, @on_pollfd_added, @on_pollfd_removed, nil
end
# Register a hotplug event notification.
#
# Register a callback with the {LIBUSB::Context}. The callback will fire
# when a matching event occurs on a matching device. The callback is armed
# until either it is deregistered with {HotplugCallback#deregister} or the
# supplied block returns +:finish+ to indicate it is finished processing events.
#
# If the flag {Call::HotplugFlags HOTPLUG_ENUMERATE} is passed the callback will be
# called with a {Call::HotplugEvents :HOTPLUG_EVENT_DEVICE_ARRIVED} for all devices
# already plugged into the machine. Note that libusb modifies its internal
# device list from a separate thread, while calling hotplug callbacks from
# {#handle_events}, so it is possible for a device to already be present
# on, or removed from, its internal device list, while the hotplug callbacks
# still need to be dispatched. This means that when using
# {Call::HotplugFlags HOTPLUG_ENUMERATE}, your callback may be called twice for the arrival
# of the same device, once from {#on_hotplug_event} and once
# from {#handle_events}; and/or your callback may be called for the
# removal of a device for which an arrived call was never made.
#
# Since libusb version 1.0.16.
#
# @param [Hash] args
# @option args [Fixnum,Symbol] :events bitwise or of events that will trigger this callback.
# Default is +LIBUSB::HOTPLUG_EVENT_DEVICE_ARRIVED|LIBUSB::HOTPLUG_EVENT_DEVICE_LEFT+ .
# See {Call::HotplugEvents HotplugEvents}
# @option args [Fixnum,Symbol] :flags hotplug callback flags. Default is 0. See {Call::HotplugFlags HotplugFlags}
# @option args [Fixnum] :vendor_id the vendor id to match. Default is {HOTPLUG_MATCH_ANY}.
# @option args [Fixnum] :product_id the product id to match. Default is {HOTPLUG_MATCH_ANY}.
# @option args [Fixnum] :dev_class the device class to match. Default is {HOTPLUG_MATCH_ANY}.
# @return [HotplugCallback] The handle to the registered callback.
#
# @yieldparam [Device] device the attached or removed {Device} is yielded to the block
# @yieldparam [Symbol] event a {Call::HotplugEvents HotplugEvents} symbol
# @yieldreturn [Symbol] +:finish+ to deregister the callback, +:repeat+ to receive additional events
# @raise [ArgumentError, LIBUSB::Error] in case of failure
def on_hotplug_event(args={}, &block)
events = args.delete(:events) || (HOTPLUG_EVENT_DEVICE_ARRIVED | HOTPLUG_EVENT_DEVICE_LEFT)
flags = args.delete(:flags) || 0
vendor_id = args.delete(:vendor_id) || HOTPLUG_MATCH_ANY
product_id = args.delete(:product_id) || HOTPLUG_MATCH_ANY
dev_class = args.delete(:dev_class) || HOTPLUG_MATCH_ANY
raise ArgumentError, "invalid params #{args.inspect}" unless args.empty?
handle = HotplugCallback.new self, @ctx, @hotplug_callbacks
block2 = proc do |ctx, pDevice, event, _user_data|
raise "internal error: unexpected context" unless @ctx==ctx
dev = Device.new @ctx, pDevice
blres = block.call(dev, event)
case blres
when :finish
1
when :repeat
0
else
raise ArgumentError, "hotplug event handler must return :finish or :repeat"
end
end
res = Call.libusb_hotplug_register_callback(@ctx,
events, flags,
vendor_id, product_id, dev_class,
block2, nil, handle)
LIBUSB.raise_error res, "in libusb_hotplug_register_callback" if res<0
# Avoid GC'ing of the block:
@hotplug_callbacks[handle[:handle]] = block2
return handle
end
end
end