picocalc-app
This skill should be used when the user asks to "create a PicoCalc app", "build a game for PicoCalc", "write a PicoCalc script", "new app for the handheld", "make a tool for PicoCalc", "add a PicoCalc application", or mentions building any application, game, utility, or tool for the PicoCalc device. Also activate when the user mentions writing MicroPython code targeting the 320x320 display, membrane keyboard, or audio PWM on the Clockwork Pi PicoCalc with Raspberry Pi Pico 2W.
Create PicoCalc Applications
Generate MicroPython applications for the Clockwork Pi PicoCalc (RP2350, 320x320 LCD, membrane keyboard, SD card, audio PWM).
Application Structure
Follow this exact class-based pattern used by all existing apps (tetris.py, snake.py, synth.py):
"""
AppName - Brief description
Features:
- Feature list
"""
import picocalc
import utime
import urandom
import gc
from machine import Pin, PWM
# Arrow key escape sequences for PicoCalc
KEY_UP = b'\x1b[A'
KEY_DOWN = b'\x1b[B'
KEY_LEFT = b'\x1b[D'
KEY_RIGHT = b'\x1b[C'
KEY_ESC = b'\x1b\x1b'
# Audio pins
AUDIO_LEFT = 28 # PWM_L
AUDIO_RIGHT = 27 # PWM_R
# 4-bit grayscale colors (0=black, 15=white)
COLOR_BLACK = 0
COLOR_DARK_GRAY = 5
COLOR_GRAY = 8
COLOR_LIGHT_GRAY = 11
COLOR_WHITE = 15
class AppName:
def __init__(self):
self.display = picocalc.display
self.width = self.display.width # 320
self.height = self.display.height # 320
self.key_buffer = bytearray(10)
# Initialize app state here
def handle_input(self):
if not picocalc.terminal:
return False
count = picocalc.terminal.readinto(self.key_buffer)
if not count:
return False
key_data = bytes(self.key_buffer[:count])
if key_data == KEY_ESC:
return "EXIT"
# Handle other keys here
return True
def draw(self):
self.display.fill(0)
# Draw app content here
self.display.show()
def run(self):
try:
while True:
result = self.handle_input()
if result == "EXIT":
break
# Update logic here
self.draw()
utime.sleep_ms(50)
except KeyboardInterrupt:
pass
# Cleanup
self.display.fill(0)
self.display.show()
def main():
gc.collect()
try:
print(f"Free memory: {gc.mem_free()} bytes")
app = AppName()
app.run()
except Exception as e:
print(f"Error: {e}")
import sys
sys.print_exception(e)
if __name__ == "__main__":
main()
Display API Quick Reference
picocalc.display is a PicoDisplay (extends framebuf.FrameBuffer). Screen: 320x320 pixels.
| Method | Description |
|---|---|
fill(color) | Clear entire screen |
fill_rect(x, y, w, h, color) | Filled rectangle |
rect(x, y, w, h, color) | Outline rectangle |
text(string, x, y, color) | 6x8 font text (53 cols x 40 rows) |
hline(x, y, w, color) | Horizontal line |
vline(x, y, h, color) | Vertical line |
line(x1, y1, x2, y2, color) | Arbitrary line |
pixel(x, y, color) | Single pixel |
show() | Push framebuffer to screen (required after drawing) |
Input Handling
Read keys via picocalc.terminal.readinto(buf). Returns byte count or None.
Match key data as bytes:
- Arrow keys:
b'\x1b[A'(up),b'\x1b[B'(down),b'\x1b[D'(left),b'\x1b[C'(right) - ESC:
b'\x1b\x1b'(always provide as exit path) - Single chars:
self.key_buffer[0] == ord('p')whencount == 1 - ENTER:
b'\r\n'
Audio Pattern
audio_left = PWM(Pin(28))
audio_right = PWM(Pin(27))
audio_left.freq(440) # Set frequency in Hz
audio_left.duty_u16(16384) # 25% duty = moderate volume
audio_left.duty_u16(0) # Stop sound
Always clean up PWM (set duty to 0) on exit.
Flicker-Free Drawing
beginDraw() helps but does NOT fully eliminate flicker due to LCD scan-line timing. The real fix is to avoid fill(0) in the draw loop entirely. Three patterns, best to worst:
Pattern 1: Static + animate (BEST — zero flicker)
Draw static content once, then only update the parts that change each frame:
def _draw_static(self):
"""Called once on mode entry."""
self.display.beginDraw()
self.display.fill(0)
self.display.text("TITLE", 100, 4, 15)
# ... draw all static content ...
self.display.show()
self._static_drawn = True
def draw(self):
if not self._static_drawn:
self._draw_static()
# Only update the changing element — no fill(0)!
self.display.fill_rect(0, 300, 320, 12, 0) # erase small area
self.display.text(f"Score: {self.score}", 4, 302, 15)
self.display.show()
Pattern 2: Erase-move-draw (GOOD — minimal flicker)
For moving objects, erase at old position, draw at new position:
def draw(self):
# Erase at old position
self.display.fill_rect(self.old_x, self.old_y, w, h, 0)
# Update position
self.old_x, self.old_y = self.x, self.y
self.x += self.dx
# Draw at new position
self.display.fill_rect(self.x, self.y, w, h, color)
self.display.show()
Pattern 3: Full-width overwrite (OK for bands/rows)
If content fills the full width, no clear needed — bands overwrite each other:
def draw(self):
for row in range(16):
y = 20 + row * 18
shade = (row + self.offset) % 16
self.display.fill_rect(0, y, 320, 18, shade) # overwrites previous
self.display.show()
Pattern 4: beginDraw + fill (LAST RESORT — may flicker)
Only use when the entire screen changes and none of the above patterns work:
self.display.beginDraw()
self.display.fill(0)
# ... draw everything ...
self.display.show()
This reduces flicker but horizontal banding may still be visible due to Core 1 / LCD scan-line race.
Exit handling
Do NOT call fill(0); show() on exit — it causes a black screen flash. Just return and let the menu redraw.
Deploying Apps
Dashboard (recommended): python3 MicroPython/tools/dashboard.py — web UI with drag-and-drop upload, one-click deploy, file diff, and in-browser editor.
Manual: mpremote resume cp my_app.py :/sd/py_scripts/my_app.py
Use resume flag to avoid soft-reset re-triggering the menu.
Critical Constraints
- Call
gc.collect()at app start and periodically during long operations - Always provide ESC key exit path back to menu
- Wrap top-level in try/except with
sys.print_exception(e) - Do NOT call
fill(0); show()on exit — just return, the menu redraws itself - Stop audio PWM (
duty_u16(0)) on exit - Scripts go in
/sd/py_scripts/for auto-discovery by py_run.py menu - Use
utimenottimeforticks_ms,ticks_diff,sleep_ms - Use
urandomnotrandomfor random numbers - Memory is limited (~190KB heap): prefer bytearray reuse, avoid large allocations
- Colors are 4-bit grayscale (0-15), NOT RGB values
- Coordinates: x=0..319, y=0..319
Additional Resources
For detailed patterns and templates, consult:
- references/app-template.md - Full annotated app template with Sound class
- references/display-api.md - Complete display API reference
- references/input-handling.md - All key codes and VT100 sequences
- references/audio-patterns.md - PWM audio, tones, melodies, sound effects
- references/color-palette.md - 4-bit grayscale conventions