sparkle_motion 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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +22 -0
  4. data/README.md +47 -0
  5. data/bin/sm-discover +18 -0
  6. data/bin/sm-mark-lights +58 -0
  7. data/bin/sm-off +38 -0
  8. data/bin/sm-on +50 -0
  9. data/bin/sm-simulate +595 -0
  10. data/bin/sm-watch-memory +9 -0
  11. data/bin/sparkle-motion +48 -0
  12. data/config.yml +201 -0
  13. data/data/dynamic/bridges.csv +5 -0
  14. data/data/static/devices.csv +7 -0
  15. data/lib/sparkle_motion.rb +72 -0
  16. data/lib/sparkle_motion/config.rb +45 -0
  17. data/lib/sparkle_motion/env.rb +14 -0
  18. data/lib/sparkle_motion/http.rb +26 -0
  19. data/lib/sparkle_motion/hue/ssdp.rb +39 -0
  20. data/lib/sparkle_motion/launch_pad/color.rb +41 -0
  21. data/lib/sparkle_motion/launch_pad/widget.rb +187 -0
  22. data/lib/sparkle_motion/launch_pad/widgets/button.rb +29 -0
  23. data/lib/sparkle_motion/launch_pad/widgets/horizontal_slider.rb +43 -0
  24. data/lib/sparkle_motion/launch_pad/widgets/radio_group.rb +53 -0
  25. data/lib/sparkle_motion/launch_pad/widgets/toggle.rb +48 -0
  26. data/lib/sparkle_motion/launch_pad/widgets/vertical_slider.rb +43 -0
  27. data/lib/sparkle_motion/lazy_request_config.rb +87 -0
  28. data/lib/sparkle_motion/node.rb +80 -0
  29. data/lib/sparkle_motion/nodes/generator.rb +8 -0
  30. data/lib/sparkle_motion/nodes/generators/const.rb +15 -0
  31. data/lib/sparkle_motion/nodes/generators/perlin.rb +26 -0
  32. data/lib/sparkle_motion/nodes/generators/wave2.rb +20 -0
  33. data/lib/sparkle_motion/nodes/transform.rb +34 -0
  34. data/lib/sparkle_motion/nodes/transforms/contrast.rb +20 -0
  35. data/lib/sparkle_motion/nodes/transforms/range.rb +41 -0
  36. data/lib/sparkle_motion/nodes/transforms/spotlight.rb +35 -0
  37. data/lib/sparkle_motion/output.rb +69 -0
  38. data/lib/sparkle_motion/results.rb +78 -0
  39. data/lib/sparkle_motion/utility.rb +68 -0
  40. data/lib/sparkle_motion/vector2.rb +11 -0
  41. data/lib/sparkle_motion/version.rb +3 -0
  42. data/sparkle_motion.gemspec +44 -0
  43. metadata +178 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bbef0303576ccb86e06cecb1cf2be03219822d74
4
+ data.tar.gz: 6b94c6225bff6d537328dbbe1381c2d8014bc048
5
+ SHA512:
6
+ metadata.gz: cc7c4cce004cb6b4f4a4b116f345e13123fa42322ad3e21e3ea813cde541995d1fbb58906449e04237056d750f07ac92655fd477a4e66d18e7e9aaadde7b024e
7
+ data.tar.gz: b561b42d6323c11c4fc7a878fe2a05e30ba51e32483b3977f1d85e02647f25d41555291453059cd997fda48b472b5206baadb12786a2d2335cf37b480057dd6e
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changes
2
+
3
+ ## Next
4
+
5
+ ## 0.1.0 - 2015-09-12
6
+
7
+ * Initial release. Totally undocumented, annoyingly hard-coded, etc.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jon Frisby, http://MrJoy.com
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,47 @@
1
+ # Sparkle Motion
2
+
3
+ Generative event lighting system using Philips Hue and Novation Launchpad.
4
+
5
+
6
+ ## Installation
7
+
8
+ After cloning this repo, run:
9
+
10
+ ```bash
11
+ brew install portmidi
12
+ gem install sparkle_motion
13
+ sm-discover # Find all available bridges using SSDP.
14
+ # Create `config.yml`, and register the username(s) in it with the relevant bridges.
15
+ # You probably want to start with the one in this project's source repo as a baseline.
16
+ sm-mark-lights # Ensure your lights are physically arranged properly.
17
+ sm-on # Switch all the lights on, and set color to expected base state.
18
+ sparkle-motion # Run the simulation.
19
+ ```
20
+
21
+ __TODO: Document how to register user with hub(s).__
22
+
23
+
24
+ ## Usage
25
+
26
+ * `bin/sm-discover`: Discover all Philips Hue bridges on your network.
27
+ * `bin/sm-mark-lights`: Mark the lights distinctively to help ensure they're physically arranged properly.
28
+ * `bin/sm-off`: Turn all configured lights off.
29
+ * `bin/sm-on`: Turn all configured lights on, and set them to the base color.
30
+ * `bin/sm-simulate`: Run the effect system directly. You probably want `sparkle-motion` instead.
31
+ * `bin/sparkle-motion`: Runs the effect system with configuration settings for debugging, and restarts it if the kick-in-the-head button is pressed. See source for details.
32
+
33
+ ## Using the Code
34
+
35
+ * `examples/tictactoe.rb`: A simple example of the Novation Launchpad widgets, and how to use/extend them.
36
+ * `tools/chunker.rb`: Helper for churning through `*.raw` files and preparing them for visualization.
37
+ * `tools/color_scale.rb`: A small playground for defining color schemes for Novation LaunchPad widgets.
38
+
39
+
40
+ ## Debugging
41
+
42
+ * `bin/sm-watch-memory`: External monitor to keep an eye on the process size of `sm-simulate`. Useful for debugging memory allocations and GC pressure.
43
+
44
+
45
+ ## Configuration
46
+
47
+ __ TODO: Write me.__
data/bin/sm-discover ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ###############################################################################
4
+ # Early Initialization/Helpers
5
+ ###############################################################################
6
+ lib = File.expand_path("../../lib", __FILE__)
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+ require "sparkle_motion"
9
+ SparkleMotion.init!("discover")
10
+ SparkleMotion.use_hue!(discovery: true)
11
+
12
+ def ip_atob(ip)
13
+ ip.split(/\./).map(&:to_i).pack("C4")
14
+ end
15
+
16
+ # TODO: Show MAC address so we can map the IPs to bridges sanely!
17
+ results = SparkleMotion::Hue::SSDP.new.scan
18
+ puts results.values.sort { |a, b| ip_atob(a) <=> ip_atob(b) }.join("\n")
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ ###############################################################################
3
+ # Early Initialization/Helpers
4
+ ###############################################################################
5
+ lib = File.expand_path("../../lib", __FILE__)
6
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
+ require "sparkle_motion"
8
+ SparkleMotion.init!("mark_lights")
9
+ SparkleMotion.use_hue!(api: true)
10
+ LOGGER = SparkleMotion.logger
11
+
12
+ ###############################################################################
13
+ # Main Logic
14
+ ###############################################################################
15
+ # TODO: Use Novation Launchpad to be able to toggle lights.
16
+ def light_state(hue, index, num_lights)
17
+ target = (254 * (index / num_lights.to_f)).round
18
+ data = { "on" => true,
19
+ "hue" => hue,
20
+ "sat" => target,
21
+ "bri" => target }
22
+ with_transition_time(data, 0)
23
+ end
24
+
25
+ # TODO: Speed this up by setting on/hue via group message per bridge...
26
+ %w(main_lights dance_lights accent_lights).each do |group_name|
27
+ config = SparkleMotion::LightConfig.new(config: CONFIG, group: group_name)
28
+ url_req_map = {}
29
+ config.bridges.each do |bridge_name, bridge|
30
+ light_ids = config.lights[bridge_name].map(&:last)
31
+ hue = bridge["debug_hue"]
32
+ index = 0
33
+ num_lights = light_ids.length
34
+ requests = light_ids
35
+ .map do |lid|
36
+ url = hue_light_endpoint(bridge, lid)
37
+ color = light_state(hue, index, num_lights)
38
+ index += 1
39
+ url_req_map[url] = SparkleMotion::LazyRequestConfig.new(LOGGER, bridge, url) do
40
+ color
41
+ end
42
+ end
43
+
44
+ next unless requests.length > 0
45
+ while requests.length > 0
46
+ retry_queue = []
47
+ Curl::Multi.http(requests, MULTI_OPTIONS) do |easy|
48
+ if easy.response_code != 200 ||
49
+ easy.body =~ /error/
50
+ url = easy.url
51
+ LOGGER.error { "#{url} => #{easy.response_code} / #{easy.body}" }
52
+ retry_queue << url
53
+ end
54
+ end
55
+ requests = retry_queue.map { |url| url_req_map[url] }
56
+ end
57
+ end
58
+ end
data/bin/sm-off ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ###############################################################################
4
+ # Early Initialization/Helpers
5
+ ###############################################################################
6
+ lib = File.expand_path("../../lib", __FILE__)
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+ require "sparkle_motion"
9
+ SparkleMotion.init!("off")
10
+ SparkleMotion.use_hue!(api: true)
11
+
12
+ ###############################################################################
13
+ # Helper Functions
14
+ ###############################################################################
15
+ def make_req_struct(url, data)
16
+ { method: :put,
17
+ url: url,
18
+ put_data: Oj.dump(data) }.merge(EASY_OPTIONS)
19
+ end
20
+
21
+ def hue_init(config)
22
+ make_req_struct(hue_group_endpoint(config, 0), "on" => false)
23
+ end
24
+
25
+ ###############################################################################
26
+ # Main
27
+ ###############################################################################
28
+ # TODO: Hoist this into a separate script.
29
+ # debug "Initializing lights..."
30
+ init_reqs = CONFIG["bridges"]
31
+ .values
32
+ .map { |config| hue_init(config) }
33
+ Curl::Multi.http(init_reqs, MULTI_OPTIONS) do |easy|
34
+ if easy.response_code != 200
35
+ SparkleMotion.logger.error { "Failed to initialize light: #{easy.url}" }
36
+ add(easy)
37
+ end
38
+ end
data/bin/sm-on ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ###############################################################################
4
+ # Early Initialization/Helpers
5
+ ###############################################################################
6
+ lib = File.expand_path("../../lib", __FILE__)
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+ require "sparkle_motion"
9
+ SparkleMotion.init!("on")
10
+ SparkleMotion.use_hue!(api: true)
11
+
12
+ ###############################################################################
13
+ # Effect
14
+ #
15
+ # Tweak this to change the visual effect.
16
+ ###############################################################################
17
+ INIT_HUE = env_int("INIT_HUE", true) || 49_500
18
+ INIT_SAT = env_int("INIT_SAT", true) || 254
19
+ INIT_BRI = env_int("INIT_BRI", true) || 127
20
+
21
+ ###############################################################################
22
+ # Helper Functions
23
+ ###############################################################################
24
+ def make_req_struct(url, data)
25
+ { method: :put,
26
+ url: url,
27
+ put_data: Oj.dump(data) }.merge(EASY_OPTIONS)
28
+ end
29
+
30
+ def hue_init(config)
31
+ make_req_struct(hue_group_endpoint(config, 0), "on" => true,
32
+ "bri" => INIT_BRI,
33
+ "sat" => INIT_SAT,
34
+ "hue" => INIT_HUE)
35
+ end
36
+
37
+ ###############################################################################
38
+ # Main
39
+ ###############################################################################
40
+ # TODO: Hoist this into a separate script.
41
+ # debug "Initializing lights..."
42
+ init_reqs = CONFIG["bridges"]
43
+ .values
44
+ .map { |config| hue_init(config) }
45
+ Curl::Multi.http(init_reqs, MULTI_OPTIONS) do |easy|
46
+ if easy.response_code != 200
47
+ SparkleMotion.logger.error { "Failed to initialize light: #{easy.url}" }
48
+ add(easy)
49
+ end
50
+ end
data/bin/sm-simulate ADDED
@@ -0,0 +1,595 @@
1
+ #!/usr/bin/env ruby
2
+ # def bench_init!; @first_time = @last_time = Time.now.to_f; end
3
+
4
+ # def bench_snap!(depth = 0)
5
+ # t = Time.now.to_f
6
+ # elapsed = t - @last_time
7
+ # @last_time = t
8
+ # key = caller[depth].split(":")[0..1].join(":").split("/").last
9
+ # key = "TOTAL" if depth > 0
10
+ # puts "%s => %f sec (@%f)" % [key, elapsed, t]
11
+ # end
12
+
13
+ # def bench_end!
14
+ # @last_time = @first_time
15
+ # bench_snap!(2)
16
+ # end
17
+
18
+ # TODO: Run update across nodes from back to front for simulation rather than
19
+ # TODO: relying on a call-chain. This should make it easy to eliminate the
20
+ # TODO: `yield` usage and avoid associated allocations.
21
+
22
+ # TODO: Tool to read journaled debug data and produce a PNG.
23
+
24
+ # TODO: Deeper memory profiling to ensure this process can run for hours.
25
+
26
+ # TODO: Pick four downlights for the dance floor, and treat them as a separate
27
+ # TODO: simulation. Consider how spotlighting and the like will be relevant to
28
+ # TODO: them.
29
+
30
+ # TODO: Node to *clamp* brightness range so we can set the absolute limits at
31
+ # TODO: the end of the chain? Need to consider use-cases more thoroughly.
32
+ # TODO: May be useful for photographer!
33
+
34
+ # TODO: Rename widgets to clarify that they're LaunchPad-specific, and hoist all
35
+ # TODO: LaunchPad code into one namespace.
36
+
37
+ # TODO: Hoist all Hue code into one namespace.
38
+
39
+ # TODO: Possibly break out discovery / user registration into a separate gem?
40
+
41
+ # f = Fiber.new do
42
+ # meth(1) do
43
+ # Fiber.yield
44
+ # end
45
+ # end
46
+ # meth(2) do
47
+ # f.resume
48
+ # end
49
+ # f.resume
50
+ # p Thread.current[:name]
51
+
52
+ ###############################################################################
53
+ # Early Initialization/Helpers
54
+ ###############################################################################
55
+ bench_init! if defined?(bench_init!)
56
+ lib = File.expand_path("../../lib", __FILE__)
57
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
58
+ require "sparkle_motion"
59
+
60
+ SparkleMotion.init!("simulate")
61
+ # We load the following unconditionally because so much plugs into the graph
62
+ # we don't currently have a good way of decoupling things gracefully. So all
63
+ # USE_GRAPH=0 really means is that we don't run the simulation thread.
64
+ SparkleMotion.use_graph!
65
+
66
+ # Code loading / modular behavior configuration:
67
+ USE_LIGHTS = env_bool("USE_LIGHTS")
68
+ USE_INPUT = env_bool("USE_INPUT")
69
+ USE_SWEEP = env_bool("USE_SWEEP")
70
+ USE_GRAPH = env_bool("USE_GRAPH")
71
+ SparkleMotion.use_hue!(api: true) if USE_LIGHTS
72
+ SparkleMotion.use_widgets!
73
+ SparkleMotion.use_launchpad! if USE_INPUT
74
+
75
+ # Crufty common code:
76
+ require "sparkle_motion/output"
77
+
78
+ ###############################################################################
79
+ # Profiling and Debugging
80
+ ###############################################################################
81
+ LOGGER = SparkleMotion.logger
82
+ profile_run = ENV["PROFILE_RUN"]
83
+ PROFILE_RUN = (profile_run != "") ? profile_run : nil
84
+ SKIP_GC = env_bool("SKIP_GC")
85
+ DEBUG_FLAGS = Hash[(ENV["DEBUG_NODES"] || "")
86
+ .split(/\s*,\s*/)
87
+ .map(&:upcase)
88
+ .map { |nn| [nn, true] }]
89
+
90
+ ###############################################################################
91
+ # Shared State Setup
92
+ ###############################################################################
93
+ # TODO: Run all simulations, and use a mixer to blend between them...
94
+ num_lights = CONFIG["main_lights"].length
95
+ LIGHTS_FOR_THREADS = SparkleMotion::LightConfig.new(config: CONFIG, group: "main_lights")
96
+ INTERACTION = USE_INPUT ? Launchpad::Interaction.new(use_threads: false) : nil
97
+ INT_STATES = []
98
+ NODES = {}
99
+ PENDING_COMMANDS = Queue.new
100
+ CURRENT_STATE = {}
101
+ STATE_FILENAME = "/tmp/sparkle-motion.state"
102
+ SKIP_STATE_PERSISTENCE = [false]
103
+ HAVE_STATE_FILE = File.exist?(STATE_FILENAME)
104
+ if HAVE_STATE_FILE
105
+ age = Time.now.to_f - File.stat(STATE_FILENAME).mtime.to_f
106
+ if age > 3600
107
+ LOGGER.warn do
108
+ "#{STATE_FILENAME} is #{age} seconds old!"\
109
+ " This is probably not what you want, but you're the boss..."
110
+ end
111
+ end
112
+ CURRENT_STATE.merge!(YAML.load(File.read(STATE_FILENAME)))
113
+ end
114
+
115
+ def update_state!(key, value)
116
+ old_value = CURRENT_STATE[key]
117
+ return if old_value == value
118
+ CURRENT_STATE[key] = value
119
+ return if SKIP_STATE_PERSISTENCE[0]
120
+ LOGGER.debug { "Persisting control state." }
121
+ # TODO: Maybe keep the file open, and rewind?
122
+ File.open(STATE_FILENAME, "w") do |fh|
123
+ fh.write(CURRENT_STATE.to_yaml)
124
+ end
125
+ end
126
+
127
+ ###############################################################################
128
+ # Simulation Graph Configuration / Setup
129
+ ###############################################################################
130
+ # Root nodes (don't act as modifiers on other nodes' output):
131
+ n_cfg = CONFIG["simulation"]["nodes"]
132
+ p_speed = n_cfg["perlin"]["speed"]
133
+ NODES["PERLIN"] = SparkleMotion::Nodes::Generators::Perlin.new(lights: num_lights, speed: p_speed)
134
+ last = NODES["PERLIN"]
135
+
136
+ # Transform nodes (act as a chain of modifiers):
137
+ c_cfg = n_cfg["contrast"]
138
+ c_fun = c_cfg["function"]
139
+ c_iter = c_cfg["iterations"]
140
+ NODES["STRETCHED"] = last = SparkleMotion::Nodes::Transforms::Contrast.new(function: c_fun,
141
+ iterations: c_iter,
142
+ source: last)
143
+ # Create one control group here per "quadrant"...
144
+ intensity_cfg = CONFIG["simulation"]["controls"]["intensity"]
145
+ LIGHTS_FOR_THREADS.bridges.each_with_index do |(bridge_name, _bridge_config), idx|
146
+ mask = LIGHTS_FOR_THREADS.masks[bridge_name]
147
+ int_vals = intensity_cfg["values"]
148
+ last = SparkleMotion::Nodes::Transforms::Range.new(mid_point: int_vals[0][0],
149
+ delta: int_vals[0][1],
150
+ source: last,
151
+ mask: mask,
152
+ logger: LOGGER)
153
+ NODES["SHIFTED_#{idx}"] = last
154
+
155
+ int_colors = intensity_cfg["colors"]
156
+ pos = intensity_cfg["positions"][idx]
157
+ int_widget = Kernel.const_get(intensity_cfg["widget"])
158
+ int_key = "SHIFTED_#{idx}"
159
+ INT_STATES[idx] = int_widget.new(launchpad: INTERACTION,
160
+ x: pos[0],
161
+ y: pos[1],
162
+ size: intensity_cfg["size"],
163
+ on: int_colors["on"],
164
+ off: int_colors["off"],
165
+ down: int_colors["down"],
166
+ on_change: proc do |val|
167
+ ival = int_vals[val]
168
+ LOGGER.info { "Intensity[#{idx},#{val}]: #{ival}" }
169
+ NODES[int_key].set_range(ival[0], ival[1])
170
+ update_state!(int_key, val)
171
+ end)
172
+ end
173
+
174
+ SAT_STATES = []
175
+ sat_cfg = CONFIG["simulation"]["controls"]["saturation"]
176
+ sat_len = sat_cfg["transition"]
177
+ sat_colors = sat_cfg["colors"]
178
+ sat_vals = sat_cfg["values"]
179
+ sat_grps = sat_cfg["groups"]
180
+ sat_widget = Kernel.const_get(sat_cfg["widget"])
181
+ sat_cfg["positions"].each_with_index do |(xx, yy), idx|
182
+ sat_grp_info = sat_grps[idx]
183
+ sat_bridge = CONFIG["bridges"][sat_grp_info[0]]
184
+ sat_group = sat_grp_info[1]
185
+ sat_key = "SAT_STATES[#{idx}]"
186
+ SAT_STATES << sat_widget.new(launchpad: INTERACTION,
187
+ x: xx,
188
+ y: yy,
189
+ size: sat_cfg["size"],
190
+ on: sat_colors["on"],
191
+ off: sat_colors["off"],
192
+ down: sat_colors["down"],
193
+ on_change: proc do |val|
194
+ ival = sat_vals[val]
195
+ LOGGER.info { "Saturation[#{idx},#{val}]: #{ival}" }
196
+ data = with_transition_time({ "sat" => ival }, sat_len)
197
+ req = { method: :put,
198
+ url: hue_group_endpoint(sat_bridge, sat_group),
199
+ put_data: Oj.dump(data) }.merge(EASY_OPTIONS)
200
+ PENDING_COMMANDS << req
201
+ update_state!(sat_key, val)
202
+ end)
203
+ end
204
+
205
+ last = NODES["SPOTLIT"] = SparkleMotion::Nodes::Transforms::Spotlight.new(source: last)
206
+ FINAL_RESULT = last # The end node that will be rendered to the lights.
207
+ sl_cfg = CONFIG["simulation"]["controls"]["spotlighting"]
208
+ sl_colors = sl_cfg["colors"]
209
+ sl_map_raw = sl_cfg["mappings"]
210
+ sl_pos = sl_map_raw.flatten
211
+ sl_key = "SPOTLIT"
212
+ sl_size = [sl_map_raw.map(&:length).sort[-1], sl_map_raw.length]
213
+ SL_STATE = SparkleMotion::LaunchPad::Widgets::RadioGroup.new(launchpad: INTERACTION,
214
+ x: sl_cfg["x"],
215
+ y: sl_cfg["y"],
216
+ size: sl_size,
217
+ on: sl_colors["on"],
218
+ off: sl_colors["off"],
219
+ down: sl_colors["down"],
220
+ on_select: proc do |x|
221
+ LOGGER.info { "Spot ##{sl_pos[x]}" }
222
+ NODES[sl_key].spotlight!(sl_pos[x])
223
+ update_state!(sl_key, x)
224
+ end,
225
+ on_deselect: proc do
226
+ LOGGER.info { "Spot Off" }
227
+ NODES["SPOTLIT"].clear!
228
+ update_state!(sl_key, nil)
229
+ end)
230
+
231
+ NODES.each do |name, node|
232
+ node.debug = DEBUG_FLAGS[name]
233
+ end
234
+
235
+ ###############################################################################
236
+ # Operational Configuration
237
+ ###############################################################################
238
+ ITERATIONS = env_int("ITERATIONS", true) || 0
239
+ TIME_TO_DIE = [false, :terminate]
240
+
241
+ ###############################################################################
242
+ # Profiling Support
243
+ ###############################################################################
244
+ # TODO: Make this optional.
245
+ e_cfg = CONFIG["simulation"]["controls"]["exit"]
246
+ EXIT_BUTTON = SparkleMotion::LaunchPad::Widgets::Button.new(launchpad: INTERACTION,
247
+ position: e_cfg["position"].to_sym,
248
+ color: e_cfg["colors"]["color"],
249
+ down: e_cfg["colors"]["down"],
250
+ on_press: lambda do |value|
251
+ return unless value != 0
252
+ LOGGER.unknown { "Kick!" }
253
+ TIME_TO_DIE[1] = :restart
254
+ TIME_TO_DIE[0] = true
255
+ end)
256
+
257
+ def start_ruby_prof!
258
+ return unless PROFILE_RUN == "ruby-prof"
259
+
260
+ SparkleMotion.logger.unknown { "Enabling ruby-prof, be careful!" }
261
+ require "ruby-prof"
262
+ RubyProf.measure_mode = RubyProf.const_get(ENV.fetch("RUBY_PROF_MODE").upcase)
263
+ RubyProf.start
264
+ end
265
+
266
+ def stop_ruby_prof!
267
+ return unless PROFILE_RUN == "ruby-prof"
268
+
269
+ result = RubyProf.stop
270
+ printer = RubyProf::CallStackPrinter.new(result)
271
+ File.open("tmp/results.html", "w") do |fh|
272
+ printer.print(fh)
273
+ end
274
+ end
275
+
276
+ ###############################################################################
277
+ # Main Simulation
278
+ ###############################################################################
279
+ def clear_board!
280
+ return unless defined?(Launchpad)
281
+
282
+ # TODO: Generalize this to deal with the entire board.
283
+
284
+ # TODO: Hoist setup / teardown of the board into separate binaries and
285
+ # TODO: don't do it from here to avoid startup overhead!
286
+
287
+ INT_STATES.map(&:blank)
288
+ sleep 0.01 # 88 updates/sec input limit!
289
+ SAT_STATES.map(&:blank)
290
+ sleep 0.01 # 88 updates/sec input limit!
291
+ SL_STATE.blank
292
+ sleep 0.01
293
+ EXIT_BUTTON.blank
294
+ end
295
+
296
+ def any_in_state(threads, state)
297
+ threads = Array(threads)
298
+ threads.find { |th| th.status != state }
299
+ end
300
+
301
+ def wait_for(threads, state)
302
+ threads = Array(threads)
303
+ sleep 0.01 while any_in_state(threads, state)
304
+ end
305
+
306
+ def without_persistence(&block)
307
+ SKIP_STATE_PERSISTENCE[0] = defined?(LaunchPad) ? true : false
308
+ block.call
309
+ ensure
310
+ SKIP_STATE_PERSISTENCE[0] = false
311
+ end
312
+
313
+ def setup_intensity_controls!
314
+ INT_STATES.each_with_index do |ctrl, idx|
315
+ ctrl.update(CURRENT_STATE.fetch("SHIFTED_#{idx}", ctrl.max_v / 2))
316
+ end
317
+ end
318
+
319
+ def setup_saturation_controls!
320
+ SAT_STATES.each_with_index do |ctrl, idx|
321
+ ctrl.update(CURRENT_STATE.fetch("SAT_STATES[#{idx}]", ctrl.max_v))
322
+ end
323
+ end
324
+
325
+ def setup_spotlight_controls!
326
+ SL_STATE.update(CURRENT_STATE.fetch("SPOTLIT", nil))
327
+ end
328
+
329
+ def setup_exit_controls!
330
+ EXIT_BUTTON.update(false)
331
+ end
332
+
333
+ def guarded_thread(name, &block)
334
+ Thread.new { guard_call(name, &block) }
335
+ end
336
+
337
+ def launch_input_thread!
338
+ without_persistence do
339
+ setup_intensity_controls!
340
+ setup_saturation_controls!
341
+ setup_spotlight_controls!
342
+ setup_exit_controls!
343
+ end
344
+ # Don't send updates from our attempts at setting things up when we're
345
+ # picking up where we left of...
346
+ PENDING_COMMANDS.clear if HAVE_STATE_FILE
347
+
348
+ return nil unless defined?(Launchpad)
349
+
350
+ guarded_thread("Input Handler") do
351
+ Thread.stop
352
+ INTERACTION.start
353
+ end
354
+ end
355
+
356
+ def launch_graph_thread!
357
+ return nil unless USE_GRAPH
358
+ guarded_thread("Graph Renderer") do
359
+ Thread.stop
360
+
361
+ loop do
362
+ t = Time.now.to_f
363
+ FINAL_RESULT.update(t)
364
+ el = Time.now.to_f - t
365
+ # Try to adhere to a specific update frequency...
366
+ sleep SparkleMotion::Node::FRAME_PERIOD - el if el < SparkleMotion::Node::FRAME_PERIOD
367
+ end
368
+ end
369
+ end
370
+
371
+ def add_group_command!(config, data)
372
+ # TODO: Hoist the hash into something reusable?
373
+ PENDING_COMMANDS << { method: :put,
374
+ url: hue_group_endpoint(config, 0),
375
+ put_data: Oj.dump(data) }.merge(EASY_OPTIONS)
376
+ end
377
+
378
+ def launch_sweep_thread!(sweep_cfg)
379
+ return nil unless USE_LIGHTS && USE_SWEEP
380
+ hues = sweep_cfg["values"]
381
+ sweep_len = sweep_cfg["transition"]
382
+ sweep_wait = sweep_len
383
+ if sweep_len < 0
384
+ sweep_wait = sweep_len.abs
385
+ sweep_len = 0.0
386
+ end
387
+ guarded_thread("Sweeper") do
388
+ Thread.stop
389
+
390
+ loop do
391
+ before_time = Time.now.to_f
392
+ idx = ((before_time / sweep_wait) % hues.length).floor
393
+ data = with_transition_time({ "hue" => hues[idx] }, sweep_len)
394
+ CONFIG["bridges"].each do |(_name, config)|
395
+ add_group_command!(config, data)
396
+ end
397
+
398
+ elapsed = Time.now.to_f - before_time
399
+ sleep sweep_wait - elapsed if elapsed < sweep_wait
400
+ end
401
+ end
402
+ end
403
+
404
+ def launch_dummy_light_threads!
405
+ threads = []
406
+ threads << guarded_thread("Dummy Thread") do
407
+ Thread.stop
408
+ iters = ITERATIONS
409
+ iters = 20 if iters == 0
410
+ sleep iters * 5.0
411
+ TIME_TO_DIE[1] = :terminate
412
+ TIME_TO_DIE[0] = true
413
+ end
414
+ threads
415
+ end
416
+
417
+ def launch_light_threads!(cfg, global_results)
418
+ threads = []
419
+ return launch_dummy_light_threads! unless USE_LIGHTS
420
+
421
+ transition = cfg["transition"]
422
+ debug = DEBUG_FLAGS["OUTPUT"]
423
+ threads += LIGHTS_FOR_THREADS.bridges.map do |(bridge_name, config)|
424
+ guarded_thread(bridge_name) do
425
+ lights = LIGHTS_FOR_THREADS.lights[bridge_name]
426
+ stats = defined?(Results) ? Results.new(logger: LOGGER) : nil
427
+ iterator = (ITERATIONS > 0) ? ITERATIONS.times : loop
428
+
429
+ LOGGER.unknown do
430
+ light_list = lights.map(&:first).join(", ")
431
+ "#{bridge_name}: Thread set to handle #{lights.count} lights (#{light_list})."
432
+ end
433
+
434
+ Thread.stop
435
+
436
+ requests = lights
437
+ .map do |(idx, lid)|
438
+ url = hue_light_endpoint(config, lid)
439
+ SparkleMotion::LazyRequestConfig.new(LOGGER, config, url, stats, debug: debug) do
440
+ # TODO: Recycle this hash?
441
+ data = { "bri" => (FINAL_RESULT[idx] * 254).to_i }
442
+ with_transition_time(data, transition)
443
+ end
444
+ end
445
+
446
+ iterator.each do
447
+ Curl::Multi.http(requests.dup, MULTI_OPTIONS) do
448
+ end
449
+
450
+ global_results.add_from(stats) if global_results
451
+ stats.clear! if stats
452
+ sleep 0.1
453
+ end
454
+ end
455
+ end
456
+
457
+ threads << guarded_thread("Command Queue") do
458
+ Thread.stop
459
+ loop do
460
+ sleep 0.05 while PENDING_COMMANDS.empty?
461
+
462
+ # TODO: Gather stats about success/failure...
463
+ # results = Results.new(logger: LOGGER)
464
+ # global_results.add_from(results)
465
+ # results.clear!
466
+
467
+ requests = []
468
+ requests << PENDING_COMMANDS.pop until PENDING_COMMANDS.empty?
469
+ next if requests.length == 0
470
+ LOGGER.debug { "Processing #{requests.length} pending commands." }
471
+ Curl::Multi.http(requests, MULTI_OPTIONS) do |easy|
472
+ rc = easy.response_code
473
+ body = easy.body
474
+ next if rc >= 200 && rc < 400 && body !~ /error/
475
+ LOGGER.warn { "Problem processing command: #{easy.url} => #{rc}; #{body}" }
476
+ end
477
+ end
478
+ end
479
+ end
480
+
481
+ def launch_all_threads!(sim_cfg, global_results)
482
+ tmp = { input: [launch_input_thread!].compact,
483
+ graph: [launch_graph_thread!].compact,
484
+ sweep: [launch_sweep_thread!(sim_cfg["sweep"])].compact,
485
+ lights: launch_light_threads!(sim_cfg["output"], global_results) }
486
+ tmp[:all] = tmp.values.flatten.compact
487
+ tmp
488
+ end
489
+
490
+ def pre_init!
491
+ trap("INT") do
492
+ TIME_TO_DIE[0] = true
493
+ # If we hit ctrl-c, it'll show up on the terminal, mucking with log output right when we're
494
+ # about to produce reports. This annoys me, so I'm working around it:
495
+ puts
496
+ end
497
+ Thread.abort_on_exception = false
498
+ end
499
+
500
+ def nodes_under_debug
501
+ NODES.select { |name, _node| DEBUG_FLAGS[name] }
502
+ end
503
+
504
+ def debugging?
505
+ nodes_under_debug.length > 0 || DEBUG_FLAGS["OUTPUT"] || PROFILE_RUN
506
+ end
507
+
508
+ def wait_for_threads!(threads)
509
+ LOGGER.unknown { "Waiting for threads to finish initializing..." }
510
+ wait_for(threads, "sleep")
511
+ end
512
+
513
+ def init!(global_results)
514
+ LOGGER.unknown { "Initializing system..." }
515
+ if SKIP_GC
516
+ LOGGER.unknown { "Disabling garbage collection! BE CAREFUL!" }
517
+ GC.disable
518
+ end
519
+ global_results.begin! if global_results
520
+ start_ruby_prof!
521
+ FINAL_RESULT.update(Time.now.to_f)
522
+ end
523
+
524
+ def wake!(threads)
525
+ LOGGER.unknown { "Final setup done, waking threads..." }
526
+ threads.each(&:run)
527
+ end
528
+
529
+ def spin!(threads)
530
+ LOGGER.unknown { "Waiting for the world to end..." }
531
+ loop do
532
+ # Someone hit the exit button:
533
+ break if TIME_TO_DIE[0]
534
+ # We went through and did `ITERATIONS` update loops over the lights:
535
+ # ... the `- 1` is for the command queue thread!
536
+ unfinished = (threads.length - threads.count { |th| th.status == false }) - 1
537
+ break if USE_LIGHTS && unfinished == 0
538
+ sleep 0.25
539
+ end
540
+ end
541
+
542
+ def stop!(threads)
543
+ LOGGER.unknown { "Stopping threads..." }
544
+ %i(lights sweep graph input).each do |thread_group|
545
+ threads[thread_group].each(&:terminate)
546
+ end
547
+ end
548
+
549
+ def main
550
+ pre_init!
551
+
552
+ announce_iteration_config(ITERATIONS)
553
+
554
+ global_results = defined?(Results) ? Results.new(logger: LOGGER) : nil
555
+ threads = launch_all_threads!(CONFIG["simulation"], global_results)
556
+
557
+ wait_for_threads!(threads[:all])
558
+ init!(global_results)
559
+ wake!(threads[:all])
560
+ spin!(threads[:lights])
561
+ stop!(threads)
562
+
563
+ LOGGER.unknown { "Doing final shutdown..." }
564
+ global_results.done! if global_results
565
+ clear_board!
566
+
567
+ print_results(global_results) if global_results
568
+ dump_debug_data!
569
+ exit 127 if TIME_TO_DIE[1] == :restart
570
+ end
571
+
572
+ def profile!(&block)
573
+ unless PROFILE_RUN == "memory_profiler"
574
+ block.call
575
+ return
576
+ end
577
+
578
+ LOGGER.unknown { "Enabling memory_profiler, be careful!" }
579
+ require "memory_profiler"
580
+ report = MemoryProfiler.report do
581
+ block.call
582
+ LOGGER.unknown { "Preparing MemoryProfiler report." }
583
+ end
584
+ LOGGER.unknown { "Dumping MemoryProfiler report." }
585
+ # TODO: Dump this to a file...
586
+ report.pretty_print
587
+ end
588
+
589
+ ###############################################################################
590
+ # Launcher
591
+ ###############################################################################
592
+ profile! do
593
+ bench_end! if defined?(bench_end!)
594
+ main
595
+ end