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

Lua API Reference

This page documents all functions available to Lua scripts in Byonk.

Global Variables

params

A table containing device-specific parameters from config.yaml.

local station = params.station  -- From config.yaml
local limit = params.limit or 10  -- With default

Type: table

device

A table containing device information (when available).

-- Check battery level
if device.battery_voltage and device.battery_voltage < 3.3 then
  log_warn("Low battery: " .. device.battery_voltage .. "V")
end

-- Check signal strength
if device.rssi and device.rssi < -80 then
  log_warn("Weak WiFi signal: " .. device.rssi .. " dBm")
end

-- Responsive layout based on device type
if device.width == 1872 then
  -- TRMNL X layout
else
  -- TRMNL OG layout
end

Fields:

FieldTypeDescription
macstringDevice MAC address (e.g., “AC:15:18:D4:7B:E2”)
battery_voltagenumber or nilBattery voltage (e.g., 4.12)
rssinumber or nilWiFi signal strength in dBm (e.g., -65)
modelstring or nilDevice model (“og” or “x”)
firmware_versionstring or nilFirmware version string
widthnumber or nilDisplay width in pixels (800 or 1872)
heightnumber or nilDisplay height in pixels (480 or 1404)
boardstring or nilBoard identifier (e.g., “trmnl_og_4clr”)
colorstable or nilDisplay palette as hex RGB strings (e.g., {“#000000”, “#FFFFFF”})
dithertablePre-script resolved dither tuning (see below)

Type: table

Note: Device fields may be nil if the device doesn’t report them. Always check before using.

device.dither

The device.dither sub-table contains the pre-script resolved dither tuning values (panel defaults merged with device config). Scripts can read these to make selective adjustments rather than setting everything blindly.

-- Read current tuning
local algo = device.dither.algorithm       -- "floyd-steinberg" (resolved algorithm)
local ec = device.dither.error_clamp       -- 0.08 (from panel/device config)
local ns = device.dither.noise_scale       -- 4.0
local cc = device.dither.chroma_clamp      -- nil (not set)
local st = device.dither.strength          -- 1.0 (default)

-- Selectively override: halve the error clamp, keep everything else
return {
  data = { ... },
  refresh_rate = 300,
  error_clamp = (device.dither.error_clamp or 0.1) * 0.5,
  -- noise_scale not returned -> keeps panel/device value
}
FieldTypeDescription
algorithmstring or nilPre-script resolved dither algorithm
error_clampnumber or nilError diffusion clamp (from device config / panel)
noise_scalenumber or nilBlue noise jitter scale
chroma_clampnumber or nilChromatic error clamp
strengthnumber or nilError diffusion strength (0.0–2.0, default 1.0)

layout

A table containing pre-computed responsive layout values. These values are automatically calculated based on the device dimensions, making it easy to create screens that work on both TRMNL OG (800×480) and TRMNL X (1872×1404).

-- Use pre-computed values directly
local margin = layout.margin        -- pixel-aligned margin
local center = layout.center_x      -- screen center X

-- Access display palette
local colors = layout.colors         -- {"#000000", "#555555", "#AAAAAA", "#FFFFFF"}
local count = layout.color_count     -- 4
local greys = layout.grey_count      -- 4 (colors where R=G=B)

Fields:

FieldTypeDescriptionDefault (OG)Example (X)
widthintegerDevice width in pixels8001872
heightintegerDevice height in pixels4801404
scalenumberScale factor: min(width/800, height/480)1.02.34
center_xintegerHorizontal center: floor(width/2)400936
center_yintegerVertical center: floor(height/2)240702
colorstableDisplay palette as hex RGB strings{“#000000”,“#555555”,“#AAAAAA”,“#FFFFFF”}16 grey values
color_countintegerNumber of palette colors416
grey_countintegerNumber of grey levels (colors where R=G=B)416
marginintegerStandard margin: floor(20 * scale)2046
margin_smintegerSmall margin: floor(10 * scale)1023
margin_lgintegerLarge margin: floor(40 * scale)4093

Type: table

Note: All margin values are pre-floored for pixel-aligned positioning.

fonts

A table of all available font families and their faces. Keyed by family name, each value is an array of face records.

-- List all font families
for family, faces in pairs(fonts) do
  print(family)  -- "X11Helv", "TerminusTTF", "Outfit", ...
end

-- Query a specific family
for _, face in ipairs(fonts["X11Helv"]) do
  print(face.style)           -- "Normal", "Italic", "Oblique"
  print(face.weight)          -- 400 (number)
  print(face.stretch)         -- "Normal", "Condensed", ...
  print(face.monospaced)      -- true/false
  print(face.post_script_name)-- "X11Helv"
  -- Bitmap strike sizes (sorted ppem values), empty for outline-only fonts
  for _, ppem in ipairs(face.bitmap_strikes) do
    print(ppem)               -- 8, 10, 11, 12, ...
  end
end

Face fields:

FieldTypeDescription
stylestring"Normal", "Italic", or "Oblique"
weightnumberCSS-style weight (100–900, 400 = normal, 700 = bold)
stretchstring"Normal", "Condensed", "Expanded", etc.
monospacedbooleanWhether the face is monospaced
post_script_namestringPostScript name of the face
bitmap_strikestableSorted array of available bitmap ppem sizes (empty if none)

Type: table

Layout Helper Functions

These functions help scale values appropriately for different device resolutions.

scale_font(value)

Scales a font size value by the layout scale factor. Returns a float to preserve precision for font rendering.

local title_size = scale_font(48)    -- 48.0 on OG, 112.32 on X
local body_size = scale_font(24)     -- 24.0 on OG, 56.16 on X

Parameters:

NameTypeDescription
valuenumberBase font size (designed for 800×480)

Returns: number - Scaled font size (float)

scale_pixel(value)

Scales a pixel value by the layout scale factor and floors the result for pixel-aligned positioning.

local header_y = scale_pixel(70)     -- 70 on OG, 163 on X
local icon_size = scale_pixel(32)    -- 32 on OG, 74 on X

Parameters:

NameTypeDescription
valuenumberBase pixel value (designed for 800×480)

Returns: integer - Scaled and floored pixel value

greys(levels)

Generates a grey palette with the specified number of levels. Useful for creating gradients or color swatches that match the device’s grey level capability.

-- Generate palette matching device capability
local palette = greys(layout.grey_levels)

for i, entry in ipairs(palette) do
  print(entry.value)       -- 0-255 grey value
  print(entry.color)       -- "#000000" to "#ffffff"
  print(entry.text_color)  -- "#ffffff" for dark, "#000000" for light
end

Parameters:

NameTypeDescription
levelsintegerNumber of grey levels (typically 4 or 16)

Returns: table - Array of palette entries

Palette entry fields:

FieldTypeDescription
valueintegerGrey value from 0 (black) to 255 (white)
colorstringHex color string (e.g., “#808080”)
text_colorstringContrasting text color (“#ffffff” or “#000000”)

Example with 4 levels:

local palette = greys(4)
-- palette[1] = {value=0,   color="#000000", text_color="#ffffff"}
-- palette[2] = {value=85,  color="#555555", text_color="#ffffff"}
-- palette[3] = {value=170, color="#aaaaaa", text_color="#000000"}
-- palette[4] = {value=255, color="#ffffff", text_color="#000000"}

Example: Responsive Screen

Here’s how to create a screen that works on both TRMNL OG and TRMNL X:

-- Before (manual boilerplate):
local width = device and device.width or 800
local height = device and device.height or 480
local scale = math.min(width / 800, height / 480)
local font_size = math.floor(48 * scale)  -- Wrong: shouldn't floor fonts
local header_y = math.floor(70 * scale)   -- Correct: pixel-aligned

-- After (using helpers):
local font_size = scale_font(48)     -- Preserves precision for fonts
local header_y = scale_pixel(70)     -- Pixel-aligned position
local margin = layout.margin         -- Pre-computed pixel margin
local colors = layout.colors                 -- Display palette colors

HTTP Functions

Byonk provides three HTTP functions: http_request (full control), http_get (GET shorthand), and http_post (POST shorthand).

http_request(url, options?)

Core HTTP function with full control over the request method and options.

-- GET request (default)
local response = http_request("https://api.example.com/data")

-- POST with JSON body
local response = http_request("https://api.example.com/users", {
  method = "POST",
  json = { name = "Alice", email = "alice@example.com" }
})

-- PUT request with headers
local response = http_request("https://api.example.com/users/123", {
  method = "PUT",
  headers = { ["Authorization"] = "Bearer " .. params.token },
  json = { name = "Alice Updated" }
})

-- DELETE request
local response = http_request("https://api.example.com/users/123", {
  method = "DELETE",
  headers = { ["Authorization"] = "Bearer " .. params.token }
})

Parameters:

NameTypeDescription
urlstringThe URL to fetch
optionstable (optional)Request options (see below)

Options:

NameTypeDefaultDescription
methodstring“GET”HTTP method: “GET”, “POST”, “PUT”, “DELETE”, “PATCH”, “HEAD”
paramstablenoneQuery parameters (automatically URL-encoded)
headerstablenoneKey-value pairs of HTTP headers
bodystringnoneRequest body as string
jsontablenoneRequest body as JSON (auto-serializes, sets Content-Type)
basic_authtablenoneBasic auth: { username = "...", password = "..." }
timeoutnumber30Request timeout in seconds
follow_redirectsbooleantrueWhether to follow HTTP redirects
max_redirectsnumber10Maximum number of redirects to follow
danger_accept_invalid_certsbooleanfalseAccept self-signed/expired certificates (insecure!)
ca_certstringnonePath to CA certificate PEM file for server verification
client_certstringnonePath to client certificate PEM file for mTLS
client_keystringnonePath to client private key PEM file for mTLS
cache_ttlnumbernoneCache response for N seconds (LRU cache, max 100 entries)

Returns: string - The response body

Throws: Error if the request fails

JSON option details:

The json option supports complex nested structures. Tables with sequential integer keys (starting at 1) become JSON arrays; tables with string keys become JSON objects. Use bracket syntax for keys with spaces or special characters:

http_post("https://api.example.com/data", {
  json = {
    -- Nested objects and arrays
    users = {
      { name = "Alice", tags = {"admin", "user"} },
      { name = "Bob", roles = { level = 2, active = true } }
    },
    -- Keys with spaces or special characters
    ["Content-Type"] = "application/json",
    ["my key with spaces"] = "works fine",
    -- Mixed types
    count = 42,
    enabled = true,
    optional = nil  -- becomes JSON null
  }
})

http_get(url, options?)

Convenience wrapper for GET requests. Same as http_request with method = "GET".

-- Simple usage
local response = http_get("https://api.example.com/data")

-- With query parameters (auto URL-encoded)
local response = http_get("https://api.example.com/search", {
  params = {
    query = "hello world",  -- becomes ?query=hello%20world&limit=10
    limit = 10
  }
})

-- With authentication header
local response = http_get("https://api.example.com/data", {
  headers = { ["Authorization"] = "Bearer " .. params.api_token }
})

-- With basic auth
local response = http_get("https://api.example.com/data", {
  basic_auth = { username = params.user, password = params.pass }
})

-- Accept self-signed certificates (for internal APIs)
local response = http_get("https://internal.example.com/data", {
  danger_accept_invalid_certs = true
})

-- Use custom CA certificate for server verification
local response = http_get("https://internal.example.com/data", {
  ca_cert = "/path/to/ca.pem"
})

-- Mutual TLS (mTLS) with client certificate
local response = http_get("https://secure-api.example.com/data", {
  ca_cert = "/path/to/ca.pem",
  client_cert = "/path/to/client.pem",
  client_key = "/path/to/client-key.pem"
})

-- Cache response for 5 minutes (300 seconds)
-- Useful for APIs with rate limits or data that doesn't change frequently
local response = http_get("https://api.weather.com/current", {
  params = { city = "Zurich" },
  cache_ttl = 300  -- Cache for 5 minutes
})

Response Caching:

The cache_ttl option enables response caching with LRU (Least Recently Used) eviction:

  • Responses are cached in memory for the specified number of seconds
  • Cache key is based on URL, method, params, headers, and body
  • Maximum 100 cached entries; oldest entries are evicted when full
  • Cache is shared across all script executions
  • Useful for reducing API calls to rate-limited services or slow APIs
-- First call fetches from API, subsequent calls within 60s use cache
local data = http_get("https://api.example.com/data", { cache_ttl = 60 })

http_post(url, options?)

Convenience wrapper for POST requests. Same as http_request with method = "POST".

-- POST with JSON body
local response = http_post("https://api.example.com/data", {
  json = { key = "value", count = 42 }
})

-- POST with form-like body
local response = http_post("https://api.example.com/data", {
  headers = { ["Content-Type"] = "application/x-www-form-urlencoded" },
  body = "key=value&count=42"
})

-- POST with authentication
local response = http_post("https://api.example.com/data", {
  headers = { ["Authorization"] = "Bearer " .. params.token },
  json = { action = "update" }
})

Example with error handling:

local ok, response = pcall(function()
  return http_get("https://api.example.com/data", {
    headers = { ["Authorization"] = "Bearer " .. params.token }
  })
end)

if not ok then
  log_error("Request failed: " .. tostring(response))
end

JSON Functions

json_decode(str)

Parses a JSON string into a Lua table.

local data = json_decode('{"name": "Alice", "age": 30}')
print(data.name)  -- "Alice"

Parameters:

NameTypeDescription
strstringJSON string to parse

Returns: table - The parsed JSON as a Lua table

Notes:

  • JSON arrays become 1-indexed Lua tables
  • JSON null becomes Lua nil

json_encode(table)

Converts a Lua table to a JSON string.

local json = json_encode({name = "Bob", items = {1, 2, 3}})
-- '{"name":"Bob","items":[1,2,3]}'

Parameters:

NameTypeDescription
tabletableLua table to encode

Returns: string - JSON representation

Notes:

  • Tables with sequential integer keys become arrays
  • Tables with string keys become objects

HTML Parsing Functions

html_parse(html)

Parses an HTML string and returns a document object.

local doc = html_parse("<html><body><h1>Hello</h1></body></html>")

Parameters:

NameTypeDescription
htmlstringHTML string to parse

Returns: Document - Parsed document object

Document Methods

doc:select(selector)

Queries elements using a CSS selector.

local links = doc:select("a.nav-link")
local items = doc:select("ul > li")

Parameters:

NameTypeDescription
selectorstringCSS selector

Returns: Elements - Collection of matching elements

Supported selectors:

  • Tag: div, a, span
  • Class: .classname
  • ID: #idname
  • Attribute: [href], [data-id="123"]
  • Combinators: div > p, ul li, h1 + p
  • Pseudo-classes: :first-child, :nth-child(2)

doc:select_one(selector)

Returns only the first matching element.

local title = doc:select_one("h1")
if title then
  print(title:text())
end

Parameters:

NameTypeDescription
selectorstringCSS selector

Returns: Element or nil - First matching element

Elements Methods

elements:each(fn)

Iterates over all elements in the collection.

doc:select("li"):each(function(el)
  print(el:text())
end)

Parameters:

NameTypeDescription
fnfunctionCallback receiving each element

Element Methods

element:text()

Gets the inner text content.

local heading = doc:select_one("h1")
local text = heading:text()  -- "Welcome"

Returns: string - Text content

element:attr(name)

Gets an attribute value.

local link = doc:select_one("a")
local href = link:attr("href")  -- "https://..."
local class = link:attr("class")  -- "nav-link" or nil

Parameters:

NameTypeDescription
namestringAttribute name

Returns: string or nil - Attribute value

element:html()

Gets the inner HTML.

local div = doc:select_one("div.content")
local inner = div:html()  -- "<p>Paragraph</p><p>Another</p>"

Returns: string - Inner HTML

element:select(selector)

Queries descendants of this element.

local table = doc:select_one("table.data")
local rows = table:select("tr")

Parameters:

NameTypeDescription
selectorstringCSS selector

Returns: Elements - Matching descendants

element:select_one(selector)

Returns first matching descendant.

local row = doc:select_one("tr")
local first_cell = row:select_one("td")

Parameters:

NameTypeDescription
selectorstringCSS selector

Returns: Element or nil

Time Functions

time_now()

Returns the current Unix timestamp.

local now = time_now()  -- e.g., 1703672400

Returns: number - Unix timestamp (seconds since 1970)

time_format(timestamp, format)

Formats a timestamp into a string using the server’s local timezone.

local now = time_now()
time_format(now, "%H:%M")      -- "14:32"
time_format(now, "%Y-%m-%d")   -- "2024-12-27"
time_format(now, "%A, %B %d")  -- "Friday, December 27"

Parameters:

NameTypeDescription
timestampnumberUnix timestamp
formatstringstrftime format string

Returns: string - Formatted date/time

Format codes:

CodeDescriptionExample
%YYear (4 digit)2024
%yYear (2 digit)24
%mMonth (01-12)12
%dDay (01-31)27
%HHour 24h (00-23)14
%IHour 12h (01-12)02
%MMinute (00-59)32
%SSecond (00-59)05
%AWeekday nameFriday
%aWeekday shortFri
%BMonth nameDecember
%bMonth shortDec
%pAM/PMPM
%ZTimezoneCET
%%Literal %%

time_parse(str, format)

Parses a date string into a Unix timestamp.

local ts = time_parse("2024-12-27 14:30", "%Y-%m-%d %H:%M")

Parameters:

NameTypeDescription
strstringDate string to parse
formatstringstrftime format string

Returns: number - Unix timestamp

Note: Uses local timezone for interpretation.

Asset Functions

read_asset(path)

Reads a file from the current screen’s asset directory.

-- From hello.lua, reads screens/hello/logo.png
local logo_bytes = read_asset("logo.png")

Parameters:

NameTypeDescription
pathstringRelative path within the screen’s asset directory

Returns: string - Binary file contents

Throws: Error if the file cannot be read

Asset directory convention:

screens/
├── hello.lua         # Script at top level
├── hello.svg         # Template at top level
└── hello/            # Assets for "hello" screen
    ├── logo.png
    └── icon.svg

When read_asset("logo.png") is called from hello.lua, it reads screens/hello/logo.png.

Example: Embedding an image in data:

local logo = read_asset("logo.png")
local logo_b64 = base64_encode(logo)

return {
    data = {
        logo_src = "data:image/png;base64," .. logo_b64
    },
    refresh_rate = 3600
}

base64_encode(data)

Encodes binary data (string) to a base64 string.

local encoded = base64_encode(raw_bytes)

Parameters:

NameTypeDescription
datastringBinary data to encode

Returns: string - Base64-encoded string

Example: Creating a data URI from a local asset:

local image_data = read_asset("icon.png")
local data_uri = "data:image/png;base64," .. base64_encode(image_data)

Example: Embedding a remote image:

local image_bytes = http_get("https://example.com/photo.png", { cache_ttl = 3600 })
local image_src = "data:image/png;base64," .. base64_encode(image_bytes)

See Embedding Remote Images for a complete example with error handling.

URL Encoding Functions

url_encode(str)

URL-encodes a string for safe use in URLs (query parameters, path segments).

local encoded = url_encode("hello world")  -- "hello%20world"
local station = url_encode("Zürich, HB")   -- "Z%C3%BCrich%2C%20HB"

Parameters:

NameTypeDescription
strstringString to URL-encode

Returns: string - URL-encoded string

Example: Building a URL with special characters:

local station = params.station  -- "Zürich, HB"
local url = "https://api.example.com/departures?station=" .. url_encode(station)
-- Result: https://api.example.com/departures?station=Z%C3%BCrich%2C%20HB

Note: When using the params option in http_get/http_request, parameters are automatically URL-encoded. Use url_encode only when building URLs manually.

url_decode(str)

Decodes a URL-encoded string.

local decoded = url_decode("hello%20world")  -- "hello world"
local station = url_decode("Z%C3%BCrich%2C%20HB")  -- "Zürich, HB"

Parameters:

NameTypeDescription
strstringURL-encoded string to decode

Returns: string - Decoded string

Throws: Error if the string contains invalid UTF-8 after decoding

QR Code Functions

qr_svg(data, options)

Generates a pixel-aligned QR code as an SVG fragment for embedding in templates. Uses anchor-based positioning with edge margins, so you don’t need to calculate the QR code size.

-- Position QR code in bottom-right corner with 10px margins
local qr = qr_svg("https://example.com", {
  anchor = "bottom-right",
  right = 10,
  bottom = 10,
  module_size = 4
})

-- Centered QR code
local qr = qr_svg("https://example.com", {
  anchor = "center",
  module_size = 5
})

-- Top-left with custom margins
local qr = qr_svg("https://example.com", {
  anchor = "top-left",
  left = 20,
  top = 20,
  module_size = 4,
  ec_level = "H"
})

Parameters:

NameTypeDescription
datastringContent to encode (URL, text, etc.)
optionstablePositioning and rendering options (see below)

Options:

NameTypeDefaultDescription
anchorstring“top-left”Which corner to anchor: “top-left”, “top-right”, “bottom-left”, “bottom-right”, “center”
topinteger0Margin from top edge in pixels (for top-* anchors)
leftinteger0Margin from left edge in pixels (for *-left anchors)
rightinteger0Margin from right edge in pixels (for *-right anchors)
bottominteger0Margin from bottom edge in pixels (for bottom-* anchors)
module_sizeinteger4Size of each QR module in pixels (recommended: 3-6)
ec_levelstring“M”Error correction level: “L” (7%), “M” (15%), “Q” (25%), “H” (30%)
quiet_zoneinteger4QR quiet zone in modules

Anchor and margin combinations:

AnchorRelevant margins
top-lefttop, left
top-righttop, right
bottom-leftbottom, left
bottom-rightbottom, right
center(centered, margins ignored)

Returns: string - SVG fragment (<g> element with <rect> elements)

Throws: Error if QR code generation fails or if an invalid anchor is specified.

Example in template:

-- hello.lua
return {
  data = {
    -- QR code anchored to bottom-right with 10px margin
    qr_code = qr_svg("https://www.youtube.com/watch?v=dQw4w9WgXcQ", {
      anchor = "bottom-right",
      right = 10,
      bottom = 10,
      module_size = 4
    })
  },
  refresh_rate = 3600
}
<!-- hello.svg -->
{{ data.qr_code | safe }}

Notes:

  • Screen dimensions are automatically read from device.width and device.height (defaults to 800x480)
  • Use integer values for margins and module_size for crisp rendering on e-ink displays
  • Module size 3-6 pixels works well for 800x480 displays
  • Higher error correction allows the QR code to remain scannable even if partially obscured

Logging Functions

log_info(message)

Logs an informational message.

log_info("Processing request for: " .. station)

Parameters:

NameTypeDescription
messagestringMessage to log

Server output:

INFO script=true: Processing request for: Olten

log_warn(message)

Logs a warning message.

log_warn("API response was empty")

Parameters:

NameTypeDescription
messagestringMessage to log

log_error(message)

Logs an error message.

log_error("Failed to parse response: " .. err)

Parameters:

NameTypeDescription
messagestringMessage to log

Script Return Value

Every script must return a table with this structure:

return {
  data = {
    -- Any data structure
    -- Available in template as data.*
    title = "My Title",
    items = { ... }
  },
  refresh_rate = 300,       -- Seconds until next refresh
  skip_update = false,      -- Optional: skip rendering, just check back later
  colors = { "#000000", "#FFFFFF", "#FF0000" },  -- Optional: override display palette
  dither = "atkinson",      -- Optional: dither algorithm
  preserve_exact = true,    -- Optional: preserve exact palette matches (default: true)
  error_clamp = 0.08,       -- Optional: error diffusion clamp
  noise_scale = 0.6,        -- Optional: blue noise jitter scale
  chroma_clamp = 2.0,       -- Optional: chromatic error clamp
  strength = 0.8,           -- Optional: error diffusion strength (default 1.0)
}

data

FieldTypeDescription
datatableData passed to the Tera template under data.* namespace

The data table can contain any Lua values:

  • Strings, numbers, booleans
  • Nested tables (become objects)
  • Arrays (1-indexed tables with sequential keys)

In templates, access this data with the data. prefix:

<text>{{ data.title }}</text>
{% for item in data.items %}...{% endfor %}

refresh_rate

FieldTypeDescription
refresh_ratenumberSeconds until device should refresh

Guidelines:

  • 30-60: Real-time data (transit, stocks)
  • 300-900: Regular updates (weather, calendar)
  • 3600+: Static or slow-changing content

If refresh_rate is 0 or omitted, the screen’s default_refresh from config is used.

colors

FieldTypeDescription
colorstable or nilOptional array of hex RGB color strings to override the display palette

When colors is returned by a script, it takes the highest priority in the color palette chain:

  1. Script colors (strongest) — returned in the script result table
  2. Device config colors — set per-device in config.yaml
  3. Firmware Colors header — sent by device hardware
  4. System default#000000,#555555,#AAAAAA,#FFFFFF
-- Force a 3-color palette for this screen
return {
  data = { ... },
  refresh_rate = 300,
  colors = { "#000000", "#FFFFFF", "#FF0000" }
}

dither

FieldTypeDescription
ditherstring or nilOptional dithering algorithm

Controls the dithering algorithm used when converting SVG to e-ink PNG. Available values:

ValueAlgorithmDescription
"atkinson" (default)AtkinsonError diffusion (75% propagation)
"atkinson-hybrid"Atkinson Hybrid100% achromatic / 75% chromatic propagation
"floyd-steinberg"Floyd-SteinbergGeneral-purpose error diffusion
"jarvis-judice-ninke"JJNWide kernel, least oscillation
"sierra"Sierra10-neighbor error diffusion
"sierra-two-row"Sierra Two-Row7-neighbor error diffusion
"sierra-lite"Sierra LiteFastest error diffusion
"stucki"StuckiWide 12-neighbor kernel similar to JJN
"burkes"Burkes7-neighbor, good balance of speed and quality

The dither mode follows a priority chain:

  1. Dev UI override (strongest) — set in dev mode
  2. Script dither — returned in the script result table
  3. Device config dither — set per-device in config.yaml
  4. Default"atkinson"
-- Use Floyd-Steinberg dithering for a screen that displays images
return {
  data = { image_url = "..." },
  refresh_rate = 3600,
  dither = "floyd-steinberg"
}

preserve_exact

FieldTypeDescription
preserve_exactboolean or nilWhether to preserve exact palette color matches (default: true)

When true (default), pixels that exactly match a palette color are kept as-is without dithering. This preserves sharp edges for text, lines, and borders. Set to false to force all pixels through the dithering pipeline.

return {
  data = { ... },
  refresh_rate = 300,
  preserve_exact = false  -- force all pixels through dithering
}

error_clamp, noise_scale, chroma_clamp, strength

FieldTypeDescription
error_clampnumber or nilLimits error diffusion amplitude (e.g. 0.08)
noise_scalenumber or nilBlue noise jitter scale (e.g. 0.6)
chroma_clampnumber or nilLimits chromatic error propagation (e.g. 2.0)
strengthnumber or nilError diffusion strength multiplier (0.0 = no diffusion, 1.0 = standard, default)

Fine-tune dithering behavior per-script. These override device config and panel default values but are overridden by dev UI settings.

Priority chain: dev UI > script return > device config > panel dither defaults > algorithm defaults.

Use dev mode to interactively find good values, then set them here or in the panel dither defaults for production use.

-- Tuned values for a photo screen on a 4-color panel
return {
  data = { ... },
  refresh_rate = 3600,
  dither = "floyd-steinberg",
  error_clamp = 0.08,
  noise_scale = 0.5,
  strength = 0.8
}

skip_update

FieldTypeDescription
skip_updatebooleanIf true, don’t update the display - just tell device to check back later

When skip_update is true:

  • No new image is rendered
  • The device keeps its current display content
  • The device will check back after refresh_rate seconds

This is useful when your data source hasn’t changed:

-- Check if data has changed since last update
local cached_hash = get_data_hash()
local current_data = fetch_data()
local new_hash = compute_hash(current_data)

if cached_hash == new_hash then
  -- No changes - tell device to check back in 5 minutes
  return {
    data = {},
    refresh_rate = 300,
    skip_update = true
  }
end

-- Data changed - render new content
return {
  data = current_data,
  refresh_rate = 300,
  skip_update = false  -- or just omit it
}

Note: When skip_update is true, the data table is ignored since no rendering occurs.

Standard Lua Functions

Byonk uses Lua 5.4. Standard library functions available include:

String

  • string.format, string.sub, string.find
  • string.match, string.gmatch, string.gsub
  • string.upper, string.lower, string.len

Table

  • table.insert, table.remove
  • table.sort, table.concat
  • ipairs, pairs

Math

  • math.floor, math.ceil, math.abs
  • math.min, math.max
  • math.random

Other

  • tonumber, tostring, type
  • pcall (for error handling)

Not available: File I/O, OS functions, network (except http_get)