1
- # Simple NeoPixel light painter for CPX. Single image filename and speed are set
2
- # in code, there's no interface for selecting items/speed/triggering etc.
1
+ """Circuit Playground Express Light Paintbrush"""
2
+ # Single images only. Filename and speed are set in code,
3
+ # images should be 30px high, up to 100px wide, 24-bit .bmp files
3
4
4
5
import gc
5
6
import time
10
11
11
12
# uncomment one line only here to select bitmap
12
13
FILENAME = "bats.bmp" # BMP file to load from flash filesystem
13
- # FILENAME = "digikey .bmp"
14
- # FILENAME = "burger .bmp"
15
- # FILENAME = "afbanner .bmp"
16
- # FILENAME = "blinka .bmp"
17
- # FILENAME = "ghost04 .bmp"
18
- # FILENAME = "ghost07 .bmp"
19
- # FILENAME = "ghost02 .bmp"
20
- # FILENAME = "helix-32x30 .bmp"
21
- # FILENAME = "wales2-107x30 .bmp"
22
- # FILENAME = "pumpkin .bmp"
23
- # FILENAME = "rainbow .bmp"
24
- # FILENAME = "rainbowRoad .bmp"
25
- # FILENAME = "rainbowZig .bmp"
26
- # FILENAME = "skull .bmp"
27
- # FILENAME = "adabot .bmp"
28
- # FILENAME = "green_stripes .bmp"
29
- # FILENAME = "red_blue .bmp"
30
- # FILENAME = "minerva.bmp"
31
-
32
- TOUCH = touchio . TouchIn ( board . A5 ) # Rightmost capacitive touch pad
33
- BRIGHTNESS = 1.0 # NeoPixel brightness 0.0 (min) to 1.0 (max)
14
+ #FILENAME = "jpw01 .bmp"
15
+ #FILENAME = "digikey .bmp"
16
+ #FILENAME = "burger .bmp"
17
+ #FILENAME = "afbanner .bmp"
18
+ #FILENAME = "blinka .bmp"
19
+ #FILENAME = "ghost .bmp"
20
+ #FILENAME = "helix-32x30 .bmp"
21
+ #FILENAME = "wales2-107x30 .bmp"
22
+ #FILENAME = "pumpkin .bmp"
23
+ #FILENAME = "rainbow .bmp"
24
+ #FILENAME = "rainbowRoad .bmp"
25
+ #FILENAME = "rainbowZig .bmp"
26
+ #FILENAME = "skull .bmp"
27
+ #FILENAME = "adabot .bmp"
28
+ #FILENAME = "green_stripes .bmp"
29
+ #FILENAME = "red_blue .bmp"
30
+ #FILENAME = "minerva .bmp"
31
+
32
+ TOUCH = touchio . TouchIn ( board . A5 ) # capacitive touch pad
33
+ SPEED = 50000
34
+ BRIGHTNESS = 1.0 # Set brightness here, NOT in NeoPixel constructor
34
35
GAMMA = 2.7 # Adjusts perceived brighthess linearity
35
36
NUM_PIXELS = 30 # NeoPixel strip length (in pixels)
36
- SPEED = 20000 # adjust this to change the playback speed e.g. 50000 is slow
37
- LOOP = False # set to True for looping
38
- # Switch off onboard NeoPixel...
39
- NEOPIXEL_PIN = digitalio .DigitalInOut (board .NEOPIXEL )
40
- NEOPIXEL_PIN .direction = digitalio .Direction .OUTPUT
41
- neopixel_write (NEOPIXEL_PIN , bytearray (3 ))
42
- # ...then assign NEOPIXEL_PIN to the external NeoPixel connector:
43
- NEOPIXEL_PIN = digitalio .DigitalInOut (board .A1 )
37
+ NEOPIXEL_PIN = board .A1 # Pin where NeoPixels are connected
38
+ DELAY_TIME = 0.01 # Timer delay before it starts
39
+ LOOP = False # Set to True for looping
40
+
41
+ # Enable NeoPixel pin as output and clear the strip
42
+ NEOPIXEL_PIN = digitalio .DigitalInOut (NEOPIXEL_PIN )
44
43
NEOPIXEL_PIN .direction = digitalio .Direction .OUTPUT
45
44
neopixel_write (NEOPIXEL_PIN , bytearray (NUM_PIXELS * 3 ))
46
45
47
- # Interpret multi-byte value from file as little-endian value
48
46
def read_le (value ):
47
+ """Interpret multi-byte value from file as little-endian value"""
49
48
result = 0
50
49
shift = 0
51
50
for byte in value :
@@ -54,72 +53,69 @@ def read_le(value):
54
53
return result
55
54
56
55
class BMPError (Exception ):
56
+ """Error handler for BMP-loading function"""
57
57
pass
58
58
59
59
def load_bmp (filename ):
60
+ """Load BMP file, return as list of column buffers"""
61
+ # pylint: disable=too-many-locals, too-many-branches
60
62
try :
61
63
print ("Loading" , filename )
62
- with open ("/" + filename , "rb" ) as f :
64
+ with open ("/" + filename , "rb" ) as bmp :
63
65
print ("File opened" )
64
- if f .read (2 ) != b'BM' : # check signature
66
+ if bmp .read (2 ) != b'BM' : # check signature
65
67
raise BMPError ("Not BitMap file" )
66
68
67
- f .read (4 ) # Read & ignore file size
68
- f .read (4 ) # Read & ignore creator bytes
69
+ bmp .read (8 ) # Read & ignore file size and creator bytes
69
70
70
- bmpImageoffset = read_le (f .read (4 )) # Start of image data
71
- f .read (4 ) # Read & ignore header size
72
- bmpWidth = read_le (f .read (4 ))
73
- bmpHeight = read_le (f .read (4 ))
71
+ bmp_image_offset = read_le (bmp .read (4 )) # Start of image data
72
+ bmp .read (4 ) # Read & ignore header size
73
+ bmp_width = read_le (bmp .read (4 ))
74
+ bmp_height = read_le (bmp .read (4 ))
74
75
# BMPs are traditionally stored bottom-to-top.
75
- # If bmpHeight is negative, image is in top-down order.
76
+ # If bmp_height is negative, image is in top-down order.
76
77
# This is not BMP canon but has been observed in the wild!
77
78
flip = True
78
- if bmpHeight < 0 :
79
- bmpHeight = - bmpHeight
79
+ if bmp_height < 0 :
80
+ bmp_height = - bmp_height
80
81
flip = False
81
82
82
- print ("WxH: (%d,%d)" % (bmpWidth , bmpHeight ))
83
+ print ("WxH: (%d,%d)" % (bmp_width , bmp_height ))
83
84
84
- if read_le (f .read (2 )) != 1 :
85
+ if read_le (bmp .read (2 )) != 1 :
85
86
raise BMPError ("Not single-plane" )
86
- bmpDepth = read_le (f .read (2 )) # bits per pixel
87
- # print("Bit depth: %d" % (bmpDepth))
88
- if bmpDepth != 24 :
87
+ if read_le (bmp .read (2 )) != 24 : # bits per pixel
89
88
raise BMPError ("Not 24-bit" )
90
- if read_le (f .read (2 )) != 0 :
89
+ if read_le (bmp .read (2 )) != 0 :
91
90
raise BMPError ("Compressed file" )
92
91
93
92
print ("Image format OK, reading data..." )
94
93
95
- rowSize = (bmpWidth * 3 + 3 ) & ~ 3 # 32-bit line boundary
94
+ row_size = (bmp_width * 3 + 3 ) & ~ 3 # 32-bit line boundary
96
95
97
96
# Constrain rows loaded to pixel strip length
98
- clippedHeight = bmpHeight
99
- if clippedHeight > NUM_PIXELS :
100
- clippedHeight = NUM_PIXELS
97
+ clipped_height = min (bmp_height , NUM_PIXELS )
101
98
102
99
# Allocate per-column pixel buffers, sized for NeoPixel strip:
103
- columns = [bytearray (NUM_PIXELS * 3 ) for i in range (bmpWidth )]
100
+ columns = [bytearray (NUM_PIXELS * 3 ) for _ in range (bmp_width )]
104
101
105
102
# Image is displayed at END (not start) of NeoPixel strip,
106
103
# this index works incrementally backward in column buffers...
107
104
idx = (NUM_PIXELS - 1 ) * 3
108
- for row in range (clippedHeight ): # For each scanline...
105
+ for row in range (clipped_height ): # For each scanline...
109
106
if flip : # Bitmap is stored bottom-to-top order (normal BMP)
110
- pos = bmpImageoffset + (bmpHeight - 1 - row ) * rowSize
107
+ pos = bmp_image_offset + (bmp_height - 1 - row ) * row_size
111
108
else : # Bitmap is stored top-to-bottom
112
- pos = bmpImageoffset + row * rowSize
113
- f .seek (pos ) # Start of scanline
114
- for c in columns : # For each pixel of scanline...
109
+ pos = bmp_image_offset + row * row_size
110
+ bmp .seek (pos ) # Start of scanline
111
+ for column in columns : # For each pixel of scanline...
115
112
# BMP files use BGR color order
116
- # blue, green, red = bytearray(f.read(3))
117
- blue , green , red = f .read (3 )
113
+ blue , green , red = bmp .read (3 )
118
114
# Rearrange into NeoPixel strip's color order,
119
115
# while handling brightness & gamma correction:
120
- c [idx ] = int (pow (green / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
121
- c [idx + 1 ] = int (pow (red / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
122
- c [idx + 2 ] = int (pow (blue / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
116
+ column [idx ] = int (pow (green / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
117
+ column [idx + 1 ] = int (pow (red / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
118
+ column [idx + 2 ] = int (pow (blue / 255 , GAMMA ) * BRIGHTNESS * 255 + 0.5 )
123
119
idx -= 3 # Advance (back) one pixel
124
120
125
121
# Add one more column with no color data loaded. This is used
@@ -131,40 +127,39 @@ def load_bmp(filename):
131
127
gc .collect () # Garbage-collect now so playback is smoother
132
128
return columns
133
129
134
- except OSError as e :
135
- if e .args [0 ] == 28 :
130
+ except OSError as err :
131
+ if err .args [0 ] == 28 :
136
132
raise OSError ("OS Error 28 0.25" )
137
133
else :
138
134
raise OSError ("OS Error 0.5" )
139
- except BMPError as e :
140
- print ("Failed to parse BMP: " + e .args [0 ])
135
+ except BMPError as err :
136
+ print ("Failed to parse BMP: " + err .args [0 ])
141
137
142
138
143
139
# Load BMP image, return 'columns' array:
144
- columns = load_bmp (FILENAME )
140
+ COLUMNS = load_bmp (FILENAME )
145
141
146
142
print ("Mem free:" , gc .mem_free ())
147
- # Orig code: 10320 bytes free
148
- # New code: 13216 bytes free
149
143
150
- column_delay = SPEED / 65535.0 / 10.0 # 0.0 to 0.1 seconds
144
+ COLUMN_DELAY = SPEED / 65535.0 / 10.0 # 0.0 to 0.1 seconds
145
+ # print(COLUMN_DELAY)
146
+
151
147
while LOOP :
152
- for c in columns :
153
- neopixel_write (NEOPIXEL_PIN , c )
154
- time .sleep (column_delay ) # Column-to-column delay
148
+ for COLUMN in COLUMNS :
149
+ neopixel_write (NEOPIXEL_PIN , COLUMN )
150
+ time .sleep (COLUMN_DELAY )
155
151
156
152
while True :
157
153
# Wait for touch pad input:
158
154
while not TOUCH .value :
159
155
continue
160
156
161
- column_delay = SPEED / 65535.0 / 10.0 # 0.0 to 0.1 seconds
162
- # print(column_delay)
157
+ time .sleep (DELAY_TIME )
163
158
164
- # Play back color data loaded into each column:
165
- for c in columns :
166
- neopixel_write (NEOPIXEL_PIN , c )
167
- time .sleep (column_delay ) # Column-to-column delay
159
+ # Play back color data loaded into each column:
160
+ for COLUMN in COLUMNS :
161
+ neopixel_write (NEOPIXEL_PIN , COLUMN )
162
+ time .sleep (COLUMN_DELAY )
168
163
# Last column is all 0's, no need to explicitly clear strip
169
164
170
165
# Wait for touch pad release, just in case:
0 commit comments