1010"""
1111
1212# pylint: disable=import-error
13- import os
1413import gc
14+ from time import monotonic , sleep
1515import board
1616import busio
1717import displayio
18- from time import monotonic , sleep
1918from digitalio import DigitalInOut , Direction
2019from bmp2led import BMP2LED , BMPError
2120from neopixel_write import neopixel_write
3433PATH = '/bmps-72px' # Folder containing BMP images (or '' for root path)
3534TEMPFILE = '/led.dat' # Working file for LED data (will be clobbered!)
3635FLIP_SCREEN = False # If True, turn CLUE screen & buttons upside-down
37- GAMMA = 2.6 # Correction factor for perceptually linear brightness
36+ GAMMA = 2.4 # Correction factor for perceptually linear brightness
37+
38+ # Temporary line during development, delete before use:
39+ PIXEL_ORDER = 'gbr' # Old DotStar strip with different color order
3840
39- PIXEL_ORDER = 'gbr'
4041
4142def centered_label (text , y_pos , scale ):
4243 """
@@ -73,11 +74,8 @@ def __init__(self, flip, path, tempfile, num_pixels, pixel_order,
7374 file for LED data (will be clobbered).
7475 num_pixels (int) : LED strip length.
7576 pixel_order (string) : LED data order, e.g. 'grb'.
76- pixel_pins (tuple) : Board pin(s) for LED data output. If a
77- single value (int), a NeoPixel strip is
78- being used. If two values (tuple or
79- list), it's a DotStar strip (pins are
80- data and clock of an SPI port).
77+ pixel_pins (tuple) : Board pin for LED data output (SPI data
78+ and clock pins respectively).
8179 gamma (float) : Correction for perceptual linearity.
8280 """
8381 self .bmp2led = BMP2LED (num_pixels , pixel_order , gamma )
@@ -92,8 +90,7 @@ def __init__(self, flip, path, tempfile, num_pixels, pixel_order,
9290 self .spi .configure (baudrate = 8000000 )
9391
9492 # Determine filesystem-to-LEDs throughput (also clears LED strip)
95- # self.rows_per_second, self.row_size = self.benchmark()
96- self .rows_per_second , self .row_size = 1000 , 500
93+ self .rows_per_second , self .row_size = self .benchmark ()
9794
9895 # Configure hardware initial state
9996 self .button_left = RichButton (board .BUTTON_A )
@@ -134,25 +131,32 @@ def benchmark(self):
134131 (including DotStar header and footer) (int).
135132 """
136133 # Generate a small temporary file equal to one full LED row,
137- # all set 'off' (bonus, this turns off LED strip on startup).
134+ # all set 'off'.
135+ row_data = bytearray ([0 ] * 4 +
136+ [255 , 0 , 0 , 0 ] * self .bmp2led .num_pixels +
137+ [255 ] * ((self .bmp2led .num_pixels + 15 ) //
138+ 16 ))
139+ row_size = len (row_data )
138140 with open (self .tempfile , 'wb' ) as file :
139- row_data = bytearray ([0 ] * 4 +
140- [255 , 0 , 0 , 0 ] * self .bmp2led .num_pixels +
141- [255 ] * ((self .bmp2led .num_pixels + 15 ) //
142- 16 ))
143141 file .write (row_data )
144- row_size = len (row_data )
145142
146143 # For a period of 1 second, repeatedly seek to start of file,
147144 # read row of data and write to LED strip as fast as possible.
148145 # Not super precise, but good-enough guess of light painting speed.
146+ # (Bonus, this will turn off LED strip on startup).
149147 rows = 0
150148 with open (self .tempfile , 'rb' ) as file :
151- gc .collect ()
152149 start_time = monotonic ()
153150 while monotonic () - start_time < 1.0 :
154151 file .seek (0 )
155- self .spi .write (file .read (row_size ))
152+ # using readinto() instead of read() reduces the amount
153+ # of work the garbage collector needs to do each row.
154+ file .readinto (row_data )
155+ self .spi .write (row_data )
156+ # Garbage collection is done on EVERY row...even though
157+ # this slows down painting a LOT, it keeps the timing more
158+ # consistent (else there would be conspicuous glitches).
159+ gc .collect ()
156160 rows += 1
157161
158162 return rows , row_size
@@ -180,8 +184,8 @@ def load_progress(self, amount):
180184 def load_image (self ):
181185 """
182186 Load BMP from image list, determined by variable self.image_num
183- (not a passed argument). Data is converted and placed in variable
184- self.columns[] .
187+ (not a passed argument). Data is converted and placed in
188+ self.tempfile .
185189 """
186190 # Minimal progress display while image is loaded.
187191 group = displayio .Group ()
@@ -191,8 +195,8 @@ def load_image(self):
191195 group .append (self .rect )
192196 board .DISPLAY .show (group )
193197
194- #duration = 5.0 - self.speed * 4.5
195- duration = 3. 0 - self .speed * 2.75
198+ # Playback time is about 1/4 to 5 seconds, non linearly spaced
199+ duration = 0.25 + 4.75 * (( 1. 0 - self .speed ) ** 2.5 )
196200 rows = duration * self .rows_per_second
197201 try :
198202 self .num_rows = self .bmp2led .process (self .path + '/' +
@@ -214,15 +218,15 @@ def load_image(self):
214218 def paint (self ):
215219 """
216220 Paint mode. Watch for button taps to start/stop image playback,
217- or button hold to switch to config mode. During playback, do all
218- the nifty image processing.
221+ or button hold to switch to config mode.
219222 """
220223
221224 board .DISPLAY .brightness = 0 # Screen backlight OFF
222225 painting = False
223226
224227 with open (self .tempfile , 'rb' ) as file :
225- gc .collect () # Helps make playback a little smoother
228+ led_buffer = bytearray (self .row_size )
229+ gc .collect ()
226230
227231 while True :
228232 action_set = {self .button_left .action (),
@@ -238,7 +242,14 @@ def paint(self):
238242
239243 if painting :
240244 file .seek (row * self .row_size )
241- self .spi .write (file .read (self .row_size ))
245+ # using readinto() instead of read() reduces the amount
246+ # of work the garbage collector needs to do each row.
247+ file .readinto (led_buffer )
248+ self .spi .write (led_buffer )
249+ # Garbage collection is done on EVERY row...even though
250+ # this slows down painting a LOT, it keeps the timing more
251+ # consistent (else there would be conspicuous glitches).
252+ gc .collect ()
242253 row += 1
243254 if row >= self .num_rows :
244255 if self .loop :
@@ -253,7 +264,7 @@ def paint(self):
253264 # function. This way definitely generates less pylint gas pains.
254265 # Also, creating and destroying elements (rather than creating
255266 # them all up-front and showing or hiding elements as needed)
256- # tends to use less RAM, leaving more for image .
267+ # tends to use less RAM.
257268
258269 def make_ui_group (self , main_config , config_label , rect_val = None ):
259270 """
@@ -335,7 +346,6 @@ def config_select(self, first_run=False):
335346 prev_mode = self .config_mode
336347
337348 # Before exiting to paint mode, check if new image needs loaded
338- # DO IMAGE CONVERSION HERE!
339349 if reload_image :
340350 self .load_image ()
341351
@@ -348,7 +358,8 @@ def config_image(self):
348358 be reloaded, second indicates if returning to paint mode vs
349359 more config.
350360 """
351- group = self .make_ui_group (False , self .images [self .image_num ])
361+ group = self .make_ui_group (False ,
362+ self .images [self .image_num ].split ('.' )[0 ])
352363 orig_image , prev_image = self .image_num , self .image_num
353364
354365 while True :
@@ -365,8 +376,8 @@ def config_image(self):
365376
366377 if self .image_num is not prev_image :
367378 group .pop ()
368- group .append (centered_label (self . images [ self . image_num ],
369- 40 , 3 ))
379+ group .append (centered_label (
380+ self . images [ self . image_num ]. split ( '.' )[ 0 ], 40 , 3 ))
370381 prev_image = self .image_num
371382
372383
@@ -450,7 +461,8 @@ def config_brightness(self):
450461
451462 def run (self ):
452463 """
453- Application loop just consists of alternating paint and
464+ Post-init application loop. After a one-time visit to image select
465+ (and possibly other config), just consists of alternating paint and
454466 config modes. Each function has its own condition for return
455467 (switching to the opposite mode). Repeat forever.
456468 """
@@ -465,12 +477,5 @@ def run(self):
465477 self .config_select ()
466478
467479
468- # Note to future self: make program start in image-select mode,
469- # then when that returns, go into settings or paint depending
470- # on return status.
471- # # Load first image in list
472- # self.load_image()
473-
474-
475480ClueLightPainter (FLIP_SCREEN , PATH , TEMPFILE ,
476481 NUM_PIXELS , PIXEL_ORDER , PIXEL_PINS , GAMMA ).run ()
0 commit comments