playful 0.1.0.alpha.1
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/.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
|