sparkle_motion 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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