multi_movingsign 0.0.1

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.
Files changed (53) hide show
  1. data/.gitignore +21 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +9 -0
  4. data/.yardopts +1 -0
  5. data/CHANGELOG.md +17 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/PAGE_DEFINITION.md +115 -0
  9. data/README.md +133 -0
  10. data/Rakefile +1 -0
  11. data/bin/multi_movingsign +5 -0
  12. data/example.jpg +0 -0
  13. data/fonts/7-row-normal.png +0 -0
  14. data/fonts/README.md +1 -0
  15. data/lib/multi_movingsign.rb +9 -0
  16. data/lib/multi_movingsign/cli.rb +81 -0
  17. data/lib/multi_movingsign/errors.rb +10 -0
  18. data/lib/multi_movingsign/page_renderer.rb +223 -0
  19. data/lib/multi_movingsign/server.rb +317 -0
  20. data/lib/multi_movingsign/settings.rb +55 -0
  21. data/lib/multi_movingsign/sign.rb +37 -0
  22. data/lib/multi_movingsign/signs.rb +39 -0
  23. data/lib/multi_movingsign/testrc_loader.rb +17 -0
  24. data/lib/multi_movingsign/version.rb +3 -0
  25. data/multi_movingsign.gemspec +31 -0
  26. data/spec/cli_spec.rb +166 -0
  27. data/spec/noop_movingsign_sign.rb +47 -0
  28. data/spec/noop_movingsign_sign.yml +7 -0
  29. data/spec/page_renderer/example_1/1.yml +16 -0
  30. data/spec/page_renderer/example_1/2.yml +28 -0
  31. data/spec/page_renderer/example_1/4.yml +44 -0
  32. data/spec/page_renderer/example_1/5.json +10 -0
  33. data/spec/page_renderer/example_1/example_spec.rb +23 -0
  34. data/spec/page_renderer/example_1/page.yml +27 -0
  35. data/spec/page_renderer/example_2/1.yml +7 -0
  36. data/spec/page_renderer/example_2/2.yml +8 -0
  37. data/spec/page_renderer/example_2/4.json +9 -0
  38. data/spec/page_renderer/example_2/example_spec.rb +24 -0
  39. data/spec/page_renderer/example_2/page.yml +7 -0
  40. data/spec/page_renderer/example_3/1.yml +9 -0
  41. data/spec/page_renderer/example_3/3.yml +14 -0
  42. data/spec/page_renderer/example_3/example_spec.rb +22 -0
  43. data/spec/page_renderer/example_3/page.yml +12 -0
  44. data/spec/page_renderer/example_4/2.yml +12 -0
  45. data/spec/page_renderer/example_4/4.json +9 -0
  46. data/spec/page_renderer/example_4/example_spec.rb +24 -0
  47. data/spec/page_renderer/example_4/page.yml +12 -0
  48. data/spec/settings_1.yml +3 -0
  49. data/spec/settings_spec.rb +36 -0
  50. data/spec/spec_helper.rb +36 -0
  51. data/spec/support/doubles_support.rb +26 -0
  52. data/spec/support/executable_support.rb +112 -0
  53. metadata +244 -0
@@ -0,0 +1,55 @@
1
+ require 'yaml'
2
+ require 'hashie'
3
+ require 'multi_movingsign/sign'
4
+
5
+ module MultiMovingsign
6
+ # Settings wrapper (reads/saves from/to YAML file via {load} / {#dump})
7
+ class Settings
8
+ attr_accessor :mash
9
+
10
+ # Constructs a new {Settings} instance from settings saves in the specified YAML file
11
+ def self.load(path)
12
+ if File.exists? path
13
+ self.new YAML.load(File.read(path))
14
+ else
15
+ self.new({})
16
+ end
17
+ end
18
+
19
+ def initialize(hash = {})
20
+ self.mash = Hashie::Mash.new hash
21
+ end
22
+
23
+ # Returns an array of the configured {Sign}s
24
+ def signs
25
+ self.mash.signs ||= []
26
+
27
+ self.mash.signs.map { |hash| Sign.load(hash) }
28
+ end
29
+
30
+ # +true+ if any signs are configured
31
+ def signs?
32
+ ! self.signs.empty?
33
+ end
34
+
35
+ # Sets the list of conifgured {Sign}s via an array of serial port paths
36
+ #
37
+ # @example
38
+ # settings.sign_paths = ['/dev/ttyUSB0', '/dev/ttyUSB1']
39
+ def sign_paths=(paths)
40
+ self.mash.signs = paths.map { |path| {'path' => path} }
41
+ end
42
+
43
+ # Serializes (dumps) the settings into the specified YAML file
44
+ def dump(path)
45
+ File.open(path, 'w') do |f|
46
+ f.write(self.mash.to_hash.to_yaml)
47
+ end
48
+ end
49
+
50
+ # Default path for the settings YAML file
51
+ def self.default_settings_path
52
+ File.join(ENV['HOME'], '.multi_movingsign.yml')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ require 'movingsign_api'
2
+ require 'multi_movingsign/sign'
3
+
4
+ module MultiMovingsign
5
+ # Represents an individual Movingsign LED sign being driven
6
+ class Sign
7
+ attr_accessor :path
8
+
9
+ def initialize(path)
10
+ self.path = path
11
+ end
12
+
13
+ def self.load(hash)
14
+ path = hash['path'] || (raise InvalidInputError, "path key not specified")
15
+
16
+ self.new path
17
+ end
18
+
19
+ def show_text(text, options = {})
20
+ sign.show_text text, options
21
+ end
22
+
23
+ def set_sound(on)
24
+ sign.set_sound on
25
+ end
26
+
27
+ def to_hash
28
+ {'path' => self.path}
29
+ end
30
+
31
+ private
32
+
33
+ def sign
34
+ MovingsignApi::Sign.new self.path
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ require 'multi_movingsign/sign'
2
+
3
+ module MultiMovingsign
4
+ # Represents a collection of {Sign}s that you want to drive in unison
5
+ class Signs
6
+ attr_accessor :signs
7
+
8
+ # @param options [Array<Sign>] an array of {Sign} instances
9
+ def initialize(options)
10
+ if options.kind_of?(Array) && options.all? { |v| v.kind_of?(Sign) }
11
+ self.signs = options
12
+ else
13
+ raise InvalidInputError, "Invalid Signs#new options"
14
+ end
15
+ end
16
+
17
+ # Sends/show the specified page solution on the signs
18
+ def show_page_solution(solution)
19
+ threads = []
20
+ solution['signs'].each_with_index do |hash, i|
21
+ threads << Thread.new do
22
+ # Prepend "\xFDB" to set color to HIGH RED
23
+ # .gsub("\n", "\x7F") - replace new lines with the sign specific newline character
24
+ text = "\xFDB" + hash['content'].gsub("\n", "\x7F")
25
+ self.signs[i].show_text text, :display_pause => 3
26
+ end
27
+ end
28
+ threads.each { |t| t.join }
29
+ end
30
+
31
+ def show_identity
32
+ self.signs.each_with_index { |s, i| s.show_text "#{i} #{s.path}" }
33
+ end
34
+
35
+ def length
36
+ self.signs.length
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ require 'yaml'
2
+
3
+ # Helper that reads a test YAML file and loads/executes the appropriate scripts
4
+ # {file:spec/noop_movingsign_sign.yml}
5
+ class TestRCLoader
6
+ def self.load(testrc_path)
7
+ base_dir = File.expand_path(File.dirname testrc_path)
8
+ testrc_hash = YAML::load(File.read(testrc_path))
9
+
10
+ script_path = testrc_hash['script']
11
+ script_path = File.join(base_dir, script_path) if Pathname.new(script_path).relative?
12
+
13
+ parameters = testrc_hash['parameters'] || {}
14
+
15
+ Kernel.eval(File.read(script_path), binding, script_path)
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module MultiMovingsign
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'multi_movingsign/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "multi_movingsign"
8
+ spec.version = MultiMovingsign::VERSION
9
+ spec.authors = ["Eric Webb"]
10
+ spec.email = ["eric@collectivegenius.net"]
11
+ spec.description = "Code that drives multiple MovingSign display boards in unison."
12
+ spec.summary = "Code that drives multiple MovingSign display boards in unison."
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ .reject { |path| path.match /\A\.idea\// } # Ignore .idea/ IntelliJ project directory
18
+ .reject { |path| path.match /\A[^\/]+\.iml/ } # Ignore *.iml IntelliJ module file
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "wwtd"
27
+
28
+ spec.add_runtime_dependency 'thor', '~> 0.18.1'
29
+ spec.add_runtime_dependency 'hashie', '~> 2.0'
30
+ spec.add_runtime_dependency 'movingsign_api', '0.0.2'
31
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+ require 'timeout'
4
+
5
+ describe MultiMovingsign::Cli do
6
+ let(:temp_settings_file) { File.join(Dir.mktmpdir('test-settings-'), 'test-settings.yml') }
7
+ let(:multi_movingsign_exe) { Gem.bin_path('multi_movingsign', 'multi_movingsign') }
8
+
9
+ # Testing magic, @see TestRCLoader and {file:spec/noop_movingsign_sign.rb}
10
+ let(:testrc_noop) { "spec/noop_movingsign_sign.yml" }
11
+ let(:testrc_results) { Hashie::Mash.new YAML::load(File.read('testrc.yml')) }
12
+
13
+ let(:setup_command_results) { execute("#{multi_movingsign_exe} setup --rc #{temp_settings_file} --signs /dev/ttyUSB0") }
14
+
15
+ describe "'setup' command" do
16
+ it "No Options" do
17
+ expect(execute("#{multi_movingsign_exe} setup")).to be_success
18
+ end
19
+
20
+ describe "--signs /dev/ttyUSB0" do
21
+ let (:execution_results) { execute("#{multi_movingsign_exe} setup --rc #{temp_settings_file} --signs /dev/ttyUSB0") }
22
+ let (:hash) { YAML.load(File.read(temp_settings_file)) }
23
+
24
+ before(:each) do
25
+ execution_results
26
+ end
27
+
28
+ it "Succeeds" do
29
+ expect(execution_results).to be_success
30
+ end
31
+
32
+ it "Settings File Exists" do
33
+ expect(File.exists? temp_settings_file).to be_true
34
+ end
35
+
36
+ it "Settings File Contains ttyUSB0" do
37
+ expect(File.read(temp_settings_file)).to include 'ttyUSB0'
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "'settings' command" do
43
+ let (:settings_command_results) { execute("#{multi_movingsign_exe} settings --rc #{temp_settings_file}") }
44
+
45
+ describe "Without Setup" do
46
+ before(:each) do
47
+ settings_command_results
48
+ end
49
+
50
+ it "Succeeds" do
51
+ expect(settings_command_results).to be_success
52
+ end
53
+
54
+ it "Containts 'Signs (0)'" do
55
+ expect(settings_command_results.stdout).to include 'Signs (0)'
56
+ end
57
+ end
58
+
59
+ describe "With Setup" do
60
+ before(:each) do
61
+ setup_command_results
62
+ settings_command_results
63
+ end
64
+
65
+ it "Succeeds" do
66
+ expect(settings_command_results).to be_success
67
+ end
68
+
69
+ it "Containts 'ttyUSB0'" do
70
+ expect(settings_command_results.stdout).to include 'ttyUSB0'
71
+ end
72
+
73
+ it "Containts 'Signs (1)'" do
74
+ expect(settings_command_results.stdout).to include 'Signs (1)'
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "'show-identity' command" do
80
+ let (:show_identity_command_results) { execute("#{multi_movingsign_exe} show-identity --rc #{temp_settings_file} --testrc #{testrc_noop}") }
81
+
82
+ before(:each) do
83
+ setup_command_results
84
+
85
+ double_movingsign_sign
86
+ show_identity_command_results
87
+ end
88
+
89
+ it "Succeeds" do
90
+ expect_command_success show_identity_command_results
91
+ end
92
+
93
+ it "Signs Calls == 1" do
94
+ expect(testrc_results.calls.length).to eq 1
95
+ end
96
+ end
97
+
98
+ describe "'server' subcommand" do
99
+ let (:server_configuration_directory) { Dir.mktmpdir "multi_movingsign_server-" }
100
+ let (:server_thread) { thread = Thread.new { execute("#{multi_movingsign_exe} server start --rc #{temp_settings_file} --serverrc #{server_configuration_directory} --testrc #{testrc_noop}") }; sleep 1; thread }
101
+
102
+ before(:each) do
103
+ setup_command_results
104
+ end
105
+
106
+ describe "'start' command" do
107
+ before(:each) do
108
+ double_movingsign_sign
109
+ server_thread
110
+ end
111
+
112
+ after(:each) do
113
+ server_thread.kill
114
+ server_thread.join
115
+ end
116
+
117
+ it "Is Alive?" do
118
+ expect(server_thread).to be_alive
119
+ end
120
+
121
+ it "Creates PID File" do
122
+ expect(File.exists? File.join(server_configuration_directory, 'server.lock')).to be_true
123
+ end
124
+
125
+ it "Creates SOCK File" do
126
+ expect(File.exists? File.join(server_configuration_directory, 'server.sock')).to be_true
127
+ end
128
+ end
129
+
130
+ describe "'stop' command" do
131
+ let (:server_stop_results) { execute "#{multi_movingsign_exe} server stop --rc #{temp_settings_file} --serverrc #{server_configuration_directory}" }
132
+
133
+ before(:each) do
134
+ double_movingsign_sign
135
+ server_thread
136
+
137
+ timeout(60) { server_stop_results }
138
+ end
139
+
140
+ after(:each) do
141
+ server_thread.kill
142
+ server_thread.join
143
+ end
144
+
145
+ it "Shuts Down Server" do
146
+ sleep 3
147
+
148
+ expect(server_thread).to_not be_alive
149
+ expect(server_stop_results).to be_success
150
+ end
151
+ end
152
+
153
+ pending "'add-page' command"
154
+ pending "'delete-page' command"
155
+ pending "'alert' command"
156
+ end
157
+
158
+ #it "show-page command"
159
+ #it "show-page succeeds"
160
+ #it "show-page sends show_text on each display"
161
+ #it "server"
162
+ #multi_movingsign server add-page --name=NAME --page=PAGE # Adds a page to the server rotation
163
+ #multi_movingsign server alert --page=PAGE # Sends a page to display as an alert
164
+ #multi_movingsign server delete-page --name=NAME # Deletes a page to the server rotation
165
+ #multi_movingsign server help [COMMAND] # Describe subcommands or one specific subcommand
166
+ end
@@ -0,0 +1,47 @@
1
+ #
2
+ # Loaded by noop_movingsign_sign.yml
3
+ #
4
+ # This script makes all MovingsignApi::Sign methods a noop, allowing you to run/test/validate the CLI
5
+ # without having physical LED signs plugged in.
6
+ #
7
+ require 'movingsign_api/sign'
8
+
9
+ klass = Class.new do
10
+ def double_movingsign_sign
11
+ MovingsignApi::Sign.class_exec do
12
+ @@mutex = Mutex.new
13
+
14
+ class << self
15
+ attr_accessor :calls
16
+ end
17
+
18
+ (MovingsignApi::Sign.public_instance_methods.to_a - Object.public_instance_methods.to_a - [:device_path]).each do |method|
19
+ define_method(method) do |*args|
20
+ @@mutex.synchronize do
21
+ # sanitize arguments (since they contain invalid characters)
22
+ sanitized_args = []
23
+ args.each do |arg|
24
+ if arg.kind_of? String
25
+ sanitized_args << arg.encode("ASCII", :invalid => :replace, :undef => :replace, :replace => "?")
26
+ end
27
+ end
28
+
29
+ self.calls << [self.device_path, method, sanitized_args]
30
+
31
+ File.open('testrc.yml', 'w') do |f|
32
+ f.write({'calls' => self.calls}.to_yaml)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def calls
39
+ self.class.calls
40
+ end
41
+ end
42
+
43
+ MovingsignApi::Sign.calls = []
44
+ end
45
+ end
46
+
47
+ klass.new.double_movingsign_sign
@@ -0,0 +1,7 @@
1
+ # Configuration file for testing
2
+ #
3
+ # Sets up MovingsignApi::Sign to be no-ops...enabling testings + validation without LED Signs being attached
4
+
5
+ ---
6
+ script: noop_movingsign_sign.rb
7
+ parameters:
@@ -0,0 +1,16 @@
1
+ lines: 13
2
+ signs:
3
+ - content: |-
4
+ Top Drinkers
5
+ 1. mikey
6
+ 1. 445 pints
7
+ 2. capn
8
+ 2. 183 pints
9
+ 3. eric
10
+ 3. 122 pints
11
+ 4. whitey
12
+ 4. 114 pints
13
+ 5. justin
14
+ 5. 110 pints
15
+ 6. scarfjerk
16
+ 6. 109 pints
@@ -0,0 +1,28 @@
1
+ lines: 12
2
+ signs:
3
+ - content: |-
4
+ Top Drinkers
5
+ Top Drinkers
6
+ Top Drinkers
7
+ Top Drinkers
8
+ Top Drinkers
9
+ Top Drinkers
10
+ Top Drinkers
11
+ Top Drinkers
12
+ Top Drinkers
13
+ Top Drinkers
14
+ Top Drinkers
15
+ Top Drinkers
16
+ - content: |-
17
+ 1. mikey
18
+ 1. 445 pints
19
+ 2. capn
20
+ 2. 183 pints
21
+ 3. eric
22
+ 3. 122 pints
23
+ 4. whitey
24
+ 4. 114 pints
25
+ 5. justin
26
+ 5. 110 pints
27
+ 6. scarfjerk
28
+ 6. 109 pints