rubymtp 0.1

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,298 @@
1
+ require 'facet/string/camelize'
2
+
3
+ module MTP
4
+ class Protocol
5
+
6
+ @@logger = Logger.new(STDERR)
7
+ @@logger.level = Logger::INFO
8
+
9
+ def self.logger
10
+ @@logger
11
+ end
12
+
13
+ class EndPoints
14
+ attr_reader :in, :out, :interrupt
15
+ def initialize(protocol)
16
+ protocol.interface.endpoints.each do |ep|
17
+ if (ep.bEndpointAddress & 0b10000000).zero?
18
+ @out = ep
19
+ else
20
+ if ep.bmAttributes == 0x02
21
+ @in = ep
22
+ elsif ep.bmAttributes == 0x03
23
+ @interrupt = ep
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class Transaction
31
+ attr_accessor :request, :data, :io
32
+ attr_reader :response
33
+ def initialize(ph)
34
+ @ph = ph
35
+ @io = io
36
+ end
37
+
38
+ def io?
39
+ !@io.nil?
40
+ end
41
+
42
+ def send_data(&block)
43
+ @data.transaction_id = @request.transaction_id
44
+ @data.code = @request.code
45
+
46
+ if @ph.split_data_packets?
47
+ Protocol.logger.debug(sprintf("=> %-8s: 0x%08x, 0x%04x - %s", "DATA", @data.transaction_id, @data.code, @data.code.name))
48
+ @ph.raw_send(@data.pack_header)
49
+ if io?
50
+ @ph.raw_send_io(@io, @data.size, &block)
51
+ else
52
+ @ph.raw_send(@data.pack_payload, &block)
53
+ end
54
+ else
55
+ @ph.send(@data)
56
+ end
57
+ end
58
+
59
+ def read_data(&block)
60
+ @data = @response
61
+ if @ph.split_data_packets?
62
+ if io?
63
+ @data.payload = @ph.raw_read_io(@io, @data.length - 12, &block)
64
+ else
65
+ @data.payload = @ph.raw_read
66
+ end
67
+ end
68
+ end
69
+
70
+ def execute(&block)
71
+ @ph.send(@request)
72
+ send_data(&block) unless @data.nil?
73
+ @response = @ph.receive
74
+ if @response.is_a?(Data)
75
+ read_data(&block)
76
+ @response = @ph.receive
77
+ end
78
+ self
79
+ end
80
+
81
+ def expect(*expects)
82
+ unless expects.empty? or @response.one_of?(*expects)
83
+ raise CommandError.new(@ph, @request, @response)
84
+ end
85
+ end
86
+
87
+ def method_missing(method, *args, &block)
88
+ method = method.to_s
89
+ if method.match(/^(.*)_with_io$/)
90
+ method = $1
91
+ @io = args.shift
92
+ end
93
+ @request = Request.for(method.to_s.camelize, *args)
94
+ execute(&block)
95
+ self
96
+ end
97
+ end
98
+
99
+ def self.mtp?(usb_device, timeout = 1000)
100
+ ret = true
101
+ usb_device.open do |usb|
102
+ # check for MS OS descriptors
103
+ mod = usb.get_string_simple(238)
104
+
105
+ if mod.nil? or mod[0,4] != "MSFT"
106
+ logger.debug("Microsoft OS Descriptor invalid")
107
+ ret = false
108
+ break
109
+ end
110
+ bVendorCode = mod[7]
111
+ result = "\0" * 0x20
112
+ expd = usb.usb_control_msg(USB::USB_ENDPOINT_IN|USB::USB_RECIP_DEVICE|USB::USB_TYPE_VENDOR, bVendorCode, 0, 4, result, timeout)
113
+ unless expd > 0x15 and result[0x12,3] == "MTP"
114
+ logger.debug("Microsoft Extended Descriptor invalid")
115
+ ret = false
116
+ break
117
+ end
118
+
119
+ result = "\0" * 0x20
120
+ expd = usb.usb_control_msg(USB::USB_ENDPOINT_IN|USB::USB_RECIP_DEVICE|USB::USB_TYPE_VENDOR, bVendorCode, 0, 5, result, timeout)
121
+ unless expd > 0x15 and result[0x12,3] == "MTP"
122
+ logger.debug("Microsoft Extended Descriptor invalid")
123
+ ret = false
124
+ break
125
+ end
126
+ end
127
+ ret
128
+ end
129
+
130
+ def logger
131
+ Protocol.logger
132
+ end
133
+
134
+ attr_reader :device
135
+ def initialize(device, usb_device)
136
+ @device = device
137
+ @transaction_id = 0
138
+ @split_data_packets = false
139
+ @usb_handle = nil
140
+ @usb_device = usb_device
141
+ @timeout = 10000
142
+ logger.info("new device #{@usb_device.product}")
143
+ end
144
+
145
+ def split_data_packets?
146
+ @split_data_packets
147
+ end
148
+
149
+ def configuration
150
+ @usb_device.configurations.first
151
+ end
152
+
153
+ def interface
154
+ @usb_device.interfaces.first
155
+ end
156
+
157
+ def transaction_id
158
+ tid = @transaction_id
159
+ @transaction_id = (tid % 0xffffffff) + 1
160
+ tid
161
+ end
162
+
163
+ def raw_send(raw_packet, &block)
164
+ raw_packet = raw_packet.clone
165
+ if (raw_packet.length % @end_points.out.wMaxPacketSize).zero?
166
+ send_empty_packet = true
167
+ logger.warn("packet match max packet size, need to send NULL packet")
168
+ end
169
+ written, total = 0, raw_packet.length
170
+ until raw_packet.empty?
171
+ if (wrt = @usb_handle.usb_bulk_write(@end_points.out.bEndpointAddress,
172
+ raw_packet.slice!(0, @end_points.out.wMaxPacketSize), @timeout)) < 0
173
+ raise WriteError.new(self)
174
+ end
175
+ written += wrt
176
+ yield written, total if block_given?
177
+ end
178
+ end
179
+
180
+ def raw_send_io(io, size, &block)
181
+ packet_size = @end_points.out.wMaxPacketSize
182
+ written = 0
183
+ until (str = io.read(packet_size)).nil?
184
+ if (wrt = @usb_handle.usb_bulk_write(@end_points.out.bEndpointAddress, str, @timeout)) < 0
185
+ raise WriteError.new(self)
186
+ end
187
+ written += wrt
188
+ yield written, size if block_given?
189
+ end
190
+ end
191
+
192
+ def send(request)
193
+ unless request.is_a?(Data) or request.code.name == "GetDeviceInfo"
194
+ raise UnsupportedRequest.new(self, request) unless @device.support?(request.code)
195
+ request.transaction_id = transaction_id
196
+ end
197
+
198
+ raw_send(request.pack)
199
+
200
+ logger.debug(sprintf("=> %-8s: 0x%08x, 0x%04x - %s", (request.is_a?(Data) ? "DATA" : "REQUEST"), request.transaction_id, request.code, request.code.name))
201
+ request
202
+ end
203
+
204
+ def raw_read
205
+ read = packet_size = @end_points.in.wMaxPacketSize
206
+ raw_packet = ""
207
+
208
+ # packet is received when we receive an emtpy packet
209
+ # or a packet whose size is smaller than the packet size
210
+ while read == packet_size
211
+ buffer = "\0" * packet_size
212
+ read = @usb_handle.usb_bulk_read(@end_points.in.bEndpointAddress, buffer, @timeout)
213
+ if read.zero?
214
+ logger.warn("packet match max packet size, need to send NULL packet")
215
+ end
216
+ raw_packet << buffer[0, read]
217
+ end
218
+ raw_packet
219
+ end
220
+
221
+ def raw_read_io(io, size, &block)
222
+ read = packet_size = @end_points.in.wMaxPacketSize
223
+
224
+ total = 0
225
+ # packet is received when we receive an emtpy packet
226
+ # or a packet whose size is smaller than the packet size
227
+ while read == packet_size
228
+ buffer = "\0" * packet_size
229
+ read = @usb_handle.usb_bulk_read(@end_points.in.bEndpointAddress, buffer, @timeout)
230
+ if read.zero?
231
+ logger.warn("packet match max packet size, need to send NULL packet")
232
+ end
233
+ io.write(buffer[0, read])
234
+ total += read
235
+ yield total, size if block_given?
236
+ end
237
+ end
238
+
239
+ def receive
240
+ raw_packet = raw_read
241
+ response = Container.parse(raw_packet)
242
+ logger.debug(sprintf("<= %-8s: 0x%08x, 0x%04x - %s", (response.is_a?(Data) ? "DATA" : "RESPONSE"),
243
+ response.transaction_id, response.code, response.code.name))
244
+ response
245
+ end
246
+
247
+ # do not use normal procedure as we need to determine if we use
248
+ # splitted header / data
249
+ def info
250
+ request = Request.for("GetDeviceInfo")
251
+ request.transaction_id = 0
252
+ send(request)
253
+ data = receive
254
+
255
+ raise CommandError.new(self, request, data, "expected a data packet") unless data.is_a?(Data)
256
+
257
+ if data.payload.empty?
258
+ logger.debug("device split packet headers from payload")
259
+ @split_data_packets = true
260
+ data.payload = raw_read
261
+ end
262
+ response = receive
263
+ yield data if block_given?
264
+ end
265
+
266
+ def open
267
+ logger.debug("low level open")
268
+ @usb_handle = @usb_device.usb_open
269
+ begin
270
+ @usb_handle.set_configuration(configuration)
271
+ @usb_handle.claim_interface(interface.settings.first)
272
+ @end_points = EndPoints.new(self)
273
+ rescue Exception => e
274
+ close
275
+ end
276
+ self
277
+ end
278
+
279
+ def reset_end_points
280
+ @usb_handle.usb_clear_halt(@end_points.out.bEndpointAddress)
281
+ @usb_handle.usb_clear_halt(@end_points.in.bEndpointAddress)
282
+ @usb_handle.usb_clear_halt(@end_points.interrupt.bEndpointAddress)
283
+ end
284
+
285
+ def close
286
+ logger.debug("low level close")
287
+ reset_end_points
288
+ @usb_handle.release_interface(interface.settings.first)
289
+ @usb_handle.usb_reset
290
+ @usb_handle.usb_close
291
+ end
292
+
293
+ def transaction
294
+ Transaction.new(self)
295
+ end
296
+
297
+ end
298
+ end
@@ -0,0 +1,39 @@
1
+ module MTP
2
+ class Storage
3
+ attr_reader(:id, :type, :file_system, :access_capability, :max_capability, :free_space_in_bytes, :free_space_in_objects,
4
+ :description, :volume_label)
5
+
6
+ TYPES = {
7
+ 0x0000 => "Undefined", 0x0001 => "Fixed ROM", 0x0002 => "Removable ROM",
8
+ 0x0003 => "Fixed RAM", 0x0004 => "Removable RAM"
9
+ }
10
+
11
+ FILE_SYSTEMS = {
12
+ 0x0000 => "Undefined", 0x0001 => "Generic Flat", 0x0002 => "Generic Hierarchical", 0x0003 => "DCF"
13
+ }
14
+
15
+
16
+ def self.load(ph, id)
17
+ storage = Storage.new
18
+ t = ph.transaction.get_storage_info(id)
19
+ t.expect("OK")
20
+ storage.instance_eval do
21
+ @id = id
22
+ @type, @file_system, @access_capability, @max_capability, @free_space_in_bytes,
23
+ @free_space_in_objects, @description, @volume_label =
24
+ t.data.payload.unpack("SSSQQIJJ")
25
+ @type = Datacode.new(@type, TYPES)
26
+ @file_system = Datacode.new(@file_system, FILE_SYSTEMS)
27
+ end
28
+ storage
29
+ end
30
+
31
+ def type_description
32
+ TYPES[@type]
33
+ end
34
+
35
+ def file_system_description
36
+ FILE_SYSTEMS[@file_system]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ require 'mp3info'
2
+
3
+ module MTP
4
+ class Track < Object
5
+ end
6
+
7
+ class MP3Track < Track
8
+ register_format self, Datacode.new("MP3")
9
+ property_writer :artist, :track, :genre, :original_release_date, :name, :album_name
10
+ property_accessor :duration
11
+
12
+ def initialize(localname = nil)
13
+ super()
14
+ self.format = "MP3"
15
+ self.localname = localname unless localname.nil?
16
+ end
17
+
18
+ def load(ph, id, payload)
19
+ super(ph, id, payload)
20
+ end
21
+
22
+ # read track data and info from a local file
23
+ def localname=(localname)
24
+ @localname = localname
25
+ @info = Mp3Info.new(localname)
26
+ @info.close
27
+ self.filename = File.basename(@localname)
28
+ self.compressed_size = File.size(@localname)
29
+ end
30
+
31
+ def artist
32
+ new_object? ? @info.tag.artist : properties.artist
33
+ end
34
+
35
+ def name
36
+ new_object? ? @info.tag.title : properties.name
37
+ end
38
+
39
+ def track
40
+ new_object? ? @info.tag.tracknum : properties.track
41
+ end
42
+
43
+ def genre
44
+ new_object? ? @info.tag.genre_s : properties.genre
45
+ end
46
+
47
+ def original_release_date
48
+ new_object? ? Time.local(@info.tag.year) : properties.original_release_date
49
+ end
50
+
51
+ def album_name
52
+ new_object? ? @info.tag.album : properties.album_name
53
+ end
54
+
55
+ def inspect
56
+ "#<MTP::MP3Track:0x%08x @parent_id=0x%08x @artist=%s @track=%u @name=%s @genre=%s @album_name%s>" %
57
+ [ @id, @parent_id, artist.inspect, track, name, genre.inspect, album_name.inspect]
58
+ end
59
+
60
+ def set_properties_from_mp3info(info)
61
+ self.artist = info.tag.artist
62
+ self.name = info.tag.title
63
+ self.track = info.tag.tracknum
64
+ self.genre = info.tag.genre_s
65
+ self.original_release_date = Time.local(info.tag.year) unless info.tag.year.nil?
66
+ self.album_name = info.tag.album
67
+ end
68
+
69
+ def after_sending
70
+ super
71
+ set_properties_from_mp3info(@info) unless @info.nil?
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ diff -ur ruby-mp3info-0.5/lib/mp3info.rb /var/lib/gems/1.8/gems/ruby-mp3info-0.5/lib/mp3info.rb
2
+ --- ruby-mp3info-0.5/lib/mp3info.rb 2005-12-05 23:30:22.000000000 +0100
3
+ +++ /var/lib/gems/1.8/gems/ruby-mp3info-0.5/lib/mp3info.rb 2007-02-14 13:48:48.000000000 +0100
4
+ @@ -155,8 +155,6 @@
5
+ # Instantiate a new Mp3Info object with name +filename+
6
+ def initialize(filename)
7
+ $stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
8
+ - raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
9
+ - @filename = filename
10
+ @hastag1 = false
11
+
12
+ @tag1 = {}
13
+ @@ -164,7 +162,15 @@
14
+
15
+ @tag2 = ID3v2.new
16
+
17
+ - @file = File.new(filename, "rb")
18
+ + if filename.is_a?(String)
19
+ + raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
20
+ + @filename = filename
21
+ + @file = File.new(filename, "rb")
22
+ + @is_io = false
23
+ + else
24
+ + @is_io = true
25
+ + @file = filename
26
+ + end
27
+ @file.extend(Mp3FileMethods)
28
+
29
+ begin
30
+ @@ -245,7 +251,8 @@
31
+ @vbr = true
32
+ else
33
+ # for cbr, calculate duration with the given bitrate
34
+ - @streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
35
+ + size = (@file.respond_to?(:stat) ? @file.stat.size : @file.size)
36
+ + @streamsize = size - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
37
+ @length = ((@streamsize << 3)/1000.0)/@bitrate
38
+ if @tag2["TLEN"]
39
+ # but if another duration is given and it isn't close (within 5%)
40
+ @@ -313,7 +320,7 @@
41
+
42
+ # write to another filename at close()
43
+ def rename(new_filename)
44
+ - @filename = new_filename
45
+ + @filename = new_filename unless @is_io
46
+ end
47
+
48
+ # Flush pending modifications to tags and close the file
49
+ @@ -332,7 +339,7 @@
50
+
51
+ if @tag1 != @tag1_orig
52
+ puts "@tag1 has changed" if $DEBUG
53
+ - raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
54
+ + raise(Mp3InfoError, "file is not writable") if @is_io or !File.writable?(@filename)
55
+ @tag1_orig.update(@tag1)
56
+ #puts "@tag1_orig: #{@tag1_orig.inspect}"
57
+ File.open(@filename, 'rb+') do |file|
58
+ @@ -405,7 +412,7 @@
59
+
60
+ ### parses the id3 tags of the currently open @file
61
+ def parse_tags
62
+ - return if @file.stat.size < TAGSIZE # file is too small
63
+ + #return if @file.stat.size < TAGSIZE # file is too small
64
+ @file.seek(0)
65
+ f3 = @file.read(3)
66
+ gettag1 if f3 == "TAG" # v1 tag at beginning
67
+ @@ -467,7 +474,12 @@
68
+ # is a id3v2 tag.
69
+
70
+ #dummyproof = @file.stat.size - @file.pos => WAS TOO MUCH
71
+ - dummyproof = [ @file.stat.size - @file.pos, 2000000 ].min
72
+ + if @file.respond_to?(:stat)
73
+ + size = @file.stat.size
74
+ + else
75
+ + size = @file.size
76
+ + end
77
+ + dummyproof = [ size - @file.pos, 2000000 ].min
78
+ dummyproof.times do |i|
79
+ if @file.getc == 0xff
80
+ data = @file.read(3)