# -*- coding: utf-8; fill-column: 80 -*- # # Copyright (c) 2012 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' class Device < LIBUSB::Device include Logging # TODO: Document the attr! attr_reader :pDev, :pDevDesc attr_reader :aoap_version, :accessory, :in, :out, :handle 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 def open_and_claim @handle = open @handle.claim_interface(0) @handle.clear_halt(@in) @handle end 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 # 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 # Simple recv method. # @param [Fixnum] buffer_size # The buffer size of the received buffer. # @return [String] the received buffer (at most buffer_size bytes). def recv(buffer_size) begin handle.bulk_transfer(endpoint: @in, dataIn: buffer_size) rescue LIBUSB::ERROR_TIMEOUT nil # maybe we should implement a internal thread, a sleep and a retry end end # Simple send method. # @param [String] data # The data to be sent. # @return [Fixnum] the number of bytes sent. def send(data) # TODO: Add timeout param? handle.bulk_transfer(endpoint: @out, dataOut: data) end # Associate an AOAP compatible device with a virtual accessory and switch the Android device # 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 REENUMERATION_DELAY # 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 # Send identifying string information to the device and request the device start up in accessory # mode. def start_accessory_mode logger.debug 'start_accessory_mode' sn = self.serial_number self.open_interface(0) do |handle| @handle = handle send_accessory_id send_start @handle = nil end wait_and_retrieve_by_serial(sn) end # 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 = @context.devices(idVendor: GOOGLE_VID).collect do |d| d.serial_number == sn ? d : nil end.compact.first 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 {Ligo::Device} is in accessory mode. # @return [true, false] true if the {Ligo::Device} is in accessory mode, # false otherwise. def accessory_mode? self.idVendor == GOOGLE_VID end # Check if the current {Ligo::Device} supports AOAP. # @return [true, false] true if the {Ligo::Device} supports AOAP, false # otherwise. def aoap? @aoap_version = self.get_protocol logger.info "#{self.inspect} supports AOAP version #{@aoap_version}." @aoap_version >= 1 end # Check if the current {Ligo::Device} is in UMS mode. # @return [true, false] true if the {Ligo::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 # Send a 51 control request ("Get Protocol") to figure out if the device # supports the Android accessory protocol. # @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 end # Send identifying string information to the device. 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 # Request the device start up in accessory mode 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 # Internal use only. def wait_and_retrieve_by_serial(sn) sleep REENUMERATION_DELAY # The device should now reappear on the usb bus with the Google vendor id. # We retrieve it by using its serial number. device = @context.devices(idVendor: GOOGLE_VID).collect do |d| d.serial_number == sn ? d : nil end.compact.first if device # Retrieve new pointers (check if the old ones should be dereferenced) @pDev = device.pDev @pDevDesc = device.pDevDesc 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 end end private :wait_and_retrieve_by_serial end end