brewby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +112 -0
- data/Rakefile +6 -0
- data/brewby.gemspec +20 -0
- data/examples/basic_control.rb +24 -0
- data/examples/brewby_recipe.rb +35 -0
- data/examples/config.json +21 -0
- data/examples/recipe_loader.rb +28 -0
- data/lib/brewby.rb +6 -0
- data/lib/brewby/application.rb +88 -0
- data/lib/brewby/heating_element.rb +61 -0
- data/lib/brewby/inputs.rb +15 -0
- data/lib/brewby/inputs/ds18b20.rb +51 -0
- data/lib/brewby/inputs/test.rb +14 -0
- data/lib/brewby/outputs.rb +15 -0
- data/lib/brewby/outputs/gpio.rb +40 -0
- data/lib/brewby/outputs/test.rb +23 -0
- data/lib/brewby/step_loader.rb +27 -0
- data/lib/brewby/steps.rb +8 -0
- data/lib/brewby/steps/dsl/step.rb +48 -0
- data/lib/brewby/steps/temp_control.rb +111 -0
- data/lib/brewby/temp_sensor.rb +13 -0
- data/lib/brewby/timed.rb +50 -0
- data/lib/brewby/version.rb +3 -0
- data/spec/application_spec.rb +49 -0
- data/spec/heating_element_spec.rb +44 -0
- data/spec/inputs/ds18b20_spec.rb +39 -0
- data/spec/inputs_spec.rb +8 -0
- data/spec/outputs/gpio_spec.rb +65 -0
- data/spec/outputs_spec.rb +8 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/step_loader_spec.rb +18 -0
- data/spec/steps/dsl/step_spec.rb +61 -0
- data/spec/steps/temp_control_spec.rb +187 -0
- data/spec/support/sample_recipe.rb +32 -0
- data/spec/support/virtual_view.rb +31 -0
- data/spec/timed_spec.rb +110 -0
- metadata +151 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::HeatingElement do
|
4
|
+
def millis_from_now i
|
5
|
+
(Time.now.to_i + i) * 1000
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
adapter = Brewby::Outputs::Test.new
|
10
|
+
@element = Brewby::HeatingElement.new(adapter, pulse_range: 5000)
|
11
|
+
@element.pulse_width = 3000
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'relay pulsing' do
|
15
|
+
it 'turn on the relay when first pulsed' do
|
16
|
+
@element.should be_off
|
17
|
+
@element.pulse
|
18
|
+
@element.should be_on
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'turns on the relay while within pulse width' do
|
22
|
+
@element.should be_off
|
23
|
+
@element.instance_variable_set(:@pulse_range_end, millis_from_now(4))
|
24
|
+
@element.pulse
|
25
|
+
@element.should be_on
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'turns off the relay when time exceeds pulse width' do
|
29
|
+
@element.instance_variable_set(:@pulse_range_end, millis_from_now(5))
|
30
|
+
@element.pulse
|
31
|
+
@element.should be_on
|
32
|
+
@element.instance_variable_set(:@pulse_range_end, millis_from_now(1))
|
33
|
+
@element.pulse
|
34
|
+
@element.should be_off
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'turns on the relay when time hits the next pulse range' do
|
38
|
+
@element.should be_off
|
39
|
+
@element.instance_variable_set(:@pulse_range_end, millis_from_now(-1))
|
40
|
+
@element.pulse
|
41
|
+
@element.should be_on
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::Inputs::DS18B20 do
|
4
|
+
it 'accepts a hardware_id' do
|
5
|
+
sensor = Brewby::Inputs::DS18B20.new hardware_id: 'something'
|
6
|
+
sensor.hardware_id.should == 'something'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'picks the first hardware_id it finds when no hardware_id is specified' do
|
10
|
+
Dir.mktmpdir do |dir|
|
11
|
+
File.write "#{dir}/28-12345", "1"
|
12
|
+
sensor = Brewby::Inputs::DS18B20.new device_path: dir
|
13
|
+
sensor.hardware_id.should == '28-12345'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'reading sensor data' do
|
18
|
+
before do
|
19
|
+
Brewby::Inputs::DS18B20.any_instance.stub(:read_raw) { "f5 00 4b 46 7f ff 0b 10 d7 : crc=d7 YES\nf5 00 4b 46 7f ff 0b 10 d7 t=15312" }
|
20
|
+
@sensor = Brewby::Inputs::DS18B20.new device_path: @device_dir, hardware_id: '28-12345'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'reads the temperaturee and returns it in fahrenheit' do
|
24
|
+
input = @sensor.read
|
25
|
+
input.should == 59.562
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'parses the raw data to celsius' do
|
29
|
+
tempC = @sensor.parse @sensor.read_raw
|
30
|
+
tempC.should == 15.312
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns nil when an error occurs when parsing' do
|
34
|
+
Brewby::Inputs::DS18B20.any_instance.stub(:read_raw) { "f5 00 4b 46 7f ff 0b 10 d7 : crc=d7 NO\nf5 00 4b 46 7f ff 0b 10 d7" }
|
35
|
+
input = @sensor.read
|
36
|
+
input.should be_nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/inputs_spec.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::Outputs::GPIO do
|
4
|
+
before do
|
5
|
+
@gpio_dir = Dir.mktmpdir
|
6
|
+
Dir.mkdir "#{@gpio_dir}/gpio1"
|
7
|
+
File.write "#{@gpio_dir}/export", ""
|
8
|
+
File.write "#{@gpio_dir}/gpio1/direction", ""
|
9
|
+
end
|
10
|
+
|
11
|
+
after do
|
12
|
+
FileUtils.remove_entry @gpio_dir
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'GPIO initialization' do
|
16
|
+
it 'initializes the GPIO pin via export' do
|
17
|
+
Brewby::Outputs::GPIO.new pin: 1, gpio_path: @gpio_dir
|
18
|
+
data = File.read "#{@gpio_dir}/export"
|
19
|
+
data.should == '1'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'initializes the GPIO pin direction' do
|
23
|
+
Brewby::Outputs::GPIO.new pin: 1, gpio_path: @gpio_dir
|
24
|
+
data = File.read "#{@gpio_dir}/gpio1/direction"
|
25
|
+
data.should == 'out'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does not initialize the GPIO pin if already initialized' do
|
29
|
+
File.write "#{@gpio_dir}/gpio1/value", ""
|
30
|
+
Brewby::Outputs::GPIO.new pin: 1, gpio_path: @gpio_dir
|
31
|
+
data = File.read "#{@gpio_dir}/export"
|
32
|
+
data.should == ''
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'output' do
|
37
|
+
before do
|
38
|
+
@output = Brewby::Outputs::GPIO.new pin: 1, gpio_path: @gpio_dir
|
39
|
+
File.write "#{@gpio_dir}/gpio1/value", ""
|
40
|
+
end
|
41
|
+
|
42
|
+
def pin_value
|
43
|
+
File.read "#{@gpio_dir}/gpio1/value"
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'sets the pin value to 1 when on' do
|
47
|
+
@output.on
|
48
|
+
pin_value.should == '1'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sets the pin value to 0 when off' do
|
52
|
+
@output.off
|
53
|
+
pin_value.should == '0'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns false when off' do
|
57
|
+
@output.should_not be_on
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns true when on' do
|
61
|
+
@output.on
|
62
|
+
@output.should be_on
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::Outputs do
|
4
|
+
it 'determines output class based on adapter' do
|
5
|
+
Brewby::Outputs.adapter_class(:test).should == Brewby::Outputs::Test
|
6
|
+
Brewby::Outputs.adapter_class(:raspberry_pi).should == Brewby::Outputs::GPIO
|
7
|
+
end
|
8
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::StepLoader do
|
4
|
+
before do
|
5
|
+
Brewby::Application.any_instance.stub(:render)
|
6
|
+
Brewby::Application.any_instance.stub(:configure_view)
|
7
|
+
|
8
|
+
@application = Brewby::Application.new adapter: :test,
|
9
|
+
outputs: [{ pin: 1, name: :hlt }, { pin: 2, name: :mlt }, { pin: 3, name: :bk }],
|
10
|
+
inputs: [{ name: :hlt}, { name: :mlt }, { name: :bk }]
|
11
|
+
@loader = Brewby::StepLoader.new @application
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'reads a Brewby process file' do
|
15
|
+
@loader.load_file File.join(File.dirname(__FILE__), 'support', 'sample_recipe.rb')
|
16
|
+
@application.should have(4).steps
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::Steps::DSL::Step do
|
4
|
+
before do
|
5
|
+
@outputs = [
|
6
|
+
{ name: :hlt, pin: 3 },
|
7
|
+
{ name: :mlt, pin: 2 },
|
8
|
+
{ name: :bk, pin: 1 }
|
9
|
+
]
|
10
|
+
|
11
|
+
@inputs = [
|
12
|
+
{ name: :bk },
|
13
|
+
{ name: :mlt },
|
14
|
+
{ name: :hlt }
|
15
|
+
]
|
16
|
+
|
17
|
+
Brewby::Application.any_instance.stub(:render)
|
18
|
+
Brewby::Application.any_instance.stub(:configure_view)
|
19
|
+
@application = Brewby::Application.new adapter: :test, outputs: @outputs, inputs: @inputs
|
20
|
+
@step = Brewby::Steps::DSL::Step.new 'Test Step', @application
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'accepts a type' do
|
24
|
+
@step.type :temp_control
|
25
|
+
@step.step_class.should == Brewby::Steps::TempControl
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'accepts options on the type' do
|
29
|
+
@step.type :temp_control, mode: :auto, target: 155.0, duration: 60
|
30
|
+
@step.options[:mode].should == :auto
|
31
|
+
@step.options[:target].should == 155.0
|
32
|
+
@step.options[:duration].should == 60
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'accepts a mode' do
|
36
|
+
@step.mode :manual
|
37
|
+
@step.options[:mode].should == :manual
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'accepts a target' do
|
41
|
+
@step.target 155.0
|
42
|
+
@step.options[:target].should == 155.0
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'creation' do
|
46
|
+
before do
|
47
|
+
@step.type :temp_control, mode: :manual, power_level: 1.0
|
48
|
+
@step.input :mlt
|
49
|
+
@step.output :bk
|
50
|
+
@created_step = @step.create!
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should translate the input correctly' do
|
54
|
+
@created_step.input.name.should == :mlt
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should translate the output correctly' do
|
58
|
+
@created_step.output.name.should == :bk
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Brewby::Steps::TempControl do
|
4
|
+
let(:sensor) { Brewby::TempSensor.new 1 }
|
5
|
+
let(:adapter) { Brewby::Outputs::Test.new }
|
6
|
+
let(:element) { Brewby::HeatingElement.new adapter, pulse_width: 5000 }
|
7
|
+
let(:step) { Brewby::Steps::TempControl.new mode: :manual, input: sensor, output: element }
|
8
|
+
|
9
|
+
it 'configures an input sensor' do
|
10
|
+
step.input.should be_instance_of Brewby::TempSensor
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'configures an output sensor' do
|
14
|
+
step.output.should be_instance_of Brewby::HeatingElement
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'automatic temperature control' do
|
18
|
+
before do
|
19
|
+
@step = Brewby::Steps::TempControl.new mode: :auto, target: 155.0,
|
20
|
+
duration: 15, output: element, input: sensor
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'configures a PID controller' do
|
24
|
+
@step.pid.should be_instance_of Temper::PID
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sets a target temperature' do
|
28
|
+
@step.target.should == 155.0
|
29
|
+
@step.pid.setpoint.should == 155.0
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sets a temperature hold duration' do
|
33
|
+
@step.duration.should == 15
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'defaults to a 1 minute temperature hold duration' do
|
37
|
+
step = Brewby::Steps::TempControl.new mode: :auto, target: 155.0
|
38
|
+
step.duration.should == 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns true for automatic control' do
|
42
|
+
@step.automatic_control?.should be_true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns false for manual control' do
|
46
|
+
@step.manual_control?.should be_false
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'calculates the output level based on PID levels' do
|
50
|
+
@step.calculate_power_level
|
51
|
+
@step.power_level.should == 1.0
|
52
|
+
@step.output.pulse_width.should == 5000
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'does not explode with a faulty input' do
|
56
|
+
@step.stub(:read_input) { nil }
|
57
|
+
@step.calculate_power_level
|
58
|
+
@step.output.pulse_width.should == 0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'manual temperature control' do
|
63
|
+
before do
|
64
|
+
@step = Brewby::Steps::TempControl.new mode: :manual, power_level: 0.85, output: element, input: sensor
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'does not create a PID controller' do
|
68
|
+
@step.pid.should be_nil
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns true for manual mode' do
|
72
|
+
@step.should be_manual_control
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'returns false for automatic control' do
|
76
|
+
@step.should_not be_automatic_control
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'sets the power level' do
|
80
|
+
@step.power_level.should == 0.85
|
81
|
+
@step.output.pulse_width.should == 4250
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can have the power level set manually' do
|
85
|
+
@step.set_power_level 0.75
|
86
|
+
@step.power_level.should == 0.75
|
87
|
+
@step.output.pulse_width.should == 3750
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'sensor input' do
|
92
|
+
before do
|
93
|
+
@step = Brewby::Steps::TempControl.new mode: :auto, target: 155.0,
|
94
|
+
duration: 15, input: sensor, output: element
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'reads sensor input' do
|
98
|
+
@step.input.should_receive(:read) { 115.0 }
|
99
|
+
@step.read_input.should == 115.0
|
100
|
+
@step.last_reading.should == 115.0
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'does not set last_reading if sensor input is faulty' do
|
104
|
+
@step.input.stub(:read) { nil }
|
105
|
+
@step.read_input.should be_nil
|
106
|
+
@step.last_reading.should == 0.0
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'step iteration' do
|
111
|
+
context 'with manual control' do
|
112
|
+
before do
|
113
|
+
@step = Brewby::Steps::TempControl.new mode: :manual, power_level: 0.85,
|
114
|
+
input: sensor, output: element
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'pulses the element' do
|
118
|
+
@step.step_iteration
|
119
|
+
@step.output.should be_on
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'takes a sensor reading' do
|
123
|
+
@step.input.stub(:read) { 115.0 }
|
124
|
+
@step.step_iteration
|
125
|
+
@step.last_reading.should == 115.0
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'does not take a sensor reading if input does not exist' do
|
129
|
+
@step = Brewby::Steps::TempControl.new mode: :manual, power_level: 0.85, output: element
|
130
|
+
@step.step_iteration
|
131
|
+
@step.last_reading.should == 0.0
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'with automatic control' do
|
136
|
+
before do
|
137
|
+
@step = Brewby::Steps::TempControl.new mode: :auto, target: 155.0,
|
138
|
+
duration: 15, input: sensor, output: element
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'pulses the element' do
|
142
|
+
@step.output.should_receive(:pulse)
|
143
|
+
@step.step_iteration
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'calculates the power level and adjusts the heating element' do
|
147
|
+
@step.pid.stub(:control) { 3000 }
|
148
|
+
@step.step_iteration
|
149
|
+
@step.output.pulse_width.should == 3000
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'reads from the sensor and logs to last_reading' do
|
153
|
+
@step.input.stub(:read) { 125.0 }
|
154
|
+
@step.step_iteration
|
155
|
+
@step.last_reading.should == 125.0
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'when temperature threshold is reached' do
|
159
|
+
before do
|
160
|
+
@step.input.stub(:read) { 156.0 }
|
161
|
+
@step.step_iteration
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'sets the threshold as true' do
|
165
|
+
@step.threshold_reached.should be_true
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'maintains threshold_reached even when temp drops below threshold' do
|
169
|
+
@step.input.stub(:read) { 145.0 }
|
170
|
+
@step.step_iteration
|
171
|
+
@step.threshold_reached.should be_true
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'starts the clock on time remaining' do
|
175
|
+
(@step.time_remaining > 0).should be_true
|
176
|
+
(@step.time_remaining <= @step.duration_in_seconds).should be_true
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'stops the step when temperature has hit target for duration' do
|
180
|
+
@step.instance_variable_set(:@step_finishes_at, Time.now.to_i - 10)
|
181
|
+
@step.step_iteration
|
182
|
+
@step.should be_ended
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|