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.

MethodDescription
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') when count == 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 utime not time for ticks_ms, ticks_diff, sleep_ms
  • Use urandom not random for 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: