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,9 @@
1
+ #!/bin/bash
2
+
3
+ while [ 1 ]; do
4
+ ps auxwww |
5
+ grep rub[y] |
6
+ grep sm-simulate |
7
+ awk '{ print $6 }' | perl -pse 's/\n/, /g' | perl -pse 's/, $/\n/'
8
+ sleep 1
9
+ done
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ ###############################################################################
3
+ # Visual Effects
4
+ ###############################################################################
5
+ # Whether to use background sweep thread for saw-tooth pattern on hue:
6
+ export USE_SWEEP=1
7
+ # Whether to actually run main lighting threads:
8
+ export USE_LIGHTS=1
9
+ # Whether or not to run the simulation graph:
10
+ export USE_GRAPH=1
11
+ # Whether or not to use Novation LaunchPad for controls:
12
+ export USE_INPUT=1
13
+
14
+
15
+ ###############################################################################
16
+ # Debugging
17
+ ###############################################################################
18
+ # Run for a fixed number of iterations, or until we're killed (0):
19
+ export ITERATIONS=0
20
+
21
+ # Forcibly disable Ruby GC:
22
+ export SKIP_GC=0
23
+
24
+ # Logging verbosity. Valid values: DEBUG, INFO, WARN, ERROR. Default is INFO.
25
+ export SPARKLEMOTION_LOGLEVEL=INFO
26
+
27
+ # Whether to run a profiler:
28
+ export PROFILE_RUN= # ruby-prof|memory_profiler
29
+
30
+ # If using ruby-prof, what mode to run it in:
31
+ export RUBY_PROF_MODE=allocations # ALLOCATIONS, CPU_TIME, GC_RUNS, GC_TIME, MEMORY, PROCESS_TIME, WALL_TIME
32
+
33
+ # Dump various PNGs showing the results of given nodes in the DAG over time.
34
+ # This is VERY VERY memory intensize! Don't try to use it for a long run!
35
+ # Current nodes: perlin, stretched, shifted_0, shifted_1, shifted_2, shifted_3, spotlit, output
36
+ # ... however you probably don't care about shifted_0..shifted_2.
37
+ export DEBUG_NODES= #perlin,stretched,shifted_3,spotlit,output
38
+
39
+
40
+ ###############################################################################
41
+ echo "Beginning simulation. Press ctrl-c to end."
42
+ touch /tmp/sparkle-motion.state
43
+ EXIT_FLAG=127
44
+ while [ $EXIT_FLAG != 0 ]; do
45
+ ./bin/sm-simulate
46
+ EXIT_FLAG=$?
47
+ echo "Process terminated with exit code: $EXIT_FLAG"
48
+ done
data/config.yml ADDED
@@ -0,0 +1,201 @@
1
+ ---
2
+ common_username: &common_username "1234567890"
3
+ # Determine HTTP request concurrency for updating lights. Per-bridge.
4
+ max_connects: 3
5
+ bridges:
6
+ "Bridge-01":
7
+ ip: "192.168.2.10"
8
+ username: *common_username
9
+ debug_hue: 0
10
+ "Bridge-02":
11
+ ip: "192.168.2.6"
12
+ username: *common_username
13
+ debug_hue: 25000
14
+ "Bridge-03":
15
+ ip: "192.168.2.7"
16
+ username: *common_username
17
+ debug_hue: 45000
18
+ "Bridge-04":
19
+ ip: "192.168.2.9"
20
+ username: *common_username
21
+ debug_hue: 12000
22
+ simulation:
23
+ # TODO: Have day/night configurations and a way to LERP between them.
24
+ #
25
+ # TODO: Tool for testing bulb positioning.
26
+ #
27
+ # TODO: Use mid-point/range notation for intensity to make tweaking easier.
28
+ #
29
+ # TODO: Duck intensity when tweaking saturation.
30
+ #
31
+ # TODO: Finish simulation visualization tool.
32
+ #
33
+ # TODO: Re-parameterize sleep option, and maybe add a way to tweak it on the fly.
34
+ #
35
+ # TODO: Add outer-loop script, and use exit codes to differentiate between restart and terminate.
36
+ #
37
+ # TODO: Way to tweak output transition time on the fly?
38
+ #
39
+ # TODO: Separate color-sweeps per bridge.
40
+ #
41
+ # TODO: Way to tweak sweep transition time on the fly?
42
+ #
43
+ # TODO: Play with X multiplier for Perlin component to see if that makes the lighting more visually interesting.
44
+ #
45
+ # TODO: Group all writes to a bridge into a single thread.
46
+ #
47
+ # TODO: Either self-tune delays, or avoid sending refreshes to each light/group too fast?
48
+ output:
49
+ transition: 0.3
50
+ sweep:
51
+ # Don't set this transition much below 1.0! ZigBee spec only allows 1
52
+ # group update/sec, but Hue Bridge/lights seem to be OK with about 1 every
53
+ # 0.75 sec...
54
+ #
55
+ # Negative values mean to use a transition time of 0 for the change, but
56
+ # wait the absolute value between steps.
57
+ transition: 1.5
58
+ # Ballpark estimation of Jen's palette:
59
+ values:
60
+ - 49500
61
+ - 49500
62
+ - 48000
63
+ - 49500
64
+ - 49500
65
+ - 51000
66
+ nodes:
67
+ # wave2:
68
+ # speed: [0.1, 1.0]
69
+ perlin:
70
+ speed: [0.1, 4.0]
71
+ contrast:
72
+ # Function: LINEAR, CUBIC, QUINTIC -- don't bother using iterations > 1
73
+ # with LINEAR, as LINEAR is a no-op.
74
+ function: cubic
75
+ iterations: 3
76
+ controls:
77
+ exit:
78
+ position: mixer
79
+ colors:
80
+ "color": dark_gray
81
+ "down": white
82
+ intensity:
83
+ widget: SparkleMotion::LaunchPad::Widgets::VerticalSlider
84
+ positions:
85
+ - [0, 4]
86
+ - [1, 4]
87
+ - [2, 4]
88
+ - [3, 4]
89
+ size: 4
90
+ values:
91
+ # Mid-point, delta (was low/high):
92
+ - [0.350, 0.050] #[0.25, 0.45]
93
+ - [0.500, 0.050] #[0.40, 0.60]
94
+ - [0.675, 0.125] #[0.55, 0.80]
95
+ - [0.875, 0.125] #[0.75, 1.00]
96
+ colors:
97
+ "on": 0x22003F
98
+ "off": 0x05000A
99
+ "down": 0x27103F
100
+ saturation:
101
+ widget: SparkleMotion::LaunchPad::Widgets::VerticalSlider
102
+ transition: 0.3
103
+ positions:
104
+ - [4, 4]
105
+ - [5, 4]
106
+ - [6, 4]
107
+ - [7, 4]
108
+ size: 4
109
+ groups:
110
+ - ["Bridge-01", 0]
111
+ - ["Bridge-02", 0]
112
+ - ["Bridge-03", 0]
113
+ - ["Bridge-04", 0]
114
+ values:
115
+ - 102
116
+ - 152
117
+ - 203
118
+ - 254
119
+ colors:
120
+ "on": 0x1C103F
121
+ "off": 0x03030C
122
+ "down": 0x10103F
123
+ spotlighting:
124
+ x: 0
125
+ y: 0
126
+ mappings:
127
+ # NOTE: Mappings defines the width/height of the widget implicitly!
128
+ # NOTE: Values are indexes into main_lights array.
129
+ #
130
+ # Excluding outermost lights, and going top-down for left-most to
131
+ # right-most light across left then right strings:
132
+ #
133
+ # Bridge 3/4:
134
+ - [14, 15, 16, 17, 18, 19, 20, 21]
135
+ # Bridge 1/2:
136
+ - [ 2, 3, 4, 5, 6, 7, 8, 9]
137
+ colors:
138
+ "on": 0x272700
139
+ "off": 0x020200
140
+ "down": 0x3F3F10
141
+ # Main Lights is the group for which the main simulation will be applied.
142
+ # It can be any number of lights from any number of bridges but you'll need to
143
+ # plan groups out for saturation controls.
144
+ main_lights:
145
+ # Strip 1:
146
+ - ["Bridge-01", 37]
147
+ - ["Bridge-01", 36]
148
+ - ["Bridge-01", 38]
149
+ - ["Bridge-01", 39]
150
+ - ["Bridge-01", 40]
151
+ - ["Bridge-01", 35]
152
+
153
+ - ["Bridge-02", 12]
154
+ - ["Bridge-02", 21]
155
+ - ["Bridge-02", 20]
156
+ - ["Bridge-02", 19]
157
+ - ["Bridge-02", 15]
158
+ - ["Bridge-02", 18]
159
+
160
+ # Strip 2:
161
+ - ["Bridge-03", 2]
162
+ - ["Bridge-03", 3]
163
+ - ["Bridge-03", 8]
164
+ - ["Bridge-03", 10]
165
+ - ["Bridge-03", 9]
166
+ - ["Bridge-03", 6]
167
+
168
+ - ["Bridge-04", 7]
169
+ - ["Bridge-04", 11]
170
+ - ["Bridge-04", 12]
171
+ - ["Bridge-04", 1]
172
+ - ["Bridge-04", 9]
173
+ - ["Bridge-04", 8]
174
+ # TODO: Come up with appropriate behaviors/extensions of behavior for dance
175
+ # TODO: floor:
176
+ # TODO: * Spotighting.
177
+ # TODO: * Saturation.
178
+ # TODO: * Intensity.
179
+ #
180
+ # Dance Lights is the group of lights above the dance floor, which will get
181
+ # their own simulation, although updates will be interleaved with the main
182
+ # lights per-bridge. It should be a positional map affording spatial
183
+ # coherence.
184
+ #
185
+ # For spotlighting/saturation/intensity purposes, it will be treated as one
186
+ # group.
187
+ dance_lights:
188
+ - ["Bridge-01", 26]
189
+ - ["Bridge-02", 11]
190
+ - ["Bridge-03", 7]
191
+ - ["Bridge-04", 5]
192
+ accent_lights:
193
+ - ["Bridge-01", 9]
194
+ - ["Bridge-01", 10]
195
+ - ["Bridge-01", 11]
196
+ - ["Bridge-01", 12]
197
+ - ["Bridge-01", 13]
198
+ - ["Bridge-01", 33]
199
+ - ["Bridge-01", 34]
200
+ - ["Bridge-02", 7]
201
+ - ["Bridge-02", 8]
@@ -0,0 +1,5 @@
1
+ Bridge,MAC,"Zigbee Channel"
2
+ Bridge-01,00:17:88:12:26:f3,25
3
+ Bridge-02,00:17:88:10:5e:da,15
4
+ Bridge-03,00:17:88:18:53:d0,11
5
+ Bridge-04,00:17:88:1a:1d:5c,20
@@ -0,0 +1,7 @@
1
+ Manufacturer,Type,"Model ID",Series
2
+ Philips,"Color light",LLC011,"Hue Bloom"
3
+ Philips,"Color light",LST001,"LightStrips"
4
+ Philips,"Dimmable light",LWB004,"Hue Lux"
5
+ Philips,"Extended color light",LCT001,"Hue Lamp"
6
+ Philips,"Extended color light",LCT002,"Hue Downlight"
7
+ Philips,"Extended color light",LLC020,"Hue Go"
@@ -0,0 +1,72 @@
1
+ lib = File.expand_path("../", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ # We shave 1/6th of a sec off launch time by not doing the following, but it
5
+ # presumes the user is using RVM properly to isolate the gem env, and that
6
+ # there are no git/path-based gems in the Gemfile:
7
+ # require "rubygems"
8
+ # require "bundler/setup"
9
+
10
+ require "yaml"
11
+ require "logger-better"
12
+
13
+ # System for building interesting, dynamic lighting effects for the Philips Hue,
14
+ # using the Novation Launchpad for control.
15
+ module SparkleMotion
16
+ def self.logger; @logger; end
17
+
18
+ def self.init!(name)
19
+ @logger = Logger::Better.new(STDOUT)
20
+ @logger.level = (ENV["SPARKLEMOTION_LOGLEVEL"] || "info").downcase.to_sym
21
+ @logger.progname = name
22
+ end
23
+
24
+ # Load code for talking to Philips Hue lighting system.
25
+ def self.use_hue!(discovery: false, api: false)
26
+ if api
27
+ require "sparkle_motion/results"
28
+ require "sparkle_motion/lazy_request_config"
29
+ end
30
+
31
+ if discovery
32
+ require "sparkle_motion/hue/ssdp"
33
+ end
34
+ end
35
+
36
+ # Load code for graph-structured effect generation.
37
+ def self.use_graph!
38
+ # Base classes:
39
+ require "sparkle_motion/node"
40
+ require "sparkle_motion/nodes/generator"
41
+ require "sparkle_motion/nodes/transform"
42
+
43
+ # Simulation root nodes:
44
+ require "sparkle_motion/nodes/generators/const"
45
+ require "sparkle_motion/nodes/generators/perlin"
46
+ require "sparkle_motion/nodes/generators/wave2"
47
+
48
+ # Simulation transform nodes:
49
+ require "sparkle_motion/nodes/transforms/contrast"
50
+ require "sparkle_motion/nodes/transforms/range"
51
+ require "sparkle_motion/nodes/transforms/spotlight"
52
+ end
53
+
54
+ def self.use_widgets!
55
+ require "sparkle_motion/launch_pad/widget"
56
+ require "sparkle_motion/launch_pad/widgets/horizontal_slider"
57
+ require "sparkle_motion/launch_pad/widgets/vertical_slider"
58
+ require "sparkle_motion/launch_pad/widgets/radio_group"
59
+ require "sparkle_motion/launch_pad/widgets/button"
60
+ end
61
+
62
+ # Load code/widgets for Novation LaunchPad.
63
+ def self.use_launchpad!
64
+ require "launchpad"
65
+ end
66
+ end
67
+
68
+ require "sparkle_motion/version"
69
+ require "sparkle_motion/utility"
70
+ require "sparkle_motion/config"
71
+ require "sparkle_motion/env"
72
+ require "sparkle_motion/http"
@@ -0,0 +1,45 @@
1
+ # TODO: Load this on-demand, not automatically! Namespace it! AUGH!
2
+ require "sparkle_motion/vector2"
3
+ require "sparkle_motion/launch_pad/color"
4
+
5
+ def unpack_color(col)
6
+ if col.is_a?(String)
7
+ SparkleMotion::LaunchPad::Color.const_get(col.upcase).to_h
8
+ else
9
+ { r: ((col >> 16) & 0xFF),
10
+ g: ((col >> 8) & 0xFF),
11
+ b: (col & 0xFF) }
12
+ end
13
+ end
14
+
15
+ def unpack_colors_in_place!(cfg)
16
+ cfg.each do |key, val|
17
+ if val.is_a?(Array)
18
+ cfg[key] = val.map { |vv| unpack_color(vv) }
19
+ else
20
+ cfg[key] = unpack_color(val)
21
+ end
22
+ end
23
+ end
24
+
25
+ def unpack_vector_in_place!(cfg)
26
+ cfg.each do |key, val|
27
+ next unless val.is_a?(Array) && val.length == 2
28
+ cfg[key] = Vector2.new(x: val[0], y: val[1])
29
+ end
30
+ end
31
+
32
+ CONFIG = YAML.load(File.read("config.yml"))
33
+ CONFIG["bridges"].map do |name, cfg|
34
+ cfg["name"] = name
35
+ end
36
+
37
+ CONFIG["simulation"]["controls"].values.each do |cfg|
38
+ next unless cfg && cfg["colors"]
39
+ unpack_colors_in_place!(cfg["colors"])
40
+ end
41
+
42
+ CONFIG["simulation"]["nodes"].values.each do |cfg|
43
+ next unless cfg
44
+ unpack_vector_in_place!(cfg)
45
+ end
@@ -0,0 +1,14 @@
1
+ # TODO: Namespacing/classes/etc!
2
+ def env_int(name, allow_zero = false)
3
+ return nil unless ENV.key?(name)
4
+ tmp = ENV[name].to_i
5
+ tmp = nil if tmp == 0 && !allow_zero
6
+ tmp
7
+ end
8
+
9
+ def env_float(name)
10
+ return nil unless ENV.key?(name)
11
+ ENV[name].to_f
12
+ end
13
+
14
+ def env_bool(name); (env_int(name, true) || 1) != 0; end
@@ -0,0 +1,26 @@
1
+ # TODO: Namespacing/classes/etc!
2
+ require "curb"
3
+ require "oj"
4
+
5
+ # TODO: Try to figure out how to set Curl::CURLOPT_TCP_NODELAY => true
6
+ # TODO: Disable Curl from sending keepalives by trying HTTP/1.0.
7
+ MULTI_OPTIONS = { pipeline: false,
8
+ max_connects: (CONFIG["max_connects"] || 3) }
9
+ EASY_OPTIONS = { "timeout" => 5,
10
+ "connect_timeout" => 5,
11
+ "follow_location" => false,
12
+ "max_redirects" => 0 } # ,
13
+ # version: Curl::HTTP_1_0 }
14
+ # easy.header_str.grep(/keep-alive/)
15
+ # Force keepalive off to see if that makes any difference...
16
+ # TODO: Use this: `easy.headers["Expect"] = ''` to remove a default header we don't care about!
17
+
18
+ def hue_server(config); "http://#{config['ip']}"; end
19
+ def hue_base(config); "#{hue_server(config)}/api/#{config['username']}"; end
20
+ def hue_light_endpoint(config, light_id); "#{hue_base(config)}/lights/#{light_id}/state"; end
21
+ def hue_group_endpoint(config, group); "#{hue_base(config)}/groups/#{group}/action"; end
22
+
23
+ def with_transition_time(data, transition)
24
+ data["transitiontime"] = (transition * 10.0).round(0)
25
+ data
26
+ end
@@ -0,0 +1,39 @@
1
+ require "frisky/ssdp"
2
+ Frisky.logging_enabled = false # Frisky is super verbose
3
+
4
+ module SparkleMotion
5
+ module Hue
6
+ # Helpers for SSDP discovery of bridges.
7
+ class SSDP
8
+ def scan
9
+ raw = Frisky::SSDP
10
+ .search("IpBridge")
11
+ .select { |resp| ssdp_response?(resp) }
12
+ .map { |resp| ssdp_extract(resp) }
13
+ .select { |resp| resp["name"] == "upnp:rootdevice" }
14
+ Hash[raw.map { |resp| [resp["id"], resp["ipaddress"]] }]
15
+ end
16
+
17
+ # Ensure we're *only* getting responses from a Philips Hue bridge. The
18
+ # Hue Bridge tends to be obnoxious and announce itself on *any* SSDP
19
+ # SSDP request, so we assume that we may encounter other obnoxious gear
20
+ # as well...
21
+ def ssdp_response?(resp)
22
+ (resp[:server] || "")
23
+ .split(/[,\s]+/)
24
+ .find { |token| token =~ %r{\AIpBridge/\d+(\.\d+)*\z} }
25
+ end
26
+
27
+ def ssdp_extract(resp)
28
+ { "id" => usn_to_id(resp[:usn]),
29
+ "name" => resp[:st],
30
+ "ipaddress" => URI.parse(resp[:location]).host }
31
+ end
32
+
33
+ # TODO: With all the hassle around ID and the fact that I'm essentially
34
+ # TODO: coercing it down to just MAC address.... Just use the damned IP
35
+ # TODO: or MAC!
36
+ def usn_to_id(usn); usn.split(/:/, 3)[1].split(/-/).last; end
37
+ end
38
+ end
39
+ end