UPnP 1.0.0 → 1.1.0

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,187 @@
1
+ # Original code copyright (c) 2005,2007 Assaf Arkin
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'fileutils'
23
+ require 'thread'
24
+ require 'tmpdir'
25
+ require 'UPnP'
26
+
27
+ ##
28
+ # This UUID class is here to make Assaf Arkin's uuid gem not write to $stdout.
29
+ # Used under MIT license (see source code).
30
+ #
31
+ # To generate a UUID:
32
+ #
33
+ # UUID.setup
34
+ # uuid = UUID.new
35
+ # uuid.generate
36
+
37
+ class UPnP::UUID
38
+
39
+ ##
40
+ # File holding the NIC MAC address
41
+
42
+ NIC_FILE = '~/.UPnP/uuid_mac_address'
43
+
44
+ ##
45
+ # Clock multiplier. Converts Time (resolution: seconds) to UUID clock
46
+ # (resolution: 10ns)
47
+
48
+ CLOCK_MULTIPLIER = 10000000
49
+
50
+ ##
51
+ # Clock gap is the number of ticks (resolution: 10ns) between two Ruby Time
52
+ # ticks.
53
+
54
+ CLOCK_GAPS = 100000
55
+
56
+ ##
57
+ # Version number stamped into the UUID to identify it as time-based.
58
+
59
+ VERSION_CLOCK = 0x0100
60
+
61
+ ##
62
+ # Formats supported by the UUID generator.
63
+ #
64
+ # <tt>:default</tt>:: Produces 36 characters, including hyphens separating
65
+ # the UUID value parts
66
+ # <tt>:compact</tt>:: Produces a 32 digits (hexadecimal) value with no
67
+ # hyphens
68
+ # <tt>:urn</tt>:: Adds the prefix <tt>urn:uuid:</tt> to the
69
+ # <tt>:default</tt> format
70
+
71
+ FORMATS = {
72
+ :compact => '%08x%04x%04x%04x%012x',
73
+ :default => '%08x-%04x-%04x-%04x-%012x',
74
+ :urn => 'urn:uuid:%08x-%04x-%04x-%04x-%012x',
75
+ }
76
+
77
+ @uuid = nil
78
+
79
+ ##
80
+ # Sets up the UUID class generates a UUID in the default format.
81
+
82
+ def self.generate(nic_file = NIC_FILE)
83
+ return @uuid.generate if @uuid
84
+ setup nic_file
85
+ @uuid = new
86
+ @uuid.generate
87
+ end
88
+
89
+ ##
90
+ # Discovers the NIC MAC address and saves it to +nic_file+. Works for UNIX
91
+ # (ifconfig) and Windows (ipconfig).
92
+
93
+ def self.setup(nic_file = NIC_FILE)
94
+ nic_file = File.expand_path nic_file
95
+
96
+ return if File.exist? nic_file
97
+
98
+ FileUtils.mkdir_p File.dirname(nic_file)
99
+
100
+ # Run ifconfig for UNIX, or ipconfig for Windows.
101
+ config = ''
102
+ Dir.chdir Dir.tmpdir do
103
+ config << `ifconfig 2>/dev/null`
104
+ config << `ipconfig /all 2>NUL`
105
+ end
106
+
107
+ addresses = config.scan(/[^:\-](?:[\da-z][\da-z][:\-]){5}[\da-z][\da-z][^:\-]/i)
108
+ addresses = addresses.map { |addr| addr[1..-2] }
109
+
110
+ raise Error, 'MAC address not found via ifconfig or ipconfig' if
111
+ addresses.empty?
112
+
113
+ open nic_file, 'w' do |io| io.write addresses.first end
114
+ end
115
+
116
+ ##
117
+ # Creates a new UUID generator using the NIC stored in NIC_FILE.
118
+
119
+ def initialize(nic_file = NIC_FILE)
120
+ if File.exist? nic_file then
121
+ address = File.read nic_file
122
+
123
+ raise Error, "invalid MAC address #{address}" unless
124
+ address =~ /([\da-f]{2}[:\-]){5}[\da-f]{2}/i
125
+ @address = address.scan(/[0-9a-fA-F]{2}/).join.hex & 0x7FFFFFFFFFFF
126
+ else
127
+ @address = rand(0x800000000000) | 0xF00000000000
128
+ end
129
+
130
+ @drift = 0
131
+ @last_clock = (Time.new.to_f * CLOCK_MULTIPLIER).to_i
132
+ @mutex = Mutex.new
133
+ @sequence = rand 0x10000
134
+ end
135
+
136
+ ##
137
+ # Generates a new UUID string using +format+. See FORMATS for a list of
138
+ # supported formats.
139
+
140
+ def generate(format = :default)
141
+ template = FORMATS[format]
142
+
143
+ raise ArgumentError, "unknown UUID format #{format.inspect}" if
144
+ template.nil?
145
+
146
+ # The clock must be monotonically increasing. The clock resolution is at
147
+ # best 100 ns (UUID spec), but practically may be lower (on my setup,
148
+ # around 1ms). If this method is called too fast, we don't have a
149
+ # monotonically increasing clock, so the solution is to just wait.
150
+ #
151
+ # It is possible for the clock to be adjusted backwards, in which case we
152
+ # would end up blocking for a long time. When backward clock is detected,
153
+ # we prevent duplicates by asking for a new sequence number and continue
154
+ # with the new clock.
155
+
156
+ clock = @mutex.synchronize do
157
+ clock = (Time.new.to_f * CLOCK_MULTIPLIER).to_i & 0xFFFFFFFFFFFFFFF0
158
+
159
+ if clock > @last_clock then
160
+ @drift = 0
161
+ @last_clock = clock
162
+ elsif clock == @last_clock then
163
+ drift = @drift += 1
164
+
165
+ if drift < 10000
166
+ @last_clock += 1
167
+ else
168
+ Thread.pass
169
+ nil
170
+ end
171
+ else
172
+ @sequence = rand 0x10000
173
+ @last_clock = clock
174
+ end
175
+ end while not clock
176
+
177
+ template % [
178
+ clock & 0xFFFFFFFF,
179
+ (clock >> 32) & 0xFFFF,
180
+ ((clock >> 48) & 0xFFFF | VERSION_CLOCK),
181
+ @sequence & 0xFFFF,
182
+ @address & 0xFFFFFFFFFFFF
183
+ ]
184
+ end
185
+
186
+ end
187
+
@@ -217,7 +217,9 @@ class UPnP::Control::Service
217
217
 
218
218
  def self.create(description, url)
219
219
  type = description.elements['serviceType'].text.strip
220
- klass_name = type.sub(/#{UPnP::SERVICE_SCHEMA_PREFIX}:([^:]+):.*/, '\1')
220
+
221
+ # HACK need vendor namespaces
222
+ klass_name = type.sub(/urn:[^:]+:service:([^:]+):.*/, '\1')
221
223
 
222
224
  begin
223
225
  klass = const_get klass_name
@@ -386,6 +388,7 @@ class UPnP::Control::Service
386
388
  range = [minimum, maximum, step]
387
389
 
388
390
  range.map do |value|
391
+ value = value.text
389
392
  value =~ /\./ ? Float(value) : Integer(value)
390
393
  end
391
394
  end
@@ -0,0 +1,692 @@
1
+ require 'UPnP'
2
+ require 'UPnP/SSDP'
3
+ require 'UPnP/UUID'
4
+ require 'UPnP/root_server'
5
+ require 'UPnP/service'
6
+ require 'builder'
7
+ require 'fileutils'
8
+
9
+ ##
10
+ # A device contains sub devices, services and holds information about the
11
+ # services provided. If you use ::create, UPnP will maintain device UUIDs
12
+ # across startups.
13
+ #
14
+ # = Creating a UPnP::Device class
15
+ #
16
+ # A concrete UPnP device looks like this:
17
+ #
18
+ # require 'UPnP/device'
19
+ # require 'UPnP/service/content_directory'
20
+ # require 'UPnP/service/connection_manager'
21
+ #
22
+ # class UPnP::Device::MediaServer < UPnP::Device
23
+ # VERSION = '1.0'
24
+ #
25
+ # add_service_id UPnP::Service::ContentDirectory, 'ContentDirectory'
26
+ # add_service_id UPnP::Service::ConnectionManager, 'ConnectorManager'
27
+ # end
28
+ #
29
+ # Require the sub-services and sub-devices this device requires. For a
30
+ # MediaServer, only a ContentDirectory and ConnectionManager service is
31
+ # required.
32
+ #
33
+ # Subclass UPnP::Device in the UPnP::Device namespace. UPnP::Device looks in
34
+ # its own namespace for various information when instantiating the device.
35
+ #
36
+ # Add a VERSION constant for your device implementation. This will be
37
+ # reported in device advertisements.
38
+ #
39
+ # Add the service ids defined in the device specification document. Not every
40
+ # service's type matches up to its service id.
41
+ #
42
+ # = Instantiating a UPnP::Device
43
+ #
44
+ # A device instantiation looks like this:
45
+ #
46
+ # name = Socket.gethostname.split('.', 2).first
47
+ #
48
+ # device = UPnP::Device.create 'MediaServer', name do |ms|
49
+ # ms.manufacturer = 'Eric Hodel'
50
+ # ms.model_name = 'Media Server'
51
+ #
52
+ # ms.add_service 'ContentDirectory'
53
+ # ms.add_service 'ConnectionManager'
54
+ # end
55
+ #
56
+ # The first argument to ::create is the device type. UPnP looks in the
57
+ # UPnP::Device namespace for a constant matching this name. The second is the
58
+ # friendly name of the device. (A hostname-based name seems sane enough for
59
+ # this example.)
60
+ #
61
+ # Various UPnP device settings can be given next. The manufacturer and model
62
+ # name are required by the UPnP specification. The remainder are attributes
63
+ # you can see below.
64
+ #
65
+ # add_service adds a service of the given type to the device. UPnP looks in
66
+ # the UPnP::Service namespace for a constant matching this name.
67
+ #
68
+ # #add_device can be used to add a sub-device. Like ::create, it takes a type
69
+ # and friendly name, and yield a block that you must set the manufacturer and
70
+ # model name in, in addition to any required sub-devices and sub-services.
71
+ #
72
+ # = Running a UPnP Device
73
+ #
74
+ # After instantiating a device it will advertise itself to the network when
75
+ # you call #run.
76
+ #
77
+ # = Creating a UPnP device executable
78
+ #
79
+ # All the methods you need to create a UPnP device executable are built-in,
80
+ # you only need to override option_parser and ::run in your UPnP::Device
81
+ # subclass. See the documentation below for details.
82
+ #
83
+ # When you're done, create an executable file, require your device file, and
84
+ # call ::run on your class:
85
+ #
86
+ # #!/usr/bin/env ruby
87
+ #
88
+ # require 'rubygems'
89
+ # require 'UPnP/device/my_device'
90
+ #
91
+ # UPnP::Device::MyDevice.run
92
+ #
93
+ # Mark it as executable, and you are good to go!
94
+
95
+ class UPnP::Device
96
+
97
+ ##
98
+ # Base device error class
99
+
100
+ class Error < UPnP::Error
101
+ end
102
+
103
+ ##
104
+ # Raised when device validation fails
105
+
106
+ class ValidationError < Error
107
+ end
108
+
109
+ ##
110
+ # Maps services for a device to their service ids
111
+
112
+ SERVICE_IDS = Hash.new { |h, device| h[device] = {} }
113
+
114
+ ##
115
+ # UPnP 1.0 device schema
116
+
117
+ SCHEMA_URN = 'urn:schemas-upnp-org:device-1-0'
118
+
119
+ ##
120
+ # Short device description for the end user
121
+
122
+ attr_accessor :friendly_name
123
+
124
+ ##
125
+ # Manufacturer's name
126
+
127
+ attr_accessor :manufacturer
128
+
129
+ ##
130
+ # Manufacturer's web site
131
+
132
+ attr_accessor :manufacturer_url
133
+
134
+ ##
135
+ # Long model description for the end user
136
+
137
+ attr_accessor :model_description
138
+
139
+ ##
140
+ # Model name
141
+
142
+ attr_accessor :model_name
143
+
144
+ ##
145
+ # Model number
146
+
147
+ attr_accessor :model_number
148
+
149
+ ##
150
+ # Web site for model
151
+
152
+ attr_accessor :model_url
153
+
154
+ ##
155
+ # Unique Device Name (UDN), a universally unique identifier for the device
156
+ # whether root or embedded.
157
+
158
+ attr_accessor :name
159
+
160
+ ##
161
+ # This device's parent device, or nil if it is the root.
162
+
163
+ attr_reader :parent
164
+
165
+ ##
166
+ # Serial number
167
+
168
+ attr_accessor :serial_number
169
+
170
+ ##
171
+ # Devices that are immediate children of this device
172
+
173
+ attr_accessor :sub_devices
174
+
175
+ ##
176
+ # Services that are immediate children of this device
177
+
178
+ attr_accessor :sub_services
179
+
180
+ ##
181
+ # Type of UPnP device. Use type_urn for the full URN
182
+
183
+ attr_reader :type
184
+
185
+ ##
186
+ # Universal Product Code
187
+
188
+ attr_accessor :upc
189
+
190
+ @option_parser = nil
191
+ @options = nil
192
+
193
+ def self.add_service_id(service, id)
194
+ SERVICE_IDS[self][service] = id
195
+ end
196
+
197
+ ##
198
+ # Loads a device of type +type+ and named +friendly_name+, or creates a new
199
+ # device from +block+ and dumps it.
200
+ #
201
+ # If a dump exists for the same device type and friendly_name the dump is
202
+ # loaded and used as defaults. This preserves the device name (UUID) across
203
+ # device restarts.
204
+
205
+ def self.create(type, friendly_name, &block)
206
+ klass = const_get type
207
+
208
+ device_definition = File.join '~', '.UPnP', type, friendly_name
209
+ device_definition = File.expand_path device_definition
210
+
211
+ device = nil
212
+
213
+ if File.exist? device_definition then
214
+ open device_definition, 'rb' do |io|
215
+ device = Marshal.load io.read
216
+ end
217
+
218
+ yield device if block_given?
219
+ else
220
+ device = klass.new type, friendly_name, &block
221
+ end
222
+
223
+ device.dump
224
+ device
225
+ rescue NameError => e
226
+ raise unless e.message =~ /UPnP::Service::#{type}/
227
+ raise Error, "unknown device type #{type}"
228
+ end
229
+
230
+ ##
231
+ # True when in debug mode
232
+
233
+ def self.debug?
234
+ @debug ||= false
235
+ end
236
+
237
+ ##
238
+ # Set debug mode to +value+
239
+
240
+ def self.debug=(value)
241
+ @debug = value
242
+ end
243
+
244
+ ##
245
+ # Creates an instance of the UPnP::Device subclass named +type+ if it is in
246
+ # the UPnP::Device namespace.
247
+
248
+ def self.new(type, *args)
249
+ if UPnP::Device == self then
250
+ klass = begin
251
+ const_get type
252
+ rescue NameError
253
+ self
254
+ end
255
+
256
+ klass.new(type, *args)
257
+ else
258
+ super
259
+ end
260
+ end
261
+
262
+ ##
263
+ # Creates a new OptionParser and yields the option parser and an options
264
+ # hash for adding a banner or setting device-specific command line
265
+ # arguments.
266
+ #
267
+ # Example:
268
+ #
269
+ # def self.option_parser
270
+ # super do |option_parser, options|
271
+ # options[:name] = Socket.gethostname.split('.', 2).first
272
+ #
273
+ # option_parser.banner = <<-EOF
274
+ # Usage: #{option_parser.program_name} [options]
275
+ #
276
+ # Starts a thingy with the stuff...
277
+ # EOF
278
+ #
279
+ # option_parser.on '-n', '--name=NAME', 'Set the name' do |value|
280
+ # options[:name] = value
281
+ # end
282
+ # end
283
+ # end
284
+ #
285
+ # option_parser automatically provides debug, help and version options. See
286
+ # also OptionParser in ri for more information on working with OptionParser.
287
+
288
+ def self.option_parser
289
+ require 'optparse'
290
+
291
+ @options = {}
292
+
293
+ @option_parser = OptionParser.new do |option_parser|
294
+ option_parser.version = if const_defined? :VERSION then
295
+ self::VERSION
296
+ else
297
+ UPnP::VERSION
298
+ end
299
+
300
+ option_parser.summary_indent = ' ' * 4
301
+
302
+ yield option_parser, @options
303
+
304
+ option_parser.program_name = File.basename $0 unless
305
+ option_parser.program_name
306
+
307
+ unless option_parser.banner then
308
+ option_parser.banner = "Usage: #{option_parser.program_name} [options]"
309
+ end
310
+
311
+ option_parser.separator ''
312
+
313
+ option_parser.on('--[no-]debug', 'Provide extra logging') do |value|
314
+ @debug = value
315
+ end
316
+ end
317
+ end
318
+
319
+ ##
320
+ # Processes +argv+, but must be overridden in a subclass to
321
+ # create and run the device.
322
+ #
323
+ # Override this in a subclass. The overriden run should super, then #create
324
+ # a device using @options as parsed by option_parser, then call #run on the
325
+ # created device.
326
+ #
327
+ # Example:
328
+ #
329
+ # def self.run(argv = ARGV)
330
+ # super
331
+ #
332
+ # device = create 'MyDevice' do |md|
333
+ # md.manufacturer = '...'
334
+ # # device-specific setup
335
+ # end
336
+ #
337
+ # device.run
338
+ # end
339
+ #
340
+ # run takes care of invalid arguments and options for you by printing out
341
+ # the help followed by the invalid argument.
342
+
343
+ def self.run(argv = ARGV)
344
+ option_parser.parse argv
345
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument,
346
+ OptionParser::NeedlessArgument => e
347
+ puts option_parser
348
+ puts
349
+ puts e
350
+
351
+ exit 1
352
+ end
353
+
354
+ ##
355
+ # Creates a new device of +type+ using +friendly_name+ with a new name
356
+ # (UUID). Use #dump and ::create to preserve device names.
357
+
358
+ def initialize(type, friendly_name, parent_device = nil)
359
+ @type = type
360
+ @friendly_name = friendly_name
361
+
362
+ @manufacturer ||= nil
363
+ @manufacturer_url ||= nil
364
+
365
+ @model_description ||= nil
366
+ @model_name ||= nil
367
+ @model_number ||= nil
368
+ @model_url ||= nil
369
+
370
+ @serial_number ||= nil
371
+ @upc ||= nil
372
+
373
+ @sub_devices ||= []
374
+ @sub_services ||= []
375
+ @parent ||= parent_device
376
+
377
+ yield self if block_given?
378
+
379
+ @name ||= "uuid:#{UPnP::UUID.generate}"
380
+
381
+ @ssdp = nil
382
+ end
383
+
384
+ ##
385
+ # A device is equal to another device if it has the same name
386
+
387
+ def ==(other)
388
+ UPnP::Device === other and @name == other.name
389
+ end
390
+
391
+ ##
392
+ # Adds a sub-device of +type+ with +friendly_name+. Devices must have
393
+ # unique types and friendly names. A sub-device will not be created if it
394
+ # already exists, but the block will be called with the existing sub-device.
395
+
396
+ def add_device(type, friendly_name = type, &block)
397
+ sub_device = @sub_devices.find do |d|
398
+ d.type == type and d.friendly_name == friendly_name
399
+ end
400
+
401
+ if sub_device then
402
+ yield sub_device if block_given?
403
+ return sub_device
404
+ end
405
+
406
+ sub_device = UPnP::Device.new(type, friendly_name, self, &block)
407
+ @sub_devices << sub_device
408
+ sub_device
409
+ end
410
+
411
+ ##
412
+ # Adds a UPnP::Service of +type+. +block+ is passed to the created service
413
+ # for service-specific setup.
414
+
415
+ def add_service(type, &block)
416
+ sub_service = @sub_services.find { |s| s.type == type }
417
+ block.call sub_service if sub_service and block
418
+ return sub_service if sub_service
419
+
420
+ sub_service = UPnP::Service.create(self, type, &block)
421
+ @sub_services << sub_service
422
+ sub_service
423
+ end
424
+
425
+ ##
426
+ # Advertises this device, its sub-devices and services. Always advertises
427
+ # from the root device.
428
+
429
+ def advertise
430
+ addrinfo = Socket.getaddrinfo Socket.gethostname, 0, Socket::AF_INET,
431
+ Socket::SOCK_STREAM
432
+ @hosts = addrinfo.map { |type, port, host, ip,| ip }.uniq
433
+
434
+ @advertise_thread = Thread.start do
435
+ Thread.abort_on_exception = true
436
+
437
+ ssdp.advertise root_device, @server[:Port], @hosts
438
+ end
439
+ end
440
+
441
+ ##
442
+ # Returns an XML document describing the root device
443
+
444
+ def description
445
+ validate
446
+
447
+ description = []
448
+
449
+ xml = Builder::XmlMarkup.new :indent => 2, :target => description
450
+ xml.instruct!
451
+
452
+ xml.root :xmlns => SCHEMA_URN do
453
+ xml.specVersion do
454
+ xml.major 1
455
+ xml.minor 0
456
+ end
457
+
458
+ root_device.device_description xml
459
+ end
460
+
461
+ description.join
462
+ end
463
+
464
+ ##
465
+ # Adds a description for this device to +xml+
466
+
467
+ def device_description(xml)
468
+ validate
469
+
470
+ xml.device do
471
+ xml.deviceType type_urn
472
+ xml.UDN @name
473
+
474
+ xml.friendlyName @friendly_name
475
+
476
+ xml.manufacturer @manufacturer
477
+ xml.manufacturerURL @manufacturer_url if @manufacturer_url
478
+
479
+ xml.modelDescription @model_description if @model_description
480
+ xml.modelName @model_name
481
+ xml.modelNumber @model_number if @model_number
482
+ xml.modelURL @model_url if @model_url
483
+
484
+ xml.serialNumber @serial_number if @serial_number
485
+
486
+ xml.UPC @upc if @upc
487
+
488
+ unless @sub_services.empty? then
489
+ xml.serviceList do
490
+ @sub_services.each do |service|
491
+ service.description(xml)
492
+ end
493
+ end
494
+ end
495
+
496
+ unless @sub_devices.empty? then
497
+ xml.deviceList do
498
+ @sub_devices.each do |device|
499
+ device.device_description(xml)
500
+ end
501
+ end
502
+ end
503
+ end
504
+ end
505
+
506
+ ##
507
+ # This device and all its sub-devices
508
+
509
+ def devices
510
+ [self] + @sub_devices.map do |device|
511
+ device.devices
512
+ end.flatten
513
+ end
514
+
515
+ ##
516
+ # Writes this device description into ~/.UPnP so an identically named
517
+ # version can be created on the next load.
518
+
519
+ def dump
520
+ device_definition = File.join '~', '.UPnP', @type, @friendly_name
521
+ device_definition = File.expand_path device_definition
522
+
523
+ FileUtils.mkdir_p File.dirname(device_definition)
524
+
525
+ open device_definition, 'wb' do |io|
526
+ Marshal.dump self, io
527
+ end
528
+ end
529
+
530
+ ##
531
+ # Custom Marshal method that only dumps device-specific data.
532
+
533
+ def marshal_dump
534
+ [
535
+ @type,
536
+ @friendly_name,
537
+ @sub_devices,
538
+ @sub_services,
539
+ @parent,
540
+ @name,
541
+ @manufacturer,
542
+ @manufacturer_url,
543
+ @model_description,
544
+ @model_name,
545
+ @model_number,
546
+ @model_url,
547
+ @serial_number,
548
+ @upc,
549
+ ]
550
+ end
551
+
552
+ ##
553
+ # Custom Marshal method that only loads device-specific data.
554
+
555
+ def marshal_load(data)
556
+ @type = data.shift
557
+ @friendly_name = data.shift
558
+ @sub_devices = data.shift
559
+ @sub_services = data.shift
560
+ @parent = data.shift
561
+ @name = data.shift
562
+ @manufacturer = data.shift
563
+ @manufacturer_url = data.shift
564
+ @model_description = data.shift
565
+ @model_name = data.shift
566
+ @model_number = data.shift
567
+ @model_url = data.shift
568
+ @serial_number = data.shift
569
+ @upc = data.shift
570
+ end
571
+
572
+ ##
573
+ # This device's root device
574
+
575
+ def root_device
576
+ device = self
577
+ device = device.parent until device.parent.nil?
578
+ device
579
+ end
580
+
581
+ ##
582
+ # Starts a root server for the device and advertises it via SSDP. INT and
583
+ # TERM signal handlers are automatically added, and exit when invoked. This
584
+ # method won't return until the server is shutdown.
585
+
586
+ def run
587
+ setup_server
588
+ advertise
589
+
590
+ puts "listening on port #{@server[:Port]}"
591
+
592
+ trap 'INT' do shutdown; exit end
593
+ trap 'TERM' do shutdown; exit end
594
+
595
+ @server.start
596
+ end
597
+
598
+ ##
599
+ # Retrieves a serviceId for +service+ from the concrete device's service id
600
+ # list
601
+
602
+ def service_id(service)
603
+ service_id = service_ids[service.class]
604
+
605
+ raise Error, "unknown serviceId for #{service.class}" unless service_id
606
+
607
+ service_id
608
+ end
609
+
610
+ ##
611
+ # Retrieves the concrete device's service id list. Requires a SERVICE_IDS
612
+ # constant in the concrete class.
613
+
614
+ def service_ids
615
+ SERVICE_IDS[self.class]
616
+ end
617
+
618
+ ##
619
+ # All service and sub-services of this device
620
+
621
+ def services
622
+ services = @sub_services.dup
623
+ services.push(*@sub_devices.map { |d| d.services })
624
+ services.flatten
625
+ end
626
+
627
+ ##
628
+ # Shut down this device
629
+
630
+ def shutdown
631
+ @advertise_thread.kill if @advertise_thread
632
+
633
+ ssdp.byebye self, @hosts
634
+
635
+ @server.shutdown
636
+ end
637
+
638
+ ##
639
+ # Creates a root server and attaches this device's services to it.
640
+
641
+ def setup_server
642
+ @server = UPnP::RootServer.new self
643
+
644
+ services.each do |service|
645
+ @server.mount_service service
646
+ end
647
+
648
+ @server
649
+ end
650
+
651
+ ##
652
+ # UPnP::SSDP accessor
653
+
654
+ def ssdp
655
+ return @ssdp if @ssdp
656
+
657
+ @ssdp = UPnP::SSDP.new
658
+ @ssdp.log = @server[:Logger]
659
+
660
+ @ssdp
661
+ end
662
+
663
+ ##
664
+ # URN of this device's type
665
+
666
+ def type_urn
667
+ "#{UPnP::DEVICE_SCHEMA_PREFIX}:#{@type}:1"
668
+ end
669
+
670
+ ##
671
+ # Raises a ValidationError if any of the required fields are nil
672
+
673
+ def validate
674
+ raise ValidationError, 'friendly_name missing' if @friendly_name.nil?
675
+ raise ValidationError, 'manufacturer missing' if @manufacturer.nil?
676
+ raise ValidationError, 'model_name missing' if @model_name.nil?
677
+ end
678
+
679
+ ##
680
+ # The version of this device, or the UPnP version if the device did not
681
+ # define it
682
+
683
+ def version
684
+ if self.class.const_defined? :VERSION then
685
+ self.class::VERSION
686
+ else
687
+ UPnP::VERSION
688
+ end
689
+ end
690
+
691
+ end
692
+