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