Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Overview

Byonk is designed as a content server that bridges dynamic data sources with e-ink displays. This page explains how the system is structured and how requests flow through it.

System Overview

flowchart LR
    Display[TRMNL Display]

    subgraph Server[Byonk Server]
        Router[HTTP Router]
        Registry[(Device Registry)]
        Signer[URL Signer]
        Lua[Lua Runtime]
        Template[Template Service]
        Renderer[SVG Renderer]
    end

    Display --> Router
    Router --> Registry
    Router --> Signer
    Router --> Lua
    Lua --> Template
    Template --> Renderer

Core Components

HTTP Router

The entry point for all device requests. Built with Axum, it handles:

  • Device registration (/api/setup)
  • Content requests (/api/display, /api/image/:id)
  • Logging (/api/log)
  • API documentation (/swagger-ui)

Device Registry

Stores device information in memory:

  • MAC address to API key mapping
  • Device metadata (firmware version, model, battery level)
  • Last seen timestamps

Note: The current implementation uses an in-memory store. Device registrations are lost on restart. The architecture supports adding database persistence in the future.

URL Signer

Provides security for image URLs using HMAC-SHA256:

  • Signs image URLs with expiration timestamps
  • Validates signatures on image requests
  • Prevents unauthorized access to device content

Content Pipeline

The heart of Byonk - orchestrates content generation:

  1. Looks up screen configuration for the device
  2. Executes Lua script with device parameters
  3. Renders SVG template with script data
  4. Converts SVG to PNG with dithering

Lua Runtime

Executes Lua scripts in a sandboxed environment:

  • HTTP client for fetching external data
  • JSON/HTML parsing utilities
  • Time functions
  • Logging

Template Service

Renders SVG templates using Tera:

  • Jinja2-style syntax
  • Custom filters (truncate, format_time)
  • Fresh loading on each request (hot reload)

SVG Renderer

Converts SVG to PNG optimized for e-ink:

  • Uses resvg for rendering
  • Loads custom fonts from fonts/ directory
  • Blue-noise dithering to 4 gray levels
  • Outputs 2-bit indexed PNG

Request Flow

The device-server interaction happens in three phases:

Phase 1: Device Registration

sequenceDiagram
    participant Device as E-ink Display
    participant Router as HTTP Router
    participant Registry as Device Registry

    Device->>+Router: GET /api/setup
    Router->>Registry: lookup/create device
    Registry-->>Router: api_key
    Router-->>-Device: {api_key, friendly_id}
    Note right of Device: Store api_key

Phase 2: Content Generation

sequenceDiagram
    participant Device
    participant Router
    participant Lua
    participant API as External API
    participant Template
    participant Cache

    Device->>+Router: GET /api/display
    Router->>+Lua: execute script
    Lua->>+API: http_get(url)
    API-->>-Lua: JSON data
    Lua-->>-Router: {data, refresh_rate}
    Router->>+Template: render SVG with data
    Template-->>-Router: SVG document
    Router->>Cache: store SVG + hash
    Router-->>-Device: {image_url, filename, refresh_rate}
    Note right of Device: filename is content hash

Phase 3: Image Rendering

sequenceDiagram
    participant Device
    participant Router
    participant Cache
    participant Renderer

    Device->>+Router: GET /api/image/:id
    Router->>Cache: get cached SVG
    Cache-->>Router: SVG document
    Router->>+Renderer: convert to PNG
    Renderer-->>-Router: dithered PNG
    Router-->>-Device: PNG image
    Note right of Device: Display and sleep

Request Details

PhaseEndpointPurpose
1. SetupGET /api/setupDevice registers, receives API key
2. DisplayGET /api/displayRuns Lua script, renders SVG, caches it, returns signed image URL and content hash
3. ImageGET /api/image/:idConverts cached SVG to PNG, returns image

Phase 2 (content generation):

  1. Load and execute Lua script with params and device context
  2. Script fetches external data via http_get()
  3. Render SVG template with script data
  4. Cache rendered SVG with content hash
  5. Sign image URL and return to device with filename set to content hash

The filename field contains a hash of the rendered SVG content. This allows TRMNL devices to detect when content has actually changed, even if the same screen is configured.

Phase 3 (image rendering):

  1. Verify URL signature
  2. Retrieve cached SVG
  3. Convert SVG to PNG with blue-noise dithering
  4. Return PNG to device

Technology Stack

ComponentTechnology
Web frameworkAxum
Async runtimeTokio
Scriptingmlua (Lua 5.4)
TemplatingTera
SVG renderingresvg (patched for variable fonts)
HTTP clientreqwest
HTML parsingscraper

Design Principles

Fresh Loading

Lua scripts and SVG templates are loaded from disk on every request. This enables:

  • Live editing during development
  • No restart needed for content changes
  • Simple deployment (just copy files)

Blocking Isolation

CPU-intensive operations run in a blocking task pool:

  • Lua HTTP requests
  • SVG rendering
  • Image encoding

This prevents blocking the async event loop.

Graceful Degradation

If content generation fails, devices receive an error screen rather than nothing. The error message helps debugging while keeping the device functional.

Security Model

Signed URLs

Image URLs are signed with HMAC-SHA256:

  • 1-hour expiration
  • Prevents URL enumeration
  • Protects against unauthorized access

No Authentication Required

The /api/setup endpoint is open - any device can register. This matches TRMNL’s design where devices self-register.

Script Sandboxing

Lua scripts run in a controlled environment:

  • Only exposed functions are available
  • No filesystem access
  • No arbitrary code execution