SolarOS

SolarOS 4.6.8 manual · api

SolarOS Lua API

SolarOS embeds Lua as the lua foreground application. It can run an interactive REPL or execute .lua files from storage.

The SolarOS API is preloaded as the global table solaros. A minimal require("solaros") shim is also provided:

local solaros = require("solaros")

print("SolarOS " .. solaros.version())
print(solaros.identity.format())

Lua allocations prefer PSRAM. Host-facing Lua io, os, and dynamic package loading are intentionally not opened; scripts should use SolarOS services for hardware, storage, networking, and foreground UI.

Top-Level Helpers

For example, solaros.tick_interval(5) lets a foreground Lua app drain terminal, TUI, and graphics events at a best-effort 5 ms cadence. It does not schedule or preempt Lua code, and it is not a hard-real-time timer. The setting lasts for the current foreground Lua app only; headless script jobs cannot change it.

Service Tables

Lua mirrors the Python solaros module structure:

The Lua runtime package requires PSRAM. Hardware and network tables are present only when the board/flavor includes the corresponding service package. For example, an ODROID-GO full build includes Lua with solaros.spi and solaros.onewire, while omitting solaros.adc and solaros.i2c because those service packages are not available on that board.

Lua strings are binary-safe, so byte-oriented APIs such as uart.read, i2c.read_reg, clipboard.get, and mqtt.read().payload return Lua strings.

solaros.messages.send(conversation_id, body[, allow_untrusted]) queues a message and returns its stable hexadecimal ID. list() also represents message IDs as hexadecimal strings, and cancel(id) accepts that representation. Blocked direct endpoints are always rejected; discovered endpoints require the optional boolean for that one send. solaros.contacts returns only contact summaries and endpoint IDs, never credentials or endpoint secret material.

solaros.onewire.scan(pin) returns tables containing a 16-digit hexadecimal address and numeric family code. solaros.onewire.xfer(pin, read_len[, data]) resets the bus, writes the binary-safe data string, and returns read_len bytes. Reads and writes are each limited to 64 bytes.

Named buses and expansion devices

solaros.buses discovers board-defined and runtime-created buses independently of the legacy single-board-bus and direct-pin service tables.

Bus tables contain id, name, protocol, origin, sharing, attached, detachable, ready, and lease_count, plus protocol-specific pins and configuration. create_spi requires host, sclk, mosi, and a one-to-four-element cs array. miso and max_transfer_size are optional. I2C bus tables include port, sda_pin, scl_pin, and speed_hz. Named I2C operations take and release a shared lease automatically; the legacy solaros.i2c table remains an i2c0 shortcut. OneWire bus tables include pin. Named OneWire operations take and release an exclusive lease automatically; solaros.onewire remains the direct-pin compatibility API. UART bus tables include port, tx_pin, rx_pin, and baud_rate; named UART I/O takes and releases an exclusive lease automatically.

create_i2c requires port, sda, and scl; optional speed_hz defaults to

  1. create_onewire requires pin. Both claim their approved runtime pins
  2. until remove(name).

create_uart requires port, tx, and rx; optional baud_rate defaults to

  1. Runtime descriptors are detachable and removable. Board descriptors
  2. whose signal pins are marked releasable are detachable but never removable; fixed-pin board descriptors reject detach. Attached buses own their hardware endpoint and signal pins, while protocol hardware starts on first lease.

local solaros = require("solaros")

local bus = solaros.buses.create_spi("spi1", {
    host = solaros.buses.SPI3_HOST,
    sclk = 1,
    mosi = 2,
    miso = 3,
    cs = {17},
})
print(bus.name, bus.origin)

local reply = solaros.buses.spi_xfer("spi1", "gpio17", "\x9f\x00\x00\x00")
print(#reply)
solaros.buses.remove("spi1")
local solaros = require("solaros")

local i2c1 = solaros.buses.create_i2c("i2c1", {
    port = 1,
    sda = 14,
    scl = 15,
    speed_hz = 100000,
})
print(#solaros.buses.i2c_scan(i2c1.name))
solaros.buses.remove(i2c1.name)

local onewire0 = solaros.buses.create_onewire("onewire0", {pin = 16})
print(#solaros.buses.onewire_scan(onewire0.name))
solaros.buses.remove(onewire0.name)

local uart1 = solaros.buses.create_uart("uart1", {
    port = 1,
    tx = 14,
    rx = 15,
    baud_rate = 115200,
})
solaros.buses.uart_write(uart1.name, "AT\r\n")
print(solaros.buses.uart_read(uart1.name, 64, 500))
solaros.buses.detach(uart1.name)
solaros.buses.attach(uart1.name)
solaros.buses.remove(uart1.name)
local solaros = require("solaros")

local bus = solaros.buses.get("i2c0")
print(bus.name, bus.speed_hz)
local addresses = solaros.buses.i2c_scan("i2c0")
solaros.buses.i2c_probe("i2c0", 0x3c)
local solaros = require("solaros")

local bus = solaros.buses.get("onewire0")
print(bus.name, bus.pin)
local devices = solaros.buses.onewire_scan("onewire0")
local reply = solaros.buses.onewire_xfer("onewire0", 9, "\xcc\x44")

solaros.expansion.drivers() lists compiled drivers, and devices() lists active devices with normalized bindings. attach(driver, name, bindings) and detach(name) mirror the shell lifecycle. Binding tables accept spi, cs (or ce), i2c, addr, uart, gpio, irq, reset (or rst), dc, busy, data, adc, pwm, and count. cs requires spi, addr requires i2c, and unknown fields are rejected.

solaros.expansion.attach("pcd8544", "lcd0", {
    spi = "spi0",
    cs = 10,
    dc = 4,
    reset = 5,
})
print(#solaros.expansion.devices())
solaros.expansion.detach("lcd0")

NeoPixel set and fill update a buffer; call show once after a batch of changes. clear updates and transmits immediately.

solaros.expansion.attach("neopixel", "pixels0", {data = 1, count = 8})
solaros.neopixel.fill("pixels0", 0, 0, 8)
solaros.neopixel.set("pixels0", 3, 16, 0, 0)
solaros.neopixel.show("pixels0")

solaros.spi is a compatibility table that selects spi0 when present, otherwise the first registered named SPI bus. On a dynamic-only board its status().available value remains false until a bus is created. status() reports the selected bus pins, transfer limit, and configured chip-select slots. xfer(cs, data[, mode[, speed_hz]]) performs a full-duplex transaction. read(cs, length[, fill[, mode[, speed_hz]]]) and write(cs, data[, mode[, speed_hz]]) provide one-direction convenience forms. The cs argument accepts a configured slot name or its numeric GPIO. Lua data and return values are binary-safe strings. New code should address buses explicitly through solaros.buses.spi_*.

USB HID

service.hid is retained as a dormant package and is not compiled into the standard SolarOS flavors because the TinyUSB composite stack currently costs too much internal SRAM. On an ESP32-S3 build that explicitly enables it, the same USB connection remains available as cdc0 while also advertising keyboard, mouse, and gamepad HID reports. Lua uses the same typed operations and constants as Python:

local hid = solaros.hid

hid.keyboard.press(hid.KEY_LEFT_CTRL, hid.KEY_C)
hid.keyboard.release_all()
hid.mouse.move(10, -4)
hid.mouse.button(hid.MOUSE_LEFT, true)
hid.mouse.button(hid.MOUSE_LEFT, false)
hid.gamepad.axis(hid.AXIS_X, -12000)
hid.gamepad.button(1, true)
hid.gamepad.hat(hid.HAT_UP)
hid.gamepad.send()

Keyboard transitions are queued, mouse deltas accumulate, and gamepad state is coalesced until send(). Axes use -32768..32767; gamepad buttons are 1..32. Disconnected or unavailable HID calls raise ESP_ERR_INVALID_STATE. SolarOS sends neutral reports whenever the Lua runtime exits, fails, or is force-stopped.

solaros.uart is the default uart0 compatibility table; use solaros.buses.uart_* for another named UART and solaros.buses.attach() or detach() for lifecycle control. solaros.uart.status() includes the bus name, attached, rx_buffered, and rx_buffered_valid. When another owner is actively using the UART, rx_buffered_valid is false because the live RX count is not sampled.

Identity

solaros.identity.user() and hostname() return the NVS-backed device identity. set_user(name) and set_hostname(name) validate and persist new values. Reboot before expecting an already initialized Wi-Fi interface to advertise a changed hostname.

SSH and SCP use the identity user as their default remote username when user@host is not supplied.

Existing /.solar/user and /.solar/hostname files are imported once when their corresponding NVS keys are absent.

TUI

solaros.tui draws through the foreground UI queue. It exposes constants NORMAL, BOLD, INVERSE, plus common key constants such as KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_ESCAPE, KEY_PAGE_UP, and KEY_PAGE_DOWN.

Functions:

Example:

local solaros = require("solaros")
local tui = solaros.tui

tui.clear()
tui.box(0, 0, tui.rows(), tui.cols())
tui.addstr(1, 2, "SolarOS Lua", tui.BOLD)
tui.addstr(3, 2, "Press ESC")
tui.refresh()

while not solaros.should_exit() do
    local key = tui.getch(250)
    if key == tui.KEY_ESCAPE then
        break
    end
end

Jobs

solaros.jobs.list() and solaros.jobs.status(name) return the effective tick_interval_ms and tick_deadline_ms plus tick_last_us, tick_max_us, and tick_deadline_misses runtime telemetry. worker_stack_bytes is the declared launch-admission requirement and worker_stack_external identifies its memory region. Job control is available through start(name[, args]) and stop(name).

Sessions

solaros.sessions creates manual port shell sessions and closes sessions by id. Script-created port shells do not run /.shell/startup.

Example:

local solaros = require("solaros")

pcall(function()
    solaros.jobs.stop("slip")
end)

local sid = solaros.sessions.create_shell(
    "uart0", {term = "ansi", cols = 80, rows = 25, charset = "ascii"}
)
-- later:
solaros.sessions.close(sid)
solaros.jobs.start("slip", {"uart0", "115200"})

Graphics

solaros.gfx draws through the foreground graphics service. begin() uses the display framebuffer of the shell that launched the script; from a port or headless shell it raises an error because there is no foreground display. begin(target) claims a verified named display target, such as one returned by solaros.expansion.devices(), until end() or script cleanup. Colors are WHITE, LIGHT, DARK, BLACK, and gray(level) with 0..GRAY_MAX. Fonts are FONT_SMALL, FONT_MONO, FONT_BOLD, regular document fonts FONT_MONO_12 through FONT_MONO_20, bold document fonts FONT_BOLD_12 through FONT_BOLD_20, and matching italic/bold-italic constants. Italic constants currently map to the closest upright face in the trimmed firmware font set.

Functions:

Bitmap and sprite rows are packed least-significant bit first, with (width + 7) // 8 bytes per row. Set bits draw in the current color and clear bits remain transparent. One call accepts at most 128 packed bytes, enough for a 32 by 32 sprite. Lua passes the packed bytes in a binary string.

Example:

local solaros = require("solaros")
local gfx = solaros.gfx

gfx.begin()
local w, h = gfx.size()
gfx.clear(gfx.WHITE)
gfx.color(gfx.BLACK)
gfx.rect(8, 8, w - 16, h - 16)
gfx.font(gfx.FONT_BOLD)
gfx.text(24, 36, "SolarOS Lua")
gfx.color(gfx.gray(12))
gfx.fill_circle(w // 2, h // 2, 36)
gfx.color(gfx.BLACK)
gfx.circle(w // 2, h // 2, 36)
gfx.refresh()

while not solaros.should_exit() do
    local key = gfx.getch(250)
    if key == gfx.KEY_ESCAPE then
        break
    end
end

gfx["end"]()

For an attached auxiliary display, first verify its ready target name, then pass that name:

gfx.begin("lcd0")
gfx.clear(gfx.WHITE)
gfx.text(2, 14, "aux")
gfx.present()
gfx["end"]()

Notes

Lua tables returned as lists use normal Lua 1-based array indexes. Direct block lookup with solaros.storage.block(index) follows the underlying storage service index, matching Python's 0-based block(index).

The Lua bridge intentionally does not expose raw SSH/SCP session handles. Those need explicit object lifetime and event-loop rules before becoming scriptable.

Quick reference

Load solaros and use its service tables for storage, time, networking, hardware, jobs, sessions, TUI, and graphics. Lua arrays are 1-based unless an individual service explicitly exposes a native index. Close resources and keep long-running loops cooperative.