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,10 @@
1
+ module MultiMovingsign
2
+ class Error < StandardError
3
+
4
+ end
5
+
6
+ # Raised with invalid input
7
+ class InvalidInputError < Error
8
+
9
+ end
10
+ end
@@ -0,0 +1,223 @@
1
+ module MultiMovingsign
2
+ # Renders a page definition (Hash/YAML) into something that is easily displayable by the MovingsignApi (page solution)
3
+ class PageRenderer
4
+ DEFAULT_CHARACTER_WIDTH = 5
5
+
6
+ # @param page [Hash] page definition as a Hash
7
+ # @param options [Hash] options for the rendering operation
8
+ # @option options [Integer] :count the number of signs to render to (default: 1)
9
+ #
10
+ # @return [Hash] a page solution hash
11
+ def render(page, options = {})
12
+ # Vocabulary - Terms used here
13
+ #
14
+ # Sign
15
+ # A single LED sign, stacked vertically with other LED signs...together forming a screen
16
+ # Screen
17
+ # N LED signs stacked vertically. Together they can display a screen of information at a time
18
+ # Page Definition
19
+ # A page of information to be broken up and displayed on available signs, consisting of a title and n Line Definitions.
20
+ # Line Definition
21
+ # A line of information from the page definition. NOTE: a single line might turn into multiple screens of information
22
+ # Line Segment
23
+ # A piece of a line definition, displayed on it's own sign, seprate from previous line segments of the same line.
24
+ #
25
+
26
+ signs_available = (options[:count] || 1)
27
+ page_title = page['title']
28
+ line_definitions = page['lines']
29
+ pin_title = signs_available > 1
30
+
31
+ page_definition = PageDefinition.from_hash page
32
+ page_segments = page_definition.calculate_segments(signs_available)
33
+ screens = page_segments.map { |s| s.calculate_screens(signs_available, screen_width) }.flatten
34
+
35
+ ## Preview Solution
36
+ #screens.each do |screen|
37
+ # puts "----"
38
+ # (0..(signs_available-1)).each do |i|
39
+ # puts screen.line(i)
40
+ # end
41
+ #end
42
+ #puts "----"
43
+
44
+ signs = (0..(signs_available-1)).map { |sign_index| {'content' => screens.map { |s| s.line(sign_index) }.join("\n")} }
45
+
46
+ {'signs' => signs, 'lines' => screens.length}
47
+ end
48
+
49
+ private
50
+
51
+ def screen_width
52
+ 80
53
+ end
54
+
55
+ def self.calculate_width(string)
56
+ string.length * DEFAULT_CHARACTER_WIDTH
57
+ end
58
+ end
59
+
60
+ class PageDefinition
61
+ attr_accessor :title
62
+ attr_accessor :line_definitions
63
+
64
+ def self.from_hash(hash)
65
+ obj = self.new
66
+
67
+ obj.title = hash['title'] || ''
68
+ obj.line_definitions = hash['lines'].map { |ld| LineDefinition.from_hash ld }
69
+
70
+ obj
71
+ end
72
+
73
+ # Splits a {PageDefinition} into an array of {PageSegment}s
74
+ #
75
+ # @param signs [Integer] the number of signs (lines) available to render to
76
+ # @param options [Hash]
77
+ # @option options [Boolean] +:pin_title+
78
+ def calculate_segments(signs, options = {})
79
+ pin_title = signs > 1 && (options[:pin_title] != false)
80
+ page_segments = []
81
+ line_definitions = self.line_definitions.clone.reverse
82
+
83
+ index = 0
84
+ while !line_definitions.empty?
85
+ include_title = pin_title || index == 0 # include the title in this line segment?
86
+ line_count = include_title ? signs - 1 : signs # number of line definitions to include in this page segment (less the title if included)
87
+
88
+ page_segments << PageSegment.new(include_title ? self.title : nil, line_definitions.pop(line_count).reverse)
89
+
90
+ index += 1
91
+ end
92
+
93
+ page_segments
94
+ end
95
+ end
96
+
97
+ class LineDefinition
98
+ attr_accessor :prefix
99
+ attr_accessor :line_segments
100
+
101
+ def self.from_hash(hash)
102
+ obj = self.new
103
+
104
+ obj.prefix = hash['prefix'] || nil
105
+ obj.line_segments = (hash['segments'] || hash['content'] || []).map { |segment| LineSegment.new(obj.prefix, segment) }
106
+
107
+ obj
108
+ end
109
+
110
+ def prefix?
111
+ !!self.prefix
112
+ end
113
+ end
114
+
115
+ class LineSegment
116
+ attr_accessor :prefix
117
+ attr_accessor :segment
118
+
119
+ def initialize(prefix, segment)
120
+ self.prefix = prefix
121
+ self.segment = segment
122
+ end
123
+
124
+ def prefix?
125
+ !!self.prefix
126
+ end
127
+
128
+ # If necessary, splits this LineSegment into multiple appropriate for displaying at once on the screen
129
+ def split_if_necessary(max_width)
130
+ raise InvalidInputError, "Prefix '' is too wide!" if (prefix? && PageRenderer.calculate_width(prefix) > max_width)
131
+
132
+ if PageRenderer.calculate_width(self.to_s) <= max_width
133
+ # segment isn't too long with prefix, return it as is
134
+ [self]
135
+ else
136
+ # segment is too long, split it up into word segments finding the largest with the prefix appended that fits
137
+ segments = [] # calculated segments
138
+ prefix_width = prefix? ? PageRenderer.calculate_width(prefix) : 0
139
+
140
+ words = segment.split(/ /)
141
+ while !words.empty?
142
+ index = words.length
143
+ while index > 0 && PageRenderer.calculate_width(candidate = (candidate_words = words[0, index]).join(' ')) + prefix_width > max_width
144
+ index -= 1
145
+ end
146
+
147
+ segments << self.class.new(prefix, candidate)
148
+ words = words.drop index
149
+ end
150
+
151
+ segments
152
+ end
153
+ end
154
+
155
+ def to_s
156
+ prefix? ? prefix + segment : segment
157
+ end
158
+
159
+ def inspect
160
+ to_s.inspect
161
+ end
162
+ end
163
+
164
+ class PageSegment
165
+ attr_accessor :title
166
+ attr_accessor :line_definitions
167
+
168
+ def initialize(title, line_definitions)
169
+ self.title = title
170
+ self.line_definitions = line_definitions
171
+ end
172
+
173
+ def title?
174
+ !!self.title
175
+ end
176
+
177
+ def line_definitions?
178
+ !self.line_definitions.empty?
179
+ end
180
+
181
+ # Turns a single page segment into n rendered screens of information
182
+ def calculate_screens(number_of_signs, sign_width)
183
+ raise "Title too long!" if title? && PageRenderer.calculate_width(title) > sign_width
184
+
185
+ if title? && !line_definitions?
186
+ return Screen.new [title]
187
+ end
188
+
189
+ screens = []
190
+
191
+ #puts line_definitions.map { |d| d.line_segments.map { |s| s.split_if_necessary(sign_width).map { |s| s.to_s} } }.inspect
192
+
193
+ num_of_line_segments = line_definitions.map { |d| d.line_segments.length }.max
194
+ (0..(num_of_line_segments - 1)).each do |segment_index|
195
+ subsegments = line_definitions.map { |d| (s = d.line_segments[segment_index]) ? s.split_if_necessary(sign_width) : [] }
196
+ num_of_subsegments = subsegments.map { |s| s.length }.max
197
+
198
+ (0..(num_of_subsegments-1)).each do |subsegment_index|
199
+ lines = []
200
+
201
+ lines << title if title?
202
+ lines.concat subsegments.map { |s| s[subsegment_index % s.length] || "" }
203
+
204
+ screens << Screen.new(lines)
205
+ end
206
+ end
207
+
208
+ screens
209
+ end
210
+ end
211
+
212
+ class Screen
213
+ attr_accessor :lines
214
+
215
+ def initialize(lines)
216
+ self.lines = lines
217
+ end
218
+
219
+ def line(index)
220
+ self.lines[index] || " "
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,317 @@
1
+ require 'thor'
2
+ require 'thread'
3
+ require 'socket'
4
+ require 'multi_movingsign/signs'
5
+ require 'multi_movingsign/testrc_loader'
6
+
7
+ module MultiMovingsign
8
+ # http://stackoverflow.com/a/9439298
9
+ class TeeIO < IO
10
+ attr_accessor :destinations
11
+
12
+ def initialize(*dests)
13
+ self.destinations = dests
14
+ end
15
+
16
+ def puts(val)
17
+ time = Time.now
18
+ destinations.each do |d|
19
+ d.puts "#{time}: #{val.to_s}"
20
+ d.flush
21
+ end
22
+ end
23
+
24
+ def write(val)
25
+ destinations.each do |d|
26
+ d.write val
27
+ d.flush
28
+ end
29
+ end
30
+ end
31
+
32
+ # MultiMovingsign server command line interface
33
+ class Server < Thor
34
+ class_option :serverrc, :desc => 'Path to server persistent storage. Defaults to ~/.multi_movingsign/server'
35
+ desc 'start', 'Starts the MutliMovingsign server'
36
+ def start
37
+ TestRCLoader.load(options['testrc']) if options['testrc']
38
+
39
+ # This impl is a hacky mess... FYI!
40
+ FileUtils.mkdir_p server_settings_path
41
+
42
+ lock_path = File.join(server_settings_path, "server.lock")
43
+ File.open(lock_path, 'w') do |lock|
44
+ raise "Cannot acquire lock! Is a server already running?" unless lock.flock(File::LOCK_EX | File::LOCK_NB)
45
+
46
+ lock.puts $$
47
+ lock.flush
48
+
49
+ mutex = Mutex.new
50
+
51
+ # setup logging
52
+ log_path = File.join(server_settings_path, "server.log")
53
+ log = File.new(log_path, "a")
54
+ $stdout = TeeIO.new($stdout, log)
55
+ $stderr = TeeIO.new($stderr, log)
56
+
57
+ signs = []
58
+
59
+ page_keys = []
60
+ page_solutions = {}
61
+ page_index = 0
62
+ alert = nil
63
+ stop = nil
64
+
65
+ Thread.new do
66
+ begin
67
+ Socket.unix_server_loop(server_socket_path) do |socket, address|
68
+ puts "SOCKET LOOP!"
69
+
70
+ begin
71
+ msg = nil
72
+
73
+ begin
74
+ msg, = socket.recvmsg_nonblock
75
+ rescue IO::WaitReadable
76
+ if IO.select([socket], [], [], 5)
77
+ retry
78
+ else
79
+ raise TimeoutError, "Timeout in recvmsg_nonblock"
80
+ end
81
+ end
82
+
83
+ unless msg
84
+ $stderr.puts "Bogus unix_server_loop?"
85
+ next
86
+ end
87
+
88
+ lines = msg.lines.map { |l| l.rstrip }
89
+ puts "Got UNIX message: #{lines.inspect}"
90
+
91
+ version = lines.delete_at 0
92
+
93
+ case command = lines.delete_at(0)
94
+ when 'add page'
95
+ name = lines.delete_at(0)
96
+ yaml = lines.join "\n"
97
+
98
+ solution = PageRenderer.new.render YAML.load(yaml), :count => signs.length
99
+ page_path = File.join(server_pages_path, "#{name}.yml")
100
+ File.open(page_path, "w") { |f| f.puts yaml }
101
+
102
+ mutex.synchronize do
103
+ page_solutions[name] = solution
104
+ page_keys << name unless page_keys.include? name
105
+
106
+ puts "Added #{name}!"
107
+
108
+ page_keys.delete 'nada'
109
+ page_solutions.delete 'nada'
110
+ end
111
+
112
+ socket.puts "okay"
113
+ when 'delete page'
114
+ name = lines.delete_at(0)
115
+
116
+ mutex.synchronize do
117
+ page_path = File.join(server_pages_path, "#{name}.yml")
118
+
119
+ FileUtils.rm(page_path, :force => true) if File.exists? page_path
120
+
121
+ page_keys.delete name
122
+ page_solutions.delete name
123
+ end
124
+
125
+ puts "Deleted #{name}"
126
+
127
+ socket.puts "okay"
128
+ when 'alert'
129
+ page_yaml = lines.join("\n")
130
+
131
+ mutex.synchronize do
132
+ condition_variable = ConditionVariable.new
133
+ alert = {"solution" => PageRenderer.new.render(YAML.load(page_yaml), :count => signs.length), 'condition_variable' => condition_variable}
134
+
135
+ puts "Signaling alert..."
136
+ condition_variable.wait mutex
137
+ end
138
+
139
+ socket.puts "okay"
140
+
141
+ when 'stop'
142
+ mutex.synchronize do
143
+ cv = ConditionVariable.new
144
+
145
+ stop = {'condition_variable' => cv}
146
+
147
+ cv.wait mutex
148
+ end
149
+
150
+ socket.puts "okay"
151
+ else
152
+ $stderr.puts "Unknown command '#{command}'"
153
+ end
154
+ rescue => e
155
+ $stderr.puts "Exception in unix server loop"
156
+ $stderr.puts e.message
157
+ $stderr.puts e.backtrace.join "\n"
158
+ ensure
159
+ socket.close
160
+ puts "SOCKET CLOSED"
161
+ end
162
+ end
163
+ rescue => e
164
+ $stderr.puts "UNIX socket loop raised!"
165
+ $stderr.puts e.message
166
+ $stderr.puts e.backtrace.join '\n'
167
+
168
+ Thread::current.pi
169
+ end
170
+ end
171
+
172
+ # Loop to allow reloaded
173
+ loop do
174
+ if stop
175
+ puts "Outter loop stopping..."
176
+ break
177
+ end
178
+
179
+ puts "Starting/Reloading!"
180
+
181
+ # load sign configuration
182
+ settings = Settings.load settings_path
183
+ raise_no_signs unless settings.signs?
184
+
185
+ mutex.synchronize do
186
+ page_keys = []
187
+ page_solutions = {}
188
+ signs = Signs.new settings.signs
189
+
190
+ # Load pages and solutions
191
+ FileUtils.mkdir_p server_pages_path
192
+ Dir.glob(File.join(server_pages_path, '*.yml')).sort.each do |path|
193
+ puts "Loading #{path}"
194
+
195
+ key = File.basename(path, File.extname(path))
196
+
197
+ page_keys << key
198
+ page_solutions[key] = PageRenderer.new.render YAML.load(File.read(path)), :count => signs.length
199
+ end
200
+
201
+ if page_keys.empty?
202
+ page_keys << 'nada'
203
+ page_solutions['nada'] = PageRenderer.new.render({'lines' => [{'prefix' => '', 'content' => ['No Pages']}, {'prefix' => '', 'content' => ['Configured']}]}, :count => signs.length)
204
+ end
205
+ end
206
+
207
+ # Loop through pages
208
+ loop do
209
+ if stop
210
+ puts "Inner loop stopping..."
211
+ break
212
+ end
213
+
214
+ page_key = nil
215
+ page_solution = nil
216
+
217
+ mutex.synchronize do
218
+ page_index = 0 if page_index >= page_keys.length || page_index < 0
219
+
220
+ # check for alert
221
+ if alert
222
+ page_key = 'ALERT'
223
+ page_solution = alert['solution']
224
+
225
+ page_index -= 1
226
+
227
+ # extract condition_variable
228
+ condition_variable = alert['condition_variable']
229
+
230
+ # clear alert
231
+ alert = nil
232
+
233
+ # signal that we got it!
234
+ condition_variable.signal
235
+ else
236
+ page_key = page_keys[page_index]
237
+ page_solution = page_solutions[page_key]
238
+ end
239
+ end
240
+
241
+ puts "Sending page #{page_key}"
242
+ signs.show_page_solution page_solution
243
+
244
+
245
+ sleep_amount = page_solution['lines'] && page_solution['lines'] * 3 * 2 || 20
246
+ sleep_amount = 20 if sleep_amount < 2
247
+
248
+ sleep sleep_amount
249
+
250
+ page_index += 1
251
+ end
252
+ end
253
+
254
+ if cv = stop && stop['condition_variable']
255
+ cv.signal
256
+ sleep 1 # wait a bit for the CV recipient to finish before we do.
257
+ end
258
+ end
259
+ end
260
+
261
+ desc 'add-page', 'Adds a page to the server rotation'
262
+ option :page, :required => true, :desc => "Path to page YAML"
263
+ option :name, :required => true, :desc => "Name for the new file"
264
+ def add_page
265
+ exit send_socket_command_expect_ok ['v1', 'add page', options[:name], File.read(options[:page])]
266
+ end
267
+
268
+ desc 'delete-page', 'Deletes a page to the server rotation'
269
+ option :name, :required => true, :desc => "Name for the new file"
270
+ def delete_page
271
+ exit send_socket_command_expect_ok ['v1', 'delete page', options[:name]]
272
+ end
273
+
274
+ desc 'alert', 'Sends a page to display as an alert'
275
+ option :page, :required => true, :desc => "Path to page YAML"
276
+ def alert
277
+ exit send_socket_command_expect_ok ['v1', 'alert', File.read(options[:page])]
278
+ end
279
+
280
+ desc 'stop', 'Stops the running server'
281
+ def stop
282
+ exit send_socket_command_expect_ok ['v1', 'stop']
283
+ end
284
+
285
+ private
286
+
287
+ def send_socket_command_expect_ok(args)
288
+ UNIXSocket.open server_socket_path do |socket|
289
+ send_socket_command(socket, args)
290
+ puts "Sent message...awaiting reply..."
291
+
292
+ (got = socket.gets) && got.strip == "okay" || false
293
+ end
294
+ end
295
+
296
+ def send_socket_command(socket, args)
297
+ socket.sendmsg args.join "\n"
298
+ socket.flush
299
+ end
300
+
301
+ def server_socket_path
302
+ File.join(server_settings_path, 'server.sock')
303
+ end
304
+
305
+ def server_settings_path
306
+ options[:serverrc] || File.join(ENV['HOME'], '.multi_movingsign', 'server')
307
+ end
308
+
309
+ def server_pages_path
310
+ File.join(server_settings_path, 'pages')
311
+ end
312
+
313
+ def settings_path
314
+ options[:rc] || Settings.default_settings_path
315
+ end
316
+ end
317
+ end