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
@@ -0,0 +1,87 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
# Evil hack to convince Curb to grab simulation-based information as late as
|
3
|
+
# possible, to undo the temporal skew that comes from updating the simulation
|
4
|
+
# then spending a bunch of time feeding updates to lights.
|
5
|
+
class LazyRequestConfig
|
6
|
+
GLOBAL_HISTORY = []
|
7
|
+
# TODO: Transition should be updated late as well...
|
8
|
+
def initialize(logger, config, url, results = nil, debug: nil, &callback)
|
9
|
+
@logger = logger
|
10
|
+
@config = config
|
11
|
+
@url = url
|
12
|
+
@results = results
|
13
|
+
@callback = callback
|
14
|
+
@fixed = create_fixed(url)
|
15
|
+
@debug = debug
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&block)
|
19
|
+
EASY_OPTIONS.each do |kv|
|
20
|
+
block.call(kv)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(field)
|
25
|
+
return @fixed[field] if @fixed.key?(field)
|
26
|
+
if field == :put_data
|
27
|
+
tmp = Oj.dump(@callback.call)
|
28
|
+
journal("BEGIN", body: tmp)
|
29
|
+
return tmp
|
30
|
+
end
|
31
|
+
|
32
|
+
@logger.error { "Request for unknown field: `#{field}`! Has Curb changed internally?" }
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def error(msg); "#{@config['name']}; #{@url}: #{msg}"; end
|
39
|
+
def overloaded(easy); error("Bridge overloaded: #{easy.body}"); end
|
40
|
+
def unknown_error(easy); error("Unknown error: #{easy.response_code}, #{easy.body}"); end
|
41
|
+
def hard_timeout(_easy); error("Request timed out."); end
|
42
|
+
def soft_timeout(easy); error("Failed updating light: #{easy.body}"); end
|
43
|
+
|
44
|
+
def create_fixed(url)
|
45
|
+
# TODO: Maybe skip per-event callbacks and go for single handler?
|
46
|
+
{ url: url,
|
47
|
+
method: :put,
|
48
|
+
headers: nil,
|
49
|
+
on_failure: proc { |easy, _| failure!(easy) },
|
50
|
+
on_success: proc { |easy| success!(easy) },
|
51
|
+
on_progress: nil,
|
52
|
+
on_debug: nil,
|
53
|
+
on_body: nil,
|
54
|
+
on_header: nil }
|
55
|
+
end
|
56
|
+
|
57
|
+
def journal(stage, easy)
|
58
|
+
return unless @debug
|
59
|
+
GLOBAL_HISTORY << "#{Time.now.to_f},#{stage},#{@url},#{easy.try(:body_str)}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def failure!(easy)
|
63
|
+
journal("END", easy)
|
64
|
+
case easy.response_code
|
65
|
+
when 404 # Hit Bridge hardware limit.
|
66
|
+
@results.failure!(overloaded(easy)) if @results
|
67
|
+
when 0 # Hit timeout.
|
68
|
+
@results.hard_timeout!(hard_timeout(easy)) if @results
|
69
|
+
else
|
70
|
+
@results.failure!(unknown_error(easy))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def success!(easy)
|
75
|
+
journal("END", easy)
|
76
|
+
if easy.body =~ /error/
|
77
|
+
# TODO: Check the error type field to be sure, and handle accordingly.
|
78
|
+
|
79
|
+
# Hit bridge rate limit / possibly ZigBee
|
80
|
+
# limit?.
|
81
|
+
@results.soft_timeout!(soft_timeout(easy)) if @results
|
82
|
+
else
|
83
|
+
@results.success! if @results
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# TODO: Namespacing/classes/etc!
|
2
|
+
|
3
|
+
module SparkleMotion
|
4
|
+
# Base class representing the state of an ordered set of lights, with an ability to debug
|
5
|
+
# things via PNG dump.
|
6
|
+
class Node
|
7
|
+
FRAME_PERIOD = 0.04
|
8
|
+
DEBUG_SCALE = Vector2.new(x: 2, y: 1)
|
9
|
+
|
10
|
+
attr_accessor :history, :debug, :lights
|
11
|
+
|
12
|
+
def initialize(lights:)
|
13
|
+
@lights = lights
|
14
|
+
@state = Array.new(@lights)
|
15
|
+
lights.times do |n|
|
16
|
+
@state[n] = 0.0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def debug=(val)
|
21
|
+
@debug = val
|
22
|
+
@history ||= [] if @debug
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](n); @state[n]; end
|
26
|
+
def []=(n, val); @state[n] = val; end
|
27
|
+
|
28
|
+
def update(t)
|
29
|
+
return unless @debug
|
30
|
+
prev_t = @history.last
|
31
|
+
prev_t = prev_t ? prev_t[:t] : t
|
32
|
+
@history << { t: t,
|
33
|
+
dt: t - prev_t,
|
34
|
+
state: (0..(@lights - 1)).map { |n| self[n] } }
|
35
|
+
end
|
36
|
+
|
37
|
+
def snapshot_to!(fname)
|
38
|
+
enrich_history!
|
39
|
+
png = new_image
|
40
|
+
history.inject(0) do |y, snapshot|
|
41
|
+
next_y = y + (snapshot[:y] || 0)
|
42
|
+
colors = snapshot[:state].map { |z| to_color(z) }
|
43
|
+
(y..(next_y - 1)).each do |yy|
|
44
|
+
colors.each_with_index do |c, x|
|
45
|
+
x1 = (x * DEBUG_SCALE.x).to_i
|
46
|
+
x2 = ((x + 1) * DEBUG_SCALE.x).to_i - 1
|
47
|
+
(x1..x2).each do |xx|
|
48
|
+
png[xx, yy] = c
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
next_y
|
53
|
+
end
|
54
|
+
png.save(fname, interlace: false)
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def new_image
|
60
|
+
require "oily_png" unless defined?(::ChunkyPNG)
|
61
|
+
ChunkyPNG::Image.new((@lights * DEBUG_SCALE.x).to_i,
|
62
|
+
history.map { |sn| sn[:y] }.inject(0) { |a, e| (a || 0) + e },
|
63
|
+
ChunkyPNG::Color::TRANSPARENT)
|
64
|
+
end
|
65
|
+
|
66
|
+
def enrich_history!
|
67
|
+
@history.each do |snapshot|
|
68
|
+
frames = snapshot[:dt] * (1 / FRAME_PERIOD) # A "frame" == fixed update interval, ms.
|
69
|
+
elapsed = (frames * DEBUG_SCALE.y).round.to_i
|
70
|
+
snapshot[:y] = (elapsed > 0) ? elapsed : DEBUG_SCALE.y.to_i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_color(val)
|
75
|
+
# Based on precision of Hue API...
|
76
|
+
z = (val * 254).to_i
|
77
|
+
ChunkyPNG::Color.rgba(z, z, z, 255)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
module Generators
|
4
|
+
# For debugging, output 1.0 all the time.
|
5
|
+
class Const < Generator
|
6
|
+
def initialize(lights:, value: 1.0)
|
7
|
+
super(lights: lights)
|
8
|
+
@lights.times do |n|
|
9
|
+
self[n] = value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "perlin_noise"
|
2
|
+
|
3
|
+
module SparkleMotion
|
4
|
+
module Nodes
|
5
|
+
module Generators
|
6
|
+
# Manage and run a Perlin-noise based simulation.
|
7
|
+
#
|
8
|
+
# TODO: Play with octaves / persistence, etc.
|
9
|
+
class Perlin < Generator
|
10
|
+
def initialize(lights:, speed:)
|
11
|
+
super(lights: lights)
|
12
|
+
@speed = speed
|
13
|
+
# TODO: See if we need/want to tinker with the `interval` option...
|
14
|
+
@perlin = ::Perlin::Noise.new(2, seed: 0)
|
15
|
+
end
|
16
|
+
|
17
|
+
def update(t)
|
18
|
+
@lights.times do |n|
|
19
|
+
self[n] = @perlin[n * @speed.x, t * @speed.y]
|
20
|
+
end
|
21
|
+
super(t)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
module Generators
|
4
|
+
# Manage and run a simulation of just `sin(x + y)`.
|
5
|
+
class Wave2 < Generator
|
6
|
+
def initialize(lights:, speed:)
|
7
|
+
super(lights: lights)
|
8
|
+
@speed = speed
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(t)
|
12
|
+
@lights.times do |n|
|
13
|
+
self[n] = (Math.sin((n * @speed.x) + (t * @speed.y)) * 0.5) + 0.5
|
14
|
+
end
|
15
|
+
super(t)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
# A sub-class of `Node` for any node that operates as a transform, rather than as a root node.
|
4
|
+
#
|
5
|
+
# Sub-classes should override `update(t)` and call it like so:
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# def update(t)
|
9
|
+
# super(t) do |x|
|
10
|
+
# # calculate value for light `x` from `@source[x]` here.
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
# ```
|
14
|
+
class Transform < Node
|
15
|
+
def initialize(source:, mask: nil)
|
16
|
+
super(lights: source.lights)
|
17
|
+
@source = source
|
18
|
+
@mask = mask
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(t)
|
22
|
+
@source.update(t)
|
23
|
+
if block_given?
|
24
|
+
(0..(@lights - 1)).each do |x|
|
25
|
+
# Apply to all lights if no mask, and apply to specific lights if mask.
|
26
|
+
apply_transform = !@mask || @mask[x]
|
27
|
+
@state[x] = apply_transform ? yield(x) : @source[x]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
super(t)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
module Transforms
|
4
|
+
# Transform values from 0..1 into a new range.
|
5
|
+
class Contrast < Transform
|
6
|
+
def initialize(function:, iterations:, source:, mask: nil)
|
7
|
+
super(source: source, mask: mask)
|
8
|
+
function = Perlin::Curve.const_get(function.to_s.upcase)
|
9
|
+
@contrast = Perlin::Curve.contrast(function, iterations.to_i)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(t)
|
13
|
+
super(t) do |x|
|
14
|
+
@contrast.call(@source[x])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
module Transforms
|
4
|
+
# Transform values from 0..1 into a new range.
|
5
|
+
#
|
6
|
+
# TODO: Allow change to range to apply over time?
|
7
|
+
class Range < Transform
|
8
|
+
def initialize(mid_point:, delta:, source:, mask: nil, logger:)
|
9
|
+
super(source: source, mask: mask)
|
10
|
+
@logger = logger
|
11
|
+
set_range(mid_point, delta)
|
12
|
+
end
|
13
|
+
|
14
|
+
def update(t)
|
15
|
+
super(t) do |x|
|
16
|
+
(@source[x] * (@max - @min)) + @min
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_range(mid_point, delta)
|
21
|
+
@min = clamp("min", mid_point, delta, (mid_point - delta).round)
|
22
|
+
@max = clamp("max", mid_point, delta, (mid_point + delta).round)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def clamp(name, mid_point, delta, val)
|
28
|
+
if val < 0
|
29
|
+
@logger.warn { "Bad range [#{mid_point} +/- #{delta}] - #{name} was <0! (#{val})" }
|
30
|
+
val = 0
|
31
|
+
end
|
32
|
+
if val > 255
|
33
|
+
@logger.warn { "Bad range [#{mid_point} +/- #{delta}] - #{name} was >255! (#{val})" }
|
34
|
+
val = 255
|
35
|
+
end
|
36
|
+
val
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SparkleMotion
|
2
|
+
module Nodes
|
3
|
+
module Transforms
|
4
|
+
# Spotlight a particular point on the line.
|
5
|
+
#
|
6
|
+
# TODO: Integrate the underlying light value but ensure we contrast-stretch
|
7
|
+
# TODO: to ensure a bright-enough spotlight over the destination. Maybe a LERP?
|
8
|
+
#
|
9
|
+
# TODO: Allow effect to come in / go out over time.
|
10
|
+
class Spotlight < Transform
|
11
|
+
def initialize(source:, mask: nil)
|
12
|
+
super(source: source, mask: mask)
|
13
|
+
@spotlight = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def spotlight!(x)
|
17
|
+
@spotlight = x
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear!; @spotlight = nil; end
|
21
|
+
|
22
|
+
def update(t)
|
23
|
+
super(t) do |x|
|
24
|
+
val = @source[x]
|
25
|
+
if @spotlight
|
26
|
+
falloff = 0.9**((@spotlight - x).abs**3)
|
27
|
+
val = falloff
|
28
|
+
end
|
29
|
+
val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# TODO: Namespacing/classes/etc!
|
2
|
+
def announce_iteration_config(iters)
|
3
|
+
SparkleMotion.logger.unknown do
|
4
|
+
if iters > 0
|
5
|
+
"Running for #{iters} iterations."
|
6
|
+
else
|
7
|
+
"Running until we're killed. Send SIGHUP to terminate with stats."
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def format_float(num); num ? num.round(2) : "-"; end
|
13
|
+
|
14
|
+
def format_rate(rate); "#{format_float(rate)}/sec"; end
|
15
|
+
|
16
|
+
def print_stat(name, value, rate)
|
17
|
+
SparkleMotion.logger.unknown { "* #{value} #{name} (#{format_rate(rate)})" }
|
18
|
+
end
|
19
|
+
|
20
|
+
STATS = [
|
21
|
+
["requests", :requests, :requests_sec],
|
22
|
+
["successes", :successes, :successes_sec],
|
23
|
+
["failures", :failures, :failures_sec],
|
24
|
+
["hard timeouts", :hard_timeouts, :hard_timeouts_sec],
|
25
|
+
["soft timeouts", :soft_timeouts, :soft_timeouts_sec],
|
26
|
+
]
|
27
|
+
|
28
|
+
def print_basic_stats(results)
|
29
|
+
STATS.each do |(name, count, rate)|
|
30
|
+
print_stat(name, results.send(count), results.send(rate))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def print_other_stats(results)
|
35
|
+
SparkleMotion.logger.unknown { "* #{format_float(results.failure_rate)}% failure rate" }
|
36
|
+
suffix = " (#{format_float(results.elapsed / ITERATIONS.to_f)}/iteration)" if ITERATIONS > 0
|
37
|
+
SparkleMotion.logger.unknown { "* #{format_float(results.elapsed)} seconds elapsed#{suffix}" }
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: Show per-bridge and aggregate stats.
|
41
|
+
def print_results(results)
|
42
|
+
SparkleMotion.logger.unknown { "Results:" }
|
43
|
+
print_basic_stats(results)
|
44
|
+
print_other_stats(results)
|
45
|
+
end
|
46
|
+
|
47
|
+
def dump_node_debug_data!(prefix)
|
48
|
+
nodes_under_debug.each_with_index do |(name, node), index|
|
49
|
+
node.snapshot_to!("tmp/%s_%02d_%s.png" % [prefix, index, name.downcase])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def dump_output_debug_data!(prefix)
|
54
|
+
return unless DEBUG_FLAGS["OUTPUT"] && USE_LIGHTS
|
55
|
+
File.open("tmp/#{prefix}_output.raw", "w") do |fh|
|
56
|
+
fh.write(SparkleMotion::LazyRequestConfig::GLOBAL_HISTORY.join("\n"))
|
57
|
+
fh.write("\n")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def dump_debug_data!
|
62
|
+
return unless debugging?
|
63
|
+
prefix = "%010.0f" % Time.now.to_f
|
64
|
+
|
65
|
+
SparkleMotion.logger.unknown { "Dumping debug and/or profiling data to `tmp/#{prefix}_*`." }
|
66
|
+
stop_ruby_prof!
|
67
|
+
dump_node_debug_data!(prefix)
|
68
|
+
dump_output_debug_data!(prefix)
|
69
|
+
end
|