whipped-cream 0.0.1pre1
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.
- data/.cane +3 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +10 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +10 -0
- data/WELCOME +14 -0
- data/bin/whipped-cream +7 -0
- data/demo.rb +9 -0
- data/lib/whipped-cream.rb +8 -0
- data/lib/whipped-cream/builder.rb +60 -0
- data/lib/whipped-cream/button.rb +15 -0
- data/lib/whipped-cream/cli.rb +43 -0
- data/lib/whipped-cream/control.rb +12 -0
- data/lib/whipped-cream/pi_piper.rb +28 -0
- data/lib/whipped-cream/plugin.rb +38 -0
- data/lib/whipped-cream/public/assets/application.css +9 -0
- data/lib/whipped-cream/runner.rb +79 -0
- data/lib/whipped-cream/sensor.rb +21 -0
- data/lib/whipped-cream/server.rb +80 -0
- data/lib/whipped-cream/version.rb +3 -0
- data/lib/whipped-cream/views/button.erb +3 -0
- data/lib/whipped-cream/views/index.erb +4 -0
- data/lib/whipped-cream/views/layout.erb +12 -0
- data/lib/whipped-cream/views/sensor.erb +2 -0
- data/spec/lib/whipped-cream/builder_spec.rb +139 -0
- data/spec/lib/whipped-cream/button_spec.rb +13 -0
- data/spec/lib/whipped-cream/cli_spec.rb +52 -0
- data/spec/lib/whipped-cream/plugin_spec.rb +48 -0
- data/spec/lib/whipped-cream/runner_spec.rb +80 -0
- data/spec/lib/whipped-cream/sensor_spec.rb +27 -0
- data/spec/lib/whipped-cream/server_spec.rb +39 -0
- data/spec/lib/whipped-cream_spec.rb +7 -0
- data/spec/spec_helper.rb +20 -0
- data/whipped-cream.gemspec +34 -0
- metadata +226 -0
data/.cane
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
guard :rspec, all_on_start: true, all_after_pass: true, env: { skip_coverage: true } do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
4
|
+
watch('spec/spec_helper.rb') { "spec" }
|
5
|
+
end
|
6
|
+
|
7
|
+
guard :cane do
|
8
|
+
watch(/.*\.rb/)
|
9
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Justin Campbell
|
2
|
+
|
3
|
+
MIT License
|
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
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# whipped-cream
|
2
|
+
|
3
|
+
## DSL
|
4
|
+
|
5
|
+
```rb
|
6
|
+
name "Garage"
|
7
|
+
|
8
|
+
camera
|
9
|
+
|
10
|
+
button "Open/Close", pin: 1 do
|
11
|
+
tap
|
12
|
+
end
|
13
|
+
|
14
|
+
sensor "Door",
|
15
|
+
pin: 2,
|
16
|
+
low: "Closed",
|
17
|
+
high: "Open",
|
18
|
+
on_high: :door_opened
|
19
|
+
|
20
|
+
sensor "Temperature", pin: 3, unit: "F" do
|
21
|
+
binary_to_farenheit(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
helpers do
|
25
|
+
def binary_to_farenheit(binary)
|
26
|
+
binary.to_f * 123.45
|
27
|
+
end
|
28
|
+
|
29
|
+
def door_opened
|
30
|
+
WhippedCream.find("Front Door").ring_bell
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
data/Rakefile
ADDED
data/WELCOME
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Thanks for installing whipped-cream! Please don't hesitate to report bugs
|
3
|
+
and feature requests at:
|
4
|
+
|
5
|
+
https://github.com/justincampbell/whipped-cream/issues
|
6
|
+
|
7
|
+
To run a demo, just do:
|
8
|
+
|
9
|
+
whipped-cream demo
|
10
|
+
|
11
|
+
Or use the help command to see more options:
|
12
|
+
|
13
|
+
whipped-cream help
|
14
|
+
|
data/bin/whipped-cream
ADDED
data/demo.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module WhippedCream
|
2
|
+
# Creates Plugin instances from a DSL
|
3
|
+
class Builder
|
4
|
+
def self.build(&block)
|
5
|
+
builder = new(&block)
|
6
|
+
builder.build
|
7
|
+
builder.plugin
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_file(path)
|
11
|
+
contents = File.read(path)
|
12
|
+
|
13
|
+
from_string(contents)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_string(string)
|
17
|
+
builder = new(string)
|
18
|
+
builder.build
|
19
|
+
builder.plugin
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :block, :string
|
23
|
+
|
24
|
+
def initialize(string = nil, &block)
|
25
|
+
@block = block
|
26
|
+
@string = string
|
27
|
+
end
|
28
|
+
|
29
|
+
def build
|
30
|
+
@build ||= string ?
|
31
|
+
instance_eval(string) :
|
32
|
+
instance_eval(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def plugin
|
36
|
+
@plugin ||= Plugin.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def button(name, options = {}, &block)
|
40
|
+
options = options.merge({ block: block })
|
41
|
+
plugin.controls << Button.new(name, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def camera
|
45
|
+
plugin.camera = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def helpers(&block)
|
49
|
+
plugin.instance_eval(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def name(string)
|
53
|
+
plugin.name = string
|
54
|
+
end
|
55
|
+
|
56
|
+
def sensor(name, options = {}, &block)
|
57
|
+
plugin.controls << Sensor.new(name, options, &block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'whipped-cream/control'
|
2
|
+
|
3
|
+
module WhippedCream
|
4
|
+
# A Button represents a one-time action, such as momentarily turning on a
|
5
|
+
# pin, or sending a message to an object
|
6
|
+
class Button < Control
|
7
|
+
attr_reader :name, :pin, :block
|
8
|
+
|
9
|
+
def initialize(name, options = {})
|
10
|
+
@name = name
|
11
|
+
@pin = options[:pin]
|
12
|
+
@block = options[:block]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module WhippedCream
|
4
|
+
# The CLI gets invoked from the binary, and encapsulates all user interaction
|
5
|
+
# logic
|
6
|
+
class CLI < Thor
|
7
|
+
default_task :usage
|
8
|
+
|
9
|
+
desc "usage", "Display usage banner", hide: true
|
10
|
+
def usage
|
11
|
+
puts [
|
12
|
+
"whipped-cream #{WhippedCream::VERSION}",
|
13
|
+
"https://github.com/justincampbell/whipped-cream"
|
14
|
+
].join("\n")
|
15
|
+
|
16
|
+
puts "\n"
|
17
|
+
|
18
|
+
help
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "demo", "Start the demo plugin"
|
22
|
+
def demo
|
23
|
+
plugin = Plugin.from_file(File.expand_path('../../../demo.rb', __FILE__))
|
24
|
+
server = Server.new(plugin)
|
25
|
+
server.start
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "start PLUGIN", "Start a plugin"
|
29
|
+
def start(plugin_name)
|
30
|
+
plugin_path = resolve_plugin(plugin_name)
|
31
|
+
|
32
|
+
plugin = Plugin.from_file(plugin_path)
|
33
|
+
server = Server.new(plugin)
|
34
|
+
server.start
|
35
|
+
end
|
36
|
+
|
37
|
+
no_tasks do
|
38
|
+
def resolve_plugin(name)
|
39
|
+
name # TODO: resolve name to filename
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Fake PiPiper
|
2
|
+
module PiPiper
|
3
|
+
# Just enough to imitate a pin
|
4
|
+
class Pin
|
5
|
+
attr_reader :pin, :direction, :value
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@pin = options[:pin]
|
9
|
+
@direction = options[:direction]
|
10
|
+
end
|
11
|
+
|
12
|
+
def on
|
13
|
+
@value = :on
|
14
|
+
|
15
|
+
log "Pin #{pin} on"
|
16
|
+
end
|
17
|
+
|
18
|
+
def off
|
19
|
+
@value = :off
|
20
|
+
|
21
|
+
log "Pin #{pin} off"
|
22
|
+
end
|
23
|
+
|
24
|
+
def log(message)
|
25
|
+
puts message unless ENV['RUBY_ENV'] == 'test'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module WhippedCream
|
2
|
+
# Data representation of a Plugin
|
3
|
+
class Plugin
|
4
|
+
attr_accessor :camera, :name
|
5
|
+
|
6
|
+
def self.build(&block)
|
7
|
+
Builder.build(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_file(path)
|
11
|
+
Builder.from_file(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_string(string)
|
15
|
+
Builder.from_string(string)
|
16
|
+
end
|
17
|
+
|
18
|
+
def controls
|
19
|
+
@controls ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def buttons
|
23
|
+
controls.select { |control| control.is_a? Button }
|
24
|
+
end
|
25
|
+
|
26
|
+
def fields
|
27
|
+
controls.select { |control| control.is_a? Field }
|
28
|
+
end
|
29
|
+
|
30
|
+
def sensors
|
31
|
+
controls.select { |control| control.is_a? Sensor }
|
32
|
+
end
|
33
|
+
|
34
|
+
def switches
|
35
|
+
controls.select { |control| control.is_a? Switch }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
begin
|
2
|
+
require 'pi_piper'
|
3
|
+
rescue LoadError
|
4
|
+
require 'whipped-cream/pi_piper'
|
5
|
+
end
|
6
|
+
|
7
|
+
module WhippedCream
|
8
|
+
# Actor that manages all interaction with a plugin
|
9
|
+
class Runner
|
10
|
+
def self.instance
|
11
|
+
@instance
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.create_instance(plugin)
|
15
|
+
@instance = new(plugin)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :plugin
|
19
|
+
|
20
|
+
def initialize(plugin)
|
21
|
+
@plugin = plugin
|
22
|
+
|
23
|
+
configure
|
24
|
+
end
|
25
|
+
|
26
|
+
def name
|
27
|
+
plugin.name
|
28
|
+
end
|
29
|
+
|
30
|
+
def pins
|
31
|
+
@pins ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def configure
|
37
|
+
configure_buttons
|
38
|
+
configure_sensors
|
39
|
+
end
|
40
|
+
|
41
|
+
def configure_buttons
|
42
|
+
plugin.buttons.each do |button|
|
43
|
+
create_pin button, direction: :out
|
44
|
+
|
45
|
+
define_singleton_method button.id do
|
46
|
+
pin = pins[button.id]
|
47
|
+
|
48
|
+
pin.on
|
49
|
+
sleep 0.25
|
50
|
+
pin.off
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def configure_sensors
|
56
|
+
plugin.sensors.each do |sensor|
|
57
|
+
create_pin sensor, direction: :in
|
58
|
+
|
59
|
+
define_singleton_method sensor.id do
|
60
|
+
if sensor.pin
|
61
|
+
pin = pins[sensor.id]
|
62
|
+
|
63
|
+
pin.value == 1 ? sensor.high : sensor.low
|
64
|
+
else
|
65
|
+
sensor.block.call
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_pin(control, options = {})
|
72
|
+
return unless control.pin
|
73
|
+
|
74
|
+
options[:pin] = control.pin
|
75
|
+
|
76
|
+
pins[control.id] = PiPiper::Pin.new(options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'whipped-cream/control'
|
2
|
+
|
3
|
+
module WhippedCream
|
4
|
+
# A Sensor displays the state of something, such as a pin's value, or the
|
5
|
+
# return value of the method. Sensors can also have callbacks associated with
|
6
|
+
# state changes.
|
7
|
+
class Sensor < Control
|
8
|
+
attr_reader :name, :pin, :low, :high, :on_low, :on_high, :block
|
9
|
+
|
10
|
+
def initialize(name, options = {}, &block)
|
11
|
+
@name = name
|
12
|
+
@pin = options[:pin]
|
13
|
+
@low = options[:low]
|
14
|
+
@high = options[:high]
|
15
|
+
@on_low = options[:on_low]
|
16
|
+
@on_high = options[:on_high]
|
17
|
+
|
18
|
+
@block = block
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module WhippedCream
|
4
|
+
# A server handles building a plugin/runner and starting a web server
|
5
|
+
class Server
|
6
|
+
attr_reader :plugin
|
7
|
+
|
8
|
+
def initialize(plugin)
|
9
|
+
@plugin = plugin
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
ensure_routes_built
|
14
|
+
ensure_runner_started
|
15
|
+
|
16
|
+
start_web
|
17
|
+
end
|
18
|
+
|
19
|
+
def runner
|
20
|
+
@runner ||= Runner.create_instance(plugin)
|
21
|
+
end
|
22
|
+
|
23
|
+
def port
|
24
|
+
8080
|
25
|
+
end
|
26
|
+
|
27
|
+
def web
|
28
|
+
@web ||= Web
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def ensure_runner_started
|
34
|
+
runner
|
35
|
+
end
|
36
|
+
|
37
|
+
def ensure_routes_built
|
38
|
+
@routes_built ||= build_routes || true
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_web
|
42
|
+
Rack::Server.start app: web, port: port
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_routes
|
46
|
+
build_button_routes
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_button_routes
|
50
|
+
plugin.buttons.each do |button|
|
51
|
+
web.get "/#{button.id}" do
|
52
|
+
runner.send(button.id)
|
53
|
+
redirect to('/')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# A Sinatra application skeleton that is used to build up the web server
|
59
|
+
# for this plugin.
|
60
|
+
class Web < Sinatra::Application
|
61
|
+
get '/' do
|
62
|
+
erb :index
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def controls
|
68
|
+
runner.plugin.controls
|
69
|
+
end
|
70
|
+
|
71
|
+
def runner
|
72
|
+
Runner.instance
|
73
|
+
end
|
74
|
+
|
75
|
+
def title
|
76
|
+
runner.name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<!DOCTYPE HTML>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%= title %></title>
|
5
|
+
<link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css">
|
6
|
+
<meta content="width=device-width" name="viewport">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<h1><%= title %></h1>
|
10
|
+
<%= yield %>
|
11
|
+
</body>
|
12
|
+
</html>
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Builder do
|
4
|
+
subject { plugin }
|
5
|
+
|
6
|
+
let(:plugin) {
|
7
|
+
described_class.build do
|
8
|
+
name "Garage"
|
9
|
+
end
|
10
|
+
}
|
11
|
+
let(:plugin_file_path) { "foo/bar/baz" }
|
12
|
+
let(:plugin_string) {
|
13
|
+
<<-PLUGIN
|
14
|
+
name "Garage"
|
15
|
+
|
16
|
+
button "Open/Close", pin: 1
|
17
|
+
PLUGIN
|
18
|
+
}
|
19
|
+
|
20
|
+
it "returns a plugin" do
|
21
|
+
should be_a(WhippedCream::Plugin)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with helper methods" do
|
25
|
+
subject {
|
26
|
+
described_class.build do
|
27
|
+
helpers do
|
28
|
+
def foo
|
29
|
+
:bar
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
it "defines the methods on the object" do
|
36
|
+
expect(subject.foo).to eq(:bar)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe ".from_file" do
|
41
|
+
before do
|
42
|
+
File.stub read: plugin_string
|
43
|
+
end
|
44
|
+
|
45
|
+
it "reads the file" do
|
46
|
+
expect(File).to receive(:read).with(plugin_file_path)
|
47
|
+
|
48
|
+
described_class.from_file plugin_file_path
|
49
|
+
end
|
50
|
+
|
51
|
+
it "parses the contents" do
|
52
|
+
plugin = described_class.from_file(plugin_file_path)
|
53
|
+
|
54
|
+
expect(plugin.name).to eq("Garage")
|
55
|
+
expect(plugin.controls.first.name).to eq("Open/Close")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe ".from_string" do
|
60
|
+
it "parses the string" do
|
61
|
+
plugin = described_class.from_string(plugin_string)
|
62
|
+
|
63
|
+
expect(plugin.name).to eq("Garage")
|
64
|
+
expect(plugin.controls.first.name).to eq("Open/Close")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#camera" do
|
69
|
+
subject { plugin.camera }
|
70
|
+
|
71
|
+
it { should be_false }
|
72
|
+
|
73
|
+
context "with camera in the plugin" do
|
74
|
+
let(:plugin) {
|
75
|
+
described_class.build do
|
76
|
+
camera
|
77
|
+
end
|
78
|
+
}
|
79
|
+
|
80
|
+
it { should be_true }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#button" do
|
85
|
+
subject { plugin.buttons }
|
86
|
+
|
87
|
+
it { should be_empty }
|
88
|
+
|
89
|
+
context "with a button" do
|
90
|
+
let(:plugin) {
|
91
|
+
described_class.build do
|
92
|
+
button "Open/Close", pin: 1 do
|
93
|
+
:tap
|
94
|
+
end
|
95
|
+
end
|
96
|
+
}
|
97
|
+
|
98
|
+
it "adds a button" do
|
99
|
+
expect(plugin.buttons).to have(1).item
|
100
|
+
|
101
|
+
button = plugin.buttons.first
|
102
|
+
|
103
|
+
expect(button.name).to eq("Open/Close")
|
104
|
+
expect(button.pin).to eq(1)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#sensor(s)" do
|
110
|
+
subject { plugin.sensors }
|
111
|
+
|
112
|
+
it { should be_empty }
|
113
|
+
|
114
|
+
context "with a sensor" do
|
115
|
+
let(:plugin) {
|
116
|
+
described_class.build do
|
117
|
+
sensor "Door",
|
118
|
+
pin: 2,
|
119
|
+
low: "Closed",
|
120
|
+
high: "Open",
|
121
|
+
on_high: :door_opened
|
122
|
+
end
|
123
|
+
}
|
124
|
+
|
125
|
+
it "adds a sensor" do
|
126
|
+
expect(plugin.sensors).to have(1).item
|
127
|
+
|
128
|
+
sensor = plugin.sensors.first
|
129
|
+
|
130
|
+
expect(sensor.name).to eq("Door")
|
131
|
+
expect(sensor.pin).to eq(2)
|
132
|
+
expect(sensor.low).to eq("Closed")
|
133
|
+
expect(sensor.high).to eq("Open")
|
134
|
+
expect(sensor.on_high).to eq(:door_opened)
|
135
|
+
expect(sensor.block).to be_nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Button do
|
4
|
+
subject(:button) { described_class.new(name, pin: pin, block: block) }
|
5
|
+
|
6
|
+
let(:name) { "Open/Close" }
|
7
|
+
let(:pin) { nil }
|
8
|
+
let(:block) { nil }
|
9
|
+
|
10
|
+
its(:name) { should eq(name) }
|
11
|
+
its(:id) { should eq(:open_close) }
|
12
|
+
its(:type) { should eq(:button) }
|
13
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
describe WhippedCream::CLI do
|
7
|
+
subject { cli }
|
8
|
+
let(:cli) { described_class.new }
|
9
|
+
|
10
|
+
let(:plugin_filename) { File.join(tmpdir, "garage.rb") }
|
11
|
+
let(:plugin_string) {
|
12
|
+
<<-PLUGIN
|
13
|
+
name "Garage"
|
14
|
+
|
15
|
+
button "Open/Close", pin: 1
|
16
|
+
PLUGIN
|
17
|
+
}
|
18
|
+
let(:tmpdir) { Dir.mktmpdir }
|
19
|
+
|
20
|
+
before do
|
21
|
+
File.open(plugin_filename, 'w') { |file| file.write plugin_string }
|
22
|
+
end
|
23
|
+
|
24
|
+
after do
|
25
|
+
FileUtils.rm_rf tmpdir
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#demo" do
|
29
|
+
it "launches a web server with an example plugin" do
|
30
|
+
expect(Rack::Server).to receive(:start)
|
31
|
+
|
32
|
+
cli.demo
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#usage" do
|
37
|
+
it "displays a banner and help" do
|
38
|
+
expect(cli).to receive(:puts).exactly(2).times
|
39
|
+
expect(cli).to receive(:help)
|
40
|
+
|
41
|
+
cli.usage
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#start" do
|
46
|
+
it "starts a server for the plugin" do
|
47
|
+
expect(Rack::Server).to receive(:start)
|
48
|
+
|
49
|
+
cli.start(plugin_filename)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Plugin do
|
4
|
+
subject(:plugin) { described_class.new }
|
5
|
+
|
6
|
+
its(:camera) { should be_nil }
|
7
|
+
its(:name) { should be_nil }
|
8
|
+
|
9
|
+
its(:controls) { should be_empty }
|
10
|
+
|
11
|
+
its(:buttons) { should be_empty }
|
12
|
+
its(:fields) { should be_empty }
|
13
|
+
its(:sensors) { should be_empty }
|
14
|
+
its(:switches) { should be_empty }
|
15
|
+
|
16
|
+
describe ".build" do
|
17
|
+
it "delegates to Builder" do
|
18
|
+
block = -> {}
|
19
|
+
expect(WhippedCream::Builder).to receive(:build).with(&block)
|
20
|
+
described_class.build(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".from_file" do
|
25
|
+
it "delegates to Builder" do
|
26
|
+
path = "foo/bar"
|
27
|
+
expect(WhippedCream::Builder).to receive(:from_file).with(path)
|
28
|
+
described_class.from_file(path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe ".from_string" do
|
33
|
+
it "delegates to Builder" do
|
34
|
+
string = "name 'Garage'"
|
35
|
+
expect(WhippedCream::Builder).to receive(:from_string).with(string)
|
36
|
+
described_class.from_string(string)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with a button" do
|
41
|
+
before do
|
42
|
+
plugin.controls << WhippedCream::Button.new("Open/Close")
|
43
|
+
end
|
44
|
+
|
45
|
+
its(:controls) { should_not be_empty }
|
46
|
+
its(:buttons) { should_not be_empty }
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Runner do
|
4
|
+
subject(:runner) { described_class.new(plugin) }
|
5
|
+
|
6
|
+
let(:plugin) {
|
7
|
+
WhippedCream::Plugin.build do
|
8
|
+
name "Garage"
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
its(:name) { should eq("Garage") }
|
13
|
+
|
14
|
+
context "with a button" do
|
15
|
+
let(:plugin) {
|
16
|
+
WhippedCream::Plugin.build do
|
17
|
+
button "Open/Close", pin: 1
|
18
|
+
end
|
19
|
+
}
|
20
|
+
|
21
|
+
it "sets up that pin with direction: :out" do
|
22
|
+
pin = runner.pins[:open_close]
|
23
|
+
|
24
|
+
expect(pin).to be_a(PiPiper::Pin)
|
25
|
+
expect(pin.pin).to eq(1)
|
26
|
+
expect(pin.direction).to eq(:out)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "defines an open_close method that taps the pin" do
|
30
|
+
pin = runner.pins[:open_close]
|
31
|
+
|
32
|
+
expect(pin).to receive(:on)
|
33
|
+
expect(runner).to receive(:sleep).with(0.25)
|
34
|
+
expect(pin).to receive(:off)
|
35
|
+
|
36
|
+
runner.open_close
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with a sensor" do
|
41
|
+
let(:plugin) {
|
42
|
+
WhippedCream::Plugin.build do
|
43
|
+
sensor "Door", pin: 2, low: "Open", high: "Closed"
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
it "sets up that pin with direction: :in" do
|
48
|
+
pin = runner.pins[:door]
|
49
|
+
|
50
|
+
expect(pin).to be_a(PiPiper::Pin)
|
51
|
+
expect(pin.pin).to eq(2)
|
52
|
+
expect(pin.direction).to eq(:in)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "defines a method that reads and converts the pin's value" do
|
56
|
+
pin = runner.pins[:door]
|
57
|
+
pin.stub value: 1
|
58
|
+
|
59
|
+
expect(runner.door).to eq("Closed")
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with a block and no pin" do
|
63
|
+
let(:plugin) {
|
64
|
+
WhippedCream::Plugin.build do
|
65
|
+
sensor "Foo" do
|
66
|
+
"Bar"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
}
|
70
|
+
|
71
|
+
it "does not set up a pin" do
|
72
|
+
expect(runner.pins[:door]).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "defines a method that calls the block" do
|
76
|
+
expect(runner.foo).to eq("Bar")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Sensor do
|
4
|
+
subject(:sensor) { described_class.new(name, options, &block) }
|
5
|
+
|
6
|
+
let(:name) { "Door" }
|
7
|
+
let(:options) {
|
8
|
+
{
|
9
|
+
pin: 2,
|
10
|
+
high: "Open",
|
11
|
+
low: "Closed",
|
12
|
+
on_high: :door_opened
|
13
|
+
}
|
14
|
+
}
|
15
|
+
let(:block) { nil }
|
16
|
+
|
17
|
+
its(:name) { should eq(name) }
|
18
|
+
its(:id) { should eq(:door) }
|
19
|
+
|
20
|
+
its(:pin) { should eq(2) }
|
21
|
+
|
22
|
+
its(:high) { should eq("Open") }
|
23
|
+
its(:low) { should eq("Closed") }
|
24
|
+
|
25
|
+
its(:on_high) { should eq(:door_opened) }
|
26
|
+
its(:on_low) { should be_nil }
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WhippedCream::Server do
|
4
|
+
subject(:server) { described_class.new(plugin) }
|
5
|
+
|
6
|
+
let(:plugin) {
|
7
|
+
WhippedCream::Plugin.build do
|
8
|
+
button "Open/Close", pin: 1
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
before do
|
13
|
+
Rack::Server.stub :start
|
14
|
+
end
|
15
|
+
|
16
|
+
it "creates a runner with the plugin" do
|
17
|
+
server.runner.stub :sleep
|
18
|
+
|
19
|
+
server.runner.open_close
|
20
|
+
end
|
21
|
+
|
22
|
+
it "reuses the same runner" do
|
23
|
+
expect(server.runner).to eq(server.runner)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "builds up a Sinatra application from a plugin" do
|
27
|
+
server.start
|
28
|
+
|
29
|
+
expect(
|
30
|
+
server.web.routes['GET'].find { |route| route.first.match('/open_close') }
|
31
|
+
).to be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "starts the Sinatra application" do
|
35
|
+
expect(Rack::Server).to receive(:start)
|
36
|
+
|
37
|
+
server.start
|
38
|
+
end
|
39
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
ENV['RUBY_ENV'] ||= 'test'
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
puts RUBY_DESCRIPTION
|
6
|
+
|
7
|
+
unless ENV['skip_coverage']
|
8
|
+
require 'simplecov'
|
9
|
+
SimpleCov.start
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'whipped-cream'
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
config.expect_with :rspec do |c|
|
16
|
+
c.syntax = :expect
|
17
|
+
end
|
18
|
+
|
19
|
+
config.order = :random
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'whipped-cream/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'whipped-cream'
|
8
|
+
spec.version = WhippedCream::VERSION
|
9
|
+
spec.authors = ["Justin Campbell"]
|
10
|
+
spec.email = ["justin@justincampbell.me"]
|
11
|
+
spec.description = "HTTP topping for Raspberry Pi"
|
12
|
+
spec.summary = "HTTP topping for Raspberry Pi"
|
13
|
+
spec.homepage = 'https://github.com/justincampbell/whipped-cream'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.post_install_message = File.read('WELCOME')
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.required_ruby_version = '>= 1.9.3'
|
24
|
+
|
25
|
+
spec.add_runtime_dependency 'pi_piper'
|
26
|
+
spec.add_runtime_dependency 'sinatra'
|
27
|
+
spec.add_runtime_dependency 'thor'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
30
|
+
spec.add_development_dependency 'cane'
|
31
|
+
spec.add_development_dependency 'rake'
|
32
|
+
spec.add_development_dependency 'rspec'
|
33
|
+
spec.add_development_dependency 'simplecov'
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: whipped-cream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1pre1
|
5
|
+
prerelease: 5
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Justin Campbell
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-05 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pi_piper
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sinatra
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: thor
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: bundler
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.3'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.3'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: cane
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rake
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rspec
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: simplecov
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
description: HTTP topping for Raspberry Pi
|
143
|
+
email:
|
144
|
+
- justin@justincampbell.me
|
145
|
+
executables:
|
146
|
+
- whipped-cream
|
147
|
+
extensions: []
|
148
|
+
extra_rdoc_files: []
|
149
|
+
files:
|
150
|
+
- .cane
|
151
|
+
- .gitignore
|
152
|
+
- .rspec
|
153
|
+
- .ruby-version
|
154
|
+
- .travis.yml
|
155
|
+
- Gemfile
|
156
|
+
- Guardfile
|
157
|
+
- LICENSE.txt
|
158
|
+
- README.md
|
159
|
+
- Rakefile
|
160
|
+
- WELCOME
|
161
|
+
- bin/whipped-cream
|
162
|
+
- demo.rb
|
163
|
+
- lib/whipped-cream.rb
|
164
|
+
- lib/whipped-cream/builder.rb
|
165
|
+
- lib/whipped-cream/button.rb
|
166
|
+
- lib/whipped-cream/cli.rb
|
167
|
+
- lib/whipped-cream/control.rb
|
168
|
+
- lib/whipped-cream/pi_piper.rb
|
169
|
+
- lib/whipped-cream/plugin.rb
|
170
|
+
- lib/whipped-cream/public/assets/application.css
|
171
|
+
- lib/whipped-cream/runner.rb
|
172
|
+
- lib/whipped-cream/sensor.rb
|
173
|
+
- lib/whipped-cream/server.rb
|
174
|
+
- lib/whipped-cream/version.rb
|
175
|
+
- lib/whipped-cream/views/button.erb
|
176
|
+
- lib/whipped-cream/views/index.erb
|
177
|
+
- lib/whipped-cream/views/layout.erb
|
178
|
+
- lib/whipped-cream/views/sensor.erb
|
179
|
+
- spec/lib/whipped-cream/builder_spec.rb
|
180
|
+
- spec/lib/whipped-cream/button_spec.rb
|
181
|
+
- spec/lib/whipped-cream/cli_spec.rb
|
182
|
+
- spec/lib/whipped-cream/plugin_spec.rb
|
183
|
+
- spec/lib/whipped-cream/runner_spec.rb
|
184
|
+
- spec/lib/whipped-cream/sensor_spec.rb
|
185
|
+
- spec/lib/whipped-cream/server_spec.rb
|
186
|
+
- spec/lib/whipped-cream_spec.rb
|
187
|
+
- spec/spec_helper.rb
|
188
|
+
- whipped-cream.gemspec
|
189
|
+
homepage: https://github.com/justincampbell/whipped-cream
|
190
|
+
licenses:
|
191
|
+
- MIT
|
192
|
+
post_install_message: ! "\n Thanks for installing whipped-cream! Please don't hesitate
|
193
|
+
to report bugs\n and feature requests at:\n\n https://github.com/justincampbell/whipped-cream/issues\n\n
|
194
|
+
\ To run a demo, just do:\n\n whipped-cream demo\n\n Or use the help command
|
195
|
+
to see more options:\n\n whipped-cream help\n\n"
|
196
|
+
rdoc_options: []
|
197
|
+
require_paths:
|
198
|
+
- lib
|
199
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
200
|
+
none: false
|
201
|
+
requirements:
|
202
|
+
- - ! '>='
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: 1.9.3
|
205
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
206
|
+
none: false
|
207
|
+
requirements:
|
208
|
+
- - ! '>'
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: 1.3.1
|
211
|
+
requirements: []
|
212
|
+
rubyforge_project:
|
213
|
+
rubygems_version: 1.8.23
|
214
|
+
signing_key:
|
215
|
+
specification_version: 3
|
216
|
+
summary: HTTP topping for Raspberry Pi
|
217
|
+
test_files:
|
218
|
+
- spec/lib/whipped-cream/builder_spec.rb
|
219
|
+
- spec/lib/whipped-cream/button_spec.rb
|
220
|
+
- spec/lib/whipped-cream/cli_spec.rb
|
221
|
+
- spec/lib/whipped-cream/plugin_spec.rb
|
222
|
+
- spec/lib/whipped-cream/runner_spec.rb
|
223
|
+
- spec/lib/whipped-cream/sensor_spec.rb
|
224
|
+
- spec/lib/whipped-cream/server_spec.rb
|
225
|
+
- spec/lib/whipped-cream_spec.rb
|
226
|
+
- spec/spec_helper.rb
|