whipped-cream 0.1.1 → 0.2.0.beta1

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/.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