Custom firmware for M5Paper that turns it into a TRMNL BYOD display.
- WiFi captive portal for setup — no hardcoded credentials
- Works with official TRMNL server and custom/local servers
- Deep sleep with timer and button wake
- NVS-based persistent settings (survives reboots)
- Device registration via MAC address (TRMNL API compatible)
- Battery voltage reporting
- Automatic retry with failure counting (WiFi and server)
- Initialize RTC from TRMNL server Date header when device clock is unset (no NTP required)
- OTA firmware updates
- Daily GitHub release check (once per 24h)
- OTA safety cooldown (max one OTA attempt per 24h)
- Clone-aware OTA source (uses GitHub origin repo at build time)
- M5Paper — ESP32-D0WDQ5, 4.7" e-paper 960×540
- Power architecture: GPIO2 drives SY7088 boost converter (no PMIC)
- Wake sources: RTC timer, button (GPIO39)
Requires PlatformIO.
pio run # build
pio run -t upload # flash via USBLogging controls are split between server log submission and serial debug output:
ENABLE_SERVER_LOGS: controls sending logs to/api/log(enabled by default inplatformio.ini).DEBUG_LOGS: controls serial debug output (disabled by default inplatformio.ini).
Typical combinations:
- Production with server logs only:
ENABLE_SERVER_LOGSenabled,DEBUG_LOGSdisabled. - Local serial debugging + server logs: enable both flags.
- Silent mode: disable both flags.
- Flash the firmware
- On first boot the device starts a WiFi AP (
TRMNL-XXXX) - Connect to the AP, configure WiFi credentials and TRMNL API key
- Device reboots, registers with TRMNL, and begins refresh cycle
Hold the button on boot to re-enter setup mode.
Reset UX:
- Hold >5s after wake: clear WiFi credentials (returns to captive portal)
- Hold ~15s after wake: factory reset (clear all settings + reboot)
Each wake cycle:
- Connect to WiFi
- Fetch current display from TRMNL API
- Render image on e-paper
- Enter deep sleep (default 15 min)
The device wakes on timer expiry or button press.
The firmware supports two OTA trigger paths:
- Server-driven OTA (/api/display):
- Uses update_firmware + firmware_url when returned by the TRMNL-compatible API.
- GitHub releases OTA fallback:
- Checks releases/latest once per 24 hours.
- Downloads the asset matching
OTA_FIRMWARE_ASSET_NAMEwhen release version is newer than FW_VERSION.
Default asset naming:
OTA_FIRMWARE_ASSET_NAMEdefaults tom5paper.bin(derived fromDEVICE_MODEL ".bin").- This prevents wrong-device updates when multiple firmware assets are published in one release.
Safety rules:
- GitHub check cooldown: 24h.
- OTA attempt cooldown: 24h.
- OTA is skipped on low battery unless external power is present.
The device can automatically switch OTA channel based on the name returned by the server.
- If the server-provided device name ends with Beta (case-insensitive), the device switches to beta OTA channel.
- If Beta is removed from the end of the name, the device switches back to stable OTA channel.
- Examples that enable beta channel: TRMNL Beta, TRMNL beta, TRMNL Beta:
Where the name comes from:
- The firmware reads name fields from setup/display API payloads and updates internal OTA mode.
Channel behavior:
- Stable channel: checks stable releases only (releases/latest).
- Beta channel: checks both releases and prereleases, with priority for stable releases when both are available.
On channel switch:
- OTA cooldown state is reset, so update checks/attempts can run again immediately for the new channel.
Use the manual workflow:
- .github/workflows/prerelease.yml
How it works:
- Input base version (for example 2.6.0).
- Workflow scans existing tags matching v2.6.0-beta.N.
- It picks the next available N automatically and creates a GitHub prerelease.
Generated naming:
- Tag: v2.6.0-beta.1, v2.6.0-beta.2, ...
- Release title: v2.6.0 beta 1, v2.6.0 beta 2, ...
Published asset:
- dist/m5paper.bin (used by OTA asset selection)
At build time, PlatformIO runs a pre-script that reads git remote origin and injects:
- OTA_GITHUB_OWNER
- OTA_GITHUB_REPO
This means if someone forks/clones the project and builds from their own GitHub remote, OTA checks their repository releases automatically.
If origin cannot be parsed as a GitHub URL, firmware falls back to default:
- owner: paveldn
- repo: trmnl-m5paper
The pre-build script also reports why fallback was used:
- no remote.origin.url configured,
- non-GitHub origin host detected (for example GitLab or Bitbucket), or
- GitHub origin present but owner/repo could not be parsed.
Release workflow note:
.github/workflows/release.ymlpublishesdist/m5paper.binso OTA can select a stable, device-specific asset name.
Compatibility / Notes
- The firmware follows official TRMNL header and API behavior: it sends
IDandAccess-Tokenheaders and includes aWake-Timeheader for statistics. - Image downloads from the same TRMNL server include authentication headers so self-hosted servers should accept the same
Access-Token. - Debug logs (when enabled) are posted to
/api/login an official-compatiblelogsarray payload with fields such ascreated_at,source_path,wake_reason,battery_voltage, andfirmware_version. - Log submission is deferred on low battery.
- The device will attempt to initialize its RTC from the HTTP
Dateresponse header on first successful server contact if the RTC is unset.
- M5Paper product page
- M5Paper Arduino API
- M5Paper deep sleep / wakeup
- M5Unified library
- Community: M5Paper shutdown & deep sleep
MIT