ML-166: First working implementation

This commit is contained in:
Claudio Ortolina
2026-05-05 14:04:35 +01:00
parent be76d0f214
commit 9d0ab1ffc6
7 changed files with 1580 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
# Presto Music Library - Records On This Day
A MicroPython application for the [Pimoroni Presto](https://shop.pimoroni.com/products/presto?variant=54894104019323) (RP2350B, 720×720 touch display, WiFi) that connects to the Music Library API and lets you browse your record collection by release date.
## Features
- **Month calendar** — 7×6 grid with day-of-week headers, today highlight
- **Tap a day** — see records released on that date with cover art, titles, and artist names
- **Month navigation** — arrow buttons to browse previous/future months
- **WiFi auto-connect** — connects on boot with retry logic
- **NTP time sync** — calendar always shows the correct date
- **Error handling** — graceful messages when the server is unreachable or images fail to load
## Requirements
- Pimoroni Presto running stock firmware (MicroPython + PicoGraphics + touch driver)
- WiFi network with internet access
- A valid API token for the Music Library API
## Setup
### 1. Create secrets.py
```bash
cp config.example.py secrets.py
```
Edit `secrets.py` and fill in your credentials:
```python
WIFI_SSID = "your-wifi-ssid"
WIFI_PASSWORD = "your-wifi-password"
API_TOKEN = "your-api-token"
```
> `secrets.py` is git-ignored and never committed.
### 2. Deploy to your Presto
Copy the files to the root of your Presto's flash storage. You can use Thonny, `mpremote`, or any MicroPython file transfer tool.
**Using `mpremote`:**
```bash
# Install mpremote if you don't have it
pip install mpremote
# Copy files to the Presto
mpremote fs cp main.py :main.py
mpremote fs cp secrets.py :secrets.py
# Reset the device to start the app
mpremote reset
```
**Using Thonny:**
1. Open Thonny and select "MicroPython (Raspberry Pi Pico)" as the interpreter
2. Open `main.py` and `secrets.py` in Thonny
3. Use File → Save Copy → Raspberry Pi Pico to save each file to the device
4. Press the reset button on the Presto, or use Run → Send EOF / Soft Reboot
### 3. The app starts automatically
The Presto runs `main.py` on boot. The app will:
1. Show a startup screen
2. Connect to WiFi (showing progress)
3. Sync time via NTP
4. Display the current month calendar
## Usage
### Month view
- **Today's date** is highlighted in blue
- Tap **← →** arrows to navigate months
- Tap any **day cell** to see records released on that date
### Day view
- Shows up to 4 records per screen with **cover art**, **title**, and **artists**
- Tap **< Back** to return to the month view
- If there are more than 4 records, use **^ v** arrows to scroll
### Error states
- **"Could not reach server"** — the API is unreachable. Tap anywhere on the day view to retry, or tap Back to return to the month view.
- **"WiFi connection failed"** — WiFi credentials are wrong or the network is down. The app retries every 10 seconds.
- **Grey placeholder** in place of cover art — the image could not be loaded (network issue or invalid URL). The rest of the record info is still shown.
## Files
```
presto/
main.py # Application entry point (auto-runs on boot)
config.example.py # Template for secrets.py
secrets.py # Your credentials (git-ignored, create from example)
README.md # This file
```
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Stuck on "secrets.py not found" | `secrets.py` not on the device | Copy `config.example.py` to `secrets.py` and deploy it |
| Stuck on "WiFi connection failed" | Wrong SSID/password, or network down | Check `secrets.py` credentials; verify network is up |
| "Could not reach server" on day tap | API token invalid, or server down | Check `API_TOKEN` in `secrets.py`; verify the server is running |
| Images show as grey rectangles | JPEG decoder not available, or image URLs unreachable | Check firmware version; ensure Presto has internet access |
| Touch not responding | Firmware touch API mismatch | Try updating Presto firmware to the latest version |
## API Endpoint
The app talks to:
```
GET https://music-library.claudio-ortolina.org/api/v1/collection/on_this_day?date=YYYY-MM-DD
Authorization: Bearer <API_TOKEN>
```
Response format:
```json
{
"records": [
{
"id": "...",
"title": "Album Title",
"artists": ["Artist Name"],
"thumb_url": "https://.../api/v1/assets/thumb.jpg?width=480",
"release_date": "1973-03-01",
"genres": ["Rock"],
"format": "Vinyl"
}
]
}
```
## License
This application is part of the Music Library project.
+20
View File
@@ -0,0 +1,20 @@
# config.example.py — Template for secrets.py
# ==============================================
# Copy this file to secrets.py and fill in your values.
# secrets.py is git-ignored; never commit it.
#
# To deploy to your Presto:
# 1. Copy this file: cp config.example.py secrets.py
# 2. Edit secrets.py with your credentials
# 3. Copy secrets.py to the Presto alongside main.py
#
# --- Required variables ---
# Your WiFi network name (SSID)
WIFI_SSID = "your-wifi-ssid"
# Your WiFi password
WIFI_PASSWORD = "your-wifi-password"
# Music Library API token (get it from the app admin)
API_TOKEN = "your-api-token"
+10
View File
@@ -0,0 +1,10 @@
[tools]
"uv" = "latest"
"pipx:mpremote" = "latest"
[tasks.presto]
run = '''
mpremote fs cp records_on_the_day.py :main.py
mpremote reset
'''
dir = "{{cwd}}"
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
# ICON schedule
# NAME Word Clock
# DESC No hands!
import time
import machine
import ntptime
import pngdec
from presto import Presto
# Setup for the Presto display
presto = Presto()
display = presto.display
WIDTH, HEIGHT = display.get_bounds()
BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(200, 200, 200)
GRAY = display.create_pen(30, 30, 30)
# Clear the screen before the network call is made
display.set_pen(BLACK)
display.clear()
presto.update()
# Length of time between updates in minutes.
UPDATE_INTERVAL = 15
rtc = machine.RTC()
time_string = None
words = ["it", "d", "is", "m", "about", "lv", "half", "c", "quarter", "b", "to", "past", "n", "one",
"two", "three", "four", "five", "six", "eleven", "ten", "d", "qdh", "eight", "seven", "rm", "twelve", "nine", "p", "ncsnheypresto", "O'Clock", "agrdsp"]
def show_message(text):
display.set_pen(BLACK)
display.clear()
display.set_pen(WHITE)
display.text(f"{text}", 5, 10, WIDTH, 2)
presto.update()
show_message("Connecting...")
try:
wifi = presto.connect()
except ValueError as e:
while True:
show_message(e)
except ImportError as e:
while True:
show_message(e)
# Set the correct time using the NTP service.
try:
ntptime.settime()
except OSError:
while True:
show_message("Unable to get time.\n\nCheck your network try again.")
def approx_time(hours, minutes):
nums = {0: "twelve", 1: "one", 2: "two",
3: "three", 4: "four", 5: "five", 6: "six",
7: "seven", 8: "eight", 9: "nine", 10: "ten",
11: "eleven", 12: "twelve"}
if hours == 12:
hours = 0
if minutes >= 0 and minutes < 8:
return "it is about " + nums[hours] + " O'Clock"
if minutes >= 8 and minutes < 23:
return "it is about quarter past " + nums[hours]
if minutes >= 23 and minutes < 38:
return "it is about half past " + nums[hours]
if minutes >= 38 and minutes < 53:
return "it is about quarter to " + nums[hours + 1]
return "it is about " + nums[hours + 1] + " O'Clock"
def update():
global time_string
# grab the current time from the ntp server and update the Pico RTC
try:
ntptime.settime()
except OSError:
print("Unable to contact NTP server")
current_t = rtc.datetime()
time_string = approx_time(current_t[4] - 12 if current_t[4] > 12 else current_t[4], current_t[5])
# Splits the string into an array of words for displaying later
time_string = time_string.split()
print(time_string)
def draw():
global time_string
display.set_font("bitmap8")
display.set_layer(1)
# Clear the screen
display.set_pen(BLACK)
display.clear()
default_x = 25
x = default_x
y = 35
line_space = 20
letter_space = 15
margin = 25
scale = 1
spacing = 1
for word in words:
if word in time_string:
display.set_pen(WHITE)
else:
display.set_pen(GRAY)
for letter in word:
text_length = display.measure_text(letter, scale, spacing)
if not x + text_length <= WIDTH - margin:
y += line_space
x = default_x
display.text(letter.upper(), x, y, WIDTH, scale=scale, spacing=spacing)
x += letter_space
presto.update()
# Set the background in layer 0
# This means we don't need to decode the image every frame
display.set_layer(0)
try:
p = pngdec.PNG(display)
p.open_file("wordclock_background.png")
p.decode(0, 0)
except OSError:
display.set_pen(BLACK)
display.clear()
while True:
update()
draw()
time.sleep(60 * UPDATE_INTERVAL)