Skip to content

Commit f5624d9

Browse files
committed
first commit Light Paintstick HalloWing and CPX code
1 parent 04f0cd1 commit f5624d9

File tree

2 files changed

+345
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)