brewby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 67034c466328c725de4cd15b591c95955e0eed93
|
4
|
+
data.tar.gz: 9ede21737fa8fcd2bfe51ad241822d99c0e31305
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bc34919f55b18b24f5e2ab51b717ad0563b568cace52f123b3751766c729d29922147dacfe42c16cf3bb6b78a942af0f139ba65fa49453e0cf2651364bc03c39
|
7
|
+
data.tar.gz: e33629c1845ef23ba705db6a63c8de1201b00948bdc9c4db5859cb82e52a76e3201d29e0dba0c1bc0ca0a88271f9f06260eacb861d0cd4a7994476329c78e3ef
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Andrew Nordman
|
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,112 @@
|
|
1
|
+
# Brewby
|
2
|
+
|
3
|
+
Brewby Core functionality underpinning Brewby - IO interfacing Layer, Recipe DSL, Configuration Loading,
|
4
|
+
and more.
|
5
|
+
|
6
|
+
This library is the foundation for brewery automation applications themselves. Unless you are
|
7
|
+
writing a brewery automation application, you will not use this library directly.
|
8
|
+
|
9
|
+
|
10
|
+
## Input Sensors
|
11
|
+
|
12
|
+
brewby provides adapters for input sensors in a consistent interface. The following adapters
|
13
|
+
are currently supported, with several more on the way:
|
14
|
+
|
15
|
+
* `Brewby::Input::DS18B20` - Dallas 18B20 temperature sensors runnning on the One-Wire Bus
|
16
|
+
* `Brewby::Input::Test` - A fake sensor that grabs a random number. Used for testing purposes
|
17
|
+
|
18
|
+
## Output Adapters
|
19
|
+
|
20
|
+
brewby also provides an interface for controlling output for heat sources. There are two
|
21
|
+
layers to this system. The first is the low-level interface adapters for output: GPIO, serial
|
22
|
+
communication, etc. These are what trigger the on/off states directly. On top of that is the
|
23
|
+
layer that handles knowing when to turn on/off a heating element. We control this via the
|
24
|
+
`Brewby::HeatingElement` class, passing it an output adapter. By setting a few variables, we
|
25
|
+
can simulate a percentage of total output power utilizing software-based pulse-width modulation.
|
26
|
+
The principle of pulse width modulation is that you break on/off cycle into equal time lengths
|
27
|
+
with an identical output waves. For example, if you wish to simulate 50% power but only have an
|
28
|
+
on/off switch, you break it up into equal, one-second time lengths, with 500 milliseconds on
|
29
|
+
and 500 milliseconds off. The duration of the wave length is called the pulse width, and the
|
30
|
+
total size of the wave length is called the pulse range. By modulating between on/off states,
|
31
|
+
you simulate power levels.
|
32
|
+
|
33
|
+
Here is an example of the `Brewby::HeatingElement` class in action, with a 5 second pulse range
|
34
|
+
and a 2.5 second pulse width. By continuously calling `pulse`, we check the time and toggle the
|
35
|
+
element on and off via the passed in adapter:
|
36
|
+
|
37
|
+
``` ruby
|
38
|
+
adapter = Brewby::Outputs::Test.new
|
39
|
+
element = Brewby::HeatingElement.new(adapter, pulse_range: 5000)
|
40
|
+
element.pulse_width = 2500 # 50% power
|
41
|
+
|
42
|
+
loop do
|
43
|
+
element.pulse
|
44
|
+
if element.on?
|
45
|
+
puts "Within pulse width"
|
46
|
+
else
|
47
|
+
puts "Outside pulse width"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
## Recipe Steps
|
53
|
+
|
54
|
+
Creating a good brew requires several steps in the process, some automatable and some eot. Brewby
|
55
|
+
treats these steps as logic gates, encapsulating the control for each step based on a step type.
|
56
|
+
Currently, Brewby only has the `TempControl` step for handling temperature control via manual
|
57
|
+
and automatic control and duration. Here is an example of a temperature control step for a mash
|
58
|
+
step, holding at 155F for 75 minutes:
|
59
|
+
|
60
|
+
``` ruby
|
61
|
+
sensor = Brewby::Inputs::Test.new
|
62
|
+
relay = Brewby::Outputs::Test.new
|
63
|
+
element = Brewby::HeatingElement.new(relay, pulse_range: 5000)
|
64
|
+
step = Brewby::Steps::TempControl.new({
|
65
|
+
mode: :auto,
|
66
|
+
input: sensor,
|
67
|
+
output: element,
|
68
|
+
target: 150.0,
|
69
|
+
duration: 75
|
70
|
+
})
|
71
|
+
|
72
|
+
loop do
|
73
|
+
step.step_iteration
|
74
|
+
break if step.time_remaining <= 0
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
## Brewby Applications
|
79
|
+
|
80
|
+
As you can imagine, writing out several steps can be very repetitive, especially when handling
|
81
|
+
something like a multiple decoction step mash schedule. Not only do the steps need to be handled,
|
82
|
+
but the equipment configuration needs to be setup every time a new step is created. To handle this,
|
83
|
+
steps are wrapped into an Application to act as the bridge between Steps and IO.
|
84
|
+
|
85
|
+
``` ruby
|
86
|
+
class StepMash < Brewby::Application
|
87
|
+
def tick
|
88
|
+
super
|
89
|
+
render_status
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_status
|
93
|
+
@last_output ||= Time.now
|
94
|
+
if @last_output < (Time.now - 1)
|
95
|
+
puts "Target: #{current_step.target}F\tActual: #{current_step.last_reading}F\tPower Level: #{current_step.power_level * 100}%"
|
96
|
+
@last_output = Time.now
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
application = StepMash.new
|
102
|
+
application.add_input :test
|
103
|
+
application.add_output :test
|
104
|
+
application.add_step :temp_control, target: 125.0, duration: 15
|
105
|
+
application.add_step :temp_control, target: 155.0, duration: 35
|
106
|
+
application.add_step :temp_control, target: 168.0, duration: 10
|
107
|
+
|
108
|
+
application.start
|
109
|
+
```
|
110
|
+
|
111
|
+
By default, Applications use the first input and output when adding new steps unless passed in
|
112
|
+
as an option to the `add_step` method.
|
data/Rakefile
ADDED
data/brewby.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |gem|
|
3
|
+
gem.authors = ["Andrew Nordman"]
|
4
|
+
gem.email = ["cadwallion@gmail.com"]
|
5
|
+
gem.summary = %q{The core components of the Brewby brewing system}
|
6
|
+
gem.homepage = ""
|
7
|
+
|
8
|
+
gem.files = `git ls-files`.split($\)
|
9
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
10
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
11
|
+
gem.name = "brewby"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.version = "0.1.0"
|
14
|
+
|
15
|
+
gem.add_development_dependency 'rspec'
|
16
|
+
gem.add_development_dependency 'rake'
|
17
|
+
gem.add_development_dependency 'pry'
|
18
|
+
|
19
|
+
gem.add_dependency 'temper-control'
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'brewby'
|
3
|
+
|
4
|
+
class StepMash < Brewby::Application
|
5
|
+
def tick
|
6
|
+
super
|
7
|
+
render_status
|
8
|
+
end
|
9
|
+
|
10
|
+
def render_status
|
11
|
+
@last_output ||= Time.now
|
12
|
+
if @last_output < (Time.now - 1)
|
13
|
+
puts "Target: #{current_step.target}F\tActual: #{current_step.last_reading}F\tPower Level: #{current_step.power_level * 100}%"
|
14
|
+
@last_output = Time.now
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
application = StepMash.new adapter: 'test', inputs: [{}], outputs: [{}]
|
20
|
+
application.add_step :temp_control, target: 125.0, duration: 15
|
21
|
+
application.add_step :temp_control, target: 155.0, duration: 35
|
22
|
+
application.add_step :temp_control, target: 168.0, duration: 10
|
23
|
+
|
24
|
+
application.start
|
@@ -0,0 +1,35 @@
|
|
1
|
+
recipe 'Honey Ale' do
|
2
|
+
step 'Strike Water' do
|
3
|
+
type :temp_control
|
4
|
+
mode :auto
|
5
|
+
target 168.0
|
6
|
+
hold_duration 5
|
7
|
+
input :hlt
|
8
|
+
output :hlt
|
9
|
+
end
|
10
|
+
|
11
|
+
step 'Infusion Mash Step' do
|
12
|
+
type :temp_control
|
13
|
+
mode :auto
|
14
|
+
target 150.0
|
15
|
+
hold_duration 60
|
16
|
+
input :mlt
|
17
|
+
output :hlt
|
18
|
+
end
|
19
|
+
|
20
|
+
step 'Fly Sparge' do
|
21
|
+
type :temp_control
|
22
|
+
mode :auto
|
23
|
+
target 168.0
|
24
|
+
hold_duration 45
|
25
|
+
input :hlt
|
26
|
+
output :hlt
|
27
|
+
end
|
28
|
+
|
29
|
+
step 'Boil' do
|
30
|
+
type :temp_control
|
31
|
+
mode :manual
|
32
|
+
input :bk
|
33
|
+
output :bk
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"adapter":"raspberry_pi",
|
3
|
+
"inputs":[
|
4
|
+
{
|
5
|
+
"hardware_id":"28-something"
|
6
|
+
},
|
7
|
+
{
|
8
|
+
"hardware_id":"28-somethingelse"
|
9
|
+
}
|
10
|
+
],
|
11
|
+
"outputs":[
|
12
|
+
{
|
13
|
+
"pulse_range":5000,
|
14
|
+
"pin":17
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"pulse_range":5000,
|
18
|
+
"pin":19
|
19
|
+
}
|
20
|
+
]
|
21
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'brewby'
|
3
|
+
|
4
|
+
class RecipeLoader < Brewby::Application
|
5
|
+
def tick
|
6
|
+
super
|
7
|
+
render_status
|
8
|
+
end
|
9
|
+
|
10
|
+
def render_status
|
11
|
+
@last_output ||= Time.now
|
12
|
+
if @last_output < (Time.now - 1)
|
13
|
+
puts "Target: #{current_step.target}F\tActual: #{current_step.last_reading}F\tPower Level: #{current_step.power_level * 100}%"
|
14
|
+
@last_output = Time.now
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
app = RecipeLoader.new({
|
20
|
+
adapter: 'test',
|
21
|
+
inputs: [{ name: 'hlt' }, { name: 'bk' }],
|
22
|
+
outputs: [{ name: 'hlt' }, { name: 'bk' }]
|
23
|
+
})
|
24
|
+
|
25
|
+
file = ARGV[0] || "examples/brewby_recipe.rb"
|
26
|
+
puts "Loading Recipe #{file}"
|
27
|
+
app.load_recipe File.expand_path(file)
|
28
|
+
app.start
|
data/lib/brewby.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'brewby/inputs'
|
2
|
+
require 'brewby/outputs'
|
3
|
+
|
4
|
+
module Brewby
|
5
|
+
class Application
|
6
|
+
attr_reader :outputs, :inputs, :steps
|
7
|
+
attr_accessor :adapter, :name
|
8
|
+
|
9
|
+
include Brewby::Timed
|
10
|
+
|
11
|
+
def initialize options = {}
|
12
|
+
@options = options
|
13
|
+
@steps = []
|
14
|
+
@adapter = options[:adapter].to_sym
|
15
|
+
configure_inputs
|
16
|
+
configure_outputs
|
17
|
+
@ready = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure_inputs
|
21
|
+
@inputs = []
|
22
|
+
|
23
|
+
@options[:inputs].each do |input_options|
|
24
|
+
add_input @adapter, input_options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_input adapter, options = {}
|
29
|
+
sensor = Brewby::Inputs.adapter_class(adapter).new options
|
30
|
+
@inputs.push sensor
|
31
|
+
end
|
32
|
+
|
33
|
+
def configure_outputs
|
34
|
+
@outputs = []
|
35
|
+
|
36
|
+
@options[:outputs].each do |output_options|
|
37
|
+
add_output @adapter, output_options
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_output adapter, options = {}
|
42
|
+
output_adapter = Brewby::Outputs.adapter_class(adapter).new options
|
43
|
+
element = Brewby::HeatingElement.new output_adapter, pulse_range: options[:pulse_range], name: options[:name]
|
44
|
+
@outputs.push element
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_step step_type, options = {}
|
48
|
+
case step_type
|
49
|
+
when :temp_control
|
50
|
+
default_options = { input: @inputs.first, output: @outputs.first }
|
51
|
+
step = Brewby::Steps::TempControl.new default_options.merge(options)
|
52
|
+
end
|
53
|
+
@steps.push step
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_recipe file
|
57
|
+
Brewby::StepLoader.new(self).load_file file
|
58
|
+
end
|
59
|
+
|
60
|
+
def start
|
61
|
+
start_timer
|
62
|
+
@steps.each do |step|
|
63
|
+
start_step step
|
64
|
+
until ready_for_next_step?
|
65
|
+
tick
|
66
|
+
end
|
67
|
+
@ready = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ready_for_next_step?
|
72
|
+
@ready
|
73
|
+
end
|
74
|
+
|
75
|
+
def tick
|
76
|
+
current_step.step_iteration
|
77
|
+
end
|
78
|
+
|
79
|
+
def start_step step
|
80
|
+
@current_step = step
|
81
|
+
step.start_timer
|
82
|
+
end
|
83
|
+
|
84
|
+
def current_step
|
85
|
+
@current_step || @steps[0]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Brewby
|
2
|
+
class HeatingElement
|
3
|
+
attr_accessor :pulse_width, :pulse_range, :name, :adapter
|
4
|
+
|
5
|
+
def initialize adapter, options = {}
|
6
|
+
@pulse_range = options[:pulse_range] || 1000
|
7
|
+
@on = false
|
8
|
+
@pulse_range_end = (Time.now.to_i * 1000) + @pulse_range
|
9
|
+
@adapter = adapter
|
10
|
+
@pulse_width = 0
|
11
|
+
@name = options[:name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def pulse
|
15
|
+
set_pulse_time
|
16
|
+
update_pulse_range if pulse_exceeds_range?
|
17
|
+
|
18
|
+
if pulse_within_width?
|
19
|
+
on!
|
20
|
+
else
|
21
|
+
off!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_pulse_time
|
26
|
+
@pulse_time = (Time.now.to_i * 1000)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pulse_within_width?
|
30
|
+
@pulse_time <= pulse_end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pulse_exceeds_range?
|
34
|
+
@pulse_time > @pulse_range_end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_pulse_range
|
38
|
+
@pulse_range_end += @pulse_range
|
39
|
+
end
|
40
|
+
|
41
|
+
def pulse_end
|
42
|
+
@pulse_range_end - (@pulse_range - @pulse_width)
|
43
|
+
end
|
44
|
+
|
45
|
+
def on!
|
46
|
+
@adapter.on
|
47
|
+
end
|
48
|
+
|
49
|
+
def off!
|
50
|
+
@adapter.off
|
51
|
+
end
|
52
|
+
|
53
|
+
def on?
|
54
|
+
@adapter.on?
|
55
|
+
end
|
56
|
+
|
57
|
+
def off?
|
58
|
+
!on?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|