Add CYD photo album with jpg/png/gif viewer and sample assets.
Ship TF-card album UI, half-res decode canvas, prebuilt firmware, and documented flash/upload workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
75
tools/make_test_photos.py
Normal file
75
tools/make_test_photos.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate 320x240 JPG/PNG/GIF samples into assets/photos."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "assets" / "photos"
|
||||
W, H = 320, 240
|
||||
|
||||
|
||||
def font(size: int = 28) -> ImageFont.ImageFont:
|
||||
for name in (
|
||||
r"C:\Windows\Fonts\msyh.ttc",
|
||||
r"C:\Windows\Fonts\arial.ttf",
|
||||
r"C:\Windows\Fonts\consola.ttf",
|
||||
):
|
||||
try:
|
||||
return ImageFont.truetype(name, size)
|
||||
except OSError:
|
||||
pass
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None:
|
||||
im = Image.new("RGB", (W, H), bg)
|
||||
d = ImageDraw.Draw(im)
|
||||
for i in range(0, H, 12):
|
||||
c = tuple(max(0, min(255, v + (i % 24) - 12)) for v in accent)
|
||||
d.rectangle((0, i, W, i + 6), fill=c)
|
||||
d.rectangle((16, 40, W - 16, H - 40), fill=bg)
|
||||
d.rectangle((16, 40, W - 16, H - 40), outline=accent, width=3)
|
||||
d.text((28, 70), title, fill=(234, 240, 246), font=font(36))
|
||||
d.text((28, 130), subtitle, fill=(122, 139, 156), font=font(20))
|
||||
d.text((28, 170), "CYD ALBUM TEST", fill=accent, font=font(16))
|
||||
if fmt == "JPEG":
|
||||
im.save(path, "JPEG", quality=90)
|
||||
else:
|
||||
im.save(path, "PNG")
|
||||
print(f"wrote {path} ({path.stat().st_size} bytes)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
card((10, 16, 22), (79, 195, 247), "01 JPG", "tap left / right", OUT / "01_blue.jpg", "JPEG")
|
||||
card((20, 12, 28), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG")
|
||||
card((8, 28, 20), (61, 220, 151), "03 PNG", "green sample", OUT / "03_green.png", "PNG")
|
||||
|
||||
frames = []
|
||||
for i in range(6):
|
||||
im = Image.new("RGB", (W, H), (10, 16, 22))
|
||||
d = ImageDraw.Draw(im)
|
||||
d.text((24, 24), "04 GIF", fill=(234, 240, 246), font=font(32))
|
||||
d.text((24, 70), "animated", fill=(122, 139, 156), font=font(18))
|
||||
x = 20 + i * 40
|
||||
d.rectangle((x, 120, x + 80, 180), fill=(255, 200, 40))
|
||||
d.rectangle((20, 200, 300, 210), fill=(36, 49, 64))
|
||||
frames.append(im)
|
||||
gif_path = OUT / "04_bounce.gif"
|
||||
frames[0].save(
|
||||
gif_path,
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
duration=120,
|
||||
loop=0,
|
||||
optimize=False,
|
||||
)
|
||||
print(f"wrote {gif_path} ({gif_path.stat().st_size} bytes)")
|
||||
print("DONE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
150
tools/sd_put.py
Normal file
150
tools/sd_put.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Upload a file to the CYD TF card over serial (#PUT + ACK window)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
except ImportError:
|
||||
print("Need pyserial: pip install pyserial", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
WIN = 4096
|
||||
|
||||
|
||||
def pick_port(preferred: str | None) -> str:
|
||||
if preferred:
|
||||
return preferred
|
||||
ports = list(list_ports.comports())
|
||||
for p in ports:
|
||||
desc = (p.description or "") + (p.hwid or "")
|
||||
if "USB" in desc.upper() or "UART" in desc.upper() or "CP210" in desc.upper() or "CH340" in desc.upper():
|
||||
return p.device
|
||||
if ports:
|
||||
return ports[0].device
|
||||
raise SystemExit("No serial port found")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("file", type=Path, help="Local file to upload")
|
||||
ap.add_argument("--port", default=None)
|
||||
ap.add_argument("--baud", type=int, default=460800)
|
||||
ap.add_argument("--remote", default="/paomo.mp3")
|
||||
args = ap.parse_args()
|
||||
|
||||
path: Path = args.file
|
||||
if not path.is_file():
|
||||
print(f"Missing file: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
size = path.stat().st_size
|
||||
port = pick_port(args.port)
|
||||
print(f"Port {port} {path.name} -> {args.remote} ({size} bytes)", flush=True)
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = args.baud
|
||||
ser.timeout = 1
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
# 硬复位 ESP32
|
||||
ser.rts = True
|
||||
time.sleep(0.1)
|
||||
ser.rts = False
|
||||
print("waiting BOOT_OK...", flush=True)
|
||||
deadline = time.time() + 12
|
||||
booted = False
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if "BOOT_OK" in line:
|
||||
booted = True
|
||||
break
|
||||
if not booted:
|
||||
print("warn: BOOT_OK not seen, continuing", flush=True)
|
||||
time.sleep(0.5)
|
||||
ser.reset_input_buffer()
|
||||
|
||||
cmd = f"#PUT {args.remote} {size}\n".encode("ascii")
|
||||
print(f"send {cmd!r}", flush=True)
|
||||
ser.write(cmd)
|
||||
ser.flush()
|
||||
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if line == "READY":
|
||||
break
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 2
|
||||
else:
|
||||
print("Timeout waiting READY", file=sys.stderr, flush=True)
|
||||
return 3
|
||||
|
||||
sent = 0
|
||||
t0 = time.time()
|
||||
with path.open("rb") as f:
|
||||
while sent < size:
|
||||
chunk = f.read(min(WIN, size - sent))
|
||||
if not chunk:
|
||||
break
|
||||
ser.write(chunk)
|
||||
ser.flush()
|
||||
sent += len(chunk)
|
||||
|
||||
# 等 ACK <sent>
|
||||
ack_deadline = time.time() + 30
|
||||
acked = False
|
||||
while time.time() < ack_deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if not line:
|
||||
continue
|
||||
print("<", line, flush=True)
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 4
|
||||
if line.startswith("ACK "):
|
||||
try:
|
||||
got = int(line.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
continue
|
||||
if got >= sent:
|
||||
acked = True
|
||||
break
|
||||
if line == "DONE" and sent >= size:
|
||||
print("OK", flush=True)
|
||||
return 0
|
||||
if not acked:
|
||||
print(f"Timeout waiting ACK after {sent}", file=sys.stderr, flush=True)
|
||||
return 4
|
||||
|
||||
if sent % (256 * 1024) < WIN or sent == size:
|
||||
rate = sent / max(time.time() - t0, 0.001)
|
||||
print(f"sent {sent}/{size} ({rate/1024:.1f} KB/s)", flush=True)
|
||||
|
||||
deadline = time.time() + 30
|
||||
while time.time() < deadline:
|
||||
line = ser.readline().decode("utf-8", errors="ignore").strip()
|
||||
if line:
|
||||
print("<", line, flush=True)
|
||||
if line == "DONE":
|
||||
print("OK", flush=True)
|
||||
return 0
|
||||
if line.startswith("PUT_ERR"):
|
||||
return 5
|
||||
|
||||
print("Timeout waiting DONE", file=sys.stderr, flush=True)
|
||||
return 6
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user