1212import math
1313import configparser
1414from enum import Enum
15- from tempfile import NamedTemporaryFile
15+ from collections import deque
1616
1717import board
1818import digitalio
3030API_KEYS_FILE = "~/keys.txt"
3131PROMPT_FILE = "/boot/bookprompt.txt"
3232
33+ # Quit Settings (Close book QUIT_CLOSES within QUIT_TIME_PERIOD to quit)
34+ QUIT_CLOSES = 3
35+ QUIT_TIME_PERIOD = 5 # Time period in Seconds
36+
3337# Neopixel Settings
3438NEOPIXEL_COUNT = 10
3539NEOPIXEL_BRIGHTNESS = 0.2
3640NEOPIXEL_ORDER = neopixel .GRBW
37- NEOPIXEL_SLEEP_COLOR = (0 , 0 , 255 , 0 )
38- NEOPIXEL_WAITING_COLOR = (255 , 255 , 0 , 0 )
39- NEOPIXEL_READY_COLOR = (0 , 255 , 0 , 0 )
41+ NEOPIXEL_LOADING_COLOR = (0 , 255 , 0 , 0 ) # Loading/Dreaming (Green)
42+ NEOPIXEL_SLEEP_COLOR = (0 , 0 , 0 , 0 ) # Sleeping (Off)
43+ NEOPIXEL_WAITING_COLOR = (255 , 255 , 0 , 0 ) # Waiting for Input (Yellow)
44+ NEOPIXEL_READING_COLOR = (0 , 0 , 255 , 0 ) # Reading (Blue)
4045NEOPIXEL_PULSE_SPEED = 0.1
4146
4247# Image Names
6267
6368# Delays to control the speed of the text
6469WORD_DELAY = 0.1
65- WELCOME_IMAGE_DELAY = 0
6670TITLE_FADE_TIME = 0.05
6771TITLE_FADE_STEPS = 25
6872TEXT_FADE_TIME = 0.25
@@ -194,6 +198,9 @@ def __init__(self, rotation=0):
194198 self ._sleep_request = False
195199 self ._running = True
196200 self ._busy = False
201+ self ._loading = False
202+ # Use a Double Ended Queue to handle the heavy lifting
203+ self ._closing_times = deque (maxlen = QUIT_CLOSES )
197204 # Use a cursor to keep track of where we are in the text area
198205 self .cursor = {"x" : 0 , "y" : 0 }
199206 self .listener = None
@@ -206,13 +213,14 @@ def __init__(self, rotation=0):
206213 auto_write = False ,
207214 )
208215 self ._prompt = ""
209- # Load the prompt file
210- with open (PROMPT_FILE , "r" ) as f :
211- self ._prompt = f .read ()
216+ self ._load_thread = threading .Thread (target = self ._handle_loading_status )
217+ self ._load_thread .start ()
212218
213219 def start (self ):
214220 # Output to the LCD instead of the console
215- #os.putenv("DISPLAY", ":0")
221+ os .putenv ("DISPLAY" , ":0" )
222+
223+ self ._set_status_color (NEOPIXEL_LOADING_COLOR )
216224
217225 # Initialize the display
218226 pygame .init ()
@@ -225,7 +233,10 @@ def start(self):
225233 # Preload welcome image and display it
226234 self ._load_image ("welcome" , WELCOME_IMAGE )
227235 self .display_welcome ()
228- start_time = time .monotonic ()
236+
237+ # Load the prompt file
238+ with open (PROMPT_FILE , "r" ) as f :
239+ self ._prompt = f .read ()
229240
230241 #Initialize the Listener
231242 self .listener = Listener (openai .api_key , ENERGY_THRESHOLD , RECORD_TIMEOUT )
@@ -297,28 +308,18 @@ def start(self):
297308 self ._sleep_check_thread = threading .Thread (target = self ._handle_sleep )
298309 self ._sleep_check_thread .start ()
299310
300- # Light the neopixels to indicate the book is ready
301- self .pixels .fill (NEOPIXEL_READY_COLOR )
302- self .pixels .show ()
303-
304- # Continue showing the image until the minimum amount of time has passed
305- time .sleep (max (0 , WELCOME_IMAGE_DELAY - (time .monotonic () - start_time )))
311+ self ._set_status_color (NEOPIXEL_READING_COLOR )
306312
307313 def deinit (self ):
308314 self ._running = False
309315 self ._sleep_check_thread .join ()
316+ self ._load_thread .join ()
310317 self .backlight .power = True
311318
312319 def _handle_sleep (self ):
313320 reed_switch = digitalio .DigitalInOut (REED_SWITCH_PIN )
314321 reed_switch .direction = digitalio .Direction .INPUT
315322 reed_switch .pull = digitalio .Pull .UP
316- pulse = Pulse (
317- self .pixels ,
318- speed = NEOPIXEL_PULSE_SPEED ,
319- color = NEOPIXEL_SLEEP_COLOR ,
320- period = 3 ,
321- )
322323
323324 while self ._running :
324325 if self ._sleeping and reed_switch .value : # Book Open
@@ -328,10 +329,37 @@ def _handle_sleep(self):
328329 ): # Book Closed
329330 self ._sleep ()
330331
331- if self ._sleeping :
332- pulse .animate ()
333332 time .sleep (self .sleep_check_delay )
334333
334+ def _handle_loading_status (self ):
335+ pulse = Pulse (
336+ self .pixels ,
337+ speed = NEOPIXEL_PULSE_SPEED ,
338+ color = NEOPIXEL_LOADING_COLOR ,
339+ period = 3 ,
340+ )
341+
342+ while self ._running :
343+ if self ._loading :
344+ pulse .animate ()
345+ time .sleep (0.1 )
346+
347+ # Turn off the Neopixels
348+ self .pixels .fill (0 )
349+ self .pixels .show ()
350+
351+ def _set_status_color (self , status_color ):
352+ if status_color not in [NEOPIXEL_READING_COLOR , NEOPIXEL_WAITING_COLOR , NEOPIXEL_SLEEP_COLOR , NEOPIXEL_LOADING_COLOR ]:
353+ raise ValueError (f"Invalid status color { status_color } ." )
354+
355+ # Handle loading color by setting the loading flag
356+ self ._loading = status_color == NEOPIXEL_LOADING_COLOR
357+
358+ # Handle other status colors by setting the neopixels
359+ if status_color != NEOPIXEL_LOADING_COLOR :
360+ self .pixels .fill (status_color )
361+ self .pixels .show ()
362+
335363 def handle_events (self ):
336364 if not self ._sleeping :
337365 for event in pygame .event .get ():
@@ -514,6 +542,7 @@ def new_story(self):
514542 def display_loading (self ):
515543 self ._display_surface (self .images ["loading" ], 0 , 0 )
516544 pygame .display .update ()
545+ self ._set_status_color (NEOPIXEL_LOADING_COLOR )
517546
518547 def display_welcome (self ):
519548 self ._display_surface (self .images ["welcome" ], 0 , 0 )
@@ -556,6 +585,7 @@ def load_story(self, story):
556585 if self .cursor ["y" ] > 0 :
557586 self .cursor ["y" ] += PARAGRAPH_SPACING
558587 print (f"Loaded story at index { self .story } with { len (self .pages )} pages" )
588+ self ._set_status_color (NEOPIXEL_READING_COLOR )
559589 self ._busy = False
560590
561591 def _add_page (self , title = None ):
@@ -583,13 +613,12 @@ def generate_new_story(self):
583613 def show_waiting ():
584614 # Pause for a beat because the listener doesn't
585615 # immediately start listening sometimes
586- time .sleep (2 )
616+ time .sleep (1 )
587617 self .pixels .fill (NEOPIXEL_WAITING_COLOR )
588618 self .pixels .show ()
589619
590620 self .listener .listen (ready_callback = show_waiting )
591- self .pixels .fill (NEOPIXEL_READY_COLOR )
592- self .pixels .show ()
621+
593622 if self ._sleep_request :
594623 self ._busy = False
595624 return
@@ -623,7 +652,20 @@ def _sleep(self):
623652 while self ._busy :
624653 time .sleep (0.1 )
625654 self ._sleep_request = False
655+
656+ self ._closing_times .append (time .monotonic ())
657+
658+ # Check if we've closed the book a certain number of times
659+ # within a certain number of seconds
660+ if (
661+ len (self ._closing_times ) == QUIT_CLOSES
662+ and self ._closing_times [- 1 ] - self ._closing_times [0 ] < QUIT_TIME_PERIOD
663+ ):
664+ self ._running = False
665+ return
666+
626667 self ._sleeping = True
668+ self ._set_status_color (NEOPIXEL_SLEEP_COLOR )
627669 self .sleep_check_delay = 0
628670 self .saved_screen = self .screen .copy ()
629671 self .screen .fill ((0 , 0 , 0 ))
@@ -638,8 +680,7 @@ def _wake(self):
638680 pygame .display .update ()
639681 self .saved_screen = None
640682 self .sleep_check_delay = 0.1
641- self .pixels .fill (NEOPIXEL_READY_COLOR )
642- self .pixels .show ()
683+ self ._set_status_color (NEOPIXEL_READING_COLOR )
643684 self ._sleeping = False
644685
645686 def _make_story_prompt (self , request ):
@@ -669,6 +710,9 @@ def _sendchat(self, prompt):
669710 # Send the heard text to ChatGPT and return the result
670711 return strip_fancy_quotes (response )
671712
713+ @property
714+ def running (self ):
715+ return self ._running
672716
673717def parse_args ():
674718 parser = argparse .ArgumentParser ()
@@ -692,7 +736,7 @@ def main(args):
692736 book .generate_new_story ()
693737 book .display_current_page ()
694738
695- while True :
739+ while book . running :
696740 book .handle_events ()
697741 except KeyboardInterrupt :
698742 pass
0 commit comments