Skip to content

LTDev-LLC/inky-renderer

Repository files navigation

Inky Renderer

Firmware + Cloudflare Worker rendering service for Inkplate devices.

Supported Devices

  1. Inkplate 10
  2. Inkplate 6COLOR

API Overview

The renderer now uses v1 as the primary API namespace.

  • Main rendering API: /api/v1
  • Timezone helper API (used by firmware NTP flow): /api/v0

PlatformIO Libraries (Firmware)

The firmware dependencies are declared in platformio.ini under lib_deps.

  1. Inkplate Arduino Library
  2. esp32-stb-image
  3. WiFiManager
  4. QRCode

How The Render Pipeline Works

  1. Firmware calls /api/v1/content/....
  2. The content API returns metadata in JSON or CBOR (not final JPEG bytes) for all providers, and firmware requests CBOR for this step.
  3. Metadata includes:
    • For image providers: transformer (preferred API string) and transformers (ordered candidates)
    • For render/remote/ai-slop: direct display.source pointing to /api/v1/render/...
    • display.source, optional requestHeaders, and compact display
  4. Firmware tries ordered transformer candidates when present; otherwise it fetches display.source directly.
  5. render/remote/ai-slop do not use worker/wsrv transformers.
  6. Firmware applies display metadata (source/messages/no-dither/meta) from CBOR metadata and does not depend on custom X-* response headers.
  7. If sleepwindow is active on firmware, the device renders a local Good Night screen and skips provider fetches unless a button override wake is active.

Endpoint Reference

GET /api/v1/content/:providers?/:raw?

Primary endpoint for all providers.

  • :providers: single provider (weather) or list (unsplash,wallhaven,xkcd).
  • Provider lists are split by comma, pipe, semicolon, or space; one is selected each request.
  • :raw is accepted for backward compatibility; response remains metadata and can be encoded as JSON or CBOR.

Behavior:

  • Returns metadata for all providers, defaulting to JSON, with ?cbor=true or Accept: application/cbor selecting CBOR.
  • image providers include transform/failover candidates plus compact display metadata.
  • render/remote/ai-slop providers return direct /api/v1/render/... URLs with no transformer candidates.
  • Firmware uses CBOR for /api/v1/content/... requests, while browser/debug clients can keep using JSON.

GET /api/v1/render/:providers?/:raw?

Render execution endpoint for non-image providers.

  • Generates JPEG output for render, remote, and ai-slop.
  • Intended to be called by firmware via metadata returned from /api/v1/content/....
  • Does not apply worker/wsrv transformer pipelines for these provider types.
  • Firmware only relies on standard HTTP headers (Content-Type, Content-Length, Transfer-Encoding) when fetching image bytes.

Common content query params:

  • w, h: output dimensions.
  • q: JPEG quality.
  • f or fit: cover, contain, crop, pad, scale-down.
  • grayscale: defaults to true; set grayscale=false to disable (applies to browser screenshot output and transformer grayscale settings).
  • ta / transform-api / transformApi: preferred transformer.
  • tf / transformers: failover transformer order.
  • transform: set false to disable transform settings generation.
  • json=true: raw/debug JSON mode.

Weighted provider selection is set directly in the :providers path segment. Supported separators between providers: ,, |, ;, or whitespace. Examples:

  • /api/v1/content/weather:20,nasa=30,media-gallery:10|unsplash=40
  • /api/v1/content/weather:20;nasa=30;media-gallery:10;unsplash=40
  • /api/v1/content/unsplash,wallhaven,xkcd

If no weight is provided for a provider, its weight defaults to 1.

GET|POST /api/v1/transform

Transformer endpoint used for fallback or direct transform calls.

  • Accepts URL + transform settings.
  • Supports worker and wsrv transformer modes.
  • Used by firmware when provider metadata requires worker-side transform fallback.

GET /api/v1/_internal/ai-slop/:token?

Internal AI image generation endpoint.

  • Protected by SLOP_ACCESS_TOKEN.
  • Intended for worker-internal use, not public direct use.

GET /api/v1/_internal/media-gallery/:id

Internal media object streaming endpoint for media-gallery provider.

  • Streams image by R2 object key.
  • Can be protected via MEDIA_INTERNAL_TOKEN.

GET /api/v0/timezone/:timezone?

Timezone lookup endpoint used by firmware NTP sync logic.

  • Supports JSON or CBOR mode (?cbor=true or ?cbor=1).
  • Accepts timezone from path, query, headers, or CF request context.
  • Firmware requests CBOR for timezone lookups.

Provider Catalog

Image Providers (/api/v1/content/<provider>)

  1. Unsplash (/unsplash) - Optimized for: 10, 6COLOR
  2. Pixabay (/pixabay) - Optimized for: 10, 6COLOR
  3. Pexels (/pexels) - Optimized for: 10, 6COLOR
  4. Wallhaven (/wallhaven) - Optimized for: 10, 6COLOR
  5. NASA APOD (/nasa) - Optimized for: 10, 6COLOR
  6. xkcd (/xkcd) - Optimized for: 10 (supported on 6COLOR)
  7. AI Slop (/ai-slop) - Optimized for: 10, 6COLOR
  8. RAWG.io (/rawg) - Optimized for: 10, 6COLOR
  9. Media Gallery (/media-gallery) - Optimized for: 10, 6COLOR

Notes:

  • Wallhaven can return NSFW content depending on query/purity.
  • xkcd can be harder to read on smaller text renderings.

Render/Template Providers (/api/v1/content/<provider>)

  1. NYTimes (/news, /nytimes) - Optimized for: 10 (supported on 6COLOR)
  2. Weather (Visual Crossing) (/weather) - Optimized for: 10, 6COLOR
  3. Hacker News (/hn) - Optimized for: 10 (supported on 6COLOR)
  4. Spotify Now Playing (/spotify, /music) - Optimized for: 10 (supported on 6COLOR)
  5. Google Calendar (/google-calendar) - Optimized for: 10 (supported on 6COLOR)
  6. RSS/Atom Feed (/feed, /rss, /atom) - Optimized for: 10 (supported on 6COLOR)

Notes:

  • NYTimes and Weather layouts are generally denser on 6COLOR, so concise query options are recommended.
  • Spotify supports ?spotify_user=<spotify_user_id> (preferred), plus ?user= / ?username= for profile lookup. Playback resolves from the spotify:user:* KV record written by /api/v0/spotify/auth.
  • Spotify OAuth UI: /api/v0/spotify/auth stores the refresh token and related user metadata directly under spotify:user:<spotify_user_id>, then returns mapped /api/v1/content/spotify?spotify_user=... and /api/v1/render/spotify?spotify_user=... links.
  • Spotify requires OAuth env vars (SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET) and the SPOTIFY_TOKENS KV binding for username-based refresh-token lookup.
  • Google Calendar is text-heavy and usually reads best on Inkplate 10.
  • RSS/Atom feed accepts feed, url, or src query params. limit defaults to 6 (max 25). Example: /api/v1/content/feed?feed=https%3A%2F%2Fexample.com%2Frss.xml&limit=6

Environment Variables

Use .secrets.example.json as the template.

Core:

  • SKIP_AUTH
  • USERS
  • USE_BROWSER_SESSIONS
  • TRANSFORM_API

Provider/API keys:

  • UNSPLASH_CLIENT_ID
  • PIXABAY_API_KEY
  • PEXELS_API_KEY
  • WALLHAVEN_API_KEY
  • NASA_API_KEY
  • NYTIMES_API_KEY
  • WEATHER_API_KEY
  • RAWG_API_KEY
  • SPOTIFY_CLIENT_ID
  • SPOTIFY_CLIENT_SECRET
  • SPOTIFY_REDIRECT_URI (optional override for OAuth callback URL)
  • SPOTIFY_AUTH_SCOPES (optional space/comma-separated scope override)

AI:

  • SLOP_PROMPT_MODEL
  • SLOP_IMAGE_MODEL
  • SLOP_ACCESS_TOKEN

Optional defaults:

  • DEFAULT_WEATHER_LOCATION
  • DEFAULT_GOOGLE_CALENDAR_ID
  • DEFAULT_FEED_URL

Media gallery:

  • IMAGE_GALLERY_R2
  • MEDIA_INTERNAL_TOKEN

Required Worker Bindings

In wrangler.jsonc:

  • AI
  • BROWSER
  • INKY_IMAGES
  • IMAGE_GALLERY (only required for media-gallery provider)
  • SPOTIFY_TOKENS KV namespace (required for /api/v0/spotify/auth username mapping flow)

Firmware Config

Use:

  • config.example.json for Inkplate 10
  • config_6color.example.json for Inkplate 6COLOR

Important renderer keys:

  • renderer.basepath (default /api/v1)
  • renderer.default (default endpoint used for normal timed renders)
  • renderer.button (endpoint used for button-triggered render wakes)
  • renderer.wakes (time -> endpoint map, e.g. "8:00am": "/content/weather?...")
  • renderer.wake-interval (duration syntax like 30m, 2h, 1d2h)
  • renderer.sleepwindow.start / renderer.sleepwindow.stop (quiet-hours window)
  • renderer.transform-api (worker or wsrv)
  • renderer.timeout
  • renderer.retries

Important display keys:

  • display.rotation (0..3, runtime display orientation)
  • display.cleardisplay (show "Please Stand By" pre-render screen)
  • display.grayscale
  • display.jpeg-quality

Notes:

  • display.rotation is applied at runtime from config instead of build flags.
  • display.grayscale is forced to color mode on Inkplate 6COLOR firmware builds.
  • Source config files in the repo stay as JSON, but merge_fs.py stages them as .cbor for LittleFS and firmware loads CONFIG_FILE_PATH.
  • merge_fs.py prefers an existing sibling .cbor config over regenerating one from JSON.

Quiet Hours Behavior

When renderer.sleepwindow is configured and current local RTC time is inside the window:

  • Firmware shows a local Good Night screen and does not call the renderer API.
  • Button wake can still force normal rendering via renderer.button.
  • Scheduler can pre-wake at sleep window start so quiet-hours screen is shown on entry.

WiFi Credential Management

WiFi credentials are managed by WiFiManager on device (stored in ESP32 NVS, not in the firmware config file on LittleFS).

Ways to open Device Management Portal:

  • Manual: wake with button and hold for ~2-5 seconds, then release on Device Management.
  • Automatic fallback: on normal boot, if saved WiFi fails, firmware can open captive portal.

Portal details:

  • AP name: Inky-Renderer
  • Portal menu includes OTA Updates for firmware, LittleFS, and config CBOR uploads.
  • Typical captive portal timeout:
    • Forced setup flow: ~180 seconds
    • Normal boot fallback: ~30 seconds

To update credentials:

  1. Enter Device Management mode.
  2. Connect to Inky-Renderer.
  3. Open captive portal and either save WiFi credentials, or open OTA Updates for firmware/LittleFS/config updates.
  4. Device restarts and uses new credentials.

OTA Updates

Enter the Device Management Portal:

  • Wake with button and hold for >2 seconds, then release on Device Management.

Behavior:

  • Device starts the Device Management Portal captive portal.
  • Connect to Inky-Renderer and open http://192.168.4.1.
  • Use the OTA Updates menu entry for local firmware, LittleFS, and config uploads.

Upload options:

  • App firmware image (type defaults to flash app update).
  • Filesystem image (type=fs) for LittleFS updates.
  • Config upload (type=config) accepts .cbor only and writes it to LittleFS (CONFIG_FILE_PATH).
  • If you only have a JSON source config, convert it first with tools/json2cbor.mjs.

Safety/exit behavior:

  • Successful update reboots automatically.
  • If you only update WiFi credentials, the device restarts and boots normally.

Setup

  1. Clone the repo.
  2. Copy firmware config:
    • config.example.json -> config.json
    • config_6color.example.json -> config_6color.json
  3. Copy worker secrets:
    • .secrets.example.json -> .secrets.json
    • Fill values and run npm run secrets
  4. Deploy:
    • npm run deploy

Tools Folder Usage

The tools/ folder includes utility scripts for firmware assets and local worker setup.

tools/json2env.mjs

Converts .secrets.json into .dev.vars for wrangler dev.

Direct usage:

  • node tools/json2env.mjs
  • node tools/json2env.mjs .secrets.json --out .dev.vars --force
  • node tools/json2env.mjs .secrets.example.json --out .dev.vars.example --force

NPM shortcut:

  • npm run json2env -- .secrets.json --out .dev.vars --force

tools/json2cbor.mjs

Converts a JSON config file into a sibling .cbor file for firmware/LittleFS use.

Direct usage:

  • node tools/json2cbor.mjs config.json
  • node tools/json2cbor.mjs config_6color.json --out config_6color.cbor --force

NPM shortcut:

  • npm run json2cbor -- config.json --force

Notes:

  • merge_fs.py will use an existing .cbor config if one is already present next to the JSON source file.
  • The firmware still uses CBOR for the staged LittleFS config even though the repo source configs are JSON.

tools/html2h.mjs

Minifies HTML/CSS files into a C header for firmware embedding. Supports either gzipped byte-array output for served pages or plain minified string output for injected snippets.

Direct usage:

  • node tools/html2h.mjs html/ota.html --out firmware/include
  • node tools/html2h.mjs html/portal.css --out firmware/include
  • node tools/html2h.mjs html/wifi_portal_styles.html --out firmware/include --format string
  • bash tools/html2h-all.sh
  • node tools/html2h.mjs html/ota.html --out /tmp

NPM shortcut:

  • npm run html2h -- html/ota.html --out firmware/include
  • npm run html2h:all

tools/img2logo.mjs

Converts an image into compressed Inkplate logo assets for both Inkplate 10 and Inkplate 6COLOR targets.

Direct usage:

  • node tools/img2logo.mjs assets/logos/inky-transparent.png
  • node tools/img2logo.mjs assets/logos/inky-transparent.png --scale 0.666
  • node tools/img2logo.mjs assets/logos/inky-transparent.png --outSrc firmware/src/images --outInc firmware/include/images

NPM shortcut:

  • npm run img2logo -- assets/logos/inky-transparent.pngg --scale 0.666

HTTPS (Firmware)

Firmware uses insecure HTTPS for all HTTPS requests it makes, including:

  • /api/v1/content metadata fetches
  • /api/v0/timezone timezone lookups
  • direct image fetches from display.source
  • transformer/render fallback fetches

Notes:

  • Firmware calls setInsecure() for its HTTPS clients
  • TLS certificate validation is currently disabled in firmware

FAQ

Why does /api/v1/content/... return metadata instead of JPEG?

/api/v1/content is metadata-only. It defaults to JSON for browser/debug clients, and firmware requests CBOR before fetching the final JPEG bytes from transformer URLs or /api/v1/render/... direct URLs.

When does the firmware use CBOR?

Only for the metadata/timezone steps:

  • /api/v1/content/...
  • /api/v0/timezone/...

Image bytes are still fetched as normal HTTP bodies after metadata resolution.

How do I force a specific transformer?

Use ta (or transform-api) and optionally tf (or transformers) in the request, and set renderer.transform-api in firmware config for default behavior.

How do provider weights work?

Pass weighted providers in path form, for example: /api/v1/content/weather:20;nasa=30;unsplash=40. If omitted, a provider weight is 1.

How does Spotify auth get stored in KV?

The OAuth callback stores the refresh token and Spotify user metadata directly in spotify:user:<spotify_user_id>.

Why do progressive JPEG conversions still fail even with free PSRAM?

The fallback conversion path still needs a large contiguous decoded-image allocation, and that can fail because of fragmentation or peak size even when total free PSRAM looks healthy. Prefer baseline JPEG from the worker, smaller output dimensions, or lower quality before the firmware fallback has to run.

Can quiet hours still be overridden?

Yes. Normal timed wakes inside renderer.sleepwindow show the local Good Night screen, but button-triggered wake can still run renderer.button.

Where are WiFi credentials stored?

WiFiManager stores credentials in ESP32 NVS, not in the firmware config file on LittleFS.

How do I update firmware and filesystem assets?

Use the Device Management Portal OTA Updates menu entry. It supports firmware uploads, filesystem uploads (type=fs), and config CBOR uploads.

Does firmware validate HTTPS certificates?

No. Firmware currently calls setInsecure() for HTTPS clients, so worker and image TLS certificates are not validated on-device.

Do repo config files need to be checked in as CBOR?

No. The source configs in the repo stay as JSON. merge_fs.py will generate or reuse a sibling .cbor file when staging the LittleFS image, and OTA config uploads require .cbor.

License

This project is licensed under the MIT License.

About

A remote render service for Inkplate devices via Cloudflare Workers.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors