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.
Generate MicroPython applications for the Clockwork Pi PicoCalc (RP2350, 320x320 LCD, membrane keyboard, SD card, audio PWM).
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()
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) |
Read keys via picocalc.terminal.readinto(buf). Returns byte count or None.
Match key data as bytes:
b'\x1b[A' (up), b'\x1b[B' (down), b'\x1b[D' (left), b'\x1b[C' (right)b'\x1b\x1b' (always provide as exit path)self.key_buffer[0] == ord('p') when count == 1b'\r\n'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.
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:
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()
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()
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()
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.
Do NOT call fill(0); show() on exit — it causes a black screen flash. Just return and let the menu redraw.
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.
gc.collect() at app start and periodically during long operationssys.print_exception(e)fill(0); show() on exit — just return, the menu redraws itselfduty_u16(0)) on exit/sd/py_scripts/ for auto-discovery by py_run.py menuutime not time for ticks_ms, ticks_diff, sleep_msurandom not random for random numbersFor detailed patterns and templates, consult: