Firmware + Cloudflare Worker rendering service for Inkplate devices.
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
The firmware dependencies are declared in platformio.ini under lib_deps.
- Firmware calls
/api/v1/content/.... - The content API returns metadata in JSON or CBOR (not final JPEG bytes) for all providers, and firmware requests CBOR for this step.
- Metadata includes:
- For
imageproviders:transformer(preferred API string) andtransformers(ordered candidates) - For
render/remote/ai-slop: directdisplay.sourcepointing to/api/v1/render/... display.source, optionalrequestHeaders, and compactdisplay
- For
- Firmware tries ordered transformer candidates when present; otherwise it fetches
display.sourcedirectly. render/remote/ai-slopdo not use worker/wsrv transformers.- Firmware applies
displaymetadata (source/messages/no-dither/meta) from CBOR metadata and does not depend on customX-*response headers. - If
sleepwindowis active on firmware, the device renders a local Good Night screen and skips provider fetches unless a button override wake is active.
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.
:rawis 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=trueorAccept: application/cborselecting CBOR. imageproviders include transform/failover candidates plus compactdisplaymetadata.render/remote/ai-slopproviders 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.
Render execution endpoint for non-image providers.
- Generates JPEG output for
render,remote, andai-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.forfit:cover,contain,crop,pad,scale-down.grayscale: defaults totrue; setgrayscale=falseto disable (applies to browser screenshot output and transformer grayscale settings).ta/transform-api/transformApi: preferred transformer.tf/transformers: failover transformer order.transform: setfalseto 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.
Transformer endpoint used for fallback or direct transform calls.
- Accepts URL + transform settings.
- Supports
workerandwsrvtransformer modes. - Used by firmware when provider metadata requires worker-side transform fallback.
Internal AI image generation endpoint.
- Protected by
SLOP_ACCESS_TOKEN. - Intended for worker-internal use, not public direct use.
Internal media object streaming endpoint for media-gallery provider.
- Streams image by R2 object key.
- Can be protected via
MEDIA_INTERNAL_TOKEN.
Timezone lookup endpoint used by firmware NTP sync logic.
- Supports JSON or CBOR mode (
?cbor=trueor?cbor=1). - Accepts timezone from path, query, headers, or CF request context.
- Firmware requests CBOR for timezone lookups.
- Unsplash (
/unsplash) - Optimized for: 10, 6COLOR - Pixabay (
/pixabay) - Optimized for: 10, 6COLOR - Pexels (
/pexels) - Optimized for: 10, 6COLOR - Wallhaven (
/wallhaven) - Optimized for: 10, 6COLOR - NASA APOD (
/nasa) - Optimized for: 10, 6COLOR - xkcd (
/xkcd) - Optimized for: 10 (supported on 6COLOR) - AI Slop (
/ai-slop) - Optimized for: 10, 6COLOR - RAWG.io (
/rawg) - Optimized for: 10, 6COLOR - 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.
- NYTimes (
/news,/nytimes) - Optimized for: 10 (supported on 6COLOR) - Weather (Visual Crossing) (
/weather) - Optimized for: 10, 6COLOR - Hacker News (
/hn) - Optimized for: 10 (supported on 6COLOR) - Spotify Now Playing (
/spotify,/music) - Optimized for: 10 (supported on 6COLOR) - Google Calendar (
/google-calendar) - Optimized for: 10 (supported on 6COLOR) - 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 thespotify:user:*KV record written by/api/v0/spotify/auth. - Spotify OAuth UI:
/api/v0/spotify/authstores the refresh token and related user metadata directly underspotify: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 theSPOTIFY_TOKENSKV 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, orsrcquery params.limitdefaults to6(max25). Example:/api/v1/content/feed?feed=https%3A%2F%2Fexample.com%2Frss.xml&limit=6
Use .secrets.example.json as the template.
Core:
SKIP_AUTHUSERSUSE_BROWSER_SESSIONSTRANSFORM_API
Provider/API keys:
UNSPLASH_CLIENT_IDPIXABAY_API_KEYPEXELS_API_KEYWALLHAVEN_API_KEYNASA_API_KEYNYTIMES_API_KEYWEATHER_API_KEYRAWG_API_KEYSPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETSPOTIFY_REDIRECT_URI(optional override for OAuth callback URL)SPOTIFY_AUTH_SCOPES(optional space/comma-separated scope override)
AI:
SLOP_PROMPT_MODELSLOP_IMAGE_MODELSLOP_ACCESS_TOKEN
Optional defaults:
DEFAULT_WEATHER_LOCATIONDEFAULT_GOOGLE_CALENDAR_IDDEFAULT_FEED_URL
Media gallery:
IMAGE_GALLERY_R2MEDIA_INTERNAL_TOKEN
In wrangler.jsonc:
AIBROWSERINKY_IMAGESIMAGE_GALLERY(only required for media-gallery provider)SPOTIFY_TOKENSKV namespace (required for/api/v0/spotify/authusername mapping flow)
Use:
config.example.jsonfor Inkplate 10config_6color.example.jsonfor 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 like30m,2h,1d2h)renderer.sleepwindow.start/renderer.sleepwindow.stop(quiet-hours window)renderer.transform-api(workerorwsrv)renderer.timeoutrenderer.retries
Important display keys:
display.rotation(0..3, runtime display orientation)display.cleardisplay(show "Please Stand By" pre-render screen)display.grayscaledisplay.jpeg-quality
Notes:
display.rotationis applied at runtime from config instead of build flags.display.grayscaleis forced to color mode on Inkplate 6COLOR firmware builds.- Source config files in the repo stay as JSON, but
merge_fs.pystages them as.cborfor LittleFS and firmware loadsCONFIG_FILE_PATH. merge_fs.pyprefers an existing sibling.cborconfig over regenerating one from JSON.
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 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 Updatesfor firmware, LittleFS, and config CBOR uploads. - Typical captive portal timeout:
- Forced setup flow: ~180 seconds
- Normal boot fallback: ~30 seconds
To update credentials:
- Enter Device Management mode.
- Connect to
Inky-Renderer. - Open captive portal and either save WiFi credentials, or open
OTA Updatesfor firmware/LittleFS/config updates. - Device restarts and uses new credentials.
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-Rendererand openhttp://192.168.4.1. - Use the
OTA Updatesmenu entry for local firmware, LittleFS, and config uploads.
Upload options:
- App firmware image (
typedefaults to flash app update). - Filesystem image (
type=fs) for LittleFS updates. - Config upload (
type=config) accepts.cboronly 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.
- Clone the repo.
- Copy firmware config:
config.example.json->config.jsonconfig_6color.example.json->config_6color.json
- Copy worker secrets:
.secrets.example.json->.secrets.json- Fill values and run
npm run secrets
- Deploy:
npm run deploy
The tools/ folder includes utility scripts for firmware assets and local worker setup.
Converts .secrets.json into .dev.vars for wrangler dev.
Direct usage:
node tools/json2env.mjsnode tools/json2env.mjs .secrets.json --out .dev.vars --forcenode tools/json2env.mjs .secrets.example.json --out .dev.vars.example --force
NPM shortcut:
npm run json2env -- .secrets.json --out .dev.vars --force
Converts a JSON config file into a sibling .cbor file for firmware/LittleFS use.
Direct usage:
node tools/json2cbor.mjs config.jsonnode tools/json2cbor.mjs config_6color.json --out config_6color.cbor --force
NPM shortcut:
npm run json2cbor -- config.json --force
Notes:
merge_fs.pywill use an existing.cborconfig 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.
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/includenode tools/html2h.mjs html/portal.css --out firmware/includenode tools/html2h.mjs html/wifi_portal_styles.html --out firmware/include --format stringbash tools/html2h-all.shnode tools/html2h.mjs html/ota.html --out /tmp
NPM shortcut:
npm run html2h -- html/ota.html --out firmware/includenpm run html2h:all
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.pngnode tools/img2logo.mjs assets/logos/inky-transparent.png --scale 0.666node 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
Firmware uses insecure HTTPS for all HTTPS requests it makes, including:
/api/v1/contentmetadata fetches/api/v0/timezonetimezone 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
/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.
Only for the metadata/timezone steps:
/api/v1/content/.../api/v0/timezone/...
Image bytes are still fetched as normal HTTP bodies after metadata resolution.
Use ta (or transform-api) and optionally tf (or transformers) in the request, and set renderer.transform-api in firmware config for default behavior.
Pass weighted providers in path form, for example: /api/v1/content/weather:20;nasa=30;unsplash=40. If omitted, a provider weight is 1.
The OAuth callback stores the refresh token and Spotify user metadata directly in spotify:user:<spotify_user_id>.
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.
Yes. Normal timed wakes inside renderer.sleepwindow show the local Good Night screen, but button-triggered wake can still run renderer.button.
WiFiManager stores credentials in ESP32 NVS, not in the firmware config file on LittleFS.
Use the Device Management Portal OTA Updates menu entry. It supports firmware uploads, filesystem uploads (type=fs), and config CBOR uploads.
No. Firmware currently calls setInsecure() for HTTPS clients, so worker and image TLS certificates are not validated on-device.
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.
This project is licensed under the MIT License.
- Root project license:
LICENSE - Firmware license:
firmware/LICENSE