Skip to content

Commit 30cde11

Browse files
authored
Merge pull request #303 from jedgarpark/light-paintstick-hallowing-code
first commit Light Paintstick HalloWing and CPX code
2 parents 04f0cd1 + 2f830df commit 30cde11

File tree

2 files changed

+337
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)