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