whipped-cream 0.1.1 → 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -6,3 +6,6 @@ rvm:
6
6
  matrix:
7
7
  allow_failures:
8
8
  - rvm: ruby-head
9
+ before_install:
10
+ - sudo apt-get update -qq
11
+ - sudo apt-get install -qq avahi-daemon libavahi-compat-libdnssd-dev
data/CHANGELOG.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ * Broadcast and discover Whipped Cream servers using mDNS (@rringler)
6
+ * Improve button functionality (@rringler)
7
+ * Support starting plugins with or without an `.rb` extension (@rringler)
8
+
3
9
  ## 0.1.1
4
10
 
5
- * Validate GPIO pin numbers (@rringer)
11
+ * Validate GPIO pin numbers (@rringler)
6
12
 
7
13
  ## 0.1.0
8
14
 
@@ -1,4 +1,5 @@
1
1
  require 'thor'
2
+ require 'dnssd'
2
3
 
3
4
  module WhippedCream
4
5
  # The CLI gets invoked from the binary, and encapsulates all user interaction
@@ -47,9 +48,58 @@ module WhippedCream
47
48
  server.start
48
49
  end
49
50
 
51
+ desc "discover", "Discovers any whipped-cream servers on the local network"
52
+ def discover
53
+ services = browse_services('_whipped-cream._tcp.')
54
+
55
+ services.select { |_, service| service.flags.add? }.each do |_, service|
56
+ host = resolve_service(service)
57
+
58
+ puts "#{host[:address]}:#{host[:port]}\t#{host[:name]}"
59
+ end
60
+ end
61
+
50
62
  no_tasks do
51
63
  def resolve_plugin(name)
52
- name # TODO: resolve name to filename
64
+ name += '.rb' unless name.split('.').last == 'rb'
65
+ name
66
+ end
67
+
68
+ def browse_services(service_type)
69
+ services = {}
70
+
71
+ DNSSD::Service.new.browse(service_type) do |reply|
72
+ services[reply.fullname] = reply
73
+ break unless reply.flags.more_coming?
74
+ end
75
+
76
+ services
77
+ end
78
+
79
+ def resolve_service(service)
80
+ host = {}
81
+
82
+ DNSSD::Service.new.resolve(service) do |reply|
83
+ address = get_ipv4_address(reply)
84
+
85
+ host[:name] = "#{reply.name}"
86
+ host[:address] = "#{address}"
87
+ host[:port] = "#{reply.port}"
88
+ break unless reply.flags.more_coming?
89
+ end
90
+
91
+ host
92
+ end
93
+
94
+ def get_ipv4_address(reply)
95
+ address = nil
96
+
97
+ DNSSD::Service.new.getaddrinfo(reply.target, 1) do |addrinfo|
98
+ address = addrinfo.address
99
+ break unless addrinfo.flags.more_coming?
100
+ end
101
+
102
+ address
53
103
  end
54
104
  end
55
105
  end
@@ -55,6 +55,12 @@ module WhippedCream
55
55
  time sudo apt-get install ruby1.9.3 -y)
56
56
  fi
57
57
 
58
+ dpkg --status avahi-daemon > /dev/null ||
59
+ (time sudo apt-get update &&
60
+ time sudo apt-get install avahi-daemon &&
61
+ time sudo apt-get install libavahi-compat-libdnssd-dev &&
62
+ time sudo insserv avahi-daemon)
63
+
58
64
  which whipped-cream ||
59
65
  time sudo gem install whipped-cream --no-ri --no-rdoc --pre
60
66
 
@@ -31,6 +31,10 @@ module WhippedCream
31
31
  @pins ||= {}
32
32
  end
33
33
 
34
+ def read_pin(pin)
35
+ pin.read.zero? ? :off : :on
36
+ end
37
+
34
38
  private
35
39
 
36
40
  def configure
@@ -65,7 +69,7 @@ module WhippedCream
65
69
  define_singleton_method sensor.id do
66
70
  pin = pins[sensor.id]
67
71
 
68
- pin.read == 1 ? sensor.high : sensor.low
72
+ read_pin(pin) == :on ? sensor.high : sensor.low
69
73
  end
70
74
  end
71
75
 
@@ -79,8 +83,8 @@ module WhippedCream
79
83
  plugin.switches.each do |switch|
80
84
  create_pin switch, direction: :out
81
85
 
82
- define_singleton_method switch.id do
83
- toggle_pin(pins[switch.id])
86
+ define_singleton_method switch.id do |state|
87
+ set_pin(pins[switch.id], state)
84
88
  end
85
89
  end
86
90
  end
@@ -94,20 +98,18 @@ module WhippedCream
94
98
  end
95
99
 
96
100
  def tap_pin(pin)
97
- pin.on
101
+ set_pin(pin, :on)
98
102
 
99
103
  Thread.new {
100
104
  sleep 0.25
101
- pin.off
105
+ set_pin(pin, :off)
102
106
  }
103
107
  end
104
108
 
105
- def toggle_pin(pin)
106
- if pin.read.nonzero?
107
- pin.off
108
- else
109
- pin.on
110
- end
109
+ def set_pin(pin, state)
110
+ return if read_pin(pin) == state
111
+
112
+ pin.send(state)
111
113
  end
112
114
  end
113
115
  end
@@ -1,4 +1,5 @@
1
1
  require 'rack'
2
+ require 'dnssd'
2
3
 
3
4
  module WhippedCream
4
5
  # A server handles building a plugin/runner and starting a web server
@@ -14,6 +15,7 @@ module WhippedCream
14
15
  ensure_routes_built
15
16
  ensure_runner_started
16
17
 
18
+ register_server
17
19
  start_web
18
20
  end
19
21
 
@@ -47,6 +49,17 @@ module WhippedCream
47
49
  Rack::Server.start rack_options
48
50
  end
49
51
 
52
+ def register_server
53
+ name = runner.name || "<none>"
54
+ service = "_whipped-cream._tcp"
55
+ domain = nil
56
+ port = rack_options[:Port]
57
+
58
+ DNSSD.register(name, service, domain, port) do |reply|
59
+ raise "Unable to register web server" unless reply.flags.add?
60
+ end
61
+ end
62
+
50
63
  def build_routes
51
64
  build_button_routes
52
65
  build_switch_routes
@@ -63,8 +76,8 @@ module WhippedCream
63
76
 
64
77
  def build_switch_routes
65
78
  plugin.switches.each do |switch|
66
- web.get "/#{switch.id}" do
67
- runner.send switch.id
79
+ web.post "/#{switch.id}" do
80
+ runner.send switch.id, params[:state]
68
81
  redirect to('/')
69
82
  end
70
83
  end
@@ -1,3 +1,3 @@
1
1
  module WhippedCream
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0.beta1"
3
3
  end
@@ -1,7 +1,17 @@
1
1
  <div class="item">
2
2
  <h2><%= control.name %></h2>
3
- <p><%= runner.pins[control.id].read.nonzero? ? "On" : "Off" %></p>
4
- <form action="/<%= control.id %>" method="get">
5
- <input type="submit" value="<%= runner.pins[control.id].read.nonzero? ? "Off" : "On" %>">
3
+ <% pin = runner.pins[control.id] %>
4
+ <% pin_state = runner.read_pin(pin) %>
5
+ <p><%= pin_state.to_s.capitalize %></p>
6
+ <form action="/<%= control.id %>" method="post">
7
+ <input type="radio"
8
+ name="state"
9
+ value="on"
10
+ <%= "checked" if pin_state == :on %>> On <br />
11
+ <input type="radio"
12
+ name="state"
13
+ value="off"
14
+ <%= "checked" if pin_state == :off %>> Off <br />
15
+ <input type="submit" value="Submit">
6
16
  </form>
7
17
  </div>
@@ -7,7 +7,7 @@ describe WhippedCream::CLI do
7
7
  subject { cli }
8
8
  let(:cli) { described_class.new }
9
9
 
10
- let(:plugin_filename) { File.join(tmpdir, "garage.rb") }
10
+ let(:plugin_filename) { File.join(tmpdir, "garage") }
11
11
  let(:plugin_string) {
12
12
  <<-PLUGIN
13
13
  name "Garage"
@@ -19,7 +19,7 @@ describe WhippedCream::CLI do
19
19
  let(:tmpdir) { Dir.mktmpdir }
20
20
 
21
21
  before do
22
- File.open(plugin_filename, 'w') { |file| file.write plugin_string }
22
+ File.open("#{plugin_filename}.rb", 'w') { |file| file.write plugin_string }
23
23
  end
24
24
 
25
25
  after do
@@ -48,11 +48,22 @@ describe WhippedCream::CLI do
48
48
  describe "#start" do
49
49
  let(:server_double) { double(WhippedCream::Server) }
50
50
 
51
- it "starts a server for the plugin" do
52
- expect(WhippedCream::Server).to receive(:new) { server_double }
53
- expect(server_double).to receive(:start)
51
+ context "with a filename and extension" do
52
+ it "starts a server for the plugin" do
53
+ expect(WhippedCream::Server).to receive(:new) { server_double }
54
+ expect(server_double).to receive(:start)
55
+
56
+ cli.start("#{plugin_filename}.rb")
57
+ end
58
+ end
54
59
 
55
- cli.start(plugin_filename)
60
+ context "with just a filename and no extension" do
61
+ it "starts a server for the plugin" do
62
+ expect(WhippedCream::Server).to receive(:new) { server_double }
63
+ expect(server_double).to receive(:start)
64
+
65
+ cli.start(plugin_filename)
66
+ end
56
67
  end
57
68
 
58
69
  context "with --daemonize" do
@@ -83,6 +94,36 @@ describe WhippedCream::CLI do
83
94
  end
84
95
  end
85
96
 
97
+ describe "#discover" do
98
+ let(:service_type) { "_whipped-cream._tcp." }
99
+ let(:services_double) {
100
+ { "Test._whipped-cream._tcp.local." => double("reply") }
101
+ }
102
+ let(:host_double) {
103
+ { name: "Test",
104
+ address: "192.168.0.100",
105
+ port: "8080" }
106
+ }
107
+ let(:output_string) {
108
+ "#{host_double[:address]}:#{host_double[:port]}\t#{host_double[:name]}"
109
+ }
110
+
111
+ it "displays the host information for any running servers" do
112
+ cli.should_receive(:browse_services)
113
+ .with(service_type)
114
+ .and_return(services_double)
115
+
116
+ cli.should_receive(:resolve_service)
117
+ .and_return(host_double)
118
+
119
+ services_double.should_receive(:select).and_return(services_double)
120
+
121
+ expect(cli).to receive(:puts).with(output_string)
122
+
123
+ cli.discover
124
+ end
125
+ end
126
+
86
127
  describe "#usage" do
87
128
  it "displays a banner and help" do
88
129
  expect(cli).to receive(:puts).exactly(2).times
@@ -91,4 +132,42 @@ describe WhippedCream::CLI do
91
132
  cli.usage
92
133
  end
93
134
  end
135
+
136
+ describe "#browse_services" do
137
+ let(:service_type) { '_whipped-cream._tcp.' }
138
+
139
+ it "should call DNSSD::Services#browse" do
140
+ DNSSD::Service.any_instance
141
+ .should_receive(:browse)
142
+ .with(service_type)
143
+
144
+ cli.browse_services(service_type)
145
+ end
146
+ end
147
+
148
+ describe "#resolve_service" do
149
+ let(:service_double) { double("service") }
150
+
151
+ it "should call DNSSD::Services#resolve" do
152
+ DNSSD::Service.any_instance
153
+ .should_receive(:resolve)
154
+ .with(service_double)
155
+
156
+ cli.resolve_service(service_double)
157
+ end
158
+ end
159
+
160
+ describe "#get_ipv4_address" do
161
+ let(:reply_double) { double("dnssd_reply") }
162
+
163
+ it "should call DNSSD::Services#getaddrinfo" do
164
+ reply_double.stub(:target).and_return("test_host")
165
+
166
+ DNSSD::Service.any_instance
167
+ .should_receive(:getaddrinfo)
168
+ .with(reply_double.target, 1)
169
+
170
+ cli.get_ipv4_address(reply_double)
171
+ end
172
+ end
94
173
  end
@@ -29,9 +29,7 @@ describe WhippedCream::Runner do
29
29
  it "defines an open_close method that taps the pin" do
30
30
  pin = runner.pins[:open_close]
31
31
 
32
- expect(pin).to receive(:on)
33
- expect(runner).to receive(:sleep).with(0.25)
34
- expect(pin).to receive(:off)
32
+ expect(runner).to receive(:tap_pin).with(pin).and_call_original
35
33
 
36
34
  runner.open_close.join
37
35
  end
@@ -96,13 +94,59 @@ describe WhippedCream::Runner do
96
94
  it "defines a light method that switches the pin on and off" do
97
95
  pin = runner.pins[:light]
98
96
 
99
- expect(pin.read).to eq(0)
100
- runner.light
101
- expect(pin.read).to eq(1)
102
- runner.light
103
- expect(pin.read).to eq(0)
104
- runner.light
105
- expect(pin.read).to eq(1)
97
+ expect(runner.read_pin(pin)).to eq(:off)
98
+ runner.light(:on)
99
+ expect(runner.read_pin(pin)).to eq(:on)
100
+ runner.light(:off)
101
+ expect(runner.read_pin(pin)).to eq(:off)
102
+ runner.light(:on)
103
+ expect(runner.read_pin(pin)).to eq(:on)
104
+ end
105
+ end
106
+
107
+ describe "#tap_pin" do
108
+ let(:plugin) {
109
+ WhippedCream::Plugin.build do
110
+ button "Open/Close", pin: 4
111
+ end
112
+ }
113
+
114
+ it "turns the pin on momentarily and then turns it off" do
115
+ pin = runner.pins[:garage]
116
+
117
+ expect(runner).to receive(:set_pin).with(pin, :on)
118
+ expect(runner).to receive(:sleep).with(0.25)
119
+ expect(runner).to receive(:set_pin).with(pin, :off)
120
+
121
+ runner.send(:tap_pin, pin).join
122
+ end
123
+ end
124
+
125
+ describe "#set_pin" do
126
+ let(:plugin) {
127
+ WhippedCream::Plugin.build do
128
+ switch "Light", pin: 18
129
+ end
130
+ }
131
+ let(:pin) { runner.pins[:light] }
132
+ before(:each) { runner.send(:set_pin, pin, :off) }
133
+
134
+ it "orders the desired pin state when different from the current state" do
135
+ expect(pin).to receive(:on).and_call_original
136
+ runner.send(:set_pin, pin, :on)
137
+
138
+ expect(pin).to receive(:off)
139
+ runner.send(:set_pin, pin, :off)
140
+ end
141
+
142
+ it "does nothing when the ordered pin state matches the current state" do
143
+ expect(pin).not_to receive(:off)
144
+ runner.send(:set_pin, pin, :off)
145
+
146
+ runner.send(:set_pin, pin, :on)
147
+
148
+ expect(pin).not_to receive(:on)
149
+ runner.send(:set_pin, pin, :on)
106
150
  end
107
151
  end
108
152
  end
@@ -51,7 +51,7 @@ describe WhippedCream::Server do
51
51
  before { server.start }
52
52
 
53
53
  it "creates a switch route" do
54
- expect(server.web.routes['GET'].find { |route|
54
+ expect(server.web.routes['POST'].find { |route|
55
55
  route.first.match('/light')
56
56
  }).to be_true
57
57
  end
@@ -66,6 +66,17 @@ describe WhippedCream::Server do
66
66
  server.start
67
67
  end
68
68
 
69
+ it "registers the server via mDNS" do
70
+ expect(DNSSD).to receive(:register).with(
71
+ server.runner.name || "<none>",
72
+ '_whipped-cream._tcp',
73
+ nil,
74
+ server.options.fetch(:port, 8080)
75
+ )
76
+
77
+ server.start
78
+ end
79
+
69
80
  context "with daemonize: true" do
70
81
  let(:options) {
71
82
  { daemonize: true }
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_runtime_dependency 'pi_piper'
28
28
  spec.add_runtime_dependency 'sinatra'
29
29
  spec.add_runtime_dependency 'thor'
30
+ spec.add_runtime_dependency 'dnssd'
30
31
 
31
32
  spec.add_development_dependency 'bundler', '~> 1.3'
32
33
  spec.add_development_dependency 'cane'
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whipped-cream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
5
- prerelease:
4
+ version: 0.2.0.beta1
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Justin Campbell
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-10 00:00:00.000000000 Z
12
+ date: 2014-02-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-scp
@@ -91,6 +91,22 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: dnssd
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
94
110
  - !ruby/object:Gem::Dependency
95
111
  name: bundler
96
112
  requirement: !ruby/object:Gem::Requirement
@@ -269,9 +285,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
269
285
  required_rubygems_version: !ruby/object:Gem::Requirement
270
286
  none: false
271
287
  requirements:
272
- - - ! '>='
288
+ - - ! '>'
273
289
  - !ruby/object:Gem::Version
274
- version: '0'
290
+ version: 1.3.1
275
291
  requirements: []
276
292
  rubyforge_project:
277
293
  rubygems_version: 1.8.23