ws_light 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dbf069c0977c0d9f8df28c61681c404780820a38
4
+ data.tar.gz: 5119cbe7ce8d5014ff8b9b63c299a68ef266dcb9
5
+ SHA512:
6
+ metadata.gz: 9792d608a3f9859c70ff2c5319d6abb4480f21a4b39bbb7385c735fdbbe16e274184e1d45e347c2880ea38be1e6557ca4c9128d56a672b88f6fb2b652c9f9e5e
7
+ data.tar.gz: 2adef99d7dee1b1f081d510189535ab4b813697252e19bf9f1cd48e9a97971a9ef1e3428e8e9fa0da5d68e94a3d38f26a9b15b5f23e0aff3d542f52e7f6e9d0f
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /.idea
2
+ *.log
3
+
4
+ # Ignore built gems
5
+ *.gem
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Gerrit Visscher
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # led_strip
2
+
3
+ This gem is used (by me) to control my two led strip in my apartment. It is connected to two motion detectors and
4
+ lights up my hall from the direction I enter. This gem is probably quite useless without a description of the
5
+ hardware I am using. This will follow here once it works sufficiently.
6
+
7
+ If you are interested, drop me a note.
8
+
9
+ At the moment, you would need:
10
+ - A raspberry pi or another computer with an SPI interface
11
+ - One or two WS2801 led strips
12
+ - One or two motion detectors, something like a HC-SR501
13
+ - A power supply (obviously)
data/bin/ws_light ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ DEBUG = true
4
+
5
+ if ARGV[0] && ARGV[0]=='-b'
6
+ case ARGV[1]
7
+ when 'set'
8
+ require 'ws_light/benchmark/set_benchmark'
9
+ when 'animation'
10
+ require 'ws_light/benchmark/animation_benchmark'
11
+ when 'ws2801'
12
+ require 'ws_light/benchmark/ws2801_benchmark'
13
+ else
14
+ puts 'Available benchmarks: set, animation, ws2801'
15
+ end
16
+ exit
17
+ end
18
+
19
+ require 'pi_piper'
20
+
21
+ include PiPiper
22
+
23
+ require 'bundler'
24
+ require 'ws_light/strip'
25
+ require 'ws_light/sd_logger'
26
+
27
+ logger = WSLight::SDLogger.new
28
+ logger.debug = DEBUG
29
+ logger.filename = 'motion.log'
30
+ logger.log 'Starting up'
31
+
32
+ # Trap ^C
33
+ Signal.trap('INT') {
34
+ logger.log 'Shutting down'
35
+ logger.write_log
36
+ exit
37
+ }
38
+
39
+ # Trap `kill `
40
+ Signal.trap('TERM') {
41
+ logger.log 'Shutting down'
42
+ logger.write_log
43
+ exit
44
+ }
45
+
46
+ strip = WSLight::Strip.new
47
+ strip.debug = DEBUG
48
+
49
+ pin_right = PiPiper::Pin.new(:pin => 22, :direction => :in)
50
+ pin_left = PiPiper::Pin.new(:pin => 23, :direction => :in)
51
+
52
+ after :pin => 23, :goes => :high do
53
+ logger.log('Motion detected: RIGHT')
54
+ strip.on(WSLight::Strip::DIRECTION_RIGHT)
55
+ while pin_right.on?
56
+ strip.on(WSLight::Strip::DIRECTION_RIGHT)
57
+ sleep 1
58
+ end
59
+ end
60
+
61
+ after :pin => 24, :goes => :high do
62
+ logger.log('Motion detected: LEFT')
63
+ strip.on(WSLight::Strip::DIRECTION_LEFT)
64
+ while pin_left.on?
65
+ strip.on(WSLight::Strip::DIRECTION_LEFT)
66
+ sleep 1
67
+ end
68
+ end
69
+
70
+ PiPiper.wait
71
+
@@ -0,0 +1,21 @@
1
+ module WSLight
2
+ module Animation
3
+ # Base class for all animations, defines common methods
4
+ class BaseAnimation
5
+ attr_accessor :set_from, :set_to, :type
6
+
7
+ def initialize(set_from, set_to)
8
+ @set_from = set_from
9
+ @set_to = set_to
10
+ end
11
+
12
+ def frame_data(count)
13
+ frame(count).collect{|color| color.to_a}.flatten
14
+ end
15
+
16
+ def frames_per_second
17
+ 25.0
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require 'ws_light/animation/base_animation'
2
+
3
+ module WSLight
4
+ module Animation
5
+ # Slides from one set to another from left to right (obviously depending on the hardware setup)
6
+ class FadeAnimation < BaseAnimation
7
+ FADE_DURATION = 50
8
+
9
+ def frames
10
+ FADE_DURATION + 1
11
+ end
12
+
13
+ def frame(count)
14
+ set = []
15
+
16
+ @set_from.next_frame
17
+ @set_to.next_frame
18
+
19
+ @set_from.full_length.times do |i|
20
+ set << @set_from.pixel(i).mix(@set_to.pixel(i), count.to_f/FADE_DURATION.to_f)
21
+ end
22
+
23
+ set
24
+ end
25
+
26
+ def frames_per_second
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ require 'ws_light/animation/base_animation'
2
+
3
+ module WSLight
4
+ module Animation
5
+ # Slides from one set to another from left to right (obviously depending on the hardware setup)
6
+ class SlideLeftAnimation < BaseAnimation
7
+ def frames
8
+ @set_from.length + 1 # one for each led plus one for all zero
9
+ end
10
+
11
+ def frame(count)
12
+ set = []
13
+ reverse_set = []
14
+
15
+ @set_from.next_frame
16
+ @set_to.next_frame
17
+
18
+ (set_from.length - count).times do |i|
19
+ set << set_from.pixel(i)
20
+ reverse_set << set_from.pixel((set_from.length * 2) - 1 - i) if set_from.type == :double
21
+ end
22
+
23
+ count.times do |i|
24
+ set << set_to.pixel(set_from.length - count + i)
25
+ reverse_set << set_to.pixel(set_from.length + count - 1 - i) if set_from.type == :double
26
+ end
27
+
28
+ set += reverse_set.reverse if set_from.type == :double
29
+
30
+ set
31
+ end
32
+
33
+ def frames_per_second
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require 'ws_light/animation/base_animation'
2
+
3
+ module WSLight
4
+ module Animation
5
+ # Slides from one set to another from right to left (obviously depending on the hardware setup)
6
+ class SlideRightAnimation < BaseAnimation
7
+
8
+ def frames
9
+ @set_from.length + 1 # one for each led plus one for all zero
10
+ end
11
+
12
+ def frame(count)
13
+ set = []
14
+ reverse_set = []
15
+
16
+ @set_from.next_frame
17
+ @set_to.next_frame
18
+
19
+ count.times do |i|
20
+ set << set_to.pixel(i)
21
+ reverse_set << set_to.pixel((set_from.length * 2) - 1 - i)
22
+ end
23
+
24
+ (set_from.length - count).times do |i|
25
+ set << set_from.pixel(i + count)
26
+ reverse_set << set_from.pixel((set_from.length * 2) - count - 1 - i)
27
+ end
28
+
29
+ set += reverse_set.reverse if set_from.type == :double
30
+
31
+ set
32
+ end
33
+
34
+ def frames_per_second
35
+ nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ require 'ws_light/animation/fade_animation'
2
+ require 'ws_light/animation/slide_left_animation'
3
+ require 'ws_light/animation/slide_right_animation'
4
+ require 'ws_light/set/color_set'
5
+
6
+ require 'benchmark'
7
+ include Benchmark
8
+
9
+
10
+ color_set_from = WSLight::Set::ColorSet.new
11
+ color_set_from.color = WSLight::Color.new(255, 127, 0)
12
+ color_set_to = WSLight::Set::ColorSet.new
13
+ color_set_to.color = WSLight::Color.new(0, 127, 255)
14
+
15
+ @fade_animation = WSLight::Animation::FadeAnimation.new(color_set_from, color_set_to)
16
+ @slide_left_animation = WSLight::Animation::SlideLeftAnimation.new(color_set_from, color_set_to)
17
+ @slide_right_animation = WSLight::Animation::SlideRightAnimation.new(color_set_from, color_set_to)
18
+
19
+ n = 10_000
20
+
21
+ puts "Testing #{n} animation cycles with a simple color set..."
22
+
23
+ Benchmark.bm(15) do |x|
24
+ x.report('FadeAnimation:') { n.times do @fade_animation.frame(n%50) end }
25
+ x.report('SlideLeftAnimation:') { n.times do @slide_left_animation.frame(n%50) end }
26
+ x.report('SlideRightAnimation:') { n.times do @slide_right_animation.frame(n%50) end }
27
+ end
28
+
29
+ puts "To run smoothly, the benchmarks should all be lower than #{n/50.0} seconds."
@@ -0,0 +1,35 @@
1
+ require 'ws_light/set/gradient_set'
2
+ require 'ws_light/set/rainbow_set'
3
+ require 'ws_light/set/semolina_set'
4
+ require 'ws_light/set/random_set'
5
+ require 'ws_light/set/strawberry_set'
6
+ require 'ws_light/set/watermelon_set'
7
+
8
+
9
+ require 'benchmark'
10
+ include Benchmark
11
+
12
+ @gradient_set = WSLight::Set::GradientSet.new
13
+ @rainbow_set = WSLight::Set::RainbowSet.new
14
+ @semolina_set = WSLight::Set::SemolinaSet.new
15
+ @random_set = WSLight::Set::RandomSet.new
16
+ @strawberry_set = WSLight::Set::StrawberrySet.new
17
+ @watermelon_set = WSLight::Set::WatermelonSet.new
18
+
19
+ @gradient_set.color_from = WSLight::Color.random_from_set
20
+ @gradient_set.color_to = WSLight::Color.random_from_set
21
+
22
+ n = 10_000
23
+
24
+ puts "Testing #{n} cycles over all sets..."
25
+
26
+ Benchmark.bm(15) do |x|
27
+ x.report('GradientSet:') { n.times do @gradient_set.frame end }
28
+ x.report('RainbowSet:') { n.times do @rainbow_set.frame end }
29
+ x.report('SemolinaSet:') { n.times do @semolina_set.frame end }
30
+ x.report('RandomSet:') { n.times do @random_set.frame end }
31
+ x.report('StrawberrySet:') { n.times do @strawberry_set.frame end }
32
+ x.report('WatermelonSet:') { n.times do @watermelon_set.frame end }
33
+ end
34
+
35
+ puts "To run smoothly, the benchmarks should all be lower than #{n/50.0} seconds."
@@ -0,0 +1,18 @@
1
+ require 'ws2801'
2
+ require 'benchmark'
3
+ include Benchmark
4
+
5
+ WS2801.length(320)
6
+
7
+ light = [1] * 320 * 3
8
+ dark = [0] * 320 * 3
9
+
10
+ n = 1000
11
+
12
+ puts "Testing #{n} writes to the led strip..."
13
+
14
+ Benchmark.bm(15) do |x|
15
+ x.report('Write:') { n.times do |i| WS2801.strip(i % 2 == 0 ? light : dark); WS2801.write end }
16
+ end
17
+
18
+ puts "To run smoothly, the benchmarks should all be lower than #{n/50.0} seconds."
@@ -0,0 +1,65 @@
1
+ module WSLight
2
+ class Color
3
+ attr_accessor :r, :g, :b
4
+
5
+ COLORS = {
6
+ pink: { r: 255, g: 16, b:32 },
7
+ red: { r: 255, g: 0, b: 0},
8
+ blue: { r: 0, g: 0, b: 255},
9
+ green: { r: 0, g: 255, b: 0},
10
+ cyan: { r: 0, g: 127, b: 127},
11
+ orange: {r: 255, g:70, b: 0},
12
+ yellow: {r: 255, g:220, b: 0},
13
+ purple: {r: 80, g:0, b: 180}
14
+ }
15
+
16
+ def initialize(r=0, g=0, b=0)
17
+ @r = r > 255 ? 255 : r.to_i
18
+ @g = g > 255 ? 255 : g.to_i
19
+ @b = b > 255 ? 255 : b.to_i
20
+ @r = @r < 0 ? 0 : @r
21
+ @g = @g < 0 ? 0 : @g
22
+ @b = @b < 0 ? 0 : @b
23
+ end
24
+
25
+ def to_a
26
+ [@b, @g, @r]
27
+ end
28
+
29
+ def mix(other, ratio)
30
+ Color.new(
31
+ (@r * (1-ratio) + other.r * ratio).to_i,
32
+ (@g * (1-ratio) + other.g * ratio).to_i,
33
+ (@b * (1-ratio) + other.b * ratio).to_i
34
+ )
35
+ end
36
+
37
+ def mix!(other, ratio)
38
+ @r = (@r * (1-ratio) + other.r * ratio).to_i
39
+ @g = (@g * (1-ratio) + other.g * ratio).to_i
40
+ @b = (@b * (1-ratio) + other.b * ratio).to_i
41
+ self
42
+ end
43
+
44
+ def self.random
45
+ Color.new(rand(192), rand(192), rand(192))
46
+ end
47
+
48
+ def self.by_name(name)
49
+ if Color::COLORS[name]
50
+ selected_color = Color::COLORS[name]
51
+ Color.new(selected_color[:r], selected_color[:g], selected_color[:b])
52
+ elsif name == :black
53
+ Color.new(0, 0, 0)
54
+ else
55
+ fail "Cannot find color #{name}"
56
+ end
57
+ end
58
+
59
+ def self.random_from_set
60
+ color_values = Color::COLORS.values
61
+ selected_color = color_values[rand(color_values.length)]
62
+ Color.new(selected_color[:r], selected_color[:g], selected_color[:b])
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,40 @@
1
+ module WSLight
2
+ # Provides a logger which writes only in long intervals, thus reducing write access to the cd card
3
+ # (out data is not that crucial)
4
+ class SDLogger
5
+ attr_accessor :entries, :interval, :entries, :debug, :filename
6
+
7
+ def initialize
8
+ @filename = 'log.txt'
9
+ @interval = 1800 # log interval in seconds
10
+ @entries = []
11
+ @last_write = Time.now
12
+ @debug = false
13
+ end
14
+
15
+ def log(text)
16
+ puts Time.now.to_s + ' -> ' + text if @debug
17
+ entries << {
18
+ text: text,
19
+ time: Time.now
20
+ }
21
+ write_log if timeout?
22
+ end
23
+
24
+ def write_log
25
+ return if @entries.empty?
26
+ file = File.open(@filename, File.exists?(@filename) ? 'a' : 'w')
27
+ @entries.each do |entry|
28
+ file.puts(entry[:time].to_s + ', ' + entry[:text])
29
+ end
30
+ file.close
31
+ @entries = []
32
+ @last_write = Time.now
33
+ end
34
+
35
+ def timeout?
36
+ (Time.now - @last_write) > @interval
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ require 'ws_light/color'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Base set with one color
6
+ class ColorSet
7
+ DEFAULT_LENGTH = 160
8
+ DEFAULT_TYPE = :double
9
+
10
+ attr_accessor :type, :length, :color, :full_length
11
+
12
+ def initialize(length = DEFAULT_LENGTH, type = DEFAULT_TYPE)
13
+ @length = length
14
+ @type = type
15
+ @full_length = (@type == :double ? @length * 2 : @length)
16
+ @color = Color.random
17
+ @frame_count = 0
18
+ init
19
+ end
20
+
21
+ def frame_data
22
+ frame.collect{|color| color.to_a}.flatten
23
+ end
24
+
25
+ def frame
26
+ length = type == :double ? @length * 2 : @length
27
+ set = []
28
+ length.times do
29
+ set << @color
30
+ end
31
+ set
32
+ end
33
+
34
+ def next_frame
35
+ # reimplement if necessary, please :)
36
+ end
37
+
38
+ def pixel(_number, _frame = 0)
39
+ @color
40
+ end
41
+
42
+ def init
43
+ # do some initializing stuff here
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a gradient from one color to another
6
+ class GradientSet < ColorSet
7
+ attr_accessor :color_from, :color_to
8
+
9
+ def init
10
+ @color_from = Color.new(0,0,0)
11
+ @color_to = Color.new(255,255,255)
12
+ end
13
+
14
+ def frame
15
+ @set ||= create_frame
16
+ end
17
+
18
+ def create_frame
19
+ set = []
20
+ @length.times do |i|
21
+ set << pixel(i)
22
+ end
23
+
24
+ set += set.reverse if type == :double # this should be faster than generating the pixel one after another
25
+
26
+ set
27
+ end
28
+
29
+ def pixel(number)
30
+ number = @full_length - 1 - number if number >= @length
31
+ @color_from.mix(@color_to, number.to_f/(@length-1))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a moving rainbow (actually a color circle)
6
+ class RainbowSet < ColorSet
7
+ def init
8
+ @frequency = Math::PI / @length
9
+ end
10
+
11
+ def next_frame
12
+ @frame_count += 1
13
+ end
14
+
15
+ def frame
16
+ next_frame
17
+ set = []
18
+
19
+ @length.times do |i|
20
+ set << pixel(i)
21
+ end
22
+
23
+ set += set.reverse if type == :double
24
+
25
+ set
26
+ end
27
+
28
+ def pixel(number)
29
+ number = @full_length - 1 - number if number >= @length
30
+ x = @frequency*(number+@frame_count)
31
+ Color.new(
32
+ (Math.sin(x)**2 * 127),
33
+ (Math.sin(x + 2.0*Math::PI/3.0)**2 * 127),
34
+ (Math.sin(x + 4.0*Math::PI/3.0)**2 * 127)
35
+ )
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a set with all random colors
6
+ class RandomSet < ColorSet
7
+ def frame
8
+ @set ||= generate_set
9
+ end
10
+
11
+ def pixel(number, _frame = 0)
12
+ frame[number]
13
+ end
14
+
15
+ def generate_set
16
+ set = []
17
+ @full_length.times do
18
+ set << Color.random
19
+ end
20
+ set
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates semolina with raspberries
6
+ class SemolinaSet < ColorSet
7
+ COLOR_SEMOLINA = Color.new(255, 127, 15)
8
+ COLOR_RASPBERRY = Color.new(255, 7, 15)
9
+ RASPBERRY_SIZE = 10
10
+ RASPBERRY_COUNT = 8
11
+
12
+ def init
13
+ @raspberries = []
14
+
15
+ while @raspberries.size < RASPBERRY_COUNT do
16
+ position = rand(@full_length)
17
+ @raspberries << position unless (at_end?(position) || between_strips?(position) || is_raspberry?(position))
18
+ end
19
+ end
20
+
21
+ def between_strips?(position)
22
+
23
+ @type == :double && ((@full_length/2 - 1 - RASPBERRY_SIZE)..(@full_length/2)).include?(position)
24
+ end
25
+
26
+ def at_end?(position)
27
+ position >= (@full_length - 1 - RASPBERRY_SIZE)
28
+ end
29
+
30
+ def frame
31
+ @set ||= create_frame
32
+ end
33
+
34
+ def create_frame
35
+ set = []
36
+
37
+ @full_length.times do |i|
38
+ set << (is_raspberry?(i) ? COLOR_RASPBERRY : COLOR_SEMOLINA)
39
+ end
40
+
41
+ set
42
+ end
43
+
44
+ def is_raspberry?(pixel)
45
+ @raspberries.each do |raspberry|
46
+ return true if pixel > raspberry && pixel < (raspberry + RASPBERRY_SIZE)
47
+ end
48
+ false
49
+ end
50
+
51
+ def pixel(number)
52
+ frame[number]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,60 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a set with all random colors
6
+ class StarSet < ColorSet
7
+ FRAMES_PER_STAR = 75
8
+ VISIBLE_STARS = 7
9
+ BLACK = Color.new(0,0,0)
10
+
11
+ def init
12
+ @stars = (0..(@full_length-4)).to_a.shuffle
13
+ @max = @stars.size
14
+ @stars += @stars[@stars.size - VISIBLE_STARS, VISIBLE_STARS]
15
+ generate_frame
16
+ end
17
+
18
+ def next_frame
19
+ @frame_count += 1
20
+ @frame_count = @frame_count % (@max * FRAMES_PER_STAR)
21
+ generate_frame
22
+ end
23
+
24
+ def frame
25
+ next_frame
26
+ @set
27
+ end
28
+
29
+ def generate_frame
30
+ @set = []
31
+ @full_length.times{ @set << BLACK }
32
+ start = (@frame_count * VISIBLE_STARS / FRAMES_PER_STAR) % @max
33
+ VISIBLE_STARS.times do |i|
34
+ draw_star(@stars[start + i], @frame_count - ((start - VISIBLE_STARS + 1 + i) * (FRAMES_PER_STAR.to_f / VISIBLE_STARS.to_f)).to_i)
35
+ end
36
+ end
37
+
38
+ def draw_star(position, star_frame)
39
+ #(-3..3).each do |i|
40
+ # white = brightness(i.abs, (star_frame - (FRAMES_PER_STAR/2)).abs)
41
+ # @set[position + i] = Color.new(white, white, white)
42
+ #end
43
+ #return if star_frame < 0
44
+ white = brightness(0, (star_frame.to_f - (FRAMES_PER_STAR.to_f/2.0)).abs)
45
+ #puts "#{position}, #{star_frame}, #{(star_frame.to_f - (FRAMES_PER_STAR.to_f/2.0)).abs}, #{white}"
46
+ @set[position] = Color.new(white, white, white)
47
+ end
48
+
49
+ def pixel(number)
50
+ @set[number]
51
+ end
52
+
53
+ def brightness(led_distance, frame_distance)
54
+ #return 0 if led_distance > 3
55
+ #(2 ** ((3 - led_distance) * 3 - 1) - 1) * (1 - (frame_distance/(FRAMES_PER_STAR/2)))
56
+ (255 * (1.0 - (frame_distance.to_f/(FRAMES_PER_STAR.to_f/2.0)))).to_i
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,48 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a strawberry set, some green, lots of pinkish red with a few greenish dots
6
+ class StrawberrySet < ColorSet
7
+ LENGTH_RED = 0.9
8
+ COLOR_NUT = Color.new(220, 255, 15)
9
+
10
+ def frame
11
+ @set ||= create_frame
12
+ end
13
+
14
+ def create_frame
15
+ set = []
16
+
17
+ length_red = (LENGTH_RED * @length).to_i
18
+
19
+
20
+ color_strawberry = Color.new(255, 7, 15)
21
+ color_leaves = Color.new(15, 191, 15)
22
+
23
+ @length.times do |i|
24
+ set << (i < length_red ? color_strawberry : color_leaves)
25
+ end
26
+
27
+ set = sprinkle_nuts(set)
28
+
29
+ type == :double ? set + set.reverse : set
30
+ end
31
+
32
+ def sprinkle_nuts(set)
33
+ length_red = (LENGTH_RED * @length).to_i
34
+ distance = 0
35
+ while distance < length_red - 21
36
+ distance += 15 + rand(5)
37
+ set[distance] = COLOR_NUT
38
+ end
39
+
40
+ set
41
+ end
42
+
43
+ def pixel(number)
44
+ frame[number]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ require 'ws_light/set/color_set'
2
+
3
+ module WSLight
4
+ module Set
5
+ # Creates a watermelon set, some green, some white, lots of red with a few red dots
6
+ class WatermelonSet < ColorSet
7
+ def create_frame
8
+ set = []
9
+
10
+ length_red = (0.72 * @length).to_i
11
+ length_red_to_white = (0.1 * @length).to_i
12
+ length_white = (0.1 * @length).to_i
13
+
14
+ white = Color.new(255, 255, 255)
15
+ red = Color.new(255, 0, 0)
16
+
17
+ @length.times do |i|
18
+ if i < length_red
19
+ set << Color.new((rand(25) < 1 ? 0 : 255), 0, 0)
20
+ elsif i < length_red + length_red_to_white
21
+ ratio = (i - length_red) / (length_red + length_red_to_white)
22
+ set << red.mix(white, ratio)
23
+ elsif i < length_red + length_red_to_white + length_white
24
+ set << white
25
+ else
26
+ set << Color.new(0, 127, 0)
27
+ end
28
+ end
29
+
30
+ type == :double ? set + set.reverse : set
31
+ end
32
+
33
+ def frame
34
+ @set ||= create_frame
35
+ end
36
+
37
+ def pixel(number)
38
+ frame[number]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,218 @@
1
+ require 'ws2801'
2
+ require 'ws_light/color'
3
+ require 'pp'
4
+ require 'date'
5
+ require 'json'
6
+ require 'open-uri'
7
+
8
+ require 'ws_light/animation/slide_left_animation'
9
+ require 'ws_light/animation/slide_right_animation'
10
+ require 'ws_light/animation/fade_animation'
11
+
12
+ require 'ws_light/set/color_set'
13
+ require 'ws_light/set/gradient_set'
14
+ require 'ws_light/set/random_set'
15
+ require 'ws_light/set/rainbow_set'
16
+ require 'ws_light/set/strawberry_set'
17
+ require 'ws_light/set/watermelon_set'
18
+ require 'ws_light/set/semolina_set'
19
+ require 'ws_light/set/star_set'
20
+
21
+
22
+
23
+ # Ideas
24
+ # - Fire?
25
+ # Config file
26
+
27
+ module WSLight
28
+ # Controls the led strip
29
+ class Strip
30
+ attr_accessor :direction, :last_event, :state, :current_set, :debug
31
+
32
+ LENGTH = 160
33
+ TYPE = :double
34
+
35
+ DIRECTION_NONE = 0
36
+ DIRECTION_LEFT = 1
37
+ DIRECTION_RIGHT = 2
38
+ TIMEOUT = 12
39
+
40
+ STATE_OFF = :state_off
41
+ STATE_ON = :state_on
42
+ STATE_STARTING_UP = :state_starting_up
43
+ STATE_SHUTTING_DOWN = :state_shutting_down
44
+
45
+ WEATHER_URL = 'http://api.openweathermap.org/data/2.5/weather?q=Hannover,de'
46
+
47
+ FRAMES_PER_SECOND = 25
48
+
49
+ def initialize
50
+ WS2801.length(Strip::TYPE == :double ? Strip::LENGTH * 2 : Strip::LENGTH)
51
+ WS2801.autowrite(true)
52
+ update_daylight
53
+ self_test
54
+ @listen_thread = Thread.new { while true do check_timer; sleep 0.5; end }
55
+ @last_event = Time.now - 3600 # set last event to a longer time ago
56
+ @state = STATE_OFF
57
+ @debug = false
58
+ @current_set = Set::ColorSet.new
59
+ @current_set.color = Color.new(0,0,0)
60
+ end
61
+
62
+ def on(direction)
63
+ @last_event = Time.now
64
+ puts "triggered event 'on': #{last_event.to_f} from state #{@state}" if @debug
65
+ @state = STATE_STARTING_UP if @state == STATE_SHUTTING_DOWN
66
+ return if @state != STATE_OFF
67
+
68
+ puts 'Loading a new set...' if @debug
69
+
70
+ @direction = direction
71
+ @state = STATE_STARTING_UP
72
+
73
+ case rand(100)
74
+ when 0..3
75
+ set = Set::RainbowSet.new
76
+ when 4..6
77
+ set = Set::RandomSet.new
78
+ when 7..9
79
+ set = Set::StrawberrySet.new
80
+ when 10..12
81
+ set = Set::WatermelonSet.new
82
+ when 13..15
83
+ set = Set::SemolinaSet.new
84
+ else
85
+ set = Set::GradientSet.new
86
+ set.color_from = Color.random_from_set
87
+ set.color_to = Color.random_from_set
88
+ end
89
+
90
+ set = Set::StarSet.new if night?
91
+
92
+ puts "Set #{set.class}" if @debug
93
+
94
+ animation = animation_for(direction).new(@current_set, set)
95
+
96
+ animate(animation)
97
+ @current_set = set
98
+
99
+ @state = STATE_ON
100
+
101
+ # Move show() into background, so we can accept new events on the main thread
102
+ Thread.new { show(@current_set, animation.frames) }
103
+ end
104
+
105
+ def off(direction = nil)
106
+ puts "triggered event 'off': #{Time.now.to_f} during state #{@state}" if @debug
107
+ return if @state != STATE_ON
108
+
109
+ @state = STATE_SHUTTING_DOWN
110
+ sleep 0.2
111
+ @direction = direction if direction
112
+
113
+ set = Set::ColorSet.new
114
+ set.color = Color.by_name :black
115
+
116
+ animation = animation_for(@direction).new(@current_set, set)
117
+
118
+ if animate(animation)
119
+ @state = STATE_OFF
120
+ @current_set = set
121
+ else
122
+ @state = STATE_ON
123
+ Thread.new { show(@current_set, animation.frames) }
124
+ end
125
+
126
+ puts "finished shutting off: #{Time.now.to_f}" if @debug
127
+ end
128
+
129
+ def animation_for(direction)
130
+ return Animation::FadeAnimation if night?
131
+
132
+ if direction == DIRECTION_LEFT
133
+ Animation::SlideLeftAnimation
134
+ else
135
+ Animation::SlideRightAnimation
136
+ end
137
+ end
138
+
139
+ def animate(animation)
140
+ current_frame = 0
141
+ beginning_state = @state
142
+
143
+ animation.frames.times do |i|
144
+ WS2801.strip(animation.frame_data(current_frame = i))
145
+ WS2801.write
146
+ sleep (1.0/animation.frames_per_second) if animation.frames_per_second
147
+ break if @state != beginning_state # Reverse shutting off when a new event is triggered
148
+ end
149
+
150
+ # This is run when the animation is reversed
151
+ if (current_frame + 1) < animation.frames
152
+ current_frame.times do |i|
153
+ WS2801.strip(animation.frame_data(current_frame - i - 1))
154
+ WS2801.write
155
+ sleep (1.0/animation.frames_per_second) if animation.frames_per_second
156
+ end
157
+ false
158
+ else
159
+ true
160
+ end
161
+ end
162
+
163
+ def show(set, start_frame = 0)
164
+ current_state = @state
165
+ i = start_frame
166
+ while @state == current_state
167
+ WS2801.strip(set.frame_data)
168
+ WS2801.write
169
+ sleep 1.0/FRAMES_PER_SECOND.to_f
170
+ i += 1
171
+ end
172
+ end
173
+
174
+ def night?
175
+ time = Time.now
176
+ time.to_i < (@daylight[:start] - 3600) || time.to_i > (@daylight[:end] + 3600)
177
+ end
178
+
179
+ # Gets the sunset/sunrise data
180
+ # this might get out of sync when the clock is not set correctly
181
+ # anyway, one day off is not a problem :)
182
+ def update_daylight
183
+ data = JSON.parse(open(WEATHER_URL).read)
184
+ @daylight = {
185
+ start: data['sys']['sunrise'].to_i,
186
+ end: data['sys']['sunset'].to_i,
187
+ day: Time.now.day
188
+ }
189
+ pp @daylight
190
+ end
191
+
192
+ def shutdown
193
+ WS2801.set(r: 0, g: 0, b: 0)
194
+ end
195
+
196
+ def self_test
197
+ WS2801.set(r: 0, g: 0, b: 255)
198
+ sleep 1
199
+ WS2801.set(r: 0, g: 255, b: 0)
200
+ sleep 1
201
+ WS2801.set(r: 255, g: 0, b: 0)
202
+ sleep 1
203
+ WS2801.set(r: 0, g: 0, b: 0)
204
+ end
205
+
206
+ def check_timer
207
+ WS2801.set(r: 0, g: 0, b: 0) if @state == STATE_OFF
208
+ # Test after 2 a.m. to make sure we can the correct date even if our time is slightly off
209
+ time = Time.now
210
+ update_daylight if @daylight[:day] != time.day && time.hour > 1
211
+ off if timeout?
212
+ end
213
+
214
+ def timeout?
215
+ @last_event < (Time.now - TIMEOUT)
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,4 @@
1
+ # Provides a global version number
2
+ module WSLight
3
+ VERSION = '0.2.0'
4
+ end
data/ws_light.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ws_light/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ws_light'
8
+ spec.version = WSLight::VERSION
9
+ spec.authors = ['Gerrit Visscher']
10
+ spec.email = ['g.visscher@core4.de']
11
+ spec.summary = 'A lighting gem for WS2801 led strips.'
12
+ spec.description = 'Controls one or two WS2801 led strips with a Raspberry Pi or another computer with an SPI interface.'
13
+ spec.homepage = 'https://github.com/kayssun/ws_light'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+
23
+ spec.add_runtime_dependency 'pi_piper', '~>1'
24
+ spec.add_runtime_dependency 'ws2801', '~>1'
25
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ws_light
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Gerrit Visscher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pi_piper
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ws2801
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ description: Controls one or two WS2801 led strips with a Raspberry Pi or another
56
+ computer with an SPI interface.
57
+ email:
58
+ - g.visscher@core4.de
59
+ executables:
60
+ - ws_light
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - CODE_OF_CONDUCT.md
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - bin/ws_light
70
+ - lib/ws_light/animation/base_animation.rb
71
+ - lib/ws_light/animation/fade_animation.rb
72
+ - lib/ws_light/animation/slide_left_animation.rb
73
+ - lib/ws_light/animation/slide_right_animation.rb
74
+ - lib/ws_light/benchmark/animation_benchmark.rb
75
+ - lib/ws_light/benchmark/set_benchmark.rb
76
+ - lib/ws_light/benchmark/ws2801_benchmark.rb
77
+ - lib/ws_light/color.rb
78
+ - lib/ws_light/sd_logger.rb
79
+ - lib/ws_light/set/color_set.rb
80
+ - lib/ws_light/set/gradient_set.rb
81
+ - lib/ws_light/set/rainbow_set.rb
82
+ - lib/ws_light/set/random_set.rb
83
+ - lib/ws_light/set/semolina_set.rb
84
+ - lib/ws_light/set/star_set.rb
85
+ - lib/ws_light/set/strawberry_set.rb
86
+ - lib/ws_light/set/watermelon_set.rb
87
+ - lib/ws_light/strip.rb
88
+ - lib/ws_light/version.rb
89
+ - ws_light.gemspec
90
+ homepage: https://github.com/kayssun/ws_light
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.4.5
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: A lighting gem for WS2801 led strips.
114
+ test_files: []