ws2812 0.0.2

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.
@@ -0,0 +1,69 @@
1
+ /*
2
+ * ws2811.h
3
+ *
4
+ * Copyright (c) 2014 Jeremy Garff <jer @ jers.net>
5
+ *
6
+ * All rights reserved.
7
+ *
8
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
9
+ * provided that the following conditions are met:
10
+ *
11
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
12
+ * conditions and the following disclaimer.
13
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list
14
+ * of conditions and the following disclaimer in the documentation and/or other materials
15
+ * provided with the distribution.
16
+ * 3. Neither the name of the owner nor the names of its contributors may be used to endorse
17
+ * or promote products derived from this software without specific prior written permission.
18
+ *
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
20
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
22
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
24
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
26
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ *
28
+ */
29
+
30
+
31
+ #ifndef __WS2811_H__
32
+ #define __WS2811_H__
33
+
34
+ #include "pwm.h"
35
+
36
+
37
+ #define WS2811_TARGET_FREQ 800000 // Can go as low as 400000
38
+
39
+ struct ws2811_device;
40
+
41
+ typedef uint32_t ws2811_led_t; //< 0x00RRGGBB
42
+ typedef struct
43
+ {
44
+ int gpionum; //< GPIO Pin with PWM alternate function, 0 if unused
45
+ int invert; //< Invert output signal
46
+ int count; //< Number of LEDs, 0 if channel is unused
47
+ int brightness; //< Brightness value between 0 and 255
48
+ ws2811_led_t *leds; //< LED buffers, allocated by driver based on count
49
+ } ws2811_channel_t;
50
+
51
+ typedef struct
52
+ {
53
+ struct ws2811_device *device; //< Private data for driver use
54
+ uint32_t freq; //< Required output frequency
55
+ int dmanum; //< DMA number _not_ already in use
56
+ ws2811_channel_t channel[RPI_PWM_CHANNELS];
57
+ } ws2811_t;
58
+
59
+
60
+ int ws2811_init(ws2811_t *ws2811); //< Initialize buffers/hardware
61
+ void ws2811_fini(ws2811_t *ws2811); //< Tear it all down
62
+ extern uint8_t ws2811_direct_colors; //< Set to non-zero to bypass "brightness" [gamma correction] altogether (default is 0)
63
+ int ws2811_render(ws2811_t *ws2811); //< Send LEDs off to hardware
64
+ extern uint32_t ws2811_dma_error; //< DMA errors from ws2811_wait go here
65
+ int ws2811_wait(ws2811_t *ws2811); //< Wait for DMA completion
66
+
67
+
68
+ #endif /* __WS2811_H__ */
69
+
data/lib/ws2812.rb ADDED
@@ -0,0 +1,20 @@
1
+ module Ws2812
2
+ VERSION = "0.0.2"
3
+ end
4
+
5
+ # to make it all less confusing
6
+ WS2812 = Ws2812
7
+
8
+ if $__WS2812_SKIP_LL
9
+ module Ws2812
10
+ module Lowlevel
11
+ # XXX: could provide full blown emulation of all calls? :)
12
+ end
13
+ end
14
+ else
15
+ require 'ws2812/lowlevel'
16
+ end
17
+ require 'ws2812/color'
18
+ require 'ws2812/basic'
19
+ require 'ws2812/unicornhat'
20
+ require 'ws2812/gamma_correction'
@@ -0,0 +1,195 @@
1
+ module Ws2812
2
+ ##
3
+ # Provides basic interface for so called NeoPixels (ws2812 RGB LED
4
+ # chips)
5
+ #
6
+ # This library internally uses C (and SWIG) extension from Richard Hirst's
7
+ # version of Jeremy Garff's rpi_ws281x library.
8
+ #
9
+ # And this particular class is heavily inspired by the included
10
+ # <tt>neopixel.py</tt> Python class within these projects.
11
+ #
12
+ # See:
13
+ # [jgarff] https://github.com/jgarff/rpi_ws281x
14
+ # [richardghirst] https://github.com/richardghirst/rpi_ws281x
15
+ # [pimoroni] https://github.com/pimoroni/unicorn-hat/tree/master/python/rpi-ws281x
16
+ #
17
+ #
18
+ class Basic
19
+ include Ws2812::Lowlevel
20
+
21
+ ##
22
+ # Initializes the basic ws2812 driver for +num+ leds at given +pin+,
23
+ # with initial +brightness+ (0..255)
24
+ #
25
+ # The +options+ hash can contain various additional options
26
+ # (all keys as symbols):
27
+ #
28
+ # [freq] frequency (Hz) to communicate at, defaults to 800_000
29
+ # [dma] dma channel to use, defaults to 5
30
+ # [invert] use inverted logic, defaults to false
31
+ # [channel] which channel to use, defaults to 0 (permissible 0, 1)
32
+ def initialize(num, pin, brightness = 50, options = {})
33
+
34
+ freq = options.fetch(:freq) { 800_000 }
35
+ dma = options.fetch(:dma) { 5 }
36
+ invert = options.fetch(:invert) { false }
37
+ channel = options.fetch(:channel) { 0 }
38
+
39
+ @leds = Ws2811_t.new
40
+ @leds.freq = freq
41
+ @leds.dmanum = dma
42
+
43
+ @channel = ws2811_channel_get(@leds, channel)
44
+ @channel.count = num
45
+ @channel.gpionum = pin
46
+ @channel.invert = invert ? 1 : 0
47
+ @channel.brightness = brightness
48
+
49
+ @count = num
50
+
51
+ at_exit { self.close }
52
+
53
+ @open = false
54
+ end
55
+
56
+ ##
57
+ # Is gamma correction (brightness) bypassed?
58
+ def direct?
59
+ !Ws2812::Lowlevel.ws2811_direct_colors.zero?
60
+ end
61
+
62
+ ##
63
+ # Instruct lowlevel driver to bypass gamma correction (brightness)
64
+ # and use the color values straight as they are given
65
+ def direct=(value)
66
+ Ws2812::Lowlevel.ws2811_direct_colors = !!value ? 1 : 0
67
+ end
68
+
69
+ ##
70
+ # Actually opens (initializes) communication with the LED strand
71
+ #
72
+ # Raises an exception when the initialization fails.
73
+ #
74
+ # Failure is usually because you don't have root permissions
75
+ # which are needed to access /dev/mem and to create special
76
+ # devices.
77
+ def open
78
+ return nil if @open
79
+ resp = ws2811_init(@leds)
80
+ fail "init failed with code: " + resp.to_s + ", perhaps you need to run as root?" unless resp.zero?
81
+ @open = true
82
+ self
83
+ end
84
+
85
+ ##
86
+ # Closes (deinitializes) communication with the LED strand
87
+ #
88
+ # Can be called on already closed device just fine
89
+ def close
90
+ if @open && @leds
91
+ @open = false
92
+ ws2811_fini(@leds)
93
+ @channel = nil # will GC the memory
94
+ @leds = nil
95
+ # Note: ws2811_fini will free mem used by led_data internally
96
+ end
97
+ self
98
+ end
99
+
100
+
101
+ ##
102
+ # Apply all changes since last show
103
+ #
104
+ # This method renders all changes (brightness, pixels) done to the
105
+ # strand since last show
106
+ def show
107
+ resp = ws2811_render(@leds)
108
+ fail "show failed with code: " + resp.to_s unless resp.zero?
109
+ end
110
+
111
+ ##
112
+ # Set given pixel identified by +index+ to +color+
113
+ #
114
+ # See +set+ for a method that takes individual +r+, +g+, +b+
115
+ # components
116
+ def []=(index, color)
117
+ if index.respond_to?(:to_a)
118
+ index.to_a.each do |i|
119
+ check_index(i)
120
+ ws2811_led_set(@channel, i, color.to_i)
121
+ end
122
+ else
123
+ check_index(index)
124
+ ws2811_led_set(@channel, index, color.to_i)
125
+ end
126
+ end
127
+
128
+ ##
129
+ # Set given pixel identified by +index+ to +r+, +g+, +b+
130
+ #
131
+ # See <tt>[]=</tt> for a method that takes +Color+ instance instead
132
+ # of individual components
133
+ def set(index, r, g, b)
134
+ check_index(index)
135
+ self[index] = Color.new(r, g, b)
136
+ end
137
+
138
+ ##
139
+ # Set brightness used for all pixels
140
+ #
141
+ # The value is from +0+ to +255+ and is internally used as a scaler
142
+ # for all colors values that are supplied via <tt>[]=</tt>
143
+ def brightness=(val)
144
+ @channel.brightness = val
145
+ end
146
+
147
+ ##
148
+ # Return brightness used for all pixels
149
+ #
150
+ # The value is from +0+ to +255+ and is internally used as a scaler
151
+ # for all colors values that are supplied via <tt>[]=</tt>
152
+ def brightness
153
+ @channel.brightness
154
+ end
155
+
156
+ ##
157
+ # Number of leds it's initialized for
158
+ #
159
+ # Method actually passes to low-level implementation for this
160
+ # value; it doesn't use the parameter passed during construction
161
+ def count
162
+ @channel.count
163
+ end
164
+
165
+ ##
166
+ # Return +Color+ of led located at given index
167
+ #
168
+ # Indexed from 0 upto <tt>#count - 1</tt>
169
+ def [](index)
170
+ if index.respond_to?(:to_a)
171
+ index.to_a.map do |i|
172
+ check_index(i)
173
+ Color.from_i(ws2811_led_get(@channel, i))
174
+ end
175
+ else
176
+ check_index(index)
177
+ Color.from_i(ws2811_led_get(@channel, index))
178
+ end
179
+ end
180
+
181
+ ##
182
+ # Verify supplied index
183
+ #
184
+ # Raises ArgumentError if the supplied index is invalid
185
+ # (doesn't address configured pixel)
186
+ def check_index(index)
187
+ if 0 <= index && index < @count
188
+ true
189
+ else
190
+ fail ArgumentError, "index #{index} outside of permitted range [0..#{count})"
191
+ end
192
+ end
193
+ private :check_index
194
+ end
195
+ end
@@ -0,0 +1,33 @@
1
+ module Ws2812
2
+ ##
3
+ # Simple wrapper class around RGB based color
4
+ class Color
5
+ def initialize(r, g, b)
6
+ @r, @g, @b = r, g, b
7
+ end
8
+ attr_accessor :r, :g, :b
9
+
10
+ ##
11
+ # Converts color to integer by encoding +r+, +g+, +b+
12
+ # as 8bit values (in this order)
13
+ #
14
+ # Thus <tt>Color.new(1,2,3).to_i # => 66051</tt>
15
+ def to_i
16
+ ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff)
17
+ end
18
+
19
+ ##
20
+ # Converts color from integer +i+ by taking the least significant
21
+ # 24 bits and using them for r(8), g(8), b(8); in this order.
22
+ def self.from_i(i)
23
+ Color.new((i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff)
24
+ end
25
+
26
+
27
+ # Makes sense to represent color as hex, right?
28
+ def to_hex
29
+ "#%02x%02x%02x" % [r & 0xff, g & 0xff, b & 0xff]
30
+ end
31
+ alias_method :to_s, :to_hex
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ module Ws2812
2
+ ##
3
+ # This class implements (on Ruby level) the gamma correction that is
4
+ # internally used as part of the +brightness+ setup.
5
+ #
6
+ # It's especially useful if you want to mess around with pixel brightness
7
+ # in <em>direct mode</em>. See the <em>digiclock</em> example for more info.
8
+ class GammaCorrection
9
+ # correction table thanks to http://rgb-123.com/ws2812-color-output/
10
+ GAMMA_TABLE = [
11
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
12
+ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2,
13
+ 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
14
+ 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11,
15
+ 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18,
16
+ 19, 19, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28,
17
+ 29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40,
18
+ 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54,
19
+ 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
20
+ 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 88, 89,
21
+ 90, 91, 93, 94, 95, 96, 98, 99,100,102,103,104,106,107,109,110,
22
+ 111,113,114,116,117,119,120,121,123,124,126,128,129,131,132,134,
23
+ 135,137,138,140,142,143,145,146,148,150,151,153,155,157,158,160,
24
+ 162,163,165,167,169,170,172,174,176,178,179,181,183,185,187,189,
25
+ 191,193,194,196,198,200,202,204,206,208,210,212,214,216,218,220,
26
+ 222,224,227,229,231,233,235,237,239,241,244,246,248,250,252,255
27
+ ]
28
+
29
+ def initialize(brightness)
30
+ self.brightness = brightness
31
+ end
32
+
33
+ def brightness=(value)
34
+ raise ArgumentError, "brightness should be within (0..255)" unless (0..255).include?(value.to_i)
35
+ @brightness = value.to_i
36
+ end
37
+ attr_reader :brightness
38
+
39
+ def correct(value)
40
+ if value.kind_of?(Color)
41
+ Color.new(
42
+ self.correct(value.r),
43
+ self.correct(value.g),
44
+ self.correct(value.b))
45
+ else
46
+ raise ArgumentError, "value should be within (0..255)" unless (0..255).include?(value.to_i)
47
+ (GAMMA_TABLE[value.to_i] * (@brightness + 1)) >> 8
48
+ end
49
+ end
50
+
51
+ def correct_with_min_max(value, min = nil, max = nil)
52
+ trim(correct(value), min, max)
53
+ end
54
+
55
+ def trim(value, min = nil, max = nil)
56
+ if value.kind_of?(Color)
57
+ Color.new(
58
+ self.trim(value.r, min && min.r, max && max.r),
59
+ self.trim(value.g, min && min.g, max && max.g),
60
+ self.trim(value.b, min && min.b, max && max.b))
61
+ else
62
+ if min && value.to_i < min.to_i
63
+ min.to_i
64
+ elsif max && value.to_i > max.to_i
65
+ max.to_i
66
+ else
67
+ value.to_i
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,209 @@
1
+ module Ws2812
2
+ ##
3
+ # Provides interface for *Unicorn HAT* (a 8x8 ws2812 matrix by Pimoroni)
4
+ #
5
+ # This particular class is heavily inspired by the <tt>unicornhat.py</tt>
6
+ # class from UnicornHat's repo.
7
+ #
8
+ # See:
9
+ # [unicornhat] https://github.com/pimoroni/unicorn-hat
10
+ #
11
+ class UnicornHAT
12
+ UHAT_MAP = [
13
+ [7 ,6 ,5 ,4 ,3 ,2 ,1 ,0 ],
14
+ [8 ,9 ,10,11,12,13,14,15],
15
+ [23,22,21,20,19,18,17,16],
16
+ [24,25,26,27,28,29,30,31],
17
+ [39,38,37,36,35,34,33,32],
18
+ [40,41,42,43,44,45,46,47],
19
+ [55,54,53,52,51,50,49,48],
20
+ [56,57,58,59,60,61,62,63]
21
+ ]
22
+ ##
23
+ # Initializes the matrix ws2812 driver at given +pin+,
24
+ # with initial +brightness+ (0..255)
25
+ #
26
+ # The +options+ hash can contain various additional options,
27
+ # see +Basic+ class' description of options for more details.
28
+ def initialize(pin = 18, brightness = 50, options = {})
29
+ @hat = Basic.new(64, pin, brightness, options)
30
+ @hat.open
31
+
32
+ @rotation = 0
33
+ @pixels = Array.new(8) { |x| Array.new(8) { |y| Color.new(0,0,0) } }
34
+ push_all_pixels
35
+ rescue Object
36
+ @hat = nil
37
+ raise
38
+ end
39
+
40
+ # Query direct mode. See <em>Ws2812::Basic</em> for explanation.
41
+ def direct?
42
+ @hat.direct?
43
+ end
44
+
45
+ ##
46
+ # Set direct mode. See <em>Ws2812::Basic</em> for explanation.
47
+ def direct=(value)
48
+ @hat.direct = value
49
+ end
50
+
51
+ ##
52
+ # Closes (deinitializes) communication with the matrix
53
+ #
54
+ # Can be called on already closed device just fine
55
+ def close
56
+ if @hat
57
+ @hat.close
58
+ @hat = nil
59
+ end
60
+ self
61
+ end
62
+
63
+ ##
64
+ # Apply all changes since last show
65
+ #
66
+ # This method renders all changes (brightness, pixels, rotation)
67
+ # done to the strand since last show
68
+ def show
69
+ @hat.show
70
+ end
71
+
72
+ ##
73
+ # Set given pixel identified by +x+, +y+ to +color+
74
+ #
75
+ # See +set+ for a method that takes individual +r+, +g+, +b+
76
+ # components.
77
+ #
78
+ # You still have to call +show+ to make the changes visible.
79
+ def []=(x, y, color)
80
+ check_coords(x, y)
81
+ @pixels[x][y] = color
82
+ @hat[map_coords(x, y)] = color
83
+ end
84
+
85
+ ##
86
+ # Set given pixel identified by +x+, +y+ to +r+, +g+, +b+
87
+ #
88
+ # See <tt>[]=</tt> for a method that takes +Color+ instance instead
89
+ # of individual components.
90
+ #
91
+ # You still have to call +show+ to make the changes visible.
92
+ def set(x, y, r, g, b)
93
+ check_coords(x, y)
94
+ self[x, y] = Color.new(r, g, b)
95
+ end
96
+
97
+ ##
98
+ # Set brightness used for all pixels
99
+ #
100
+ # The value is from +0+ to +255+ and is internally used as a scaler
101
+ # for all colors values that are supplied via <tt>[]=</tt>
102
+ #
103
+ # You still have to call +show+ to make the changes visible.
104
+ def brightness=(val)
105
+ @hat.brightness = val
106
+ end
107
+
108
+ ##
109
+ # Return brightness used for all pixels
110
+ #
111
+ # The value is from +0+ to +255+ and is internally used as a scaler
112
+ # for all colors values that are supplied via <tt>[]=</tt>
113
+ def brightness
114
+ @hat.brightness
115
+ end
116
+
117
+ ##
118
+ # Return +Color+ of led located at given +x+, +y+
119
+ def [](x, y)
120
+ @pixels[x][y]
121
+ end
122
+
123
+ ##
124
+ # Returns current rotation as integer; one of: [0, 90, 180, 270]
125
+ def rotation
126
+ @rotation
127
+ end
128
+
129
+ ##
130
+ # Set rotation of the Unicorn HAT to +val+
131
+ #
132
+ # Permissible values for rotation are [0, 90, 180, 270] (mod 360).
133
+ #
134
+ # You still have to call +show+ to make the changes visible.
135
+ def rotation=(val)
136
+ permissible = [0, 90, 180, 270]
137
+ fail ArgumentError, "invalid rotation, permissible: #{permissible.join(', ')}" unless permissible.include?(val % 360)
138
+ @rotation = val % 360
139
+ push_all_pixels
140
+ end
141
+
142
+ ##
143
+ # Clears all pixels (sets them to black) and calls +show+ if +do_show+
144
+ def clear(do_show = true)
145
+ set_all(Color.new(0, 0, 0))
146
+ show if do_show
147
+ end
148
+
149
+ ##
150
+ # Sets all pixels to +color+
151
+ #
152
+ # You still have to call +show+ to make the changes visible.
153
+ def set_all(color)
154
+ 0.upto(7) do |x|
155
+ 0.upto(7) do |y|
156
+ self[x, y] = color
157
+ end
158
+ end
159
+ end
160
+
161
+ ##
162
+ # Pushes all pixels from buffer to the lower level (physical device)
163
+ #
164
+ # This is internally used when changing rotation but it can be useful
165
+ # when you set several pixels to the same Color instance and then
166
+ # manipulate those pixels' color all at once.
167
+ def push_all_pixels
168
+ 0.upto(7) do |x|
169
+ 0.upto(7) do |y|
170
+ @hat[map_coords(x, y)] = @pixels[x][y]
171
+ end
172
+ end
173
+ end
174
+
175
+
176
+ ##
177
+ # Maps +x+, +y+ coordinates to index on the physical matrix
178
+ # (takes rotation into account)
179
+ def map_coords(x, y)
180
+ check_coords(x, y)
181
+ y = 7 - y
182
+ case rotation
183
+ when 90
184
+ x, y = y, 7 - x
185
+ when 180
186
+ x, y = 7 - x, 7 - y
187
+ when 270
188
+ x, y = 7 - y, x
189
+ end
190
+
191
+ UHAT_MAP[x][y]
192
+ end
193
+ private :map_coords
194
+
195
+ ##
196
+ # Verify supplied coords +x+ and +y+
197
+ #
198
+ # Raises ArgumentError if the supplied coords are invalid
199
+ # (doesn't address configured pixel)
200
+ def check_coords(x, y)
201
+ if 0 <= x && x < 8 && 0 <= y && y < 8
202
+ true
203
+ else
204
+ fail ArgumentError, "coord (#{x},#{y}) outside of permitted range ((0..7), (0..7))"
205
+ end
206
+ end
207
+ private :check_coords
208
+ end
209
+ end