druzy-upnp 1.0.0 → 2.0.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.
- checksums.yaml +4 -4
- data/lib/druzy/upnp/ssdp.rb +52 -182
- data/lib/druzy/upnp/upnp_device.rb +73 -0
- data/lib/druzy/upnp/upnp_service.rb +74 -0
- data/lib/druzy/upnp/version.rb +2 -2
- metadata +17 -187
- data/lib/core_ext/hash_patch.rb +0 -5
- data/lib/core_ext/socket_patch.rb +0 -16
- data/lib/core_ext/to_upnp_s.rb +0 -65
- data/lib/druzy/upnp/control_point/base.rb +0 -70
- data/lib/druzy/upnp/control_point/device.rb +0 -465
- data/lib/druzy/upnp/control_point/error.rb +0 -15
- data/lib/druzy/upnp/control_point/service.rb +0 -387
- data/lib/druzy/upnp/control_point.rb +0 -161
- data/lib/druzy/upnp/device.rb +0 -31
- data/lib/druzy/upnp/ssdp/broadcast_searcher.rb +0 -110
- data/lib/druzy/upnp/ssdp/error.rb +0 -8
- data/lib/druzy/upnp/ssdp/listener.rb +0 -36
- data/lib/druzy/upnp/ssdp/multicast_connection.rb +0 -109
- data/lib/druzy/upnp/ssdp/network_constants.rb +0 -19
- data/lib/druzy/upnp/ssdp/notifier.rb +0 -39
- data/lib/druzy/upnp/ssdp/searcher.rb +0 -85
- data/lib/rack/upnp_control_point.rb +0 -70
@@ -1,387 +0,0 @@
|
|
1
|
-
require 'savon'
|
2
|
-
require_relative 'base'
|
3
|
-
require_relative 'error'
|
4
|
-
require 'core_ext/hash_patch'
|
5
|
-
|
6
|
-
require 'em-http'
|
7
|
-
HTTPI.adapter = :em_http
|
8
|
-
|
9
|
-
module Druzy
|
10
|
-
module Upnp
|
11
|
-
class ControlPoint
|
12
|
-
|
13
|
-
# An object of this type functions as somewhat of a proxy to a UPnP device's
|
14
|
-
# service. The object sort of defines itself when you call #fetch; it
|
15
|
-
# gets the description file from the device, parses it, populates its
|
16
|
-
# attributes (as accessors) and defines singleton methods from the list of
|
17
|
-
# actions that the service defines.
|
18
|
-
#
|
19
|
-
# After the fetch is done, you can call Ruby methods on the service and
|
20
|
-
# expect a Ruby Hash back as a return value. The methods will look just
|
21
|
-
# the SOAP actions and will always return a Hash, where key/value pairs are
|
22
|
-
# converted from the SOAP response; values are converted to the according
|
23
|
-
# Ruby type based on <dataType> in the <serviceStateTable>.
|
24
|
-
#
|
25
|
-
# Types map like:
|
26
|
-
# * Integer
|
27
|
-
# * ui1
|
28
|
-
# * ui2
|
29
|
-
# * ui4
|
30
|
-
# * i1
|
31
|
-
# * i2
|
32
|
-
# * i4
|
33
|
-
# * int
|
34
|
-
# * Float
|
35
|
-
# * r4
|
36
|
-
# * r8
|
37
|
-
# * number
|
38
|
-
# * fixed.14.4
|
39
|
-
# * float
|
40
|
-
# * String
|
41
|
-
# * char
|
42
|
-
# * string
|
43
|
-
# * uuid
|
44
|
-
# * TrueClass
|
45
|
-
# * 1
|
46
|
-
# * true
|
47
|
-
# * yes
|
48
|
-
# * FalseClass
|
49
|
-
# * 0
|
50
|
-
# * false
|
51
|
-
# * no
|
52
|
-
#
|
53
|
-
# @example No "in" params
|
54
|
-
# my_service.GetSystemUpdateID # => { "Id" => 1 }
|
55
|
-
#
|
56
|
-
class Service < Base
|
57
|
-
include EventMachine::Deferrable
|
58
|
-
|
59
|
-
#vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
60
|
-
# Passed in by +service_list_info+
|
61
|
-
#
|
62
|
-
|
63
|
-
# @return [String] UPnP service type, including URN.
|
64
|
-
attr_reader :service_type
|
65
|
-
|
66
|
-
# @return [String] Service identifier, unique within this service's devices.
|
67
|
-
attr_reader :service_id
|
68
|
-
|
69
|
-
# @return [URI::HTTP] Service description URL.
|
70
|
-
attr_reader :scpd_url
|
71
|
-
|
72
|
-
# @return [URI::HTTP] Control URL.
|
73
|
-
attr_reader :control_url
|
74
|
-
|
75
|
-
# @return [URI::HTTP] Eventing URL.
|
76
|
-
attr_reader :event_sub_url
|
77
|
-
|
78
|
-
#
|
79
|
-
# DONE +service_list_info+
|
80
|
-
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
81
|
-
|
82
|
-
#vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
83
|
-
# Determined by service description file
|
84
|
-
#
|
85
|
-
|
86
|
-
# @return [String]
|
87
|
-
attr_reader :xmlns
|
88
|
-
|
89
|
-
# @return [String]
|
90
|
-
attr_reader :spec_version
|
91
|
-
|
92
|
-
# @return [Array<Hash>]
|
93
|
-
attr_reader :action_list
|
94
|
-
|
95
|
-
# Probably don't need to keep this long-term; just adding for testing.
|
96
|
-
attr_reader :service_state_table
|
97
|
-
|
98
|
-
#
|
99
|
-
# DONE description
|
100
|
-
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
101
|
-
|
102
|
-
# @return [Hash] The whole description... just in case.
|
103
|
-
attr_reader :description
|
104
|
-
|
105
|
-
# @param [String] device_base_url URL given (or otherwise determined) by
|
106
|
-
# <URLBase> from the device that owns the service.
|
107
|
-
# @param [Hash] service_list_info Info given in the <serviceList> section
|
108
|
-
# of the device description.
|
109
|
-
def initialize(device_base_url, service_list_info)
|
110
|
-
@service_list_info = service_list_info
|
111
|
-
@action_list = []
|
112
|
-
@xmlns = ''
|
113
|
-
extract_service_list_info(device_base_url)
|
114
|
-
configure_savon
|
115
|
-
end
|
116
|
-
|
117
|
-
# Fetches the service description file, parses it, extracts attributes
|
118
|
-
# into accessors, and defines Ruby methods from SOAP actions. Since this
|
119
|
-
# is a long-ish process, this is done using EventMachine Deferrable
|
120
|
-
# behavior.
|
121
|
-
def fetch
|
122
|
-
if @scpd_url.empty?
|
123
|
-
set_deferred_success self
|
124
|
-
return
|
125
|
-
end
|
126
|
-
|
127
|
-
description_getter = EventMachine::DefaultDeferrable.new
|
128
|
-
get_description(@scpd_url, description_getter)
|
129
|
-
|
130
|
-
description_getter.errback do
|
131
|
-
msg = 'Failed getting service description.'
|
132
|
-
# @todo Should this return self? or should it succeed?
|
133
|
-
set_deferred_status(:failed, msg)
|
134
|
-
|
135
|
-
if ControlPoint.raise_on_remote_error
|
136
|
-
raise ControlPoint::Error, msg
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
description_getter.callback do |description|
|
141
|
-
@description = description
|
142
|
-
@xmlns = @description[:scpd][:@xmlns]
|
143
|
-
extract_spec_version
|
144
|
-
extract_service_state_table
|
145
|
-
|
146
|
-
if @description[:scpd][:actionList]
|
147
|
-
define_methods_from_actions(@description[:scpd][:actionList][:action])
|
148
|
-
end
|
149
|
-
|
150
|
-
set_deferred_status(:succeeded, self)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
private
|
155
|
-
|
156
|
-
# Extracts all of the basic service information from the information
|
157
|
-
# handed over from the device description about the service. The actual
|
158
|
-
# service description info gathering is *not* done here.
|
159
|
-
#
|
160
|
-
# @param [String] device_base_url The URLBase from the device. Used to
|
161
|
-
# build absolute URLs for the service.
|
162
|
-
def extract_service_list_info(device_base_url)
|
163
|
-
@control_url = if @service_list_info[:controlURL]
|
164
|
-
build_url(device_base_url, @service_list_info[:controlURL])
|
165
|
-
else
|
166
|
-
''
|
167
|
-
end
|
168
|
-
|
169
|
-
@event_sub_url = if @service_list_info[:eventSubURL]
|
170
|
-
build_url(device_base_url, @service_list_info[:eventSubURL])
|
171
|
-
else
|
172
|
-
''
|
173
|
-
end
|
174
|
-
|
175
|
-
@service_type = @service_list_info[:serviceType]
|
176
|
-
@service_id = @service_list_info[:serviceId]
|
177
|
-
|
178
|
-
@scpd_url = if @service_list_info[:SCPDURL]
|
179
|
-
build_url(device_base_url, @service_list_info[:SCPDURL])
|
180
|
-
else
|
181
|
-
''
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
def extract_spec_version
|
186
|
-
"#{@description[:scpd][:specVersion][:major]}.#{@description[:scpd][:specVersion][:minor]}"
|
187
|
-
end
|
188
|
-
|
189
|
-
def extract_service_state_table
|
190
|
-
@service_state_table = if @description[:scpd][:serviceStateTable].is_a? Hash
|
191
|
-
@description[:scpd][:serviceStateTable][:stateVariable]
|
192
|
-
elsif @description[:scpd][:serviceStateTable].is_a? Array
|
193
|
-
@description[:scpd][:serviceStateTable].map do |state|
|
194
|
-
state[:stateVariable]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# Determines if <actionList> from the service description contains a
|
200
|
-
# single action or multiple actions and delegates to create Ruby methods
|
201
|
-
# accordingly.
|
202
|
-
#
|
203
|
-
# @param [Hash,Array] action_list The value from <scpd><actionList><action>
|
204
|
-
# from the service description.
|
205
|
-
def define_methods_from_actions(action_list)
|
206
|
-
|
207
|
-
if action_list.is_a? Hash
|
208
|
-
@action_list << action_list
|
209
|
-
define_method_from_action(action_list[:name].to_sym,
|
210
|
-
action_list[:argumentList][:argument])
|
211
|
-
elsif action_list.is_a? Array
|
212
|
-
action_list.each do |action|
|
213
|
-
=begin
|
214
|
-
in_args_count = action[:argumentList][:argument].find_all do |arg|
|
215
|
-
arg[:direction] == 'in'
|
216
|
-
end.size
|
217
|
-
=end
|
218
|
-
@action_list << action
|
219
|
-
args = action[:argumentList] ? action[:argumentList][:argument] : {}
|
220
|
-
define_method_from_action(action[:name].to_sym, args)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# Defines a Ruby method from the SOAP action.
|
226
|
-
#
|
227
|
-
# All resulting methods will either take no arguments or a single Hash as
|
228
|
-
# an argument, whatever the SOAP action describes as its "in" arguments.
|
229
|
-
# If the action describes "in" arguments, then you must provide a Hash
|
230
|
-
# where keys are argument names and values are the values for those
|
231
|
-
# arguments.
|
232
|
-
#
|
233
|
-
# For example, the GetCurrentConnectionInfo action from the
|
234
|
-
# "ConnectionManager:1" service describes an "in" argument named
|
235
|
-
# "ConnectionID" whose dataType is "i4". To call this action via the
|
236
|
-
# Ruby method, it'd look like:
|
237
|
-
#
|
238
|
-
# connection_manager.GetCurrentConnectionInfo({ "ConnectionID" => 42 })
|
239
|
-
#
|
240
|
-
# There is currently no type checking for these "in" arguments.
|
241
|
-
#
|
242
|
-
# Calling that Ruby method will, in turn, call the SOAP action by the same
|
243
|
-
# name, with the body set to:
|
244
|
-
#
|
245
|
-
# <connectionID>42</connectionID>
|
246
|
-
#
|
247
|
-
# The UPnP device providing the service will reply with a SOAP
|
248
|
-
# response--either a fault or with good data--and that will get converted
|
249
|
-
# to a Hash. This Hash will contain key/value pairs defined by the "out"
|
250
|
-
# argument names and values. Each value is converted to an associated
|
251
|
-
# Ruby type, determined from the serviceStateTable. If no return data
|
252
|
-
# is relevant for the request you made, some devices may return an empty
|
253
|
-
# body.
|
254
|
-
#
|
255
|
-
# @param [Symbol] action_name The extracted value from <actionList>
|
256
|
-
# <action><name> from the spec.
|
257
|
-
# @param [Hash,Array] argument_info The extracted values from
|
258
|
-
# <actionList><action><argumentList><argument> from the spec.
|
259
|
-
def define_method_from_action(action_name, argument_info)
|
260
|
-
# Do this here because, for some reason, @service_type is out of scope
|
261
|
-
# in the #request block below.
|
262
|
-
st = @service_type
|
263
|
-
|
264
|
-
define_singleton_method(action_name) do |params|
|
265
|
-
begin
|
266
|
-
response = @soap_client.call(action_name.to_s) do |locals|
|
267
|
-
locals.message_tags 'xmlns:u' => @service_type
|
268
|
-
locals.soap_action "#{st}##{action_name}"
|
269
|
-
#soap.namespaces["s:encodingStyle"] = "http://schemas.xmlsoap.org/soap/encoding/"
|
270
|
-
|
271
|
-
unless params.nil?
|
272
|
-
raise ArgumentError,
|
273
|
-
'Method only accepts Hashes' unless params.is_a? Hash
|
274
|
-
soap.body = params.symbolize_keys!
|
275
|
-
end
|
276
|
-
end
|
277
|
-
rescue Savon::SOAPFault, Savon::HTTPError => ex
|
278
|
-
hash = xml_parser.parse(ex.http.body)
|
279
|
-
msg = <<-MSG
|
280
|
-
SOAP request failure!
|
281
|
-
HTTP response code: #{ex.http.code}
|
282
|
-
HTTP headers: #{ex.http.headers}
|
283
|
-
HTTP body: #{ex.http.body}
|
284
|
-
HTTP body as Hash: #{hash}
|
285
|
-
MSG
|
286
|
-
|
287
|
-
raise(ActionError, msg) if ControlPoint.raise_on_remote_error
|
288
|
-
|
289
|
-
if hash.empty?
|
290
|
-
return ex.http.body
|
291
|
-
else
|
292
|
-
return hash[:Envelope][:Body]
|
293
|
-
end
|
294
|
-
end
|
295
|
-
|
296
|
-
return_value = if argument_info.is_a?(Hash) && argument_info[:direction] == 'out'
|
297
|
-
return_ruby_from_soap(action_name, response, argument_info)
|
298
|
-
elsif argument_info.is_a? Array
|
299
|
-
argument_info.map do |arg|
|
300
|
-
if arg[:direction] == 'out'
|
301
|
-
return_ruby_from_soap(action_name, response, arg)
|
302
|
-
end
|
303
|
-
end
|
304
|
-
else
|
305
|
-
{}
|
306
|
-
end
|
307
|
-
|
308
|
-
return_value
|
309
|
-
end
|
310
|
-
|
311
|
-
end
|
312
|
-
|
313
|
-
# Uses the serviceStateTable to look up the output from the SOAP response
|
314
|
-
# for the given action, then converts it to the according Ruby data type.
|
315
|
-
#
|
316
|
-
# @param [String] action_name The name of the SOAP action that was called
|
317
|
-
# for which this will get the response from.
|
318
|
-
# @param [Savon::SOAP::Response] soap_response The response from making
|
319
|
-
# the SOAP call.
|
320
|
-
# @param [Hash] out_argument The Hash that tells out the "out" argument
|
321
|
-
# which tells what data type to return.
|
322
|
-
# @return [Hash] Key will be the "out" argument name as a Symbol and the
|
323
|
-
# key will be the value as its converted Ruby type.
|
324
|
-
def return_ruby_from_soap(action_name, soap_response, out_argument)
|
325
|
-
out_arg_name = out_argument[:name]
|
326
|
-
#puts "out arg name: #{out_arg_name}"
|
327
|
-
|
328
|
-
related_state_variable = out_argument[:relatedStateVariable]
|
329
|
-
#puts "related state var: #{related_state_variable}"
|
330
|
-
|
331
|
-
state_variable = @service_state_table.find do |state_var_hash|
|
332
|
-
state_var_hash[:name] == related_state_variable
|
333
|
-
end
|
334
|
-
|
335
|
-
#puts "state var: #{state_variable}"
|
336
|
-
|
337
|
-
int_types = %w[ui1 ui2 ui4 i1 i2 i4 int]
|
338
|
-
float_types = %w[r4 r8 number fixed.14.4 float]
|
339
|
-
string_types = %w[char string uuid]
|
340
|
-
true_types = %w[1 true yes]
|
341
|
-
false_types = %w[0 false no]
|
342
|
-
|
343
|
-
if soap_response.success? && soap_response.to_xml.empty?
|
344
|
-
return {}
|
345
|
-
end
|
346
|
-
|
347
|
-
if int_types.include? state_variable[:dataType]
|
348
|
-
{
|
349
|
-
out_arg_name.to_sym => soap_response.
|
350
|
-
hash[:Envelope][:Body]["#{action_name}Response".to_sym][out_arg_name.to_sym].to_i
|
351
|
-
}
|
352
|
-
elsif string_types.include? state_variable[:dataType]
|
353
|
-
{
|
354
|
-
out_arg_name.to_sym => soap_response.
|
355
|
-
hash[:Envelope][:Body]["#{action_name}Response".to_sym][out_arg_name.to_sym].to_s
|
356
|
-
}
|
357
|
-
elsif float_types.include? state_variable[:dataType]
|
358
|
-
{
|
359
|
-
out_arg_name.to_sym => soap_response.
|
360
|
-
hash[:Envelope][:Body]["#{action_name}Response".to_sym][out_arg_name.to_sym].to_f
|
361
|
-
}
|
362
|
-
elsif true_types.include? state_variable[:dataType]
|
363
|
-
{
|
364
|
-
out_arg_name.to_sym => true
|
365
|
-
}
|
366
|
-
elsif false_types.include? state_variable[:dataType]
|
367
|
-
{
|
368
|
-
out_arg_name.to_sym => false
|
369
|
-
}
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
def configure_savon
|
374
|
-
@soap_client = Savon.client do |globals|
|
375
|
-
globals.endpoint @control_url
|
376
|
-
globals.namespace @service_type
|
377
|
-
|
378
|
-
globals.convert_request_keys_to :camelcase
|
379
|
-
globals.namespace_identifier :u
|
380
|
-
globals.env_namespace :s
|
381
|
-
globals.log true
|
382
|
-
end
|
383
|
-
end
|
384
|
-
end
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|
@@ -1,161 +0,0 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'nori'
|
3
|
-
require 'em-synchrony'
|
4
|
-
require_relative 'ssdp'
|
5
|
-
require_relative 'control_point/service'
|
6
|
-
require_relative 'control_point/device'
|
7
|
-
require_relative 'control_point/error'
|
8
|
-
|
9
|
-
module Druzy
|
10
|
-
module Upnp
|
11
|
-
|
12
|
-
# Allows for controlling a UPnP device as defined in the UPnP spec for control
|
13
|
-
# points.
|
14
|
-
#
|
15
|
-
# It uses +Nori+ for parsing the description XML files, which will use +Nokogiri+
|
16
|
-
# if you have it installed.
|
17
|
-
class ControlPoint
|
18
|
-
|
19
|
-
def self.config
|
20
|
-
yield self
|
21
|
-
end
|
22
|
-
|
23
|
-
class << self
|
24
|
-
attr_accessor :raise_on_remote_error
|
25
|
-
end
|
26
|
-
|
27
|
-
@@raise_on_remote_error ||= true
|
28
|
-
|
29
|
-
attr_reader :devices
|
30
|
-
|
31
|
-
# @param [String] search_target The device(s) to control.
|
32
|
-
# @param [Hash] search_options Options to pass on to SSDP search and listen calls.
|
33
|
-
# @option options [Fixnum] response_wait_time
|
34
|
-
# @option options [Fixnum] m_search_count
|
35
|
-
# @option options [Fixnum] ttl
|
36
|
-
def initialize(search_target, search_options = {})
|
37
|
-
@search_target = search_target
|
38
|
-
@search_options = search_options
|
39
|
-
@search_options[:ttl] ||= 4
|
40
|
-
@devices = []
|
41
|
-
@new_device_channel = EventMachine::Channel.new
|
42
|
-
@old_device_channel = EventMachine::Channel.new
|
43
|
-
end
|
44
|
-
|
45
|
-
# Starts the ControlPoint. If an EventMachine reactor is running already,
|
46
|
-
# it'll join that reactor, otherwise it'll start the reactor.
|
47
|
-
#
|
48
|
-
# @yieldparam [EventMachine::Channel] new_device_channel The means through
|
49
|
-
# which clients can get notified when a new device has been discovered
|
50
|
-
# either through SSDP searching or from an +ssdp:alive+ notification.
|
51
|
-
# @yieldparam [EventMachine::Channel] old_device_channel The means through
|
52
|
-
# which clients can get notified when a device has gone offline (have sent
|
53
|
-
# out a +ssdp:byebye+ notification).
|
54
|
-
def start(&blk)
|
55
|
-
@stopping = false
|
56
|
-
|
57
|
-
starter = -> do
|
58
|
-
ssdp_search_and_listen(@search_target, @search_options)
|
59
|
-
blk.call(@new_device_channel, @old_device_channel)
|
60
|
-
@running = true
|
61
|
-
end
|
62
|
-
|
63
|
-
if EM.reactor_running?
|
64
|
-
starter.call
|
65
|
-
else
|
66
|
-
EM.synchrony(&starter)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def listen(ttl)
|
71
|
-
EM.defer do
|
72
|
-
listener = SSDP.listen(ttl)
|
73
|
-
|
74
|
-
listener.alive_notifications.subscribe do |advertisement|
|
75
|
-
|
76
|
-
if @devices.any? { |d| d.usn == advertisement[:usn] }
|
77
|
-
else
|
78
|
-
create_device(advertisement)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
listener.byebye_notifications.subscribe do |advertisement|
|
83
|
-
|
84
|
-
@devices.reject! do |device|
|
85
|
-
device.usn == advertisement[:usn]
|
86
|
-
end
|
87
|
-
|
88
|
-
@old_device_channel << advertisement
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def ssdp_search_and_listen(search_for, options = {})
|
94
|
-
searcher = SSDP.search(search_for, options)
|
95
|
-
|
96
|
-
searcher.discovery_responses.subscribe do |notification|
|
97
|
-
create_device(notification)
|
98
|
-
end
|
99
|
-
|
100
|
-
# Do I need to do this?
|
101
|
-
EM.add_timer(options[:response_wait_time]) do
|
102
|
-
searcher.close_connection
|
103
|
-
listen(options[:ttl])
|
104
|
-
end
|
105
|
-
|
106
|
-
EM.add_periodic_timer(5) do
|
107
|
-
@timer_time = Time.now
|
108
|
-
puts "<#{self.class}> Device count: #{@devices.size}"
|
109
|
-
puts "<#{self.class}> Device unique: #{@devices.uniq.size}"
|
110
|
-
end
|
111
|
-
|
112
|
-
trap_signals
|
113
|
-
end
|
114
|
-
|
115
|
-
def create_device(notification)
|
116
|
-
deferred_device = Device.new(ssdp_notification: notification)
|
117
|
-
|
118
|
-
deferred_device.errback do |partially_built_device, message|
|
119
|
-
#add_device(partially_built_device)
|
120
|
-
|
121
|
-
if self.class.raise_on_remote_error
|
122
|
-
raise ControlPoint::Error, message
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
deferred_device.callback do |built_device|
|
127
|
-
add_device(built_device)
|
128
|
-
end
|
129
|
-
|
130
|
-
deferred_device.fetch
|
131
|
-
end
|
132
|
-
|
133
|
-
def add_device(built_device)
|
134
|
-
if (@devices.any? { |d| d.usn == built_device.usn }) ||
|
135
|
-
(@devices.any? { |d| d.udn == built_device.udn })
|
136
|
-
#if @devices.any? { |d| d.usn == built_device.usn }
|
137
|
-
else
|
138
|
-
@new_device_channel << built_device
|
139
|
-
@devices << built_device
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def stop
|
144
|
-
@running = false
|
145
|
-
@stopping = false
|
146
|
-
|
147
|
-
EM.stop if EM.reactor_running?
|
148
|
-
end
|
149
|
-
|
150
|
-
def running?
|
151
|
-
@running
|
152
|
-
end
|
153
|
-
|
154
|
-
def trap_signals
|
155
|
-
trap('INT') { stop }
|
156
|
-
trap('TERM') { stop }
|
157
|
-
trap('HUP') { stop } if RUBY_PLATFORM !~ /mswin|mingw/
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
data/lib/druzy/upnp/device.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'thin'
|
2
|
-
require 'em-synchrony'
|
3
|
-
require 'rack'
|
4
|
-
require 'rack/lobster'
|
5
|
-
|
6
|
-
module Druzy
|
7
|
-
module Upnp
|
8
|
-
|
9
|
-
class Device
|
10
|
-
|
11
|
-
# Multicasts discovery messages to advertise its root device, any embedded
|
12
|
-
# devices, and any services.
|
13
|
-
def start
|
14
|
-
EM.synchrony do
|
15
|
-
web_server = Thin::Server.start('0.0.0.0', 3000) do
|
16
|
-
use Rack::CommonLogger
|
17
|
-
use Rack::ShowExceptions
|
18
|
-
|
19
|
-
map '/presentation' do
|
20
|
-
use Rack::Lint
|
21
|
-
run Rack::Lobster.new
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# Do advertisement
|
26
|
-
# Listen for subscribers
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
require 'core_ext/socket_patch'
|
2
|
-
require_relative 'network_constants'
|
3
|
-
require 'ipaddr'
|
4
|
-
require 'socket'
|
5
|
-
require 'eventmachine'
|
6
|
-
|
7
|
-
|
8
|
-
# TODO: DRY this up!! (it's mostly the same as Playful::SSDP::MulticastConnection)
|
9
|
-
module Druzy
|
10
|
-
module Upnp
|
11
|
-
class SSDP
|
12
|
-
class BroadcastSearcher < EventMachine::Connection
|
13
|
-
include EventMachine::Deferrable
|
14
|
-
include Druzy::Upnp::SSDP::NetworkConstants
|
15
|
-
|
16
|
-
# @return [Array] The list of responses from the current discovery request.
|
17
|
-
attr_reader :discovery_responses
|
18
|
-
|
19
|
-
attr_reader :available_responses
|
20
|
-
attr_reader :byebye_responses
|
21
|
-
|
22
|
-
def initialize(search_target, response_wait_time, ttl=TTL)
|
23
|
-
@ttl = ttl
|
24
|
-
@discovery_responses = []
|
25
|
-
@alive_notifications = []
|
26
|
-
@byebye_notifications = []
|
27
|
-
|
28
|
-
setup_broadcast_socket
|
29
|
-
|
30
|
-
@search = m_search(search_target, response_wait_time)
|
31
|
-
end
|
32
|
-
|
33
|
-
def post_init
|
34
|
-
if send_datagram(@search, BROADCAST_IP, MULTICAST_PORT) > 0
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def m_search(search_target, response_wait_time)
|
39
|
-
<<-MSEARCH
|
40
|
-
M-SEARCH * HTTP/1.1\r
|
41
|
-
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
42
|
-
MAN: "ssdp:discover"\r
|
43
|
-
MX: #{response_wait_time}\r
|
44
|
-
ST: #{search_target}\r
|
45
|
-
\r
|
46
|
-
MSEARCH
|
47
|
-
end
|
48
|
-
|
49
|
-
# Gets the IP and port from the peer that just sent data.
|
50
|
-
#
|
51
|
-
# @return [Array<String,Fixnum>] The IP and port.
|
52
|
-
def peer_info
|
53
|
-
peer_bytes = get_peername[2, 6].unpack('nC4')
|
54
|
-
port = peer_bytes.first.to_i
|
55
|
-
ip = peer_bytes[1, 4].join('.')
|
56
|
-
|
57
|
-
[ip, port]
|
58
|
-
end
|
59
|
-
|
60
|
-
def receive_data(response)
|
61
|
-
ip, port = peer_info
|
62
|
-
parsed_response = parse(response)
|
63
|
-
|
64
|
-
if parsed_response.has_key? :nts
|
65
|
-
if parsed_response[:nts] == 'ssdp:alive'
|
66
|
-
@alive_notifications << parsed_response
|
67
|
-
elsif parsed_response[:nts] == 'ssdp:bye-bye'
|
68
|
-
@byebye_notifications << parsed_response
|
69
|
-
else
|
70
|
-
raise "Unknown NTS value: #{parsed_response[:nts]}"
|
71
|
-
end
|
72
|
-
else
|
73
|
-
@discovery_responses << parsed_response
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Converts the headers to a set of key-value pairs.
|
78
|
-
#
|
79
|
-
# @param [String] data The data to convert.
|
80
|
-
# @return [Hash] The converted data. Returns an empty Hash if it didn't
|
81
|
-
# know how to parse.
|
82
|
-
def parse(data)
|
83
|
-
new_data = {}
|
84
|
-
|
85
|
-
unless data =~ /\n/
|
86
|
-
return new_data
|
87
|
-
end
|
88
|
-
|
89
|
-
data.each_line do |line|
|
90
|
-
line =~ /(\S*):(.*)/
|
91
|
-
|
92
|
-
unless $1.nil?
|
93
|
-
key = $1
|
94
|
-
value = $2
|
95
|
-
key = key.gsub('-', '_').downcase.to_sym
|
96
|
-
new_data[key] = value.strip
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
new_data
|
101
|
-
end
|
102
|
-
|
103
|
-
# Sets Socket options to allow for brodcasting.
|
104
|
-
def setup_broadcast_socket
|
105
|
-
set_sock_opt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|