# -*- coding: utf-8; fill-column: 80 -*- # # Copyright (c) 2012, 2013 Renaud AUBIN # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # module Ligo require 'ligo/constants' # USB reenumeration delay in seconds @@reenumeration_delay = 1 def self.setDelay(x) @@reenumeration_delay = x end def self.getDelay @@reenumeration_delay end # This class provides a convenient wrapper class around `LIBUSB::Device` and # implements the Android Open Accessory Protocol to interact with compatible # devices. # # This class is a derivative work of `LIBUSB::Device` as included in # [LIBUSB](https://github.com/larskanis/libusb), written by Lars Kanis and # released under the LGPLv3. # @author Renaud AUBIN # @api public class Device < LIBUSB::Device include Logging # @api private attr_reader :pDev # @api private attr_reader :pDevDesc # Returns the version of the AOA protocol that this device supports # @return [Fixnum] the version of the AOA protocol that this device # supports. attr_reader :aoap_version # Returns the associated {Accessory} # @return [Accessory, nil] the associated accessory if any or nil. attr_reader :accessory # Returns the accessory mode input endpoint # @return [LIBUSB::Endpoint, nil] the input endpoint or nil if the device is # not in accessory mode. attr_reader :in # Returns the accessory mode output endpoint # @return [LIBUSB::Endpoint, nil] the output endpoint or nil if the device # is not in accessory mode. attr_reader :out # Returns the device handle # @todo Improve the :handle doc # @return [LIBUSB::DevHandle, nil] the device handle or nil. attr_reader :handle # @api private def initialize context, pDev @aoap_version = 0 @accessory, @in, @out, @handle = nil, nil, nil, nil super context, pDev end def process(&block) begin self.open_interface(0) do |handle| @handle = handle yield handle @handle = nil end # close rescue LIBUSB::ERROR_NO_DEVICE msg = 'The target device has been disconnected' logger.debug msg # close raise Interrupt, msg end end # Opens an handle and claim the default interface for further operations # @return [LIBUSB::DevHandle] the handle to operate on. # @raise def open_and_claim @handle = open @handle.claim_interface(0) @handle.clear_halt(@in) @handle end # Finalizes the device (release and close) # @return # @raise [LIBUSB::ERROR_TIMEOUT] in case of timeout. def finalize if @handle @handle.release_interface(0) @handle.close end end # Simple write method (blocking until timeout) # @param [Fixnum] buffer_size # The number of bytes expected to be received. # @param [Fixnum] timeout # The timeout in ms (default: 1000). 0 for an infinite timeout. # @return [String] the received buffer (at most buffer_size bytes). # @raise [LIBUSB::ERROR_TIMEOUT] in case of timeout. def read(buffer_size, timeout = 1000) handle.bulk_transfer(endpoint: @in, dataIn: buffer_size, timeout: timeout) end alias_method :recv, :read # Simple write method (blocking until timeout) # @param [String] buffer # The buffer to be sent. # @param [Fixnum] timeout # The timeout in ms (default: 1000). 0 for an infinite timeout. # @return [Fixnum] the number of bytes actually sent. # @raise [LIBUSB::ERROR_TIMEOUT] in case of timeout. def write(buffer, timeout = 1000) handle.bulk_transfer(endpoint: @out, dataOut: buffer, timeout: timeout) end alias_method :send, :write # Associates with an accessory and switch to accessory mode # # Prepare an OAP compatible device to interact with a given {Ligo::Accessory}: # * Switch the current assigned device to accessory mode # * Set the I/O endpoints # @param [Ligo::Accessory] accessory # The virtual accessory to be associated with the Android device. # @return [true, false] true for success, false otherwise. def attach_accessory(accessory) logger.debug "attach_accessory(#{accessory})" @accessory = accessory if accessory_mode? # if the device is already in accessory mode, we send # set_configuration to force an usb attached event on the device begin set_configuration rescue LIBUSB::ERROR_NO_DEVICE logger.debug ' set_configuration raises LIBUSB::ERROR_NO_DEVICE - Retry' sleep Ligo::getDelay # Set configuration may fail retry end else # the device is not in accessory mode, start_accessory_mode is # sufficient to get an usb attached event on the device return false unless start_accessory_mode end # Find out the in/out endpoints self.interfaces.first.endpoints.each do |ep| if ep.bEndpointAddress & 0b10000000 == 0 @out = ep if @out.nil? else @in = ep if @in.nil? end end true end # Switches to accessory mode # # Send identifying string information to the device and request the device start up in accessory # mode. # @return [true, false] true for success, false otherwise. def start_accessory_mode logger.debug 'start_accessory_mode' sn = self.serial_number self.open do |handle| @handle = handle send_accessory_id send_start @handle = nil end wait_and_retrieve_by_serial(sn) end # Sends a `set configuration` control transfer # # Set the device's configuration to a value of 1 with a SET_CONFIGURATION (0x09) device # request. # @return [true, false] true for success, false otherwise. def set_configuration logger.debug 'set_configuration' res = nil sn = self.serial_number device = get_device(sn) begin device.open_interface(0) do |handle| req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_STANDARD res = handle.control_transfer(bmRequestType: req_type, bRequest: LIBUSB::REQUEST_SET_CONFIGURATION, wValue: 1, wIndex: 0x0, dataOut: nil) end wait_and_retrieve_by_serial(sn) res == 0 end end # Check if the current {Device} is in accessory mode # @return [true, false] true if the {Device} is in accessory mode, false # otherwise. def accessory_mode? (self.idVendor == GOOGLE_VID) && (GOOGLE_PIDS.include? self.idProduct) end # Check if the current {Device} supports AOAP # @return [true, false] true if the {Ligo::Device} supports AOAP, false # otherwise. def aoap? @aoap_version = self.get_protocol aoap_supported = (@aoap_version >= 1) if aoap_supported logger.info "#{self.inspect} supports AOA Protocol version #{@aoap_version}." else logger.info "#{self.inspect} doesn't support AOA Protocol." end aoap_supported end # Check if the current {Device} is in UMS mode # @return [true, false] true if the {Device} is in UMS mode, false otherwise def uas? if RUBY_PLATFORM=~/linux/i # http://cateee.net/lkddb/web-lkddb/USB_UAS.html (self.settings[0].bInterfaceClass == 0x08) && (self.settings[0].bInterfaceSubClass == 0x06) else false end end # Sends a `get protocol` control transfer # # Send a 51 control request ("Get Protocol") to figure out if the device # supports the Android accessory protocol. We assume here that the device # has not been opened. # @return [Fixnum] the AOAP protocol version supported by the device (0 for # no AOAP support). def get_protocol logger.debug 'get_protocol' res, version = 0, 0 self.open do |h| h.detach_kernel_driver(0) if self.uas? && h.kernel_driver_active?(0) req_type = LIBUSB::ENDPOINT_IN | LIBUSB::REQUEST_TYPE_VENDOR res = h.control_transfer(bmRequestType: req_type, bRequest: COMMAND_GETPROTOCOL, wValue: 0x0, wIndex: 0x0, dataIn: 2) version = res.unpack('S')[0] end (res.size == 2 && version >= 1 ) ? version : 0 rescue LIBUSB::ERROR_NOT_SUPPORTED, LIBUSB::ERROR_PIPE 0 end # Sends identifying string information to the device # # We assume here that the device has already been opened. # @api private # @return def send_accessory_id logger.debug 'send_accessory_id' req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_VENDOR @accessory.each do |k,v| # Ensure the string is terminated by a null char s = "#{v}\0" r = @handle.control_transfer(bmRequestType: req_type, bRequest: COMMAND_SENDSTRING, wValue: 0x0, wIndex: @accessory.keys.index(k), dataOut: s) # TODO: Manage an exception there. This should terminate the program. logger.error "Failed to send #{k} string" unless r == s.size end end private :send_accessory_id # Sends AOA protocol start command to the device # @api private # @return [Fixnum] def send_start logger.debug 'send_start' req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_VENDOR res = @handle.control_transfer(bmRequestType: req_type, bRequest: COMMAND_START, wValue: 0x0, wIndex: 0x0, dataOut: nil) end private :send_start # Retrieves an AOAP device by its serial number # @api private # @param [String] sn # The serial number of the device to be found. # @return [LIBUSB::Device] the device matching the given serial number. def get_device(sn) device = @context.devices(idVendor: GOOGLE_VID).collect do |d| d.serial_number == sn ? d : nil end.compact.first end # @api private # @return [true, false] true for success, false otherwise. def wait_and_retrieve_by_serial(sn) sleep Ligo::getDelay # The device should now reappear on the usb bus with the Google vendor id. # We retrieve it by using its serial number. device = get_device(sn) if device # Retrieve new pointers (check if the old ones should be dereferenced) @pDev = device.pDev @pDevDesc = device.pDevDesc true else logger.error ['Failed to retrieve the device after switching to ', 'accessory mode. This may be due to a lack of proper ', 'permissions ⇒ check your udev rules.', "\n", 'The Google vendor id rule may look like:', "\n", 'SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ', 'MODE="0666", GROUP="plugdev"' ].join false end end private :wait_and_retrieve_by_serial end end