CrossLua Reader exposes five modules to Lua plugins: display, input, storage, system, and font. These are registered automatically when a Lua state is created via api_create_state().
Standard Lua libraries available: base, string, table, math, utf8, coroutine.
Excluded: io, os, debug (replaced by CrossLua HAL APIs).
Screen rendering and measurement.
Fill the framebuffer. color: 0xFF = white (default), 0x00 = black.
Push framebuffer to e-ink display. mode: 0 = full, 1 = half, 2 = fast (default).
Full refresh (best quality, clears ghosting).
Draw UTF-8 text. fontId from font.load(). x, y are logical coordinates (top-left of text line). If a fallback font is set via font.setFallback(), missing glyphs are automatically rendered from the fallback font.
Draw a black line between two points.
Draw a rectangle outline (1px border).
Draw a filled rectangle.
Draw a filled black rectangle with rounded corners. radius defaults to 6.
Draw a filled rectangle with rounded corners using a dithered gray checkerboard pattern. radius defaults to 6. This is the recommended selection highlight — text remains readable on the gray background, matching CrossPoint's Lyra theme.
Draw white text (for rendering text on a dark/selected background). Same as drawText but draws white pixels instead of black.
Set the screen orientation. 0 = portrait, 1 = landscape CW, 2 = inverted, 3 = landscape CCW. Affects all subsequent drawing and dimension queries.
Returns the current orientation (0-3).
Logical screen width for current orientation.
Logical screen height for current orientation.
Draw a line in physical (portrait) coordinates, bypassing orientation. Used by button hints to always render at the physical bottom.
Draw a rectangle in physical coordinates.
Draw text in physical coordinates. Used by button hints.
Measure text advance width in pixels.
Line height for the font.
Example:
local f = font.load("/fonts/NotoSans-14-Regular.cfont")
display.clear()
display.drawText(f, 20, 20, "Hello, World!")
display.drawLine(20, 50, 400, 50)
display.refresh()Button state queries and constants.
No-op — the main loop polls buttons automatically before calling your loop(). This function exists for API compatibility but does nothing. Do not call hal_gpio_poll() manually — it would clear button edge states.
True if the logical button is currently held down. Scans all physical buttons through the remap table.
True if the logical button was pressed since last frame. Orientation-aware via setMapping.
True if any physical button was pressed since last frame.
True if the logical button was released since last frame. Orientation-aware via setMapping.
True if any physical button was released since last frame.
Milliseconds the current button(s) have been held.
Block until a button is pressed. Returns the logical button ID (remapped via setMapping). Yields to FreeRTOS while waiting.
Set the orientation-aware button remap. The table maps logical actions to physical hardware indices: {back=0, confirm=1, left=2, right=3, up=4, down=5}. Internally inverted to a physical→logical lookup table. Called automatically by home.lua and settings.lua using buttons.get_mapping(orientation).
Reset button mapping to identity (physical == logical). No remap.
| Constant | Value | Description |
|---|---|---|
input.BACK |
0 | Back button |
input.CONFIRM |
1 | Confirm/OK |
input.LEFT |
2 | Left |
input.RIGHT |
3 | Right |
input.UP |
4 | Up (side) |
input.DOWN |
5 | Down (side) |
input.POWER |
6 | Power button |
Example:
input.poll()
if input.wasPressed(input.CONFIRM) then
system.log("Confirm pressed!")
end
-- Or block until any button:
local btn = input.waitButton()
system.log("Button " .. btn .. " pressed")SD card file and directory operations.
Read entire file as a string. Max 256KB. Returns nil, errmsg on failure.
Read a byte range from a file. Max 64KB per read.
Write string to file (creates/overwrites).
Check if file or directory exists.
Create directory (and parents).
Delete a file.
Get file size in bytes. Returns nil if file doesn't exist.
List directory contents. Returns array of {name=string, isDir=bool}.
Example:
-- Write and read back
storage.write("/test.txt", "Hello from Lua!")
local content = storage.read("/test.txt")
system.log("Read: " .. content)
-- List a directory
local entries = storage.list("/books")
if entries then
for _, e in ipairs(entries) do
local kind = e.isDir and "[DIR]" or "[FILE]"
system.log(kind .. " " .. e.name)
end
endDevice information, timing, and logging.
Current free heap in bytes.
Total heap size in bytes.
Battery level 0-100%. Cached (polled every 30 seconds).
Milliseconds since boot.
Yield to FreeRTOS for ms milliseconds. Use this in tight loops to prevent watchdog timeouts.
Print a message to the serial log.
Firmware version string.
Reboot the device. Does not return.
Enter deep sleep. Does not return. Wake with power button.
Set the auto-sleep timeout. minutes: 1-60. 0 = disable auto-sleep.
Suppress or restore auto-sleep. Use when WiFi server is active or during downloads. true = prevent sleep, false = restore. Note: USB connection is auto-detected and suppresses sleep automatically.
Set the sleep screen mode. mode: 0 = blank (white screen), 1 = single wallpaper, 2 = cycle through /wallpapers/, 3 = random from /wallpapers/, 4 = clear (keep current page, overlay "SLEEP" text). Called from home plugin on boot to push persisted setting to C.
Set the wallpaper filename for single mode (e.g., "sunset.bmp"). The file must exist in /wallpapers/ on the SD card.
Register a Lua callback that runs after the base sleep screen renders but before the display refresh. The callback can draw anything to the framebuffer using display.drawText(), display.fillRect(), etc. Pass nil to clear the hook.
The hook is automatically cleared when the plugin exits or crashes. If the hook itself errors, the error is logged and sleep proceeds normally.
-- Example: quote overlay on sleep screen
system.setSleepHook(function()
local f = fonts.reader or fonts.ui
if not f then return end
-- Draw a bordered box near the bottom
display.fillRect(20, 620, 440, 100) -- white fill
display.drawRect(20, 620, 440, 100) -- black border
display.drawText(f, 30, 630, '"Be the change you wish to see."')
display.drawText(f, 30, 670, "— Gandhi")
end)
-- Clear the hook:
system.setSleepHook(nil)Request a deferred SD-card reinit + plugin-system restart from home. Use after SD card hot-swap; the firmware-bundled rescue home calls this from its Confirm handler.
Deferred semantics (important): system.reload() returns immediately — it sets a flag. The actual hal_storage_reinit() + plugin_manager_reinit() + restart runs after the current plugin.loop() (or any other Lua callback) returns and unwinds. This is required for safety: doing the reinit synchronously would lua_close the very state the caller is running in. Code after system.reload() in the same Lua frame still executes; the reload happens on the next dispatcher tick.
The same pathway is invoked automatically by holding the power button for >2 seconds (handled C-side in main.cpp, doesn't go through Lua).
Example:
system.log("CrossLua Reader v" .. system.version())
system.log("Free heap: " .. system.freeHeap() .. " bytes")
system.log("Battery: " .. system.batteryPercent() .. "%")Load, unload, and configure .cfont font files from the SD card.
Load a .cfont file. Returns an integer font ID for use with display.drawText() and display.getTextWidth(). Max 4 fonts loaded simultaneously.
Free a loaded font and its memory. Also clears any fallback references pointing to this font.
Set a fallback font for a slot. When a glyph is missing from the primary font, the renderer automatically tries the fallback font before rendering the replacement character. Used for non-Latin script support (e.g., NotoSans primary with NotoSansHebrew fallback).
Returns false if either font is not loaded, if IDs are the same, or if it would create a circular chain.
Remove the fallback font from a slot.
Returns the slot id of the firmware boot font, or -1 if no boot font is currently loaded. The boot font is loaded once at startup (Ubuntu-12-Regular) — from SD when present, from a firmware-bundled copy in .rodata as fallback. This is the only font guaranteed to be available without an SD card, so the firmware-bundled rescue home and any plugin that needs to render text in degraded conditions should use it.
The boot font slot is shared — don't font.unload() it.
Example:
local reader = font.load("/fonts/NotoSans-14-Regular.cfont")
local hebrew = font.load("/languages/he/fonts/NotoSansHebrew-14-Regular.cfont")
font.setFallback(reader, hebrew)
-- Now drawText with reader font automatically renders Hebrew from the fallback:
display.drawText(reader, 20, 20, "Hello שלום") -- seamless mixed script
font.clearFallback(reader)
font.unload(hebrew)
font.unload(reader)Centralized display layout engine. Divides the screen into three non-overlapping regions — Header, Body, Footer — and provides authoritative bounds and line metrics. C-side computation, Lua-side configuration.
Set header region height in pixels. 0 = hidden (body extends to top). Default 0.
Set footer region height in pixels. 0 = hidden (body extends to bottom). Default 40.
Set body margins in pixels. Content is inset from the body region edges.
Set uniform margin on all sides.
Set extra pixels between lines. Default 0.
Manually set line height. Overrides font-derived value. 0 = derive from font.
Derive line height from a loaded font's advance_y metric. Recalculates linesPerPage.
Set orientation (0-3). Recalculates all regions for new display dimensions.
Reserve space for the physical button bar in landscape modes. 48 = UI plugins (default, reserves space for button hint labels), 0 = readers (no button hints, reclaims full screen). In portrait, the footer covers the button bar zone. In landscape, the button bar maps to a side edge and must be explicitly reserved.
Note: The plugin manager calls layout.resetDefaults() automatically before each plugin's onEnter(), resetting all layout values including button bar to defaults (48). Readers override with layout.setButtonBar(0) in their onEnter().
How many uniform text lines fit in the body area. Single source of truth — indexer and renderer both use this.
Effective line height: line_height + line_spacing.
Header region bounds. All zero if header height is 0.
Body region bounds with margins applied. Use for text content positioning.
Body region bounds without margins. Use for free drawing (backgrounds, borders).
Footer region bounds. All zero if footer height is 0.
Convenience: body width with margins.
Convenience: body height with margins.
Example — Reader plugin setup:
-- Configure layout for reader mode (no header, status bar footer)
layout.setHeaderHeight(0)
layout.setFooterHeight(40)
layout.setMargin(settings.get("screenMargin", 10))
layout.setFont(fonts.reader)
-- Query body area for text positioning
local bx, by, bw, bh = layout.bodyArea()
local lh = layout.lineHeight()
local lpp = layout.linesPerPage()
-- Query footer area for status bar positioning
local fx, fy, fw, fh = layout.footerArea()
display.drawLine(fx, fy, fx + fw, fy) -- separator at top of footer
display.drawText(font, fx + 20, fy + 8, "1/42")Example — App plugin setup:
-- Configure layout for app mode (header + footer)
layout.setHeaderHeight(84)
layout.setFooterHeight(40)
layout.setMargin(20)
local bx, by, bw, bh = layout.bodyArea()
-- Draw menu items within bx, by, bw, bhC-side text indexing and page layout. Streams files from SD in 8KB chunks — never loads the full file. Word wrapping uses font measurement directly in C for maximum speed.
Scan an entire text file and build a page offset index. Streams the file in chunks, word-wraps each line with font measurement, and returns a Lua table of byte offsets (one per page). Uses layout.bodyWidth() and layout.linesPerPage() from the layout engine — configure the layout before calling.
- Keeps one file handle open during the entire scan
- Uses static buffer (no heap allocation)
- Glyph cache stays warm across the scan
- Returns
nil, errmsgon failure
Read one page of text starting at a byte offset. Word-wraps with font measurement and returns a table of display line strings. Uses layout engine for width and line count.
Returns nil on failure.
Example:
-- Configure layout engine first
layout.setHeaderHeight(0)
layout.setFooterHeight(40)
layout.setMargin(10)
layout.setFont(fonts.reader)
-- Index the file (fast, C-side, uses layout engine)
local offsets = text.indexPages(fonts.reader, "/books/story.txt")
-- Render page 5
local lines = text.getPageLines(fonts.reader, "/books/story.txt", offsets[5])
local bx, by = layout.bodyArea()
for i, line in ipairs(lines) do
display.drawText(fonts.reader, bx, by + (i-1) * layout.lineHeight(), line)
end
display.refresh()ZIP archive reader. Wraps lib/zip for SD-resident archives. The handle returned by zip.open is a userdata; methods are called via : syntax. Handles are GC-collected if you forget to call :close().
Opens a .zip / .epub and parses the central directory. When validate_epub_mimetype=true, also verifies the OCF magic (first entry must be mimetype, STORE method, exact bytes application/epub+zip).
Idempotent. Frees the native handle.
All entry names in the central directory.
Existence check by exact name.
Uncompressed size of the named entry (0 if not found or empty).
Read full entry into a Lua string. Capped at 256 KB — for chapters and images use streaming reads via the epub module.
Inspects META-INF/encryption.xml. "obfuscation" = IDPF/Adobe font obfuscation only (book is openable). "drm" = real DRM (book must be rejected).
Streaming XML/HTML SAX parser wrapping expat. Element local names are stripped of namespace URIs; attribute keys keep their literal form (e.g. epub:type).
Parses an XML or HTML string with callbacks.
- input: string (whole document).
- opts:
{ mode = "strict" | "html" }— strict for OPF/container/NCX, html for NavDoc and chapter XHTML. - handlers:
{ on_start = function(tag, attrs), on_end = function(tag), on_text = function(text) }— any field may benil.attrsis a table{ [key] = value }.
xml.parse(text, {mode = "strict"}, {
on_start = function(tag, attrs)
if tag == "rootfile" then opf_path = attrs["full-path"] end
end,
})High-level EPUB book object. Opens a .epub, parses container + OPF + TOC, and exposes navigation primitives.
Opens a .epub and parses its metadata. On failure returns three values: nil, an error code string, and a human-readable message.
Possible errcode values: "io_error", "not_epub", "drm", "malformed_container", "malformed_opf", "no_spine", "fixed_layout", "oom".
Idempotent. Frees the parsed structures and underlying ZIP handle.
Returns {title, author, language, identifier, modified, publisher, date_published, description, cover_id, epub_version, page_progression_direction}. String fields are nil if not present in the OPF. epub_version is 2 or 3. page_progression_direction is "ltr" or "rtl".
Manifest item access. manifest_at is 1-based. Each item is a table {id, href, media_type, properties}. properties is the raw EPUB 3 properties string (e.g. "cover-image nav") or nil.
Spine in linear reading order. 1-based. Each entry is {idref, href, media_type, linear} where href and media_type are pre-resolved from the manifest.
Hierarchical TOC. Each node: {label, href, depth, children} where children is an array of nodes (omitted for leaves). NCX (EPUB 2) and NavDoc (EPUB 3) are both normalized to this shape.
Resolve an internal link. spine_index is 1-based. fragment is the part after # (or nil). Returns nil if the target doesn't match a spine entry.
Read a manifest item to a Lua string (href resolved relative to the OPF base path). Capped at 256 KB.
Item size lookup; cumulative bytes through the given spine index for percent-progress reporting.
Strings returned at open time.
local book, err = epub.open(path)
if not book then
system.log("open failed: " .. err)
return
end
local m = book:metadata()
print(m.title, "by", m.author)
for i = 1, book:spine_count() do
print(i, book:spine_at(i).href)
end
book:close()-- /plugins/hello.lua
local font_id = font.load("/fonts/NotoSans-14-Regular.cfont")
display.clear()
display.drawText(font_id, 20, 20, "Hello, CrossLua Reader!")
display.drawText(font_id, 20, 60, "Free heap: " .. system.freeHeap())
display.drawText(font_id, 20, 100, "Battery: " .. system.batteryPercent() .. "%")
display.drawLine(20, 140, display.width() - 20, 140)
display.drawText(font_id, 20, 160, "Press any button to exit")
display.refresh()
local btn = input.waitButton()
system.log("Exit button: " .. btn)
font.unload(font_id)