trmnl-rs
A Rust framework for building TRMNL BYOS (Bring Your Own Server) applications.
What is TRMNL?
TRMNL is an e-ink display device with an ESP32-C3 microcontroller and 7.5" screen. It connects to WiFi and periodically polls a server for content to display.
Terminology:
- BYOS (Bring Your Own Server) - You have a TRMNL device and point it at your own server instead of TRMNL's cloud
- BYOD (Bring Your Own Device) - You have your own e-ink hardware (not TRMNL) running TRMNL firmware
When to Use This Crate
Use this crate if you want to:
- Run your own server instead of using TRMNL's cloud
- Have complete control over what your display shows
- Integrate private data sources (home automation, internal APIs, databases)
- Build in Rust (there are also Ruby and PHP implementations)
Don't use this crate if you:
- Want to use TRMNL's cloud with webhooks and Liquid templates (just use their cloud)
- Don't have a server to host your BYOS endpoint
Getting Started
1. Get Your Device Ready
If you have a TRMNL device:
- Purchase from usetrmnl.com
- During WiFi setup, configure it to point to your server URL instead of TRMNL's cloud
- See TRMNL's BYOS guide for device configuration
If you're bringing your own device (BYOD):
- Flash your ESP32-based e-ink display with TRMNL firmware
- Configure it to point to your server
- See TRMNL's BYOD guide
2. Set Up Your Server
Add to your Cargo.toml:
[]
= { = "0.1", = ["axum", "render"] }
= "0.8"
= { = "1", = ["full"] }
Create a minimal server (see full examples below):
use ;
use ;
async
async
3. Configure Your Device
Point your device to https://yourserver.com/api/display. The device will poll this endpoint and display whatever image URL you return.
Official TRMNL Resources
- TRMNL API Documentation - Complete API reference
- BYOS Setup Guide - Official BYOS documentation
- BYOD Setup Guide - Bring your own device
- How It Works - Device architecture
- Firmware Source - Open source firmware (MIT)
How BYOS Works
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ TRMNL │ GET │ Your Server │ fetch │ Your Data │
│ Device │ ──────► │ (built with │ ◄─────► │ Sources │
│ │ ◄────── │ this crate) │ │ │
└─────────────┘ JSON └─────────────────┘ └─────────────┘
+ PNG
Your device polls your server every N seconds. Your server returns a JSON response pointing to a PNG image. The device downloads and displays it.
Where Can You Run Your Server?
This crate helps you build a BYOS server - a Rust binary you can run anywhere:
| Deployment | Notes |
|---|---|
| Home server | Raspberry Pi, NAS, old laptop - works great |
| VPS | DigitalOcean, Linode, Hetzner, etc. |
| Cloud | AWS, GCP, Azure, Fly.io, Railway |
| Local machine | For development/testing |
Requirements:
- Your server must be reachable from your TRMNL device (same network or public internet)
- If using HTML rendering (
renderfeature), Chrome/Chromium must be installed - HTTPS recommended for public deployments (device supports both HTTP and HTTPS)
Home server tips:
- Use a static local IP or hostname (e.g.,
http://192.168.1.100:3000orhttp://myserver.local:3000) - For access outside your home, set up port forwarding or use a tunnel (Cloudflare Tunnel, Tailscale, ngrok)
- Raspberry Pi 4 handles HTML rendering fine; Pi Zero may struggle with Chrome
Choose Your Setup
Option A: Dynamic HTML Rendering (Most Common)
Best for: Dashboards, data displays, anything that changes frequently.
[]
= { = "0.1", = ["axum", "render"] }
= "0.8"
= { = "1", = ["full"] }
use ;
use ;
use ;
async
async
Requirements: Chrome or Chromium installed on your server.
Option B: Static/Pre-generated Images
Best for: Simple displays, images generated elsewhere, or when you can't install Chrome.
[]
= { = "0.1", = ["axum"] }
= "0.8"
= { = "1", = ["full"] }
use ;
use ;
async
API Reference
DeviceInfo
Automatically extracted from request headers when using axum:
async
DisplayResponse
// Minimal
new
// With options
new
.with_refresh_rate // Seconds until next poll (default: 60)
.with_firmware_update // Trigger OTA
.with_reset // Reset device
Important: The filename must change when your image changes. The device compares filenames to detect updates. Use timestamps:
let filename = format!;
Render Config
use RenderConfig;
let config = RenderConfig ;
BYOS Protocol
Your server implements:
| Endpoint | Method | Required | Purpose |
|---|---|---|---|
/api/display |
GET | Yes | Returns image URL |
/api/setup |
GET | No | Device registration |
/api/log |
POST | No | Receive device logs |
The device sends these headers:
ID: MAC addressBattery-Voltage: e.g., "4.2"FW-Version: Firmware versionRSSI: WiFi signal strengthRefresh-Rate: Current refresh rate
Authentication (Optional)
By default, BYOS endpoints are public—anyone who knows your URL can access them. The device's MAC address (in the ID header) identifies the device but doesn't authenticate it.
This crate provides optional token-based authentication via query parameters:
Setup
-
Configure your device URL with a token:
https://yourserver.com/api/display?token=your-secret-token -
Set the token on your server (environment variable):
-
Validate it in your handler:
use ; use ; async
TokenAuth Methods
// Validate against a specific value
auth.validate?;
// Validate against environment variable (if not set, allows all requests)
auth.validate_env?;
// Check if a token was provided (without validating)
if auth.has_token
// Manual extraction (for non-axum use)
let auth = from_query_string;
Changing the Device URL
The BYOS URL (including any ?token= parameter) is configured on the device during WiFi setup. To change it:
- Factory reset the device (hold button for 10+ seconds until LED flashes)
- Re-run WiFi setup through the TRMNL app
- Enter the new BYOS URL with your token when prompted
There's no way to change the BYOS URL without re-running WiFi setup—it's baked into the device's firmware configuration.
Important: If you add token authentication to an existing BYOS setup, your device will start getting 401 errors until you update the URL on the device.
Device URL Format
When configuring your device, use this URL format:
https://yourserver.com?token=your-secret-token
The device will automatically append /api/display, /api/log, etc. to this base URL.
Display Constraints
- Resolution: 800×480 pixels (fixed)
- Max file size: 90KB (device rejects larger)
- Format: PNG
- Colors: 16 or fewer for best e-ink rendering
- Orientation: Landscape only
Battery Life
The TRMNL device uses a LiPo battery (3.0V-4.2V range). Battery drain depends primarily on refresh rate:
| Refresh Rate | Polls/Day | Expected Battery Life |
|---|---|---|
| 60s (1 min) | 1,440 | ~3-5 days |
| 300s (5 min) | 288 | ~2-3 weeks |
| 900s (15 min) | 96 | ~1-2 months |
| 1800s (30 min) | 48 | ~2-3 months |
| 3600s (1 hr) | 24 | ~3-4 months |
Tips for extending battery life:
- Use longer refresh rates for static content (weather, quotes)
- Use shorter rates only for time-sensitive data (transit, meetings)
- The device reports battery voltage in the
Battery-Voltageheader - Use
device.battery_percentage()to display remaining charge
Building Text Dashboards
For text-heavy dashboards (tasks, calendars, briefings), use HTML with Chrome headless rendering. The key is fixed pixel positioning—Chrome headless doesn't handle flexbox reliably.
Dashboard Design Principles
- Fixed dimensions: Always set
width: 800px; height: 480pxon body - Absolute positioning: Use
position: absolutefor major sections - High contrast: Black text on white background only
- No images: Text renders sharper on e-ink than images
- Generous spacing: E-ink needs more whitespace than LCD
Example Layout
┌────────────────────────────────────────────────────────────┐
│ Header: Date/Time (left) Weather (right) │
├────────────────────────────┬───────────────────────────────┤
│ │ │
│ Left Column │ Right Column │
│ - Status/metrics │ - Briefing text │
│ - Task list │ - Quotes/highlights │
│ - Calendar/meetings │ - News/updates │
│ │ │
├────────────────────────────┴───────────────────────────────┤
│ Footer: Battery (left) Message (center) │
└────────────────────────────────────────────────────────────┘
Example Template
See templates/dashboard.html for a complete example with:
- Two-column layout with header and footer
- Task lists with due dates
- Meeting schedules
- Body text sections
- Quote/highlight sections
Font Size Guidelines
| Element | Size | Use For |
|---|---|---|
| 20px | Headers | Date, main titles |
| 18px | Subheaders | Time, weather |
| 16px | Emphasis | Key values, footer message |
| 14-15px | Body | Section titles, quotes |
| 12-13px | Details | Task items, body text |
| 11px | Meta | Timestamps, sources |
CSS Template
}
}
}
}
}
}
Refresh Rate Scheduling
The schedule feature lets you configure different refresh rates based on time of day and day of week. This helps optimize battery life while keeping displays fresh when needed.
[]
= { = "0.1", = ["axum", "schedule"] }
Schedule Configuration (YAML)
Create a schedule config file:
# config/schedule.yaml
timezone: "America/New_York"
default_refresh_rate: 300 # 5 minutes (fallback if no rule matches)
schedule:
# Sleep hours - infrequent updates to save battery
- days: all
start: "23:00"
end: "06:00"
refresh_rate: 1800 # 30 minutes
# Morning routine - frequent updates
- days: weekdays
start: "06:00"
end: "09:00"
refresh_rate: 60 # 1 minute
# Work hours - moderate updates
- days: weekdays
start: "09:00"
end: "18:00"
refresh_rate: 120 # 2 minutes
# Weekend - relaxed
- days: weekends
start: "06:00"
end: "23:00"
refresh_rate: 600 # 10 minutes
Day Selectors
all- Every dayweekdays- Monday through Fridayweekends- Saturday and Sunday["mon", "wed", "fri"]- Specific days (list format)monday/mon- Single day
Usage
Option 1: Global schedule (recommended for most apps)
use ;
async
async
Option 2: Manual schedule management
use RefreshSchedule;
// Load schedule at startup
let schedule = load?;
// In your display handler
async
Time Ranges
- Normal ranges:
09:00to17:00matches 9am-5pm - Overnight ranges:
23:00to06:00matches 11pm-6am (spans midnight) - End time is exclusive:
09:00to17:00does not include exactly 17:00
Feature Flags
| Feature | Dependencies Added | Use When |
|---|---|---|
axum |
axum, http | Building a web server (most users) |
render |
tokio | Generating images from HTML (requires Chrome) |
schedule |
chrono, chrono-tz, serde_yaml | Time-based refresh rate scheduling |
full |
All of the above | You want everything |
Examples
See the examples/ directory:
basic_byos.rs- Minimal BYOS serverwith_render.rs- HTML rendering example
Run with:
License
MIT