Simple Service Discovery Protocol for the UPnP Device Architecture.
Currently SSDP only handles the discovery portions of SSDP.
To listen for SSDP notifications from UPnP devices:
ssdp = SSDP.new notifications = ssdp.listen
To discover all devices and services:
ssdp = SSDP.new resources = ssdp.search
After a device has been found you can create a Device object for it:
UPnP::Control::Device.create resource.location
Based on code by Kazuhiro NISHIYAMA (zn@mbf.nifty.com)
Default broadcast address
Default port
Default timeout
Default packet time to live (hops)
Broadcast address to use when sending searches and listening for notifications
A WEBrick::Log logger for unified logging
Port to use for SSDP searching and listening
Time to wait for SSDP responses
Creates a new SSDP object. Use the accessors to override broadcast, port, timeout or ttl.
# File lib/UPnP/SSDP.rb, line 397 def initialize @broadcast = BROADCAST @port = PORT @timeout = TIMEOUT @ttl = TTL @log = nil @listener = nil @queue = Queue.new @search_thread = nil @notify_thread = nil end
Listens for M-SEARCH requests and advertises the requested services
# File lib/UPnP/SSDP.rb, line 415 def advertise(root_device, port, hosts) @socket ||= new_socket @notify_thread = Thread.start do loop do hosts.each do |host| uri = "http://#{host}:#{port}/description" send_notify uri, 'upnp:rootdevice', root_device root_device.devices.each do |d| send_notify uri, d.name, d send_notify uri, d.type_urn, d end root_device.services.each do |s| send_notify uri, s.type_urn, s end end sleep 60 end end listen @search_thread = Thread.start do loop do search = @queue.pop break if search == :shutdown next unless Search === search case search.target when %r^#{UPnP::DEVICE_SCHEMA_PREFIX}/ then devices = root_device.devices.select do |d| d.type_urn == search.target end devices.each do |d| hosts.each do |host| uri = "http://#{host}:#{port}/description" send_response uri, search.target, "#{d.name}::#{search.target}", d end end when 'upnp:rootdevice' then hosts.each do |host| uri = "http://#{host}:#{port}/description" send_response uri, search.target, search.target, root_device end else warn "Unhandled target #{search.target}" end end end sleep ensure @queue.push :shutdown stop_listening @notify_thread.kill @socket.close if @socket and not @socket.closed? @socket = nil end
# File lib/UPnP/SSDP.rb, line 483 def byebye(root_device, hosts) @socket ||= new_socket hosts.each do |host| send_notify_byebye 'upnp:rootdevice', root_device root_device.devices.each do |d| send_notify_byebye d.name, d send_notify_byebye d.type_urn, d end root_device.services.each do |s| send_notify_byebye s.type_urn, s end end end
Discovers UPnP devices sending NOTIFY broadcasts.
If given a block, yields each Notification as it is received and never returns. Otherwise, discover waits for timeout seconds and returns all notifications received in that time.
# File lib/UPnP/SSDP.rb, line 507 def discover @socket ||= new_socket listen if block_given? then loop do notification = @queue.pop yield notification end else sleep @timeout notifications = [] notifications << @queue.pop until @queue.empty? notifications end ensure stop_listening @socket.close if @socket and not @socket.closed? @socket = nil end
Listens for UDP packets from devices in a Thread and enqueues them for processing. Requires a socket from search or discover.
# File lib/UPnP/SSDP.rb, line 535 def listen return @listener if @listener and @listener.alive? @listener = Thread.start do loop do response, (family, port, hostname, address) = @socket.recvfrom 1024 begin adv = parse response info = case adv when Notification then adv.type when Response then adv.target when Search then adv.target else 'unknown' end response =~ %r\A(\S+)/ log :debug, "SSDP recv #{$1} #{hostname}:#{port} #{info}" @queue << adv rescue warn $!.message warn $!.backtrace end end end end
# File lib/UPnP/SSDP.rb, line 564 def log(level, message) return unless @log @log.send level, message end
Sets up a UDPSocket for multicast send and receive
# File lib/UPnP/SSDP.rb, line 573 def new_socket membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton ttl = [@ttl].pack 'i' socket = UDPSocket.new socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\0000" socket.setsockopt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, ttl socket.bind '0.0.0.0', @port socket end
Returns a Notification, Response or Search created from response
.
# File lib/UPnP/SSDP.rb, line 592 def parse(response) case response when %r\ANOTIFY/ then Notification.parse response when %r\AHTTP/ then Response.parse response when %r\AM-SEARCH/ then Search.parse response else raise Error, "Unknown response #{response[/\A.*$/]}" end end
Sends M-SEARCH requests looking for targets
. Waits timeout
seconds for responses then returns the collected responses.
Supply no arguments to search for all devices and services.
Supply :root
to search for root devices only.
Supply [:device, 'device_type:version']
to search for a
specific device type.
Supply [:service, 'service_type:version']
to search for a
specific service type.
Supply "uuid:..."
to search for a UUID.
Supply "urn:..."
to search for a URN.
# File lib/UPnP/SSDP.rb, line 623 def search(*targets) @socket ||= new_socket if targets.empty? then send_search 'ssdp:all' else targets.each do |target| if target == :root then send_search 'upnp:rootdevice' elsif Array === target and target.first == :device then target = [UPnP::DEVICE_SCHEMA_PREFIX, target.last] send_search target.join(':') elsif Array === target and target.first == :service then target = [UPnP::SERVICE_SCHEMA_PREFIX, target.last] send_search target.join(':') elsif String === target and target =~ %r\A(urn|uuid|ssdp):/ then send_search target end end end listen sleep @timeout responses = [] responses << @queue.pop until @queue.empty? responses ensure stop_listening @socket.close if @socket and not @socket.closed? @socket = nil end
Builds and sends a NOTIFY message
# File lib/UPnP/SSDP.rb, line 659 def send_notify(uri, type, obj) if type =~ %r^uuid:/ then name = obj.name else # HACK maybe this should be .device? name = "#{obj.root_device.name}::#{type}" end server_info = "Ruby UPnP/#{UPnP::VERSION}" device_info = "#{obj.root_device.class}/#{obj.root_device.version}" http_notify = "NOTIFY * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r CACHE-CONTROL: max-age=120\r LOCATION: #{uri}\r NT: #{type}\r NTS: ssdp:alive\r SERVER: #{server_info} UPnP/1.0 #{device_info}\r USN: #{name}\r \r " log :debug, "SSDP sent NOTIFY #{type}" @socket.send http_notify, 0, @broadcast, @port end
Builds and sends a byebye NOTIFY message
# File lib/UPnP/SSDP.rb, line 690 def send_notify_byebye(type, obj) if type =~ %r^uuid:/ then name = obj.name else # HACK maybe this should be .device? name = "#{obj.root_device.name}::#{type}" end http_notify = "NOTIFY * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r NT: #{type}\r NTS: ssdp:byebye\r USN: #{name}\r \r " log :debug, "SSDP sent byebye #{type}" @socket.send http_notify, 0, @broadcast, @port end
Builds and sends a response to an M-SEARCH request“
# File lib/UPnP/SSDP.rb, line 715 def send_response(uri, type, name, device) server_info = "Ruby UPnP/#{UPnP::VERSION}" device_info = "#{device.root_device.class}/#{device.root_device.version}" http_response = "HTTP/1.1 200 OK\r CACHE-CONTROL: max-age=120\r EXT:\r LOCATION: #{uri}\r SERVER: #{server_info} UPnP/1.0 #{device_info}\r ST: #{type}\r NTS: ssdp:alive\r USN: #{name}\r Content-Length: 0\r \r " log :debug, "SSDP sent M-SEARCH OK #{type}" @socket.send http_response, 0, @broadcast, @port end
Builds and sends an M-SEARCH request looking for
search_target
.
# File lib/UPnP/SSDP.rb, line 740 def send_search(search_target) search = "M-SEARCH * HTTP/1.1\r HOST: #{@broadcast}:#{@port}\r MAN: "ssdp:discover"\r MX: #{@timeout}\r ST: #{search_target}\r \r " log :debug, "SSDP sent M-SEARCH #{search_target}" @socket.send search, 0, @broadcast, @port end
Stops and clears the listen thread.
# File lib/UPnP/SSDP.rb, line 758 def stop_listening @listener.kill if @listener @queue = Queue.new @listener = nil end