playful 0.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/History.rdoc +3 -0
- data/LICENSE.rdoc +22 -0
- data/README.rdoc +194 -0
- data/Rakefile +20 -0
- data/features/control_point.feature +13 -0
- data/features/device.feature +22 -0
- data/features/device_discovery.feature +9 -0
- data/features/step_definitions/control_point_steps.rb +19 -0
- data/features/step_definitions/device_discovery_steps.rb +40 -0
- data/features/step_definitions/device_steps.rb +28 -0
- data/features/support/common.rb +9 -0
- data/features/support/env.rb +17 -0
- data/features/support/fake_upnp_device_collection.rb +108 -0
- data/features/support/world_extensions.rb +15 -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/playful.rb +5 -0
- data/lib/playful/control_point.rb +175 -0
- data/lib/playful/control_point/base.rb +74 -0
- data/lib/playful/control_point/device.rb +511 -0
- data/lib/playful/control_point/error.rb +13 -0
- data/lib/playful/control_point/service.rb +404 -0
- data/lib/playful/device.rb +28 -0
- data/lib/playful/logger.rb +8 -0
- data/lib/playful/ssdp.rb +195 -0
- data/lib/playful/ssdp/broadcast_searcher.rb +114 -0
- data/lib/playful/ssdp/error.rb +6 -0
- data/lib/playful/ssdp/listener.rb +38 -0
- data/lib/playful/ssdp/multicast_connection.rb +112 -0
- data/lib/playful/ssdp/network_constants.rb +17 -0
- data/lib/playful/ssdp/notifier.rb +41 -0
- data/lib/playful/ssdp/searcher.rb +87 -0
- data/lib/playful/version.rb +3 -0
- data/lib/rack/upnp_control_point.rb +70 -0
- data/playful.gemspec +38 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/search_responses.rb +134 -0
- data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
- data/spec/unit/playful/control_point/device_spec.rb +7 -0
- data/spec/unit/playful/control_point_spec.rb +45 -0
- data/spec/unit/playful/ssdp/listener_spec.rb +29 -0
- data/spec/unit/playful/ssdp/multicast_connection_spec.rb +157 -0
- data/spec/unit/playful/ssdp/notifier_spec.rb +76 -0
- data/spec/unit/playful/ssdp/searcher_spec.rb +110 -0
- data/spec/unit/playful/ssdp_spec.rb +214 -0
- data/tasks/control_point.html +30 -0
- data/tasks/control_point.thor +43 -0
- data/tasks/search.thor +128 -0
- data/tasks/test_js/FABridge.js +1425 -0
- data/tasks/test_js/WebSocketMain.swf +807 -0
- data/tasks/test_js/swfobject.js +825 -0
- data/tasks/test_js/web_socket.js +1133 -0
- data/test/test_ssdp.rb +298 -0
- data/test/test_ssdp_notification.rb +74 -0
- data/test/test_ssdp_response.rb +31 -0
- data/test/test_ssdp_search.rb +23 -0
- metadata +339 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9760e12299c1d42be1a43596cc66d5d70ad07065
|
4
|
+
data.tar.gz: 4f64c01cb96411ca5f93174bfb145a12a34cc3f3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 51668cea99a82a52dbcbba1e08f134fb2300db97327fa7f7be0f15135646e53ecf4c5ab4f4a6bdf7a5b2be4b3823b9bff083df61717bc03c7332939ff9295053
|
7
|
+
data.tar.gz: 76f05414e46d0683230d8c38856efb78a2d412880a31bf9a00878b359d899caf9d83d0a8609e39e375276f8f138ac36056d3b0d550fab0487f2f366901c8484b
|
data/.gemtest
ADDED
File without changes
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/History.rdoc
ADDED
data/LICENSE.rdoc
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2012 Steve Loveless
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
= playful
|
2
|
+
|
3
|
+
* {Homepage}[http://github.com/turboladen/playful]
|
4
|
+
* {UPnP Device Architecture Documentation}[http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf]
|
5
|
+
* previously `upnp`
|
6
|
+
|
7
|
+
{<img src="https://travis-ci.org/turboladen/playful.png?branch=master" alt="Build Status" />}[https://travis-ci.org/turboladen/playful]
|
8
|
+
{<img src="https://coveralls.io/repos/turboladen/playful/badge.png" alt="Coverage Status" />}[https://coveralls.io/r/turboladen/playful]
|
9
|
+
|
10
|
+
|
11
|
+
== Description
|
12
|
+
|
13
|
+
Ruby's UPnP RubyGem was outdated in Ruby 1.9 when support for Soap4r was
|
14
|
+
dropped. This gem intends to fill that void for Ruby >= 1.9 and allow for SSDP
|
15
|
+
search, discovery, advertisement, the ability to act as a UPnP control point, as
|
16
|
+
well as provide UPnP devices and services.
|
17
|
+
|
18
|
+
This uses EventMachine[http://github.com/eventmachine/eventmachine], so if
|
19
|
+
you're not already, getting familiar with its concepts will be helpful here.
|
20
|
+
|
21
|
+
=== Getting Started
|
22
|
+
|
23
|
+
I'm still working out the overall design of these components, and thus won't be
|
24
|
+
working towards a gem release until this settles down. As such, don't expect
|
25
|
+
interfaces to stay the same if you update. In the mean time, I do intend to
|
26
|
+
keep the master branch (mostly) stable, so please feel free to give it whirl and
|
27
|
+
report any issues you encounter.
|
28
|
+
|
29
|
+
* <code>gem install bundler</code>
|
30
|
+
* <code>bundle install</code>
|
31
|
+
|
32
|
+
== Features
|
33
|
+
|
34
|
+
=== Implemented
|
35
|
+
|
36
|
+
* SSDP search, discovery. (almost settled down)
|
37
|
+
* Ability to act as a UPnP Control Point. (in progress)
|
38
|
+
* Rack middleware to allow for device access in a Rack app.
|
39
|
+
|
40
|
+
=== Coming
|
41
|
+
|
42
|
+
* UPnP Devices & Services (server)
|
43
|
+
|
44
|
+
== Examples
|
45
|
+
|
46
|
+
Take a look at the +tasks+ directory; I've created some working examples using
|
47
|
+
Thor[https://github.com/wycats/thor]. You can get a list of these tasks by
|
48
|
+
doing `thor -T`.
|
49
|
+
|
50
|
+
There's also a more involved, in-progress, working example at
|
51
|
+
http://github.com/turboladen/upnp_cp_on_sinatra that uses the Rack middleware
|
52
|
+
to build a Sinatra app that allows for controling devices in your network.
|
53
|
+
|
54
|
+
=== SSDP Searches
|
55
|
+
|
56
|
+
An SSDP search simply sends the M-SEARCH out to the multicast group and listens
|
57
|
+
for responses for a given (or default of 5 seconds) amount of time. The return
|
58
|
+
from this depends on if you're running it within an EventMachine reactor or not.
|
59
|
+
If not, it returns is an Array of responses as Hashes, where keys are the header
|
60
|
+
names, values are the header values. Take a look at the SSDP.search docs for
|
61
|
+
more on the options here.
|
62
|
+
|
63
|
+
require 'playful/ssdp'
|
64
|
+
|
65
|
+
# Search for all devices (do an M-SEARCH with the ST header set to 'ssdp:all')
|
66
|
+
all_devices = UPnP::SSDP.search # this is default
|
67
|
+
all_devices = UPnP::SSDP.search 'ssdp:all' # or be explicit
|
68
|
+
all_devices = UPnP::SSDP.search :all # or use short-hand
|
69
|
+
|
70
|
+
# Search for root devices (do an M-SEARCH with ST header set to 'upnp:rootdevices')
|
71
|
+
root_devices = UPnP::SSDP.search 'upnp:rootdevices'
|
72
|
+
root_devices = UPnP::SSDP.search :root # or use short-hand
|
73
|
+
|
74
|
+
# Search for a device with a specific UUID
|
75
|
+
my_device = UPnP::SSDP.search 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d'
|
76
|
+
|
77
|
+
# Search for devices of a specific type
|
78
|
+
my_media_server = UPnP::SSDP.search 'urn:schemas-upnp-org:device:MediaServer:1'
|
79
|
+
|
80
|
+
# All of these searches will return something that looks like
|
81
|
+
# => [
|
82
|
+
# {
|
83
|
+
# :control => "max-age=1200",
|
84
|
+
# :date => "Sun, 23 Sep 2012 20:31:48 GMT",
|
85
|
+
# :location => "http://192.168.10.3:5001/description/fetch",
|
86
|
+
# :server => "Linux-i386-2.6.38-15-generic-pae, UPnP/1.0, PMS/1.50.0",
|
87
|
+
# :st => "upnp:rootdevice",
|
88
|
+
# :ext => "",
|
89
|
+
# :usn => "uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice",
|
90
|
+
# :length => "0"
|
91
|
+
# }
|
92
|
+
# ]
|
93
|
+
|
94
|
+
If you do the search inside of an EventMachine reactor, as the
|
95
|
+
UPnP::SSDP::Searcher receives and parses responses, it adds them to the accessor
|
96
|
+
#discovery_responses, which is an EventMachine::Channel. This lets you subscribe
|
97
|
+
to the resposnes and do what you want with them (most likely you'll want to create
|
98
|
+
UPnP::ControlPoint::Device objects so you can control your device) as you
|
99
|
+
receive them.
|
100
|
+
|
101
|
+
require 'playful/ssdp'
|
102
|
+
require 'playful/control_point/device'
|
103
|
+
|
104
|
+
EM.run do
|
105
|
+
searcher = UPnP::SSDP.search 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d'
|
106
|
+
|
107
|
+
# Create a deferrable object that can be notified when the device we want
|
108
|
+
# has been found and created.
|
109
|
+
device_controller = EventMachine::DefaultDeferrable.new
|
110
|
+
|
111
|
+
# This callback will get called when the device_creator callback is called
|
112
|
+
# (which is called after the device has been created).
|
113
|
+
device_controller.callback do |device|
|
114
|
+
p device.service_list.first.class # UPnP::ControlPoint::Service
|
115
|
+
p device.service_list.first.service_type # "urn:schemas-upnp-org:service:ContentDirectory:1"
|
116
|
+
|
117
|
+
# SOAP actions are converted to Ruby methods--show those
|
118
|
+
p device.service_list.first.singleton_methods # [:GetSystemUpdateID, :Search, :GetSearchCapabilities, :GetSortCapabilities, :Browse]
|
119
|
+
|
120
|
+
# Call a SOAP method defined in the service. The response is extracted from the
|
121
|
+
# XML SOAP response and the value is converted from the UPnP dataType to
|
122
|
+
# the related Ruby type. Reponses are always contained in a Hash, so as
|
123
|
+
# to maintain the relation defined in the service.
|
124
|
+
p device.service_list.first.GetSystemUpdateID # { :Id => 1 }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Note that you don't have to check for items in the Channel or for when the
|
128
|
+
# Channel is empty: EventMachine will pop objects off the Channel as soon as
|
129
|
+
# they're put there and stop when there are none left.
|
130
|
+
searcher.discovery_responses.pop do |notification|
|
131
|
+
|
132
|
+
# UPnP::ControlPoint::Device objects are EventMachine::Deferrables, so you
|
133
|
+
# need to define callback and errback blocks to handle when the Device
|
134
|
+
# object is done being created.
|
135
|
+
device_creator = UPnP::ControlPoint::Device.new(ssdp_notification: notification)
|
136
|
+
|
137
|
+
device_creator.errback do
|
138
|
+
puts "Failed creating the device."
|
139
|
+
exit!
|
140
|
+
end
|
141
|
+
|
142
|
+
device_creator.callback do |built_device|
|
143
|
+
puts "Device has been created now."
|
144
|
+
|
145
|
+
# This lets the device_controller know that the device has been created,
|
146
|
+
# calls its callback, and passes the built device to it.
|
147
|
+
device_controller.set_deferred_status(:succeeded, built_device)
|
148
|
+
end
|
149
|
+
|
150
|
+
# This actually starts the Device creation process and will call the
|
151
|
+
# callback or errback (above) when it's done.
|
152
|
+
device_creator.fetch
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
=== ControlPoints
|
157
|
+
|
158
|
+
If you're wanting to control devices and their services, you'll probably be more
|
159
|
+
interested in using a UPnP::ControlPoint, instead of doing all that work (above)
|
160
|
+
to create a UPnP::ControlPoint::Device. The ControlPoint will handle doing the
|
161
|
+
search and device/service creation for you and will hand you over Devices to
|
162
|
+
control them (and present them in a UI, perhaps?) as you need. More to come on
|
163
|
+
this as the design settles down.
|
164
|
+
|
165
|
+
== Requirements
|
166
|
+
|
167
|
+
* Rubies (tested)
|
168
|
+
* 1.9.3
|
169
|
+
* 2.0.0
|
170
|
+
* 2.1.0
|
171
|
+
* Gems
|
172
|
+
* eventmachine
|
173
|
+
* em-http-request
|
174
|
+
* em-synchrony
|
175
|
+
* nori
|
176
|
+
* log_switch
|
177
|
+
* savon
|
178
|
+
* Gems (development)
|
179
|
+
* bundler
|
180
|
+
* rspec
|
181
|
+
* simplecov
|
182
|
+
* thin
|
183
|
+
* yard
|
184
|
+
|
185
|
+
== Install
|
186
|
+
|
187
|
+
# This won't work yet, as the gem has not yet been released...
|
188
|
+
$ gem install playful
|
189
|
+
|
190
|
+
== Copyright
|
191
|
+
|
192
|
+
Copyright (c) 2012 sloveless
|
193
|
+
|
194
|
+
See LICENSE.rdoc for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
|
7
|
+
YARD::Rake::YardocTask.new
|
8
|
+
Cucumber::Rake::Task.new(:features)
|
9
|
+
RSpec::Core::RakeTask.new
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
12
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
13
|
+
spec.rcov = true
|
14
|
+
end
|
15
|
+
|
16
|
+
# Alias for rubygems-test
|
17
|
+
task test: :spec
|
18
|
+
|
19
|
+
task default: :test
|
20
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Control Point
|
2
|
+
As a consumer of UPnP devices and services
|
3
|
+
I want to act as a UPnP control point
|
4
|
+
So that I can control the devices and services that fulfill my needs
|
5
|
+
|
6
|
+
Scenario: Search for devices on startup
|
7
|
+
Given I have a non-local IP address
|
8
|
+
And a UDP port on that IP is free
|
9
|
+
When I create my control point
|
10
|
+
And tell it to find all root devices
|
11
|
+
And tell it to find all services
|
12
|
+
Then it gets a list of root devices
|
13
|
+
And it gets a list of services
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Feature: Controlled Device
|
2
|
+
As a UPnP device user
|
3
|
+
I want to use the device that offers some service
|
4
|
+
So that I can consume that service
|
5
|
+
|
6
|
+
Scenario: Device added to the network
|
7
|
+
Given I have a non-local IP address
|
8
|
+
And a UDP port on that IP is free
|
9
|
+
When I start my device on that IP address and port
|
10
|
+
Then the device multicasts a discovery message
|
11
|
+
|
12
|
+
@negative
|
13
|
+
Scenario: Device startup without an IP
|
14
|
+
Given I don't have an IP address
|
15
|
+
When I start the device
|
16
|
+
Then I get an error message saying I don't have an IP address
|
17
|
+
|
18
|
+
Scenario: Device startup with a local-link IP
|
19
|
+
Given I have a local-link IP address
|
20
|
+
When I start the device
|
21
|
+
Then the device starts running normally
|
22
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Feature: Device discovery
|
2
|
+
As a device control point, I want to be able to discover devices
|
3
|
+
so that I can use the services those devices provide
|
4
|
+
|
5
|
+
Scenario: A single root device
|
6
|
+
Given there's at least 1 root device in my network
|
7
|
+
When I come online
|
8
|
+
Then I should discover at least 1 root device
|
9
|
+
And the location of that device should match my fake device's location
|
@@ -0,0 +1,19 @@
|
|
1
|
+
When /^I create my control point$/ do
|
2
|
+
@control_point = UPnP::ControlPoint.new
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^tell it to find all root devices$/ do
|
6
|
+
@control_point.find_devices(:root, 5)
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^tell it to find all services$/ do
|
10
|
+
@control_point.find_services
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^it gets a list of root devices$/ do
|
14
|
+
@control_point.device_list.should_not be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^it gets a list of services$/ do
|
18
|
+
@control_point.services.should_not be_empty
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../support/fake_upnp_device_collection'
|
2
|
+
require 'cucumber/rspec/doubles'
|
3
|
+
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
UPnP::SSDP.log = false
|
6
|
+
|
7
|
+
Given /^there's at least (\d+) root device in my network$/ do |device_count|
|
8
|
+
fake_device_collection.respond_with = <<-ROOT_DEVICE
|
9
|
+
HTTP/1.1 200 OK\r
|
10
|
+
CACHE-CONTROL: max-age=1200\r
|
11
|
+
DATE: Mon, 26 Sep 2011 06:40:19 GMT\r
|
12
|
+
LOCATION: http://#{local_ip}:4567\r
|
13
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1\r
|
14
|
+
ST: upnp:rootdevice\r
|
15
|
+
EXT:\r
|
16
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice\r
|
17
|
+
Content-Length: 0\r
|
18
|
+
|
19
|
+
ROOT_DEVICE
|
20
|
+
|
21
|
+
Thread.start { fake_device_collection.start_ssdp_listening }
|
22
|
+
Thread.start { fake_device_collection.start_serving_description }
|
23
|
+
sleep 0.2
|
24
|
+
end
|
25
|
+
|
26
|
+
When /^I come online$/ do
|
27
|
+
control_point.should be_a UPnP::ControlPoint
|
28
|
+
end
|
29
|
+
|
30
|
+
Then /^I should discover at least (\d+) root device$/ do |device_count|
|
31
|
+
control_point.find_devices(:root)
|
32
|
+
fake_device_collection.stop_ssdp_listening
|
33
|
+
fake_device_collection.stop_serving_description
|
34
|
+
control_point.device_list.should have_at_least(device_count.to_i).items
|
35
|
+
end
|
36
|
+
|
37
|
+
Then /^the location of that device should match my fake device's location$/ do
|
38
|
+
locations = control_point.device_list.map { |device| device[:location] }
|
39
|
+
locations.should include "http://#{local_ip}:4567"
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
When /^I start my device on that IP address and port$/ do
|
2
|
+
@device = UPnP::Device.new(@local_ip, @port)
|
3
|
+
@device.start.should be_true
|
4
|
+
end
|
5
|
+
|
6
|
+
Then /^the device multicasts a discovery message$/ do
|
7
|
+
pending # express the regexp above with the code you wish you had
|
8
|
+
end
|
9
|
+
|
10
|
+
Given /^I don't have an IP address$/ do
|
11
|
+
pending # express the regexp above with the code you wish you had
|
12
|
+
end
|
13
|
+
|
14
|
+
When /^I start the device$/ do
|
15
|
+
pending # express the regexp above with the code you wish you had
|
16
|
+
end
|
17
|
+
|
18
|
+
Then /^I get an error message saying I don't have an IP address$/ do
|
19
|
+
pending # express the regexp above with the code you wish you had
|
20
|
+
end
|
21
|
+
|
22
|
+
Given /^I have a local\-link IP address$/ do
|
23
|
+
pending # express the regexp above with the code you wish you had
|
24
|
+
end
|
25
|
+
|
26
|
+
Then /^the device starts running normally$/ do
|
27
|
+
pending # express the regexp above with the code you wish you had
|
28
|
+
end
|