ligo 0.1.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ # -*- coding: utf-8; fill-column: 80 -*-
2
+ #
3
+ # Copyright (c) 2012 Renaud AUBIN
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Ligo
19
+
20
+ require 'ligo/constants'
21
+
22
+ class Device < LIBUSB::Device
23
+ include Logging
24
+
25
+ # TODO: Document the attr!
26
+ attr_reader :pDev, :pDevDesc
27
+ attr_reader :aoap_version, :accessory, :in, :out, :handle
28
+
29
+ def initialize context, pDev
30
+ @aoap_version = 0
31
+ @accessory, @in, @out, @handle = nil, nil, nil, nil
32
+ super context, pDev
33
+ end
34
+
35
+ def process(&block)
36
+ begin
37
+ self.open_interface(0) do |handle|
38
+ @handle = handle
39
+ yield handle
40
+ @handle = nil
41
+ end
42
+ # close
43
+ rescue LIBUSB::ERROR_NO_DEVICE
44
+ msg = 'The target device has been disconnected'
45
+ logger.debug msg
46
+ # close
47
+ raise Interrupt, msg
48
+ end
49
+ end
50
+
51
+ def open_and_claim
52
+ @handle = open
53
+ @handle.claim_interface(0)
54
+ @handle.clear_halt(@in)
55
+ @handle
56
+ end
57
+
58
+ def finalize
59
+ if @handle
60
+ @handle.release_interface(0)
61
+ @handle.close
62
+ end
63
+ end
64
+
65
+ # Simple write method (blocking until timeout).
66
+ # @param [Fixnum] buffer_size
67
+ # The number of bytes expected to be received.
68
+ # @param [Fixnum] timeout
69
+ # The timeout in ms (default: 1000). 0 for an infinite timeout.
70
+ # @return [String] the received buffer (at most buffer_size bytes).
71
+ # @raise [LIBUSB::ERROR_TIMEOUT] in case of timeout.
72
+ def read(buffer_size, timeout = 1000)
73
+ handle.bulk_transfer(endpoint: @in,
74
+ dataIn: buffer_size,
75
+ timeout: timeout)
76
+ end
77
+
78
+ # Simple write method (blocking until timeout).
79
+ # @param [String] buffer
80
+ # The buffer to be sent.
81
+ # @param [Fixnum] timeout
82
+ # The timeout in ms (default: 1000). 0 for an infinite timeout.
83
+ # @return [Fixnum] the number of bytes actually sent.
84
+ # @raise [LIBUSB::ERROR_TIMEOUT] in case of timeout.
85
+ def write(buffer, timeout = 1000)
86
+ handle.bulk_transfer(endpoint: @out,
87
+ dataOut: buffer,
88
+ timeout: timeout)
89
+ end
90
+
91
+ # Simple recv method.
92
+ # @param [Fixnum] buffer_size
93
+ # The buffer size of the received buffer.
94
+ # @return [String] the received buffer (at most buffer_size bytes).
95
+ def recv(buffer_size)
96
+ begin
97
+ handle.bulk_transfer(endpoint: @in,
98
+ dataIn: buffer_size)
99
+ rescue LIBUSB::ERROR_TIMEOUT
100
+ nil
101
+ # maybe we should implement a internal thread, a sleep and a retry
102
+ end
103
+ end
104
+
105
+ # Simple send method.
106
+ # @param [String] data
107
+ # The data to be sent.
108
+ # @return [Fixnum] the number of bytes sent.
109
+ def send(data)
110
+ # TODO: Add timeout param?
111
+ handle.bulk_transfer(endpoint: @out, dataOut: data)
112
+ end
113
+
114
+ # Associate an AOAP compatible device with a virtual accessory and switch the Android device
115
+ # to accessory mode.
116
+ #
117
+ # Prepare an OAP compatible device to interact with a given {Ligo::Accessory}:
118
+ # * Switch the current assigned device to accessory mode
119
+ # * Set the I/O endpoints
120
+ # @param [Ligo::Accessory] accessory
121
+ # The virtual accessory to be associated with the Android device.
122
+ # @return [true, false] true for success, false otherwise.
123
+ def attach_accessory(accessory)
124
+ logger.debug "attach_accessory(#{accessory})"
125
+
126
+ @accessory = accessory
127
+
128
+ if accessory_mode?
129
+ # if the device is already in accessory mode, we send
130
+ # set_configuration to force an usb attached event on the device
131
+ begin
132
+ set_configuration
133
+ rescue LIBUSB::ERROR_NO_DEVICE
134
+ logger.debug ' set_configuration raises LIBUSB::ERROR_NO_DEVICE - Retry'
135
+ sleep REENUMERATION_DELAY
136
+ # Set configuration may fail
137
+ retry
138
+ end
139
+ else
140
+ # the device is not in accessory mode, start_accessory_mode is
141
+ # sufficient to get an usb attached event on the device
142
+ return false unless start_accessory_mode
143
+ end
144
+
145
+ # Find out the in/out endpoints
146
+ self.interfaces.first.endpoints.each do |ep|
147
+ if ep.bEndpointAddress & 0b10000000 == 0
148
+ @out = ep if @out.nil?
149
+ else
150
+ @in = ep if @in.nil?
151
+ end
152
+ end
153
+ true
154
+ end
155
+
156
+ # Send identifying string information to the device and request the device start up in accessory
157
+ # mode.
158
+ def start_accessory_mode
159
+ logger.debug 'start_accessory_mode'
160
+ sn = self.serial_number
161
+
162
+ self.open_interface(0) do |handle|
163
+ @handle = handle
164
+ send_accessory_id
165
+ send_start
166
+ @handle = nil
167
+ end
168
+
169
+ wait_and_retrieve_by_serial(sn)
170
+ end
171
+
172
+ # Set the device's configuration to a value of 1 with a SET_CONFIGURATION (0x09) device
173
+ # request.
174
+ # @return [true, false] true for success, false otherwise.
175
+ def set_configuration
176
+ logger.debug 'set_configuration'
177
+ res = nil
178
+ sn = self.serial_number
179
+ device = @context.devices(idVendor: GOOGLE_VID).collect do |d|
180
+ d.serial_number == sn ? d : nil
181
+ end.compact.first
182
+
183
+ begin
184
+ device.open_interface(0) do |handle|
185
+ req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_STANDARD
186
+ res = handle.control_transfer(bmRequestType: req_type,
187
+ bRequest: LIBUSB::REQUEST_SET_CONFIGURATION,
188
+ wValue: 1, wIndex: 0x0, dataOut: nil)
189
+ end
190
+
191
+ wait_and_retrieve_by_serial(sn)
192
+ res == 0
193
+ end
194
+ end
195
+
196
+ # Check if the current {Ligo::Device} is in accessory mode.
197
+ # @return [true, false] true if the {Ligo::Device} is in accessory mode,
198
+ # false otherwise.
199
+ def accessory_mode?
200
+ self.idVendor == GOOGLE_VID
201
+ end
202
+
203
+ # Check if the current {Ligo::Device} supports AOAP.
204
+ # @return [true, false] true if the {Ligo::Device} supports AOAP, false
205
+ # otherwise.
206
+ def aoap?
207
+ @aoap_version = self.get_protocol
208
+ logger.info "#{self.inspect} supports AOAP version #{@aoap_version}."
209
+ @aoap_version >= 1
210
+ end
211
+
212
+ # Check if the current {Ligo::Device} is in UMS mode.
213
+ # @return [true, false] true if the {Ligo::Device} is in UMS mode, false
214
+ # otherwise.
215
+ def uas?
216
+ if RUBY_PLATFORM=~/linux/i
217
+ # http://cateee.net/lkddb/web-lkddb/USB_UAS.html
218
+ (self.settings[0].bInterfaceClass == 0x08) &&
219
+ (self.settings[0].bInterfaceSubClass == 0x06)
220
+ else
221
+ false
222
+ end
223
+ end
224
+
225
+ # Send a 51 control request ("Get Protocol") to figure out if the device
226
+ # supports the Android accessory protocol.
227
+ # @return [Fixnum] the AOAP protocol version supported by the device (0 for
228
+ # no AOAP support).
229
+ def get_protocol
230
+ logger.debug 'get_protocol'
231
+ res, version = 0, 0
232
+ self.open do |h|
233
+
234
+ h.detach_kernel_driver(0) if self.uas? && h.kernel_driver_active?(0)
235
+ req_type = LIBUSB::ENDPOINT_IN | LIBUSB::REQUEST_TYPE_VENDOR
236
+ res = h.control_transfer(bmRequestType: req_type,
237
+ bRequest: COMMAND_GETPROTOCOL,
238
+ wValue: 0x0, wIndex: 0x0, dataIn: 2)
239
+
240
+ version = res.unpack('S')[0]
241
+ end
242
+
243
+ (res.size == 2 && version >= 1 ) ? version : 0
244
+ end
245
+
246
+ # Send identifying string information to the device.
247
+ def send_accessory_id
248
+ logger.debug 'send_accessory_id'
249
+ req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_VENDOR
250
+ @accessory.each do |k,v|
251
+ # Ensure the string is terminated by a null char
252
+ s = "#{v}\0"
253
+ r = @handle.control_transfer(bmRequestType: req_type,
254
+ bRequest: COMMAND_SENDSTRING, wValue: 0x0,
255
+ wIndex: @accessory.keys.index(k), dataOut: s)
256
+
257
+ # TODO: Manage an exception there. This should terminate the program.
258
+ logger.error "Failed to send #{k} string" unless r == s.size
259
+ end
260
+ end
261
+ private :send_accessory_id
262
+
263
+ # Request the device start up in accessory mode
264
+ def send_start
265
+ logger.debug 'send_start'
266
+ req_type = LIBUSB::ENDPOINT_OUT | LIBUSB::REQUEST_TYPE_VENDOR
267
+ res = @handle.control_transfer(bmRequestType: req_type,
268
+ bRequest: COMMAND_START, wValue: 0x0,
269
+ wIndex: 0x0, dataOut: nil)
270
+ end
271
+ private :send_start
272
+
273
+ # Internal use only.
274
+ def wait_and_retrieve_by_serial(sn)
275
+ sleep REENUMERATION_DELAY
276
+ # The device should now reappear on the usb bus with the Google vendor id.
277
+ # We retrieve it by using its serial number.
278
+ device = @context.devices(idVendor: GOOGLE_VID).collect do |d|
279
+ d.serial_number == sn ? d : nil
280
+ end.compact.first
281
+
282
+ if device
283
+ # Retrieve new pointers (check if the old ones should be dereferenced)
284
+ @pDev = device.pDev
285
+ @pDevDesc = device.pDevDesc
286
+ else
287
+ logger.error ['Failed to retrieve the device after switching to ',
288
+ 'accessory mode. This may be due to a lack of proper ',
289
+ 'permissions ⇒ check your udev rules.', "\n",
290
+ 'The Google vendor id rule may look like:', "\n",
291
+ 'SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ',
292
+ 'MODE="0666", GROUP="plugdev"'
293
+ ].join
294
+ end
295
+ end
296
+ private :wait_and_retrieve_by_serial
297
+
298
+ end
299
+
300
+ end
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8; fill-column: 80 -*-
2
+ #
3
+ # Copyright (c) 2012 Renaud AUBIN
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'logger'
19
+
20
+ module Ligo
21
+
22
+ # Logging module.
23
+ #
24
+ # This module enables to share the same logger between all the Ligo classes.
25
+ module Logging
26
+
27
+ def logger
28
+ @logger ||= Logging.logger_for(self.class.name)
29
+ end
30
+
31
+ # Use a hash class-ivar to cache a unique Logger per class:
32
+ @loggers = {}
33
+ @out = STDOUT
34
+
35
+ class << self
36
+ def logger_for(classname)
37
+ @loggers[classname] ||= configure_logger_for(classname)
38
+ end
39
+
40
+ def configure_logger_for(classname)
41
+ logger = Logger.new(@out)
42
+ logger.progname = classname
43
+ logger
44
+ end
45
+
46
+ #
47
+ def configure_logger_output(logout)
48
+ @out = logout if logout != 'STDOUT'
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8; fill-column: 80 -*-
2
+ #
3
+ # Copyright (c) 2012 Renaud AUBIN
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Ligo
19
+ # ligō version
20
+ VERSION = "0.1.0.beta"
21
+ end
data/lib/ligo.rb ADDED
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8; fill-column: 80 -*-
2
+ #
3
+ # Copyright (c) 2012 Renaud AUBIN
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'logger'
19
+ require 'libusb'
20
+
21
+ require 'ligo/version'
22
+
23
+ # Ligo module.
24
+ #
25
+ # This module contains the Android Open Accessory Protocol utility classes to
26
+ # enable custom USB I/O with AOAP-compatible devices.
27
+ #
28
+ # @see http://source.android.com/tech/accessories/aoap/aoa.html
29
+ # @see http://source.android.com/tech/accessories/index.html
30
+ # @see
31
+ # http://developer.android.com/guide/topics/connectivity/usb/accessory.html
32
+ module Ligo
33
+ require 'ligo/logging'
34
+ require 'ligo/constants'
35
+ require 'ligo/accessory'
36
+ require 'ligo/context'
37
+ require 'ligo/device'
38
+ end
data/ligo.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/ligo/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'ligo'
7
+ gem.version = Ligo::VERSION
8
+ gem.authors = ['Renaud Aubin']
9
+ gem.date = Date.today
10
+ gem.summary = %q{A ruby utility to create virtual accessories able to communicate with Android devices.}
11
+ gem.description = %q{Ligo: virtual accessories for Android}
12
+ gem.license = 'Apache License, Version 2.0'
13
+ gem.authors = ['Renaud AUBIN']
14
+ gem.email = 'root@renaud.io'
15
+ gem.homepage = 'https://github.com/nibua-r/ligo#readme'
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ['lib']
21
+
22
+ gem.add_dependency 'libusb', '~> 0.2.2'
23
+
24
+ gem.add_development_dependency 'rspec', '~> 2.4'
25
+ gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
26
+ gem.add_development_dependency 'yard', '~> 0.8'
27
+ gem.add_development_dependency 'pry', '~> 0.9.10'
28
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ligo::Accessory do
4
+ before(:all) do
5
+ @accessory = Ligo::Accessory.new
6
+ end
7
+
8
+ subject { @accessory }
9
+ it { should respond_to :manufacturer}
10
+ it { should respond_to :model}
11
+ it { should respond_to :description}
12
+ it { should respond_to :version}
13
+ it { should respond_to :uri}
14
+ it { should respond_to :serial}
15
+
16
+ describe '#new' do
17
+
18
+ context 'when called with nil' do
19
+ it 'should raise ArgumentError' do
20
+ expect { Ligo::Accessory.new(nil) }.to raise_error(ArgumentError)
21
+ end
22
+ end
23
+
24
+ context 'when called with an empty Hash' do
25
+ it 'should raise ArgumentError' do
26
+ expect { Ligo::Accessory.new(Hash.new) }.to raise_error(ArgumentError)
27
+ end
28
+ end
29
+
30
+ context 'when called with invalid data (> max length)' do
31
+ before(:all) do
32
+ @accessory_arg = {
33
+ manufacturer: 'a',
34
+ model: 'a',
35
+ description: 'a',
36
+ version: 'a',
37
+ uri: 'a',
38
+ serial: (0...256).map{ ('a'..'z').to_a[rand(26)] }.join
39
+ }
40
+ end
41
+
42
+ it 'should raise ArgumentError' do
43
+ expect do
44
+ Ligo::Accessory.new(@accessory_arg)
45
+ end.to raise_error(ArgumentError,
46
+ 'serial must contain at most 255 bytes')
47
+ end
48
+ end
49
+
50
+ context 'when called with invalid data (wrong datatype)' do
51
+ before(:all) do
52
+ @accessory_arg = {
53
+ manufacturer: 0,
54
+ model: 'a',
55
+ description: 'a',
56
+ version: 'a',
57
+ uri: 'a',
58
+ serial: 'a'
59
+ }
60
+ end
61
+
62
+ it 'should raise ArgumentError' do
63
+ expect do
64
+ Ligo::Accessory.new(@accessory_arg)
65
+ end.to raise_error(ArgumentError,
66
+ 'manufacturer is not a String')
67
+ end
68
+ end
69
+
70
+ context 'when called with missing data' do
71
+ before(:all) do
72
+ @accessory_arg = {
73
+ manufacturer: 'a',
74
+ model: 'a',
75
+ description: 'a',
76
+ version: 'a',
77
+ serial: 'a'
78
+ }
79
+ end
80
+
81
+ it 'should raise ArgumentError' do
82
+ expect do
83
+ Ligo::Accessory.new(@accessory_arg)
84
+ end.to raise_error(ArgumentError,
85
+ 'Missing argument: uri')
86
+ end
87
+ end
88
+
89
+ end # describe #new
90
+
91
+ describe '#each' do
92
+ it 'must be implemented soon'
93
+ end # describe #each
94
+
95
+ describe '#keys' do
96
+ it 'must be implemented soon'
97
+ end # describe #keys
98
+
99
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ # Assumption: two devices are connected on the usb bus and not yet in accessory
4
+ # mode. Those devices are a Galaxy Nexus running 4.2 AOSP and a HTC Flyer
5
+ # running 3.2 (with UMS support).
6
+
7
+ describe Ligo::Context do
8
+ it "should derive from LIBUSB::Context" do
9
+ subject.class.superclass.should be LIBUSB::Context
10
+ end
11
+
12
+ describe "#devices" do
13
+ it "should return an Array" do
14
+ subject.devices.class.should be Array
15
+ end
16
+ it "should return 2 devices" do
17
+ subject.devices.size.should equal 2
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ # Assumption: two devices are connected on the usb bus and not yet in accessory
4
+ # mode. Those devices are a Galaxy Nexus running 4.2 AOSP and a HTC Flyer
5
+ # running 3.2 (with UMS support).
6
+
7
+ describe Ligo::Device do
8
+ before(:all) do
9
+ @default_accessory = Ligo::Accessory.new
10
+ ctx = Ligo::Context.new
11
+ @gnexus = ctx.devices(idVendor: 0x04e8).first
12
+ @flyer = ctx.devices(idVendor: 0x0bb4).first
13
+ end
14
+
15
+ it 'should derive from LIBUSB::Device' do
16
+ Ligo::Device.superclass.should be LIBUSB::Device
17
+ end
18
+
19
+ context 'when passing the Galaxy Nexus to accessory mode' do
20
+ specify { @gnexus.should_not be_accessory_mode }
21
+ specify { @gnexus.idVendor.should be 0x04e8 }
22
+ specify { @gnexus.idProduct.should be 0x6860 }
23
+ specify { @gnexus.should be_aoap }
24
+ specify { @gnexus.should_not be_uas }
25
+ specify { @gnexus.aoap_version.should be 2 }
26
+ # Now, the order matters!
27
+ specify { @gnexus.attach_accessory(@default_accessory).should be true }
28
+ specify { @gnexus.idVendor.should be 0x18d1 }
29
+ specify { Ligo::GOOGLE_PIDS.should include @gnexus.idProduct }
30
+ end
31
+
32
+ context 'when passing the Flyer to accessory mode' do
33
+ specify { @flyer.should_not be_accessory_mode }
34
+ specify { @flyer.idVendor.should be 0x0bb4 }
35
+ specify { @flyer.idProduct.should be 0x0ca9 }
36
+ specify { @flyer.should be_aoap }
37
+ specify { @flyer.should be_uas }
38
+ specify { @flyer.aoap_version.should be 1 }
39
+ # Now, the order matters!
40
+ specify { @flyer.attach_accessory(@default_accessory).should be true }
41
+ specify { @flyer.idVendor.should be 0x18d1 }
42
+ specify { Ligo::GOOGLE_PIDS.should include @flyer.idProduct }
43
+ end
44
+
45
+ end
data/spec/ligo_spec.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ligo do
4
+ it "should have a VERSION constant" do
5
+ subject.const_get('VERSION').should_not be_empty
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ gem 'rspec', '~> 2.4'
2
+ require 'rspec'
3
+ require 'ligo'
4
+
5
+ include Ligo
6
+ Ligo::Logging.configure_logger_output('/tmp/ligo-spec.log')