UPnP 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+