class UPnP::SSDP

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)

Constants

BROADCAST

Default broadcast address

PORT

Default port

TIMEOUT

Default timeout

TTL

Default packet time to live (hops)

Attributes

broadcast[RW]

Broadcast address to use when sending searches and listening for notifications

log[W]

A WEBrick::Log logger for unified logging

port[RW]

Port to use for SSDP searching and listening

timeout[RW]

Time to wait for SSDP responses

ttl[RW]

TTL for SSDP packets

Public Class Methods

new() click to toggle source

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

Public Instance Methods

advertise(root_device, port, hosts) click to toggle source

Listens for M-SEARCH requests and advertises the requested services

byebye(root_device, hosts) click to toggle source
# 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
discover() { |notification| ... } click to toggle source

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
listen() click to toggle source

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
log(level, message) click to toggle source
# File lib/UPnP/SSDP.rb, line 564
def log(level, message)
  return unless @log

  @log.send level, message
end
new_socket() click to toggle source

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
parse(response) click to toggle source

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
send_notify(uri, type, obj) click to toggle source

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
send_notify_byebye(type, obj) click to toggle source

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
send_response(uri, type, name, device) click to toggle source

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
stop_listening() click to toggle source

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