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