sparkle_motion 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE +22 -0
- data/README.md +47 -0
- data/bin/sm-discover +18 -0
- data/bin/sm-mark-lights +58 -0
- data/bin/sm-off +38 -0
- data/bin/sm-on +50 -0
- data/bin/sm-simulate +595 -0
- data/bin/sm-watch-memory +9 -0
- data/bin/sparkle-motion +48 -0
- data/config.yml +201 -0
- data/data/dynamic/bridges.csv +5 -0
- data/data/static/devices.csv +7 -0
- data/lib/sparkle_motion.rb +72 -0
- data/lib/sparkle_motion/config.rb +45 -0
- data/lib/sparkle_motion/env.rb +14 -0
- data/lib/sparkle_motion/http.rb +26 -0
- data/lib/sparkle_motion/hue/ssdp.rb +39 -0
- data/lib/sparkle_motion/launch_pad/color.rb +41 -0
- data/lib/sparkle_motion/launch_pad/widget.rb +187 -0
- data/lib/sparkle_motion/launch_pad/widgets/button.rb +29 -0
- data/lib/sparkle_motion/launch_pad/widgets/horizontal_slider.rb +43 -0
- data/lib/sparkle_motion/launch_pad/widgets/radio_group.rb +53 -0
- data/lib/sparkle_motion/launch_pad/widgets/toggle.rb +48 -0
- data/lib/sparkle_motion/launch_pad/widgets/vertical_slider.rb +43 -0
- data/lib/sparkle_motion/lazy_request_config.rb +87 -0
- data/lib/sparkle_motion/node.rb +80 -0
- data/lib/sparkle_motion/nodes/generator.rb +8 -0
- data/lib/sparkle_motion/nodes/generators/const.rb +15 -0
- data/lib/sparkle_motion/nodes/generators/perlin.rb +26 -0
- data/lib/sparkle_motion/nodes/generators/wave2.rb +20 -0
- data/lib/sparkle_motion/nodes/transform.rb +34 -0
- data/lib/sparkle_motion/nodes/transforms/contrast.rb +20 -0
- data/lib/sparkle_motion/nodes/transforms/range.rb +41 -0
- data/lib/sparkle_motion/nodes/transforms/spotlight.rb +35 -0
- data/lib/sparkle_motion/output.rb +69 -0
- data/lib/sparkle_motion/results.rb +78 -0
- data/lib/sparkle_motion/utility.rb +68 -0
- data/lib/sparkle_motion/vector2.rb +11 -0
- data/lib/sparkle_motion/version.rb +3 -0
- data/sparkle_motion.gemspec +44 -0
- 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
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")
|
data/bin/sm-mark-lights
ADDED
@@ -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
|