# 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 USB device detected on the system.
#
# Devices of the system can be obtained with {Context#devices} .
class Device
include Comparable
include ContextReference
# @return [Context] the context this device belongs to.
attr_reader :context
def initialize context, pDev
@context = context
@pDev = pDev
register_context(context.instance_variable_get(:@ctx), :libusb_unref_device)
Call.libusb_ref_device(pDev)
@pDevDesc = Call::DeviceDescriptor.new
res = Call.libusb_get_device_descriptor(@pDev, @pDevDesc)
LIBUSB.raise_error res, "in libusb_get_device_descriptor" if res!=0
end
# The pointer for ContextReference
private def pointer
@pDev
end
# Open the device and obtain a device handle.
#
# A handle allows you to perform I/O on the device in question.
# This is a non-blocking function; no requests are sent over the bus.
#
# If called with a block, the handle is passed to the block
# and is closed when the block has finished.
#
# You need proper device access:
# * Linux: read+write permissions to /dev/bus/usb//
# * Windows: by installing a WinUSB-driver for the device (see {file:README.rdoc#Usage_on_Windows} )
#
# @return [DevHandle] Handle to the device.
def open
ppHandle = FFI::MemoryPointer.new :pointer
res = Call.libusb_open(@pDev, ppHandle)
LIBUSB.raise_error res, "in libusb_open" if res!=0
handle = DevHandle.new self, ppHandle.read_pointer
return handle unless block_given?
begin
yield handle
ensure
handle.close
end
end
# Open the device and claim an interface.
#
# This is a convenience method to {Device#open} and {DevHandle#claim_interface}.
# Must be called with a block. When the block has finished, the interface
# will be released and the device will be closed.
#
# @param [Interface, Fixnum] interface the interface or it's bInterfaceNumber you wish to claim
def open_interface(interface)
open do |dev|
dev.claim_interface(interface) do
yield dev
end
end
end
# Get the number of the bus that a device is connected to.
def bus_number
Call.libusb_get_bus_number(@pDev)
end
# Get the address of the device on the bus it is connected to.
def device_address
Call.libusb_get_device_address(@pDev)
end
if Call.respond_to?(:libusb_get_port_number)
# Get the number of the port that a device is connected to.
# Available since libusb-1.0.12.
#
# @return [Fixnum, nil] the port number (+nil+ if not available)
# @see #port_numbers
def port_number
r = Call.libusb_get_port_number(@pDev)
r==0 ? nil : r
end
# Get the the parent from the specified device [EXPERIMENTAL].
# Available since libusb-1.0.12.
#
# @return [Device, nil] the device parent or +nil+ if not available
# @see #port_numbers
def parent
pppDevs = FFI::MemoryPointer.new :pointer
Call.libusb_get_device_list(@context.instance_variable_get(:@ctx), pppDevs)
ppDevs = pppDevs.read_pointer
pParent = Call.libusb_get_parent(@pDev)
parent = pParent.null? ? nil : Device.new(@context, pParent)
Call.libusb_free_device_list(ppDevs, 1)
parent
end
# Get the list of all port numbers from root for the specified device.
# Available since libusb-1.0.12.
#
# @return [Array]
# @see #parent
# @see #port_number
def port_numbers
# As per the USB 3.0 specs, the current maximum limit for the depth is 7.
path_len = 7
pPath = FFI::MemoryPointer.new :pointer, path_len
l = if Call.respond_to?(:libusb_get_port_numbers)
Call.libusb_get_port_numbers(@pDev, pPath, path_len)
else
Call.libusb_get_port_path(@context.instance_variable_get(:@ctx), @pDev, pPath, path_len)
end
pPath.read_array_of_uint8(l)
end
alias port_path port_numbers
end
if Call.respond_to?(:libusb_get_device_speed)
# Get the negotiated connection speed for a device.
# Available since libusb-1.0.9.
#
# @return [Symbol] a {Call::Speeds Speeds} symbol, where +:SPEED_UNKNOWN+ means that
# the OS doesn't know or doesn't support returning the negotiated speed.
def device_speed
Call.libusb_get_device_speed(@pDev)
end
end
# Convenience function to retrieve the wMaxPacketSize value for a
# particular endpoint in the active device configuration.
#
# @param [Endpoint, Fixnum] endpoint (address of) the endpoint in question
# @return [Fixnum] the wMaxPacketSize value
def max_packet_size(endpoint)
endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress
res = Call.libusb_get_max_packet_size(@pDev, endpoint)
LIBUSB.raise_error res, "in libusb_get_max_packet_size" unless res>=0
res
end
if Call.respond_to?(:libusb_get_max_alt_packet_size)
# Calculate the maximum packet size which a specific endpoint is capable of
# sending or receiving in the duration of 1 microframe
#
# Only the active configuration is examined. The calculation is based on the
# wMaxPacketSize field in the endpoint descriptor as described in section
# 9.6.6 in the USB 2.0 specifications.
#
# If acting on an isochronous or interrupt endpoint, this function will
# multiply the value found in bits 0:10 by the number of transactions per
# microframe (determined by bits 11:12). Otherwise, this function just
# returns the numeric value found in bits 0:10. For USB 3.0 device, it
# will attempts to retrieve the Endpoint Companion Descriptor to return
# wBytesPerInterval.
#
# This function is useful for setting up isochronous transfers, for example
# you might pass the return value from this function to
# +IsochronousTransfer.packet_lengths=+ in order to set the length field of every
# isochronous packet in a transfer.
#
# Available since libusb-1.0.27.
#
# @param [Interface, Fixnum] interface the interface or its bInterfaceNumber of the interface the endpoint belongs to
# @param [Setting, Fixnum] alternate_setting the alternate setting or its bAlternateSetting
# @param [Endpoint, Fixnum] endpoint (address of) the endpoint in question
# @return [Fixnum] the maximum packet size which can be sent/received on this endpoint
# @see max_iso_packet_size
def max_alt_packet_size(interface, alternate_setting, endpoint)
interface = interface.bInterfaceNumber if interface.respond_to? :bInterfaceNumber
alternate_setting = alternate_setting.bAlternateSetting if alternate_setting.respond_to? :bAlternateSetting
endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress
res = Call.libusb_get_max_alt_packet_size(@pDev, interface, alternate_setting, endpoint)
LIBUSB.raise_error res, "in libusb_get_max_alt_packet_size" unless res>=0
res
end
end
# Calculate the maximum packet size which a specific endpoint is capable is
# sending or receiving in the duration of 1 microframe.
#
# Only the active configution is examined. The calculation is based on the
# wMaxPacketSize field in the endpoint descriptor as described in section 9.6.6
# in the USB 2.0 specifications.
#
# If acting on an isochronous or interrupt endpoint, this function will
# multiply the value found in bits 0:10 by the number of transactions per
# microframe (determined by bits 11:12). Otherwise, this function just returns
# the numeric value found in bits 0:10.
#
# This function is useful for setting up isochronous transfers, for example
# you might use the return value from this function to call
# IsoPacket#alloc_buffer in order to set the length field
# of an isochronous packet in a transfer.
#
# @param [Endpoint, Fixnum] endpoint (address of) the endpoint in question
# @return [Fixnum] the maximum packet size which can be sent/received on this endpoint
# @see max_alt_packet_size
def max_iso_packet_size(endpoint)
endpoint = endpoint.bEndpointAddress if endpoint.respond_to? :bEndpointAddress
res = Call.libusb_get_max_iso_packet_size(@pDev, endpoint)
LIBUSB.raise_error res, "in libusb_get_max_iso_packet_size" unless res>=0
res
end
# Obtain a config descriptor of the device.
#
# @param [Fixnum] index number of the config descriptor
# @return Configuration
def config_descriptor(index)
ppConfig = FFI::MemoryPointer.new :pointer
res = Call.libusb_get_config_descriptor(@pDev, index, ppConfig)
LIBUSB.raise_error res, "in libusb_get_config_descriptor" if res!=0
pConfig = ppConfig.read_pointer
config = Configuration.new(self, pConfig)
config
end
# Size of the Descriptor in Bytes (18 bytes)
def bLength
@pDevDesc[:bLength]
end
# Device Descriptor (0x01)
def bDescriptorType
@pDevDesc[:bDescriptorType]
end
# USB specification release number which device complies too
#
# @return [Integer] in binary-coded decimal
def bcdUSB
@pDevDesc[:bcdUSB]
end
# USB-IF class code for the device (Assigned by USB Org)
#
# * If equal to 0x00, each interface specifies it's own class code
# * If equal to 0xFF, the class code is vendor specified
# * Otherwise field is valid Class Code
def bDeviceClass
@pDevDesc[:bDeviceClass]
end
# USB-IF subclass code for the device, qualified by the {Device#bDeviceClass}
# value (Assigned by USB Org)
def bDeviceSubClass
@pDevDesc[:bDeviceSubClass]
end
# USB-IF protocol code for the device, qualified by the {Device#bDeviceClass}
# and {Device#bDeviceSubClass} values (Assigned by USB Org)
def bDeviceProtocol
@pDevDesc[:bDeviceProtocol]
end
# Maximum Packet Size for Endpoint 0. Valid Sizes are 8, 16, 32, 64
def bMaxPacketSize0
@pDevDesc[:bMaxPacketSize0]
end
# USB-IF vendor ID (Assigned by USB Org)
def idVendor
@pDevDesc[:idVendor]
end
# USB-IF product ID (Assigned by Manufacturer)
def idProduct
@pDevDesc[:idProduct]
end
# Device release number in binary-coded decimal.
def bcdDevice
@pDevDesc[:bcdDevice]
end
# Index of string descriptor describing manufacturer.
def iManufacturer
@pDevDesc[:iManufacturer]
end
# Index of string descriptor describing product.
def iProduct
@pDevDesc[:iProduct]
end
# Index of string descriptor containing device serial number.
def iSerialNumber
@pDevDesc[:iSerialNumber]
end
# Number of Possible Configurations
def bNumConfigurations
@pDevDesc[:bNumConfigurations]
end
def inspect
attrs = []
attrs << "#{self.bus_number}/#{self.device_address}"
attrs << ("%04x:%04x" % [self.idVendor, self.idProduct])
attrs << self.manufacturer
attrs << self.product
attrs << self.serial_number
if self.bDeviceClass == LIBUSB::CLASS_PER_INTERFACE
devclass = self.settings.map {|i|
LIBUSB.dev_string(i.bInterfaceClass, i.bInterfaceSubClass, i.bInterfaceProtocol)
}.join(", ")
else
devclass = LIBUSB.dev_string(self.bDeviceClass, self.bDeviceSubClass, self.bDeviceProtocol)
end
attrs << "(#{devclass})"
attrs.compact!
"\#<#{self.class} #{attrs.join(' ')}>"
end
def try_string_descriptor_ascii(i)
begin
open{|h| h.string_descriptor_ascii(i) }
rescue
"?"
end
end
# Return manufacturer of the device
# @return String
def manufacturer
return @manufacturer if defined? @manufacturer
@manufacturer = try_string_descriptor_ascii(self.iManufacturer)
@manufacturer = @manufacturer.strip if @manufacturer
@manufacturer
end
# Return product name of the device.
# @return String
def product
return @product if defined? @product
@product = try_string_descriptor_ascii(self.iProduct)
@product = @product.strip if @product
@product
end
# Return serial number of the device.
# @return String
def serial_number
return @serial_number if defined? @serial_number
@serial_number = try_string_descriptor_ascii(self.iSerialNumber)
@serial_number = @serial_number.strip if @serial_number
@serial_number
end
# Return configurations of the device.
# @return [Array]
def configurations
configs = []
bNumConfigurations.times do |config_index|
begin
configs << config_descriptor(config_index)
rescue RuntimeError
# On Windows some devices don't return it's configuration.
end
end
configs
end
# Return all interfaces of this device.
# @return [Array]
def interfaces() self.configurations.map {|d| d.interfaces }.flatten end
# Return all interface decriptions of this device.
# @return [Array]
def settings() self.interfaces.map {|d| d.settings }.flatten end
# Return all endpoints of all interfaces of this device.
# @return [Array]
def endpoints() self.settings.map {|d| d.endpoints }.flatten end
def <=>(o)
t = bus_number<=>o.bus_number
t = device_address<=>o.device_address if t==0
t
end
end
end