sparkle_motion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +22 -0
  4. data/README.md +47 -0
  5. data/bin/sm-discover +18 -0
  6. data/bin/sm-mark-lights +58 -0
  7. data/bin/sm-off +38 -0
  8. data/bin/sm-on +50 -0
  9. data/bin/sm-simulate +595 -0
  10. data/bin/sm-watch-memory +9 -0
  11. data/bin/sparkle-motion +48 -0
  12. data/config.yml +201 -0
  13. data/data/dynamic/bridges.csv +5 -0
  14. data/data/static/devices.csv +7 -0
  15. data/lib/sparkle_motion.rb +72 -0
  16. data/lib/sparkle_motion/config.rb +45 -0
  17. data/lib/sparkle_motion/env.rb +14 -0
  18. data/lib/sparkle_motion/http.rb +26 -0
  19. data/lib/sparkle_motion/hue/ssdp.rb +39 -0
  20. data/lib/sparkle_motion/launch_pad/color.rb +41 -0
  21. data/lib/sparkle_motion/launch_pad/widget.rb +187 -0
  22. data/lib/sparkle_motion/launch_pad/widgets/button.rb +29 -0
  23. data/lib/sparkle_motion/launch_pad/widgets/horizontal_slider.rb +43 -0
  24. data/lib/sparkle_motion/launch_pad/widgets/radio_group.rb +53 -0
  25. data/lib/sparkle_motion/launch_pad/widgets/toggle.rb +48 -0
  26. data/lib/sparkle_motion/launch_pad/widgets/vertical_slider.rb +43 -0
  27. data/lib/sparkle_motion/lazy_request_config.rb +87 -0
  28. data/lib/sparkle_motion/node.rb +80 -0
  29. data/lib/sparkle_motion/nodes/generator.rb +8 -0
  30. data/lib/sparkle_motion/nodes/generators/const.rb +15 -0
  31. data/lib/sparkle_motion/nodes/generators/perlin.rb +26 -0
  32. data/lib/sparkle_motion/nodes/generators/wave2.rb +20 -0
  33. data/lib/sparkle_motion/nodes/transform.rb +34 -0
  34. data/lib/sparkle_motion/nodes/transforms/contrast.rb +20 -0
  35. data/lib/sparkle_motion/nodes/transforms/range.rb +41 -0
  36. data/lib/sparkle_motion/nodes/transforms/spotlight.rb +35 -0
  37. data/lib/sparkle_motion/output.rb +69 -0
  38. data/lib/sparkle_motion/results.rb +78 -0
  39. data/lib/sparkle_motion/utility.rb +68 -0
  40. data/lib/sparkle_motion/vector2.rb +11 -0
  41. data/lib/sparkle_motion/version.rb +3 -0
  42. data/sparkle_motion.gemspec +44 -0
  43. metadata +178 -0
@@ -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,8 @@
1
+ module SparkleMotion
2
+ module Nodes
3
+ # A sub-class of `Node` for any node that operates as the root of a DAG, rather than as a
4
+ # transform.
5
+ class Generator < Node
6
+ end
7
+ end
8
+ 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