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.
- data.tar.gz.sig +0 -0
- data/.autotest +9 -0
- data/History.txt +8 -0
- data/Manifest.txt +8 -0
- data/README.txt +18 -7
- data/Rakefile +3 -0
- data/lib/UPnP.rb +1 -1
- data/lib/UPnP/SSDP.rb +288 -25
- data/lib/UPnP/UUID.rb +187 -0
- data/lib/UPnP/control/service.rb +4 -1
- data/lib/UPnP/device.rb +692 -0
- data/lib/UPnP/root_server.rb +143 -0
- data/lib/UPnP/service.rb +376 -0
- data/test/test_UPnP_SSDP.rb +80 -17
- data/test/test_UPnP_SSDP_response.rb +2 -0
- data/test/test_UPnP_SSDP_search.rb +25 -0
- data/test/test_UPnP_control_device.rb +2 -0
- data/test/test_UPnP_control_service.rb +2 -0
- data/test/test_UPnP_device.rb +295 -0
- data/test/test_UPnP_root_server.rb +99 -0
- data/test/test_UPnP_service.rb +171 -0
- data/test/utilities.rb +61 -14
- metadata +36 -4
- metadata.gz.sig +0 -0
data/lib/UPnP/UUID.rb
ADDED
@@ -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
|
+
|
data/lib/UPnP/control/service.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/UPnP/device.rb
ADDED
@@ -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
|
+
|