druzy-upnp 1.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 +7 -0
- data/lib/core_ext/hash_patch.rb +5 -0
- data/lib/core_ext/socket_patch.rb +16 -0
- data/lib/core_ext/to_upnp_s.rb +65 -0
- data/lib/druzy/upnp/control_point/base.rb +70 -0
- data/lib/druzy/upnp/control_point/device.rb +465 -0
- data/lib/druzy/upnp/control_point/error.rb +15 -0
- data/lib/druzy/upnp/control_point/service.rb +387 -0
- data/lib/druzy/upnp/control_point.rb +161 -0
- data/lib/druzy/upnp/device.rb +31 -0
- data/lib/druzy/upnp/ssdp/broadcast_searcher.rb +110 -0
- data/lib/druzy/upnp/ssdp/error.rb +8 -0
- data/lib/druzy/upnp/ssdp/listener.rb +36 -0
- data/lib/druzy/upnp/ssdp/multicast_connection.rb +109 -0
- data/lib/druzy/upnp/ssdp/network_constants.rb +19 -0
- data/lib/druzy/upnp/ssdp/notifier.rb +39 -0
- data/lib/druzy/upnp/ssdp/searcher.rb +85 -0
- data/lib/druzy/upnp/ssdp.rb +195 -0
- data/lib/druzy/upnp/version.rb +5 -0
- data/lib/druzy/upnp.rb +7 -0
- data/lib/rack/upnp_control_point.rb +70 -0
- metadata +261 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'multicast_connection'
|
2
|
+
|
3
|
+
|
4
|
+
class Druzy::Upnp::SSDP::Listener < Druzy::Upnp::SSDP::MulticastConnection
|
5
|
+
|
6
|
+
# @return [EventMachine::Channel] Provides subscribers with notifications
|
7
|
+
# from devices that have come online (sent +ssdp:alive+ notifications).
|
8
|
+
attr_reader :alive_notifications
|
9
|
+
|
10
|
+
# @return [EventMachine::Channel] Provides subscribers with notifications
|
11
|
+
# from devices that have gone offline (sent +ssd:byebye+ notifications).
|
12
|
+
attr_reader :byebye_notifications
|
13
|
+
|
14
|
+
# This is the callback called by EventMachine when it receives data on the
|
15
|
+
# socket that's been opened for this connection. In this case, the method
|
16
|
+
# parses the SSDP notifications into Hashes and adds them to the
|
17
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
18
|
+
# effectively means that in each Channel, you get a Hash that represents
|
19
|
+
# the headers for each notification that comes in on the socket.
|
20
|
+
#
|
21
|
+
# @param [String] response The data received on this connection's socket.
|
22
|
+
def receive_data(response)
|
23
|
+
ip, port = peer_info
|
24
|
+
parsed_response = parse(response)
|
25
|
+
|
26
|
+
return unless parsed_response.has_key? :nts
|
27
|
+
|
28
|
+
if parsed_response[:nts] == 'ssdp:alive'
|
29
|
+
@alive_notifications << parsed_response
|
30
|
+
elsif parsed_response[:nts] == 'ssdp:byebye'
|
31
|
+
@byebye_notifications << parsed_response
|
32
|
+
else
|
33
|
+
raise "Unknown NTS value: #{parsed_response[:nts]}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'core_ext/socket_patch'
|
2
|
+
require_relative 'network_constants'
|
3
|
+
require_relative 'error'
|
4
|
+
require 'ipaddr'
|
5
|
+
require 'socket'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'em-synchrony'
|
8
|
+
|
9
|
+
module Druzy
|
10
|
+
module Upnp
|
11
|
+
class SSDP
|
12
|
+
class MulticastConnection < EventMachine::Connection
|
13
|
+
include Druzy::Upnp::SSDP::NetworkConstants
|
14
|
+
|
15
|
+
# @param [Fixnum] ttl The TTL value to use when opening the UDP socket
|
16
|
+
# required for SSDP actions.
|
17
|
+
def initialize(ttl=TTL)
|
18
|
+
@ttl = ttl
|
19
|
+
|
20
|
+
@discovery_responses = EM::Channel.new
|
21
|
+
@alive_notifications = EM::Channel.new
|
22
|
+
@byebye_notifications = EM::Channel.new
|
23
|
+
|
24
|
+
setup_multicast_socket
|
25
|
+
end
|
26
|
+
|
27
|
+
# Gets the IP and port from the peer that just sent data.
|
28
|
+
#
|
29
|
+
# @return [Array<String,Fixnum>] The IP and port.
|
30
|
+
def peer_info
|
31
|
+
peer_bytes = get_peername[2, 6].unpack('nC4')
|
32
|
+
port = peer_bytes.first.to_i
|
33
|
+
ip = peer_bytes[1, 4].join('.')
|
34
|
+
|
35
|
+
[ip, port]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Converts the headers to a set of key-value pairs.
|
39
|
+
#
|
40
|
+
# @param [String] data The data to convert.
|
41
|
+
# @return [Hash] The converted data. Returns an empty Hash if it didn't
|
42
|
+
# know how to parse.
|
43
|
+
def parse(data)
|
44
|
+
new_data = {}
|
45
|
+
|
46
|
+
unless data =~ /\n/
|
47
|
+
return new_data
|
48
|
+
end
|
49
|
+
|
50
|
+
data.each_line do |line|
|
51
|
+
line =~ /(\S+):(.*)/
|
52
|
+
|
53
|
+
unless $1.nil?
|
54
|
+
key = $1
|
55
|
+
value = $2
|
56
|
+
key = key.gsub('-', '_').downcase.to_sym
|
57
|
+
new_data[key] = value.strip
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
new_data
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets Socket options to allow for multicasting. If ENV["RUBY_UPNP_ENV"] is
|
65
|
+
# equal to "testing", then it doesn't turn off multicast looping.
|
66
|
+
def setup_multicast_socket
|
67
|
+
set_membership(IPAddr.new(MULTICAST_IP).hton +
|
68
|
+
IPAddr.new('0.0.0.0').hton)
|
69
|
+
set_multicast_ttl(@ttl)
|
70
|
+
set_ttl(@ttl)
|
71
|
+
|
72
|
+
unless ENV['RUBY_UPNP_ENV'] == 'testing'
|
73
|
+
switch_multicast_loop :off
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param [String] membership The network byte ordered String that represents
|
78
|
+
# the IP(s) that should join the membership group.
|
79
|
+
def set_membership(membership)
|
80
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
|
84
|
+
def set_multicast_ttl(ttl)
|
85
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL,
|
86
|
+
[ttl].pack('i'))
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [Fixnum] ttl TTL to set IP_TTL to.
|
90
|
+
def set_ttl(ttl)
|
91
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param [Symbol] on_off Turn on/off multicast looping. Supply :on or :off.
|
95
|
+
def switch_multicast_loop(on_off)
|
96
|
+
hex_value = case on_off
|
97
|
+
when :on then "\001"
|
98
|
+
when "\001" then "\001"
|
99
|
+
when :off then "\000"
|
100
|
+
when "\000" then "\000"
|
101
|
+
else raise SSDP::Error, "Can't switch IP_MULTICAST_LOOP to '#{on_off}'"
|
102
|
+
end
|
103
|
+
|
104
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, hex_value)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Druzy
|
2
|
+
module Upnp
|
3
|
+
class SSDP
|
4
|
+
module NetworkConstants
|
5
|
+
|
6
|
+
BROADCAST_IP = '255.255.255.255'
|
7
|
+
|
8
|
+
# Default multicast IP address
|
9
|
+
MULTICAST_IP = '239.255.255.250'
|
10
|
+
|
11
|
+
# Default multicast port
|
12
|
+
MULTICAST_PORT = 1900
|
13
|
+
|
14
|
+
# Default TTL
|
15
|
+
TTL = 4
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'multicast_connection'
|
2
|
+
|
3
|
+
|
4
|
+
class Druzy::Upnp::SSDP::Notifier < Druzy::Upnp::SSDP::MulticastConnection
|
5
|
+
|
6
|
+
def initialize(nt, usn, ddf_url, valid_for_duration)
|
7
|
+
@os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
|
8
|
+
RbConfig::CONFIG['host_os']
|
9
|
+
@upnp_version = '1.0'
|
10
|
+
@notification = notification(nt, usn, ddf_url, valid_for_duration)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post_init
|
14
|
+
if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [String] nt "Notification Type"; a potential search target. Used in
|
20
|
+
# +NT+ header.
|
21
|
+
# @param [String] usn "Unique Service Name"; a composite identifier for the
|
22
|
+
# advertisement. Used in +USN+ header.
|
23
|
+
# @param [String] ddf_url Device Description File URL for the root device.
|
24
|
+
# @param [Fixnum] valid_for_duration Duration in seconds for which the
|
25
|
+
# advertisement is valid. Used in +CACHE-CONTROL+ header.
|
26
|
+
def notification(nt, usn, ddf_url, valid_for_duration)
|
27
|
+
<<-NOTIFICATION
|
28
|
+
NOTIFY * HTTP/1.1\r
|
29
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
30
|
+
CACHE-CONTROL: max-age=#{valid_for_duration}\r
|
31
|
+
LOCATION: #{ddf_url}\r
|
32
|
+
NT: #{nt}\r
|
33
|
+
NTS: ssdp:alive\r
|
34
|
+
SERVER: #{@os} UPnP/#{@upnp_version} Playful/#{Playful::VERSION}\r
|
35
|
+
USN: #{usn}\r
|
36
|
+
\r
|
37
|
+
NOTIFICATION
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative 'multicast_connection'
|
2
|
+
|
3
|
+
module Druzy
|
4
|
+
module Upnp
|
5
|
+
|
6
|
+
# A subclass of an EventMachine::Connection, this handles doing M-SEARCHes.
|
7
|
+
#
|
8
|
+
# Search types:
|
9
|
+
# ssdp:all
|
10
|
+
# upnp:rootdevice
|
11
|
+
# uuid:[device-uuid]
|
12
|
+
# urn:schemas-upnp-org:device:[deviceType-version]
|
13
|
+
# urn:schemas-upnp-org:service:[serviceType-version]
|
14
|
+
# urn:[custom-schema]:device:[deviceType-version]
|
15
|
+
# urn:[custom-schema]:service:[serviceType-version]
|
16
|
+
class SSDP::Searcher < SSDP::MulticastConnection
|
17
|
+
|
18
|
+
DEFAULT_RESPONSE_WAIT_TIME = 5
|
19
|
+
DEFAULT_M_SEARCH_COUNT = 2
|
20
|
+
|
21
|
+
# @return [EventMachine::Channel] Provides subscribers with responses from
|
22
|
+
# their search request.
|
23
|
+
attr_reader :discovery_responses
|
24
|
+
|
25
|
+
# @param [String] search_target
|
26
|
+
# @param [Hash] options
|
27
|
+
# @option options [Fixnum] response_wait_time
|
28
|
+
# @option options [Fixnum] ttl
|
29
|
+
# @option options [Fixnum] m_search_count The number of times to send the
|
30
|
+
# M-SEARCH. UPnP 1.0 suggests to send the request more than once.
|
31
|
+
def initialize(search_target, options = {})
|
32
|
+
options[:ttl] ||= TTL
|
33
|
+
options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
|
34
|
+
@m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
|
35
|
+
|
36
|
+
@search = m_search(search_target, options[:response_wait_time])
|
37
|
+
|
38
|
+
super options[:ttl]
|
39
|
+
end
|
40
|
+
|
41
|
+
# This is the callback called by EventMachine when it receives data on the
|
42
|
+
# socket that's been opened for this connection. In this case, the method
|
43
|
+
# parses the SSDP responses/notifications into Hashes and adds them to the
|
44
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
45
|
+
# effectively means that in each Channel, you get a Hash that represents
|
46
|
+
# the headers for each response/notification that comes in on the socket.
|
47
|
+
#
|
48
|
+
# @param [String] response The data received on this connection's socket.
|
49
|
+
def receive_data(response)
|
50
|
+
ip, port = peer_info
|
51
|
+
parsed_response = parse(response)
|
52
|
+
|
53
|
+
return if parsed_response.has_key? :nts
|
54
|
+
return if parsed_response[:man] &&
|
55
|
+
parsed_response[:man] =~ /ssdp:discover/
|
56
|
+
|
57
|
+
@discovery_responses << parsed_response
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sends the M-SEARCH that was built during init. Logs what was sent if the
|
61
|
+
# send was successful.
|
62
|
+
def post_init
|
63
|
+
@m_search_count.times do
|
64
|
+
send_datagram(@search, MULTICAST_IP, MULTICAST_PORT)
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Builds the M-SEARCH request string.
|
70
|
+
#
|
71
|
+
# @param [String] search_target
|
72
|
+
# @param [Fixnum] response_wait_time
|
73
|
+
def m_search(search_target, response_wait_time)
|
74
|
+
<<-MSEARCH
|
75
|
+
M-SEARCH * HTTP/1.1\r
|
76
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
77
|
+
MAN: "ssdp:discover"\r
|
78
|
+
MX: #{response_wait_time}\r
|
79
|
+
ST: #{search_target}\r
|
80
|
+
\r
|
81
|
+
MSEARCH
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'core_ext/socket_patch'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'core_ext/to_upnp_s'
|
5
|
+
require_relative 'ssdp/error'
|
6
|
+
require_relative 'ssdp/network_constants'
|
7
|
+
require_relative 'ssdp/listener'
|
8
|
+
require_relative 'ssdp/searcher'
|
9
|
+
require_relative 'ssdp/notifier'
|
10
|
+
|
11
|
+
require_relative 'ssdp/broadcast_searcher'
|
12
|
+
|
13
|
+
module Druzy
|
14
|
+
module Upnp
|
15
|
+
|
16
|
+
# This is the main class for doing SSDP stuff. You can have a look at child
|
17
|
+
# classes, but you'll probably want to just use these methods here.
|
18
|
+
#
|
19
|
+
# SSDP is "Simple Service Discovery Protocol", which lets you find and learn
|
20
|
+
# about UPnP devices on your network. Of the six "steps" of UPnP (given in
|
21
|
+
# the UPnP spec--that's counting step 0), SSDP is what provides step 1, or the
|
22
|
+
# "discovery" step.
|
23
|
+
#
|
24
|
+
# Before you can do anything with any of the UPnP devices on your network, you
|
25
|
+
# need to +search+ your network to see what devices are available. Once you've
|
26
|
+
# found what's available, you can then decide device(s) you'd like to control
|
27
|
+
# (that's where Control Points come in; take a look at Playful::ControlPoint).
|
28
|
+
# After searching, you should then +listen+ to the activity on your network.
|
29
|
+
# New devices on your network may come online (via +ssdp:alive+) and devices
|
30
|
+
# that you care about may go offline (via +ssdp:byebye+), in which case you
|
31
|
+
# probably shouldn't try to talk to them anymore.
|
32
|
+
#
|
33
|
+
# @todo Add docs for Playful::Device perspective.
|
34
|
+
class SSDP
|
35
|
+
include NetworkConstants
|
36
|
+
|
37
|
+
# Opens a multicast UDP socket on 239.255.255.250:1900 and listens for
|
38
|
+
# alive and byebye notifications from devices.
|
39
|
+
#
|
40
|
+
# @param [Fixnum] ttl The TTL to use on the UDP socket.
|
41
|
+
#
|
42
|
+
# @return [Hash<Array>,Playful::SSDP::Listener] If the EventMachine reactor is
|
43
|
+
# _not_ running, it returns two key/value pairs--one for
|
44
|
+
# alive_notifications, one for byebye_notifications. If the reactor _is_
|
45
|
+
# running, it returns a Playful::SSDP::Listener so that that object can be
|
46
|
+
# used however desired. The latter method is used in Playful::ControlPoints
|
47
|
+
# so that an object of that type can keep track of devices it cares about.
|
48
|
+
def self.listen(ttl=TTL)
|
49
|
+
alive_notifications = Set.new
|
50
|
+
byebye_notifications = Set.new
|
51
|
+
|
52
|
+
listener = proc do
|
53
|
+
l = EM.open_datagram_socket(MULTICAST_IP, MULTICAST_PORT,
|
54
|
+
Playful::SSDP::Listener, ttl)
|
55
|
+
i = 0
|
56
|
+
EM.add_periodic_timer(5) { i += 5; Playful.log "Listening for #{i}\n" }
|
57
|
+
l
|
58
|
+
end
|
59
|
+
|
60
|
+
if EM.reactor_running?
|
61
|
+
return listener.call
|
62
|
+
else
|
63
|
+
EM.synchrony do
|
64
|
+
l = listener.call
|
65
|
+
|
66
|
+
alive_getter = Proc.new do |notification|
|
67
|
+
alive_notifications << notification
|
68
|
+
EM.next_tick { l.alive_notifications.pop(&live_getter) }
|
69
|
+
end
|
70
|
+
l.alive_notifications.pop(&alive_getter)
|
71
|
+
|
72
|
+
byebye_getter = Proc.new do |notification|
|
73
|
+
byebye_notifications << notification
|
74
|
+
EM.next_tick { l.byebye_notifications.pop(&byebye_getter) }
|
75
|
+
end
|
76
|
+
l.byebye_notifications.pop(&byebye_getter)
|
77
|
+
|
78
|
+
trap_signals
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
{
|
83
|
+
alive_notifications: alive_notifications.to_a.flatten,
|
84
|
+
byebye_notifications: byebye_notifications.to_a.flatten
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Opens a UDP socket on 0.0.0.0, on an ephemeral port, has Druzy::Upnp::SSDP::Searcher
|
89
|
+
# build and send the search request, then receives the responses. The search
|
90
|
+
# will stop after +response_wait_time+.
|
91
|
+
#
|
92
|
+
# @param [String] search_target
|
93
|
+
#
|
94
|
+
# @param [Hash] options
|
95
|
+
#
|
96
|
+
# @option options [Fixnum] response_wait_time
|
97
|
+
# @option options [Fixnum] ttl
|
98
|
+
# @option options [Fixnum] m_search_count
|
99
|
+
# @option options [Boolean] do_broadcast_search Tells the search call to also send
|
100
|
+
# a M-SEARCH over 255.255.255.255. This is *NOT* part of the UPnP spec;
|
101
|
+
# it's merely a hack for working with some types of devices that don't
|
102
|
+
# properly implement the UPnP spec.
|
103
|
+
#
|
104
|
+
# @return [Array<Hash>,Playful::SSDP::Searcher] Returns a Hash that represents
|
105
|
+
# the headers from the M-SEARCH response. Each one of these can be passed
|
106
|
+
# in to Playful::ControlPoint::Device.new to download the device's
|
107
|
+
# description file, parse it, and interact with the device's devices
|
108
|
+
# and/or services. If the reactor is already running this will return a
|
109
|
+
# a Playful::SSDP::Searcher which will make its accessors available so you
|
110
|
+
# can get responses in real time.
|
111
|
+
def self.search(search_target=:all, options = {})
|
112
|
+
response_wait_time = options[:response_wait_time] || 5
|
113
|
+
ttl = options[:ttl] || TTL
|
114
|
+
do_broadcast_search = options[:do_broadcast_search]
|
115
|
+
|
116
|
+
searcher_options = options
|
117
|
+
searcher_options.delete :do_broadcast_search
|
118
|
+
|
119
|
+
responses = []
|
120
|
+
search_target = search_target.to_upnp_s
|
121
|
+
|
122
|
+
multicast_searcher = proc do
|
123
|
+
EM.open_datagram_socket('0.0.0.0', 0, Druzy::Upnp::SSDP::Searcher,
|
124
|
+
search_target, searcher_options)
|
125
|
+
end
|
126
|
+
|
127
|
+
broadcast_searcher = proc do
|
128
|
+
EM.open_datagram_socket('0.0.0.0', 0, Druzy::Upnp::SSDP::BroadcastSearcher,
|
129
|
+
search_target, response_wait_time, ttl)
|
130
|
+
end
|
131
|
+
|
132
|
+
if EM.reactor_running?
|
133
|
+
return multicast_searcher.call
|
134
|
+
else
|
135
|
+
EM.synchrony do
|
136
|
+
ms = multicast_searcher.call
|
137
|
+
|
138
|
+
ms.discovery_responses.subscribe do |notification|
|
139
|
+
responses << notification
|
140
|
+
end
|
141
|
+
|
142
|
+
if do_broadcast_search
|
143
|
+
bs = broadcast_searcher.call
|
144
|
+
|
145
|
+
bs.discovery_responses.subscribe do |notification|
|
146
|
+
responses << notification
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
EM.add_timer(response_wait_time) { EM.stop }
|
151
|
+
trap_signals
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
responses.flatten
|
156
|
+
end
|
157
|
+
|
158
|
+
# @todo This is for Playful::Devices, which aren't implemented yet, and thus
|
159
|
+
# this may not be working.
|
160
|
+
def self.notify(notification_type, usn, ddf_url, valid_for_duration=1800)
|
161
|
+
responses = []
|
162
|
+
notification_type = notification_type.to_upnp_s
|
163
|
+
|
164
|
+
EM.synchrony do
|
165
|
+
s = send_notification(notification_type, usn, ddf_url, valid_for_duration)
|
166
|
+
EM.add_shutdown_hook { responses = s.discovery_responses }
|
167
|
+
|
168
|
+
EM.add_periodic_timer(valid_for_duration) do
|
169
|
+
s = send_notification(notification_type, usn, ddf_url, valid_for_duration)
|
170
|
+
end
|
171
|
+
|
172
|
+
trap_signals
|
173
|
+
end
|
174
|
+
|
175
|
+
responses
|
176
|
+
end
|
177
|
+
|
178
|
+
# @todo This is for Playful::Devices, which aren't implemented yet, and thus
|
179
|
+
# this may not be working.
|
180
|
+
def self.send_notification(notification_type, usn, ddf_url, valid_for_duration)
|
181
|
+
EM.open_datagram_socket('0.0.0.0', 0, Druzy::Upnp::SSDP::Notifier, notification_type,
|
182
|
+
usn, ddf_url, valid_for_duration)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
# Traps INT, TERM, and HUP signals and stops the reactor.
|
188
|
+
def self.trap_signals
|
189
|
+
trap('INT') { EM.stop }
|
190
|
+
trap('TERM') { EM.stop }
|
191
|
+
trap('HUP') { EM.stop } if RUBY_PLATFORM !~ /mswin|mingw/
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/lib/druzy/upnp.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'druzy/upnp/control_point'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
# Middleware that allows your Rack app to keep tabs on devices that the
|
7
|
+
# {Playful::ControlPoint} has found. UPnP devices that match the +search_type+
|
8
|
+
# are discovered and added to the list, then removed as those devices send out
|
9
|
+
# +ssdp:byebye+ notifications. All of this depends on +EventMachine::Channel+s,
|
10
|
+
# and thus requires that an EventMachine reactor is running. If you don't
|
11
|
+
# have one running, the {Playful::ControlPoint} will start one for you.
|
12
|
+
#
|
13
|
+
# @example Control all root devices
|
14
|
+
#
|
15
|
+
# Thin::Server.start('0.0.0.0', 3000) do
|
16
|
+
# use Rack::UPnPControlPoint, search_type: :root
|
17
|
+
#
|
18
|
+
# map "/devices" do
|
19
|
+
# run lambda { |env|
|
20
|
+
# devices = env['upnp.devices']
|
21
|
+
# friendly_names = devices.map(&:friendly_name).join("\n")
|
22
|
+
# [200, {'Content-Type' => 'text/plain'}, [friendly_names]]
|
23
|
+
# }
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class UPnPControlPoint
|
28
|
+
|
29
|
+
# @param [Rack::Builder] app Your Rack application.
|
30
|
+
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
31
|
+
# @see Playful::SSDP::Searcher
|
32
|
+
def initialize(app, options = {})
|
33
|
+
@app = app
|
34
|
+
@devices = []
|
35
|
+
options[:search_type] ||= :root
|
36
|
+
EM.next_tick { start_control_point(options[:search_type], options) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates and starts the {Playful::ControlPoint}, then manages the list of
|
40
|
+
# devices using the +EventMachine::Channel+ objects yielded in.
|
41
|
+
#
|
42
|
+
# @param [Symbol,String] search_type The device(s) you want to search for
|
43
|
+
# and control.
|
44
|
+
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
45
|
+
# @see Playful::SSDP::Searcher
|
46
|
+
def start_control_point(search_type, options)
|
47
|
+
@cp = ::Playful::ControlPoint.new(search_type, options)
|
48
|
+
|
49
|
+
@cp.start do |new_device_channel, old_device_channel|
|
50
|
+
new_device_channel.subscribe do |notification|
|
51
|
+
@devices << notification
|
52
|
+
end
|
53
|
+
|
54
|
+
old_device_channel.subscribe do |old_device|
|
55
|
+
@devices.reject! { |d| d.usn == old_device[:usn] }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds the whole list of devices to <tt>env['upnp.devices']</tt> so that
|
62
|
+
# that list can be accessed from within your app.
|
63
|
+
#
|
64
|
+
# @param [Hash] env The Rack environment.
|
65
|
+
def call(env)
|
66
|
+
env['upnp.devices'] = @devices
|
67
|
+
@app.call(env)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|