12
12
import math
13
13
import configparser
14
14
from enum import Enum
15
- from tempfile import NamedTemporaryFile
15
+ from collections import deque
16
16
17
17
import board
18
18
import digitalio
30
30
API_KEYS_FILE = "~/keys.txt"
31
31
PROMPT_FILE = "/boot/bookprompt.txt"
32
32
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
+
33
37
# Neopixel Settings
34
38
NEOPIXEL_COUNT = 10
35
39
NEOPIXEL_BRIGHTNESS = 0.2
36
40
NEOPIXEL_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)
40
45
NEOPIXEL_PULSE_SPEED = 0.1
41
46
42
47
# Image Names
62
67
63
68
# Delays to control the speed of the text
64
69
WORD_DELAY = 0.1
65
- WELCOME_IMAGE_DELAY = 0
66
70
TITLE_FADE_TIME = 0.05
67
71
TITLE_FADE_STEPS = 25
68
72
TEXT_FADE_TIME = 0.25
@@ -194,6 +198,9 @@ def __init__(self, rotation=0):
194
198
self ._sleep_request = False
195
199
self ._running = True
196
200
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 )
197
204
# Use a cursor to keep track of where we are in the text area
198
205
self .cursor = {"x" : 0 , "y" : 0 }
199
206
self .listener = None
@@ -206,13 +213,14 @@ def __init__(self, rotation=0):
206
213
auto_write = False ,
207
214
)
208
215
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 ()
212
218
213
219
def start (self ):
214
220
# 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 )
216
224
217
225
# Initialize the display
218
226
pygame .init ()
@@ -225,7 +233,10 @@ def start(self):
225
233
# Preload welcome image and display it
226
234
self ._load_image ("welcome" , WELCOME_IMAGE )
227
235
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 ()
229
240
230
241
#Initialize the Listener
231
242
self .listener = Listener (openai .api_key , ENERGY_THRESHOLD , RECORD_TIMEOUT )
@@ -297,28 +308,18 @@ def start(self):
297
308
self ._sleep_check_thread = threading .Thread (target = self ._handle_sleep )
298
309
self ._sleep_check_thread .start ()
299
310
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 )
306
312
307
313
def deinit (self ):
308
314
self ._running = False
309
315
self ._sleep_check_thread .join ()
316
+ self ._load_thread .join ()
310
317
self .backlight .power = True
311
318
312
319
def _handle_sleep (self ):
313
320
reed_switch = digitalio .DigitalInOut (REED_SWITCH_PIN )
314
321
reed_switch .direction = digitalio .Direction .INPUT
315
322
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
- )
322
323
323
324
while self ._running :
324
325
if self ._sleeping and reed_switch .value : # Book Open
@@ -328,10 +329,37 @@ def _handle_sleep(self):
328
329
): # Book Closed
329
330
self ._sleep ()
330
331
331
- if self ._sleeping :
332
- pulse .animate ()
333
332
time .sleep (self .sleep_check_delay )
334
333
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
+
335
363
def handle_events (self ):
336
364
if not self ._sleeping :
337
365
for event in pygame .event .get ():
@@ -514,6 +542,7 @@ def new_story(self):
514
542
def display_loading (self ):
515
543
self ._display_surface (self .images ["loading" ], 0 , 0 )
516
544
pygame .display .update ()
545
+ self ._set_status_color (NEOPIXEL_LOADING_COLOR )
517
546
518
547
def display_welcome (self ):
519
548
self ._display_surface (self .images ["welcome" ], 0 , 0 )
@@ -556,6 +585,7 @@ def load_story(self, story):
556
585
if self .cursor ["y" ] > 0 :
557
586
self .cursor ["y" ] += PARAGRAPH_SPACING
558
587
print (f"Loaded story at index { self .story } with { len (self .pages )} pages" )
588
+ self ._set_status_color (NEOPIXEL_READING_COLOR )
559
589
self ._busy = False
560
590
561
591
def _add_page (self , title = None ):
@@ -583,13 +613,12 @@ def generate_new_story(self):
583
613
def show_waiting ():
584
614
# Pause for a beat because the listener doesn't
585
615
# immediately start listening sometimes
586
- time .sleep (2 )
616
+ time .sleep (1 )
587
617
self .pixels .fill (NEOPIXEL_WAITING_COLOR )
588
618
self .pixels .show ()
589
619
590
620
self .listener .listen (ready_callback = show_waiting )
591
- self .pixels .fill (NEOPIXEL_READY_COLOR )
592
- self .pixels .show ()
621
+
593
622
if self ._sleep_request :
594
623
self ._busy = False
595
624
return
@@ -623,7 +652,20 @@ def _sleep(self):
623
652
while self ._busy :
624
653
time .sleep (0.1 )
625
654
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
+
626
667
self ._sleeping = True
668
+ self ._set_status_color (NEOPIXEL_SLEEP_COLOR )
627
669
self .sleep_check_delay = 0
628
670
self .saved_screen = self .screen .copy ()
629
671
self .screen .fill ((0 , 0 , 0 ))
@@ -638,8 +680,7 @@ def _wake(self):
638
680
pygame .display .update ()
639
681
self .saved_screen = None
640
682
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 )
643
684
self ._sleeping = False
644
685
645
686
def _make_story_prompt (self , request ):
@@ -669,6 +710,9 @@ def _sendchat(self, prompt):
669
710
# Send the heard text to ChatGPT and return the result
670
711
return strip_fancy_quotes (response )
671
712
713
+ @property
714
+ def running (self ):
715
+ return self ._running
672
716
673
717
def parse_args ():
674
718
parser = argparse .ArgumentParser ()
@@ -692,7 +736,7 @@ def main(args):
692
736
book .generate_new_story ()
693
737
book .display_current_page ()
694
738
695
- while True :
739
+ while book . running :
696
740
book .handle_events ()
697
741
except KeyboardInterrupt :
698
742
pass
0 commit comments