whipped-cream 0.0.1pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|