A mod for the Poem/1 e-paper device that displays AI-generated poems overlaid on photographs of stopped public clocks from stoppedclocks.org.
The Poem/1 is an e-paper device by Acts Not Facts that displays time-specific AI-generated poetry. This mod replaces the standard display with photographs of stopped clocks showing the current time, with the poem overlaid in detected whitespace zones.
At 10:15, the device shows a photograph of a clock stopped at 10:15, with a poem about 10:15 rendered on the image.
┌─────────────────────────────────────────────────────────────────────┐
│ Poem/1 Device │
│ (M5PaperS3 / ESP32-S3) │
└─────────────────────────────────────────────────────────────────────┘
│
│ Every minute:
│
┌───────────────────────┴───────────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ poem.town API │ │ stoppedclocks.org │
│ │ │ │
│ POST /api/v1/ │ │ GET /living-clock/ │
│ clock/compose │ │ living-clock-index. │
│ │ │ json │
│ Returns: │ │ │
│ - poem text │ │ Returns: │
│ - font preference │ │ - image URLs │
└───────────────────┘ │ - text zone coords │
└───────────────────────┘
Target Device: Poem/1 by Acts Not Facts (M5PaperS3 variant)
| Specification | Value |
|---|---|
| MCU | ESP32-S3 with PSRAM |
| Display | ED047TC2 4.7" e-paper (960x540) |
| Color depth | 4-bit grayscale |
| Interface | 8-bit parallel EPD bus |
| Memory | 16MB flash, 8MB PSRAM |
Important: The physical button is on GPIO 2, not GPIO 38 as M5Stack documentation states. This was verified through GPIO scanning.
| Action | Function |
|---|---|
| Single click | Show/dismiss notes from poem.town dashboard |
| Double click | Like the current poem |
Notes are displayed full-screen with dynamic font sizing (72→24px) to fit the content. When a note is displayed, the firmware automatically sends a "seen" receipt to the poem.town API.
The M5PaperS3's GT911 touch controller doesn't respond on some units, which breaks M5GFX auto-detection. This firmware uses a custom LGFX_M5PaperS3 class that directly initializes the display hardware:
class LGFX_M5PaperS3 : public lgfx::LGFX_Device {
// Explicit Bus_EPD and Panel_EPD configuration
// Bypasses GT911 detection failure
};- Images served via CloudFront CDN
- Downloaded to PSRAM buffer before rendering (stream-based loading unreliable)
- Source images: 480x270 PNG, displayed at 2x scale (960x540)
Each clock image has pre-analyzed whitespace zones for poem placement:
{
"t": "0930",
"i": [{
"url": "https://stoppedclocks.org/living-clock/images/0930_clock-name.png",
"tz": { "x": 0, "y": 0, "w": 256, "h": 296 },
"strip": "top",
"rec": "ZONE_TOP"
}]
}Zone analysis uses dither density detection (8x8 cell blocks) to find areas suitable for text overlay.
The firmware supports custom TrueType fonts via OpenFontRender:
- Inter - Clean sans-serif for modern poems
- Playfair Display - Elegant serif for classic poems
- Fonts downloaded from CDN at boot (~500KB total)
- poem.town API returns
fontfield ("INTER" or "PLAYFAIR") - Font preference controlled via poem.town dashboard
- Dynamic font sizing: tries sizes 72→24px, picks largest that fits
The firmware automatically:
- Downloads and caches TTF fonts to PSRAM
- Re-wraps poem text using actual font metrics
- Tries font sizes 72→24px, picks largest that fits within 75% of zone
- Centers text in the detected whitespace zone
- Falls back to strip positioning if no zone detected
- Only reloads fonts when preference changes (prevents memory leaks)
Clock faces show 12-hour format without AM/PM. The matching algorithm:
- Converts 24-hour current time to 12-hour
- Finds closest clock image (handles wraparound)
- Treats 10:15 clock as valid for both 10:15 AM and 10:15 PM
The firmware displays an elegant loading screen during boot with:
- Vintage clock graphic - Decorative bezel, dot hour markers, thick elegant hands at 07:07
- Large typography - "Poem/1" title at 8x scale, "Stopped Clocks Mod" subtitle at 3x
- Asymmetric layout - Clock on left, text on right with decorative separator line
- Status messages - Shows "Loading fonts...", "Loading clock index...", etc.
Notes from the poem.town dashboard are displayed full-screen when the button is pressed:
- Dynamic font sizing - Tries sizes 72→24px, picks largest that fits
- Proper text metrics - Accounts for font ascender/descender space
- Vertical centering - Text block centered within margins (30px top/bottom)
- Auto-dismiss - Returns to clock view after 10 seconds or on button press
- Read receipts - Automatically marks notes as "seen" via API
living-clock/
├── firmware/
│ ├── src/main.cpp # Main firmware (~1000 lines)
│ └── platformio.ini # Build configuration
├── generate_text_zones.py # Whitespace analysis tool
├── create-combined-index.js # Index builder
├── text-zones.json # Per-image zone data
├── living-clock-index.json # Combined time→image→zone index
├── LICENSE # MIT License
└── README.md # This file
- PlatformIO CLI or VSCode extension
- USB-C cable for flashing
Edit firmware/src/main.cpp:
const char* WIFI_SSID = "your-wifi-ssid";
const char* WIFI_PASS = "your-wifi-password";cd firmware
pio run -t uploadpio device monitor --baud 115200POST https://poem.town/api/v1/clock/compose
Authorization: Bearer <token>
Content-Type: application/json
{ "screenId": "...", "time24": "09:30" }
Returns time-specific rhyming poem with font preference ("INTER" or "PLAYFAIR"), plus any pending notes.
Note: The screenId must be the device MAC address in reverse byte order to match the poem.town dashboard format.
POST https://poem.town/api/v1/clock/notes/{noteId}/seen
Authorization: Bearer <token>
Content-Type: application/json
{ "screenId": "..." }
Marks a note as seen. Called automatically when note is displayed on device.
POST https://poem.town/api/v1/clock/likes/{poemId}/mark
Authorization: Bearer <token>
Content-Type: application/json
{ "screenId": "..." }
Records a like for the current poem. Triggered by double-click on device button.
GET https://stoppedclocks.org/living-clock/living-clock-index.json
Returns compact time→image→zone mapping for firmware consumption.
- Source: stoppedclocks.org collection (200+ UK public clocks)
- Processing: Cropped to 480x270, converted to grayscale PNG
- CDN: CloudFront at
https://stoppedclocks.org/living-clock/images/
- Inter: Downloaded from CDN (~412KB TTF)
- Playfair Display: Downloaded from CDN (~96KB TTF)
- CDN:
https://stoppedclocks.org/living-clock/fonts/
- Generation:
generate_text_zones.pyanalyzes each image - Method: Dither density analysis with 8x8 cell blocks
- Coverage: 99.6% of images have detected whitespace zones
lib_deps =
m5stack/M5GFX@^0.2.17
bblanchon/ArduinoJson@^7.0.0
https://github.com/takkaO/OpenFontRender.git- poem.town - AI poetry generation by Acts Not Facts
- Stopped Clocks - Clock photography collection by Alfie Dennen
- M5Stack - M5PaperS3 hardware and M5GFX library
- OpenFontRender - TrueType font rendering by takkaO
MIT License - see LICENSE for details.
- poem.town - The original Poem/1 service
- stoppedclocks.org - The Stopped Clocks archive
- OpenFontRender - TrueType font rendering for Arduino
Last Updated: 2025-12-15
