SolarOS

SolarOS 4.6.8 manual · api

SolarOS Python API

SolarOS embeds MicroPython as the python foreground application. It can run an interactive REPL or execute .py and .mpy files from storage.

python
python /apps/demo.py arg1 arg2

Scripts receive their arguments through sys.argv. Script output is drawn in the SolarOS terminal. The active shell's app-exit key exits the REPL or requests KeyboardInterrupt while code is running.

The native module is called solaros:

import solaros

solaros.write("SolarOS " + solaros.version() + "\n")

Conventions

Most mutating functions return None on success and raise OSError("ESP_ERR_...") on service failure. Query functions return strings, integers, booleans, dictionaries, or lists.

SolarOS uses a size-trimmed MicroPython configuration but includes the standard min() and max() built-ins used by ordinary device scripts.

Functions that accept file paths use SolarOS shell-style paths. / means the default storage mount; internally this resolves to the active storage mount point.

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

Optional API groups follow these package gates:

print(solaros.storage.resolve("/.shell/history"))

Datetime dictionaries use this shape:

{
    "year": 2026,
    "month": 6,
    "day": 19,
    "hour": 12,
    "minute": 30,
    "second": 0,
    "weekday": 5,
    "clock_integrity": True,
}

Datetime setters and converters accept either such a dict or positional values:

solaros.time.set_datetime(2026, 6, 19, 12, 30, 0)
solaros.time.set_datetime({"year": 2026, "month": 6, "day": 19, "hour": 12, "minute": 30})

Top-Level Helpers

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

solaros.contacts and solaros.messages

Provider-neutral messaging builds expose:

Scripts cannot read credentials or endpoint secret material. Blocked direct endpoints are rejected, and discovered endpoints require allow_untrusted=True for that one send.

solaros.storage

Storage functions expose SD mount and filesystem service operations.

Example:

import solaros

if not solaros.storage.is_mounted():
    solaros.storage.mount()

print(solaros.storage.usage("/"))
print(solaros.storage.read_file("/notes/example.txt", 512))
for block in solaros.storage.blocks():
    print(block["name"], block["type"], block["mounted"], block["mount_point"])

solaros.time

Time functions use the SolarOS RTC/time service.

Example:

import solaros

print("uptime", solaros.time.uptime())
print("local", solaros.time.datetime())

if solaros.wifi.status()["has_ip"]:
    print(solaros.time.ntp_sync())

solaros.battery

Available when the firmware includes the battery service.

Example:

import solaros

battery = solaros.battery.status()
print("{} mV, {}%".format(battery["voltage_mv"], battery["percent"]))

solaros.sensors

Available when the firmware includes the environmental sensor service.

Example:

import solaros

env = solaros.sensors.environment()
print("{:.1f} C {:.1f}%".format(env["temperature_c"], env["humidity_percent"]))

solaros.wifi

Wi-Fi functions expose station, SoftAP, scan, and NAT controls.

Example:

import solaros

solaros.wifi.start()
print(solaros.wifi.status())

for ap in solaros.wifi.scan():
    print(ap["rssi"], ap["auth"], ap["ssid"])

solaros.mqtt

MQTT functions expose the shared SolarOS MQTT service. Broker URL and credentials are stored in NVS, so they work without an SD card.

Example:

import solaros

solaros.mqtt.connect("mqtts://broker.example.com:8883", "user", "secret")
solaros.mqtt.publish("solaros/status", b"online", 0, False)
solaros.mqtt.subscribe("solaros/inbox/#")

while not solaros.should_exit():
    msg = solaros.mqtt.read(1000)
    if msg:
        print(msg["topic"], msg["payload"])

solaros.gpio

GPIO functions expose only runtime-safe expansion pins. Use solaros.gpio.pins() to inspect the active board. On the Waveshare ESP32-S3-RLCD-4.2 this is GPIO1, GPIO2, GPIO3, GPIO17, plus releasable GPIO43/GPIO44 while uart0 is detached. On the ESP32-S3-DevKitC-1-N16R8 this is GPIO1, GPIO2, GPIO4, GPIO5, GPIO6, GPIO7, GPIO10, GPIO14, GPIO15, GPIO16, GPIO17, GPIO18, GPIO21, GPIO39, GPIO40, GPIO41, GPIO42, and GPIO47. On ODROID-GO this is GPIO4 and GPIO15. On the Elecrow CrowPanel ESP32-S3 4.2-inch E-paper this is GPIO8, GPIO9, GPIO14, GPIO15, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, and GPIO38.

Example:

import solaros

for pin in solaros.gpio.pins():
    print(pin)

solaros.gpio.mode(17, solaros.gpio.INPUT, solaros.gpio.PULL_UP)
print("GPIO17", solaros.gpio.read(17))

solaros.gpio.write(1, 1)

solaros.onewire

OneWire functions operate on runtime-safe expansion GPIOs when the OneWire service is included in the active flavor. Use solaros.buses.onewire_* for a registered named bus. Transfers reset the bus before writing and reading, and are limited to 64 bytes in each direction.

Example:

import solaros

for device in solaros.onewire.scan(17):
    print(device["address"], device["family"])

# Skip ROM, issue a command, and read two response bytes.
response = solaros.onewire.xfer(17, 2, b"\xcc\x44")
print(response)

solaros.led

Status LED functions control a built-in board status LED when the board has one.

Example:

import solaros

solaros.led.toggle()

solaros.adc

ADC functions expose analog reads on runtime-safe expansion pins that are ADC capable. Some runtime GPIOs are digital-only; check adc_capable from solaros.adc.pins() before reading.

Example:

import solaros

print(solaros.adc.pins())
print(solaros.adc.read(1))

solaros.pwm

PWM functions expose LEDC PWM output on runtime-safe expansion pins. Active PWM outputs share one LEDC timer, so changing the frequency changes the frequency for all active PWM outputs.

Example:

import solaros

solaros.pwm.set(1, 1000, 50)
print(solaros.pwm.status())
solaros.pwm.off(1)

solaros.buses

The named-bus API discovers board-defined and runtime-created buses. It is available when the resource service is compiled, independently of the legacy single-board-bus solaros.spi module.

Bus dictionaries contain id, name, protocol, origin, sharing, attached, detachable, ready, and lease_count, plus protocol-specific pins and configuration. SPI buses include host, sclk_pin, miso_pin, mosi_pin, max_transfer_size, and cs slot dictionaries. I2C buses include port, sda_pin, scl_pin, and speed_hz. UART buses include port, tx_pin, rx_pin, and baud_rate.

Named I2C operations are present when both the resource and I2C services are compiled. They take and release a shared bus lease automatically. The legacy solaros.i2c module remains an i2c0 shortcut.

Named OneWire operations are present when both the resource and OneWire services are compiled. They take and release an exclusive bus lease automatically. OneWire bus dictionaries include pin; the legacy solaros.onewire module continues to accept a direct runtime-safe GPIO.

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

  1. create_onewire requires pin. Both validate the board runtime pin
  2. policy and claim their signal pins until remove(name).

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

  1. Named UART reads and writes take an exclusive lease automatically.
  2. Runtime descriptors are detachable and removable. Board descriptors 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 for the first lease.

create_spi accepts a configuration dictionary with required host, sclk, mosi, and cs fields. cs is a list of one to four chip-select GPIOs. Optional fields are miso (None for transmit-only) and max_transfer_size (default 4096 bytes). The board validates the selected host and all signal pins. Raw named-bus transfers take and release a temporary bus lease automatically.

Example for a runtime-routed Waveshare SPI bus:

import solaros

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

reply = solaros.buses.spi_xfer("spi1", "gpio17", b"\x9f\x00\x00\x00")
print(reply)

solaros.buses.remove("spi1")

Runtime I2C and 1-Wire examples:

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"])

onewire0 = solaros.buses.create_onewire("onewire0", {"pin": 16})
print(solaros.buses.onewire_scan(onewire0["name"]))
solaros.buses.remove(onewire0["name"])

uart1 = solaros.buses.create_uart("uart1", {
    "port": 1,
    "tx": 14,
    "rx": 15,
    "baud_rate": 115200,
})
solaros.buses.uart_write(uart1["name"], b"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"])

Named I2C example:

import solaros

print(solaros.buses.get("i2c0"))
print([hex(addr) for addr in solaros.buses.i2c_scan("i2c0")])
solaros.buses.i2c_probe("i2c0", 0x3c)

Named OneWire example for a board-defined bus:

import solaros

print(solaros.buses.get("onewire0"))
print(solaros.buses.onewire_reset("onewire0"))
for device in solaros.buses.onewire_scan("onewire0"):
    print(device["address"], device["family"])

solaros.expansion

The expansion API mirrors the expansion shell lifecycle when the expansion service is compiled.

Binding dictionaries accept spi, cs (or ce), i2c, addr, uart, gpio, irq, reset (or rst), data, dc, busy, adc, pwm, and count. cs requires spi, and addr requires i2c. Unknown keys are rejected.

import solaros

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

solaros.neopixel

Available when the NeoPixel expansion package is compiled.

import solaros

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.i2c

I2C functions expose i2c0 for diagnostics and compatibility. Use solaros.buses.i2c_* to select a named bus.

Example:

import solaros

print(solaros.i2c.info())
print([hex(addr) for addr in solaros.i2c.scan()])

solaros.spi

Available when the board and flavor include the SPI service. This compatibility module selects spi0 when present, otherwise the first registered named SPI bus. On a dynamic-only board, status()["available"] remains False until a bus is created. Chip select may be a configured CS name from status()["cs"] or its configured numeric GPIO. Transfers are limited to the selected bus's reported max_transfer_size; new code should address buses explicitly through solaros.buses.spi_*.

Example:

import solaros

status = solaros.spi.status()
cs = status["cs"][0]["name"]

# JEDEC ID command followed by three dummy bytes in one CS transaction.
response = solaros.spi.xfer(cs, b"\x9f\x00\x00\x00", solaros.spi.MODE0, 1_000_000)
print(response[1:])

solaros.uart

UART functions expose the default uart0 compatibility service. Use solaros.buses.uart_* to address another named UART bus.

Example:

import solaros

solaros.uart.baud(115200)
solaros.uart.mode("raw")
solaros.uart.write(b"AT\r\n")
print(solaros.uart.read(64, 500))

solaros.audio

Available when the firmware includes the audio service.

Audio functions expose the microphone, speaker, and WAV service.

Example:

import solaros

print(solaros.audio.status())
solaros.audio.tone(880, 200, 40)
sound = solaros.audio.tone_async(1175, 70)
print(solaros.audio.queue_status())
print(solaros.audio.level(500))

solaros.ble

BLE functions expose keyboard pairing and layout controls.

Example:

import solaros

print(solaros.ble.status())
print("layout", solaros.ble.layout())

solaros.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, USB remains a composite device: the existing cdc0 serial interface is accompanied by standard keyboard, mouse, and gamepad HID reports. The API is typed; scripts cannot replace descriptors or send arbitrary report bytes.

from solaros import 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()

Calls raise OSError("ESP_ERR_INVALID_STATE") while USB is disconnected or HID is unavailable. SolarOS emits neutral keyboard, mouse, and gamepad reports when the Python runtime exits, is interrupted, or is force-stopped.

solaros.clipboard

The clipboard is PSRAM-backed and shared with SolarOS apps that use the clipboard service.

Example:

import solaros

solaros.clipboard.set(b"hello from python")
print(solaros.clipboard.get())

solaros.identity

Identity functions read the SolarOS user and hostname service.

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

Example:

import solaros

print(solaros.identity.format())

solaros.net

Example:

import solaros

print(solaros.net.ping("example-host", 4))

solaros.ssh_keys

SSH key functions manage the default SolarOS SSH key pair.

Example:

import solaros

if not solaros.ssh_keys.default_exists():
    solaros.ssh_keys.generate()

print(solaros.ssh_keys.status())
print(solaros.ssh_keys.public_key())

solaros.jobs

Job functions control SolarOS background jobs.

Status dictionaries include tick_interval_ms, tick_deadline_ms, tick_last_us, tick_max_us, and tick_deadline_misses in addition to the job state and tick count. worker_stack_bytes is the declared launch-admission requirement and worker_stack_external identifies its memory region. These fields expose the effective cooperative scheduling policy, memory admission, and measured handler execution time.

Example:

import solaros

solaros.jobs.start("ntp-sync", ["60", "pool.ntp.org"])
print(solaros.jobs.status("ntp-sync"))
solaros.jobs.stop("ntp-sync")

solaros.sessions

Session functions create and close foreground shell/app sessions.

Manual port shell sessions created from scripts do not run /.shell/startup.

Example:

import solaros

try:
    solaros.jobs.stop("slip")
except OSError:
    pass

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"])

solaros.apps

Application functions inspect the built-in foreground app registry.

Example:

import solaros

for app in solaros.apps.list():
    print(app["name"], "-", app["summary"])

solaros.tui

TUI functions provide a small curses-like text UI layer over the SolarOS terminal. Drawing calls are queued onto the foreground UI side, so Python scripts do not write terminal memory directly.

Attributes:

Functions:

Common key constants include KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_HOME, KEY_END, KEY_DELETE, KEY_ESCAPE, KEY_PAGE_UP, and KEY_PAGE_DOWN.

Example:

import solaros
from solaros import tui

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

while not solaros.should_exit():
    key = tui.getch(250)
    if key == tui.KEY_ESCAPE:
        break

solaros.gfx

Graphics functions provide queued access to the SolarOS foreground graphics service. Call begin() before drawing and refresh()/present() to push the frame to the display. With no argument, begin() uses the display framebuffer of the shell that launched the script. A port or headless shell has no such framebuffer, so targetless begin() raises RuntimeError instead of silently drawing nowhere. begin(target) claims a verified named display target, such as one returned by solaros.expansion.devices(), until end() or script cleanup.

Colors:

gray(level) returns an encoded grayscale color. Level 0 is black and GRAY_MAX is white. Intermediate levels are rendered with ordered spatial dithering on the reflective 1-bit framebuffer.

Fonts:

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.

Example:

import solaros
from solaros import gfx

gfx.begin()
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 Graphics")
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():
    key = gfx.getch(250)
    if key == gfx.KEY_ESCAPE:
        break

gfx.end()

For an attached auxiliary display, first verify its ready target name with solaros.expansion.devices(), then pass that name:

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

Longer Example: Status Snapshot

import solaros

solaros.write("SolarOS {}\n".format(solaros.version()))
solaros.write("{}\n".format(solaros.identity.format()))
solaros.write("uptime {}\n".format(solaros.time.uptime()))

battery = solaros.battery.status()
solaros.write("battery {}% {} mV\n".format(battery["percent"], battery["voltage_mv"]))

env = solaros.sensors.environment()
solaros.write("env {:.1f} C {:.1f}%\n".format(env["temperature_c"], env["humidity_percent"]))

wifi = solaros.wifi.status()
solaros.write("wifi {} {}\n".format(wifi["state"], wifi["ip"]))

Not Exposed Yet

The Python bridge intentionally does not expose raw SSH/SCP session handles yet. Those APIs need object lifetime, ownership, and event-loop rules before they can safely become scriptable.

Quick reference

Import solaros and use its service tables for storage, time, networking, hardware, jobs, sessions, TUI, and graphics. APIs return None or raise OSError as documented. Long-running programs must yield cooperatively and release opened buses, graphics targets, and other resources in finally.