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.
@@ -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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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.
@@ -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.
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+ require "bundler/gem_tasks"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,6 @@
1
+ require "brewby/version"
2
+ require 'brewby/steps'
3
+ require 'brewby/application'
4
+
5
+ module Brewby
6
+ end
@@ -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