lib/ligo/device.rb
# -*- 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