Add RLCD animations and screensavers

This commit is contained in:
aeroreyna
2026-06-21 21:19:51 -04:00
parent edf3da1a30
commit 8dbc5f4c7b
24 changed files with 3097 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
# Reyna little32 ESP32-S3 Voice State Animations
A collection of 14 standalone MicroPython animation scripts designed for the Reyna little32 ESP32-S3 reflective screen board. These animations support voice gateway visual states instead of a spoken initial acknowledgement (ACK).
## Hardware & API Assumptions
- **Display Size:** Auto-scaling (reads `display.width` and `display.height`; defaults to 400x300 for RLCD, but supports others).
- **Colors:** Designed for monochrome-ish 0 (White/Background) and 1 (Black/Foreground) high-contrast visuals.
- **Methods Used:** `clear(c)`, `fill_rect(x,y,w,h,c)`, `rect(x,y,w,h,c)`, `line(x1,y1,x2,y2,c)`, `text(msg,x,y,c)`, `show()`.
- **Standalone:** Each script is entirely self-contained. It can be uploaded individually to board flash and requires no helper modules.
- **Robustness:** Gracefully falls back and prints a warning if `board_config` or `board_config.display_instance` is missing.
---
## Animation List & State Mappings
| Filename | Voice States / Purposes | Visual Behavior |
| :--- | :--- | :--- |
| **[ack_pulse.py](file:///home/adolforeyna/Projects/Screen/voice_animations/ack_pulse.py)** | `ack`, `received`, `got_it` | Bold checkmark with expanding concentric radar pulses. |
| **[listening_wave.py](file:///home/adolforeyna/Projects/Screen/voice_animations/listening_wave.py)** | `listening`, `audio_input` | A decorative mic icon at the top and dual active overlapping sine waves. |
| **[caption_scan.py](file:///home/adolforeyna/Projects/Screen/voice_animations/caption_scan.py)** | `captioning`, `transcribing` | Simulated typing log with scanner sweeps and blinking typewriter cursors. |
| **[waiting_orbit.py](file:///home/adolforeyna/Projects/Screen/voice_animations/waiting_orbit.py)** | `waiting`, `thinking`, `processing` | 4 circular ring nodes orbiting a central pulsing energy reactor core. |
| **[working_gears.py](file:///home/adolforeyna/Projects/Screen/voice_animations/working_gears.py)** | `working`, `tool_use`, `executing` | Large and small mechanical gears interlocking and rotating in opposite directions. |
| **[speaking_mouth.py](file:///home/adolforeyna/Projects/Screen/voice_animations/speaking_mouth.py)** | `speaking`, `audio_output` | Equalizer voiceprint spectrum bars bouncing symmetrically from a center axis. |
| **[success_spark.py](file:///home/adolforeyna/Projects/Screen/voice_animations/success_spark.py)** | `success`, `done`, `completed` | Bold checkmark inside a blooming radial starburst explosion. |
| **[error_alert.py](file:///home/adolforeyna/Projects/Screen/voice_animations/error_alert.py)** | `error`, `failure`, `alert` | Flashing bold exclamation triangle with diagonal warning hazard stripes. |
| **[network_retry.py](file:///home/adolforeyna/Projects/Screen/voice_animations/network_retry.py)** | `network_retry`, `reconnecting` | Wi-Fi waves lighting up sequentially inside looping dashed reload arrows. |
| **[battery_low.py](file:///home/adolforeyna/Projects/Screen/voice_animations/battery_low.py)** | `battery_low`, `low_power` | Battery outline with tips, flashing empty charge block, and warning triangle. |
| **[update_progress.py](file:///home/adolforeyna/Projects/Screen/voice_animations/update_progress.py)** | `update`, `uploading`, `downloading` | Sliding download arrow dropping into a tray above a percentage progress bar. |
| **[breathing_idle.py](file:///home/adolforeyna/Projects/Screen/voice_animations/breathing_idle.py)** | `idle`, `ready`, `breathing` | Concentric squares and accent dots scaling slowly in a calm breathing rhythm. |
| **[wake_attention.py](file:///home/adolforeyna/Projects/Screen/voice_animations/wake_attention.py)** | `wake`, `attention`, `active` | Stylized robot eyes blinking and shifting gaze left and right to look around. |
| **[kid_companion.py](file:///home/adolforeyna/Projects/Screen/voice_animations/kid_companion.py)** | `kid_friendly`, `companion` | Friendly robot head with wiggling ears, a winking eye, and a warm smile. |
---
## Execution Guide
### 1. Uploading to Flash
You can upload the desired animation files using tools like `ampy`, `rshell`, `mpremote`, or via the board's web console. E.g.
```bash
mpremote cp voice_animations/*.py :
```
### 2. Execution from REPL (Script Mode)
Run any script automatically with default parameters using `exec`:
```python
# Runs for 100 frames (approx. 5 seconds) and exits
exec(open("ack_pulse.py").read())
```
### 3. Importing and Running (Module Mode)
You can import the module and call its `run` method directly. The `run` method supports text customization and infinite looping:
```python
import listening_wave
# Run with custom caption text for 50 frames
listening_wave.run(text="HOTWORD CAPTURED", frames=50)
# Run indefinitely (blocking loop, break with Ctrl+C)
listening_wave.run(text="READY TO HEAR", frames=-1)
```
---
## Technical Details
### Customizing Loop Duration
In the `run()` function:
- Set `frames > 0` to run for a finite duration (useful for transition ACK animations).
- Set `frames=None` or `frames=-1` (or omit) to run indefinitely inside a `while True` loop.
- All scripts catch `KeyboardInterrupt` to exit gracefully, leaving the screen in a clean state if interrupted.
+68
View File
@@ -0,0 +1,68 @@
# Standalone MicroPython Voice Animation: ACK / Received
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "GOT IT"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a thick checkmark in the center
display.line(cx - 24, cy + 4, cx - 6, cy + 22, 1)
display.line(cx - 23, cy + 4, cx - 5, cy + 22, 1)
display.line(cx - 6, cy + 22, cx + 30, cy - 14, 1)
display.line(cx - 5, cy + 22, cx + 31, cy - 14, 1)
# Concentric pulsing squares radiating outwards
for p in range(3):
# Scale pulse radius based on frame count
r = 30 + ((frame * 4 + p * 30) % 90)
display.rect(cx - r, cy - r, r * 2, r * 2, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+91
View File
@@ -0,0 +1,91 @@
# Standalone MicroPython Voice Animation: Battery Low
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "LOW BATTERY!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Center coordinates adjusted for warning triangle on the left side
# Battery offset to the right
bat_x = cx + 20
bat_y = cy - 10
# Draw battery container outline
display.rect(bat_x - 65, bat_y - 30, 115, 60, 1)
display.rect(bat_x - 64, bat_y - 29, 113, 58, 1) # thicker
# Battery cap tip
display.fill_rect(bat_x + 50, bat_y - 12, 10, 24, 1)
# Low charge blinking segment on the left
blink_on = (frame // 4) % 2 == 0
if blink_on:
# Draw small low charge bar
display.fill_rect(bat_x - 58, bat_y - 23, 25, 46, 1)
# Draw a warning triangle on the left
warn_x = cx - 110
warn_y = cy - 10
# Warning triangle outline
display.line(warn_x, warn_y - 25, warn_x - 25, warn_y + 20, 1)
display.line(warn_x, warn_y - 25, warn_x + 25, warn_y + 20, 1)
display.line(warn_x - 25, warn_y + 20, warn_x + 25, warn_y + 20, 1)
# Warning exclamation mark
display.fill_rect(warn_x - 2, warn_y - 8, 4, 16, 1)
display.fill_rect(warn_x - 2, warn_y + 11, 4, 4, 1)
# Draw diagonal warning grid lines in battery background if empty to look stylish
if not blink_on:
for diag in range(-20, 40, 10):
display.line(bat_x - 58, bat_y - 23 + diag, bat_x - 38, bat_y + 23 + diag, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+80
View File
@@ -0,0 +1,80 @@
# Standalone MicroPython Voice Animation: Breathing Idle
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "READY"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Cosine-based breathing curve: slow and smooth, scaling from 0 to 1 and back
breath_scale = 0.5 + 0.5 * math.cos(frame * 0.07)
# Draw three concentric breathing boxes
# Base radii are scaled dynamically
r1 = int(24 + 32 * breath_scale)
r2 = int(14 + 18 * breath_scale)
r3 = int(6 + 8 * breath_scale)
# Outer box
display.rect(cx - r1, cy - r1, r1 * 2, r1 * 2, 1)
# Middle box
display.rect(cx - r2, cy - r2, r2 * 2, r2 * 2, 1)
# Inner solid core
display.fill_rect(cx - r3, cy - r3, r3 * 2, r3 * 2, 1)
# Draw small accent dots at the corners of the outer breathing box
accent = r1 + 8
display.fill_rect(cx - accent - 2, cy - accent - 2, 4, 4, 1)
display.fill_rect(cx + accent - 2, cy - accent - 2, 4, 4, 1)
display.fill_rect(cx - accent - 2, cy + accent - 2, 4, 4, 1)
display.fill_rect(cx + accent - 2, cy + accent - 2, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(65)
except AttributeError:
time.sleep(0.065)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+111
View File
@@ -0,0 +1,111 @@
# Standalone MicroPython Voice Animation: Caption Scan
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def wrap_text(msg, max_chars=36):
words = msg.split()
lines = []
curr = ""
for w in words:
if len(curr) + len(w) + 1 > max_chars:
lines.append(curr)
curr = w
else:
curr = (curr + " " + w).strip()
if curr:
lines.append(curr)
return lines
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
# Simulated text sequence if no real-time caption text is supplied
default_script = [
"RECEIVING VOICE PACKETS...",
"DECODING AUDIO STREAM...",
"MATCHING PHONEME VECTORS...",
"EXTRACTING INTENT AND CONTEXT...",
"STT METRICS: LATENCY 42MS",
"TRANSCRIPTION STABILIZED.",
"AWAITING PIPELINE RESPONSE..."
]
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw outer scan framing
display.rect(15, 15, width - 30, height - 50, 1)
# Draw moving scanner bar line
scan_y = 20 + ((frame * 6) % (height - 60))
display.line(20, scan_y, width - 20, scan_y, 1)
# Draw static top status line
display.text("SPEECH-TO-TEXT DECODER", 25, 25, 1)
display.line(25, 37, width - 25, 37, 1)
# Determine what text to write based on typewriter frame counter
if text:
lines = wrap_text(text, max_chars=(width - 50) // 8)
char_limit = int(frame * 2.5)
chars_drawn = 0
for idx, line in enumerate(lines):
if chars_drawn >= char_limit:
break
line_to_draw = line[:char_limit - chars_drawn]
display.text(line_to_draw, 25, 55 + idx * 18, 1)
chars_drawn += len(line)
else:
# Typewriter animation of simulated lines
active_line_idx = (frame // 15) % len(default_script)
char_limit = int((frame % 15) * 3)
# Show previously printed lines in full
start_line = max(0, active_line_idx - 5)
for idx in range(start_line, active_line_idx):
display.text(default_script[idx], 25, 55 + (idx - start_line) * 18, 1)
# Typewriter effect on the current line
curr_line_text = default_script[active_line_idx][:char_limit]
display.text(curr_line_text, 25, 55 + (active_line_idx - start_line) * 18, 1)
# Flash a cursor at the end of the typing line
if (frame // 3) % 2 == 0:
cursor_x = 25 + len(curr_line_text) * 8
cursor_y = 55 + (active_line_idx - start_line) * 18
display.fill_rect(cursor_x, cursor_y, 8, 8, 1)
# Draw bottom transcription indicator
display.text("TRANSCRIBING...", 25, height - 25, 1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+95
View File
@@ -0,0 +1,95 @@
# Standalone MicroPython Voice Animation: Error Alert
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "ERROR OCCURRED"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Blink animation state
# Flashes between standard warning symbol and high-contrast double border
blink_state = (frame // 4) % 2
# Base warning triangle dimensions
ty_top = cy - 40
ty_bottom = cy + 30
tx_left = cx - 45
tx_right = cx + 45
if blink_state == 0:
# Draw single bold warning triangle
display.line(cx, ty_top, tx_left, ty_bottom, 1)
display.line(cx, ty_top, tx_right, ty_bottom, 1)
display.line(tx_left, ty_bottom, tx_right, ty_bottom, 1)
else:
# Draw double thick warning triangle for "flashing" impact
display.line(cx, ty_top - 3, tx_left - 4, ty_bottom + 2, 1)
display.line(cx, ty_top - 3, tx_right + 4, ty_bottom + 2, 1)
display.line(tx_left - 4, ty_bottom + 2, tx_right + 4, ty_bottom + 2, 1)
display.line(cx, ty_top, tx_left, ty_bottom, 1)
display.line(cx, ty_top, tx_right, ty_bottom, 1)
display.line(tx_left, ty_bottom, tx_right, ty_bottom, 1)
# Draw Exclamation mark inside the triangle
# Exclamation bar
display.fill_rect(cx - 3, cy - 15, 6, 22, 1)
# Exclamation dot
display.fill_rect(cx - 3, cy + 13, 6, 6, 1)
# Draw warning stripes on the left and right edges of the screen
stripe_y = (frame * 3) % 20
for sy in range(-20, height + 20, 20):
# Left stripe
display.line(5, sy + stripe_y, 15, sy + stripe_y + 10, 1)
# Right stripe
display.line(width - 15, sy + stripe_y, width - 5, sy + stripe_y + 10, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+109
View File
@@ -0,0 +1,109 @@
# Standalone MicroPython Voice Animation: Kid Companion Robot
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "HELLO BUDDY!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Robot head main box
head_y = cy - 15
display.rect(cx - 50, head_y - 40, 100, 80, 1)
display.rect(cx - 49, head_y - 39, 98, 78, 1) # thicker
# Neck
display.fill_rect(cx - 10, head_y + 40, 20, 12, 1)
# Ears wiggling dynamically using sin waves
ear_offset = int(5 * math.sin(frame * 0.25))
# Left ear
display.rect(cx - 62, head_y - 15 + ear_offset, 12, 30, 1)
display.line(cx - 62, head_y + ear_offset, cx - 50, head_y, 1)
# Right ear
display.rect(cx + 50, head_y - 15 - ear_offset, 12, 30, 1)
display.line(cx + 62, head_y - ear_offset, cx + 50, head_y, 1)
# Antenna extending from head top
display.line(cx, head_y - 40, cx, head_y - 58, 1)
display.line(cx - 1, head_y - 40, cx - 1, head_y - 58, 1)
# Antenna bulb winks (flashes)
bulb_on = (frame // 4) % 2 == 0
if bulb_on:
display.fill_rect(cx - 6, head_y - 69, 12, 12, 1)
else:
display.rect(cx - 6, head_y - 69, 12, 12, 1)
# Eyes
eye_y = head_y - 12
# Left Eye (always open, cute square with highlight)
display.rect(cx - 32, eye_y - 8, 16, 16, 1)
display.fill_rect(cx - 28, eye_y - 4, 8, 8, 1)
display.fill_rect(cx - 26, eye_y - 2, 3, 3, 0) # highlight
# Right Eye winks every 12 frames
is_winking = (frame // 10) % 2 == 1
if is_winking:
# Wink line (happy curved line)
display.line(cx + 12, eye_y, cx + 20, eye_y + 4, 1)
display.line(cx + 20, eye_y + 4, cx + 28, eye_y, 1)
else:
# Open right eye
display.rect(cx + 16, eye_y - 8, 16, 16, 1)
display.fill_rect(cx + 20, eye_y - 4, 8, 8, 1)
display.fill_rect(cx + 22, eye_y - 2, 3, 3, 0) # highlight
# Cute happy smile mouth
display.line(cx - 16, head_y + 16, cx + 16, head_y + 16, 1)
display.line(cx - 16, head_y + 16, cx - 20, head_y + 10, 1)
display.line(cx + 16, head_y + 16, cx + 20, head_y + 10, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+88
View File
@@ -0,0 +1,88 @@
# Standalone MicroPython Voice Animation: Listening Wave
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "LISTENING"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a microphone icon at the top-center
mic_y = 35
display.rect(cx - 8, mic_y, 16, 22, 1) # Capsule
display.line(cx - 12, mic_y + 10, cx - 12, mic_y + 18, 1) # Cup left
display.line(cx - 12, mic_y + 18, cx + 12, mic_y + 18, 1) # Cup bottom
display.line(cx + 12, mic_y + 18, cx + 12, mic_y + 10, 1) # Cup right
display.line(cx, mic_y + 22, cx, mic_y + 28, 1) # Stand stem
display.line(cx - 10, mic_y + 28, cx + 10, mic_y + 28, 1) # Stand base
# Draw overlapping sine waves simulating microphone input
# Adjust wave amplitude with a breathing factor
breath = 0.7 + 0.3 * math.sin(frame * 0.15)
# Draw Wave 1 (larger, main wave)
prev_y1 = cy
for x in range(30, width - 30, 4):
val = math.sin((x + frame * 10) * 0.035) * math.sin(frame * 0.08)
y1 = cy + int(breath * 35 * val)
if x > 30:
display.line(x - 4, prev_y1, x, y1, 1)
prev_y1 = y1
# Draw Wave 2 (smaller, high frequency wave)
prev_y2 = cy
for x in range(30, width - 30, 4):
val = math.sin((x - frame * 14) * 0.07) * math.cos(frame * 0.1)
y2 = cy + int(breath * 18 * val)
if x > 30:
display.line(x - 4, prev_y2, x, y2, 1)
prev_y2 = y2
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+74
View File
@@ -0,0 +1,74 @@
{
"voice_animations": [
{
"filename": "ack_pulse.py",
"states": ["ack", "received", "got_it"],
"description": "Expanding concentric pulse squares around a central bold checkmark, representing an initial command receipt ACK."
},
{
"filename": "listening_wave.py",
"states": ["listening", "audio_input"],
"description": "An active dual-sinusoid voice waveform and microphone outline indicating active microphone capture."
},
{
"filename": "caption_scan.py",
"states": ["captioning", "transcribing", "speech_to_text"],
"description": "Real-time horizontal text line simulator with a typewriter character printer and vertical scanner sweep."
},
{
"filename": "waiting_orbit.py",
"states": ["waiting", "thinking", "processing"],
"description": "Orbital loading nodes revolving around a central pulsing engine core, indicating wait/server processing state."
},
{
"filename": "working_gears.py",
"states": ["working", "tool_use", "executing"],
"description": "Interlocking gears rotating in opposite directions, indicating background computation or external tool invocation."
},
{
"filename": "speaking_mouth.py",
"states": ["speaking", "audio_output", "text_to_speech"],
"description": "Dynamic equalizer audio spectrum bars bouncing from a center axis, representing voice response playback."
},
{
"filename": "success_spark.py",
"states": ["success", "done", "completed"],
"description": "A blooming starburst sparkle expansion and central success checkmark, representing task success."
},
{
"filename": "error_alert.py",
"states": ["error", "failure", "alert"],
"description": "A bold warning exclamation triangle flashing alongside warning stripes, representing pipeline or connection error."
},
{
"filename": "network_retry.py",
"states": ["network_retry", "reconnecting", "searching"],
"description": "Wi-Fi signal waves lighting up sequentially enclosed in rotating dash arrows, indicating connection retry."
},
{
"filename": "battery_low.py",
"states": ["battery_low", "low_power"],
"description": "A horizontal battery cell with a blinking single charge bar and alert triangle, showing low power warning."
},
{
"filename": "update_progress.py",
"states": ["update", "uploading", "downloading"],
"description": "A sliding download arrow dropping into a tray with a growing horizontal progress bar (0% - 100%)."
},
{
"filename": "breathing_idle.py",
"states": ["idle", "ready", "breathing"],
"description": "Smoothly scaling concentric squares simulating a calm breathing rhythm, representing an idle/ready state."
},
{
"filename": "wake_attention.py",
"states": ["wake", "attention", "active"],
"description": "Expressive robot eyes that look left, center, right, and blink periodically to display device wake attention."
},
{
"filename": "kid_companion.py",
"states": ["kid_friendly", "companion", "child_mode"],
"description": "A cute robot face companion with wiggling ears, a happy smile, blinking antenna, and a winking eye."
}
]
}
+101
View File
@@ -0,0 +1,101 @@
# Standalone MicroPython Voice Animation: Network Retry
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "CONNECTING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Wi-Fi Dot base at bottom of logo
dot_y = cy + 20
display.fill_rect(cx - 4, dot_y - 4, 8, 8, 1)
# Determine Wi-Fi wave stage (0 to 3) for sequential scanning
stage = (frame // 3) % 4
# Wave 1 (inner arc)
if stage >= 1:
display.line(cx - 15, dot_y - 12, cx, dot_y - 20, 1)
display.line(cx, dot_y - 20, cx + 15, dot_y - 12, 1)
# Wave 2 (middle arc)
if stage >= 2:
display.line(cx - 30, dot_y - 25, cx, dot_y - 36, 1)
display.line(cx, dot_y - 36, cx + 30, dot_y - 25, 1)
# Wave 3 (outer arc)
if stage >= 3:
display.line(cx - 45, dot_y - 38, cx, dot_y - 52, 1)
display.line(cx, dot_y - 52, cx + 45, dot_y - 38, 1)
# Draw a rotating retry circle (dashed arrow indicator) around the Wi-Fi icon
r = 68
# Compute 2 rotating arrow segments based on frame
rot_offset = frame * 0.12
for side in (0, math.pi):
for a_deg in range(0, 120, 15):
angle = side + rot_offset + math.radians(a_deg)
px = cx + int(r * math.cos(angle))
py = cy - 10 + int(r * math.sin(angle))
display.fill_rect(px - 2, py - 2, 4, 4, 1)
# Draw arrowheads at the leading edge of each segment
lead_angle = side + rot_offset + math.radians(120)
lx = cx + int(r * math.cos(lead_angle))
ly = cy - 10 + int(r * math.sin(lead_angle))
# Arrowhead wings
w1 = lead_angle - math.pi * 0.75
w2 = lead_angle + math.pi * 0.75
display.line(lx, ly, lx + int(8 * math.cos(w1)), ly + int(8 * math.sin(w1)), 1)
display.line(lx, ly, lx + int(8 * math.cos(w2)), ly + int(8 * math.sin(w2)), 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(55)
except AttributeError:
time.sleep(0.055)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+88
View File
@@ -0,0 +1,88 @@
# Standalone MicroPython Voice Animation: Speaking Mouth (Audio Spectrum)
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "SPEAKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw top and bottom boundary lines for the sound waves
display.line(cx - 100, cy - 45, cx + 100, cy - 45, 1)
display.line(cx - 100, cy + 45, cx + 100, cy + 45, 1)
# Draw a series of vertical equalizer bars modulated to simulate speech patterns
# An envelope simulates words (peaks and silences)
word_envelope = abs(math.sin(frame * 0.05))
num_bars = 15
bar_w = 8
gap = 4
total_w = num_bars * bar_w + (num_bars - 1) * gap
start_x = cx - total_w // 2
for i in range(num_bars):
bx = start_x + i * (bar_w + gap)
# Modulate individual bar heights using frame phase offsets
phase = frame * 0.28 + i * 0.45
noise_val = abs(math.sin(phase)) * 0.7 + abs(math.cos(phase * 1.5)) * 0.3
# Height peaks towards the center bars (bell shape)
center_factor = math.cos(((i - num_bars // 2) / (num_bars // 2)) * (math.pi / 2.2))
h = int(6 + 72 * word_envelope * noise_val * center_factor)
# Keep height within boundaries
h = min(80, h)
# Draw vertical bar centered at cy
display.fill_rect(bx, cy - h // 2, bar_w, h, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+84
View File
@@ -0,0 +1,84 @@
# Standalone MicroPython Voice Animation: Success Spark
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "SUCCESS!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a thick success checkmark
display.line(cx - 20, cy + 5, cx - 5, cy + 20, 1)
display.line(cx - 19, cy + 5, cx - 4, cy + 20, 1)
display.line(cx - 5, cy + 20, cx + 25, cy - 10, 1)
display.line(cx - 4, cy + 20, cx + 26, cy - 10, 1)
# Sparkles/rays radiating outwards
# The rays expand from r=30 to r=90 and loop
dist = (frame * 6) % 80
# Draw rays only if within reasonable expansion bounds
if dist < 65:
for angle_deg in range(0, 360, 45):
angle = math.radians(angle_deg)
# Outer point of sparkle
sx1 = cx + int(dist * math.cos(angle))
sy1 = cy + int(dist * math.sin(angle))
# Inner point of sparkle
sx2 = cx + int((dist + 12) * math.cos(angle))
sy2 = cy + int((dist + 12) * math.sin(angle))
display.line(sx1, sy1, sx2, sy2, 1)
# Add side sparkles for variety on cross angles (45, 135, etc.)
if angle_deg % 90 != 0:
display.fill_rect(sx2 - 2, sy2 - 2, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(45)
except AttributeError:
time.sleep(0.045)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+89
View File
@@ -0,0 +1,89 @@
# Standalone MicroPython Voice Animation: Update Progress
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a sliding downward-pointing arrow in the top half
# Offset slides down and wraps around to create motion
arrow_slide = (frame * 3) % 24
ay = cy - 45 + arrow_slide
# Arrow Shaft
display.fill_rect(cx - 6, ay - 24, 12, 24, 1)
# Arrow Head (triangle)
display.line(cx - 18, ay, cx, ay + 15, 1)
display.line(cx + 18, ay, cx, ay + 15, 1)
display.line(cx - 18, ay, cx + 18, ay, 1)
# Fill the head slightly
display.fill_rect(cx - 6, ay, 12, 8, 1)
# Draw static receiver container (horizontal tray) below the arrow path
tray_y = cy + 12
display.line(cx - 30, tray_y, cx - 30, tray_y + 10, 1)
display.line(cx - 30, tray_y + 10, cx + 30, tray_y + 10, 1)
display.line(cx + 30, tray_y + 10, cx + 30, tray_y, 1)
# Calculate update progress percentage
# (Cycles 0 to 100 over 25 frames, or follows frame count)
percent = (frame * 4) % 101
# Progress bar outline
bar_y = cy + 45
display.rect(cx - 90, bar_y, 180, 16, 1)
# Progress bar fill
fill_width = int(176 * percent / 100)
if fill_width > 0:
display.fill_rect(cx - 88, bar_y + 2, fill_width, 12, 1)
# Draw label + percentage text
label = text if text else "UPDATING SYSTEM..."
pct_label = f"{label} {percent}%"
draw_center_text(display, pct_label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(55)
except AttributeError:
time.sleep(0.055)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+81
View File
@@ -0,0 +1,81 @@
# Standalone MicroPython Voice Animation: Waiting Orbit
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "THINKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw central pulsing core
# Uses sin wave to expand/contract
pulse = int(14 + 4 * math.sin(frame * 0.18))
display.rect(cx - pulse, cy - pulse, pulse * 2, pulse * 2, 1)
display.rect(cx - pulse + 3, cy - pulse + 3, (pulse - 3) * 2, (pulse - 3) * 2, 1)
display.fill_rect(cx - 3, cy - 3, 6, 6, 1)
# Draw a dotted circular path of radius 60
for a_deg in range(0, 360, 15):
angle = math.radians(a_deg)
px = cx + int(60 * math.cos(angle))
py = cy + int(60 * math.sin(angle))
display.fill_rect(px, py, 2, 2, 1)
# Draw 4 orbiting ring nodes
for node_idx in range(4):
angle = (frame * 0.08) + (node_idx * math.pi / 2)
nx = cx + int(60 * math.cos(angle))
ny = cy + int(60 * math.sin(angle))
# Draw square ring nodes
display.fill_rect(nx - 6, ny - 6, 12, 12, 1)
display.fill_rect(nx - 4, ny - 4, 8, 8, 0)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 35, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+96
View File
@@ -0,0 +1,96 @@
# Standalone MicroPython Voice Animation: Wake Attention
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "HI, I'M LISTENING"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Left and Right eye center X coordinates
left_cx = cx - 55
right_cx = cx + 55
eye_y = cy - 10
# State cycle to control pupil gaze offset (look center, look left, look right)
gaze_state = (frame // 16) % 4
gaze_offset = 0
if gaze_state == 1:
gaze_offset = -12 # Look left
elif gaze_state == 3:
gaze_offset = 12 # Look right
# Blink cycle: close eyes for a few frames periodically
is_blinking = (frame % 22) in (0, 1, 2)
if is_blinking:
# Closed eyes: draw simple horizontal lines across the eye spaces
display.line(left_cx - 28, eye_y, left_cx + 28, eye_y, 1)
display.line(left_cx - 28, eye_y + 1, left_cx + 28, eye_y + 1, 1) # thicker
display.line(right_cx - 28, eye_y, right_cx + 28, eye_y, 1)
display.line(right_cx - 28, eye_y + 1, right_cx + 28, eye_y + 1, 1) # thicker
else:
# Open eyes: draw outer rounded rectangular bounding frames
# Left eye border
display.rect(left_cx - 32, eye_y - 22, 64, 44, 1)
display.rect(left_cx - 31, eye_y - 21, 62, 42, 1)
# Right eye border
display.rect(right_cx - 32, eye_y - 22, 64, 44, 1)
display.rect(right_cx - 31, eye_y - 21, 62, 42, 1)
# Draw pupils inside, shifted by the gaze offset
display.fill_rect(left_cx - 10 + gaze_offset, eye_y - 10, 20, 20, 1)
display.fill_rect(right_cx - 10 + gaze_offset, eye_y - 10, 20, 20, 1)
# Draw small cute highlights inside pupils
display.fill_rect(left_cx - 6 + gaze_offset, eye_y - 6, 6, 6, 0)
display.fill_rect(right_cx - 6 + gaze_offset, eye_y - 6, 6, 6, 0)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+102
View File
@@ -0,0 +1,102 @@
# Standalone MicroPython Voice Animation: Working Gears
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def draw_gear(d, gcx, gcy, r, teeth_count, rotation, c=1):
# Draw gear boundary
d.rect(gcx - r, gcy - r, r * 2, r * 2, c)
# Inner shaft hole
d.fill_rect(gcx - 5, gcy - 5, 10, 10, c)
d.fill_rect(gcx - 2, gcy - 2, 4, 4, 0)
# Draw gear teeth extending outwards
for i in range(teeth_count):
angle = rotation + i * (2 * math.pi / teeth_count)
# Inner teeth start
x1 = gcx + int((r - 2) * math.cos(angle))
y1 = gcy + int((r - 2) * math.sin(angle))
# Outer teeth end
x2 = gcx + int((r + 10) * math.cos(angle))
y2 = gcy + int((r + 10) * math.sin(angle))
d.line(x1, y1, x2, y2, c)
# Add tiny horizontal heads to teeth for thickness
perp_angle = angle + math.pi / 2
tx1 = x2 - int(3 * math.cos(perp_angle))
ty1 = y2 - int(3 * math.sin(perp_angle))
tx2 = x2 + int(3 * math.cos(perp_angle))
ty2 = y2 + int(3 * math.sin(perp_angle))
d.line(tx1, ty1, tx2, ty2, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "WORKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw two rotating, interlocking gears
# Large Gear (Left & slightly up)
rot1 = frame * 0.08
draw_gear(display, cx - 45, cy - 10, r=32, teeth_count=8, rotation=rot1, c=1)
# Small Gear (Right & slightly down)
# Rotates counter-clockwise, offset to interlock teeth
rot2 = -frame * 0.106 + (math.pi / 8)
draw_gear(display, cx + 45, cy + 20, r=24, teeth_count=6, rotation=rot2, c=1)
# Little decorative background computer dots
dot_phase = (frame // 4) % 4
for i in range(4):
fill_color = 1 if i == dot_phase else 0
display.rect(cx - 80 + i * 16, cy + 70, 8, 8, 1)
if fill_color:
display.fill_rect(cx - 78 + i * 16, cy + 72, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()