Custom TRMNL e-ink server in LuaJIT
  • HTML 54.1%
  • Lua 39%
  • Shell 6.6%
  • Dockerfile 0.3%
Find a file
Matt Hornung 612878492d fix: guard CURL and curl_write_cb types against load order
main.lua loads curl_multi before curl, causing ffi.cdef to fail with
"declaration specifier expected" if types aren't yet declared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-10 01:47:49 +00:00
api feat: add /admin page with key display and server info 2026-07-31 15:59:18 +00:00
examples docs: add Scriptable widget example and iOS client docs 2026-07-16 14:59:51 +00:00
ffi fix: guard CURL and curl_write_cb types against load order 2026-08-10 01:47:49 +00:00
k8s refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00
plugins Add birthday countdown plugin 2026-07-27 02:23:40 +00:00
public fix: remove S3 status from admin page 2026-07-31 16:15:28 +00:00
renderer Add per-screen dimensions to render pipeline 2026-07-14 16:56:46 +00:00
scripts Add scripts/seed-plugins.sh to upload plugins to B2 on fresh deploy 2026-07-14 18:30:45 +00:00
test refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00
.env.example refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00
BUGS.md Validate MAC at setup; remove blocking render (#12, #13) 2026-07-14 21:23:09 +00:00
CHANGELOG.md chore(version): 0.4.3 2026-07-16 16:26:26 +00:00
cog.toml feat: add /api/info endpoint, VERSION file, and readiness probe 2026-07-16 01:43:00 +00:00
docker-compose.yml docs: add Quick Start section with docker compose one-liner 2026-07-16 04:17:17 +00:00
Dockerfile refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00
json.lua Initial implementation of trmnl-lua 2026-07-14 01:55:08 +00:00
log.lua feat: structured logging, render stderr capture, B2 pagination, env var docs 2026-07-16 02:34:53 +00:00
main.lua server: non-blocking event loop via curl_multi + unified select() 2026-08-10 01:35:19 +00:00
metrics.lua feat: add GET /metrics Prometheus endpoint 2026-07-16 02:46:24 +00:00
README.md refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00
render.lua refactor: rename all remaining b2/B2 identifiers to s3/S3 in Lua source 2026-07-16 16:26:23 +00:00
router.lua feat: add /admin page with key display and server info 2026-07-31 15:59:18 +00:00
s3.lua server: non-blocking event loop via curl_multi + unified select() 2026-08-10 01:35:19 +00:00
state.lua refactor: rename all remaining b2/B2 identifiers to s3/S3 in Lua source 2026-07-16 16:26:23 +00:00
VERSION chore(version): 0.4.3 2026-07-16 16:26:26 +00:00
VISION.md refactor: generalize B2 storage client to any S3-compatible provider 2026-07-16 16:02:29 +00:00

trmnl-lua

A self-hosted TRMNL-compatible e-ink display server, built in LuaJIT. One process, ~15MB RAM, no database.

Live Demo · API Reference · Docs


Stack

Component Role
LuaJIT Runtime — controls everything
libmicrohttpd (FFI) HTTP server
libcurl (FFI) HTTP client for S3-compatible storage
S3-compatible storage State + rendered images (Backblaze B2 recommended)
LÖVE2D (offscreen) Render engine
ImageMagick PNG → 1-bit BMP3
Mesa softpipe Software OpenGL for headless LÖVE2D

Environment Variables

Variable Required Default Description
S3_ACCESS_KEY_ID Yes S3 access key ID
S3_SECRET_ACCESS_KEY Yes S3 secret access key
S3_BUCKET Yes Bucket name
S3_REGION Yes Region (e.g. us-west-004 for B2, auto for R2)
S3_ENDPOINT Yes S3 endpoint URL (e.g. https://s3.us-west-004.backblazeb2.com)
S3_PUBLIC_URL No Base URL for public downloads (e.g. CDN or https://f004.backblazeb2.com/file/my-bucket)
ADMIN_KEY No Bearer token for admin API; endpoints return 503 if unset
PORT No 8080 HTTP server listen port
SERVER_URL No http://localhost:8080 Base URL embedded in image links returned to devices
TRMNL_WIDTH No 800 Render canvas width in pixels
TRMNL_HEIGHT No 480 Render canvas height in pixels

Plugin API

Plugins are plain Lua files using the love.graphics API:

-- plugins/clock.lua
local M = {}
M.refresh_seconds = 60

function M.draw(W, H)
  love.graphics.setFont(love.graphics.newFont(96))
  love.graphics.setColor(0, 0, 0)
  love.graphics.printf(os.date("%H:%M"), 0, 160, W, "center")
end

return M

Plugins are stored in S3 under plugins/<name>.lua and hot-reloaded without redeployment.

Quick Start

git clone https://git.mntechstudios.com/forgejo_admin/trmnl-lua
cd trmnl-lua && cp .env.example .env   # fill in S3 credentials
docker compose up

Set your TRMNL device's custom server URL to http://your-host:8080. It registers automatically on first boot.

Deployment (microk8s)

sudo docker build -t trmnl-lua:latest .
sudo docker save trmnl-lua:latest | microk8s ctr images import -
microk8s kubectl apply -f k8s/trmnl.yaml

Releasing

This project uses Conventional Commits and cocogitto for automated semantic versioning.

Commit format

<type>[optional scope]: <description>

feat:     new feature           → minor bump (0.X.0)
fix:      bug fix               → patch bump (0.0.X)
chore:    maintenance/tooling   → no bump
refactor: code restructuring    → no bump
perf:     performance change    → no bump
docs:     documentation only    → no bump

feat!: breaking change          → major bump (X.0.0)

Cutting a release

# Preview what version would be bumped to
cog bump --dry-run

# Bump version, generate CHANGELOG.md, tag, and push
cog bump --auto

cog bump --auto reads commits since the last tag, determines the semver bump, updates CHANGELOG.md, commits it, creates an annotated tag, then pushes both to origin (via post_bump_hooks in cog.toml).

Checking commit hygiene

# Lint commits since last release
cog check -l

# Pretty conventional commit log
cog log