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.
- 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
|