SolarOS

SolarOS 4.13.4 documentation

User manual.

Browse the same 10-section, 228-topic structure used by the embedded help application. These canonical Markdown topics also power man and the native agent reference tool.

Getting started3 topics
Shell and storage2 topics
Commands101 topics
adc commandShow ADC service status.agent commandOpen a new native LLM agent TUI or make one unsaved foreground request.apps commandList registered foreground apps compiled into the firmware.audio commandShow audio state, global speaker level, tone queue, and active synth telemetry.battery commandShow voltage, state of charge, power source, config, and monitor trend.ble commandShow BLE keyboard state and the current/next boot setting.board commandPrint board ID, name, and capabilities.cat commandPrint a small text file.cd commandChange current shell directory.charger commandList registered battery chargers, concrete drivers, and valid configuration ranges.clear commandClear the active shell terminal.close commandClose a display app, display shell, or retained port app, or stop a port shell session. The final interactive shell cannot be closed.commands commandList built-in shell commands.contacts commandOpen the searchable provider-neutral contact browser.control commandInspect normalized controls, native app parameters, or target bindings.cp commandCopy a file or matched set.daq commandPrint DAQ usage.date commandShow or set the local date.df commandShow free space on mounted storage volumes.disk commandShow persistent-storage status.display commandList drawable display targets, draw a test pattern, or change driver-specific display settings.dpad commandShow ADC D-pad pins, raw values, zones, and calibration thresholds.echo commandPrint the arguments separated by spaces, followed by a newline. Quotes preserve spaces and are not printed.email commandOpen the receive-only email app.engine commandPrint or reset generic engine utilization counters for CPU/SIMD-style backends and vector bulk operations.espnow commandShow ESP-NOW owner, channel, PHY, peers, traffic, drops, conflicts, and last error.exit commandClose the current UART, USB CDC, or telnet shell when another interactive shell remains.expansion commandOpen the expansion device manager. Browse attached devices and driver categories, inspect details, attach supported drivers, save runtime attachments to the selected startup script, and detach runtime devices. Bus lifecycle remains in the io app.fg commandResume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port shell's most recently suspended app.gateway commandShow gateway configuration, connection state, and traffic counters.gesture commandList gesture-capable input sources, readiness, and the gesture kinds each source advertises.gnss commandList registered GNSS receivers and their concrete drivers.gpio commandList board GPIOs with free, releasable, or fixed pin policy.haptic commandList registered haptic devices, their concrete drivers, and supported effect range.help commandBrowse the package-aware manual or manage its signed exact-version SD copy. command.status escapes the maintenance keyword.humidity commandList humidity providers or read the default or named sensor.i2c commandShow every named I2C bus, or one selected bus.identity commandShow the configured user and hostname.imu commandList registered motion sensors, their concrete drivers, and available measurements.inbox commandOpen the universal incoming-message browser.input commandList all input sources or filter them by semantic class.job commandShow one job or all jobs.jobs commandList registered jobs and their state.led commandInspect or control the built-in status LED when available.link commandList active SolarOS Link instances and their queue/protocol counters.log commandShow runtime log ring status.ls commandList files. Hidden files are shown only with -a; sizes are human-readable with -h.man commandRead or search the package-aware SolarOS manual.mem commandPrint heap status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.meshcore commandShow MeshCore identity, radio, packet, delivery, duplicate, memory, and stack state.messages commandShow bounded-store, persistence, drop, and live provider state.midi commandShow MIDI worker, traffic, parser, and queue status.mkdir commandCreate directories.modem commandOpen the modem status and settings TUI.mqtt commandShow broker, authentication, connection, traffic, queue, and error status without revealing the password.mv commandRename or move a file or matched set.neopixel commandList attached WS2812/NeoPixel strips.netscan commandScan TCP ports on one host or a capped IPv4 range.network commandOpen the two-tab network TUI. Status shows interfaces and routes; Settings changes persistent interface priority and client routing.nfc commandList registered NFC readers and their concrete drivers.ntp commandSync the wall clock from NTP.nvs commandShow the default NVS partition size, entry usage, and namespace count.onewire commandShow every registered named 1-Wire bus, or one selected bus.osc commandInspect named outbound OSC bindings and their live source, value, send, and error state.ota commandShow running and configured OTA state.outbox commandList pending outbound messages. Sent and failed messages remain in conversation history, not Outbox.ping commandSend ICMP echo requests. Without count, ping runs until Esc, Ctrl+C, or app-exit.pkg commandPrint compiled package groups and build units.pocsag commandShow POCSAG receiver configuration, counters, correction statistics, and RSSI.port commandList byte-stream ports.power commandShow the selected and effective profiles, suspend state, sleep policy, and wake statistics.pwm commandShow PWM state.radio commandOpen the packet-radio TUI with live status and editable common config.ramfs commandList PSRAM-backed volatile filesystem mounts.reboot commandRestart the board.rm commandRemove files. -f allows directories; -rf removes directories recursively.rtc commandShow the RTC provider, capabilities, interrupt wiring, and alarm/timer owners. Reports unavailable when no RTC is present.schedule commandList persistent alarms and scheduled shell scripts. Entries show their enabled state, trigger, and action.session commandList display sessions, port shells, and retained port-owned application sessions with their owner.sessions commandList display app sessions, display shell sessions, and port shell sessions.setterm commandOpen the terminal settings TUI from the display shell.sh commandRun a simple SolarOS shell script from storage.sleep commandEnter explicit light sleep.spi commandShow every named SPI bus, or one selected bus.sshkey commandShow default SSH key status.status commandPrint a compact system summary, including the last foreground-app exit code.stream commandList dynamic typed stream endpoints.suspend commandTurn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY to resume.temperature commandList temperature providers or read the default or named sensor.time commandShow or set the local time.top commandPrint FreeRTOS task resource information when available.uart commandShow the default uart0 or a selected named UART bus.unzip commandList or extract a ZIP archive.uptime commandPrint elapsed time since boot.version commandPrint the SolarOS version and firmware flavor.wait commandPause the calling shell or shell script for 0 through 86400 seconds.watch commandRepeat another shell command until Esc, q, or the app-exit key is pressed.wifi commandOpen the Wi-Fi display TUI when launched from the display shell.wireguard commandShow profile, tunnel, selected underlay, route, peer, DNS, and kill-switch state without printing key material.xfer commandList supported and reserved transfer protocols.zip commandCreate a ZIP archive. -0 stores without compression.
Applications48 topics
agent applicationNative Responses/Chat-Completions LLM client and SolarOS agent control plane. It streams model text directly to the active shell and exposes typed system-status, storage-listing, job-listing, display-discovery, and optional Python/Lua execution tools. agent tools shows risk, policy, and runtime availability.Agent service and tool referenceProvider contract, typed tools, policy, resource bounds, and roadmapaplay applicationPlay audio files through the default registered playback endpoint. WAV and MP3 are supported when an output device is present. MP3 decoding is provided by the shared, device-independent audio codec service. aplay prints the source details, plays the file once through the shared background audio player, and then returns to the prompt without clearing existing terminal output. It can be launched from display, UART, USB CDC, Telnet, and other port shells.Application referenceUsage, controls, and examples for every foreground applicationarecord applicationRecord a registered capture endpoint to a WAV file. The default is the first compatible input; -i selects a specific stream such as adc0.capture. This requires a registered input device, but not built-in board audio. With -d, recording stops after the specified number of seconds. Without -d, recording continues until app-exit, the storage fills, or the WAV size limit is reached. It can be launched from display, UART, USB CDC, Telnet, and other port shells.calc applicationScientific calculator and function plotter. On a graphical display, calc opens an expression list beside a Cartesian plot. From UART, USB CDC, Telnet, or any other text-only shell, the same command opens a scientific REPL without the plot pane. calc --tui forces that REPL even when graphics are available.chat applicationTabbed provider-neutral conversation client. The Channels tab lists gateway and radio conversations. Enter selects a conversation and opens its bounded shared history on the Chat tab, which also contains the message/command input. The app opens and remains useful offline; network or radio transport jobs connect independently.clock applicationFull-screen graphical seven-segment clock, alarm countdown, and stopwatch. The countdown is serviced in the background, so it continues when the Clock session is suspended. It works without RTC hardware while SolarOS remains powered. With a wired RTC interrupt, the RTC alarm or countdown is also armed and explicit light sleep can wake for it.com applicationSerial terminal for a bidirectional byte-stream port. Display-keyboard or port-shell input is forwarded to the selected port, and received bytes are drawn in the active terminal. The port may be a UART or a virtual port such as a peer-bound SolarOS Link stream.contacts applicationProvider-neutral address book for gateway and MeshCore identities. Contacts can carry multiple provider endpoints while retaining trust independently for each endpoint. A signed MeshCore advert creates a discovered endpoint: the signature proves possession of the advertised key, not the human identity behind it.curl applicationHTTP client for quick text downloads and diagnostics. It can print response data to the terminal or save it to a file.edit applicationText editor for files on mounted storage. It supports cursor navigation, selection, clipboard operations, text-size changes, and syntax highlighting for known source files. The editor supports files up to 256 KiB on boards with PSRAM and 32 KiB on boards without PSRAM. Use hexedit for binary files.email applicationReceive-only IMAPS client for the configured mailbox. The app shows the provider-specific message list while every newly synchronized message is also published to the universal inbox and its shared status-bar unread counter.files applicationFile manager inspired by Midnight Commander. Its normal mode provides two panes for copy, move, delete, and launch workflows on mounted storage. Launcher mode provides a minimal single-pane application menu suitable for a startup script.Flash another ESP boardDownload verified SolarOS factory images and program another ESP board over UARTflash applicationDownload verified SolarOS factory artifacts to SD and program another supported ESP board over UART. The browser refreshes the signed catalog on request and shows which board, flavor, and version artifacts are already cached. Its tree starts folded and retains its selection and fold state after operations. Delete removes a selected cached artifact after confirmation. The shell form accepts a named UART plus optional boot and reset GPIO pins.ftp applicationTwo-pane FTP file manager. The left pane is local mounted storage. The right pane is a remote FTP server. The app uses unencrypted IPv4 FTP and passive data connections.funcgen applicationAudio-only function generator built on the shared real-time Synth service. It emits signed 16-bit stereo PCM and can use the default playback device or an explicitly selected runtime playback stream, including an attached LEDC PWM audio expansion.gameboy applicationOriginal Game Boy (DMG) emulator selected by the gameboy group on boards with PSRAM, SD storage, graphics, and a streaming display. Current integrated targets are SolarTerm, Freenove IPS, CL-32, T-LoRa-Pager, T-Deck Plus, ODROID-GO, Freenove PAL, and TTGO VGA32. The application loads a user-supplied ROM into PSRAM and writes battery-backed cartridge RAM beside it as a .sav file. Game Boy Color-only ROMs and ROMs larger than 4 MiB are rejected.help applicationForeground browser for the package-aware SolarOS manual. The foldable tree groups the topics compiled for the current firmware and shows whether it is using the embedded copy or a verified downloaded revision. All groups start folded. The selection, scroll position, and fold state remain unchanged after a topic closes.hexedit applicationTwo-pane binary editor for files on mounted storage. Each row shows a file offset, hexadecimal bytes, and their synchronized printable ASCII view. The number of bytes per row adapts to the terminal width. It uses the same 256 KiB PSRAM and 32 KiB internal-memory limits as edit.inbox applicationUniversal incoming-message browser for pages, chat notifications, mail, and other background producers. It reads the same shared inbox that supplies the status-bar unread count. Messages and read state survive reboot in the bounded /.inbox/messages.bin store; the service retains at most 64 entries and keeps the file below 32 KB even when internal flash is the only storage.invaders applicationGraphical arcade shooter.io applicationInteractive expansion I/O manager. Its default Layout view presents the board's connectors in their physical arrangement, followed by the existing pin, named-bus, and resource-claim views. It uses the same ownership and validation services as the gpio, i2c, spi, uart, midi, onewire, and expansion commands.launcher applicationConfigurable native graphical launcher for display shells. It draws the configured grid, centers one icon in each occupied cell, and shows the selected icon larger with its title centered underneath. Arrow keys move between occupied cells, Enter opens the selection, and a pointer selects and opens items by point and click. Esc or the app-exit key returns to the shell.less applicationTerminal pager for text files. It preserves original text layout and is useful for quick file inspection.logic applicationOn-device logic analyzer waveform viewer. It displays the latest capture made by the shared logic analyzer service or the SUMP job. With pin arguments it makes a new local capture before opening the viewer.lua applicationEmbedded Lua runtime. It can run an interactive REPL or execute .lua scripts from storage. Lua scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.Native SolarOS agentConfigure and use the resumable LLM agent and its typed toolsnotes applicationMarkdown-backed checklist and category manager. It stores unchecked and checked items and supports one level of category folding. A persistent bottom help bar shows the available controls, with status or text input directly above it.player applicationInteractive WAV/MP3 player and the user-facing counterpart to aplay. player keeps a persistent playlist under .player on the current storage root. Opening an audio file from Files adds it to that playlist, selects it, and starts playback. Missing files remain listed so removable media can be reattached.PlaygroundBrowse, install, uninstall, and run community Python and Lua applicationsplayground applicationBrowse the configured community catalog as a foldable category tree, search applications, and install, update, uninstall, or run Python and Lua scripts.plot applicationGraphical plotter for DAQ CSV files and live scalar streams. It is compatible with CSV generated by the daq job.python applicationEmbedded MicroPython runtime. It can run an interactive REPL, .py scripts, or .mpy files from storage. Python scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.reader applicationGraphical document reader for plain text, Markdown, and EPUB. It remembers reading position and zoom per opened file when storage is available.recorder applicationInteractive GUI/TUI counterpart to arecord. Recorder writes PCM WAV files so the channel count, sample rate, and resolution travel with the recording and the result can be played immediately. It accepts any registered signed-16-bit PCM capture stream. On the Waveshare board it initially selects audio0.capture. Mono/stereo output, 8/16-bit file resolution, and sample rates from 8 kHz through 48 kHz are converted from the selected stream as necessary.scp applicationSCP file transfer over SSH. It supports password or key authentication through the shared SSH transport and host lookup/known-host storage. When user@ is omitted, SCP uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts, preserves an explicit user@ prefix, and appends : after a unique host match.sftpsync applicationIncremental file and directory synchronization over SSH. The SolarOS client uses the server's SFTP subsystem, so it works with a normal SSH server without requiring a matching remote program. It skips regular files whose size and modification time already match. It does not delete destination-only files.sheet applicationCSV viewer for small data tables. It is intended as a companion to daq logs and simple spreadsheet-like inspection.sketch applicationPointer-driven graphical paint application. Its layout follows classic desktop paint programs: Save, Open, Import, and the sidebar controls share one aligned, equal-sized button grid; color and pattern choices are in the bottom bar. Sketch uses a compact four-color canvas and stores finished documents as interoperable indexed-color PNG files. Color TFTs show the native palette; one-bit displays use the existing dithered rendering path.ssh applicationInteractive SSH client. It supports password and key authentication, known hosts, hostname lookup through /.ssh/hosts, UTF-8 text, VT-style controls, and remote full-screen terminal applications. When user@ is omitted, SSH uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts and preserves an explicit user@ prefix.synth applicationOpen the native synthesizer and sound designer:telnet applicationTelnet client for classic TCP terminal sessions. It supports basic Telnet option negotiation, terminal type reporting, window size reporting, and raw mode.view applicationGraphical image viewer. It supports the image formats compiled into the current firmware, including common PNG/JPEG/GIF/WebP paths and automatic animated GIF playback when the media package is enabled. Images are decoded as RGB on a negotiated indexed-color display and as grayscale on a one-bit display. In the default fit mode, JPEG color conversion writes display-sized output directly, so a large source photograph does not require a full-size RGB destination.web applicationSimple graphical web browser for lightweight HTML pages. It shares document and image rendering infrastructure with reader where possible. Embedded and direct PNG, JPEG, GIF, and WebP images retain color on indexed-color displays; one-bit displays keep the grayscale decode and dither path.webradio applicationStream a direct MP3 URL through the default registered audio output. On a graphical display shell, WebRadio opens a two-tab media-player GUI. On UART, USB CDC, Telnet, SSH, and other text shells, it opens a station-list TUI.writer applicationResumable graphical Markdown editor for PSRAM display boards. Inactive blocks are formatted like reader; the block containing the cursor and every block touched by a selection show their exact Markdown source. edit remains the portable text editor for port shells and boards without graphics or PSRAM.
Background jobs28 topics
Background job referenceConfiguration, ownership, and examples for every background jobBackground jobsInspect and control bounded background workersbatmon jobBattery monitor. It periodically samples battery voltage, maintains a smoothed trend, estimates power state, and can request light sleep when the configured minimum voltage is reached.bridge jobBidirectional byte bridge between two byte-stream ports, or between one byte-stream port and an active SolarOS Link instance.chatd jobLocal SolarOS chat gateway server. It is useful for testing the chat app or for small trusted local networks.controls jobContinuous-control mapper. It samples every configured scalar-stream control at 50 Hz, applies smoothing, deadband, calibration, and inversion, then updates changed native parameter and MIDI CC bindings.daq jobData acquisition job. It captures scalar and event streams to timestamped CSV, or one byte or PCM audio source directly to a raw file.displayd jobAuthenticated HTTP display and remote control. It has two modes:email-sync jobReceive-only IMAPS mailbox polling job. It fetches mail into the provider-local email app and publishes each new message to the universal inbox.espnow-link jobESP-NOW adapter for the transport-independent SolarOS Link service.ftpd jobUnencrypted FTP file server for one exported folder. The job supports one client at a time and passive IPv4 data connections.gateway-sync jobBackground synchronizer for the gateway messaging provider. Start and stop it explicitly, using the same lifecycle as email-sync:gesture-listener jobGesture-to-command listener. Rules are configured independently with `gesture bind`, while this job owns observation and dispatch:gpio-keys jobMaps runtime-safe GPIO inputs to SolarOS keyboard presses. The job configures each pin as an input with its internal pull-up enabled, treats a low level as pressed, and applies the same 25 ms debounce used by fixed board buttons. Each debounced transition publishes a generic SolarOS key press or release. Held keys use the system repeat rate configured by setterm keyrate.graffiti jobFull-screen Palm Graffiti handwriting input for boards with absolute touch and PSRAM. The job does not draw an overlay or change the active application.httpd jobStatic HTTP file server for a folder on mounted storage.log jobRuntime SolarOS log follower. It mirrors log entries to a byte-stream port or appends them to a file.meshcore jobNon-forwarding MeshCore companion provider for Contacts and Messages.midi jobBidirectional MIDI transport on an exclusive named MIDI bus. The bus selects an available UART controller internally; users supply only its MIDI name, TX and RX pins, and an optional baud rate.ntp-sync jobNetwork time synchronization job. It updates the SolarOS wall clock from NTP and also updates the hardware RTC when the board provides one.osc jobOSC 1.0 IPv4 UDP adapter for automatic incoming native-parameter writes and explicit named outbound stream, event-stream, or normalized-control bindings.pocsag jobPOCSAG pager receiver job. It configures a registered packet radio for a continuous POCSAG byte stream, frames successive 64-byte batches, filters pages to one receiver identity code (RIC), decodes alphanumeric or numeric payloads, and publishes completed messages to the universal inbox.pppd jobPPP over any bidirectional SolarOS byte-stream port. A physical UART, USB CDC, or a virtual port carried by a packet-radio link can provide the bytes; the PPP negotiation mode and the interface's routing role are configured separately.ps2-keyboard jobReceives keyboard scan-code set 2 from an exclusive named PS/2 bus and publishes press and release transitions through the generic SolarOS input service. This job is a compatibility wrapper around a ps2-keyboard expansion attachment; new configurations can attach the device directly.radio-link jobPacket-radio adapter for the transport-independent SolarOS Link service.slip jobIPv4 SLIP gateway on a byte-stream port. This is intended for retro machines, headless boards, and serial networking experiments. The active interface is named with the slip- prefix plus the selected port, for example slip-uart0. It appears as a downstream NAT link in the Network views; its traffic follows the selected SolarOS route, which can be Wi-Fi, cellular, or WireGuard.sump jobSUMP-compatible logic analyzer server on cdc0. It claims the CDC port and uses the shared logic analyzer service for acquisition. PulseView and sigrok can connect with the OpenBench Logic Sniffer/SUMP serial driver.telnetd jobRemote Telnet shell server. The listener is a background job; each accepted connection is attached to its own normal SolarOS port-shell session.
Networking and security3 topics
Hardware and expansion6 topics
Scripting APIs29 topics
Compatibility I/O modulesUse the legacy single-bus I2C, SPI, UART, and OneWire APIsDigital signal processingPortable fixed-point DSP operations, streaming contexts, and script APIsLua API overviewRuntime basics, conventions, and service API topic indexLua apps, jobs, and identity APIApps, jobs, and identity: identity, jobs, sessions, appsLua audio and control APIAudio and control: audio, synth, dsp, controls, parameters, midi, oscLua bluetooth APIBluetooth: bleLua buses and expansion APIBuses and expansion: buses, expansionLua contacts and messages APIContacts and messages: contacts, messagesLua gpio and peripherals APIGPIO and peripherals: gpio, onewire, led, adc, pwm, i2c, spi, uart, neopixel, battery, charger, sensors, GNSS, haptic, IMU, NFCLua graphics APIDraw through SolarOS displays from LuaLua input and clipboard APIInput and clipboard: input, hid, clipboardLua networking APINetworking: wifi, mqtt, http, net, ftp, sftpsync, ssh_keysLua storage and files APIStorage and files: storageLua text user-interface APIBuild terminal applications from LuaLua time and scheduling APITime and scheduling: time, rtc, scheduleNamed runtime busesCreate and use resource-owned I2C, SPI, UART, MIDI, OneWire, and PS/2 busesPython API overviewRuntime basics, conventions, and service API topic indexPython apps, jobs, and identity APIApps, jobs, and identity: identity, jobs, sessions, appsPython audio and control APIAudio and control: audio, synth, dsp, controls, parameters, midi, oscPython bluetooth APIBluetooth: blePython buses and expansion APIBuses and expansion: buses, expansionPython contacts and messages APIContacts and messages: contacts, messagesPython gpio and peripherals APIGPIO and peripherals: gpio, onewire, led, adc, pwm, i2c, spi, uart, neopixel, battery, charger, sensors, GNSS, haptic, IMU, NFCPython graphics APIDraw through SolarOS displays from MicroPythonPython input and clipboard APIInput and clipboard: input, hid, clipboardPython networking APINetworking: wifi, mqtt, http, net, ftp, sftpsync, ssh_keysPython storage and files APIStorage and files: storagePython text user-interface APIBuild terminal applications from MicroPythonPython time and scheduling APITime and scheduling: time, rtc, schedule
System services6 topics
Boards and firmware2 topics

Getting started · SolarOS manual

Browse and refresh documentation

SolarOS always carries a manual in firmware, so man and the agent reference tool work without a network connection.

Run help to open the foreground documentation browser. Topics are grouped in a tree with all groups initially folded. Use Left and Right or Enter on a group to fold and unfold it, then select a topic and press Enter to read it. The tree retains its selection, scroll position, and folded groups when the topic closes. On a graphic display the topic opens in reader; text shells use less. Both consume the same man:TOPIC source, so TOML frontmatter is never shown as document content.

Refresh from solar-os.eu

On devices with Wi-Fi, PSRAM, and an SD card, the same manual can be refreshed without installing new firmware.

First connect Wi-Fi, inspect the available persistent disks, and mount the default removable volume if necessary:

disk lsblk
disk mount

Then run:

help update

SolarOS requests the documentation published for its exact running firmware version. It verifies the catalog with the OTA public key, downloads the single manual.zip archive authenticated by that catalog, and extracts it into a temporary revision. Every extracted Markdown page is then checked against its signed size and SHA-256 before activation. The command shows width-aware download and extraction progress, including on the narrow display shell at text size 16. An interrupted or invalid download leaves the previous manual active.

help status shows whether the external revision or embedded fallback is in use. After a firmware upgrade, SolarOS keeps using a valid signed manual that was downloaded for the previous version until help update installs the exact current-version manual. help, man, and help status warn that this retained manual may be outdated. help reset stops using the downloaded revision; it does not remove the immutable cached files from the SD card.

The maintenance word status intentionally takes precedence over the bare manual alias. Use the exact topic ID help command.status to open the shell status command page.

Why versions must match

Documentation can affect scripts produced by the agent. A newly downloaded manual is therefore activated only when its catalog names the exact running firmware version. A previously verified manual remains available after an OS upgrade instead of silently falling back to embedded content, but SolarOS marks it as potentially outdated until it is refreshed or reset.

Quick reference

help opens the foldable topic tree; help TOPIC expands the corresponding group and selects that topic initially. Graphic display shells open topics with reader; CDC, UART, Telnet, SSH, and other text shells use less. help status reports the active source, firmware and manual versions, revision, page count, update state, and last error. help update downloads one catalog-authenticated archive, verifies every extracted page, stores the exact-version manual on SD, and activates it only after signature, size, and SHA-256 verification. Use help command.status for the command page rather than the maintenance status. help reset immediately returns man, help, and the agent to the embedded manual. Refreshing requires Wi-Fi, PSRAM, and SD.

Getting started · SolarOS manual

SolarOS manual

SolarOS is a small ESP32 operating environment for pocket terminals, reflective displays, serial consoles, and low-power embedded tools. It is text-first, but not text-only: the core experience is a shell with local storage, sessions, jobs, device services, networking, scripting, and foreground applications.

On display boards SolarOS behaves like a self-contained handheld terminal. On headless boards it runs through UART or USB CDC as a compact networked maintenance node. The primary target is the Waveshare ESP32-S3-RLCD-4.2, but the codebase is built around board capabilities rather than one fixed product shape.

What SolarOS can do

  • Run a local shell with history, aliases, scripts, tab completion, storage,
  • and resumable sessions.

  • Launch foreground applications such as an editor, pager, file manager,
  • reader, image viewer, serial terminal, SSH/Telnet clients, web client, plotter, clock, chat client, games, Python, and Lua.

  • Keep background jobs running for logging, data acquisition, NTP sync, SLIP,
  • HTTP serving, Telnet shell access, serial bridging, battery monitoring, and gateway messaging synchronization.

  • Use Wi-Fi, BLE, USB CDC, UART, SD or flash storage, RTC time, GPIO, ADC, PWM,
  • I2C, SPI, 1-Wire, audio, sensors, and board-specific display hardware through shared SolarOS services.

  • Capture streams to CSV or raw files, transfer files over byte-stream ports,
  • capture GPIO waveforms through SUMP or the on-device logic analyzer, and inspect runtime resource ownership.

  • Carry ordinary SolarOS port shells and serial bridges over peer-bound,
  • retransmitted Link virtual serial ports on packet radios.

  • Build focused or full firmware images through capability-aware package
  • flavors.

The result is a deliberately small runtime for turning inexpensive microcontroller hardware into useful field terminals, diagnostic tools, portable loggers, serial/network bridges, and scripting surfaces.

Runtime model

  • Shells are interactive command surfaces on displays, UART, USB CDC, Telnet,
  • or SSH.

  • Applications are foreground programs. They may be text, graphics,
  • display-only, or port-capable according to their registry flags.

  • Jobs are background workers with explicit resource and memory claims, so
  • ports, files, streams, and listeners have visible owners.

  • Services provide shared storage, terminal, session, port, network, time,
  • sensor, hardware I/O, graphics, scripting, OTA, and power behavior.

  • Board profiles describe capabilities and pins. Runtime code asks for those
  • capabilities instead of assuming a particular display, storage bus, or peripheral layout.

Drivers own hardware detail, services own policy, and applications, jobs, and shell commands use services.

Hardware targets

Built-in targets include:

  • solar_term: SolarTerm pocket terminal built around the Waveshare
  • ESP32-S3-RLCD-4.2 board.

  • freenove_esp32_s3_display_4_0: integrated 480x320 capacitive-touch terminal
  • with speaker, microphone, SD, and battery monitoring.

  • elecrow_crowpanel_esp32_s3_4_2_epaper: 400x300 e-paper HMI with rotary
  • controls and microSD.

  • cl_32: compact 384x168 reflective terminal with an integrated keyboard,
  • microSD, RTC, battery monitor, and PWM audio.

  • t_lora_pager: 480x222 pager with keyboard, rotary input, LoRa, GNSS, NFC,
  • IMU, haptics, microSD, RTC, battery management, and audio.

  • t_deck_plus: 320x240 touch terminal with keyboard, trackball, LoRa, GNSS,
  • microSD, speaker output, and microphone input.

  • odroid_go: classic ESP32 handheld.
  • freenove_esp32_wrover_v3: classic ESP32 headless/PAL-composite target.
  • ttgo_vga32_v14: classic ESP32 desktop terminal with VGA, mono DAC audio,
  • PS/2 keyboard, and microSD.

  • esp32_s3_devkitc1_n16r8: minimal headless ESP32-S3 target.
  • esp32_devkitc_v4_wrover: minimal headless classic ESP32 target with PSRAM.

See man boards for the complete board table, capability flags, pins, build environments, and bring-up checklist. See man packages for firmware flavors and package ownership.

First commands

Once SolarOS boots, these commands give a quick view of the installed system:

help
help
apps
jobs
sessions
board
pkg
wifi
stream list
python
lua

Availability depends on the selected board and firmware flavor. The running device is authoritative: help, apps, jobs, board, and pkg show what was compiled and which hardware is exposed.

Finding documentation

Optional topics appear only when their package is part of the firmware. Open help for the foldable manual tree, or search by task:

man --list
man -k draw a circle
man -k connect wifi
man -k background job memory

Open a result with man TOPIC. Graphic display shells can also open topics from help in reader; text shells use less. Arrow and Page Up/Page Down keys scroll, / searches inside a page, and the app-exit key returns to the shell.

Where to begin

  • Use man scripting before writing a Python or Lua application.
  • Use man python.gfx or man lua.gfx for a graphical application.
  • Use man jobs to understand background workers and their memory.
  • Use man buses and man expansion before connecting external hardware.
  • Use man identity to configure the device user and hostname.
  • Use man help to learn how to refresh the signed manual on a supported device.

Quick reference

Search again with a module or task name. Topics cover gfx, tui, storage, identity, commands, jobs, sessions, applications, boards, packages, Wi-Fi, MQTT, networking, GPIO, ADC, PWM, buses, I2C, SPI, UART, OneWire, expansion, audio, BLE, clipboard, time, battery, sensors, Python, Lua, and SSH keys. Optional modules exist only when their package is installed.

Getting started · SolarOS manual

SolarOS scripting conventions

Python and Lua are the normal way to build custom SolarOS applications. Scripts call native services through the solaros module rather than assuming Unix process, filesystem, or device APIs.

Start with discovery

Inspect the installed board, packages, buses, displays, and safe pins before choosing names or hardware. Optional modules disappear when their package is not compiled.

Cooperate with the foreground session

Interactive code must check solaros.should_exit() and release displays, buses, GPIO claims, files, and interpreter-owned services on every exit path. Use try/finally in Python and pcall plus explicit cleanup in Lua. When a foreground script exits, SolarOS preserves its stdout, errors, and tracebacks in the launching shell. TUI and graphics screen contents are not copied into shell history. The normal terminal scrollback limit still applies. Successful scripts return exit code 0; failures and interruptions return a nonzero code. status shows the most recent foreground-application exit code.

Read foreground input

Use solaros.input.sources() to discover pointer, axis, and gesture sources, then use solaros.input.read(timeout_ms) for touch coordinates, mouse deltas and buttons, joystick axes, or recognized gestures. The input queue belongs to the foreground Python or Lua application. Headless source runners do not receive these events. Keep the timeout bounded so the loop can check solaros.should_exit() regularly.

Keyboard characters and navigation keys use solaros.tui.getch() instead of the pointer, axis, and gesture queue. See the Python or Lua API reference for event fields, constants, queue capacity, and overflow reporting.

Run a saved script

python /app.py argument
lua /app.lua argument

Python arguments are in sys.argv. Lua arguments follow the runtime's standard argument table.

File argument convention

Scripts that accept one primary input file should use the canonical option --file PATH. The option is not mandatory for scripts that do not consume a file. Playground recognizes this exact option and completes filesystem paths for its following argument:

playground run APP-ID --file PATH

Document --file in the application's README when it is supported. The manifest does not need a separate argument declaration.

Quick reference

Python imports the native solaros module; arguments are in sys.argv. Lua uses the preloaded global solaros or require with the module name. Mutating service failures surface as SolarOS errors. Optional modules are package-gated. Interactive code should check solaros.should_exit(). Use SolarOS service APIs instead of assuming Unix process, filesystem, or device APIs. Use --file PATH for a primary input file. Foreground pointer and axis events use solaros.input; keyboard characters use solaros.tui.getch().

Shell and storage · SolarOS manual

SolarOS Shell Commands

This document covers built-in shell commands. Foreground applications are documented separately in apps.md. Command availability depends on the compiled firmware flavor and board capabilities; commands shows the commands in the running image.

Shell Conventions

Paths are resolved relative to the current shell directory. The default storage volume is presented as /. On SD-backed boards, the primary SD card volume also has the internal mount path /sdcard, and the internal flash volume is mounted at /flash. On boards without SD support, internal flash is mounted as /.

Wildcard patterns are supported by selected filesystem commands, for example *.txt.

Tab completion covers commands, subcommands, filesystem paths, job names, port names, and stream IDs where the command exposes enough structure. Completed paths that contain spaces are inserted as quoted shell tokens. For ssh and scp, it also reads host aliases from /.ssh/hosts. An explicit user@ prefix is preserved; a unique SCP host match appends : for the remote path.

Ctrl+V pastes the shared SolarOS clipboard at the command-line cursor. Line breaks and tabs become spaces, and a paste stops at the shell input limit; it never executes a command by itself.

Invalid input is reported as a specific problem followed by only the relevant usage line. Close, unambiguous command and subcommand typos include a did you mean hint; SolarOS never runs the suggested command automatically. Missing, unexpected, and invalid arguments identify the affected argument and its expected form. Passwords, tokens, and other credential values are redacted.

Quotes and backslash escapes are checked before a command runs. Unterminated quotes, a trailing backslash, too many arguments, and unsupported shell operators such as |, >, &&, and ; reject the complete line. URLs and ordinary argument text containing punctuation remain valid.

Shell scripts use the .sh extension. Run one with sh <file> or invoke its path directly; for example, ./somescript.sh is equivalent to sh ./somescript.sh. SolarOS storage does not require an executable permission bit for this shorthand. Use echo for script progress messages and wait to insert a whole-second delay between commands. wait does not put the device into light sleep. A foreground application launch ends the current script; the script does not resume after the application closes.

History is kept in memory and cached at /.shell/history when storage is available. The optional user alias file follows the default storage volume:

/.shell/alias

Playground separately maintains /.shell/playground. Do not edit that file; installing, updating, or uninstalling community applications regenerates it.

The startup script source is selected with setterm startup [auto|flash|sd] and is stored in NVS. auto is the default, including after nvs clear: it uses a mounted board-owned SD card when available and otherwise falls back to internal flash. flash and sd are explicit selections without fallback. On an SD-capable board, the paths are /flash/.shell/startup and /sdcard/.shell/startup; on a board without SD, internal flash is mounted at /, so its path is /.shell/startup. The script runs once per boot on the first startup-enabled shell. Shell sessions created by that script do not run it again.

The device user and hostname are stored in NVS and configured with identity. The user is also the default remote username used by ssh and scp when user@host is not supplied. On the first boot after upgrading, existing /.solar/user and /.solar/hostname values are imported when the corresponding NVS value is not already set.

The display-shell app exit chord is CTRL+ALT+DEL. Port shells use Ctrl+].

Shell Control

CommandUsageDescription
commandscommandsList built-in shell commands.
helphelp [TOPIC]; help command.status; help status; help update; help resetBrowse the package-aware manual or manage its signed exact-version SD copy. command.status escapes the maintenance keyword.
manman TOPIC; man -k QUERY...; man --listRead or search the package-aware SolarOS manual.
clearclearClear the active shell terminal.
echoecho [text...]Print the arguments separated by spaces, followed by a newline. Quotes preserve spaces and are not printed.
waitwait <seconds>Pause the calling shell or shell script for 0 through 86400 seconds.
watchwatch [-n seconds] <command> [args...]Repeat another shell command until Esc, q, or the app-exit key is pressed.
shsh <file>Run a simple SolarOS shell script from storage.
exitexitClose the current UART, USB CDC, or telnet shell when another interactive shell remains.
rebootrebootRestart the board.
nvsnvs statusShow the default NVS partition size, entry usage, and namespace count.
nvsnvs list [namespace]List non-empty namespaces and their entry usage, or list one namespace's keys, types, sizes, and storage cost. Values are never displayed.
nvsnvs erase <namespace> [key]Erase one key or all data in one namespace, then reboot.
nvsnvs backup [file]Back up the complete NVS partition to disk. The default is /.solar/nvs.bin.
nvsnvs restore [file]Validate and restore a complete NVS backup, then reboot. The default is /.solar/nvs.bin.
nvsnvs clearErase all NVS-backed settings and reboot immediately.
sessionssessionsList display app sessions, display shell sessions, and port shell sessions.
fgfg [session-id]Resume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port shell's most recently suspended app.
closeclose <session-id>Close a display app, display shell, or retained port app, or stop a port shell session. The final interactive shell cannot be closed.
inboxinboxOpen the universal incoming-message browser.
inboxinbox statusShow universal incoming-message counts and storage status.
inbox`inbox list [all\unread]`List newest messages first.
inboxinbox read <id>Print one message and mark it read.
inboxinbox delete <id>Delete one message by its decimal ID.
inboxinbox clearRemove every message.
inboxinbox post <source> <message>Post a message from a shell script or for testing.
inbox`inbox notify [on\off\test]`Show, persist, disable, or test the Inbox notification sound. It defaults to on and is unavailable on boards without audio output.
contactscontactsOpen the searchable provider-neutral contact browser.
contactscontacts statusShow contact, endpoint, persistence, PSRAM, and opaque-credential counts.
contacts`contacts list [all\discovered\trusted\blocked]`List contacts, optionally filtered by endpoint trust.
contactscontacts show <contact-id>Show a contact and its bounded provider endpoints.
contactscontacts rename <contact-id> <name>Change the local contact display name.
contactscontacts trust <contact-id> [endpoint-id]Trust one endpoint or every endpoint on a contact.
contactscontacts block <contact-id> [endpoint-id]Block one endpoint or every endpoint on a contact.
contactscontacts remove <contact-id>Remove a contact and all its endpoints.
contactscontacts link <target-contact-id> <source-contact-id>Move the source endpoints into the target and remove the source contact.
messagesmessages statusShow bounded-store, persistence, drop, and live provider state.
messagesmessages conversationsList provider-neutral conversations and unread/security state.
messagesmessages list <conversation-id>List retained messages and their stable hexadecimal IDs.
messagesmessages send <conversation-id> <text> [--allow-untrusted]Queue an outbound message; discovered direct endpoints require the explicit flag.
messagesmessages read <conversation-id>Mark a conversation and its linked Inbox entries read.
messagesmessages delete <message-id>Delete one retained message and its linked Inbox projection by hexadecimal ID.
messages`messages clear <gateway\meshcore\link\all>`Clear retained history and owned Inbox projections for one provider or every messaging provider. Unrelated Inbox sources remain.
messagesmessages outboxList pending outbound requests in queue order.
messagesmessages cancel <message-id>Cancel a queued outbound message by the hexadecimal ID printed by list or send.
outboxoutbox [list]List pending outbound messages. Sent and failed messages remain in conversation history, not Outbox.
outboxoutbox cancel <message-id>Cancel one pending message by hexadecimal ID.
gatewaygateway statusShow gateway configuration, connection state, and traffic counters.
gatewaygateway configure <url> [token]Save gateway connection settings. The gateway uses the global SolarOS user identity.
gatewaygateway connect [url] [token]Enable gateway synchronization, optionally updating settings.
gatewaygateway disconnectDisable gateway synchronization.
gatewaygateway roomsList known and joined gateway rooms.
gateway`gateway join\leave\delete <room>`Queue a gateway-specific room operation.
emailemailOpen the receive-only email app.
emailemail statusShow saved account, local message counts, and last sync error.
emailemail configure <imaps://host[:port]> <user> <password> [mailbox]Save an IMAPS account; the default mailbox is INBOX.
emailemail syncStart a one-shot mailbox synchronization.
emailemail forgetRemove the saved account and local email list.
pocsagpocsag statusShow POCSAG receiver configuration, counters, correction statistics, and RSSI.
pocsag`pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha\numeric] [normal\inverted] [function]`Encode and transmit one POCSAG page.

nvs status distinguishes raw free entries from entries currently available for new data; use the available count when diagnosing a failed NVS write. nvs list does not display values, so credentials and other secrets are not printed. Namespace entry totals include the namespace record itself. Clearing a namespace removes all of its keys, but ESP-IDF retains its one-entry namespace record. nvs erase reboots after a successful change because running services can cache NVS-backed settings. Use nvs backup before erasing unfamiliar namespaces or keys. nvs backup writes a versioned, CRC-protected image of the complete default NVS partition. The file contains unencrypted credentials and settings, so protect it like a password. nvs restore accepts only a complete backup for the current NVS partition address and size, verifies its CRC before changing flash, verifies the written partition again, and reboots. nvs clear erases the complete default NVS partition, including identity, Wi-Fi and BLE state, credentials, service settings, and radio profiles, then reboots. Files on SD or the internal FAT filesystem are not affected.

Sessions are foreground application state plus shell instances attached to a display target or byte-stream port. Background services such as log followers, SLIP, DAQ, and HTTP serving are jobs and are controlled with job.

Scripts are intentionally simple. sh skips blank lines and lines whose first non-space character is #, then executes each remaining line as a normal shell command. Diagnostics produced through the common command parser include the script path and line number. A failed or malformed command does not execute, and the script continues with its next line; exit stops the script and closes its port shell. There are no variables, pipes, redirects, or conditionals yet.

man TOPIC opens one manual entry in the less pager when that app is installed. Use q, Esc, or the app-exit key to return to the shell. man -k searches page names, aliases, summaries, keywords, and API contracts; man --list shows every entry compiled into the current flavor. Optional topics are omitted when their package is absent. The same generated registry supplies the agent's solaros_reference tool, so local help and generated-code guidance do not drift apart.

Bare help opens a foldable topic tree. Graphic display shells read the selected topic in reader; text shells use less, both through the same man:TOPIC source. On builds with Wi-Fi, PSRAM, and SD, help update shows terminal-width-aware progress while downloading one manual.zip published for the exact running firmware version. The catalog signature authenticates the archive hash; after extraction every Markdown page is checked by size and SHA-256 before activation. help reset returns immediately to the embedded manual.

User aliases are stored in /.shell/alias, one per line:

name command-or-app fixed-args...

Arguments typed after the alias are appended. SolarOS reads the user file before the managed /.shell/playground aliases, so a user alias with the same name takes precedence. Native commands and firmware applications always take precedence over both alias files. Tab completion expands the complete fixed alias target. For example, run playground run completes installed application IDs after run .

The inbox is a persistent, producer-neutral message sink. Radio decoders, background chat or mail jobs, and shell scripts publish messages with a source, optional topic/sender/title, priority, timestamp, and body. New messages are unread by default. The status bar shows an envelope and unread count; reading or clearing messages durably updates that count. The newest 64 messages are kept in PSRAM when available and mirrored by a fixed-size ring at /.inbox/messages.bin; its compiled maximum is below 32 KB, so systems using the 64 KB internal flash volume cannot grow the inbox without bound. Replayed mail and chat notifications retain their existing read state. The browser shows newest messages first; opening a message marks the shared entry read.

Email configuration is saved in NVS and deliberately has no compiled remote server or account default. Only imaps:// endpoints are accepted, with TLS certificate validation enabled. Use a provider-specific app password where available. The password is supplied as a shell argument and stored with the device configuration, so treat shell history and physical access to the device as sensitive. email sync performs one synchronization; use the email-sync job for periodic polling.

System And Diagnostics

CommandUsageDescription
versionversionPrint the SolarOS version and firmware flavor.
pkgpkgPrint compiled package groups and build units.
boardboardPrint board ID, name, and capabilities.
identityidentity [status]Show the configured user and hostname.
identityidentity user <name>Save the SolarOS user and default SSH/SCP username in NVS.
identityidentity hostname <name>Save the device hostname in NVS; reboot to update Wi-Fi.
engine`engine [statusreset]`Print or reset generic engine utilization counters for CPU/SIMD-style backends and vector bulk operations.
displaydisplay [list]; display test <target>; display mode <target> [mode]List drawable display targets, draw a test pattern, or change driver-specific display settings.
input`input [statuskeyboardtouchmousejoystickdpadbuttonsgesture]`List all input sources or filter them by semantic class.
inputinput test <source>Show event counters and the last key, pointer, axis, or gesture event accepted from one source.
input`input calibrate <source> [set <min-x> <max-x> <min-y> <max-y> <width> <height>\reset]`Show, save, or reset coordinate calibration for an absolute-pointer source.
input`input emit <key\chord>`Emit a local key tap, such as RIGHT, ALT+RIGHT, ENTER, or one literal character, through the normal input-focus path.
gesturegesture [status]List gesture-capable input sources, readiness, and the gesture kinds each source advertises.
gesture`gesture bind source=<name\*> gesture=<name> [direction=<name\*>] [cooldown=<ms>] -- <command> [args...]`Configure a volatile gesture-to-command rule for the gesture-listener job.
gesturegesture bindingsShow listener state and list volatile gesture bindings, trigger counts, queue drops, cooldowns, and commands.
gesture`gesture unbind <id\all>`Remove one volatile gesture binding or all of them.
statusstatusPrint a compact system summary, including the last foreground-app exit code.
uptimeuptimePrint elapsed time since boot.
memmem [policy]Print heap status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.
toptopPrint FreeRTOS task resource information when available.
sleepsleepEnter explicit light sleep.
suspendsuspendTurn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY to resume.
powerpower [status]Show the selected and effective profiles, suspend state, sleep policy, and wake statistics.
power`power profile [performance\balanced\battery\lowpower]`Show or save the power profile.
power`power idle [off\seconds]`Show or configure the display-shell idle light-sleep timeout.
power`power key [off\sleep\suspend]`Show or configure the dedicated KEY short-press action.
powerpower sleepEnter explicit light sleep from the display shell. Press KEY to wake.
powerpower suspendTurn off the primary display while services and jobs continue. Press KEY to resume.
rtcrtc [status]Show the RTC provider, capabilities, interrupt wiring, and alarm/timer owners. Reports unavailable when no RTC is present.
rtcrtc alarm set HH:MM[:SS] [day=N] [weekday=N]Program the RTC hardware alarm, optionally matching a day or weekday.
rtcrtc alarm clearClear the RTC hardware alarm owned by this command.
rtcrtc timer set <duration> [repeat]Program the RTC countdown timer. Durations use an s, m, h, or d suffix.
rtcrtc timer clearClear the RTC countdown timer owned by this command.
rtcrtc pendingShow pending RTC alarm and timer interrupts.
rtc`rtc ack <alarm\timer\all>`Acknowledge pending RTC interrupts.
scheduleschedule; schedule listList persistent alarms and scheduled shell scripts. Entries show their enabled state, trigger, and action.
scheduleschedule show <name>Show one entry, including its run and skip counters.
schedule`schedule add <name> in <duration> <alarm\run script>`Add a one-shot monotonic schedule. Durations use an s, m, h, or d suffix.
schedule`schedule add <name> every <duration> <alarm\run script>`Add a recurring monotonic interval schedule.
schedule`schedule add <name> at YYYY-MM-DD HH:MM[:SS] <alarm\run script>`Add a one-shot schedule in configured local time. It waits for valid wall-clock time.
schedule`schedule add <name> daily HH:MM[:SS] <alarm\run script>`Add a daily schedule in configured local time.
schedule`schedule add <name> weekly <sun,mon,...> HH:MM[:SS] <alarm\run script>`Add a schedule for the selected local weekdays.
scheduleschedule enable <name>; schedule disable <name>Enable or disable an entry without removing it.
scheduleschedule remove <name>Remove an entry.
scheduleschedule run <name>Run an entry immediately. Only one scheduled shell script can run at a time.
scheduleschedule stop [name]Stop the active ringing alarm, optionally only when its name matches.
settermsettermOpen the terminal settings TUI from the display shell.
setterm`setterm --display <target> [orientation\font\textsize\palette\statusbar] [value]`Show or change the volatile terminal profile of a named display target.
setterm`setterm orientation [0\90\180\270]`Show or set primary-display orientation.
setterm`setterm font [mono\compact]; setterm textsize [10\12\14\16\18\20]`Show or set the terminal font and text size.
setterm`setterm palette [normal\inverted]`Show or set the logical terminal and shared-graphics palette.
settermsetterm foreground [#RRGGBB]; setterm background [#RRGGBB]Show or set the persistent RGB terminal theme colors.
setterm`setterm statusbar [show\hide]`Show or hide the graphical shell status bar.
settermsetterm brightness [0..100]; setterm backlight [0..100]Show or set display brightness or backlight level.
setterm`setterm profile [vt100\ansi\dumb]; setterm charset [utf8\ascii]`Configure escape sequences and TUI glyph output for the current port shell.
setterm`setterm keyboard [us\de]; setterm powerkey [sleep\suspend]`Show or set the keyboard layout and dedicated KEY action.
setterm`setterm keyrate [off\1..60 [delay-ms]]`Show or set the shared keyboard and button repeat policy.
setterm`setterm ble [default\on\off]`Show or set the BLE preference for the next boot.
setterm`setterm timezone [UTC\UTC+/-offset\Europe/Berlin\POSIX-TZ]`Show or set the timezone used for local time.
setterm`setterm startup [auto\flash\sd]`Show or select the volume containing .shell/startup for the next boot.
settermsetterm otaurl [url]Show or set the OTA metadata URL.

Input completion lists every current source after input test, only absolute pointer sources after input calibrate, common named keys after input emit, status after an input class, and set or reset after a calibration source. Gesture completion lists gesture-capable sources after source=, limits gesture= values to the selected source's advertised gestures, and offers all after gesture unbind.

power usage:

power status
power profile [performance|balanced|battery|lowpower]
power idle [off|seconds]
power key [off|sleep|suspend]
power sleep
power suspend

Profiles:

ProfileBehavior
performanceCPU fixed at 240 MHz, no automatic light sleep. This is the default.
balancedCPU fixed at 160 MHz, no automatic light sleep.
batteryCPU fixed at 160 MHz with ESP-IDF automatic light sleep.
lowpowerCPU fixed at 80 MHz with automatic light sleep and display-shell idle sleep after 60 seconds.

Suspend is different from explicit light sleep. It keeps the runtime, radios, background jobs, Inbox notifications, and audio active while the primary display is off. It temporarily uses the lowpower profile and prevents the idle policy from entering explicit light sleep. Another short press of KEY resumes the display and restores the selected profile. power status shows the selected profile, effective profile, and suspend state.

power key retains off for compatibility. sleep uses the existing light sleep path, and suspend toggles the runtime suspend state. The default for a new or cleared NVS configuration is suspend; an existing saved value remains unchanged.

rtc is the low-level hardware interface. rtc status remains useful on boards without RTC hardware and reports unavailable there.

rtc status
rtc alarm set HH:MM[:SS] [day=N] [weekday=N]
rtc alarm clear
rtc timer set <duration> [repeat]
rtc timer clear
rtc pending
rtc ack <alarm|timer|all>

Direct alarm and timer controls are leased. If the scheduler or a script owns the requested hardware slot, the command reports the owner instead of replacing its wake-up configuration.

schedule stores named alarms and script jobs in the internal flash filesystem at .solar/schedule.bin. Updates replace the file atomically. Durations accept s, m, h, or d; dates and times use configured local time.

schedule list
schedule show <name>
schedule add <name> in <duration> <alarm|run script>
schedule add <name> every <duration> <alarm|run script>
schedule add <name> at YYYY-MM-DD HH:MM[:SS] <alarm|run script>
schedule add <name> daily HH:MM[:SS] <alarm|run script>
schedule add <name> weekly <sun,mon,...> HH:MM[:SS] <alarm|run script>
schedule enable <name>
schedule disable <name>
schedule remove <name>
schedule run <name>
schedule stop [name]

Only one scheduled shell script runs at a time. A due script is skipped and its skip counter increases if another scheduled script is still running. Scheduled scripts run without a terminal, and attempts to launch foreground applications are rejected. Calendar schedules wait for valid wall-clock time. Interval schedules continue to work from monotonic uptime without an RTC.

During explicit light sleep, the nearest schedule is armed as an internal timer. When a wired interrupt-capable RTC is available, the scheduler also programs its calendar alarm when wall-clock time is valid and, for a monotonic schedule, its countdown timer. The RTC GPIO is added to the wake sources. This lets countdowns use the RTC even while wall-clock time is invalid. An RTC interrupt can wake light sleep; it cannot turn on a board whose hardware power has been switched off.

setterm usage:

setterm
setterm --display <target> [orientation|font|textsize|palette|statusbar] [value]
setterm orientation [0|90|180|270]
setterm font [mono|compact]
setterm textsize [10|12|14|16|18|20]
setterm palette [normal|inverted]
setterm foreground [#RRGGBB]
setterm background [#RRGGBB]
setterm statusbar [show|hide]
setterm brightness [0..100]
setterm backlight [0..100]
setterm profile [vt100|ansi|dumb]
setterm charset [utf8|ascii]
setterm keyboard [us|de]
setterm powerkey [sleep|suspend]
setterm keyrate [off|1..60 [delay-ms]]
setterm ble [default|on|off]
setterm timezone [UTC|UTC+/-offset|Europe/Berlin|POSIX-TZ]
setterm startup [auto|flash|sd]
setterm otaurl [url]

setterm keyrate configures the shared repeat policy for BLE, PS/2, CardKB, and the CL-32 keyboard, fixed board buttons, gpio-keys, and ADC D-pads. Analog joysticks publish axes and do not generate key events. The value is stored in NVS and is available on builds without BLE.

setterm ble selects the next-boot BLE preference. default clears the saved override and follows the active board profile; on and off remain in effect across firmware updates until changed. The current boot is unchanged.

setterm timezone accepts fixed offsets with the conventional UTC sign: UTC-8 is eight hours behind UTC and UTC+5:30 is five hours and 30 minutes ahead. Fixed offsets do not apply daylight-saving transitions. Other accepted timezone expressions use POSIX TZ syntax and its POSIX sign convention. SolarOS does not include the IANA timezone database; Europe/Berlin is a built-in daylight-saving alias.

setterm powerkey selects the dedicated KEY short-press action. sleep enters explicit light sleep; suspend turns off the display while jobs and services continue. setterm key is accepted as a shorter alias.

setterm startup selects the volume used for .shell/startup on the next boot. The default auto setting prefers a mounted board-owned SD card and falls back to flash when SD is unavailable. flash always selects internal flash, even when an SD card is mounted. sd always selects SD and is rejected on boards without SD support. Use setterm startup without a value to show the selected source and currently resolved path.

setterm profile and setterm charset are runtime-only and apply to the current port shell. From the display shell they print guidance to configure them from a port shell. profile controls terminal escape sequences; charset controls TUI glyph output. The default utf8 mode uses Unicode box drawing. Select ascii for DOS and other legacy serial terminals; TUI borders, blocks, arrows, and punctuation are replaced with readable ASCII characters. Display layout settings (orientation, font, textsize, palette, and statusbar) apply to the current display and its app sessions. Settings on the primary display are persistent; settings on secondary or virtual displays such as web0 are runtime-only. When no saved values exist, the font defaults to compact and the text size defaults to 16. palette exchanges logical black and white in terminal content and in the shared graphics palette; dithered shades are reversed as well. It remains independent of hardware inversion modes exposed by display mode, and does not rewrite an existing framebuffer. On a headless board, a port shell can set or query the persistent palette before an expansion-display session exists; subsequently created terminal and graphic sessions inherit it.

setterm --display <target> reads or changes the volatile terminal profile of a named runtime display target, even when a TUI application rather than a shell owns that display. The target profile is initialized from the single global NVS parameter set when the display registers. Orientation is relative to the target's native panel rotation, so 0 keeps every display in its normal mounting even when their drivers use different U8g2 rotations. Display-targeted absolute pointer coordinates follow this logical orientation. A targeted change applies to current and future sessions on that display until reboot or until the display target is unregistered; it does not create or update per-display NVS keys. Without a setting, the command prints the target's complete volatile profile. For example:

setterm --display oled0 statusbar hide
setterm --display oled0 textsize 10
setterm --display oled0 palette inverted

foreground and background select the persistent RGB theme colors for the built-in color display. They color terminal scanout and semantic GUI elements, including text, backgrounds, borders, and intermediate shades. Explicit RGB image, canvas, and script colors remain literal. Use six hexadecimal digits, for example setterm foreground '#d8e8ff' and setterm background '#102030'; the leading # can be omitted. The defaults are #000000 and #ffffff. These settings do not add a color framebuffer or affect monochrome-display rendering. palette inverted continues to exchange the foreground and background roles.

setterm statusbar hide removes the top status bar from graphical shell sessions and gives its space to the terminal. show restores it. The default is show when no value is stored in NVS.

On BLE-capable builds, the status bar has a separate Bluetooth symbol: plain means enabled for the current boot, slashed means disabled, and a small search marker means scanning. The keyboard symbol shows only availability: plain when at least one keyboard is available, slashed otherwise, regardless of transport. Compact displays omit icons that do not fit before the clock and unread count.

Apps And Jobs

CommandUsageDescription
appsappsList registered foreground apps compiled into the firmware.
agentagent; agent new; agent ask PROMPT...Open a new native LLM agent TUI or make one unsaved foreground request.
agentagent list; agent resume SLOT; agent delete SLOTList, restore, or delete durable local conversation slots.
agentagent status; agent toolsInspect provider state, request statistics, typed tools, risk, and policy.
agent`agent config endpointmodelkeyreasoningtoolsmax-tools VALUE`Configure the provider and tool policy.
agent`agent script python\lua (-c SOURCE \FILE) [ARGS...]`Run a bounded script through the agent execution path.
agentagent forgetErase the saved agent configuration.
jobsjobsList registered jobs and their state.
jobjob status [name]Show one job or all jobs.
jobjob start <name> [args...]Start or restart a job.
jobjob stop <name>Stop a job.
sessionsession listList display sessions, port shells, and retained port-owned application sessions with their owner.
session`session create shell <port> [--term autovt100ansidumb] [--charset utf8ascii] [--size COLSxROWS]`Start a shell session on a byte-stream port.
sessionsession create shell <display-target>Attach a shell session to a ready display target such as lcd0.
sessionsession create <app> <display-target> [args...]Start a foreground application as the active session on a named display.
sessionsession focus [display-target]Show or assign the display that receives BLE keyboard and local board-control input.
sessionsession fg [id] or session switch [id]Resume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port's last suspended app.
sessionsession close <id>Close a display app, display shell, port-owned app, or port shell session.
sessionsession send <id> <command> [args...]Run a command on an active display-shell session. The target must be at an empty prompt.
sessionsession backgroundExplain the foreground/background controls.

Port shells default to --term auto. Auto mode sends a terminal Device Attributes probe; a recognizable response enables VT100-style cursor controls and a size probe, while no response falls back to a dumb line-oriented shell. The dumb profile can run line-oriented shell commands, but cursor-addressable TUI applications cannot run on it and report <app>: can't run on a dumb terminal. Use --term vt100 or --term ansi to force escape-sequence output, --term dumb for plain text, and --size COLSxROWS to set the terminal dimensions without probing. Character encoding is independent of that profile: port shells default to --charset utf8; use --charset ascii when a legacy terminal displays Unicode TUI glyphs as unrelated code-page characters.

jobs prints a compact table that fits the built-in display terminal:

NAME         STATE    STACK KIND        EVT  TICKS RES
batmon       running      - background  tick    17   1
log          stopped   6144 background  tick     0   0

Columns:

ColumnMeaning
NAMEJob registry name.
STATEstopped, waiting, running, or failed. A waiting launch retries automatically.
STACKDeclared worker-stack admission requirement in bytes; - means no dedicated stack is declared. Dynamic allocations are not included.
KINDJob kind. Current registry jobs are background workers.
EVTtick if the job receives periodic tick events, otherwise -.
TICKSNumber of dispatched tick events while running.
RESNumber of resources currently recorded for the job.

Use job status <name> for the job summary, owner string, last error, effective worker-stack placement, tick interval/deadline, last and maximum handler time, deadline-miss count, and resource details. Running rows are bold. A waiting job has retained its launch request until the stack can be admitted while preserving the internal-memory reserve; a failed job completed a start attempt with an error. sessions prints the same timing telemetry for display and port sessions as one row per session in ID TITLE APP STATE TIME order. In the TIME column, the first pair is interval/deadline in milliseconds, the second is last/max in microseconds, n is the dispatch count, and ! is the deadline miss count. Job timing detail uses the same values with explicit labels. Job-owned resources use owner strings such as job:log; port conflicts are reported as readable messages such as job log owns cdc0.

Common job examples:

session create shell cdc0
session create shell uart0 --term ansi --charset ascii --size 80x25
session create shell lcd0
session create files display0
session focus display0
job start log cdc0
job start log file /.shell/log info
job start bridge cdc0 uart0
job start bridge uart0 link0 broadcast
link stream create link0 vser0 0x12345678
job start bridge cdc0 vser0
job start gpio-keys gpio17:UP gpio2:ENTER
job start gpio-keys --config /flash/gpio-keys.conf
job start graffiti
job start gesture-listener
job start httpd /www
job start displayd [display-target]   # display0 by default, web0 when headless
job start ntp-sync once
job start batmon 60
job start slip uart0 115200
job start pppd uart0 baud=115200
job stop log

Only one instance of each built-in job name is active at a time. Starting the same job again stops the previous instance and starts it with the new arguments.

Filesystems And Storage

CommandUsageDescription
diskdisk [status]Show persistent-storage status.
diskdisk lsblkList internal flash and detected removable block devices and partitions.
disk`disk mount [flash\sd0pN] [mount]`Mount the default volume or an explicit persistent volume.
disk`disk umount [flash\sd0pN\mount]`Unmount the default volume or an explicit volume/mount point.
disk`disk format <flash\sd0\sd0pN> --force`Create a FAT filesystem, permanently erasing the unmounted target.
ramfsramfs [status]List PSRAM-backed volatile filesystem mounts.
ramfsramfs mount /path sizeMount a volatile filesystem that reserves PSRAM, such as ramfs mount / 1m.
ramfsramfs unmount /pathUnmount a ramfs mount.
dfdfShow free space on mounted storage volumes.
cdcd [path]Change current shell directory.
ls`ls [-a] [-h] [pathpattern]`List files. Hidden files are shown only with -a; sizes are human-readable with -h.
cat`cat <pathpattern>`Print a small text file.
mkdirmkdir <path> [path...]Create directories.
rm`rm [-f-rf] <pathpattern> [pathpattern...]`Remove files. -f allows directories; -rf removes directories recursively.
mv`mv <sourcepattern> <dest>`Rename or move a file or matched set.
cp`cp <sourcepattern> <dest>`Copy a file or matched set.
zip`zip [-0] <archive.zip> <pathpattern> [pathpattern...]`Create a ZIP archive. -0 stores without compression.
unzipunzip [-l] <archive.zip> [dest]List or extract a ZIP archive.

Examples:

ls -ah /.ssh
cp *.txt /backup
rm -rf /tmp/old
disk mount sd0p2 /mnt
ramfs mount /tmp 1m
ramfs mount / 4m
ramfs unmount /tmp
zip /books/archive.zip /books/*.txt
unzip -l /books/archive.zip

Streams, Logs, Ports, And Transfers

CommandUsageDescription
streamstream or stream listList dynamic typed stream endpoints.
streamstream status <id>Show type, direction, provider, format, owner, and counters for one stream.
daqdaq helpPrint DAQ usage.
daqdaq statusShow DAQ job status.
daqdaq streamsList stream IDs.
daq`daq start <file.csv> <stream...> [--rate seconds\--rate-ms ms]; daq start <stream...> <file.csv> [--rate seconds\--rate-ms ms]`Start periodic CSV capture from one or more streams.
daq`daq start <file.csv> <stream> --changes [--append\--replace]`Capture a scalar or event stream only when its value changes.
daqdaq start <file.bin> <byte-or-audio-stream> --raw [--rate-ms ms]Capture one byte or PCM audio stream without CSV framing.
daqdaq stopStop the active data-acquisition job.
loglog statusShow runtime log ring status.
loglog show [count]Print recent SolarOS log entries.
log`log follow [errorwarninfodebug]`Follow logs in the current shell.
loglog clearClear the runtime log ring.
log`log level [errorwarninfodebug]`Show or change runtime log level.
log`log sink cdc [onoff]`Enable or disable CDC mirroring of SolarOS logs.
portport listList byte-stream ports.
portport status <name>Show port capabilities and owner.
xferxfer protocolsList supported and reserved transfer protocols.
xferxfer send <port> <file> --raw [-d ms]Send a file as raw bytes, optionally delaying between chunks.
xfer`xfer recv <port> <file> --raw [--append\--replace] [--idle-ms ms]`Receive raw bytes until the idle timeout and append or replace the destination.
xferxfer send <port> <file> --zmodemSend a file with ZMODEM.
xfer`xfer recv <port> <file> --zmodem [--append\--replace]`Receive a file with ZMODEM and append or replace the destination.

DAQ usage:

daq start <file.csv> <stream...> [--rate seconds|--rate-ms ms]
daq start <stream...> <file.csv> [--rate seconds|--rate-ms ms]
daq start <file.csv> <stream> --changes [--append|--replace]
daq start <file.bin> <byte-or-audio-stream> --raw [--rate-ms ms]
daq stop

DAQ examples:

daq start /logs/env.csv temperature humidity battery --rate 60
daq start /logs/key.csv gpio17 --changes
daq start /logs/uart0.bin uart0 --raw --rate-ms 25
daq start /logs/microphones.pcm audio0.capture --raw

daq CSV rows include uptime_ms, and include UTC time_ms when wall-clock time is trusted. Raw mode accepts one byte or audio source and writes its data directly without CSV framing. Audio uses the native PCM format reported by stream status; arecord writes the same input as a WAV file. recorder adds interactive stream and WAV-format selection, no-file live input monitoring, hardware input gain when the selected device supports it, visualization, pause, and playback.

Streams are runtime-registered endpoints, similar to services and displays. Providers can add or remove scalar sensor, event, byte, and PCM audio streams. Direction is source, sink, or duplex; sharing and current ownership are reported by stream status. Board audio devices currently publish audio0.capture and/or audio0.playback. Microphone level compatibility streams remain available as mic0 and mic1 where the board has two input channels.

Transfer usage:

xfer protocols
xfer send <port> <file> --raw [-d ms]
xfer recv <port> <file> --raw [--append|--replace] [--idle-ms ms]
xfer send <port> <file> --zmodem
xfer recv <port> <file> --zmodem [--append|--replace]

raw and zmodem are supported. kermit is reserved but not implemented.

Networking

CommandUsageDescription
networknetworkOpen the two-tab network TUI. Status shows interfaces and routes; Settings changes persistent interface priority and client routing.
networknetwork statusShow network interfaces, route selection, VPN routes, and router state as text.
networknetwork interfacesList route-capable interfaces, addresses, priorities, and the local downstream interface.
networknetwork routesShow the default route, automatic base path, and active WireGuard routes.
network router`network router [status\on\off]`Route Wi-Fi AP clients through the SolarOS route table using IPv4 forwarding and NAT.
wifiwifiOpen the Wi-Fi display TUI when launched from the display shell.
wifiwifi statusShow station/AP/NAT state and the current/next boot setting.
wifiwifi enableSave Wi-Fi enabled for the next boot. The current boot is unchanged.
wifiwifi disableSave Wi-Fi disabled for the next boot. The current boot is unchanged.
wifiwifi onStart Wi-Fi station mode and connect to remembered networks.
wifiwifi offStop station/AP networking; an active ESP-NOW lease retains the radio.
wifiwifi scanScan access points.
wifiwifi connect [ssid [password]]Connect and save/update a station profile.
wifiwifi disconnectDisconnect station mode.
wifiwifi knownList remembered station profiles.
wifi`wifi forget [ssidall]`Remove one or all remembered station profiles.
wifi repeaterwifi repeaterShow L2 IPv4 repeater state, upstream, downstream, learned clients, and forwarding counters.
wifi repeaterwifi repeater onRepeat the current or preferred saved network with the same SSID and password on the same IPv4 subnet.
wifi repeaterwifi repeater offStop L2 forwarding and the downstream AP while retaining the upstream station.
wifi apwifi ap [status]Show SoftAP status.
wifi ap`wifi ap on [ssid [password [openwpawpa2wpa/wpa2]]]`Start and save SoftAP settings.
wifi apwifi ap offStop SoftAP.
wifi nat`wifi nat [statusonoff]`Advanced control for IPv4 NAT on the SoftAP interface. Prefer network router.
wireguardwireguard [status]Show profile, tunnel, selected underlay, route, peer, DNS, and kill-switch state without printing key material.
wireguardwireguard import <file>Validate one standard WireGuard client profile and save it in NVS. The source file is not removed.
wireguardwireguard forgetLogically remove the saved profile from NVS. Bring the tunnel down first.
wireguard`wireguard up [fail-open\fail-closed]`Request the tunnel and reconnect it when the preferred network underlay changes. The default is fail-closed for a full tunnel and fail-open for a split tunnel.
wireguardwireguard downStop the tunnel, remove its routes, restore DNS, and disable its kill switch.
bleble [status]Show BLE keyboard state and the current/next boot setting.
bleble enableSave BLE enabled for the next boot. The current boot is unchanged.
bleble disableSave BLE disabled for the next boot. The current boot is unchanged.
bleble defaultClear the saved override and use the board default on the next boot.
ble`ble keepalive [on\off]`Show or save the best-effort BLE keyboard keepalive setting.
bleble scanScan nearby BLE devices.
bleble pairStart keyboard pairing.
bleble forgetErase the remembered keyboard, its BLE bond, and its cached GATT service database.
ble gattble gatt statusShow the generic GATT connection state and discovered-service count.
ble gatt`ble gatt connect <aa:bb:cc:dd:ee:ff> <public\random\rpa_public\rpa_random>`Connect to a BLE peripheral by address and address type.
ble gattble gatt disconnectRequest disconnect of the shell's GATT session and cancel its pending operation.
ble gattble gatt servicesList discovered services and their indexes and handle ranges.
ble gattble gatt chars <service-index>List the characteristics discovered for one service.
ble gattble gatt read <handle>Read a characteristic or descriptor by handle.
ble gattble gatt write <handle> <hex...>Write hexadecimal bytes and request a response.
ble gattble gatt write-nr <handle> <hex...>Write hexadecimal bytes without requesting a response.
mqttmqtt statusShow broker, authentication, connection, traffic, queue, and error status without revealing the password.
mqttmqtt connect [mqtt[s]://host[:port] [username [password]]]Connect to a broker and save supplied connection settings; omit them to reuse saved settings.
mqttmqtt disconnectDisconnect and stop the MQTT client.
mqttmqtt publish <topic> <payload> [qos] [retain]Publish a message with optional QoS 0–2 and retain flag.
mqttmqtt subscribe <topic> [qos]Subscribe and print received messages until app-exit or q.
pingping <host> [count]Send ICMP echo requests. Without count, ping runs until Esc, Ctrl+C, or app-exit.
netscan`netscan <hostrange> [ports]`Scan TCP ports on one host or a capped IPv4 range.
ntpntp [server]Sync the wall clock from NTP.

Wi-Fi is enabled by default when no saved setting exists, including after nvs clear. wifi on and wifi off control the radio in the current boot. The wifi enable and wifi disable settings take effect only after a reboot. Disabling Wi-Fi does not erase saved station, access-point, or NAT settings.

WireGuard accepts one IPv4 interface address, one peer, one optional numeric IPv4 DNS server, and at most eight IPv4 AllowedIPs prefixes. IPv6 addresses, multiple peers, interface hooks, and configuration keys outside the documented client subset are rejected. Use wireguard down before importing a replacement profile or using wireguard forget. See WireGuard VPN client for routing, secret, and disconnect behavior.

When no saved preference exists, including after nvs clear, BLE follows the board default. Most boards enable it; TTGO VGA32 v1.4 disables it to preserve internal heap. setterm ble on|off|default stores or clears the preference for the next boot. ble enable, ble disable, and ble default are equivalent compatibility commands. Disabling BLE does not forget the remembered keyboard or erase its BLE bond. On a BLE-disabled boot, SolarOS returns the unused Bluetooth controller and host memory to the internal heap before normal service initialization.

When a keyboard exposes the standard Battery Service, SolarOS subscribes to its Battery Level notifications and reads the initial level when supported. ble status, the general status command, and the Python/Lua BLE status strings include the latest percentage while that keyboard remains connected.

ble keepalive on sends the standard HID Exit Suspend command every 30 seconds when the connected keyboard exposes a writable HID Control Point. It falls back to reading the HID Information characteristic when Exit Suspend is unavailable. The setting persists and is off by default. ble keepalive reports the selected method, the last local operation result, and the attempt count. These operations do not count as SolarOS user activity. They can prevent sleep when a keyboard bases its inactivity timer on host HID or GATT traffic, but the keyboard remains in control of its own power policy, so the result is device-specific. ble keepalive off stops the operations.

BLE GATT usage:

ble gatt status
ble gatt connect <aa:bb:cc:dd:ee:ff> <public|random|rpa_public|rpa_random>
ble gatt disconnect
ble gatt services
ble gatt chars <service-index>
ble gatt read <handle>
ble gatt write <handle> <hex...>
ble gatt write-nr <handle> <hex...>

The shell GATT session owns one peer and shares the configured connection budget with app BLE sessions, which may own multiple peers. It cannot read or disconnect an app-owned connection. Disconnect and operation timeout retire the connection asynchronously; reconnect can report busy until cleanup finishes. A write without response waits for local stack completion, not a remote acknowledgement. Generic scans are unavailable while any generic peer is active or retiring.

MQTT usage:

mqtt status
mqtt connect [url [username [password]]]
mqtt disconnect
mqtt publish <topic> <payload> [qos] [retain]
mqtt subscribe <topic> [qos]

mqtt connect mqtt://host[:port] [username [password]] and mqtt connect mqtts://host[:port] [username [password]] save broker settings in NVS. Later mqtt connect reuses the saved settings.

netscan accepts a host, same-subnet range, or compact range form. Examples:

netscan 192.168.1.10 22,80,443
netscan 192.168.1.1-50 22
netscan wintermute 22

OTA And Keys

CommandUsageDescription
otaota statusShow running and configured OTA state.
otaota checkCheck signed board/flavor release metadata.
otaota upgradeDownload firmware into the inactive OTA partition and reboot into it.
otaota url [url]Show or set the OTA base URL.
otaota flavor [flavor]Show or set target OTA flavor.
ota`ota boot 01`Select an OTA slot and reboot.
sshkeysshkey [status]Show default SSH key status.
sshkey`sshkey gen [-f] [204830724096]`Generate /.ssh/id_rsa and /.ssh/id_rsa.pub.
sshkeysshkey pubPrint the default public key.
sshkeysshkey rmRemove the default key pair.

OTA resolves the artifact for the compiled board and target flavor from the configured release index, verifies the signed index, verifies firmware SHA-256, and writes the inactive ESP-IDF OTA partition. A signed ota check also caches the flavors present for the compiled board in RAM. ota flavor completion uses that verified list. Before a check, completion asks you to run ota check; changing the OTA URL or rebooting clears the list. If the selected flavor is not present in the checked release, ota check lists the flavors that are available for the compiled board.

Hardware And Time

CommandUsageDescription
batterybattery [status]Show voltage, state of charge, power source, config, and monitor trend.
batterybattery configShow battery capacity and voltage thresholds.
batterybattery capacity [mAh]Show or set capacity estimate.
battery`battery min_voltage [VmV]`Show or set low-voltage threshold.
battery`battery max_voltage [VmV]`Show or set full/external-power shortcut threshold.
audioaudio statusShow audio state, global speaker level, tone queue, and active synth telemetry.
audioaudio devicesList registered audio devices and their capture/playback streams.
audioaudio device <id>Show one audio device and its native PCM format.
audio`audio default [auto\<id>]`Show or select the preferred playback device. auto restores first-compatible-device selection.
audioaudio tone [hz] [ms] [volume]Play a diagnostic tone.
audioaudio tone-async [hz] [ms] [volume]Queue a tone and return its request ID immediately.
audioaudio queueShow asynchronous tone queue state and counters.
audioaudio cancel <request-id>Cancel a queued or playing asynchronous tone request.
audioaudio level [volume]Show or set global speaker level.
audioaudio mic [ms]Sample microphone level.
audioaudio loopback [ms] [volume]Run microphone-to-speaker loopback.
audioaudio offStop audio output.
led`led [statusonofftoggle]`Inspect or control the built-in status LED when available.
expansionexpansionOpen the expansion device manager. Browse attached devices and driver categories, inspect details, attach supported drivers, save runtime attachments to the selected startup script, and detach runtime devices. Bus lifecycle remains in the io app.
expansionexpansion statusShow expansion capabilities, named buses and leases, connector resources, active devices, and resource claims.
expansionexpansion layout [connector]Draw the board's physical connector map with live free, releasable, claimed, fixed, power, ground, and NC markers.
expansionexpansion scanList expansion resources and probe-capable drivers.
expansionexpansion driversList compiled expansion drivers.
expansionexpansion devicesList fixed board and runtime-attached expansion devices with origin, readiness, startup mode, policy, and bindings.
expansion`expansion bus create i2c <name> port=<i2c0\i2c1> sda=<gpio> scl=<gpio> [speed=<hz>]`Define a runtime I2C bus on an unused controller and approved expansion pins.
expansionexpansion bus create onewire <name> pin=<gpio>Define a runtime named 1-Wire bus on an approved expansion pin.
expansionexpansion bus create ps2 <name> clock=<gpio> data=<gpio>Define an exclusive PS/2 bus on two approved expansion pins.
expansionexpansion bus create midi <name> tx=<gpio> rx=<gpio> [baud=<rate>]Define an exclusive MIDI bus; SolarOS selects the UART backend and defaults to 31250 baud.
expansion`expansion bus create spi <name> host=<spi2\spi3> sclk=<gpio> mosi=<gpio> [miso=<gpio\none>] cs=<gpio> [cs=<gpio> ...] [max=<bytes>]`Define a runtime-routed SPI bus on a board-approved host and expansion pins.
expansion`expansion bus create uart <name> port=<uart1\uart2> tx=<gpio> rx=<gpio> [baud=<rate>]`Define a lazy runtime UART on an unused controller and approved expansion pins.
expansionexpansion bus attach <name>Attach a named detachable bus and reserve its endpoint and signal pins.
expansionexpansion bus detach <name>Detach an idle named bus, preserving its descriptor while releasing its endpoint and signal pins.
expansionexpansion bus remove <name>Remove an idle runtime bus and release its signal pins.
expansionexpansion attach <driver> <name> <resource...>Attach a compiled expansion driver or manual resource profile.
expansionexpansion detach <name>Detach an active expansion device and release its resource claims.
expansionexpansion export <path>Atomically export runtime buses and catalog-backed device attachments as a portable expansion manifest for custom-board generation.
neopixel`neopixel [status\list] [name]`List attached WS2812/NeoPixel strips.
neopixelneopixel set <name> <index> <red> <green> <blue>Set one zero-based pixel and immediately refresh the strip.
neopixelneopixel fill <name> <red> <green> <blue>Fill and immediately refresh the strip. Color components are 0..255.
neopixel`neopixel clear\show <name>`Clear a strip immediately, or transmit its buffered colors.
midimidi statusShow MIDI worker, traffic, parser, and queue status.
midimidi monitorPrint incoming CC and key messages until the app-exit key, Esc, or q is pressed.
midi`midi note-on\note-off <channel> <note> [velocity]`Queue a MIDI note message for transmission.
midimidi cc <channel> <controller> <value>Queue a MIDI control-change message.
midimidi program <channel> <program>Queue a MIDI program-change message.
midimidi send <status> [data1] [data2]Queue one validated raw MIDI message.
midimidi stream listList configured incoming MIDI CC scalar streams and their latest values.
midi`midi stream add\remove <channel> <controller>`Register or remove midi.cc.<channel>.<controller> as a scalar stream.
midimidi stream clearRemove all configured MIDI CC scalar streams.
control`control list\parameters\bindings`Inspect normalized controls, native app parameters, or target bindings.
controlcontrol create <name> <stream> <min> <max> [smooth=ms] [deadband=value] [invert]Normalize a scalar stream as a named continuous control; use manual for script-supplied values.
control`control bind <name> parameter <path> [pickup=on\off]`Bind a control to a typed native-app parameter with optional soft takeover.
controlcontrol bind <name> midi <channel> <cc>Bind a control to a MIDI Control Change target.
control`control get\set <name> [value]`Read or set a normalized 0..65535 control value.
control`control parameter get\set <path> [value]`Read or set an available native parameter in its declared unit.
controlcontrol unbind <name>Remove all target bindings owned by one named control.
controlcontrol delete <name> or control clearRemove one control and its bindings, or remove all controls and bindings.
oscosc bindingsInspect named outbound OSC bindings and their live source, value, send, and error state.
osc`osc bind <name> stream <stream> <address> [rate=hz] [delta=value] [send=change\always]`Publish one scalar stream as OSC float32 values in its native unit.
osc`osc bind <name> stream <event-stream> <address> edge=rising\falling\both [rate=hz]`Publish sampled boolean transitions as OSC int32 0 or 1.
osc`osc bind <name> control <control> <address> [rate=hz] [send=change\always]`Publish one normalized named control as an OSC float32 value from 0.0..1.0.
oscosc unbind <name> or osc clearRemove one outbound binding or all outbound bindings.
radioradioOpen the packet-radio TUI with live status and editable common config.
radio`radio statuslist`List packet radios registered by expansion drivers.
radioradio status <name>Show one packet radio, its capabilities, state, and current config.
radioradio config <name> [field value]Show or update common packet-radio configuration.
radioradio profile listList immutable built-in and persistent user radio profiles.
radioradio profile show <profile>Show every setting captured by one profile.
radioradio profile apply <radio> <profile>Apply one complete profile to a radio, restoring the prior config if application fails.
radioradio profile save <radio> <profile>Save or replace a user profile from the radio's complete current config.
radioradio profile remove <profile>Remove a user profile. Built-in profiles are read-only.
meshcoremeshcore statusShow MeshCore identity, radio, packet, delivery, duplicate, memory, and stack state.
meshcore`meshcore identity show\generate\import\export`Inspect or explicitly manage the private MeshCore identity.
meshcoremeshcore name [name]Show or set the MeshCore-specific advertised name.
meshcore`meshcore advert zero\flood`Queue a local or explicitly network-wide advert.
meshcore`meshcore channel list\add\remove\public`Join public hashtag channels or manage bounded shared-key groups while the job is stopped.
meshcoremeshcore stream list or meshcore stream status [port]Inspect trusted peer-bound virtual serial ports carried by encrypted MeshCore direct packets.
meshcoremeshcore stream create <port> <trusted-endpoint-id>Register a reliable MeshCore virtual serial port for one exact trusted endpoint. Configure both peers.
meshcoremeshcore stream remove <port>Remove an unclaimed MeshCore virtual serial port.
radio`radio state <name> [sleepstandbyrxtx]`Show or change radio operating state.
radio`radio send <name> <textbyte...>`Send one packet.
radioradio recv <name> [timeout-ms]Receive one packet and print metadata plus payload.
gnssgnss [list]List registered GNSS receivers and their concrete drivers.
gnssgnss status [name]Show power, fix, fix type, and satellite state for one logical receiver.
gnss`gnss power <on\off> [name]`Enable or disable one logical receiver using its driver-specific power control.
gnssgnss fix [name] [timeout-ms]Poll one receiver for a position, UTC time, fix type, satellite count, and accuracy. A timeout can be supplied without a receiver name.
modemmodemOpen the modem status and settings TUI.
modemmodem listList registered cellular modems, concrete drivers, transports, and power, reset, and baud capabilities.
modemmodem status [name]Read SIM readiness, LTE registration, signal, packet-context, and IP-interface state.
modem`modem power <on\off> [name]`Switch a modem's optional hardware power line.
modemmodem reset [name]Reset a supported modem using its reset line or driver-defined power cycle.
modem`modem baud [name] [auto\rate]`Show or persist an optional serial modem transport rate. auto selects the driver's preferred rate.
modem`modem profile set [name] --apn <apn> [--dns <ipv4\auto>] [--ip ipv4\ipv6\ipv4v6] [--auth none\pap\chap\auto] [--user <user> --password <password>]`Validate, apply, and persist a modem-independent cellular profile.
modem`modem profile show\clear [name]`Inspect a saved profile with its password redacted, or remove it.
modem`modem connect\disconnect [name]`Bring the saved cellular network connection and PPP interface up or down.
modemmodem sim unlock <pin> [name]Enter a required SIM PIN without persisting it.
modemmodem at <quoted-command> [name] [timeout-ms]Send one validated AT command and print the complete response.
haptichaptic [list]List registered haptic devices, their concrete drivers, and supported effect range.
haptichaptic play <effect> [name]Play one numbered effect on a haptic device.
haptichaptic stop [name]Stop the active haptic effect.
chargercharger [list]List registered battery chargers, concrete drivers, and valid configuration ranges.
chargercharger status [name]Show charger state, input status, configured limits, and the raw fault byte.
charger`charger enable <on\off> [name]`Enable or disable charging.
chargercharger input-limit <mA> [name]Set the input current limit to an advertised, exact step value.
chargercharger current <mA> [name]Set fast-charge current to an advertised, exact step value.
chargercharger voltage <mV> [name]Set charge voltage to an advertised, exact step value.
imuimu [list]List registered motion sensors, their concrete drivers, and available measurements.
imuimu sample [name] [timeout-ms]Read acceleration, angular velocity, and optional orientation from one motion sensor.
nfcnfc [list]List registered NFC readers and their concrete drivers.
nfc`nfc power <on\off> [name]`Enable or disable a reader that has a driver-managed power rail.
nfcnfc scan [name] [timeout-ms]Discover one collision-free NFC-A tag and print its UID, ATQA, and SAK.
espnowespnow [status]Show ESP-NOW owner, channel, PHY, peers, traffic, drops, conflicts, and last error.
espnow`espnow peers\list`List persistent configured and volatile learned Link-ID-to-MAC mappings.
espnowespnow peer add <link-id> <mac>Save a persistent unicast peer mapping.
espnowespnow peer remove <link-id>Remove a configured or learned peer mapping.
link`link status\list`List active SolarOS Link instances and their queue/protocol counters.
linklink status <link>Show local ID, transport MTU, queues, acknowledgements, duplicates, CRC errors, and drops.
link`link send <link> <broadcast\destination-id> <text>`Queue a text message. Unicast requests an acknowledgement.
link`link send-binary <link> <broadcast\destination-id> <byte...>`Queue a binary message.
linklink receive <link> [timeout-ms]Remove and print one received message.
linklink stream listList Link-backed virtual serial ports.
linklink stream status [port]Show virtual-port peer, connection, queue, traffic, retry, reconnect, and error state.
linklink stream create <link> <port> <peer-id>Register a reliable peer-bound Link stream as a normal SolarOS byte-stream port.
linklink stream remove <port>Remove an unclaimed Link stream port.
pocsagpocsag statusShow detailed status for the POCSAG background receiver.
pocsag`pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha\numeric] [normal\inverted] [function]`Encode and transmit one POCSAG page.
uartuart [status [bus]]Show the default uart0 or a selected named UART bus.
uartuart baud [bus] [rate]Show or set a named UART bus baud rate.
uart`uart mode [bus] [raw\line]`Show or set a named UART bus service mode.
uartuart write [bus] <text>Write text through the default or selected named UART bus.
uartuart read [bus] [ms]Read bytes from the default or selected named UART bus.
gpiogpio status or gpio listList board GPIOs with free, releasable, or fixed pin policy.
gpio`gpio mode <pin> <inout> [noneupdown]`Configure a runtime GPIO.
gpiogpio read <pin>Read a runtime GPIO.
gpio`gpio write <pin> <01>`Write a runtime GPIO configured as output.
gpiogpio release <pin>Reset a direct GPIO and release its resource claim for a bus or another service.
onewireonewire [status [bus]]Show every registered named 1-Wire bus, or one selected bus.
onewire`onewire reset <bus\pin>`Reset a named bus or direct runtime GPIO and report presence.
onewire`onewire scan <bus\pin>`Discover and list 1-Wire ROM addresses.
onewire`onewire xfer <bus\pin> <read-len> [byte...]`Reset, write bytes, then read bytes on a 1-Wire target.
adcadc statusShow ADC service status.
adcadc read <pin>Read an ADC-capable runtime pin.
dpaddpad [status]Show ADC D-pad pins, raw values, zones, and calibration thresholds.
dpaddpad calibrate [idle]Calibrate the current D-pad idle value.
dpaddpad calibrate resetRestore the compiled D-pad calibration.
pwmpwm statusShow PWM state.
pwmpwm set <pin> <freq-hz> <duty-percent>Start LEDC PWM on a runtime pin.
pwmpwm off <pin>Stop PWM on a pin.
i2ci2c [status [bus]]Show every named I2C bus, or one selected bus.
i2ci2c speed [bus] [hz]Show or change a named bus clock; defaults to i2c0 and accepts 1 through 1000000 Hz.
i2ci2c scan [bus]Scan a named bus; defaults to i2c0.
i2ci2c probe [bus] <addr>Probe one address; defaults to i2c0.
i2ci2c read [bus] <addr> <reg> [len]Read register bytes; defaults to i2c0.
i2ci2c write [bus] <addr> <reg> <byte...>Write register bytes; defaults to i2c0.
spispi [status [bus]]Show every named SPI bus, or one selected bus.
spispi xfer <bus> <cs> <mode> <hz> <byte...>Full-duplex transfer over a named SPI bus.
spispi read <bus> <cs> <mode> <hz> <len> [fill]Read bytes over a named SPI bus.
spispi write <bus> <cs> <mode> <hz> <byte...>Write bytes over a named SPI bus.
datedate [YYYY-MM-DD]Show or set the local date.
timetime [HH:MM[:SS]]Show or set the local time.
temperature`temperature [list\sensor]`List temperature providers or read the default or named sensor.
humidity`humidity [list\sensor]`List humidity providers or read the default or named sensor.

Board-specific connector resources, runtime GPIO policy, named buses, leases, and attachment examples are documented in Expansion Ports. Use expansion status and gpio list for the authoritative view on a running device.

The onewire command accepts a registered bus name or any runtime-accessible GPIO. onewire status discovers named buses, while the numeric form preserves the direct-pin workflow. Every xfer starts with a 1-Wire reset, writes the supplied bytes, and then reads read-len bytes. For example, onewire xfer 1 9 0xcc 0xbe issues Skip ROM and Read Scratchpad, then reads a nine-byte scratchpad. The equivalent named form starts with onewire xfer onewire0. ROM address bytes supplied to xfer use least-significant-byte-first wire order. The service enables the ESP32 internal pull-up, but a 4.7 kohm external pull-up from the data line to 3.3 V is strongly recommended. The internal pull-up is not a parasite-power supply.

Physical displays are listed by display list. A built-in board panel registers as a board display target such as display0; an expansion display driver stays in expansion drivers as attachable hardware and registers a display target after it is attached. The built-in board panel is not an expansion driver. display list includes the current owner when a target is claimed. display test <target> claims the target while it draws a visible frame/test pattern, then releases it. display mode <target> lists driver-specific display settings for supported display drivers; display mode <target> <mode> applies one setting. With power=auto, the built-in ST7305 path uses the normal power profile before writing changed frame content and switches to the paired lpm profile after the frame has been idle for the configured driver debounce, or immediately when a present pass finds no changed pixels. The default ST7305 idle debounce is 1000 ms. The advanced display mode <target> idle-lpm-ms=<ms> driver option updates it at runtime and persists it in the ST7305 NVS namespace. ST7305 tuning options also live on this driver-specific mode surface: power=<auto|hpm|lpm> selects automatic idle switching or a forced power mode, inverted=<on|off> selects panel inversion, lpm-hz=<0.25|0.5|1|2|4|8> changes the controller's LPM frame-rate field, and hpm-hz=<16|25.5|32|51> changes the controller's HPM frame-rate field. These driver values are stored in NVS when changed. The ST7305 inverted= setting controls panel polarity and remains independent of the terminal palette selected with setterm palette. On SSD1683 board and expansion targets, refresh=auto uses a full waveform for the first changed frame and after every 19 non-full updates, while unchanged frames are skipped. Waveshare V2 expansion targets use the changed framebuffer rectangle and the controller's partial-window waveform for those intermediate updates. refresh=fast forces a fast full-frame waveform and refresh=full forces the full cleanup waveform on every changed frame.

Packet radio devices are datagram endpoints registered by expansion drivers, not byte-stream ports. The common radio layer preserves packet metadata such as RSSI and optional source/destination IDs. Radio frequency values are Hz by default and also accept k, kHz, M, and MHz suffixes:

radio status radio0
radio config radio0 frequency 433MHz
radio config radio0 modulation gfsk
radio send radio0 hello
radio recv radio0 5000

Profiles avoid partially reconfiguring one end while copying a list of fields. lora-eu868, gfsk-eu868, and ook-eu868 are built in. Up to eight user profiles are stored as one versioned NVS record and consume no idle profile cache:

radio profile list
radio profile apply radio0 lora-eu868
radio config radio0 sf 9
radio profile save radio0 lora-sf9
radio profile show lora-sf9
radio profile remove lora-sf9

Like radio config, applying a profile leaves the radio in driver standby. If the driver rejects a setting, SolarOS attempts to restore the complete prior configuration and operating state. A profile does not bypass the driver's supported frequency, modulation, power, or packet-size checks. The operator remains responsible for regional frequency, transmit-power, and duty-cycle rules.

RFM95W radios support FSK, GFSK, MSK, GMSK, OOK, and LoRa. LoRa additionally uses bandwidth, sf, and coding-rate. For an RFM95W using the common 868 MHz profile:

radio config radio0 frequency 868MHz
radio config radio0 bandwidth 125000
radio config radio0 sf 7
radio config radio0 coding-rate 4/5

variable=on selects the normal explicit LoRa header. variable=off selects implicit-header mode and requires the configured length to match on both ends. SF6 requires implicit-header mode.

FSK-family and OOK modes use bitrate, deviation, and a single-side bandwidth. MSK and GMSK derive deviation as one quarter of bitrate; GFSK and GMSK use Gaussian BT=1.0 shaping. RFM95W FSK/OOK packets contain at most 64 payload bytes, while fixed length zero enables the unlimited FIFO-stream mode.

The POCSAG job configures an attached packet radio for one paging channel, filters addresses to one RIC, corrects up to two bad bits per BCH codeword, and publishes decoded pages to the universal inbox. Consecutive POCSAG batches are kept in one message. For the 448.425 MHz test channel:

job start pocsag radio 448425000 1200 1841525 alpha
pocsag status
inbox list unread

Use inverted as the final argument if the transmitter and receiver use opposite FSK mark/space polarity. Stopping the job restores the radio's previous configuration and state.

To transmit a page, stop the receiver when it uses the same half-duplex radio, then send the message. Alphanumeric pages default to function 3; numeric pages default to function 0. The optional final argument selects function 0 through 3. The previous radio configuration and state are restored after transmission.

job stop pocsag
pocsag send radio 448425000 1200 1841525 "SolarOS calling" alpha inverted

Quick Examples

help
version
pkg
board
wifi on
ping wintermute
sshkey gen 2048
ota check
ota upgrade
watch -n 1 battery
daq start /logs/env.csv temperature humidity battery --rate 60
session create shell cdc0 --term auto
session create shell lcd0
xfer send uart0 /logs/payload.bin --zmodem

Quick reference

Run commands to list commands compiled into the current firmware, man TOPIC for a focused guide, and help for the complete manual tree. Commands are package-aware, support shell completion where applicable, and use the current shell working directory for relative paths.

Shell and storage · SolarOS manual

Storage and shell paths

SolarOS presents the default storage volume as /. On an SD-backed target this normally means the SD card, while /flash remains the internal flash volume. On a board without SD, / normally maps to internal flash. Attaching an sdmmc or sdspi expansion does not change that mapping: internal flash remains /, and the removable card mounts at /sdcard.

Boards with an integrated SDMMC slot expose it as the fixed expansion device storage0. Its pins are claimed before storage starts, but card probing and mounting remain in the normal storage initialization phase.

Shell startup is deliberately independent of whichever volume is currently the default. setterm startup flash reads /flash/.shell/startup on SD-capable boards and /.shell/startup on boards where flash is root. setterm startup sd reads /sdcard/.shell/startup. The setting is stored in NVS and takes effect on the next boot. The default setterm startup auto first uses a mounted board-owned SD card and falls back to flash when SD cannot be mounted. Explicit flash and sd selections never fall back to the other volume. If SD is mounted but has no .shell/startup, auto does not run the flash script.

Use disk for both internal and removable persistent storage:

disk status
disk lsblk
disk mount flash
disk mount sd0p2 /mnt
disk umount sd0p2

disk lsblk names internal flash as flash, a removable card as sd0, and its partitions as sd0p1, sd0p2, and so on.

Formatting

Formatting permanently erases the selected target and creates a FAT filesystem. The target must be unmounted and the explicit --force guard is required:

disk umount flash
disk format flash --force
disk mount flash

For removable media, use disk umount without a target to unmount all card volumes before formatting sd0 or one of its partitions. Formatting sd0 creates a whole-disk FAT filesystem; formatting sd0pN preserves the partition table and formats only that partition.

SolarOS initializes a completely erased internal flash partition on first use, but it does not automatically format a non-empty partition when mounting fails. This keeps filesystem damage from silently turning into data loss; use the guarded format command only when the existing contents can be discarded.

Shell paths are not host operating-system paths. Scripts should use solaros.storage so the same code follows SolarOS mount and path rules.

Relative path arguments passed to shell commands and foreground applications start at the directory shown in the shell prompt. This includes file editors, readers, script runtimes, media applications, transfers, and files [path]. Use a leading slash to start at the active volume root.

Inspect before writing

import solaros

print(solaros.storage.status())
print(solaros.storage.blocks())
print(solaros.storage.usage())

Use resolve(path) when a native or library operation needs the resolved internal path. Check free space before copying or producing a large capture.

Volumes and directories

The storage API can create and remove directories, copy or rename files, and mount detected volumes. Destructive calls report SolarOS errors; do not assume that a failed operation partially succeeded.

Quick reference

Use solaros.storage, not host os or io APIs. Functions include status, is_mounted, mount, unmount, mount_point, usage, resolve, rescan, blocks, block_count, block, usage_for_block, mkdir, rmdir, remove, rename, copy, mount_volume, and unmount_volume. SolarOS shell paths use slash for the active default storage volume.

Commands · SolarOS manual

adc

Show ADC service status.

Usage

UsageDescription
adc statusShow ADC service status.
adc read <pin>Read an ADC-capable runtime pin.

Commands · SolarOS manual

agent

Open a new native LLM agent TUI or make one unsaved foreground request.

Usage

UsageDescription
agent; agent new; agent ask PROMPT...Open a new native LLM agent TUI or make one unsaved foreground request.
agent list; agent resume SLOT; agent delete SLOTList, restore, or delete durable local conversation slots.
agent status; agent toolsInspect provider state, request statistics, typed tools, risk, and policy.
`agent config endpointmodelkeyreasoningtoolsmax-tools VALUE`Configure the provider and tool policy.
`agent script python\lua (-c SOURCE \FILE) [ARGS...]`Run a bounded script through the agent execution path.
agent forgetErase the saved agent configuration.

Commands · SolarOS manual

apps

List registered foreground apps compiled into the firmware.

Usage

UsageDescription
appsList registered foreground apps compiled into the firmware.

Commands · SolarOS manual

audio

Show audio state, global speaker level, tone queue, and active synth telemetry.

Usage

UsageDescription
audio statusShow audio state, global speaker level, tone queue, and active synth telemetry.
audio devicesList registered audio devices and their capture/playback streams.
audio device <id>Show one audio device and its native PCM format.
`audio default [auto\<id>]`Show or select the preferred playback device. auto restores first-compatible-device selection.
audio tone [hz] [ms] [volume]Play a diagnostic tone.
audio tone-async [hz] [ms] [volume]Queue a tone and return its request ID immediately.
audio queueShow asynchronous tone queue state and counters.
audio cancel <request-id>Cancel a queued or playing asynchronous tone request.
audio level [volume]Show or set global speaker level.
audio mic [ms]Sample microphone level.
audio loopback [ms] [volume]Run microphone-to-speaker loopback.
audio offStop audio output.

Commands · SolarOS manual

battery

Show voltage, state of charge, power source, config, and monitor trend.

Usage

UsageDescription
battery [status]Show voltage, state of charge, power source, config, and monitor trend.
battery configShow battery capacity and voltage thresholds.
battery capacity [mAh]Show or set capacity estimate.
`battery min_voltage [VmV]`Show or set low-voltage threshold.
`battery max_voltage [VmV]`Show or set full/external-power shortcut threshold.

Commands · SolarOS manual

ble

Show BLE keyboard state and the current/next boot setting.

Usage

UsageDescription
ble [status]Show BLE keyboard state and the current/next boot setting.
ble enableSave BLE enabled for the next boot. The current boot is unchanged.
ble disableSave BLE disabled for the next boot. The current boot is unchanged.
ble defaultClear the saved override and use the board default on the next boot.
`ble keepalive [on\off]`Show or save the best-effort BLE keyboard keepalive setting.
ble scanScan nearby BLE devices.
ble pairStart keyboard pairing.
ble forgetErase the remembered keyboard, its BLE bond, and its cached GATT service database.
ble gatt statusShow the generic GATT connection state and discovered-service count.
`ble gatt connect <aa:bb:cc:dd:ee:ff> <public\random\rpa_public\rpa_random>`Connect to a BLE peripheral by address and address type.
ble gatt disconnectRequest disconnect of the shell's GATT session and cancel its pending operation.
ble gatt servicesList discovered services and their indexes and handle ranges.
ble gatt chars <service-index>List the characteristics discovered for one service.
ble gatt read <handle>Read a characteristic or descriptor by handle.
ble gatt write <handle> <hex...>Write hexadecimal bytes and request a response.
ble gatt write-nr <handle> <hex...>Write hexadecimal bytes without requesting a response.

Commands · SolarOS manual

board

Print board ID, name, and capabilities.

Usage

UsageDescription
boardPrint board ID, name, and capabilities.

Commands · SolarOS manual

cat

Print a small text file.

Usage

UsageDescription
`cat <pathpattern>`Print a small text file.

Commands · SolarOS manual

cd

Change current shell directory.

Usage

UsageDescription
cd [path]Change current shell directory.

Commands · SolarOS manual

charger

List registered battery chargers, concrete drivers, and valid configuration ranges.

Usage

UsageDescription
charger [list]List registered battery chargers, concrete drivers, and valid configuration ranges.
charger status [name]Show charger state, input status, configured limits, and the raw fault byte.
`charger enable <on\off> [name]`Enable or disable charging.
charger input-limit <mA> [name]Set the input current limit to an advertised, exact step value.
charger current <mA> [name]Set fast-charge current to an advertised, exact step value.
charger voltage <mV> [name]Set charge voltage to an advertised, exact step value.

Commands · SolarOS manual

clear

Clear the active shell terminal.

Usage

UsageDescription
clearClear the active shell terminal.

Commands · SolarOS manual

close

Close a display app, display shell, or retained port app, or stop a port shell session. The final interactive shell cannot be closed.

Usage

UsageDescription
close <session-id>Close a display app, display shell, or retained port app, or stop a port shell session. The final interactive shell cannot be closed.

Commands · SolarOS manual

commands

List built-in shell commands.

Usage

UsageDescription
commandsList built-in shell commands.

Commands · SolarOS manual

contacts

Open the searchable provider-neutral contact browser.

Usage

UsageDescription
contactsOpen the searchable provider-neutral contact browser.
contacts statusShow contact, endpoint, persistence, PSRAM, and opaque-credential counts.
`contacts list [all\discovered\trusted\blocked]`List contacts, optionally filtered by endpoint trust.
contacts show <contact-id>Show a contact and its bounded provider endpoints.
contacts rename <contact-id> <name>Change the local contact display name.
contacts trust <contact-id> [endpoint-id]Trust one endpoint or every endpoint on a contact.
contacts block <contact-id> [endpoint-id]Block one endpoint or every endpoint on a contact.
contacts remove <contact-id>Remove a contact and all its endpoints.
contacts link <target-contact-id> <source-contact-id>Move the source endpoints into the target and remove the source contact.

Commands · SolarOS manual

control

Inspect normalized controls, native app parameters, or target bindings.

Usage

UsageDescription
`control list\parameters\bindings`Inspect normalized controls, native app parameters, or target bindings.
control create <name> <stream> <min> <max> [smooth=ms] [deadband=value] [invert]Normalize a scalar stream as a named continuous control; use manual for script-supplied values.
`control bind <name> parameter <path> [pickup=on\off]`Bind a control to a typed native-app parameter with optional soft takeover.
control bind <name> midi <channel> <cc>Bind a control to a MIDI Control Change target.
`control get\set <name> [value]`Read or set a normalized 0..65535 control value.
`control parameter get\set <path> [value]`Read or set an available native parameter in its declared unit.
control unbind <name>Remove all target bindings owned by one named control.
control delete <name> or control clearRemove one control and its bindings, or remove all controls and bindings.

Commands · SolarOS manual

cp

Copy a file or matched set.

Usage

UsageDescription
`cp <sourcepattern> <dest>`Copy a file or matched set.

Commands · SolarOS manual

daq

Print DAQ usage.

Usage

UsageDescription
daq helpPrint DAQ usage.
daq statusShow DAQ job status.
daq streamsList stream IDs.
`daq start <file.csv> <stream...> [--rate seconds\--rate-ms ms]; daq start <stream...> <file.csv> [--rate seconds\--rate-ms ms]`Start periodic CSV capture from one or more streams.
`daq start <file.csv> <stream> --changes [--append\--replace]`Capture a scalar or event stream only when its value changes.
daq start <file.bin> <byte-or-audio-stream> --raw [--rate-ms ms]Capture one byte or PCM audio stream without CSV framing.
daq stopStop the active data-acquisition job.

Commands · SolarOS manual

date

Show or set the local date.

Usage

UsageDescription
date [YYYY-MM-DD]Show or set the local date.

Commands · SolarOS manual

df

Show free space on mounted storage volumes.

Usage

UsageDescription
dfShow free space on mounted storage volumes.

Commands · SolarOS manual

disk

Show persistent-storage status.

Usage

UsageDescription
disk [status]Show persistent-storage status.
disk lsblkList internal flash and detected removable block devices and partitions.
`disk mount [flash\sd0pN] [mount]`Mount the default volume or an explicit persistent volume.
`disk umount [flash\sd0pN\mount]`Unmount the default volume or an explicit volume/mount point.
`disk format <flash\sd0\sd0pN> --force`Create a FAT filesystem, permanently erasing the unmounted target.

Commands · SolarOS manual

display

List drawable display targets, draw a test pattern, or change driver-specific display settings.

Usage

UsageDescription
display [list]; display test <target>; display mode <target> [mode]List drawable display targets, draw a test pattern, or change driver-specific display settings.

Commands · SolarOS manual

dpad

Show ADC D-pad pins, raw values, zones, and calibration thresholds.

Usage

UsageDescription
dpad [status]Show ADC D-pad pins, raw values, zones, and calibration thresholds.
dpad calibrate [idle]Calibrate the current D-pad idle value.
dpad calibrate resetRestore the compiled D-pad calibration.

Commands · SolarOS manual

echo

Print the arguments separated by spaces, followed by a newline. Quotes preserve spaces and are not printed.

Usage

UsageDescription
echo [text...]Print the arguments separated by spaces, followed by a newline. Quotes preserve spaces and are not printed.

Commands · SolarOS manual

email

Open the receive-only email app.

Usage

UsageDescription
emailOpen the receive-only email app.
email statusShow saved account, local message counts, and last sync error.
email configure <imaps://host[:port]> <user> <password> [mailbox]Save an IMAPS account; the default mailbox is INBOX.
email syncStart a one-shot mailbox synchronization.
email forgetRemove the saved account and local email list.

Commands · SolarOS manual

engine

Print or reset generic engine utilization counters for CPU/SIMD-style backends and vector bulk operations.

Usage

UsageDescription
`engine [statusreset]`Print or reset generic engine utilization counters for CPU/SIMD-style backends and vector bulk operations.

Commands · SolarOS manual

espnow

Show ESP-NOW owner, channel, PHY, peers, traffic, drops, conflicts, and last error.

Usage

UsageDescription
espnow [status]Show ESP-NOW owner, channel, PHY, peers, traffic, drops, conflicts, and last error.
`espnow peers\list`List persistent configured and volatile learned Link-ID-to-MAC mappings.
espnow peer add <link-id> <mac>Save a persistent unicast peer mapping.
espnow peer remove <link-id>Remove a configured or learned peer mapping.

Commands · SolarOS manual

exit

Close the current UART, USB CDC, or telnet shell when another interactive shell remains.

Usage

UsageDescription
exitClose the current UART, USB CDC, or telnet shell when another interactive shell remains.

Commands · SolarOS manual

expansion

Open the expansion device manager. Browse attached devices and driver categories, inspect details, attach supported drivers, save runtime attachments to the selected startup script, and detach runtime devices. Bus lifecycle remains in the io app.

Usage

UsageDescription
expansionOpen the expansion device manager. Browse attached devices and driver categories, inspect details, attach supported drivers, save runtime attachments to the selected startup script, and detach runtime devices. Bus lifecycle remains in the io app.
expansion statusShow expansion capabilities, named buses and leases, connector resources, active devices, and resource claims.
expansion layout [connector]Draw the board's physical connector map with live free, releasable, claimed, fixed, power, ground, and NC markers.
expansion scanList expansion resources and probe-capable drivers.
expansion driversList compiled expansion drivers.
expansion devicesList fixed board and runtime-attached expansion devices with origin, readiness, startup mode, policy, and bindings.
`expansion bus create i2c <name> port=<i2c0\i2c1> sda=<gpio> scl=<gpio> [speed=<hz>]`Define a runtime I2C bus on an unused controller and approved expansion pins.
expansion bus create onewire <name> pin=<gpio>Define a runtime named 1-Wire bus on an approved expansion pin.
expansion bus create ps2 <name> clock=<gpio> data=<gpio>Define an exclusive PS/2 bus on two approved expansion pins.
expansion bus create midi <name> tx=<gpio> rx=<gpio> [baud=<rate>]Define an exclusive MIDI bus; SolarOS selects the UART backend and defaults to 31250 baud.
`expansion bus create spi <name> host=<spi2\spi3> sclk=<gpio> mosi=<gpio> [miso=<gpio\none>] cs=<gpio> [cs=<gpio> ...] [max=<bytes>]`Define a runtime-routed SPI bus on a board-approved host and expansion pins.
`expansion bus create uart <name> port=<uart1\uart2> tx=<gpio> rx=<gpio> [baud=<rate>]`Define a lazy runtime UART on an unused controller and approved expansion pins.
expansion bus attach <name>Attach a named detachable bus and reserve its endpoint and signal pins.
expansion bus detach <name>Detach an idle named bus, preserving its descriptor while releasing its endpoint and signal pins.
expansion bus remove <name>Remove an idle runtime bus and release its signal pins.
expansion attach <driver> <name> <resource...>Attach a compiled expansion driver or manual resource profile.
expansion detach <name>Detach an active expansion device and release its resource claims.
expansion export <path>Atomically export runtime buses and catalog-backed device attachments as a portable expansion manifest for custom-board generation.

Commands · SolarOS manual

fg

Resume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port shell's most recently suspended app.

Usage

UsageDescription
fg [session-id]Resume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port shell's most recently suspended app.

Commands · SolarOS manual

gateway

Show gateway configuration, connection state, and traffic counters.

Usage

UsageDescription
gateway statusShow gateway configuration, connection state, and traffic counters.
gateway configure <url> [token]Save gateway connection settings. The gateway uses the global SolarOS user identity.
gateway connect [url] [token]Enable gateway synchronization, optionally updating settings.
gateway disconnectDisable gateway synchronization.
gateway roomsList known and joined gateway rooms.
`gateway join\leave\delete <room>`Queue a gateway-specific room operation.

Commands · SolarOS manual

gesture

List gesture-capable input sources, readiness, and the gesture kinds each source advertises.

Usage

UsageDescription
gesture [status]List gesture-capable input sources, readiness, and the gesture kinds each source advertises.
`gesture bind source=<name\*> gesture=<name> [direction=<name\*>] [cooldown=<ms>] -- <command> [args...]`Configure a volatile gesture-to-command rule for the gesture-listener job.
gesture bindingsShow listener state and list volatile gesture bindings, trigger counts, queue drops, cooldowns, and commands.
`gesture unbind <id\all>`Remove one volatile gesture binding or all of them.

Commands · SolarOS manual

gnss

List registered GNSS receivers and their concrete drivers.

Usage

UsageDescription
gnss [list]List registered GNSS receivers and their concrete drivers.
gnss status [name]Show power, fix, fix type, and satellite state for one logical receiver.
`gnss power <on\off> [name]`Enable or disable one logical receiver using its driver-specific power control.
gnss fix [name] [timeout-ms]Poll one receiver for a position, UTC time, fix type, satellite count, and accuracy. A timeout can be supplied without a receiver name.

Commands · SolarOS manual

gpio

List board GPIOs with free, releasable, or fixed pin policy.

Usage

UsageDescription
gpio status or gpio listList board GPIOs with free, releasable, or fixed pin policy.
`gpio mode <pin> <inout> [noneupdown]`Configure a runtime GPIO.
gpio read <pin>Read a runtime GPIO.
`gpio write <pin> <01>`Write a runtime GPIO configured as output.
gpio release <pin>Reset a direct GPIO and release its resource claim for a bus or another service.

Commands · SolarOS manual

haptic

List registered haptic devices, their concrete drivers, and supported effect range.

Usage

UsageDescription
haptic [list]List registered haptic devices, their concrete drivers, and supported effect range.
haptic play <effect> [name]Play one numbered effect on a haptic device.
haptic stop [name]Stop the active haptic effect.

Commands · SolarOS manual

help

Browse the package-aware manual or manage its signed exact-version SD copy. command.status escapes the maintenance keyword.

Usage

UsageDescription
help [TOPIC]; help command.status; help status; help update; help resetBrowse the package-aware manual or manage its signed exact-version SD copy. command.status escapes the maintenance keyword.

Commands · SolarOS manual

humidity

List humidity providers or read the default or named sensor.

Usage

UsageDescription
`humidity [list\sensor]`List humidity providers or read the default or named sensor.

Commands · SolarOS manual

i2c

Show every named I2C bus, or one selected bus.

Usage

UsageDescription
i2c [status [bus]]Show every named I2C bus, or one selected bus.
i2c speed [bus] [hz]Show or change a named bus clock; defaults to i2c0 and accepts 1 through 1000000 Hz.
i2c scan [bus]Scan a named bus; defaults to i2c0.
i2c probe [bus] <addr>Probe one address; defaults to i2c0.
i2c read [bus] <addr> <reg> [len]Read register bytes; defaults to i2c0.
i2c write [bus] <addr> <reg> <byte...>Write register bytes; defaults to i2c0.

Commands · SolarOS manual

identity

Show the configured user and hostname.

Usage

UsageDescription
identity [status]Show the configured user and hostname.
identity user <name>Save the SolarOS user and default SSH/SCP username in NVS.
identity hostname <name>Save the device hostname in NVS; reboot to update Wi-Fi.

Commands · SolarOS manual

imu

List registered motion sensors, their concrete drivers, and available measurements.

Usage

UsageDescription
imu [list]List registered motion sensors, their concrete drivers, and available measurements.
imu sample [name] [timeout-ms]Read acceleration, angular velocity, and optional orientation from one motion sensor.

Commands · SolarOS manual

inbox

Open the universal incoming-message browser.

Usage

UsageDescription
inboxOpen the universal incoming-message browser.
inbox statusShow universal incoming-message counts and storage status.
`inbox list [all\unread]`List newest messages first.
inbox read <id>Print one message and mark it read.
inbox delete <id>Delete one message by its decimal ID.
inbox clearRemove every message.
inbox post <source> <message>Post a message from a shell script or for testing.
`inbox notify [on\off\test]`Show, persist, disable, or test the Inbox notification sound. It defaults to on and is unavailable on boards without audio output.

Commands · SolarOS manual

input

List all input sources or filter them by semantic class.

Usage

UsageDescription
`input [statuskeyboardtouchmousejoystickdpadbuttonsgesture]`List all input sources or filter them by semantic class.
input test <source>Show event counters and the last key, pointer, axis, or gesture event accepted from one source.
`input calibrate <source> [set <min-x> <max-x> <min-y> <max-y> <width> <height>\reset]`Show, save, or reset coordinate calibration for an absolute-pointer source.
`input emit <key\chord>`Emit a local key tap, such as RIGHT, ALT+RIGHT, ENTER, or one literal character, through the normal input-focus path.

Commands · SolarOS manual

job

Show one job or all jobs.

Usage

UsageDescription
job status [name]Show one job or all jobs.
job start <name> [args...]Start or restart a job.
job stop <name>Stop a job.

Commands · SolarOS manual

jobs

List registered jobs and their state.

Usage

UsageDescription
jobsList registered jobs and their state.

Commands · SolarOS manual

led

Inspect or control the built-in status LED when available.

Usage

UsageDescription
`led [statusonofftoggle]`Inspect or control the built-in status LED when available.

Commands · SolarOS manual

link

List active SolarOS Link instances and their queue/protocol counters.

Usage

UsageDescription
`link status\list`List active SolarOS Link instances and their queue/protocol counters.
link status <link>Show local ID, transport MTU, queues, acknowledgements, duplicates, CRC errors, and drops.
`link send <link> <broadcast\destination-id> <text>`Queue a text message. Unicast requests an acknowledgement.
`link send-binary <link> <broadcast\destination-id> <byte...>`Queue a binary message.
link receive <link> [timeout-ms]Remove and print one received message.
link stream listList Link-backed virtual serial ports.
link stream status [port]Show virtual-port peer, connection, queue, traffic, retry, reconnect, and error state.
link stream create <link> <port> <peer-id>Register a reliable peer-bound Link stream as a normal SolarOS byte-stream port.
link stream remove <port>Remove an unclaimed Link stream port.

Commands · SolarOS manual

log

Show runtime log ring status.

Usage

UsageDescription
log statusShow runtime log ring status.
log show [count]Print recent SolarOS log entries.
`log follow [errorwarninfodebug]`Follow logs in the current shell.
log clearClear the runtime log ring.
`log level [errorwarninfodebug]`Show or change runtime log level.
`log sink cdc [onoff]`Enable or disable CDC mirroring of SolarOS logs.

Commands · SolarOS manual

ls

List files. Hidden files are shown only with -a; sizes are human-readable with -h.

Usage

UsageDescription
`ls [-a] [-h] [pathpattern]`List files. Hidden files are shown only with -a; sizes are human-readable with -h.

Commands · SolarOS manual

man

Read or search the package-aware SolarOS manual.

Usage

UsageDescription
man TOPIC; man -k QUERY...; man --listRead or search the package-aware SolarOS manual.

Commands · SolarOS manual

mem

Print heap status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.

Usage

UsageDescription
mem [policy]Print heap status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.

Commands · SolarOS manual

meshcore

Show MeshCore identity, radio, packet, delivery, duplicate, memory, and stack state.

Usage

UsageDescription
meshcore statusShow MeshCore identity, radio, packet, delivery, duplicate, memory, and stack state.
`meshcore identity show\generate\import\export`Inspect or explicitly manage the private MeshCore identity.
meshcore name [name]Show or set the MeshCore-specific advertised name.
`meshcore advert zero\flood`Queue a local or explicitly network-wide advert.
`meshcore channel list\add\remove\public`Join public hashtag channels or manage bounded shared-key groups while the job is stopped.
meshcore stream list or meshcore stream status [port]Inspect trusted peer-bound virtual serial ports carried by encrypted MeshCore direct packets.
meshcore stream create <port> <trusted-endpoint-id>Register a reliable MeshCore virtual serial port for one exact trusted endpoint. Configure both peers.
meshcore stream remove <port>Remove an unclaimed MeshCore virtual serial port.

Commands · SolarOS manual

messages

Show bounded-store, persistence, drop, and live provider state.

Usage

UsageDescription
messages statusShow bounded-store, persistence, drop, and live provider state.
messages conversationsList provider-neutral conversations and unread/security state.
messages list <conversation-id>List retained messages and their stable hexadecimal IDs.
messages send <conversation-id> <text> [--allow-untrusted]Queue an outbound message; discovered direct endpoints require the explicit flag.
messages read <conversation-id>Mark a conversation and its linked Inbox entries read.
messages delete <message-id>Delete one retained message and its linked Inbox projection by hexadecimal ID.
`messages clear <gateway\meshcore\link\all>`Clear retained history and owned Inbox projections for one provider or every messaging provider. Unrelated Inbox sources remain.
messages outboxList pending outbound requests in queue order.
messages cancel <message-id>Cancel a queued outbound message by the hexadecimal ID printed by list or send.

Commands · SolarOS manual

midi

Show MIDI worker, traffic, parser, and queue status.

Usage

UsageDescription
midi statusShow MIDI worker, traffic, parser, and queue status.
midi monitorPrint incoming CC and key messages until the app-exit key, Esc, or q is pressed.
`midi note-on\note-off <channel> <note> [velocity]`Queue a MIDI note message for transmission.
midi cc <channel> <controller> <value>Queue a MIDI control-change message.
midi program <channel> <program>Queue a MIDI program-change message.
midi send <status> [data1] [data2]Queue one validated raw MIDI message.
midi stream listList configured incoming MIDI CC scalar streams and their latest values.
`midi stream add\remove <channel> <controller>`Register or remove midi.cc.<channel>.<controller> as a scalar stream.
midi stream clearRemove all configured MIDI CC scalar streams.

Commands · SolarOS manual

mkdir

Create directories.

Usage

UsageDescription
mkdir <path> [path...]Create directories.

Commands · SolarOS manual

modem

Open the modem status and settings TUI.

Usage

UsageDescription
modemOpen the modem status and settings TUI.
modem listList registered cellular modems, concrete drivers, transports, and power, reset, and baud capabilities.
modem status [name]Read SIM readiness, LTE registration, signal, packet-context, and IP-interface state.
`modem power <on\off> [name]`Switch a modem's optional hardware power line.
modem reset [name]Reset a supported modem using its reset line or driver-defined power cycle.
`modem baud [name] [auto\rate]`Show or persist an optional serial modem transport rate. auto selects the driver's preferred rate.
`modem profile set [name] --apn <apn> [--dns <ipv4\auto>] [--ip ipv4\ipv6\ipv4v6] [--auth none\pap\chap\auto] [--user <user> --password <password>]`Validate, apply, and persist a modem-independent cellular profile.
`modem profile show\clear [name]`Inspect a saved profile with its password redacted, or remove it.
`modem connect\disconnect [name]`Bring the saved cellular network connection and PPP interface up or down.
modem sim unlock <pin> [name]Enter a required SIM PIN without persisting it.
modem at <quoted-command> [name] [timeout-ms]Send one validated AT command and print the complete response.

Commands · SolarOS manual

mqtt

Show broker, authentication, connection, traffic, queue, and error status without revealing the password.

Usage

UsageDescription
mqtt statusShow broker, authentication, connection, traffic, queue, and error status without revealing the password.
mqtt connect [mqtt[s]://host[:port] [username [password]]]Connect to a broker and save supplied connection settings; omit them to reuse saved settings.
mqtt disconnectDisconnect and stop the MQTT client.
mqtt publish <topic> <payload> [qos] [retain]Publish a message with optional QoS 0–2 and retain flag.
mqtt subscribe <topic> [qos]Subscribe and print received messages until app-exit or q.

Commands · SolarOS manual

mv

Rename or move a file or matched set.

Usage

UsageDescription
`mv <sourcepattern> <dest>`Rename or move a file or matched set.

Commands · SolarOS manual

neopixel

List attached WS2812/NeoPixel strips.

Usage

UsageDescription
`neopixel [status\list] [name]`List attached WS2812/NeoPixel strips.
neopixel set <name> <index> <red> <green> <blue>Set one zero-based pixel and immediately refresh the strip.
neopixel fill <name> <red> <green> <blue>Fill and immediately refresh the strip. Color components are 0..255.
`neopixel clear\show <name>`Clear a strip immediately, or transmit its buffered colors.

Commands · SolarOS manual

netscan

Scan TCP ports on one host or a capped IPv4 range.

Usage

UsageDescription
`netscan <hostrange> [ports]`Scan TCP ports on one host or a capped IPv4 range.

Commands · SolarOS manual

network

Open the two-tab network TUI. Status shows interfaces and routes; Settings changes persistent interface priority and client routing.

Usage

UsageDescription
networkOpen the two-tab network TUI. Status shows interfaces and routes; Settings changes persistent interface priority and client routing.
network statusShow network interfaces, route selection, VPN routes, and router state as text.
network interfacesList route-capable interfaces, addresses, priorities, and the local downstream interface.
network routesShow the default route, automatic base path, and active WireGuard routes.
`network router [status\on\off]`Route Wi-Fi AP clients through the SolarOS route table using IPv4 forwarding and NAT.

Commands · SolarOS manual

nfc

List registered NFC readers and their concrete drivers.

Usage

UsageDescription
nfc [list]List registered NFC readers and their concrete drivers.
`nfc power <on\off> [name]`Enable or disable a reader that has a driver-managed power rail.
nfc scan [name] [timeout-ms]Discover one collision-free NFC-A tag and print its UID, ATQA, and SAK.

Commands · SolarOS manual

ntp

Sync the wall clock from NTP.

Usage

UsageDescription
ntp [server]Sync the wall clock from NTP.

Commands · SolarOS manual

nvs

Show the default NVS partition size, entry usage, and namespace count.

Usage

UsageDescription
nvs statusShow the default NVS partition size, entry usage, and namespace count.
nvs list [namespace]List non-empty namespaces and their entry usage, or list one namespace's keys, types, sizes, and storage cost. Values are never displayed.
nvs erase <namespace> [key]Erase one key or all data in one namespace, then reboot.
nvs backup [file]Back up the complete NVS partition to disk. The default is /.solar/nvs.bin.
nvs restore [file]Validate and restore a complete NVS backup, then reboot. The default is /.solar/nvs.bin.
nvs clearErase all NVS-backed settings and reboot immediately.

Commands · SolarOS manual

onewire

Show every registered named 1-Wire bus, or one selected bus.

Usage

UsageDescription
onewire [status [bus]]Show every registered named 1-Wire bus, or one selected bus.
`onewire reset <bus\pin>`Reset a named bus or direct runtime GPIO and report presence.
`onewire scan <bus\pin>`Discover and list 1-Wire ROM addresses.
`onewire xfer <bus\pin> <read-len> [byte...]`Reset, write bytes, then read bytes on a 1-Wire target.

Commands · SolarOS manual

osc

Inspect named outbound OSC bindings and their live source, value, send, and error state.

Usage

UsageDescription
osc bindingsInspect named outbound OSC bindings and their live source, value, send, and error state.
`osc bind <name> stream <stream> <address> [rate=hz] [delta=value] [send=change\always]`Publish one scalar stream as OSC float32 values in its native unit.
`osc bind <name> stream <event-stream> <address> edge=rising\falling\both [rate=hz]`Publish sampled boolean transitions as OSC int32 0 or 1.
`osc bind <name> control <control> <address> [rate=hz] [send=change\always]`Publish one normalized named control as an OSC float32 value from 0.0..1.0.
osc unbind <name> or osc clearRemove one outbound binding or all outbound bindings.

Commands · SolarOS manual

ota

Show running and configured OTA state.

Usage

UsageDescription
ota statusShow running and configured OTA state.
ota checkCheck signed board/flavor release metadata.
ota upgradeDownload firmware into the inactive OTA partition and reboot into it.
ota url [url]Show or set the OTA base URL.
ota flavor [flavor]Show or set target OTA flavor.
`ota boot 01`Select an OTA slot and reboot.

Commands · SolarOS manual

outbox

List pending outbound messages. Sent and failed messages remain in conversation history, not Outbox.

Usage

UsageDescription
outbox [list]List pending outbound messages. Sent and failed messages remain in conversation history, not Outbox.
outbox cancel <message-id>Cancel one pending message by hexadecimal ID.

Commands · SolarOS manual

ping

Send ICMP echo requests. Without count, ping runs until Esc, Ctrl+C, or app-exit.

Usage

UsageDescription
ping <host> [count]Send ICMP echo requests. Without count, ping runs until Esc, Ctrl+C, or app-exit.

Commands · SolarOS manual

pkg

Print compiled package groups and build units.

Usage

UsageDescription
pkgPrint compiled package groups and build units.

Commands · SolarOS manual

pocsag

Show POCSAG receiver configuration, counters, correction statistics, and RSSI.

Usage

UsageDescription
pocsag statusShow POCSAG receiver configuration, counters, correction statistics, and RSSI.
`pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha\numeric] [normal\inverted] [function]`Encode and transmit one POCSAG page.
pocsag statusShow detailed status for the POCSAG background receiver.
`pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha\numeric] [normal\inverted] [function]`Encode and transmit one POCSAG page.

Commands · SolarOS manual

port

List byte-stream ports.

Usage

UsageDescription
port listList byte-stream ports.
port status <name>Show port capabilities and owner.

Commands · SolarOS manual

power

Show the selected and effective profiles, suspend state, sleep policy, and wake statistics.

Usage

UsageDescription
power [status]Show the selected and effective profiles, suspend state, sleep policy, and wake statistics.
`power profile [performance\balanced\battery\lowpower]`Show or save the power profile.
`power idle [off\seconds]`Show or configure the display-shell idle light-sleep timeout.
`power key [off\sleep\suspend]`Show or configure the dedicated KEY short-press action.
power sleepEnter explicit light sleep from the display shell. Press KEY to wake.
power suspendTurn off the primary display while services and jobs continue. Press KEY to resume.

Commands · SolarOS manual

pwm

Show PWM state.

Usage

UsageDescription
pwm statusShow PWM state.
pwm set <pin> <freq-hz> <duty-percent>Start LEDC PWM on a runtime pin.
pwm off <pin>Stop PWM on a pin.

Commands · SolarOS manual

radio

Open the packet-radio TUI with live status and editable common config.

Usage

UsageDescription
radioOpen the packet-radio TUI with live status and editable common config.
`radio statuslist`List packet radios registered by expansion drivers.
radio status <name>Show one packet radio, its capabilities, state, and current config.
radio config <name> [field value]Show or update common packet-radio configuration.
radio profile listList immutable built-in and persistent user radio profiles.
radio profile show <profile>Show every setting captured by one profile.
radio profile apply <radio> <profile>Apply one complete profile to a radio, restoring the prior config if application fails.
radio profile save <radio> <profile>Save or replace a user profile from the radio's complete current config.
radio profile remove <profile>Remove a user profile. Built-in profiles are read-only.
`radio state <name> [sleepstandbyrxtx]`Show or change radio operating state.
`radio send <name> <textbyte...>`Send one packet.
radio recv <name> [timeout-ms]Receive one packet and print metadata plus payload.

Commands · SolarOS manual

ramfs

List PSRAM-backed volatile filesystem mounts.

Usage

UsageDescription
ramfs [status]List PSRAM-backed volatile filesystem mounts.
ramfs mount /path sizeMount a volatile filesystem that reserves PSRAM, such as ramfs mount / 1m.
ramfs unmount /pathUnmount a ramfs mount.

Commands · SolarOS manual

reboot

Restart the board.

Usage

UsageDescription
rebootRestart the board.

Commands · SolarOS manual

rm

Remove files. -f allows directories; -rf removes directories recursively.

Usage

UsageDescription
`rm [-f-rf] <pathpattern> [pathpattern...]`Remove files. -f allows directories; -rf removes directories recursively.

Commands · SolarOS manual

rtc

Show the RTC provider, capabilities, interrupt wiring, and alarm/timer owners. Reports unavailable when no RTC is present.

Usage

UsageDescription
rtc [status]Show the RTC provider, capabilities, interrupt wiring, and alarm/timer owners. Reports unavailable when no RTC is present.
rtc alarm set HH:MM[:SS] [day=N] [weekday=N]Program the RTC hardware alarm, optionally matching a day or weekday.
rtc alarm clearClear the RTC hardware alarm owned by this command.
rtc timer set <duration> [repeat]Program the RTC countdown timer. Durations use an s, m, h, or d suffix.
rtc timer clearClear the RTC countdown timer owned by this command.
rtc pendingShow pending RTC alarm and timer interrupts.
`rtc ack <alarm\timer\all>`Acknowledge pending RTC interrupts.

Commands · SolarOS manual

schedule

List persistent alarms and scheduled shell scripts. Entries show their enabled state, trigger, and action.

Usage

UsageDescription
schedule; schedule listList persistent alarms and scheduled shell scripts. Entries show their enabled state, trigger, and action.
schedule show <name>Show one entry, including its run and skip counters.
`schedule add <name> in <duration> <alarm\run script>`Add a one-shot monotonic schedule. Durations use an s, m, h, or d suffix.
`schedule add <name> every <duration> <alarm\run script>`Add a recurring monotonic interval schedule.
`schedule add <name> at YYYY-MM-DD HH:MM[:SS] <alarm\run script>`Add a one-shot schedule in configured local time. It waits for valid wall-clock time.
`schedule add <name> daily HH:MM[:SS] <alarm\run script>`Add a daily schedule in configured local time.
`schedule add <name> weekly <sun,mon,...> HH:MM[:SS] <alarm\run script>`Add a schedule for the selected local weekdays.
schedule enable <name>; schedule disable <name>Enable or disable an entry without removing it.
schedule remove <name>Remove an entry.
schedule run <name>Run an entry immediately. Only one scheduled shell script can run at a time.
schedule stop [name]Stop the active ringing alarm, optionally only when its name matches.

Commands · SolarOS manual

session

List display sessions, port shells, and retained port-owned application sessions with their owner.

Usage

UsageDescription
session listList display sessions, port shells, and retained port-owned application sessions with their owner.
`session create shell <port> [--term autovt100ansidumb] [--charset utf8ascii] [--size COLSxROWS]`Start a shell session on a byte-stream port.
session create shell <display-target>Attach a shell session to a ready display target such as lcd0.
session create <app> <display-target> [args...]Start a foreground application as the active session on a named display.
session focus [display-target]Show or assign the display that receives BLE keyboard and local board-control input.
session fg [id] or session switch [id]Resume a display session or a port-owned app on its owning terminal. Without an ID, restore the calling port's last suspended app.
session close <id>Close a display app, display shell, port-owned app, or port shell session.
session send <id> <command> [args...]Run a command on an active display-shell session. The target must be at an empty prompt.
session backgroundExplain the foreground/background controls.

Commands · SolarOS manual

sessions

List display app sessions, display shell sessions, and port shell sessions.

Usage

UsageDescription
sessionsList display app sessions, display shell sessions, and port shell sessions.

Commands · SolarOS manual

setterm

Open the terminal settings TUI from the display shell.

Usage

UsageDescription
settermOpen the terminal settings TUI from the display shell.
`setterm --display <target> [orientation\font\textsize\palette\statusbar] [value]`Show or change the volatile terminal profile of a named display target.
`setterm orientation [0\90\180\270]`Show or set primary-display orientation.
`setterm font [mono\compact]; setterm textsize [10\12\14\16\18\20]`Show or set the terminal font and text size.
`setterm palette [normal\inverted]`Show or set the logical terminal and shared-graphics palette.
setterm foreground [#RRGGBB]; setterm background [#RRGGBB]Show or set the persistent RGB terminal theme colors.
`setterm statusbar [show\hide]`Show or hide the graphical shell status bar.
setterm brightness [0..100]; setterm backlight [0..100]Show or set display brightness or backlight level.
`setterm profile [vt100\ansi\dumb]; setterm charset [utf8\ascii]`Configure escape sequences and TUI glyph output for the current port shell.
`setterm keyboard [us\de]; setterm powerkey [sleep\suspend]`Show or set the keyboard layout and dedicated KEY action.
`setterm keyrate [off\1..60 [delay-ms]]`Show or set the shared keyboard and button repeat policy.
`setterm ble [default\on\off]`Show or set the BLE preference for the next boot.
`setterm timezone [UTC\UTC+/-offset\Europe/Berlin\POSIX-TZ]`Show or set the timezone used for local time.
`setterm startup [auto\flash\sd]`Show or select the volume containing .shell/startup for the next boot.
setterm otaurl [url]Show or set the OTA metadata URL.

Commands · SolarOS manual

sh

Run a simple SolarOS shell script from storage.

Usage

UsageDescription
sh <file>Run a simple SolarOS shell script from storage.

Commands · SolarOS manual

sleep

Enter explicit light sleep.

Usage

UsageDescription
sleepEnter explicit light sleep.

Commands · SolarOS manual

spi

Show every named SPI bus, or one selected bus.

Usage

UsageDescription
spi [status [bus]]Show every named SPI bus, or one selected bus.
spi xfer <bus> <cs> <mode> <hz> <byte...>Full-duplex transfer over a named SPI bus.
spi read <bus> <cs> <mode> <hz> <len> [fill]Read bytes over a named SPI bus.
spi write <bus> <cs> <mode> <hz> <byte...>Write bytes over a named SPI bus.

Commands · SolarOS manual

sshkey

Show default SSH key status.

Usage

UsageDescription
sshkey [status]Show default SSH key status.
`sshkey gen [-f] [204830724096]`Generate /.ssh/id_rsa and /.ssh/id_rsa.pub.
sshkey pubPrint the default public key.
sshkey rmRemove the default key pair.

Commands · SolarOS manual

status

Print a compact system summary, including the last foreground-app exit code.

Usage

UsageDescription
statusPrint a compact system summary, including the last foreground-app exit code.

Commands · SolarOS manual

stream

List dynamic typed stream endpoints.

Usage

UsageDescription
stream or stream listList dynamic typed stream endpoints.
stream status <id>Show type, direction, provider, format, owner, and counters for one stream.

Commands · SolarOS manual

suspend

Turn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY to resume.

Usage

UsageDescription
suspendTurn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY to resume.

Commands · SolarOS manual

temperature

List temperature providers or read the default or named sensor.

Usage

UsageDescription
`temperature [list\sensor]`List temperature providers or read the default or named sensor.

Commands · SolarOS manual

time

Show or set the local time.

Usage

UsageDescription
time [HH:MM[:SS]]Show or set the local time.

Commands · SolarOS manual

top

Print FreeRTOS task resource information when available.

Usage

UsageDescription
topPrint FreeRTOS task resource information when available.

Commands · SolarOS manual

uart

Show the default uart0 or a selected named UART bus.

Usage

UsageDescription
uart [status [bus]]Show the default uart0 or a selected named UART bus.
uart baud [bus] [rate]Show or set a named UART bus baud rate.
`uart mode [bus] [raw\line]`Show or set a named UART bus service mode.
uart write [bus] <text>Write text through the default or selected named UART bus.
uart read [bus] [ms]Read bytes from the default or selected named UART bus.

Commands · SolarOS manual

unzip

List or extract a ZIP archive.

Usage

UsageDescription
unzip [-l] <archive.zip> [dest]List or extract a ZIP archive.

Commands · SolarOS manual

uptime

Print elapsed time since boot.

Usage

UsageDescription
uptimePrint elapsed time since boot.

Commands · SolarOS manual

version

Print the SolarOS version and firmware flavor.

Usage

UsageDescription
versionPrint the SolarOS version and firmware flavor.

Commands · SolarOS manual

wait

Pause the calling shell or shell script for 0 through 86400 seconds.

Usage

UsageDescription
wait <seconds>Pause the calling shell or shell script for 0 through 86400 seconds.

Commands · SolarOS manual

watch

Repeat another shell command until Esc, q, or the app-exit key is pressed.

Usage

UsageDescription
watch [-n seconds] <command> [args...]Repeat another shell command until Esc, q, or the app-exit key is pressed.

Commands · SolarOS manual

wifi

Open the Wi-Fi display TUI when launched from the display shell.

Usage

UsageDescription
wifiOpen the Wi-Fi display TUI when launched from the display shell.
wifi statusShow station/AP/NAT state and the current/next boot setting.
wifi enableSave Wi-Fi enabled for the next boot. The current boot is unchanged.
wifi disableSave Wi-Fi disabled for the next boot. The current boot is unchanged.
wifi onStart Wi-Fi station mode and connect to remembered networks.
wifi offStop station/AP networking; an active ESP-NOW lease retains the radio.
wifi scanScan access points.
wifi connect [ssid [password]]Connect and save/update a station profile.
wifi disconnectDisconnect station mode.
wifi knownList remembered station profiles.
`wifi forget [ssidall]`Remove one or all remembered station profiles.
wifi repeaterShow L2 IPv4 repeater state, upstream, downstream, learned clients, and forwarding counters.
wifi repeater onRepeat the current or preferred saved network with the same SSID and password on the same IPv4 subnet.
wifi repeater offStop L2 forwarding and the downstream AP while retaining the upstream station.
wifi ap [status]Show SoftAP status.
`wifi ap on [ssid [password [openwpawpa2wpa/wpa2]]]`Start and save SoftAP settings.
wifi ap offStop SoftAP.
`wifi nat [statusonoff]`Advanced control for IPv4 NAT on the SoftAP interface. Prefer network router.

Commands · SolarOS manual

wireguard

Show profile, tunnel, selected underlay, route, peer, DNS, and kill-switch state without printing key material.

Usage

UsageDescription
wireguard [status]Show profile, tunnel, selected underlay, route, peer, DNS, and kill-switch state without printing key material.
wireguard import <file>Validate one standard WireGuard client profile and save it in NVS. The source file is not removed.
wireguard forgetLogically remove the saved profile from NVS. Bring the tunnel down first.
`wireguard up [fail-open\fail-closed]`Request the tunnel and reconnect it when the preferred network underlay changes. The default is fail-closed for a full tunnel and fail-open for a split tunnel.
wireguard downStop the tunnel, remove its routes, restore DNS, and disable its kill switch.

Commands · SolarOS manual

xfer

List supported and reserved transfer protocols.

Usage

UsageDescription
xfer protocolsList supported and reserved transfer protocols.
xfer send <port> <file> --raw [-d ms]Send a file as raw bytes, optionally delaying between chunks.
`xfer recv <port> <file> --raw [--append\--replace] [--idle-ms ms]`Receive raw bytes until the idle timeout and append or replace the destination.
xfer send <port> <file> --zmodemSend a file with ZMODEM.
`xfer recv <port> <file> --zmodem [--append\--replace]`Receive a file with ZMODEM and append or replace the destination.

Commands · SolarOS manual

zip

Create a ZIP archive. -0 stores without compression.

Usage

UsageDescription
`zip [-0] <archive.zip> <pathpattern> [pathpattern...]`Create a ZIP archive. -0 stores without compression.

Applications · SolarOS manual

agent

Native Responses/Chat-Completions LLM client and SolarOS agent control plane. It streams model text directly to the active shell and exposes typed system-status, storage-listing, job-listing, display-discovery, and optional Python/Lua execution tools. agent tools shows risk, policy, and runtime availability.

Configure the full endpoint and model, then ask a question:

agent config endpoint https://api.openai.com/v1/responses
agent config model gpt-model
agent config key api-key
agent config reasoning medium
agent config tools confirm
agent config max-tools 16
agent
agent list
agent resume SLOT
agent ask How much memory is free on this device?
agent script python -c "print(6 * 7)"
agent script lua /script.lua argument

Bare agent and agent new open a new foreground prompt loop. Completed turns are saved, and agent list plus agent resume SLOT restore a selected transcript after leaving the app or rebooting. agent delete SLOT removes one conversation without changing provider credentials. Responses uses its saved provider continuation ID; Chat Completions uses bounded local history. Slots are 1 to 3 on internal flash or 1 to 8 on SD; the oldest is reused when full. agent ask performs one unsaved request but likewise waits for Esc or the app-exit key after completion, so display-shell output is not immediately replaced by the shell screen.

Use agent config key clear for an endpoint that does not require bearer authentication. agent status shows configuration, request counters, HTTP status, reasoning effort, duration, traffic, and internal-RAM/PSRAM measurements from the last request. The API key itself is never printed. A Chat-Completions-compatible endpoint can still be configured explicitly.

The agent permits 16 sequential tool calls by default, followed by a separately reserved final provider turn. agent config max-tools COUNT stores a per-request limit from 1 through 32 in NVS. The final turn advertises no tools after that budget is consumed, so the model concludes from the collected results instead of failing on one more tool request. Output is bounded to 16 KiB, protocol buffers and queues prefer PSRAM, and the foreground worker uses a declared 16 KiB internal stack. Full builds can reuse that worker for bounded Python or Lua source/file execution. The manual script path captures at most 4095 output bytes, has a 30-second deadline, and supports cancellation with Esc or the app-exit key. Model-generated source is capped at 640 bytes and captures 383 output bytes.

The storage registry includes bounded listing, sensitive text-file reads, and text-file replacement. Reads and writes are capped at 3072 bytes and paths below .ssh are unavailable to protect SSH identity files.

The default confirm policy runs read-only tools automatically and shows the exact arguments of sensitive, mutating, disruptive, and script calls before waiting for a local y/N decision. off disables tools, readonly excludes all protected risk classes, and explicit all allows every available tool without prompting. See Native Agent Service for the provider contract and current limits.

Controls:

  • Esc or the app-exit key cancels an active request and exits.
  • Page Up/Page Down scroll terminal output while a request is active.

Applications · SolarOS manual

Native Agent Service

service.agent is the native control plane for connecting SolarOS to remote language models. app.agent is a resumable foreground chat frontend for display and port shells.

The first provider supports both the OpenAI Responses API and compatible Chat Completions endpoints. Configure the complete endpoint URL rather than a provider base URL. Responses is recommended for OpenAI reasoning models:

agent config endpoint https://api.openai.com/v1/responses
agent config model gpt-model
agent config key api-key
agent config reasoning medium
agent config tools confirm
agent config max-tools 16
agent status

The endpoint path selects the wire protocol: URLs ending in /responses use typed Responses events, while other URLs use Chat Completions. Reasoning effort may be none, minimal, low, medium, high, xhigh, or max; support still depends on the selected model. The default is medium.

Responses tool continuations use previous_response_id, so reasoning context is retained without copying reasoning tokens through the device. Responses are stored by the provider for that request chain. Instructions are sent again on the continuation because Responses does not carry previous instructions forward automatically.

For the official OpenAI Chat Completions endpoint, the adapter instead sends reasoning_effort: none so function tools remain compatible with that API. Other Chat-compatible endpoints do not receive this provider-specific field.

Configuration is stored in the agent NVS namespace. The API key is used as a Bearer token and is never returned by status. Use agent config key clear for a local or self-hosted endpoint without authentication, or agent forget to erase all agent configuration.

Start the foreground chat with:

agent
agent new

Enter sends a message, Page Up/Down scrolls the terminal transcript, and Esc or the app-exit key returns to the shell. The prompt uses the configured SolarOS username, agent labels are bold, and the protected bottom status bar shows the latest input, output, and total token counts without adding usage lines to the conversation or scrollback. Narrow displays abbreviate those fields as I, O, and T. Completed turns are stored under .solar/agent/conversations on the preferred persistent filesystem. Use agent list, agent resume SLOT, and agent delete SLOT to manage them. New conversations use slots 1 through 3 on internal flash or 1 through 8 on SD; once full, the oldest slot is replaced atomically. Exiting and launching bare agent again still starts a new conversation; restoring an old one is always explicit.

Each provider adapter declares its resume mode. Responses resumes from the saved provider response ID. Chat Completions rebuilds a bounded request from normalized local user and assistant messages. Restoring the visible transcript streams checked records from storage rather than placing the entire conversation in internal SRAM.

Only completed turns are committed. Each conversation is an independently checksummed file replaced through temporary and backup names, so an interrupted write cannot damage other conversations. With internal flash selected, the store keeps at most three 10 KiB conversations; with SD selected, it keeps at most eight 48 KiB conversations. Oldest conversations are evicted deterministically. Provider, model, title, timestamps, messages, tool summaries, and continuation IDs are stored; the bearer key remains only in NVS.

For a single request, use:

agent ask Describe the current device status.

The one-request frontend stays open after completion so the response remains readable. It is not persisted. Use Esc or the app-exit key to return to the shell.

On full builds, the same 16 KiB foreground worker can run a Python or Lua source string or file through the reusable script-runner contract:

agent script python -c "print(6 * 7)"
agent script lua -c "print(6 * 7)"
agent script python /script.py argument
agent script lua /script.lua argument

This manual path captures output instead of streaming directly from the interpreter. Output is limited to 4095 bytes and execution to 30 seconds. The Esc or the app-exit key cancels a running script. Python and Lua each have a single-owner guard, so a captured run cannot overlap their foreground app or REPL. Exceptions, cancellation, deadlines, truncation, and completion are returned as structured runner status.

The adapter requests server-sent streaming events and emits provider-neutral events for text deltas, usage, tool calls, errors, and completion. Cancellation uses the shared HTTP client's cross-task cancellation path. TLS certificate validation uses the firmware certificate bundle. A stateless local provider turn that contains neither text nor a tool call is retried once; a second empty turn is reported as an invalid response instead of silently returning to the prompt.

Typed tools

agent tools shows the canonical registry, domain and risk metadata, runtime availability, current policy disposition, and any required capability. The registry can be larger than the schema set sent to the provider. Every request starts with only three bootstrap tools:

  • system_status: board ID, SolarOS version, uptime, free and largest internal
  • RAM blocks, and free PSRAM.

  • solaros_reference: search the same package-aware manual source exposed to
  • users by man -k and man TOPIC. Python and Lua manuals have a generated section index, so the tool returns up to three focused, firmware-matched excerpts instead of a vague page summary or an oversized full manual. It accepts exactly one query field combining the language and task, such as {"query":"lua gfx drawing"}. Every result includes mandatory SolarOS coding guidance: use documented constants rather than guessed strings or numbers, discover hardware names, respect package gates, and preserve cleanup patterns. Search considers section names and excerpt text, requires task terms to match, prioritizes the requested language, and can include the counterpart-language excerpt for a mirrored service when that contains the more complete contract. Graphics matches include copyable Python and Lua setup/cleanup skeletons using gfx.WHITE, gfx.BLACK, a verified target, and the language-correct gfx.end() or gfx["end"]().

  • tool_search: find and activate up to five installed tools relevant to an
  • exact task. A later search replaces the previous dynamic selection while retaining the three bootstrap tools, so no provider turn advertises more than eight schemas.

Selected workflow dependencies such as script_run_file may activate on demand when they were not selected for the current request. SolarOS advertises the schema and asks the provider to retry. The first call is not executed and cannot bypass normal confirmation. Other inactive tools still require tool_search, preventing a model from activating an unsuitable operation by guessing its name.

The package- and policy-gated tools discoverable through tool_search are:

  • storage_list: up to 16 file or directory entries for one path, including
  • type and size. Results report when the output was truncated.

  • storage_stat: check one exact relative or absolute path and return whether
  • it exists, its resolved path, type, and size. This is the required operation for file-existence questions. Prompts containing a path-like token such as mandel.py or /config.lua select this schema before fuzzy tool matching.

  • storage_read: read up to 3072 bytes from a text-file path. This is a
  • sensitive read and requires confirmation under the default policy.

  • storage_write: create or replace a text-file path with up to 3072 bytes.
  • This is mutating and requires confirmation by default. Relative storage paths use the invoking shell's current directory; absolute paths retain their normal SolarOS storage semantics. All storage content and editing tools reject paths below .ssh, so the agent cannot inspect or replace the device's SSH identity files.

  • storage_search: search file contents under one file or a bounded directory
  • tree, returning paths, line numbers, and excerpts. It does not search file names or test exact-path existence. It scans at most 32 paths, 64 KiB, and 12 matches per call.

  • storage_read_range: read up to 2048 bytes at a byte offset and return the
  • complete file's SHA-256, next offset, and end-of-file state.

  • storage_patch: apply up to eight ascending, non-overlapping byte-offset
  • edits to a file no larger than 128 KiB. The caller must present the SHA-256 returned by storage_read_range; a stale version returns a structured conflict without changing the file. Successful patches are staged on the same volume; the verified original is retained as a backup until the staged file has acquired the public path. If the volume has no spare allocation unit for staging, replacement uses the PSRAM copy and restores the original on a write failure.

  • jobs_list: read-only workload inspection using the actual centralized task
  • admission policy. An empty name lists jobs; a job name requests one complete record. Results include a point-in-time internal/PSRAM free and largest-block snapshot, the background reserve and task overhead, plus each job's state, generation, ready/running/waiting_for_memory/blocked start disposition, machine-readable reason, last error, declared worker-stack placement, owner, and current resource claims. It never starts or stops a job. Resource claims describe the current invocation, and ready cannot predict undeclared dynamic buffers or argument-selected resources.

  • display_list: registered display targets with their real names, drivers,
  • dimensions, readiness, roles, brightness support, and current owners. The provider must call this before generating code for an attached display and use only a returned ready target.

  • hardware_describe: compiled board identity, capabilities, and installed
  • PSRAM. This is the first check before assuming a peripheral exists.

  • gpio_list: real board GPIO slots, pin policy, availability, claims,
  • configuration, and already-readable levels without configuring a pin.

  • gpio_read: inspect one real board GPIO slot. It returns a level only when
  • the pin is already configured and readable; it never claims or configures the pin as a side effect.

  • buses_list: registered I2C, SPI, UART, MIDI, OneWire, and PS/2 buses with their actual
  • names, pin configuration, readiness, origin, sharing mode, and lease count.

  • network_status: current Wi-Fi station, IP, access-point, signal, channel,
  • and NAT state without changing the provider connection.

  • sensors_read: battery and environmental readings from installed service
  • packages. A family absent from the build is returned as unavailable.

  • script_run_python and script_run_lua: execute a source string through the
  • installed interpreter adapter. Generated scripts have access to the same SolarOS APIs as local scripts, so both tools are classified as disruptive.

  • script_run_file: execute a saved Python or Lua file with up to seven
  • arguments through that same runner, including captured output, structured errors, cancellation, ownership guards, and the 30-second deadline.

Tool policy is NVS-backed and enforced again inside the canonical executor:

PolicyBehavior
offAdvertise and execute no tools.
readonlyAdvertise and execute only read-only tools. Sensitive reads are excluded.
confirmRun read-only tools automatically and require one local confirmation for every sensitive, mutating, or disruptive call. This is the default.
allRun every available tool without confirmation. This must be selected explicitly.

Under confirm, the foreground app prints the exact bounded JSON arguments and waits up to 30 seconds at Allow once? [y/N]. Only y allows that call; n, Enter, or the timeout denies it. Esc or the app-exit key cancels the whole request. A denial is returned to the model as a structured result so it can explain or choose another approach rather than losing the conversation turn. agent status includes executed, denied, and failed tool counters.

The service permits up to 16 sequential tool calls by default and always reserves a separate provider turn for the final response. Set the per-request limit with agent config max-tools COUNT; accepted values are 1 through 32 and the selection is stored in NVS. Once the budget is consumed, the reserved final turn advertises no tools, so the model must conclude from the results already collected instead of failing by requesting another tool. This supports bounded inspect/read/change/test/repair workflows without allowing an unbounded autonomous loop. Unsupported tools, multiple simultaneous tool calls, and malformed arguments still fail the request. Definitions, input/output schemas, availability checks, risk metadata, and executors live in one declarative registry. Only available tools are sent to the provider. Discovery also filters tools denied by the current policy. The agent rejects calls to tools outside the currently advertised set, and every successful executor result is parsed as a JSON object before it is returned to the model.

Resource bounds

  • Foreground worker stack: 16 KiB internal RAM.
  • Stream-line buffer: 24 KiB, PSRAM preferred. Responses completion events
  • contain the assembled output as one event.

  • Request body: 16 KiB, PSRAM preferred.
  • Prompt: 1023 bytes.
  • Tool discovery query: 159 bytes.
  • Tool descriptor buffer: 6 KiB, allocated in PSRAM. At most three bootstrap
  • plus five discovered tool schemas are serialized per provider turn.

  • Tool arguments: 4095 bytes, held in PSRAM for a request.
  • Tool result: 4095 bytes, allocated in PSRAM.
  • API-reference matches: at most three excerpts of at most 900 bytes each per
  • lookup. Generated offsets point into the existing embedded manual body, so the index does not duplicate that body in flash.

  • Manual search: fixed stack storage for at most 12 results. Embedded page text
  • remains in flash. A verified SD override is loaded into PSRAM only while a man page or page-contract fallback is being consumed. Indexed scripting excerpts always use the embedded manual that was built with the firmware.

  • Storage read/write content: 3072 bytes.
  • Storage ranged read: 2048 requested bytes; JSON output may stop earlier when
  • escaping would exhaust the bounded tool result.

  • Storage search: 32 queued paths, 64 KiB scanned text, and 12 matches.
  • Storage patch: 128 KiB file, eight edits, and 2048 inserted bytes per edit;
  • file and edit buffers require PSRAM.

  • Generated script source: 640 bytes.
  • Generated script captured output: 383 bytes.
  • Tool confirmation deadline: 30 seconds.
  • Manual script output: 4095 bytes, allocated in PSRAM.
  • Manual script deadline: 30 seconds.
  • Model output: 16 KiB per provider turn.
  • Provider request body: 32 KiB in PSRAM, including at most 8 KiB of raw local
  • history serialized into a 16 KiB history fragment for Chat Completions.

  • Assistant capture for a completed turn: 16 KiB in PSRAM.
  • Conversation file: 10 KiB on internal flash or 48 KiB on SD; three flash
  • conversations or eight SD conversations are retained.

  • Request deadline: 90 seconds.
  • Per-I/O timeout: 15 seconds.

agent status records tool calls used versus the last request's configured budget, internal free memory, the lowest sample observed during HTTP streaming, largest internal blocks, and PSRAM before and at request completion. The completion sample still includes the foreground worker stack; run mem policy after the app returns to confirm that the task stack was reclaimed.

The agent package requires Wi-Fi and PSRAM but does not require Python or Lua. The manual command and model tool are advertised only when the corresponding runtime is in the firmware; the agent app supplies the optional adapter callback without making either interpreter a package dependency. Larger-file editing, mutating job/network/hardware operations, additional providers, remote interfaces, and scheduling are later phases.

Quick reference

The native agent uses the configured OpenAI-compatible Responses endpoint, applies the selected confirmation policy, and exposes bounded typed tools for installed SolarOS features. Completed chat turns are stored locally; agent list, agent resume SLOT, and agent delete SLOT manage them. Responses uses a provider continuation ID and Chat Completions uses bounded local history. agent status reports provider, policy, requests, tool activity, and memory. This page defines the tool contract and resource limits; man agent is the task-oriented usage guide.

Applications · SolarOS manual

aplay

Play audio files through the default registered playback endpoint. WAV and MP3 are supported when an output device is present. MP3 decoding is provided by the shared, device-independent audio codec service. aplay prints the source details, plays the file once through the shared background audio player, and then returns to the prompt without clearing existing terminal output. It can be launched from display, UART, USB CDC, Telnet, and other port shells.

Usage:

aplay [-v volume] file.wav|file.mp3

Controls:

  • Esc or app-exit stops playback early and returns to the prompt.

Applications · SolarOS manual

SolarOS Embedded Apps

This document covers foreground applications registered in SolarOS. Availability depends on the selected firmware flavor and board capabilities. The apps command shows the app set compiled into the running image.

Exit behavior:

  • Display shell: CTRL+ALT+DEL exits foreground apps.
  • Port shells: Ctrl+] exits foreground apps.
  • Application screens are private presentation state and are discarded when
  • the application exits. Text written to the application output channel is preserved in the launching shell before its next prompt; Python and Lua use this channel for stdout, errors, and tracebacks. The normal terminal scrollback limit still applies.

  • Every foreground application returns an exit code to its launching shell and
  • can return an optional message. Normal exits use code 0 and stay silent; failures use a nonzero code and a useful message. status shows the most recent foreground-application exit code.

  • The runtime assigns every launchable foreground application to one lifecycle
  • class. Commands preserve their sequential text transcript. TUI applications discard their screen buffer. GUI applications discard their framebuffer. Hybrid applications such as python, lua, calc, and webradio select the effective class from the mode that was launched.

  • A fatal startup or runtime error is an exit outcome, not an application
  • screen. The runtime closes the application immediately, restores the launching shell, prints the diagnostic there, and does not wait for an acknowledgement key.

  • Port shells: Ctrl+Z suspends a resumable app and returns to the prompt;
  • fg restores the most recently suspended app.

  • Alt+Tab or Alt+Right switches to the next resumable foreground session on
  • the locally focused display. Alt+Left switches to the previous session. Either Alt key is accepted, including AltGr on compact keyboards. Switching back restores the retained terminal or graphics frame, including Python and Lua application screens.

  • Alt+Enter toggles full-screen mode in applications that use the shared TUI.
  • Either Alt key is accepted, including AltGr; on the CL-32 keyboard, use File+OK. Full-screen mode hides the system status bar and persistent TUI footer rows without changing the saved terminal profile. TUIs start in this mode automatically when ten or fewer terminal rows are available. Input rows remain visible, and status or error feedback can temporarily cover the last content row.

agent

Native Responses/Chat-Completions LLM client and SolarOS agent control plane. It streams model text directly to the active shell and exposes typed system-status, storage-listing, job-listing, display-discovery, and optional Python/Lua execution tools. agent tools shows risk, policy, and runtime availability.

Configure the full endpoint and model, then ask a question:

agent config endpoint https://api.openai.com/v1/responses
agent config model gpt-model
agent config key api-key
agent config reasoning medium
agent config tools confirm
agent config max-tools 16
agent
agent list
agent resume SLOT
agent ask How much memory is free on this device?
agent script python -c "print(6 * 7)"
agent script lua /script.lua argument

Bare agent and agent new open a new foreground prompt loop. Completed turns are saved, and agent list plus agent resume SLOT restore a selected transcript after leaving the app or rebooting. agent delete SLOT removes one conversation without changing provider credentials. Responses uses its saved provider continuation ID; Chat Completions uses bounded local history. Slots are 1 to 3 on internal flash or 1 to 8 on SD; the oldest is reused when full. agent ask performs one unsaved request but likewise waits for Esc or the app-exit key after completion, so display-shell output is not immediately replaced by the shell screen.

Use agent config key clear for an endpoint that does not require bearer authentication. agent status shows configuration, request counters, HTTP status, reasoning effort, duration, traffic, and internal-RAM/PSRAM measurements from the last request. The API key itself is never printed. A Chat-Completions-compatible endpoint can still be configured explicitly.

The agent permits 16 sequential tool calls by default, followed by a separately reserved final provider turn. agent config max-tools COUNT stores a per-request limit from 1 through 32 in NVS. The final turn advertises no tools after that budget is consumed, so the model concludes from the collected results instead of failing on one more tool request. Output is bounded to 16 KiB, protocol buffers and queues prefer PSRAM, and the foreground worker uses a declared 16 KiB internal stack. Full builds can reuse that worker for bounded Python or Lua source/file execution. The manual script path captures at most 4095 output bytes, has a 30-second deadline, and supports cancellation with Esc or the app-exit key. Model-generated source is capped at 640 bytes and captures 383 output bytes.

The storage registry includes bounded listing, sensitive text-file reads, and text-file replacement. Reads and writes are capped at 3072 bytes and paths below .ssh are unavailable to protect SSH identity files.

The default confirm policy runs read-only tools automatically and shows the exact arguments of sensitive, mutating, disruptive, and script calls before waiting for a local y/N decision. off disables tools, readonly excludes all protected risk classes, and explicit all allows every available tool without prompting. See Native Agent Service for the provider contract and current limits.

Controls:

  • Esc or the app-exit key cancels an active request and exits.
  • Page Up/Page Down scroll terminal output while a request is active.

aplay

Play audio files through the default registered playback endpoint. WAV and MP3 are supported when an output device is present. MP3 decoding is provided by the shared, device-independent audio codec service. aplay prints the source details, plays the file once through the shared background audio player, and then returns to the prompt without clearing existing terminal output. It can be launched from display, UART, USB CDC, Telnet, and other port shells.

Usage:

aplay [-v volume] file.wav|file.mp3

Controls:

  • Esc or app-exit stops playback early and returns to the prompt.

arecord

Record a registered capture endpoint to a WAV file. The default is the first compatible input; -i selects a specific stream such as adc0.capture. This requires a registered input device, but not built-in board audio. With -d, recording stops after the specified number of seconds. Without -d, recording continues until app-exit, the storage fills, or the WAV size limit is reached. It can be launched from display, UART, USB CDC, Telnet, and other port shells.

Usage:

arecord [-d seconds] [-i capture-stream] file.wav

Controls:

  • App-exit stops recording, finalizes the WAV header, and returns to the prompt.

recorder

Interactive GUI/TUI counterpart to arecord. Recorder writes PCM WAV files so the channel count, sample rate, and resolution travel with the recording and the result can be played immediately. It accepts any registered signed-16-bit PCM capture stream. On the Waveshare board it initially selects audio0.capture. Mono/stereo output, 8/16-bit file resolution, and sample rates from 8 kHz through 48 kHz are converted from the selected stream as necessary.

Usage:

recorder [--tui] [file.wav]

The optional path supplies the initial recording folder and filename. The Setup view can edit the filename or select another destination directory with the shared file browser. When the filename is empty, each recording gets a unique local-time name such as rec-260810-113045.wav; a numeric suffix avoids overwriting a file created in the same second. Recorder remembers its selected input, folder, format, hardware input gain, output volume, and visualizer in .recorder/settings.bin on the current storage root. An explicit path argument overrides the remembered folder. The filename is not remembered, so reopening Recorder cannot accidentally reuse a one-off name.

Recorder selects its graphical interface on a graphical shell and its text interface on a port shell. --tui forces the text interface even when the launching shell has graphics.

Tab switches between Record and Setup. The Record view uses the shared cassette widget by default; V cycles through Cassette, Oscilloscope, and Spectrum. R starts recording, Space pauses or resumes, S stops and finalizes the WAV header, and P plays the most recent or selected recording. M starts or stops monitoring: it continuously streams the selected input to the default output and visualizer. Monitoring can remain enabled when recording starts and can be toggled while the WAV writer continues. Up/Down changes monitor and playback volume. The cassette reels move during recording and playback, but remain still during monitoring. Oscilloscope and Spectrum remain live during monitoring. Active visualizers are presented on the 40 ms app tick (nominally 25 frames per second), independently of the audio block rate.

Setup selects the capture stream, recording folder, mono/stereo conversion, sample rate, 8/16-bit WAV resolution, input gain, and output volume. Enter on Folder opens the WAV-filtered browser. D selects its current directory as the recording folder; Enter on a WAV file plays it and returns to Record. Input gain is not a generic stream multiplier: Recorder enables it only when the selected capture stream belongs to an audio device that advertises hardware input-gain control. Thus audio0.capture on the Waveshare controls the ES7210 microphone gain, while an unrelated generic stream shows Input gain: n/a. The hardware gain is system-wide; Recorder does not add hidden digital amplification to the saved PCM.

The text interface exposes the same setup values, browser, transport keys, monitoring, pause behavior, and playback path. Recorder is resumable: capture, monitoring, or playback continues while its UI session is in the background, and closing the app stops the worker and finalizes an active recording.

funcgen

Audio-only function generator built on the shared real-time Synth service. It emits signed 16-bit stereo PCM and can use the default playback device or an explicitly selected runtime playback stream, including an attached LEDC PWM audio expansion.

Usage:

funcgen [--tui]

The graphical interface shows the exact generated PCM in the shared oscilloscope widget. The remaining controls select sine, square, triangle, saw, pulse, or noise output; frequency from 20 through 8000 Hz; amplitude; pulse width; a repeating linear sweep; sweep end frequency; sweep time; and playback stream. The TUI exposes the same controls. --tui forces it on a graphical shell.

Controls:

  • Left/Right selects a control.
  • Up/Down, +/-, or Enter adjusts the selected control.
  • Space starts or stops output.
  • Esc, Q, or the app-exit key closes the application.

The output is off initially. Frequency is capped below the selected stream's Nyquist limit during rendering. A sweep moves linearly from Frequency to Sweep end during Sweep time, then repeats without resetting oscillator phase. Suspending the UI leaves an active generator running; closing it stops the Synth worker and releases the stream.

All controls can be targeted by the shared control-binding system while Funcgen is active:

  • funcgen.waveform: 0 sine, 1 square, 2 triangle, 3 saw, 4 pulse, 5 noise.
  • funcgen.frequency: 20 through 8000 Hz, logarithmic.
  • funcgen.amplitude: 0 through 100 percent.
  • funcgen.pulse.width: 1 through 99 percent.
  • funcgen.sweep.enabled: 0 off or 1 on.
  • funcgen.sweep.end: 20 through 8000 Hz, logarithmic.
  • funcgen.sweep.time: 100 through 60000 ms, logarithmic.
  • funcgen.output: runtime output index; 0 follows the default output.
  • funcgen.enabled: 0 off or 1 on.

The durable foreground state and oscilloscope storage use PSRAM when present. Only the bounded oscillator/render state, Synth worker stack, and PCM block remain in internal SRAM.

player

Interactive WAV/MP3 player and the user-facing counterpart to aplay. player keeps a persistent playlist under .player on the current storage root. Opening an audio file from Files adds it to that playlist, selects it, and starts playback. Missing files remain listed so removable media can be reattached.

Usage:

player [--tui] [file.wav|file.mp3]

Player selects its graphical interface on a graphical shell and its text interface on a port shell. --tui forces the text interface even when the launching shell has graphics.

On a graphical session, Tab switches between Play and Playlist. The Play tab uses the top two-thirds for a cassette visualizer by default; V cycles through Cassette, Oscilloscope, and Spectrum. The cassette reels turn only while audio plays and show track progress when duration is known. Left/Right plays the previous or next track in the playlist ring, Enter plays or stops, Space pauses or resumes, and Up/Down adjusts volume. On the Playlist tab, Up/Down selects, Enter starts the track and returns to Play, A opens the WAV/MP3 file browser, and Delete removes the selected playlist entry.

The text interface is one playlist screen: Up/Down selects, Enter plays or stops, Space pauses or resumes, A opens the filtered file browser, Delete removes an entry, and Esc exits. Its bottom status line shows the playing, paused, or stopped state with elapsed and total time. Playback follows the resumable app while another foreground session is selected and stops when Player closes. End of file advances to the next playlist entry.

WAV playback converts the file's mono/stereo channel count and sample rate to the selected output stream. A mono recording therefore plays through a fixed stereo device without changing the recording stored on disk.

calc

Scientific calculator and function plotter. On a graphical display, calc opens an expression list beside a Cartesian plot. From UART, USB CDC, Telnet, or any other text-only shell, the same command opens a scientific REPL without the plot pane. calc --tui forces that REPL even when graphics are available.

The expression engine supports +, -, *, /, %, powers with ^, parentheses, scientific notation, and implicit multiplication such as 2pi or 2(x + 1). Built-ins include sin, cos, tan, their inverse functions, sqrt, abs, exp, ln, log, floor, ceil, round, min, max, pow, and atan2. Trigonometric input is in radians; rad(degrees) and deg(radians) convert explicitly. Constants are pi and e.

Rows can hold scalar calculations, variables, one-argument functions, or plots:

a = 2
f(x) = sin(x) / x
f(pi / a)
y = f(x)

For a one-off shell calculation, use:

calc -e "sqrt(2)^2"

Text REPL commands:

  • :list or :vars shows worksheet rows and scalar results.
  • :del N removes a row; :clear empties the worksheet.
  • :save [file] and :load [file] write or restore one expression per line.
  • The default file is calc.txt in the current shell directory.

  • :help shows the compact reference; :quit returns to the shell.
  • Up/Down recalls input history; Left/Right, Home, End, Backspace, and Delete
  • edit the current line.

Graphical controls:

  • The expression editor keeps a white background for legible small text and
  • outlines the active row with a thin dark border.

  • Type to edit the selected row; Up/Down changes rows and Enter adds a row.
  • Left/Right, Home, End, Backspace, and Delete edit within a row.
  • Page Up toggles the selected graph row.
  • Tab moves between the expression list and graph.
  • In the graph, arrows pan, +/- zoom, and Home or 0 resets the view.
  • Press t for a numeric trace cursor; Left/Right moves it and Up/Down selects another plotted row. Press t again to resume panning.

  • Ctrl+S saves and Ctrl+O loads the default calc.txt worksheet.
  • App-exit closes the calculator. The worksheet is kept only when explicitly
  • saved.

chat

Tabbed provider-neutral conversation client. The Channels tab lists gateway and radio conversations. Enter selects a conversation and opens its bounded shared history on the Chat tab, which also contains the message/command input. The app opens and remains useful offline; network or radio transport jobs connect independently.

Usage:

chat [gateway|meshcore|link|conversation-id]

With no selector, Chat opens a unified view and initially selects the newest unread conversation. A provider name filters the list. A decimal conversation ID opens exactly that conversation.

The background gateway-sync job owns the gateway transport connection, retries, and joined-channel replay. Each join sends the greatest stable message ID retained for that room as its cursor; the gateway replays only newer messages. Requested membership and server-confirmed membership are separate, connection-scoped states. The shared messaging service owns queued outbound messages and gateway-sync consumes only gateway requests. Start it explicitly with job start gateway-sync, just like email-sync. Closing or suspending chat does not disconnect an already-running synchronizer. Incoming messages remain in the shared bounded messaging store and publish bounded notifications to the universal inbox; reopening the app replays retained conversations from every provider. With SD storage, full messages are retained under /.messages/messages.bin. On internal flash, Chat restores the compact message copy already retained in /.inbox/messages.bin, so it consumes no second flash ring. Both backends deduplicate transport replays by stable message identity and keep linked Inbox read state aligned.

Unlike email-sync, gateway-sync takes no interval argument: it waits for Wi-Fi and reconnects with exponential backoff while remaining in the running state.

Gateway setup and room lifecycle use gateway status, gateway configure, gateway connect, gateway disconnect, gateway rooms, gateway join, gateway leave, and gateway delete. Gateway synchronization runs only under the gateway-sync job name. Selecting a known gateway room in the Channels tab requests a join automatically and opens it after the server confirms joined.

Conversation rows show provider, unread, and security state. Outbound rows show queued/sending/sent/delivered/failed state. Use /new CONTACT_ID to open a direct conversation with the contact's preferred endpoint. Sending to a discovered endpoint asks for a second Enter confirmation; blocked endpoints cannot be messaged.

The conversation header reports the selected provider's state. Gateway uses disconnected, connecting, and connected; connectionless MeshCore and Link providers use stopped, starting, ready, and error.

In-app commands:

/help
/new contact-id
/status
/quit

Controls:

  • Tab switches between the Channels and Chat tabs.
  • In Channels, Up/Down select a conversation and Enter joins a known
  • gateway room if necessary, then opens the Chat tab.

  • In Chat, Up/Down navigate input history and Enter sends input.
  • Page Up/Page Down scroll messages.
  • Esc or app-exit key exits.

clock

Full-screen graphical seven-segment clock, alarm countdown, and stopwatch. The countdown is serviced in the background, so it continues when the Clock session is suspended. It works without RTC hardware while SolarOS remains powered. With a wired RTC interrupt, the RTC alarm or countdown is also armed and explicit light sleep can wake for it.

Usage:

clock
clock -a mm:ss
clock -s

Controls:

  • In stopwatch mode, Space starts/stops.
  • In stopwatch mode, any other ordinary key resets to zero.
  • Exiting a countdown removes that transient alarm and stops it if ringing.
  • Esc or app-exit key exits.

com

Serial terminal for a bidirectional byte-stream port. Display-keyboard or port-shell input is forwarded to the selected port, and received bytes are drawn in the active terminal. The port may be a UART or a virtual port such as a peer-bound SolarOS Link stream.

Usage:

com [--autobaud] [--hex] [port]

The port defaults to uart0. For example, com gps connects to an existing runtime UART bus named gps, while com vser0 opens a Link stream previously created with link stream create. The selected port remains claimed by the app until the session exits. com works from both display and port shells; when launched from a port shell, its terminal output is returned through that same port. The input/output shell port and selected COM port must be different.

--autobaud is available only for UART buses. It samples the RX signal for three seconds before opening the terminal. Send a repeating 0x55 or 0xaa pattern during that interval. A reliable measurement is matched to a standard UART rate and applied to the current bus connection without overwriting the saved baud setting. UART input forwarding begins when sampling completes. If measurement fails, the configured rate is kept.

--hex displays received bytes as eight-byte offset, hexadecimal, and ASCII rows instead of interpreting them as terminal text. Both options can be used together.

Examples:

com --hex gps
com --autobaud --hex uart0
com vser0

Controls:

  • App-exit key exits.

curl

HTTP client for quick text downloads and diagnostics. It can print response data to the terminal or save it to a file.

Usage:

curl [-L] [-o file] URL

Controls:

  • App-exit key cancels an active transfer.

webradio

Stream a direct MP3 URL through the default registered audio output. On a graphical display shell, WebRadio opens a two-tab media-player GUI. On UART, USB CDC, Telnet, SSH, and other text shells, it opens a station-list TUI.

The catalog is stored as .solar/webradio/catalog.bin on the current storage root and starts with the Nightride, Chillsynth, Datawave, Spacesynth, Darksynth, Horrorsynth, EBSM, and Rekt streams. Catalog changes survive reboot. On first use after an upgrade, WebRadio moves an older NVS catalog to this file and removes the large NVS blob. reset restores the initial list.

Usage:

webradio
webradio --tui
webradio https://stream.nightride.fm/nightride.mp3
webradio list
webradio add MyStation https://example.net/live.mp3
webradio remove MyStation
webradio reset

--tui forces the station-list TUI even on a graphical shell and can be combined with a direct stream URL. Catalog-management commands also accept it as a harmless interface override.

URLs are literal HTTP or HTTPS MP3 stream URLs. WebRadio does not translate station names or website addresses and does not discover streams from HTML pages. The initial implementation does not support playlists, HLS, or AAC.

Controls:

  • Tab switches between the Player and Channels tabs in the GUI.
  • The Player tab gives the top two-thirds of the screen to a live PCM
  • oscilloscope or spectrum analyzer. V switches visualizers. The spectrum analyzer uses the shared DSP service, including PIE SIMD window and FFT paths on eligible ESP32-S3 boards.

  • On the Player tab, Left and Right play the previous or next catalog
  • channel. The catalog wraps as a ring. Space or Enter stops or resumes playback, and Up/Down changes global volume in five-percent steps.

  • The bottom third of the Player tab shows the channel, playback state, volume
  • bar, and previous, stop/play, and next controls.

  • On the Channels tab, Up/Down selects a channel, A adds one, E edits
  • one, and Delete removes one. Enter starts the selected channel and returns to the Player tab. Add and edit dialogs accept a name followed by a literal stream URL.

  • The TUI is a single catalog screen. Up/Down or J/K selects a channel,
  • +/- changes output volume, A adds a channel, E edits it, and Delete removes it. Enter plays the selected channel, Space stops playback, and R reconnects it. Add and edit use inline name and URL fields on the catalog screen; Enter advances or saves and Esc cancels.

  • Q, Esc, or the app-exit key exits when no catalog dialog is open.

The app is available on Wi-Fi builds even when the board has no built-in audio hardware. Playback starts when a default output device has been registered, including an output supplied by a runtime-attached expansion.

Network reception and MP3 decoding feed an app-owned PCM jitter buffer. A separate playback worker consumes that buffer at the audio device's steady rate. Both workers continue while the resumable WebRadio session is suspended or another foreground app is selected. Closing WebRadio cancels the network operation through its bounded read timeout, stops both workers, and releases the audio device. The network worker exclusively owns the HTTP request during that shutdown.

The playback worker, not the decoder, publishes visualizer samples after each audio write. The widgets retain only the latest played block and may drop intermediate display frames, so buffering does not put the visualization ahead of the audio. WebRadio uses 256 scope samples and a 256-point SIMD-eligible FFT, grouped into 32 displayed frequency bands.

service.signal-widgets provides the reusable thread-safe signed-16-bit oscilloscope and spectrum components used by the graphical player. The widgets accept mono or interleaved multichannel PCM and own their snapshot storage; applications retain ownership of their audio streams.

help

Foreground browser for the package-aware SolarOS manual. The foldable tree groups the topics compiled for the current firmware and shows whether it is using the embedded copy or a verified downloaded revision. All groups start folded. The selection, scroll position, and fold state remain unchanged after a topic closes.

Usage:

help
help agent

Controls:

  • Up/Down, Page Up/Page Down, Home/End: move through the tree.
  • Left/Right, Enter, or Space on a group: fold or unfold it.
  • Enter or Right on a topic: open it in reader on graphic display shells
  • or less on text shells.

  • q, Esc, or the app-exit key: return to the shell.

The maintenance forms help status, help update, and help reset remain shell operations. SD-capable builds show terminal-width-aware progress while downloading and extracting one exact-version signed manual archive. Use help command.status to open the shell status command page.

edit

Text editor for files on mounted storage. It supports cursor navigation, selection, clipboard operations, text-size changes, and syntax highlighting for known source files. The editor supports files up to 256 KiB on boards with PSRAM and 32 KiB on boards without PSRAM. Use hexedit for binary files.

Usage:

edit <file>

Controls:

  • Arrows move the cursor.
  • Ctrl+Left/Ctrl+Right move by words.
  • Shift+Arrows extend selection.
  • Ctrl+A, Ctrl+C, Ctrl+X, Ctrl+V select all, copy, cut, and paste.
  • Ctrl+F or F3 opens Find. Matches are case-insensitive and wrap at the end
  • of the file.

  • Ctrl+S or F2 saves in place. Ctrl+Q, Esc, F10, or the app-exit key
  • exits. When the file has pending changes, answer Y to save and exit or N to discard the changes and exit.

  • Ctrl++ and Ctrl+- adjust editor text size for the active session.

hexedit

Two-pane binary editor for files on mounted storage. Each row shows a file offset, hexadecimal bytes, and their synchronized printable ASCII view. The number of bytes per row adapts to the terminal width. It uses the same 256 KiB PSRAM and 32 KiB internal-memory limits as edit.

Usage:

hexedit <file>

Controls:

  • Tab switches input focus between the HEX and ASCII panes. The corresponding
  • byte remains highlighted in both panes.

  • Hexadecimal digits replace the active high or low nibble in the HEX pane.
  • Printable characters replace the active byte in the ASCII pane.

  • Arrows, Page Up/Page Down, Home/End, and Ctrl+Home/Ctrl+End navigate by byte,
  • row, page, or file.

  • Shift with navigation extends a byte selection. Ctrl+A, Ctrl+C,
  • Ctrl+X, and Ctrl+V select all, copy, cut, and paste binary data.

  • Backspace and Delete remove bytes. Typing at end of file appends data.
  • Ctrl+S saves in place. Ctrl+Q, Esc, or the app-exit key exits without
  • saving pending changes.

  • Ctrl++ and Ctrl+- adjust editor text size for the active session.

launcher

Configurable native graphical launcher for display shells. It draws the configured grid, centers one icon in each occupied cell, and shows the selected icon larger with its title centered underneath. Arrow keys move between occupied cells, Enter opens the selection, and a pointer selects and opens items by point and click. Esc or the app-exit key returns to the shell.

Usage:

launcher [config.json]

Without an argument, Launcher reads launcher.json from the active storage root. The first run creates a usable default file there. A supplied path selects another configuration. Launcher reloads the file whenever a child application returns, so editing the configuration through a launcher item takes effect immediately.

The file is JSON. layout.columns and layout.rows define a grid from 1 by 1 through 8 by 8. Each item has a displayed name, an Open Iconic icon name, a SolarOS shell command, and a zero-based column and row. A cell can contain at most one item, and a configuration can contain up to 32 items:

{
  "layout": {"columns": 3, "rows": 2},
  "items": [
    {"name": "Files", "icon": "folder", "command": "files", "column": 0, "row": 0},
    {"name": "Manual", "icon": "book", "command": "help", "column": 1, "row": 0},
    {"name": "Wi-Fi", "icon": "wifi", "command": "wifi", "column": 2, "row": 0},
    {"name": "Sketch", "icon": "brush", "command": "sketch", "column": 0, "row": 1}
  ]
}

Icon names are the lowercase, hyphenated Open Iconic names, such as book, browser, brush, calculator, clock, cog, document, folder, home, musical-note, pencil, tablet, terminal, and wifi. Legacy numeric IDs from 0 through 222 remain accepted for existing configurations.

Commands use the normal SolarOS shell parser, so arguments, aliases, scripts, and application availability checks behave exactly as at the prompt. A launched foreground application returns to Launcher when it closes. A command that does not open an application returns to the shell so its output remains visible.

files

File manager inspired by Midnight Commander. Its normal mode provides two panes for copy, move, delete, and launch workflows on mounted storage. Launcher mode provides a minimal single-pane application menu suitable for a startup script.

Usage:

files [--launcher] [path]

files --launcher hides dot-prefixed entries, removes the message and function key bars, and expands one pane to the bottom of the terminal. Launcher mode is read-only: navigate with the arrows, open with Enter, refresh with r, move to the parent with Backspace, and exit with q or the app-exit key. To make a directory the startup menu, add a line such as this to /.shell/startup:

files --launcher /apps

File associations come from the installed app registry. Only apps compiled in the active firmware can be selected. Associations include images to view, WAV/MP3 to player, CSV to sheet, Python and Lua scripts to their runtimes, documents to reader (or writer when Reader is unavailable), and .gb ROMs to gameboy. Unknown files fall back to less or edit. A .sh file runs through the built-in SolarOS shell. In launcher mode, documents associated with Reader open as reader --pager for row-aware page navigation.

Controls:

  • Arrows navigate panes.
  • Tab switches active pane.
  • Enter opens directories or launches known files.
  • Returning to a parent directory restores the cursor to the directory that
  • was just exited.

  • F3/V, F4/E, F5/C, F6/M, F7/K, F8/D, and F9/Z
  • view, edit, copy, move, create a directory, delete, and create a zip archive. The function keys remain the primary map; the help bar shows the letter mnemonics within the action names.

  • Copy, move, and delete operate on the current or marked entries.
  • Copy and recursive delete scan their source trees first, then show measured progress with the current entry. Move shows progress across the selected top-level entries.

  • Esc cancels an active copy, move, or delete operation and keeps Files open.
  • A partial current-file copy is removed; top-level items completed before the cancellation remain copied, moved, or deleted.

  • File operations refresh both panes after completion.
  • App-exit key exits.

ftp

Two-pane FTP file manager. The left pane is local mounted storage. The right pane is a remote FTP server. The app uses unencrypted IPv4 FTP and passive data connections.

Usage:

ftp HOST [PORT] [--user USER --password PASSWORD] [--remote PATH] [--local PATH]

Anonymous login is the default (anonymous with solaros@ as its password). Specify both --user and --password for an authenticated server. Passwords are plaintext on the network and remain in shell history when entered on the command line. Use FTP only on a trusted network.

Controls:

  • Arrows navigate the active pane. Tab, Left, and Right switch panes.
  • Enter opens a directory. r refreshes the remote and local listings.
  • F3 views a local file. For a remote file, FTP downloads a temporary hidden
  • copy into the current local folder, opens its registered viewer, and removes the copy when control returns to FTP.

  • F5 copies the current file or directory to the other pane. Directory
  • transfers are recursive. Copy and move show the same preparing/transfer progress popup used by Files.

  • F6 moves the current file or directory to the other pane by completing the
  • copy before deleting the source.

  • F7 creates a directory in the active pane.
  • F8 recursively deletes the current item after confirmation.
  • q, F10, or the app-exit key closes the connection and exits.
  • After an operation refreshes the current directories, both panes retain their
  • cursor and scroll positions. Opening another directory starts at its top.

FTP is a resumable TUI on the display shell and VT100-compatible port shells. One operation runs at a time in its foreground worker. Existing destination files are replaced. A failed download removes its staging file and preserves an existing destination; a move does not delete its source unless the complete copy succeeds.

flash

Download verified SolarOS factory artifacts to SD and program another supported ESP board over UART. The browser refreshes the signed catalog on request and shows which board, flavor, and version artifacts are already cached. Its tree starts folded and retains its selection and fold state after operations. Delete removes a selected cached artifact after confirmation. The shell form accepts a named UART plus optional boot and reset GPIO pins.

flash
flash refresh
flash list
flash download BOARD FLAVOR [VERSION]
flash BOARD FLAVOR [version=VERSION] [port=uart0] [boot=PIN] [reset=PIN] [baud=RATE]

See Flash another ESP board for wiring, target-selection, security, storage, and verification details.

contacts

Provider-neutral address book for gateway and MeshCore identities. Contacts can carry multiple provider endpoints while retaining trust independently for each endpoint. A signed MeshCore advert creates a discovered endpoint: the signature proves possession of the advertised key, not the human identity behind it.

Open the searchable TUI:

contacts

The list is grouped by trust and provider. Press / to search, Enter to view the selected endpoint addresses, capabilities, last-seen times, and bounded provider metadata summary, and Esc or q to leave.

Use the shell surface for mutations:

contacts status
contacts list [all|discovered|trusted|blocked]
contacts show CONTACT_ID
contacts rename CONTACT_ID NAME
contacts trust CONTACT_ID [ENDPOINT_ID]
contacts block CONTACT_ID [ENDPOINT_ID]
contacts remove CONTACT_ID
contacts link TARGET_CONTACT_ID SOURCE_CONTACT_ID

Contact and endpoint identifiers autocomplete from live service snapshots. Linking moves the source endpoints to the target contact and removes the source record. When the 64-contact store is full, SolarOS may evict the oldest unpinned contact whose endpoints are all still discovered; trusted and blocked records are never automatically evicted.

The versioned store is CRC checked, uses two alternating headers and data copies, remains below 24 KiB, and normally lives at /.contacts/contacts.bin. If storage is unavailable, Contacts remains usable in volatile mode and contacts status reports the storage error.

inbox

Universal incoming-message browser for pages, chat notifications, mail, and other background producers. It reads the same shared inbox that supplies the status-bar unread count. Messages and read state survive reboot in the bounded /.inbox/messages.bin store; the service retains at most 64 entries and keeps the file below 32 KB even when internal flash is the only storage.

Each list item occupies exactly one terminal row: unread/priority markers, local reception date and time, a compact source (chat/general, email, or pocsag), and as much of the message body preview as fits the screen.

Notification sound is enabled by default on boards with audio. Press s in the Inbox or use inbox notify on, inbox notify off, or inbox notify test; the setting is persistent. Notification tones remain active while SolarOS is suspended.

Usage:

inbox

Controls:

  • Up/Down, Page Up/Page Down, Home, and End navigate.
  • Enter or Right opens the selected message and marks it read.
  • Left, Backspace, or Esc returns from a message to the list.
  • u toggles the unread-only filter.
  • m toggles the selected message between read and unread.
  • s toggles the notification sound on boards with audio output.
  • r refreshes and q or the app-exit key exits.

email

Receive-only IMAPS client for the configured mailbox. The app shows the provider-specific message list while every newly synchronized message is also published to the universal inbox and its shared status-bar unread counter.

Configure and synchronize the account before opening the app:

wifi on
email configure imaps://imap.example.com user@example.com app-password INBOX
email sync
email

Controls:

  • Up/Down, Page Up/Page Down, Home, and End navigate.
  • Enter or Right opens the selected message and marks its universal inbox
  • notification read.

  • Left, Backspace, or Esc returns from a message to the list.
  • u toggles the unread-only filter.
  • m toggles the selected message between read and unread.
  • r refreshes and q or the app-exit key exits.

The account configuration persists in NVS. The local message list is volatile and keeps the newest 32 synchronized messages. This first version displays a best-effort text preview; MIME attachments, encoded headers, sending, and server-side read flags are not implemented yet.

io

Interactive expansion I/O manager. Its default Layout view presents the board's connectors in their physical arrangement, followed by the existing pin, named-bus, and resource-claim views. It uses the same ownership and validation services as the gpio, i2c, spi, uart, midi, onewire, and expansion commands.

Usage:

io

Controls:

  • Tab switches between Layout, Pins, Buses, and Claims. Outside Layout,
  • Left and Right also switch views.

  • Arrows move through the physical connector grid in Layout and through rows
  • in the other views. Page Up/Page Down, Home, and End make larger moves.

  • Layout adapts to the terminal: compact headers show several pins across,
  • while long headers such as the DevKitC J1/J3 pair scroll vertically with one physical pin pair per row.

  • Layout markers are * free, ~ releasable, @ claimed, ! fixed/control,
  • + power, - ground, and x not connected.

  • Enter opens context-sensitive actions for a pin or bus.
  • n creates a board-approved named I2C, SPI, UART, MIDI, or 1-Wire bus.
  • Bus creation uses arrows to select fields and values; the generated bus name
  • can be edited directly.

  • Runtime buses can be attached, detached, or removed when their lease state
  • permits it. Their Autostart action idempotently appends the matching expansion bus create ... command to /.shell/startup. Direct GPIO and PWM assignments can be created and released from the Pins or Layout view.

  • A selected I2C bus has a Set I2C speed action with 100 kHz, 400 kHz, and
  • 1 MHz choices. The change applies at runtime even while devices lease the bus.

  • r refreshes; q, Esc, or the app-exit key exits.

invaders

Graphical arcade shooter.

Usage:

invaders

Controls:

  • Left/Right move the ship.
  • Space or f fires.
  • Esc or app-exit key exits.

gameboy

Original Game Boy (DMG) emulator selected by the gameboy group on boards with PSRAM, SD storage, graphics, and a streaming display. Current integrated targets are SolarTerm, Freenove IPS, CL-32, T-LoRa-Pager, T-Deck Plus, ODROID-GO, Freenove PAL, and TTGO VGA32. The application loads a user-supplied ROM into PSRAM and writes battery-backed cartridge RAM beside it as a .sav file. Game Boy Color-only ROMs and ROMs larger than 4 MiB are rejected.

The emulator runs Peanut-GB at its fixed native frame frequency in a dedicated worker. Peanut-GB renders alternate LCD frames, producing a compact 160x144 INDEX2 raster at about 30 frames per second while still emulating every frame. A shared frame presenter owns display cadence, retains the newest submitted frame, and replaces stale pending frames instead of blocking emulation. TFT targets scale the four-color raster directly to RGB565. One-bit targets convert the latest frame through ordered dithering, then use their scan-synchronized mono path. The target selects a bounded presentation rate of 25 or 30 frames per second and reduces output scale when the driver's pixel-rate budget cannot sustain the larger raster. Runtime logs report emulation, presentation, replacement, timing rebase, audio block, peak level, and transfer timing.

Audio rendering runs in its own bounded worker, applies a Game Boy-local bounded gain for low-resolution DAC outputs, and holds exclusive speaker output while Game Boy is active. The Waveshare presenter temporarily requests the panel's 25.5 Hz HPM profile. Pausing, suspending, or exiting stops the workers and restores the previous display and audio policies. PAL builds run without audio because composite scanout owns I2S0; both the 384x288 raster and the centered 320x200 safe-area mode can fit the scaled Game Boy frame.

Usage:

gameboy <file.gb>

Controls:

  • Arrows control the D-pad.
  • The physical US-Z key position is A; on a German QWERTZ keyboard this key is
  • labeled Y. The physical X key is B.

  • Enter is Start; Backspace or Delete is Select.
  • On ODROID-GO, A and B are the Game Boy A and B buttons, Menu is Start,
  • Select is Select, and Menu+Select exits.

  • p pauses and r resets.
  • q, Esc, or the app-exit key exits.

Game Boy reads the generic SolarOS held-key state. BLE/PS/2 keyboards, fixed board buttons, gpio-keys, and ADC D-pads therefore remain active until release, and combinations such as diagonal movement or direction plus A/B work simultaneously. USB HID usages preserve the physical Z/X positions across keyboard layouts. Character-only inputs such as port-shell input retain the short button-pulse fallback. Analog joystick axes do not become Game Boy keys.

less

Terminal pager for text files. It preserves original text layout and is useful for quick file inspection.

Usage:

less <file>

Controls:

  • Up/Down or j/k scroll one line.
  • Page Up/Page Down, b, or Space page.
  • Home/End or g/G jump to start/end.
  • Ctrl+F or / opens Find. F3 or n jumps to the next
  • case-insensitive match; N jumps to the previous match. Search wraps.

  • q, Esc, or app-exit key exits.

logic

On-device logic analyzer waveform viewer. It displays the latest capture made by the shared logic analyzer service or the SUMP job. With pin arguments it makes a new local capture before opening the viewer.

Usage:

logic
logic <pin[,pin...]> [rate-hz] [samples] [trigger=<pin>]

Examples:

logic
logic 1,2,3,17
logic 1,2 500000 8192
logic 1,2,3,17 10000 4096 trigger=1

trigger=<pin> waits for the next rising or falling edge on that runtime-safe GPIO before sampling. The trigger pin may be one of the captured data pins, as in the last example, or a separate GPIO that is not displayed as a channel.

Controls:

  • Left/Right pans through the capture.
  • +/- or Page Up/Page Down changes the time scale.
  • r captures again with the current local configuration when the SUMP job is
  • stopped.

  • a or Home shows the complete capture.
  • q, Esc, or the app-exit key exits.

The app is compiled only for boards with graphics and runtime-safe GPIOs. While the SUMP job is running, the app remains a viewer and automatically reloads captures received from the host.

lua

Embedded Lua runtime. It can run an interactive REPL or execute .lua scripts from storage. Lua scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.

Usage:

lua
lua file.lua [args...]

Controls:

  • exit([code]) returns from the REPL or script and reports the optional exit
  • code to the launching shell.

  • App-exit key interrupts running code or exits.

notes

Markdown-backed checklist and category manager. It stores unchecked and checked items and supports one level of category folding. A persistent bottom help bar shows the available controls, with status or text input directly above it.

Usage:

notes [file.md]

Controls:

  • Up/Down navigate.
  • Space toggles an item. After an active item is marked done, the selection
  • remains on the next active item when one is available.

  • a adds an unchecked item below the selected item. On a category, it adds the
  • first item in that category; from the done section, it adds at the end of the active items.

  • c adds a category.
  • Enter edits the selected line.
  • d or Delete deletes the selected item/category.
  • t tidies the note by deleting all completed items.
  • Shift+Up/Shift+Down reorders items within a category or moves the selected
  • category together with all of its items.

  • Left/Right collapse/expand a category.
  • q, Esc, or app-exit key exits.

plot

Graphical plotter for DAQ CSV files and live scalar streams. It is compatible with CSV generated by the daq job.

Usage:

plot <scalar-stream...> [--rate ms]
plot -f <file.csv> [column...]

Examples:

plot temperature humidity --rate 1000
plot -f /logs/env.csv temperature humidity

--rate is a best-effort live sampling interval in milliseconds. Rates below 25 ms request faster runtime ticks while the screen redraws no more often than once every 25 ms, so fast acquisition does not force an equally fast display refresh.

Controls:

  • Left/Right pan.
  • Up/Down select series.
  • +/- or Page Up/Page Down adjust visible window/zoom.
  • a or r resets the view.
  • Space pauses/resumes live mode.
  • q, Esc, or app-exit key exits.

playground

Browse the configured community catalog as a foldable category tree, search applications, and install, update, uninstall, or run Python and Lua scripts.

Usage:

playground
playground search QUERY...
playground install APP-ID [auto|flash|sd]
playground run APP-ID [ARG...]
playground delete
playground refresh
playground reload
playground source [repository-or-catalog-url|reset]
playground storage [flash|sd]

Press / to search, Enter to open an application, and use the actions shown in the bottom bar. Press i on an application in the catalog tree or details page to install it, and u to uninstall it after confirmation. Packages are verified by size and SHA-256 and installed under /playground/ on the permanently configured flash or sd storage. Community scripts run with the normal permissions of their runtime and are not sandboxed.

Catalog and application files are stored under /playground/ on the selected filesystem. playground delete recursively removes that entire directory, clears the loaded catalog from memory, and also removes the legacy hidden .solar/playground directory when present. Source and storage preferences are retained.

The shell subcommands use the local catalog for browsing and installation: refresh downloads and saves it, reload loads that saved copy without network access, search prints matches, and install downloads and verifies an application by ID. run reads an installed application's own manifest, so it does not require a catalog reload. It launches the declared Python or Lua runtime without creating a Playground session. Installation also adds the app ID to the managed /.shell/playground aliases, so qr-share --file /notes/wifi.txt is equivalent to playground run qr-share --file /notes/wifi.txt. Uninstalling removes the generated alias. Arguments are forwarded unchanged to the selected script. Opening the TUI reloads the saved catalog automatically and does not refresh it. At the top-level catalog tree, Esc, q, and the app-exit key exit Playground.

playground storage shows the persistent catalog and default application storage. Set it with playground storage flash or playground storage sd. Without a saved preference, Playground selects SD when it is mounted and flash otherwise. Omitting the install target, or specifying auto, uses the selected setting.

See Playground for controls, storage layout, source selection, and the trust model.

python

Embedded MicroPython runtime. It can run an interactive REPL, .py scripts, or .mpy files from storage. Python scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.

Usage:

python
python file.py [args...]
python file.mpy [args...]

Controls:

  • exit([code]) returns from the REPL or script and reports the optional exit
  • code to the launching shell.

  • App-exit key interrupts running code or exits.

reader

Graphical document reader for plain text, Markdown, and EPUB. It remembers reading position and zoom per opened file when storage is available.

Usage:

reader [--pager] <file.txt|file.md|file.epub|man:topic>

With --pager, Up and Down page instead of scrolling one layout row. Each forward page starts with the final visible row from the previous page, so mixed font sizes and wrapped Markdown retain a precise reading overlap.

Controls:

  • Up/Down scroll by layout row, or page when --pager is active.
  • Page Up/Page Down page with the same precise row overlap.
  • Home/End jump to start/end.
  • +/- adjust zoom.
  • Ctrl+F or / opens Find. F3 or n jumps to the next
  • case-insensitive match; N jumps to the previous match. Search wraps.

  • Esc exits search state first; otherwise exits.
  • q or app-exit key exits.

writer

Resumable graphical Markdown editor for PSRAM display boards. Inactive blocks are formatted like reader; the block containing the cursor and every block touched by a selection show their exact Markdown source. edit remains the portable text editor for port shells and boards without graphics or PSRAM.

Usage:

writer [file.md]

Without a path, Writer opens an untitled document and asks for a path on the first save. Existing files larger than 256 KiB are rejected without changing them. Saves use a synced same-directory staging file, backup rename, verified replacement, and rollback. Cursor, scroll anchor, and zoom metadata plus idle recovery snapshots live centrally under /.writer or the active persistent storage root. Writer offers a differing recovery snapshot when the document is opened again. Recovery uses R to recover, D to discard the snapshot, or C to cancel; the unsaved-changes dialog similarly uses S, D, and C.

Controls:

  • Left/Right move by UTF-8 codepoint; Up/Down move to the adjacent editable
  • visual line or Markdown block, including directly adjacent headings. Ctrl+Left/Ctrl+Right move by word. The insertion cursor blinks while idle.

  • Shift with arrows, Page Up/Page Down, Home/End, or document movement extends
  • the selection. Ctrl+Home/Ctrl+End jump to the document boundaries.

  • Ctrl+A, Ctrl+C, Ctrl+X, and Ctrl+V select all, copy, cut, and paste
  • through the shared SolarOS clipboard.

  • Ctrl+B, Ctrl+I, and Ctrl+K add or remove bold, italic, and link source.
  • F1 opens formatting for inline code, headings 1–4, bullet or numbered
  • lists, quotes, fenced code, and rules. Esc closes the active menu/dialog; from the editor it exits, using the save/discard/cancel prompt when dirty.

  • Ctrl+F opens Find and F3 jumps to the next case-insensitive match,
  • wrapping at the end. Ctrl+R prompts for find and replacement text.

  • Ctrl+S saves, Ctrl+Z/Ctrl+Y undo and redo, and Ctrl++/Ctrl+- adjust
  • zoom.

  • Esc and the app-exit key open the save/discard/cancel prompt when the
  • document is dirty. Writer writes recovery before suspend or stop and resumes in place.

scp

SCP file transfer over SSH. It supports password or key authentication through the shared SSH transport and host lookup/known-host storage. When user@ is omitted, SCP uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts, preserves an explicit user@ prefix, and appends : after a unique host match.

Usage:

scp [-P port] local [user@]host:remote
scp [-P port] local [user@]host:
scp [-P port] [user@]host:remote local
scp [-P port] [user@]host:remote-glob dir
scp [-P port] [user@]host:remote

Remote download paths can use * or ?. The local target must be an existing directory for remote wildcard downloads.

SCP runs inline in the shell. Its progress and result remain in the terminal scrollback, and usage errors or a completed transfer return directly to the prompt.

Controls:

  • App-exit key cancels an active transfer.

sftpsync

Incremental file and directory synchronization over SSH. The SolarOS client uses the server's SFTP subsystem, so it works with a normal SSH server without requiring a matching remote program. It skips regular files whose size and modification time already match. It does not delete destination-only files.

Usage:

sftpsync [-r|-a] [-n] [-P port] local [user@]host:remote
sftpsync [-r|-a] [-n] [-P port] [user@]host:remote local

-r, -a, or --recursive enables directory traversal. -n or --dry-run prints the changes without writing them. -P selects a non-default SSH port. Password and key authentication, known hosts, and aliases in /.ssh/hosts are shared with ssh and scp. A local or remote directory operand names the directory to synchronize; its contents are updated below that path. Traversal is limited to eight nested directory levels. Symbolic links and other special file types are skipped. Active transfers show an updating progress bar for each changed file, followed by the aggregate file and byte counts.

Controls:

  • Ctrl+C or the app-exit key cancels the password prompt or an active
  • synchronization.

sheet

CSV viewer for small data tables. It is intended as a companion to daq logs and simple spreadsheet-like inspection.

Usage:

sheet <file.csv>

Controls:

  • Arrows move through cells.
  • Page keys scroll.
  • App-exit key exits.

ssh

Interactive SSH client. It supports password and key authentication, known hosts, hostname lookup through /.ssh/hosts, UTF-8 text, VT-style controls, and remote full-screen terminal applications. When user@ is omitted, SSH uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts and preserves an explicit user@ prefix.

Usage:

ssh [user@]host [port]

On display shells, SSH owns a resumable terminal buffer. Switching to the SolarOS shell does not mix the local and remote scrollback, and switching back restores the SSH buffer. On UART and USB CDC port shells, SSH uses the shared port scrollback. Disconnecting returns directly to the local prompt. Remote terminal control sequences still work, including full-screen applications.

Controls:

  • Most keys are sent to the remote host, including Esc, Alt+key, cursor keys,
  • Ctrl combinations, and function keys.

  • App-exit key closes the SSH app.
  • Alt+Tab leaves the session running in the background on display builds.

synth

Open the native synthesizer and sound designer:

synth [--headless]

The app uses the same eight-voice fixed-point engine exposed to Python and Lua, but keyboard and MIDI events reach it directly. On a display, the default graphical interface provides the complete editor described below. --headless suppresses graphical rendering while keeping the same note keys, editing keys, published parameters, control bindings, and MIDI input active. This permits use from a port shell on a headless board after a playback device such as the LEDC PWM audio expansion has been attached. Its Play tab contains the waveform, envelope, volume, ADSR, and piano controls. The Wave tab is a graphical wavetable editor with selectable 16, 32, or 64-point resolution; edits reshape held notes immediately while the piano remains playable, and switching tabs does not rewrite the custom wavetable. The Filter tab adds a resonant low-pass response graph, cutoff, resonance, envelope amount, and an independent graphical ADSR filter envelope. The Oscillator 2 tab adds a second per-note source with waveform, octave, fine detune, and unity-safe mix controls. Both oscillators share the filter and envelopes. The Preset tab provides eight factory sounds and eight persistent user slots. User presets capture both oscillators, both envelopes, the filter, mono/poly mode, glide, and the complete custom wavetable. The Glide tab provides polyphonic or monophonic last-note playback, a hold mode for toggle-style piano keys, and portamento. Hold is session performance state and is not part of a preset. The display also reports active voices, output sample rate, and audio errors. The volume button changes the shared SolarOS speaker volume.

On display targets smaller than 240 pixels wide or 200 pixels high, Synth automatically replaces the full editor with a parameter HUD. The selected control gets a large value and level bar; waveform controls and the Wave tab retain compact graphs; Presets shows one slot at a time. Targets smaller than 112 by 56 pixels use a footer-free micro layout. Physical note keys and MIDI remain active. External parameter/control changes focus the changed parameter until the next local navigation action, which makes a 128-by-64 SH1106 or SSD1306 useful as a synthesizer appliance display. After attaching it as oled0, open Synth on that target with session create synth oled0. During active playing, compact displays defer visualization until note input is quiet so synchronous display transfers cannot take priority over new notes. MIDI notes highlight the matching pitch class on the on-screen piano just like physical note keys.

Controls:

  • A W S E D F T G Y/Z H U J K play one chromatic octave. The positions are
  • physical, so the Y position is the Z key on a German keyboard. Note input remains active on all tabs.

  • Tab cycles through Play, Filter, Wave, Oscillator 2, Glide, and Presets.
  • Number keys 1 through 6 select those tabs in that order.

  • X hides or shows the on-screen piano keyboard on every tab. When it is
  • hidden, the tab's knobs, graphs, panels, or preset list use the freed space; the physical note keys and MIDI input remain active.

  • On Glide, select Hold and use Up or Enter to enable it. Each physical or
  • terminal piano-key press then toggles its note on or off, and releasing the key does not stop the note. Use Down or Enter to disable hold and release all latched app-key notes. MIDI Note On, Note Off, and sustain keep their normal behavior.

  • On Play, Left/Right selects the waveform, global volume, or an ADSR knob;
  • Up/Down changes it, and +/- changes note velocity.

  • On Wave, Left/Right moves the edit cursor and Up/Down changes the
  • waveform. Shifted arrows move or draw faster, and +/- changes brush size.

  • On Wave, Enter cycles the editor through 16, 32, and 64 points. New sessions
  • start at 16 points. The current waveform is resampled rather than reset.

  • On Wave, B applies the next square, triangle, saw, Supersaw, sine, or flat
  • baseline; R restores that baseline, M smooths, N normalizes, 0 clears, and Backspace or Delete exchanges the current waveform with the undo state.

  • On Filter, Left/Right selects cutoff, resonance, envelope amount, or a
  • filter ADSR knob; Up/Down changes it, and +/- changes note velocity.

  • On Oscillator 2, Left/Right selects waveform, octave, fine detune, or mix;
  • Up/Down changes it, and +/- changes note velocity. Octave ranges from -2 through +2, detune from -100 through +100 cents, and mix from 0 through 100 percent.

  • On Preset, arrows select a factory or user slot and Enter loads it. V
  • saves the current sound to the selected user slot. Factory presets are read-only.

  • Page Up/Page Down changes octave from 2 through 6 on any tab.
  • Esc or the app-exit key exits and immediately releases audio ownership.

For an external MIDI keyboard, create and start a named MIDI bus before opening Synth:

expansion bus create midi midi0 tx=gpio1 rx=gpio2
job start midi midi0
synth

MIDI Note On/Off, velocity, sustain (CC64), All Sound Off (CC120), and All Notes Off (CC123) are supported on all 16 channels.

Published parameters

While Synth is running, control parameters exposes these native continuous parameters:

  • synth.volume: 0 through 100 percent, linear.
  • synth.envelope.attack: 0 through 10000 ms, linear.
  • synth.envelope.decay: 0 through 10000 ms, linear.
  • synth.envelope.sustain: 0 through 100 percent, linear.
  • synth.envelope.release: 0 through 10000 ms, linear.
  • synth.filter.cutoff: 40 through 18000 Hz, logarithmic.
  • synth.filter.resonance: 0 through 100 percent, linear.
  • synth.filter.envelope.amount: 0 through 100 percent, linear.
  • synth.filter.envelope.attack: 0 through 10000 ms, linear.
  • synth.filter.envelope.decay: 0 through 10000 ms, linear.
  • synth.filter.envelope.sustain: 0 through 100 percent, linear.
  • synth.filter.envelope.release: 0 through 10000 ms, linear.
  • synth.osc2.octave: -2 through +2 octaves, linear.
  • synth.osc2.detune: -100 through +100 cents, linear.
  • synth.osc2.mix: 0 through 100 percent, linear.
  • synth.glide: 0 through 2500 ms, linear.

These paths exist only while the app is active. Bindings remain configured when Synth is suspended or stopped and reconnect when it resumes. See man controls for control creation, soft pickup, and MIDI CC mappings.

telnet

Telnet client for classic TCP terminal sessions. It supports basic Telnet option negotiation, terminal type reporting, window size reporting, and raw mode.

Usage:

telnet host [port]
telnet -r host [port]

Controls:

  • App-exit key closes the Telnet app.
  • Alt+Tab leaves the session running in the background on display builds.

view

Graphical image viewer. It supports the image formats compiled into the current firmware, including common PNG/JPEG/GIF/WebP paths and automatic animated GIF playback when the media package is enabled. Images are decoded as RGB on a negotiated indexed-color display and as grayscale on a one-bit display. In the default fit mode, JPEG color conversion writes display-sized output directly, so a large source photograph does not require a full-size RGB destination.

Usage:

view [-fit|-actual] <image>

Controls:

  • Arrows pan.
  • f toggles fit/actual mode. Fit mode scales the image up or down to the screen.
  • 0 selects actual size.
  • 1 selects fit-to-screen.
  • Esc or app-exit key exits.

sketch

Pointer-driven graphical paint application. Its layout follows classic desktop paint programs: Save, Open, Import, and the sidebar controls share one aligned, equal-sized button grid; color and pattern choices are in the bottom bar. Sketch uses a compact four-color canvas and stores finished documents as interoperable indexed-color PNG files. Color TFTs show the native palette; one-bit displays use the existing dithered rendering path.

Usage:

sketch
sketch file.png

Sketch is intentionally not gated on a pointer hardware capability. If no ready absolute or relative pointer is registered when the app starts, a popup explains that a pointer can be attached at any time. Press Enter or Esc to dismiss it; the first attached pointer is accepted without restarting the app.

Controls:

  • Click Save to update the current PNG. For an untitled or imported image, the
  • first save selects the next free Sketches/sketchNNN.png path on persistent storage. Saves use a synced staging file, backup rename, atomic replacement, and rollback.

  • Click Open to select a PNG as the current document. Click Import to flatten a
  • PNG, JPEG, or GIF into the current four-color canvas; the next save creates a new PNG instead of overwriting the imported source.

  • The sidebar selects pen, straight-line, rectangle, ellipse, bucket fill,
  • or eraser drawing. The weight control cycles through 1, 2, 4, and 8 pixels. Eraser uses the selected weight and restores white pixels. Clear immediately resets the complete canvas to white.

  • Line, rectangle, and ellipse show an exact non-destructive preview during the
  • drag, including the selected color, pattern, and stroke weight. Releasing the pointer commits that preview to the canvas.

  • The first four bottom swatches select white, red, blue, or black. The next
  • four select solid, checker, dot, or diagonal-hatch application.

  • Keyboard fallbacks are S Save, O Open, I Import, P Pen, L Line,
  • R Rectangle, E Ellipse, B bucket fill, X eraser, and C clear. Arrows and Enter operate the file browser.

  • Q, Esc, or the app-exit key exits. Suspending and resuming retains the
  • current cold-allocated canvas; closing the app releases it and all browser or image resources.

web

Simple graphical web browser for lightweight HTML pages. It shares document and image rendering infrastructure with reader where possible. Embedded and direct PNG, JPEG, GIF, and WebP images retain color on indexed-color displays; one-bit displays keep the grayscale decode and dither path.

Usage:

web http://host/
web https://host/path

Controls:

  • The top toolbar provides Back, Reload, and Forward. Pointer or touch presses
  • activate the toolbar and open links or form controls directly.

  • Left or b goes back, Right or f goes forward, and r reloads while
  • retaining the current reading position.

  • Up/Down or k/j scroll one rendered line. Page Up/Page Down, Home,
  • and End provide larger movement.

  • n or Tab selects the next link or form control; p selects the previous
  • one. Selection stops at each end, and Enter activates it.

  • Links and plain page text use the same font size. Links remain underlined,
  • headings remain bold, and +/- or Ctrl++/Ctrl+- reflow page text through the same five zoom levels as Reader.

  • Ctrl+F opens Find and F3 jumps to the next case-insensitive match in
  • rendered page text, wrapping at the end.

  • Esc or app-exit key exits.

Quick reference

Use apps to list applications installed in the current firmware. Start an app by entering its name and arguments. Use the app-exit key to return to the shell; resumable applications can also be switched through sessions. This page is the complete usage and controls reference for foreground applications.

Applications · SolarOS manual

arecord

Record a registered capture endpoint to a WAV file. The default is the first compatible input; -i selects a specific stream such as adc0.capture. This requires a registered input device, but not built-in board audio. With -d, recording stops after the specified number of seconds. Without -d, recording continues until app-exit, the storage fills, or the WAV size limit is reached. It can be launched from display, UART, USB CDC, Telnet, and other port shells.

Usage:

arecord [-d seconds] [-i capture-stream] file.wav

Controls:

  • App-exit stops recording, finalizes the WAV header, and returns to the prompt.

Applications · SolarOS manual

calc

Scientific calculator and function plotter. On a graphical display, calc opens an expression list beside a Cartesian plot. From UART, USB CDC, Telnet, or any other text-only shell, the same command opens a scientific REPL without the plot pane. calc --tui forces that REPL even when graphics are available.

The expression engine supports +, -, *, /, %, powers with ^, parentheses, scientific notation, and implicit multiplication such as 2pi or 2(x + 1). Built-ins include sin, cos, tan, their inverse functions, sqrt, abs, exp, ln, log, floor, ceil, round, min, max, pow, and atan2. Trigonometric input is in radians; rad(degrees) and deg(radians) convert explicitly. Constants are pi and e.

Rows can hold scalar calculations, variables, one-argument functions, or plots:

a = 2
f(x) = sin(x) / x
f(pi / a)
y = f(x)

For a one-off shell calculation, use:

calc -e "sqrt(2)^2"

Text REPL commands:

  • :list or :vars shows worksheet rows and scalar results.
  • :del N removes a row; :clear empties the worksheet.
  • :save [file] and :load [file] write or restore one expression per line.
  • The default file is calc.txt in the current shell directory.

  • :help shows the compact reference; :quit returns to the shell.
  • Up/Down recalls input history; Left/Right, Home, End, Backspace, and Delete
  • edit the current line.

Graphical controls:

  • The expression editor keeps a white background for legible small text and
  • outlines the active row with a thin dark border.

  • Type to edit the selected row; Up/Down changes rows and Enter adds a row.
  • Left/Right, Home, End, Backspace, and Delete edit within a row.
  • Page Up toggles the selected graph row.
  • Tab moves between the expression list and graph.
  • In the graph, arrows pan, +/- zoom, and Home or 0 resets the view.
  • Press t for a numeric trace cursor; Left/Right moves it and Up/Down selects another plotted row. Press t again to resume panning.

  • Ctrl+S saves and Ctrl+O loads the default calc.txt worksheet.
  • App-exit closes the calculator. The worksheet is kept only when explicitly
  • saved.

Applications · SolarOS manual

chat

Tabbed provider-neutral conversation client. The Channels tab lists gateway and radio conversations. Enter selects a conversation and opens its bounded shared history on the Chat tab, which also contains the message/command input. The app opens and remains useful offline; network or radio transport jobs connect independently.

Usage:

chat [gateway|meshcore|link|conversation-id]

With no selector, Chat opens a unified view and initially selects the newest unread conversation. A provider name filters the list. A decimal conversation ID opens exactly that conversation.

The background gateway-sync job owns the gateway transport connection, retries, and joined-channel replay. Each join sends the greatest stable message ID retained for that room as its cursor; the gateway replays only newer messages. Requested membership and server-confirmed membership are separate, connection-scoped states. The shared messaging service owns queued outbound messages and gateway-sync consumes only gateway requests. Start it explicitly with job start gateway-sync, just like email-sync. Closing or suspending chat does not disconnect an already-running synchronizer. Incoming messages remain in the shared bounded messaging store and publish bounded notifications to the universal inbox; reopening the app replays retained conversations from every provider. With SD storage, full messages are retained under /.messages/messages.bin. On internal flash, Chat restores the compact message copy already retained in /.inbox/messages.bin, so it consumes no second flash ring. Both backends deduplicate transport replays by stable message identity and keep linked Inbox read state aligned.

Unlike email-sync, gateway-sync takes no interval argument: it waits for Wi-Fi and reconnects with exponential backoff while remaining in the running state.

Gateway setup and room lifecycle use gateway status, gateway configure, gateway connect, gateway disconnect, gateway rooms, gateway join, gateway leave, and gateway delete. Gateway synchronization runs only under the gateway-sync job name. Selecting a known gateway room in the Channels tab requests a join automatically and opens it after the server confirms joined.

Conversation rows show provider, unread, and security state. Outbound rows show queued/sending/sent/delivered/failed state. Use /new CONTACT_ID to open a direct conversation with the contact's preferred endpoint. Sending to a discovered endpoint asks for a second Enter confirmation; blocked endpoints cannot be messaged.

The conversation header reports the selected provider's state. Gateway uses disconnected, connecting, and connected; connectionless MeshCore and Link providers use stopped, starting, ready, and error.

In-app commands:

/help
/new contact-id
/status
/quit

Controls:

  • Tab switches between the Channels and Chat tabs.
  • In Channels, Up/Down select a conversation and Enter joins a known
  • gateway room if necessary, then opens the Chat tab.

  • In Chat, Up/Down navigate input history and Enter sends input.
  • Page Up/Page Down scroll messages.
  • Esc or app-exit key exits.

Applications · SolarOS manual

clock

Full-screen graphical seven-segment clock, alarm countdown, and stopwatch. The countdown is serviced in the background, so it continues when the Clock session is suspended. It works without RTC hardware while SolarOS remains powered. With a wired RTC interrupt, the RTC alarm or countdown is also armed and explicit light sleep can wake for it.

Usage:

clock
clock -a mm:ss
clock -s

Controls:

  • In stopwatch mode, Space starts/stops.
  • In stopwatch mode, any other ordinary key resets to zero.
  • Exiting a countdown removes that transient alarm and stops it if ringing.
  • Esc or app-exit key exits.

Applications · SolarOS manual

com

Serial terminal for a bidirectional byte-stream port. Display-keyboard or port-shell input is forwarded to the selected port, and received bytes are drawn in the active terminal. The port may be a UART or a virtual port such as a peer-bound SolarOS Link stream.

Usage:

com [--autobaud] [--hex] [port]

The port defaults to uart0. For example, com gps connects to an existing runtime UART bus named gps, while com vser0 opens a Link stream previously created with link stream create. The selected port remains claimed by the app until the session exits. com works from both display and port shells; when launched from a port shell, its terminal output is returned through that same port. The input/output shell port and selected COM port must be different.

--autobaud is available only for UART buses. It samples the RX signal for three seconds before opening the terminal. Send a repeating 0x55 or 0xaa pattern during that interval. A reliable measurement is matched to a standard UART rate and applied to the current bus connection without overwriting the saved baud setting. UART input forwarding begins when sampling completes. If measurement fails, the configured rate is kept.

--hex displays received bytes as eight-byte offset, hexadecimal, and ASCII rows instead of interpreting them as terminal text. Both options can be used together.

Examples:

com --hex gps
com --autobaud --hex uart0
com vser0

Controls:

  • App-exit key exits.

Applications · SolarOS manual

contacts

Provider-neutral address book for gateway and MeshCore identities. Contacts can carry multiple provider endpoints while retaining trust independently for each endpoint. A signed MeshCore advert creates a discovered endpoint: the signature proves possession of the advertised key, not the human identity behind it.

Open the searchable TUI:

contacts

The list is grouped by trust and provider. Press / to search, Enter to view the selected endpoint addresses, capabilities, last-seen times, and bounded provider metadata summary, and Esc or q to leave.

Use the shell surface for mutations:

contacts status
contacts list [all|discovered|trusted|blocked]
contacts show CONTACT_ID
contacts rename CONTACT_ID NAME
contacts trust CONTACT_ID [ENDPOINT_ID]
contacts block CONTACT_ID [ENDPOINT_ID]
contacts remove CONTACT_ID
contacts link TARGET_CONTACT_ID SOURCE_CONTACT_ID

Contact and endpoint identifiers autocomplete from live service snapshots. Linking moves the source endpoints to the target contact and removes the source record. When the 64-contact store is full, SolarOS may evict the oldest unpinned contact whose endpoints are all still discovered; trusted and blocked records are never automatically evicted.

The versioned store is CRC checked, uses two alternating headers and data copies, remains below 24 KiB, and normally lives at /.contacts/contacts.bin. If storage is unavailable, Contacts remains usable in volatile mode and contacts status reports the storage error.

Applications · SolarOS manual

curl

HTTP client for quick text downloads and diagnostics. It can print response data to the terminal or save it to a file.

Usage:

curl [-L] [-o file] URL

Controls:

  • App-exit key cancels an active transfer.

Applications · SolarOS manual

edit

Text editor for files on mounted storage. It supports cursor navigation, selection, clipboard operations, text-size changes, and syntax highlighting for known source files. The editor supports files up to 256 KiB on boards with PSRAM and 32 KiB on boards without PSRAM. Use hexedit for binary files.

Usage:

edit <file>

Controls:

  • Arrows move the cursor.
  • Ctrl+Left/Ctrl+Right move by words.
  • Shift+Arrows extend selection.
  • Ctrl+A, Ctrl+C, Ctrl+X, Ctrl+V select all, copy, cut, and paste.
  • Ctrl+F or F3 opens Find. Matches are case-insensitive and wrap at the end
  • of the file.

  • Ctrl+S or F2 saves in place. Ctrl+Q, Esc, F10, or the app-exit key
  • exits. When the file has pending changes, answer Y to save and exit or N to discard the changes and exit.

  • Ctrl++ and Ctrl+- adjust editor text size for the active session.

Applications · SolarOS manual

email

Receive-only IMAPS client for the configured mailbox. The app shows the provider-specific message list while every newly synchronized message is also published to the universal inbox and its shared status-bar unread counter.

Configure and synchronize the account before opening the app:

wifi on
email configure imaps://imap.example.com user@example.com app-password INBOX
email sync
email

Controls:

  • Up/Down, Page Up/Page Down, Home, and End navigate.
  • Enter or Right opens the selected message and marks its universal inbox
  • notification read.

  • Left, Backspace, or Esc returns from a message to the list.
  • u toggles the unread-only filter.
  • m toggles the selected message between read and unread.
  • r refreshes and q or the app-exit key exits.

The account configuration persists in NVS. The local message list is volatile and keeps the newest 32 synchronized messages. This first version displays a best-effort text preview; MIME attachments, encoded headers, sending, and server-side read flags are not implemented yet.

Applications · SolarOS manual

files

File manager inspired by Midnight Commander. Its normal mode provides two panes for copy, move, delete, and launch workflows on mounted storage. Launcher mode provides a minimal single-pane application menu suitable for a startup script.

Usage:

files [--launcher] [path]

files --launcher hides dot-prefixed entries, removes the message and function key bars, and expands one pane to the bottom of the terminal. Launcher mode is read-only: navigate with the arrows, open with Enter, refresh with r, move to the parent with Backspace, and exit with q or the app-exit key. To make a directory the startup menu, add a line such as this to /.shell/startup:

files --launcher /apps

File associations come from the installed app registry. Only apps compiled in the active firmware can be selected. Associations include images to view, WAV/MP3 to player, CSV to sheet, Python and Lua scripts to their runtimes, documents to reader (or writer when Reader is unavailable), and .gb ROMs to gameboy. Unknown files fall back to less or edit. A .sh file runs through the built-in SolarOS shell. In launcher mode, documents associated with Reader open as reader --pager for row-aware page navigation.

Controls:

  • Arrows navigate panes.
  • Tab switches active pane.
  • Enter opens directories or launches known files.
  • Returning to a parent directory restores the cursor to the directory that
  • was just exited.

  • F3/V, F4/E, F5/C, F6/M, F7/K, F8/D, and F9/Z
  • view, edit, copy, move, create a directory, delete, and create a zip archive. The function keys remain the primary map; the help bar shows the letter mnemonics within the action names.

  • Copy, move, and delete operate on the current or marked entries.
  • Copy and recursive delete scan their source trees first, then show measured progress with the current entry. Move shows progress across the selected top-level entries.

  • Esc cancels an active copy, move, or delete operation and keeps Files open.
  • A partial current-file copy is removed; top-level items completed before the cancellation remain copied, moved, or deleted.

  • File operations refresh both panes after completion.
  • App-exit key exits.

Applications · SolarOS manual

Flash another ESP board

flash downloads complete SolarOS factory images to SD and programs another supported ESP board through a named UART bus. The target must share ground with the SolarOS device. Connect the SolarOS UART TX pin to target RX and SolarOS RX to target TX. Both sides use 3.3 V logic; do not connect a 5 V UART signal.

Open the catalog browser:

flash

The browser reads only the verified catalog saved on SD. It does not access the network when it opens. Its Catalog tab presents the available board, flavor, and version entries as a foldable tree. All groups are initially folded. Use Up and Down to select an entry, Left and Right to fold or unfold a group, and Enter to act on a version. The browser retains the selected entry, scroll position, and folded groups after catalog refreshes and artifact operations. An asterisk marks an artifact that is cached on SD. Enter offers to download a remote artifact or program a cached artifact. d always offers a download, f always offers programming, and r refreshes the signed catalog. Delete or x asks for confirmation before it removes the selected cached artifact from SD. The verified catalog entry stays available, so the artifact can be downloaded again.

Press Tab to open Settings. Select the UART port with Left and Right. Select BOOT pin or RESET pin and press Enter to enter a GPIO number from 0 to 63. An empty value disables automatic control; Delete also clears the selected pin. These settings apply to the current Flash app session. The control pins refer to GPIOs on the SolarOS device, not pins on the target.

Before programming starts, a popup stays visible with the instructions for the current control-pin setup. With no control pins, put the target into ROM download mode manually while the popup is open. During refresh, download, verification, and programming, the popup shows the current stage and progress. The final success or failure remains visible until it is dismissed.

Catalog refresh uses the same repository as ota url. For example, an OTA URL of http://server/solaros/latest reads the flash catalog and signature from http://server/solaros/flash/. Artifact URLs then come from the verified catalog.

The same operations are available from the shell:

flash refresh
flash list
flash download BOARD FLAVOR [VERSION]
flash BOARD FLAVOR [version=VERSION] [port=uart0] [boot=PIN] [reset=PIN] [baud=RATE]

Shell completion for flash BOARD FLAVOR reads the verified SD catalog and offers only boards and flavors whose factory artifact is already cached.

When version is omitted, SolarOS selects the first matching catalog entry; the release catalog orders newer versions first. The program command refuses to use an artifact that is not already cached. port, boot, and reset refer to resources on the SolarOS device, not pins on the target.

If both boot and reset are present, SolarOS drives the target into ROM download mode automatically. With only boot, SolarOS holds that signal low while it starts the connection; reset the target manually. With only reset, hold the target's boot strap active while SolarOS toggles reset. With neither, put the target in download mode before you run the command.

Storage and verification

All data is stored on removable media:

/sdcard/.flash/catalog.json
/sdcard/.flash/catalog.sig
/sdcard/.flash/BOARD/FLAVOR/VERSION/flash-manifest.json
/sdcard/.flash/BOARD/FLAVOR/VERSION/factory.bin

The catalog signature uses the SolarOS OTA public key. Downloads are streamed to a staging file, checked against the catalog size and SHA-256, extracted, and checked again against flash-manifest.json before activation. A failed update keeps the previous cached artifact.

Before erasing the target, SolarOS verifies the cached factory image, opens the requested UART exclusively, identifies the target chip, and compares it with the manifest. It rejects an unsupported or different chip, an undersized flash device, secure-download mode, flash encryption, or secure boot. A successful write ends with the target bootloader's MD5 verification.

The board ID is explicit because the ESP ROM reports the chip family, not the physical board or its pin/display/storage layout. A matching chip is therefore necessary but is not enough to choose a board artifact safely.

Resource use

The flasher has no boot-time task and keeps no catalog or transfer buffers in memory while unused. It allocates its app state, catalog model, worker stack, HTTP buffers, UART loader context, and transfer buffer only after flash is opened, and releases them when the app exits. UART and optional GPIO resources are also claimed only for the active programming operation.

Quick reference

Run flash refresh to cache the signed catalog, flash list to show artifacts, and flash download BOARD FLAVOR [VERSION] to store one verified factory image under /sdcard/.flash. Program it with flash BOARD FLAVOR plus optional version=, port=, boot=, reset=, and baud= values. The target chip must match the selected board artifact. Connect crossed 3.3 V UART signals and a common ground; use both optional control pins for automatic download mode.

Applications · SolarOS manual

flash

Download verified SolarOS factory artifacts to SD and program another supported ESP board over UART. The browser refreshes the signed catalog on request and shows which board, flavor, and version artifacts are already cached. Its tree starts folded and retains its selection and fold state after operations. Delete removes a selected cached artifact after confirmation. The shell form accepts a named UART plus optional boot and reset GPIO pins.

flash
flash refresh
flash list
flash download BOARD FLAVOR [VERSION]
flash BOARD FLAVOR [version=VERSION] [port=uart0] [boot=PIN] [reset=PIN] [baud=RATE]

See Flash another ESP board for wiring, target-selection, security, storage, and verification details.

Applications · SolarOS manual

ftp

Two-pane FTP file manager. The left pane is local mounted storage. The right pane is a remote FTP server. The app uses unencrypted IPv4 FTP and passive data connections.

Usage:

ftp HOST [PORT] [--user USER --password PASSWORD] [--remote PATH] [--local PATH]

Anonymous login is the default (anonymous with solaros@ as its password). Specify both --user and --password for an authenticated server. Passwords are plaintext on the network and remain in shell history when entered on the command line. Use FTP only on a trusted network.

Controls:

  • Arrows navigate the active pane. Tab, Left, and Right switch panes.
  • Enter opens a directory. r refreshes the remote and local listings.
  • F3 views a local file. For a remote file, FTP downloads a temporary hidden
  • copy into the current local folder, opens its registered viewer, and removes the copy when control returns to FTP.

  • F5 copies the current file or directory to the other pane. Directory
  • transfers are recursive. Copy and move show the same preparing/transfer progress popup used by Files.

  • F6 moves the current file or directory to the other pane by completing the
  • copy before deleting the source.

  • F7 creates a directory in the active pane.
  • F8 recursively deletes the current item after confirmation.
  • q, F10, or the app-exit key closes the connection and exits.
  • After an operation refreshes the current directories, both panes retain their
  • cursor and scroll positions. Opening another directory starts at its top.

FTP is a resumable TUI on the display shell and VT100-compatible port shells. One operation runs at a time in its foreground worker. Existing destination files are replaced. A failed download removes its staging file and preserves an existing destination; a move does not delete its source unless the complete copy succeeds.

Applications · SolarOS manual

funcgen

Audio-only function generator built on the shared real-time Synth service. It emits signed 16-bit stereo PCM and can use the default playback device or an explicitly selected runtime playback stream, including an attached LEDC PWM audio expansion.

Usage:

funcgen [--tui]

The graphical interface shows the exact generated PCM in the shared oscilloscope widget. The remaining controls select sine, square, triangle, saw, pulse, or noise output; frequency from 20 through 8000 Hz; amplitude; pulse width; a repeating linear sweep; sweep end frequency; sweep time; and playback stream. The TUI exposes the same controls. --tui forces it on a graphical shell.

Controls:

  • Left/Right selects a control.
  • Up/Down, +/-, or Enter adjusts the selected control.
  • Space starts or stops output.
  • Esc, Q, or the app-exit key closes the application.

The output is off initially. Frequency is capped below the selected stream's Nyquist limit during rendering. A sweep moves linearly from Frequency to Sweep end during Sweep time, then repeats without resetting oscillator phase. Suspending the UI leaves an active generator running; closing it stops the Synth worker and releases the stream.

All controls can be targeted by the shared control-binding system while Funcgen is active:

  • funcgen.waveform: 0 sine, 1 square, 2 triangle, 3 saw, 4 pulse, 5 noise.
  • funcgen.frequency: 20 through 8000 Hz, logarithmic.
  • funcgen.amplitude: 0 through 100 percent.
  • funcgen.pulse.width: 1 through 99 percent.
  • funcgen.sweep.enabled: 0 off or 1 on.
  • funcgen.sweep.end: 20 through 8000 Hz, logarithmic.
  • funcgen.sweep.time: 100 through 60000 ms, logarithmic.
  • funcgen.output: runtime output index; 0 follows the default output.
  • funcgen.enabled: 0 off or 1 on.

The durable foreground state and oscilloscope storage use PSRAM when present. Only the bounded oscillator/render state, Synth worker stack, and PCM block remain in internal SRAM.

Applications · SolarOS manual

gameboy

Original Game Boy (DMG) emulator selected by the gameboy group on boards with PSRAM, SD storage, graphics, and a streaming display. Current integrated targets are SolarTerm, Freenove IPS, CL-32, T-LoRa-Pager, T-Deck Plus, ODROID-GO, Freenove PAL, and TTGO VGA32. The application loads a user-supplied ROM into PSRAM and writes battery-backed cartridge RAM beside it as a .sav file. Game Boy Color-only ROMs and ROMs larger than 4 MiB are rejected.

The emulator runs Peanut-GB at its fixed native frame frequency in a dedicated worker. Peanut-GB renders alternate LCD frames, producing a compact 160x144 INDEX2 raster at about 30 frames per second while still emulating every frame. A shared frame presenter owns display cadence, retains the newest submitted frame, and replaces stale pending frames instead of blocking emulation. TFT targets scale the four-color raster directly to RGB565. One-bit targets convert the latest frame through ordered dithering, then use their scan-synchronized mono path. The target selects a bounded presentation rate of 25 or 30 frames per second and reduces output scale when the driver's pixel-rate budget cannot sustain the larger raster. Runtime logs report emulation, presentation, replacement, timing rebase, audio block, peak level, and transfer timing.

Audio rendering runs in its own bounded worker, applies a Game Boy-local bounded gain for low-resolution DAC outputs, and holds exclusive speaker output while Game Boy is active. The Waveshare presenter temporarily requests the panel's 25.5 Hz HPM profile. Pausing, suspending, or exiting stops the workers and restores the previous display and audio policies. PAL builds run without audio because composite scanout owns I2S0; both the 384x288 raster and the centered 320x200 safe-area mode can fit the scaled Game Boy frame.

Usage:

gameboy <file.gb>

Controls:

  • Arrows control the D-pad.
  • The physical US-Z key position is A; on a German QWERTZ keyboard this key is
  • labeled Y. The physical X key is B.

  • Enter is Start; Backspace or Delete is Select.
  • On ODROID-GO, A and B are the Game Boy A and B buttons, Menu is Start,
  • Select is Select, and Menu+Select exits.

  • p pauses and r resets.
  • q, Esc, or the app-exit key exits.

Game Boy reads the generic SolarOS held-key state. BLE/PS/2 keyboards, fixed board buttons, gpio-keys, and ADC D-pads therefore remain active until release, and combinations such as diagonal movement or direction plus A/B work simultaneously. USB HID usages preserve the physical Z/X positions across keyboard layouts. Character-only inputs such as port-shell input retain the short button-pulse fallback. Analog joystick axes do not become Game Boy keys.

Applications · SolarOS manual

help

Foreground browser for the package-aware SolarOS manual. The foldable tree groups the topics compiled for the current firmware and shows whether it is using the embedded copy or a verified downloaded revision. All groups start folded. The selection, scroll position, and fold state remain unchanged after a topic closes.

Usage:

help
help agent

Controls:

  • Up/Down, Page Up/Page Down, Home/End: move through the tree.
  • Left/Right, Enter, or Space on a group: fold or unfold it.
  • Enter or Right on a topic: open it in reader on graphic display shells
  • or less on text shells.

  • q, Esc, or the app-exit key: return to the shell.

The maintenance forms help status, help update, and help reset remain shell operations. SD-capable builds show terminal-width-aware progress while downloading and extracting one exact-version signed manual archive. Use help command.status to open the shell status command page.

Applications · SolarOS manual

hexedit

Two-pane binary editor for files on mounted storage. Each row shows a file offset, hexadecimal bytes, and their synchronized printable ASCII view. The number of bytes per row adapts to the terminal width. It uses the same 256 KiB PSRAM and 32 KiB internal-memory limits as edit.

Usage:

hexedit <file>

Controls:

  • Tab switches input focus between the HEX and ASCII panes. The corresponding
  • byte remains highlighted in both panes.

  • Hexadecimal digits replace the active high or low nibble in the HEX pane.
  • Printable characters replace the active byte in the ASCII pane.

  • Arrows, Page Up/Page Down, Home/End, and Ctrl+Home/Ctrl+End navigate by byte,
  • row, page, or file.

  • Shift with navigation extends a byte selection. Ctrl+A, Ctrl+C,
  • Ctrl+X, and Ctrl+V select all, copy, cut, and paste binary data.

  • Backspace and Delete remove bytes. Typing at end of file appends data.
  • Ctrl+S saves in place. Ctrl+Q, Esc, or the app-exit key exits without
  • saving pending changes.

  • Ctrl++ and Ctrl+- adjust editor text size for the active session.

Applications · SolarOS manual

inbox

Universal incoming-message browser for pages, chat notifications, mail, and other background producers. It reads the same shared inbox that supplies the status-bar unread count. Messages and read state survive reboot in the bounded /.inbox/messages.bin store; the service retains at most 64 entries and keeps the file below 32 KB even when internal flash is the only storage.

Each list item occupies exactly one terminal row: unread/priority markers, local reception date and time, a compact source (chat/general, email, or pocsag), and as much of the message body preview as fits the screen.

Notification sound is enabled by default on boards with audio. Press s in the Inbox or use inbox notify on, inbox notify off, or inbox notify test; the setting is persistent. Notification tones remain active while SolarOS is suspended.

Usage:

inbox

Controls:

  • Up/Down, Page Up/Page Down, Home, and End navigate.
  • Enter or Right opens the selected message and marks it read.
  • Left, Backspace, or Esc returns from a message to the list.
  • u toggles the unread-only filter.
  • m toggles the selected message between read and unread.
  • s toggles the notification sound on boards with audio output.
  • r refreshes and q or the app-exit key exits.

Applications · SolarOS manual

invaders

Graphical arcade shooter.

Usage:

invaders

Controls:

  • Left/Right move the ship.
  • Space or f fires.
  • Esc or app-exit key exits.

Applications · SolarOS manual

io

Interactive expansion I/O manager. Its default Layout view presents the board's connectors in their physical arrangement, followed by the existing pin, named-bus, and resource-claim views. It uses the same ownership and validation services as the gpio, i2c, spi, uart, midi, onewire, and expansion commands.

Usage:

io

Controls:

  • Tab switches between Layout, Pins, Buses, and Claims. Outside Layout,
  • Left and Right also switch views.

  • Arrows move through the physical connector grid in Layout and through rows
  • in the other views. Page Up/Page Down, Home, and End make larger moves.

  • Layout adapts to the terminal: compact headers show several pins across,
  • while long headers such as the DevKitC J1/J3 pair scroll vertically with one physical pin pair per row.

  • Layout markers are * free, ~ releasable, @ claimed, ! fixed/control,
  • + power, - ground, and x not connected.

  • Enter opens context-sensitive actions for a pin or bus.
  • n creates a board-approved named I2C, SPI, UART, MIDI, or 1-Wire bus.
  • Bus creation uses arrows to select fields and values; the generated bus name
  • can be edited directly.

  • Runtime buses can be attached, detached, or removed when their lease state
  • permits it. Their Autostart action idempotently appends the matching expansion bus create ... command to /.shell/startup. Direct GPIO and PWM assignments can be created and released from the Pins or Layout view.

  • A selected I2C bus has a Set I2C speed action with 100 kHz, 400 kHz, and
  • 1 MHz choices. The change applies at runtime even while devices lease the bus.

  • r refreshes; q, Esc, or the app-exit key exits.

Applications · SolarOS manual

launcher

Configurable native graphical launcher for display shells. It draws the configured grid, centers one icon in each occupied cell, and shows the selected icon larger with its title centered underneath. Arrow keys move between occupied cells, Enter opens the selection, and a pointer selects and opens items by point and click. Esc or the app-exit key returns to the shell.

Usage:

launcher [config.json]

Without an argument, Launcher reads launcher.json from the active storage root. The first run creates a usable default file there. A supplied path selects another configuration. Launcher reloads the file whenever a child application returns, so editing the configuration through a launcher item takes effect immediately.

The file is JSON. layout.columns and layout.rows define a grid from 1 by 1 through 8 by 8. Each item has a displayed name, an Open Iconic icon name, a SolarOS shell command, and a zero-based column and row. A cell can contain at most one item, and a configuration can contain up to 32 items:

{
  "layout": {"columns": 3, "rows": 2},
  "items": [
    {"name": "Files", "icon": "folder", "command": "files", "column": 0, "row": 0},
    {"name": "Manual", "icon": "book", "command": "help", "column": 1, "row": 0},
    {"name": "Wi-Fi", "icon": "wifi", "command": "wifi", "column": 2, "row": 0},
    {"name": "Sketch", "icon": "brush", "command": "sketch", "column": 0, "row": 1}
  ]
}

Icon names are the lowercase, hyphenated Open Iconic names, such as book, browser, brush, calculator, clock, cog, document, folder, home, musical-note, pencil, tablet, terminal, and wifi. Legacy numeric IDs from 0 through 222 remain accepted for existing configurations.

Commands use the normal SolarOS shell parser, so arguments, aliases, scripts, and application availability checks behave exactly as at the prompt. A launched foreground application returns to Launcher when it closes. A command that does not open an application returns to the shell so its output remains visible.

Applications · SolarOS manual

less

Terminal pager for text files. It preserves original text layout and is useful for quick file inspection.

Usage:

less <file>

Controls:

  • Up/Down or j/k scroll one line.
  • Page Up/Page Down, b, or Space page.
  • Home/End or g/G jump to start/end.
  • Ctrl+F or / opens Find. F3 or n jumps to the next
  • case-insensitive match; N jumps to the previous match. Search wraps.

  • q, Esc, or app-exit key exits.

Applications · SolarOS manual

logic

On-device logic analyzer waveform viewer. It displays the latest capture made by the shared logic analyzer service or the SUMP job. With pin arguments it makes a new local capture before opening the viewer.

Usage:

logic
logic <pin[,pin...]> [rate-hz] [samples] [trigger=<pin>]

Examples:

logic
logic 1,2,3,17
logic 1,2 500000 8192
logic 1,2,3,17 10000 4096 trigger=1

trigger=<pin> waits for the next rising or falling edge on that runtime-safe GPIO before sampling. The trigger pin may be one of the captured data pins, as in the last example, or a separate GPIO that is not displayed as a channel.

Controls:

  • Left/Right pans through the capture.
  • +/- or Page Up/Page Down changes the time scale.
  • r captures again with the current local configuration when the SUMP job is
  • stopped.

  • a or Home shows the complete capture.
  • q, Esc, or the app-exit key exits.

The app is compiled only for boards with graphics and runtime-safe GPIOs. While the SUMP job is running, the app remains a viewer and automatically reloads captures received from the host.

Applications · SolarOS manual

lua

Embedded Lua runtime. It can run an interactive REPL or execute .lua scripts from storage. Lua scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.

Usage:

lua
lua file.lua [args...]

Controls:

  • exit([code]) returns from the REPL or script and reports the optional exit
  • code to the launching shell.

  • App-exit key interrupts running code or exits.

Applications · SolarOS manual

Native SolarOS agent

The agent application is a foreground chat that can inspect the current device, consult this manual, edit files, and run installed Python or Lua runtimes. It exposes only tools allowed by the current firmware and tool policy.

Configure it

The Responses API is recommended for OpenAI reasoning models:

agent config endpoint https://api.openai.com/v1/responses
agent config model gpt-model
agent config key api-key
agent config reasoning medium
agent config tools confirm
agent config max-tools 16
agent status

The default confirm policy runs read-only tools automatically and asks before sensitive reads, file changes, or script execution. Use readonly when the agent should only inspect the system.

Start a conversation

Run agent or agent new, enter a message, and wait for the response. The bottom bar shows the latest input, output, and total token counts. Tool activity stays in the conversation, while usage does not.

Completed turns are saved. Inspect and resume them explicitly:

agent list
agent resume SLOT
agent delete SLOT

Slots are numbered 1 through 3 on internal flash or 1 through 8 on SD. When all slots are occupied, a new conversation replaces the oldest one. agent resume restores the transcript. Responses endpoints continue from the saved provider response ID; Chat Completions endpoints receive a bounded local message window. Bare agent always starts a new conversation.

Use agent ask PROMPT for one request. It leaves the answer visible until you press Esc or the app-exit key; one-request answers are not added to the conversation store.

Inspect background workloads

The agent can inspect job admission without starting or stopping anything. For example, ask:

Which background jobs are running, and which stopped jobs could start now?
Why is email-sync waiting?

The answer is based on the same worker-stack admission policy used by SolarOS. It includes current internal and PSRAM headroom, each job's declared stack placement, generation, current resource claims, start disposition, and last error. A ready disposition covers declared worker-stack admission only; a job may still need dynamic buffers or an argument-selected resource when it actually starts.

Troubleshooting

  • agent status shows the endpoint, model, policy, request failures, and memory
  • telemetry without printing the API key. It also reports how many tool calls the last request used from its configured budget.

  • If a generated script guesses an API or display name, ask the agent to call
  • solaros_reference with the language and exact task, then call the relevant discovery tool. The reference lookup returns focused sections from the firmware's Python and Lua manuals rather than only their page summaries.

  • If a call is denied, the model receives a structured denial and can explain
  • the result or choose another approach.

Quick reference

Use agent or agent new for a new durable foreground conversation, agent list to inspect saved slots, agent resume SLOT to restore one, and agent delete SLOT to remove one. agent ask PROMPT makes an unsaved one-shot request. Configure endpoint, model, key, reasoning, tool policy, and maximum tools with agent config. agent tools reports the installed typed tools and their policy disposition. jobs_list is read-only and reports actual job admission and memory state; it does not start or stop jobs. The default confirm policy runs read-only tools and requires local approval for sensitive, mutating, or disruptive calls. Esc or the app-exit key returns to the shell.

Applications · SolarOS manual

notes

Markdown-backed checklist and category manager. It stores unchecked and checked items and supports one level of category folding. A persistent bottom help bar shows the available controls, with status or text input directly above it.

Usage:

notes [file.md]

Controls:

  • Up/Down navigate.
  • Space toggles an item. After an active item is marked done, the selection
  • remains on the next active item when one is available.

  • a adds an unchecked item below the selected item. On a category, it adds the
  • first item in that category; from the done section, it adds at the end of the active items.

  • c adds a category.
  • Enter edits the selected line.
  • d or Delete deletes the selected item/category.
  • t tidies the note by deleting all completed items.
  • Shift+Up/Shift+Down reorders items within a category or moves the selected
  • category together with all of its items.

  • Left/Right collapse/expand a category.
  • q, Esc, or app-exit key exits.

Applications · SolarOS manual

player

Interactive WAV/MP3 player and the user-facing counterpart to aplay. player keeps a persistent playlist under .player on the current storage root. Opening an audio file from Files adds it to that playlist, selects it, and starts playback. Missing files remain listed so removable media can be reattached.

Usage:

player [--tui] [file.wav|file.mp3]

Player selects its graphical interface on a graphical shell and its text interface on a port shell. --tui forces the text interface even when the launching shell has graphics.

On a graphical session, Tab switches between Play and Playlist. The Play tab uses the top two-thirds for a cassette visualizer by default; V cycles through Cassette, Oscilloscope, and Spectrum. The cassette reels turn only while audio plays and show track progress when duration is known. Left/Right plays the previous or next track in the playlist ring, Enter plays or stops, Space pauses or resumes, and Up/Down adjusts volume. On the Playlist tab, Up/Down selects, Enter starts the track and returns to Play, A opens the WAV/MP3 file browser, and Delete removes the selected playlist entry.

The text interface is one playlist screen: Up/Down selects, Enter plays or stops, Space pauses or resumes, A opens the filtered file browser, Delete removes an entry, and Esc exits. Its bottom status line shows the playing, paused, or stopped state with elapsed and total time. Playback follows the resumable app while another foreground session is selected and stops when Player closes. End of file advances to the next playlist entry.

WAV playback converts the file's mono/stereo channel count and sample rate to the selected output stream. A mono recording therefore plays through a fixed stereo device without changing the recording stored on disk.

Applications · SolarOS manual

Playground

playground is the catalog browser for community Python and Lua applications. It downloads one catalog from the configured repository and presents its categories as a foldable tree. Applications are scripts and assets, not firmware packages.

Start the browser:

playground
playground search QUERY...
playground install APP-ID [auto|flash|sd]
playground run APP-ID [ARG...]
playground delete
playground reload
playground refresh
playground storage [flash|sd]

Opening the TUI loads the saved local catalog without accessing the network. Press r in the tree, or run playground refresh, to download and save the current catalog. Run playground reload to explicitly reload the saved copy. Press / to search names, descriptions, authors, tags, and categories.

playground delete recursively deletes the catalog and every installed Playground application from the configured storage target, then clears the loaded catalog from memory. It retains the configured source and storage preferences. It also removes the legacy hidden .solar/playground directory when present.

The same operations are available directly from the shell. search prints matching catalog IDs, runtimes, names, and installation markers. install downloads and verifies one ID to the configured filesystem, or to an explicit flash or sd target. The omitted and auto targets both use the persistent Playground storage setting. run resolves the installed manifest and launches its Python or Lua runtime directly; it does not need the catalog to be loaded and does not create a Playground session. Additional arguments after the application ID are passed to Python through sys.argv or to Lua through arg. For example:

playground run qr-share --file /notes/wifi.txt

Every valid installed application also becomes a command named by its ID. For example, installing qr-share makes this equivalent:

qr-share --file /notes/wifi.txt

Playground rebuilds these aliases automatically in the managed /.shell/playground file after installation, update, uninstall, bulk deletion, and service initialization. User aliases in /.shell/alias take precedence; native firmware commands and applications cannot be replaced by a Playground alias. Use playground run APP-ID if an application ID has such a collision.

--file PATH is the canonical optional convention for a Playground script's primary input file. After --file, Tab completes filesystem paths. This also works through a multi-token alias such as run playground run.

After the catalog is loaded, Tab completes application IDs for install and installed IDs for run without keeping a second catalog list in memory. The generated direct commands participate in normal command completion even when the catalog is not loaded.

Opening the Playground TUI never accesses the network automatically. It shows the catalog saved by the last successful refresh. Press r or run playground refresh when you explicitly want to download the current catalog.

Application markers:

  • [ ] is available but not installed.
  • [I] is installed at the catalog version.
  • [U] has an update available.
  • [!] cannot run on this board or firmware.

Select an application and press i to install or update it directly from the catalog tree. Press u to uninstall an installed application after confirmation. The same actions are available after pressing Enter or Right for details; r runs an installed copy.

The catalog and default application installations live on the persistent storage target. Without a saved preference, Playground selects SD when it is mounted and flash otherwise. Inspect or change the setting with:

playground storage
playground storage flash
playground storage sd

The saved catalog lives at:

<configured-filesystem>/playground/catalog.json
<configured-filesystem>/playground/catalog.source

Installed files live on the selected filesystem:

<filesystem>/playground/python/APP-ID/
<filesystem>/playground/lua/APP-ID/

SD installations take precedence when the same application also exists in flash. Changing the storage setting does not migrate an existing catalog or installed applications. Refresh the catalog after changing it. When sd is configured, the SD card must be mounted to load or refresh the catalog and to install applications.

Sources and safety

The default catalog is generated by github.com/nilseuropa/solar_os_playground. Use a compatible fork by setting either its GitHub repository URL or the URL of its generated dist/catalog.json:

playground source
playground source https://github.com/example/solar_os_playground
playground source https://example.invalid/path/dist/catalog.json
playground source reset

GitHub repository URLs resolve to main/dist/catalog.json. The chosen source is stored in NVS. Each archive is checked against the exact size and SHA-256 digest declared by the downloaded catalog before installation. Installation is staged and replaces the prior version only after extraction and entry-file validation.

Playground applications are not sandboxed. Once started, they have the normal permissions of the SolarOS Python or Lua runtime. Only use repositories and applications whose maintainers you trust.

An active Playground script is a normal foreground Python or Lua application. It can use solaros.input to receive touch, mouse, and joystick events routed to its session; a script launched by a headless source runner cannot receive that foreground event stream.

Controls

  • Up/Down, Page Up/Page Down, Home/End: navigate.
  • Enter, Space, Left, Right: fold categories or open details.
  • /: search the current catalog.
  • r: refresh in the tree, or run from details.
  • i: install or update the selected application from the tree or details.
  • u: uninstall the selected application from the tree or details, after confirmation.
  • Esc: cancel or go back in nested views; exit from the catalog tree.
  • q or the app-exit key: exit.

Quick reference

Run playground to browse the configured catalog. Use / to search, Enter for details, i to install, u to uninstall, and r to run. Refresh with r in the tree or playground refresh; use playground reload to load the saved catalog without a network request. Use playground source [URL|reset] to inspect or change the catalog source, and playground storage [flash|sd] to choose persistent catalog and application storage. Packages are hash-checked but scripts are not sandboxed. Shell automation can use playground search QUERY, playground install ID [auto|flash|sd], playground run ID [ARG...], and playground delete. Installed IDs are also automatic shell commands backed by /.shell/playground. Active scripts can read foreground pointer and axis events through solaros.input.

Applications · SolarOS manual

playground

Browse the configured community catalog as a foldable category tree, search applications, and install, update, uninstall, or run Python and Lua scripts.

Usage:

playground
playground search QUERY...
playground install APP-ID [auto|flash|sd]
playground run APP-ID [ARG...]
playground delete
playground refresh
playground reload
playground source [repository-or-catalog-url|reset]
playground storage [flash|sd]

Press / to search, Enter to open an application, and use the actions shown in the bottom bar. Press i on an application in the catalog tree or details page to install it, and u to uninstall it after confirmation. Packages are verified by size and SHA-256 and installed under /playground/ on the permanently configured flash or sd storage. Community scripts run with the normal permissions of their runtime and are not sandboxed.

Catalog and application files are stored under /playground/ on the selected filesystem. playground delete recursively removes that entire directory, clears the loaded catalog from memory, and also removes the legacy hidden .solar/playground directory when present. Source and storage preferences are retained.

The shell subcommands use the local catalog for browsing and installation: refresh downloads and saves it, reload loads that saved copy without network access, search prints matches, and install downloads and verifies an application by ID. run reads an installed application's own manifest, so it does not require a catalog reload. It launches the declared Python or Lua runtime without creating a Playground session. Installation also adds the app ID to the managed /.shell/playground aliases, so qr-share --file /notes/wifi.txt is equivalent to playground run qr-share --file /notes/wifi.txt. Uninstalling removes the generated alias. Arguments are forwarded unchanged to the selected script. Opening the TUI reloads the saved catalog automatically and does not refresh it. At the top-level catalog tree, Esc, q, and the app-exit key exit Playground.

playground storage shows the persistent catalog and default application storage. Set it with playground storage flash or playground storage sd. Without a saved preference, Playground selects SD when it is mounted and flash otherwise. Omitting the install target, or specifying auto, uses the selected setting.

See Playground for controls, storage layout, source selection, and the trust model.

Applications · SolarOS manual

plot

Graphical plotter for DAQ CSV files and live scalar streams. It is compatible with CSV generated by the daq job.

Usage:

plot <scalar-stream...> [--rate ms]
plot -f <file.csv> [column...]

Examples:

plot temperature humidity --rate 1000
plot -f /logs/env.csv temperature humidity

--rate is a best-effort live sampling interval in milliseconds. Rates below 25 ms request faster runtime ticks while the screen redraws no more often than once every 25 ms, so fast acquisition does not force an equally fast display refresh.

Controls:

  • Left/Right pan.
  • Up/Down select series.
  • +/- or Page Up/Page Down adjust visible window/zoom.
  • a or r resets the view.
  • Space pauses/resumes live mode.
  • q, Esc, or app-exit key exits.

Applications · SolarOS manual

python

Embedded MicroPython runtime. It can run an interactive REPL, .py scripts, or .mpy files from storage. Python scripts can use SolarOS service bindings when the selected firmware includes the corresponding packages. Foreground scripts can consume touch coordinates, relative mouse motion, buttons, and joystick axes through solaros.input.

Usage:

python
python file.py [args...]
python file.mpy [args...]

Controls:

  • exit([code]) returns from the REPL or script and reports the optional exit
  • code to the launching shell.

  • App-exit key interrupts running code or exits.

Applications · SolarOS manual

reader

Graphical document reader for plain text, Markdown, and EPUB. It remembers reading position and zoom per opened file when storage is available.

Usage:

reader [--pager] <file.txt|file.md|file.epub|man:topic>

With --pager, Up and Down page instead of scrolling one layout row. Each forward page starts with the final visible row from the previous page, so mixed font sizes and wrapped Markdown retain a precise reading overlap.

Controls:

  • Up/Down scroll by layout row, or page when --pager is active.
  • Page Up/Page Down page with the same precise row overlap.
  • Home/End jump to start/end.
  • +/- adjust zoom.
  • Ctrl+F or / opens Find. F3 or n jumps to the next
  • case-insensitive match; N jumps to the previous match. Search wraps.

  • Esc exits search state first; otherwise exits.
  • q or app-exit key exits.

Applications · SolarOS manual

recorder

Interactive GUI/TUI counterpart to arecord. Recorder writes PCM WAV files so the channel count, sample rate, and resolution travel with the recording and the result can be played immediately. It accepts any registered signed-16-bit PCM capture stream. On the Waveshare board it initially selects audio0.capture. Mono/stereo output, 8/16-bit file resolution, and sample rates from 8 kHz through 48 kHz are converted from the selected stream as necessary.

Usage:

recorder [--tui] [file.wav]

The optional path supplies the initial recording folder and filename. The Setup view can edit the filename or select another destination directory with the shared file browser. When the filename is empty, each recording gets a unique local-time name such as rec-260810-113045.wav; a numeric suffix avoids overwriting a file created in the same second. Recorder remembers its selected input, folder, format, hardware input gain, output volume, and visualizer in .recorder/settings.bin on the current storage root. An explicit path argument overrides the remembered folder. The filename is not remembered, so reopening Recorder cannot accidentally reuse a one-off name.

Recorder selects its graphical interface on a graphical shell and its text interface on a port shell. --tui forces the text interface even when the launching shell has graphics.

Tab switches between Record and Setup. The Record view uses the shared cassette widget by default; V cycles through Cassette, Oscilloscope, and Spectrum. R starts recording, Space pauses or resumes, S stops and finalizes the WAV header, and P plays the most recent or selected recording. M starts or stops monitoring: it continuously streams the selected input to the default output and visualizer. Monitoring can remain enabled when recording starts and can be toggled while the WAV writer continues. Up/Down changes monitor and playback volume. The cassette reels move during recording and playback, but remain still during monitoring. Oscilloscope and Spectrum remain live during monitoring. Active visualizers are presented on the 40 ms app tick (nominally 25 frames per second), independently of the audio block rate.

Setup selects the capture stream, recording folder, mono/stereo conversion, sample rate, 8/16-bit WAV resolution, input gain, and output volume. Enter on Folder opens the WAV-filtered browser. D selects its current directory as the recording folder; Enter on a WAV file plays it and returns to Record. Input gain is not a generic stream multiplier: Recorder enables it only when the selected capture stream belongs to an audio device that advertises hardware input-gain control. Thus audio0.capture on the Waveshare controls the ES7210 microphone gain, while an unrelated generic stream shows Input gain: n/a. The hardware gain is system-wide; Recorder does not add hidden digital amplification to the saved PCM.

The text interface exposes the same setup values, browser, transport keys, monitoring, pause behavior, and playback path. Recorder is resumable: capture, monitoring, or playback continues while its UI session is in the background, and closing the app stops the worker and finalizes an active recording.

Applications · SolarOS manual

scp

SCP file transfer over SSH. It supports password or key authentication through the shared SSH transport and host lookup/known-host storage. When user@ is omitted, SCP uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts, preserves an explicit user@ prefix, and appends : after a unique host match.

Usage:

scp [-P port] local [user@]host:remote
scp [-P port] local [user@]host:
scp [-P port] [user@]host:remote local
scp [-P port] [user@]host:remote-glob dir
scp [-P port] [user@]host:remote

Remote download paths can use * or ?. The local target must be an existing directory for remote wildcard downloads.

SCP runs inline in the shell. Its progress and result remain in the terminal scrollback, and usage errors or a completed transfer return directly to the prompt.

Controls:

  • App-exit key cancels an active transfer.

Applications · SolarOS manual

sftpsync

Incremental file and directory synchronization over SSH. The SolarOS client uses the server's SFTP subsystem, so it works with a normal SSH server without requiring a matching remote program. It skips regular files whose size and modification time already match. It does not delete destination-only files.

Usage:

sftpsync [-r|-a] [-n] [-P port] local [user@]host:remote
sftpsync [-r|-a] [-n] [-P port] [user@]host:remote local

-r, -a, or --recursive enables directory traversal. -n or --dry-run prints the changes without writing them. -P selects a non-default SSH port. Password and key authentication, known hosts, and aliases in /.ssh/hosts are shared with ssh and scp. A local or remote directory operand names the directory to synchronize; its contents are updated below that path. Traversal is limited to eight nested directory levels. Symbolic links and other special file types are skipped. Active transfers show an updating progress bar for each changed file, followed by the aggregate file and byte counts.

Controls:

  • Ctrl+C or the app-exit key cancels the password prompt or an active
  • synchronization.

Applications · SolarOS manual

sheet

CSV viewer for small data tables. It is intended as a companion to daq logs and simple spreadsheet-like inspection.

Usage:

sheet <file.csv>

Controls:

  • Arrows move through cells.
  • Page keys scroll.
  • App-exit key exits.

Applications · SolarOS manual

sketch

Pointer-driven graphical paint application. Its layout follows classic desktop paint programs: Save, Open, Import, and the sidebar controls share one aligned, equal-sized button grid; color and pattern choices are in the bottom bar. Sketch uses a compact four-color canvas and stores finished documents as interoperable indexed-color PNG files. Color TFTs show the native palette; one-bit displays use the existing dithered rendering path.

Usage:

sketch
sketch file.png

Sketch is intentionally not gated on a pointer hardware capability. If no ready absolute or relative pointer is registered when the app starts, a popup explains that a pointer can be attached at any time. Press Enter or Esc to dismiss it; the first attached pointer is accepted without restarting the app.

Controls:

  • Click Save to update the current PNG. For an untitled or imported image, the
  • first save selects the next free Sketches/sketchNNN.png path on persistent storage. Saves use a synced staging file, backup rename, atomic replacement, and rollback.

  • Click Open to select a PNG as the current document. Click Import to flatten a
  • PNG, JPEG, or GIF into the current four-color canvas; the next save creates a new PNG instead of overwriting the imported source.

  • The sidebar selects pen, straight-line, rectangle, ellipse, bucket fill,
  • or eraser drawing. The weight control cycles through 1, 2, 4, and 8 pixels. Eraser uses the selected weight and restores white pixels. Clear immediately resets the complete canvas to white.

  • Line, rectangle, and ellipse show an exact non-destructive preview during the
  • drag, including the selected color, pattern, and stroke weight. Releasing the pointer commits that preview to the canvas.

  • The first four bottom swatches select white, red, blue, or black. The next
  • four select solid, checker, dot, or diagonal-hatch application.

  • Keyboard fallbacks are S Save, O Open, I Import, P Pen, L Line,
  • R Rectangle, E Ellipse, B bucket fill, X eraser, and C clear. Arrows and Enter operate the file browser.

  • Q, Esc, or the app-exit key exits. Suspending and resuming retains the
  • current cold-allocated canvas; closing the app releases it and all browser or image resources.

Applications · SolarOS manual

ssh

Interactive SSH client. It supports password and key authentication, known hosts, hostname lookup through /.ssh/hosts, UTF-8 text, VT-style controls, and remote full-screen terminal applications. When user@ is omitted, SSH uses the NVS-backed SolarOS identity user. Tab completion reads aliases from /.ssh/hosts and preserves an explicit user@ prefix.

Usage:

ssh [user@]host [port]

On display shells, SSH owns a resumable terminal buffer. Switching to the SolarOS shell does not mix the local and remote scrollback, and switching back restores the SSH buffer. On UART and USB CDC port shells, SSH uses the shared port scrollback. Disconnecting returns directly to the local prompt. Remote terminal control sequences still work, including full-screen applications.

Controls:

  • Most keys are sent to the remote host, including Esc, Alt+key, cursor keys,
  • Ctrl combinations, and function keys.

  • App-exit key closes the SSH app.
  • Alt+Tab leaves the session running in the background on display builds.

Applications · SolarOS manual

synth

Open the native synthesizer and sound designer:

synth [--headless]

The app uses the same eight-voice fixed-point engine exposed to Python and Lua, but keyboard and MIDI events reach it directly. On a display, the default graphical interface provides the complete editor described below. --headless suppresses graphical rendering while keeping the same note keys, editing keys, published parameters, control bindings, and MIDI input active. This permits use from a port shell on a headless board after a playback device such as the LEDC PWM audio expansion has been attached. Its Play tab contains the waveform, envelope, volume, ADSR, and piano controls. The Wave tab is a graphical wavetable editor with selectable 16, 32, or 64-point resolution; edits reshape held notes immediately while the piano remains playable, and switching tabs does not rewrite the custom wavetable. The Filter tab adds a resonant low-pass response graph, cutoff, resonance, envelope amount, and an independent graphical ADSR filter envelope. The Oscillator 2 tab adds a second per-note source with waveform, octave, fine detune, and unity-safe mix controls. Both oscillators share the filter and envelopes. The Preset tab provides eight factory sounds and eight persistent user slots. User presets capture both oscillators, both envelopes, the filter, mono/poly mode, glide, and the complete custom wavetable. The Glide tab provides polyphonic or monophonic last-note playback, a hold mode for toggle-style piano keys, and portamento. Hold is session performance state and is not part of a preset. The display also reports active voices, output sample rate, and audio errors. The volume button changes the shared SolarOS speaker volume.

On display targets smaller than 240 pixels wide or 200 pixels high, Synth automatically replaces the full editor with a parameter HUD. The selected control gets a large value and level bar; waveform controls and the Wave tab retain compact graphs; Presets shows one slot at a time. Targets smaller than 112 by 56 pixels use a footer-free micro layout. Physical note keys and MIDI remain active. External parameter/control changes focus the changed parameter until the next local navigation action, which makes a 128-by-64 SH1106 or SSD1306 useful as a synthesizer appliance display. After attaching it as oled0, open Synth on that target with session create synth oled0. During active playing, compact displays defer visualization until note input is quiet so synchronous display transfers cannot take priority over new notes. MIDI notes highlight the matching pitch class on the on-screen piano just like physical note keys.

Controls:

  • A W S E D F T G Y/Z H U J K play one chromatic octave. The positions are
  • physical, so the Y position is the Z key on a German keyboard. Note input remains active on all tabs.

  • Tab cycles through Play, Filter, Wave, Oscillator 2, Glide, and Presets.
  • Number keys 1 through 6 select those tabs in that order.

  • X hides or shows the on-screen piano keyboard on every tab. When it is
  • hidden, the tab's knobs, graphs, panels, or preset list use the freed space; the physical note keys and MIDI input remain active.

  • On Glide, select Hold and use Up or Enter to enable it. Each physical or
  • terminal piano-key press then toggles its note on or off, and releasing the key does not stop the note. Use Down or Enter to disable hold and release all latched app-key notes. MIDI Note On, Note Off, and sustain keep their normal behavior.

  • On Play, Left/Right selects the waveform, global volume, or an ADSR knob;
  • Up/Down changes it, and +/- changes note velocity.

  • On Wave, Left/Right moves the edit cursor and Up/Down changes the
  • waveform. Shifted arrows move or draw faster, and +/- changes brush size.

  • On Wave, Enter cycles the editor through 16, 32, and 64 points. New sessions
  • start at 16 points. The current waveform is resampled rather than reset.

  • On Wave, B applies the next square, triangle, saw, Supersaw, sine, or flat
  • baseline; R restores that baseline, M smooths, N normalizes, 0 clears, and Backspace or Delete exchanges the current waveform with the undo state.

  • On Filter, Left/Right selects cutoff, resonance, envelope amount, or a
  • filter ADSR knob; Up/Down changes it, and +/- changes note velocity.

  • On Oscillator 2, Left/Right selects waveform, octave, fine detune, or mix;
  • Up/Down changes it, and +/- changes note velocity. Octave ranges from -2 through +2, detune from -100 through +100 cents, and mix from 0 through 100 percent.

  • On Preset, arrows select a factory or user slot and Enter loads it. V
  • saves the current sound to the selected user slot. Factory presets are read-only.

  • Page Up/Page Down changes octave from 2 through 6 on any tab.
  • Esc or the app-exit key exits and immediately releases audio ownership.

For an external MIDI keyboard, create and start a named MIDI bus before opening Synth:

expansion bus create midi midi0 tx=gpio1 rx=gpio2
job start midi midi0
synth

MIDI Note On/Off, velocity, sustain (CC64), All Sound Off (CC120), and All Notes Off (CC123) are supported on all 16 channels.

Published parameters

While Synth is running, control parameters exposes these native continuous parameters:

  • synth.volume: 0 through 100 percent, linear.
  • synth.envelope.attack: 0 through 10000 ms, linear.
  • synth.envelope.decay: 0 through 10000 ms, linear.
  • synth.envelope.sustain: 0 through 100 percent, linear.
  • synth.envelope.release: 0 through 10000 ms, linear.
  • synth.filter.cutoff: 40 through 18000 Hz, logarithmic.
  • synth.filter.resonance: 0 through 100 percent, linear.
  • synth.filter.envelope.amount: 0 through 100 percent, linear.
  • synth.filter.envelope.attack: 0 through 10000 ms, linear.
  • synth.filter.envelope.decay: 0 through 10000 ms, linear.
  • synth.filter.envelope.sustain: 0 through 100 percent, linear.
  • synth.filter.envelope.release: 0 through 10000 ms, linear.
  • synth.osc2.octave: -2 through +2 octaves, linear.
  • synth.osc2.detune: -100 through +100 cents, linear.
  • synth.osc2.mix: 0 through 100 percent, linear.
  • synth.glide: 0 through 2500 ms, linear.

These paths exist only while the app is active. Bindings remain configured when Synth is suspended or stopped and reconnect when it resumes. See man controls for control creation, soft pickup, and MIDI CC mappings.

Applications · SolarOS manual

telnet

Telnet client for classic TCP terminal sessions. It supports basic Telnet option negotiation, terminal type reporting, window size reporting, and raw mode.

Usage:

telnet host [port]
telnet -r host [port]

Controls:

  • App-exit key closes the Telnet app.
  • Alt+Tab leaves the session running in the background on display builds.

Applications · SolarOS manual

view

Graphical image viewer. It supports the image formats compiled into the current firmware, including common PNG/JPEG/GIF/WebP paths and automatic animated GIF playback when the media package is enabled. Images are decoded as RGB on a negotiated indexed-color display and as grayscale on a one-bit display. In the default fit mode, JPEG color conversion writes display-sized output directly, so a large source photograph does not require a full-size RGB destination.

Usage:

view [-fit|-actual] <image>

Controls:

  • Arrows pan.
  • f toggles fit/actual mode. Fit mode scales the image up or down to the screen.
  • 0 selects actual size.
  • 1 selects fit-to-screen.
  • Esc or app-exit key exits.

Applications · SolarOS manual

web

Simple graphical web browser for lightweight HTML pages. It shares document and image rendering infrastructure with reader where possible. Embedded and direct PNG, JPEG, GIF, and WebP images retain color on indexed-color displays; one-bit displays keep the grayscale decode and dither path.

Usage:

web http://host/
web https://host/path

Controls:

  • The top toolbar provides Back, Reload, and Forward. Pointer or touch presses
  • activate the toolbar and open links or form controls directly.

  • Left or b goes back, Right or f goes forward, and r reloads while
  • retaining the current reading position.

  • Up/Down or k/j scroll one rendered line. Page Up/Page Down, Home,
  • and End provide larger movement.

  • n or Tab selects the next link or form control; p selects the previous
  • one. Selection stops at each end, and Enter activates it.

  • Links and plain page text use the same font size. Links remain underlined,
  • headings remain bold, and +/- or Ctrl++/Ctrl+- reflow page text through the same five zoom levels as Reader.

  • Ctrl+F opens Find and F3 jumps to the next case-insensitive match in
  • rendered page text, wrapping at the end.

  • Esc or app-exit key exits.

Applications · SolarOS manual

webradio

Stream a direct MP3 URL through the default registered audio output. On a graphical display shell, WebRadio opens a two-tab media-player GUI. On UART, USB CDC, Telnet, SSH, and other text shells, it opens a station-list TUI.

The catalog is stored as .solar/webradio/catalog.bin on the current storage root and starts with the Nightride, Chillsynth, Datawave, Spacesynth, Darksynth, Horrorsynth, EBSM, and Rekt streams. Catalog changes survive reboot. On first use after an upgrade, WebRadio moves an older NVS catalog to this file and removes the large NVS blob. reset restores the initial list.

Usage:

webradio
webradio --tui
webradio https://stream.nightride.fm/nightride.mp3
webradio list
webradio add MyStation https://example.net/live.mp3
webradio remove MyStation
webradio reset

--tui forces the station-list TUI even on a graphical shell and can be combined with a direct stream URL. Catalog-management commands also accept it as a harmless interface override.

URLs are literal HTTP or HTTPS MP3 stream URLs. WebRadio does not translate station names or website addresses and does not discover streams from HTML pages. The initial implementation does not support playlists, HLS, or AAC.

Controls:

  • Tab switches between the Player and Channels tabs in the GUI.
  • The Player tab gives the top two-thirds of the screen to a live PCM
  • oscilloscope or spectrum analyzer. V switches visualizers. The spectrum analyzer uses the shared DSP service, including PIE SIMD window and FFT paths on eligible ESP32-S3 boards.

  • On the Player tab, Left and Right play the previous or next catalog
  • channel. The catalog wraps as a ring. Space or Enter stops or resumes playback, and Up/Down changes global volume in five-percent steps.

  • The bottom third of the Player tab shows the channel, playback state, volume
  • bar, and previous, stop/play, and next controls.

  • On the Channels tab, Up/Down selects a channel, A adds one, E edits
  • one, and Delete removes one. Enter starts the selected channel and returns to the Player tab. Add and edit dialogs accept a name followed by a literal stream URL.

  • The TUI is a single catalog screen. Up/Down or J/K selects a channel,
  • +/- changes output volume, A adds a channel, E edits it, and Delete removes it. Enter plays the selected channel, Space stops playback, and R reconnects it. Add and edit use inline name and URL fields on the catalog screen; Enter advances or saves and Esc cancels.

  • Q, Esc, or the app-exit key exits when no catalog dialog is open.

The app is available on Wi-Fi builds even when the board has no built-in audio hardware. Playback starts when a default output device has been registered, including an output supplied by a runtime-attached expansion.

Network reception and MP3 decoding feed an app-owned PCM jitter buffer. A separate playback worker consumes that buffer at the audio device's steady rate. Both workers continue while the resumable WebRadio session is suspended or another foreground app is selected. Closing WebRadio cancels the network operation through its bounded read timeout, stops both workers, and releases the audio device. The network worker exclusively owns the HTTP request during that shutdown.

The playback worker, not the decoder, publishes visualizer samples after each audio write. The widgets retain only the latest played block and may drop intermediate display frames, so buffering does not put the visualization ahead of the audio. WebRadio uses 256 scope samples and a 256-point SIMD-eligible FFT, grouped into 32 displayed frequency bands.

service.signal-widgets provides the reusable thread-safe signed-16-bit oscilloscope and spectrum components used by the graphical player. The widgets accept mono or interleaved multichannel PCM and own their snapshot storage; applications retain ownership of their audio streams.

Applications · SolarOS manual

writer

Resumable graphical Markdown editor for PSRAM display boards. Inactive blocks are formatted like reader; the block containing the cursor and every block touched by a selection show their exact Markdown source. edit remains the portable text editor for port shells and boards without graphics or PSRAM.

Usage:

writer [file.md]

Without a path, Writer opens an untitled document and asks for a path on the first save. Existing files larger than 256 KiB are rejected without changing them. Saves use a synced same-directory staging file, backup rename, verified replacement, and rollback. Cursor, scroll anchor, and zoom metadata plus idle recovery snapshots live centrally under /.writer or the active persistent storage root. Writer offers a differing recovery snapshot when the document is opened again. Recovery uses R to recover, D to discard the snapshot, or C to cancel; the unsaved-changes dialog similarly uses S, D, and C.

Controls:

  • Left/Right move by UTF-8 codepoint; Up/Down move to the adjacent editable
  • visual line or Markdown block, including directly adjacent headings. Ctrl+Left/Ctrl+Right move by word. The insertion cursor blinks while idle.

  • Shift with arrows, Page Up/Page Down, Home/End, or document movement extends
  • the selection. Ctrl+Home/Ctrl+End jump to the document boundaries.

  • Ctrl+A, Ctrl+C, Ctrl+X, and Ctrl+V select all, copy, cut, and paste
  • through the shared SolarOS clipboard.

  • Ctrl+B, Ctrl+I, and Ctrl+K add or remove bold, italic, and link source.
  • F1 opens formatting for inline code, headings 1–4, bullet or numbered
  • lists, quotes, fenced code, and rules. Esc closes the active menu/dialog; from the editor it exits, using the save/discard/cancel prompt when dirty.

  • Ctrl+F opens Find and F3 jumps to the next case-insensitive match,
  • wrapping at the end. Ctrl+R prompts for find and replacement text.

  • Ctrl+S saves, Ctrl+Z/Ctrl+Y undo and redo, and Ctrl++/Ctrl+- adjust
  • zoom.

  • Esc and the app-exit key open the save/discard/cancel prompt when the
  • document is dirty. Writer writes recovery before suspend or stop and resumes in place.

Background jobs · SolarOS manual

SolarOS Jobs

This document covers the built-in background job registry. Jobs are autonomous workers such as log followers, DAQ capture, HTTP serving, Telnet shell access, SLIP, PPP, chatd, or NTP sync. Foreground applications are documented in apps.md, and shell commands are documented in commands.md. Port shells are sessions, started with session create shell <port> plus optional --term, --charset, and --size terminal settings. Display-target shells use session create shell <target>.

Job availability depends on the selected firmware flavor and board capabilities. The running system is authoritative:

jobs
job status [name]

jobs is intentionally compact so it fits on the built-in 65-column display terminal. It shows job name, state, declared worker-stack requirement, kind, event source, tick count, and resource count. Running rows are bold. Use job status <name> for the summary, owner string, worker-stack placement, last error, effective tick interval/deadline, runtime duration statistics, deadline misses, and claimed resources. Compact timing lines use interval/deadline in milliseconds, n for dispatches, us=last/max, and miss for deadline misses.

Job Control

CommandDescription
jobsList registered jobs in compact form.
job status [name]Show all jobs, or one named job with details.
job start <name> [args...]Start a job with optional arguments.
job stop <name>Stop a running job.

Only one instance of each job name can run at a time. Starting a running job first stops the previous instance, then starts it again with the new arguments.

Jobs have stable owner strings in the form job:<name>. Jobs that claim ports, files, streams, or network listeners publish those resources through the job status model. This keeps port/resource conflict messages readable and avoids job-specific inspection code in the shell.

Job states distinguish a deferred launch from a completed launch attempt:

  • waiting means SolarOS retained the start request because admitting the
  • declared worker stack would consume the internal-memory reserve. The job is not running yet; the scheduler retries it automatically as memory becomes available.

  • failed means SolarOS attempted the job's start callback and it returned a
  • terminal error. job status <name> prints that error. A no memory failure can still occur when a job or library needs dynamic memory beyond its declared worker stack; that memory is not predictable from the descriptor.

  • running means start completed successfully. stopped means there is no
  • active or retained launch request.

The STACK column is the declared worker-stack admission requirement in bytes, not a worst-case total-memory estimate. - means that the job declares no dedicated worker stack; it may still allocate dynamic buffers or use a shared service. Compare this column with the internal figures from mem policy. For an internal-stack background job, admission currently requires internal free memory of at least STACK + 1 KiB overhead + 32 KiB reserve, and an internal largest block of at least STACK + 1 KiB overhead. Treat a manual check as a snapshot: other work can allocate memory before job start, which is why SolarOS retains and retries a launch that loses that race.

Jobs that use byte-stream ports claim those ports while running. If a port is already owned, SolarOS reports the owner, for example job log owns cdc0. Radio listeners expose their radio as a custom job resource.

GPIO-key mappings claim each selected pin through the same resource model. The io application therefore shows assignments such as key:UP, their gpio-keys-job attachment owner, and the board's canonical pin policy without special GPIO-key display logic.

Tick intervals and execution-time deadlines are declared by each event-driven job. A zero descriptor value selects the runtime default. Deadline misses do not forcibly terminate a cooperative handler; SolarOS counts them, records the last and maximum duration, and emits rate-limited warnings. The DAQ and log handlers only enqueue work, so their stream, filesystem, and port I/O runs in isolated worker tasks instead of the display scheduler.

Compact list example:

NAME         STATE    STACK KIND        EVT  TICKS RES
batmon       running      - background  tick    17   1
log          stopped   6144 background  tick     0   0

Detailed status example:

job status log
NAME         STATE    STACK KIND        EVT  TICKS RES
log          running   6144 background  tick     8   1
  summary: stream SolarOS logs to a port or file
  owner: job:log
  worker stack: 6144 bytes (internal)
  tick: 250/2ms n=8 us=18/31 miss=0
  resources:
  - port   cdc0 rw

Useful ports:

cdc0
uart0

List available streams with:

stream

Startup

Jobs can be started from the normal startup script:

/.shell/startup

Example:

wifi on
job start ntp-sync once
job start batmon 60

batmon

Battery monitor. It periodically samples battery voltage, maintains a smoothed trend, estimates power state, and can request light sleep when the configured minimum voltage is reached.

Usage:

job start batmon [interval-sec]
job stop batmon
job status batmon

Defaults:

SettingValue
Interval60 seconds

Battery limits are configured with the battery shell command:

battery capacity <mAh>
battery min_voltage <volts>
battery max_voltage <volts>

Notes:

  • Discharging trend means battery power.
  • Charging trend means external power.
  • Voltage above max_voltage is a fast external-power shortcut.
  • Three consecutive samples at or below min_voltage while on battery request
  • light sleep.

Example:

job start batmon 60

bridge

Bidirectional byte bridge between two byte-stream ports, or between one byte-stream port and an active SolarOS Link instance.

Usage:

job start bridge <port-a> <port-b>
job start bridge <port> <link> [broadcast|destination-id]
job stop bridge
job status bridge

Example:

job start bridge cdc0 uart0

To expose a UART byte stream over a packet-radio Link:

job start radio-link link0 radio0 lora-eu868
job start bridge uart0 link0 broadcast

That direct Link form is best-effort. For an ordered, retransmitted stream, create a peer-bound virtual port and use the normal port-to-port bridge form:

link stream create link0 vser0 0x12345678
job start bridge cdc0 vser0

The remote device creates its matching vser0 and can attach a normal shell with session create shell vser0 --term dumb. On a headless DevKit, this leaves the primary uart0 shell free for administration while Linux uses USB cdc0 for the remote terminal.

Use a decimal or 0x 32-bit Link destination instead of broadcast for acknowledged unicast:

job start bridge uart0 link0 0x12345678

Notes:

  • The two ports must be different.
  • Both ports are claimed by the bridge job until it stops.
  • Link stream ports such as vser0 are normal byte-stream ports. Their stream
  • service supplies peer filtering, segmentation, ordering, retransmission, and bounded backpressure before the bridge sees bytes.

  • In Link mode, the serial port is claimed while the already-running Link
  • instance remains active under its transport job.

  • Available serial bytes are emitted as binary Link messages, each capped at
  • the Link payload MTU. Received text and binary payloads are written to the serial port without a separator, preserving byte-stream behavior.

  • broadcast is the default destination. Explicit destinations request normal
  • Link acknowledgements.

  • The bridge consumes the Link receive queue. Do not use link receive on the
  • same Link while the bridge is running.

  • Packet radio is usually much slower than UART. The bridge uses the existing
  • bounded Link queues and does not add an unbounded SRAM buffer; sustained serial input can therefore overrun the port or produce Link queue drops.

  • If the Link disappears, the bridge releases its serial port and stops with
  • the Link error.

  • This is the clean base for USB-to-UART converter style workflows.

gateway-sync

Background synchronizer for the gateway messaging provider. Start and stop it explicitly, using the same lifecycle as email-sync:

job start gateway-sync
job stop gateway-sync
job status gateway-sync

gateway-sync takes no polling interval. Unlike the periodic email-sync job, it maintains a live connection and applies its own exponential reconnect backoff. It can therefore be started before Wi-Fi has an address; it remains running and connects when the network becomes available. In /.shell/startup, use exactly:

job start gateway-sync

It owns transport connection lifetime, exponential retry, opaque resume cursors, joined-channel replay, and delivery of the gateway provider's shared outbound requests. The messaging service owns retained publication and Inbox projection. Replayed transport messages are deduplicated by the shared stable producer identity before another notification is published.

The gateway hello uses the SolarOS identity user and identity hostname values. Chat does not maintain separate user-name or device-name settings.

Stopping or closing app.chat has no effect on this job. Its worker performs transport startup, polling, and retry work outside the cooperative session/job scheduler.

The shared store retains at most 64 messages. SD-backed systems use the full-message /.messages/messages.bin ring. Systems using internal flash restore Chat history from the compact records already stored in /.inbox/messages.bin; no second ring is created, so Chat history cannot consume the remaining flash volume.

chatd

Local SolarOS chat gateway server. It is useful for testing the chat app or for small trusted local networks.

Usage:

job start chatd [port] [token] [--history path]
job start chatd [port] [token] [path]
job stop chatd
job status chatd

Defaults:

SettingValue
Port7777
Default channelgeneral
Maximum clients6
Maximum channels32
In-memory history64 events

Arguments are intentionally flexible. The first numeric argument is the port. The next non-option argument is the optional token. --history or --log selects an optional append-only history dump file.

Examples:

job start chatd
job start chatd 7777 secret
job start chatd 7777 secret --history /.shell/chatd.log

The local chat app can connect with:

chat local
chat 127.0.0.1:7777

On another SolarOS device or host on the same network, use the server IP:

chat 192.168.1.113:7777

Notes:

  • If a token is configured, clients must present the same token.
  • New clients receive the recent in-memory channel history.
  • Channel deletion is supported by the chat protocol and client.
  • The built-in server is a lightweight LAN gateway, not a hardened public chat
  • service.

daq

Data acquisition job. It captures scalar and event streams to timestamped CSV, or one byte or PCM audio source directly to a raw file.

The daq shell command is usually easier to remember:

daq
daq streams
daq start <stream...> <file> [options]
daq start <file> <stream...> [options]
daq stop
daq status

Direct job usage:

job start daq <stream...> <file> [--rate seconds|--rate-ms ms] [--append|--replace]
job start daq <file> <stream...> [--rate seconds|--rate-ms ms] [--append|--replace]
job start daq <byte-stream> <file> --raw [--rate-ms ms] [--append|--replace]
job start daq <audio-stream> <file> --raw [--rate-ms ms] [--append|--replace]
job stop daq
job status daq

Defaults:

ModeDefault interval
Scalar CSV1000 ms
Raw byte stream25 ms
Raw audio streamContinuous

Examples:

daq start temperature /logs/temp.csv --rate 60
daq start /logs/env.csv temperature humidity battery --rate 60
daq start uart0 /logs/uart0.bin --raw --rate-ms 25
daq start audio0.capture /logs/microphones.pcm --raw
job start daq /logs/env.csv temperature humidity battery --rate 60

Notes:

  • Multi-stream mode supports scalar and event streams only.
  • Raw capture is single-stream only and writes byte or PCM audio data directly.
  • Raw audio files contain the native format shown by
  • stream status audio0.capture; use arecord when a WAV container is needed.

  • CSV rows include a timestamp column and one value column per stream.
  • Available streams depend on board capabilities.

controls

Continuous-control mapper. It samples every configured scalar-stream control at 50 Hz, applies smoothing, deadband, calibration, and inversion, then updates changed native parameter and MIDI CC bindings.

control create cutoff adc1 0 3300 smooth=40 deadband=8
control bind cutoff parameter synth.filter.cutoff pickup=on
job start controls
job status controls
job stop controls

The job takes no arguments. Controls and bindings can be added or removed while it runs. A source read error and an unavailable target are retained in control and binding status instead of stopping the worker. Native parameter bindings retry when the application resumes and publishes the path again. MIDI bindings retry while the MIDI worker is stopped.

Control definitions are runtime configuration. Put the control create, control bind, and job start controls commands in /.shell/startup to restore a hardware setup after reboot. See man controls for calibration, manual script inputs, MIDI examples, and inspection commands.

gesture-listener

Gesture-to-command listener. Rules are configured independently with gesture bind, while this job owns observation and dispatch:

gesture bind source=gesture0 gesture=flick direction=east -- input emit ALT+RIGHT
gesture bind source=gesture0 gesture=flick direction=west -- input emit ALT+LEFT
job start gesture-listener
job status gesture-listener
job stop gesture-listener

Stopping the job preserves the rules, prevents new gesture actions, and drops queued actions. A command already executing finishes normally. Use gesture bindings to inspect rules and counters, gesture unbind <id> to remove one, or gesture unbind all to clear the table and reset the next ID to 1. Rules and job state are volatile; recreate them in the startup script when persistence is needed. The command worker is created on demand and releases its internal stack when its queue is idle.

osc

OSC 1.0 IPv4 UDP adapter for automatic incoming native-parameter writes and explicit named outbound stream, event-stream, or normalized-control bindings.

job start osc [listen=port] [target=host:port] [peer=ipv4]
job status osc
job stop osc

The listening port defaults to 9000. target= is optional for an incoming-only job and is required before an outbound binding can send. peer= accepts one exact IPv4 address and drops all other incoming sources.

The worker owns one UDP socket and a 6 KiB internal stack. It accepts packets up to 512 bytes, at most eight parameter updates per packet, immediate bundles only, and at most 100 accepted packets per second. Detailed status includes the listener, target, peer filter, inbound apply/error counters, outbound send/source errors, and the current binding count.

OSC has no authentication or encryption. Start the job only on a trusted LAN, SoftAP, or WireGuard path. Bindings are volatile and can be restored from /.shell/startup. See man osc for address mapping, binding syntax, limits, and the sampled-event caveat.

displayd

Authenticated HTTP display and remote control. It has two modes:

  • With a physical target such as display0, it mirrors and controls the
  • active session attached to that display without allocating another display framebuffer.

  • With web0, it creates an independent monochrome virtual display and a
  • detached display shell. Its logical dimensions match the board's main display (for example 384x288 on the Freenove PAL target). A headless board uses the historical 400x300 fallback. Apps launched from that shell stay on web0 and do not replace the foreground app on a physical display.

With no target argument, displayd mirrors display0 when it exists and otherwise creates web0. The latter makes the same command useful on headless PSRAM-equipped boards.

Usage:

job start displayd [target]
job stop displayd
job status displayd

Example:

wifi on
job start displayd
job start displayd web0

Starting the job prints a random six-digit access code. Open http://<device>/display, enter that code, and click the displayed image before typing. The browser frontend polls the native 1-bit U8g2 frame up to twenty times per second and performs pixel rotation in the browser instead of the HTTP server task. It sends bounded key input through the scheduler. Ctrl+] remains the application-exit key.

API:

GET  /api/displays
GET  /api/displays/<target>/frame.pbm
GET  /api/displays/<target>/frame.raw
POST /api/displays/<target>/input

All API requests require Authorization: Bearer <code>. The access code is never accepted in the URL. The built-in frontend itself is public so that it can prompt for the code, but keeps the supplied code only in page memory.

Notes:

  • web0 is registered as source=virtual, driver=framebuffer while the job
  • is running. Its framebuffer and session exist independently of whether a browser is connected.

  • displayd creates and owns the web0 shell session itself and prints its
  • session ID. Do not run session create shell web0 afterward; the target is already attached to the browser-controlled shell.

  • Ctrl+] exits a foreground app on web0 and returns to its detached shell.
  • The physical foreground session is unaffected.

  • The physical mirror reuses the active U8g2 display and does not create
  • another display session. The built-in display shell is registered as the session attached to display0.

  • A consistent 1-bit frame snapshot and same-sized raw transmit buffer are held
  • in PSRAM while the job runs. For the 400x300 Waveshare display they consume 30,400 bytes in total. web0 additionally owns a board-sized U8g2 framebuffer (15,200 bytes on Waveshare, 13,824 bytes on the 384x288 PAL target). HTTP transmission never holds a display or registry lock.

  • The snapshot is copied into the transmit buffer and released before network
  • I/O, so a slow browser cannot prevent newer display frames from being published.

  • The browser uses frame.raw to avoid per-pixel PBM conversion on the ESP32.
  • The PBM endpoint remains available for simple external clients.

  • If a browser is still reading a frame when the display presents again, that
  • publication is skipped rather than blocking the display.

  • Input is queued by the HTTP task and dispatched only by the normal SolarOS
  • scheduler. Both physical and virtual targets receive it through their active target-addressed session; browser control does not depend on the device's globally foreground session.

  • The server is plain HTTP. The six-digit code provides convenient access
  • control on a trusted Wi-Fi network but does not encrypt frames or input and is not intended for exposure to an untrusted network.

  • displayd and httpd share one HTTP server and may run simultaneously.

httpd

Static HTTP file server for a folder on mounted storage.

Usage:

job start httpd <folder>
job stop httpd
job status httpd

Example:

job start httpd /www

Notes:

  • Relative paths resolve under the default storage mount.
  • The server uses the ESP-IDF default HTTP port.
  • It shares the service-owned HTTP server with displayd.
  • It serves files and simple directory listings.
  • MIME types are provided for common text, image, audio, JSON, JavaScript, and
  • CSS files.

ftpd

Unencrypted FTP file server for one exported folder. The job supports one client at a time and passive IPv4 data connections.

Usage:

job start ftpd <folder> [port] [--user USER --password PASSWORD]
job stop ftpd
job status ftpd

Examples:

job start ftpd /shared
job start ftpd /shared 2121 --user solaros --password local-secret

Notes:

  • The default port is 21.
  • Login is anonymous by default. Anonymous clients use anonymous or ftp as
  • the username; the supplied password is ignored.

  • --user and --password must be supplied together. They provide plaintext
  • access control, not encryption. The password also remains in local shell history. Use the daemon only on a trusted network.

  • FTP / is the exported folder. Normalized paths cannot walk above that
  • folder, and the export root itself cannot be deleted, replaced, or renamed.

  • Supported operations include directory listing, download, upload, create and
  • remove directory, delete, rename, size, current directory, and passive-mode negotiation. Active mode and TLS are not supported.

log

Runtime SolarOS log follower. It mirrors log entries to a byte-stream port or appends them to a file.

Usage:

job start log <port> [error|warn|info|debug]
job start log file <path> [error|warn|info|debug]
job stop log
job status log

Examples:

job start log cdc0
job start log uart0 debug
job start log file /.shell/log info

Notes:

  • Port targets use CRLF line endings.
  • File targets use LF line endings and are flushed periodically.
  • If no level is specified, the current runtime log level is used.
  • The log job starts from the latest entry, so it follows new logs rather than
  • dumping the whole ring.

telnetd

Remote Telnet shell server. The listener is a background job; each accepted connection is attached to its own normal SolarOS port-shell session.

Usage:

job start telnetd [port] [--password password]
job stop telnetd
job status telnetd

Examples:

job start telnetd
job start telnetd 2323 --password local-secret

Notes:

  • The default port is 23.
  • One remote client is supported at a time. Additional clients receive a busy
  • response and are disconnected.

  • Telnet terminal-type and window-size negotiation select the terminal profile
  • and update the shell dimensions.

  • Interactive line edits use the shared port shell's coalesced redraws, so the
  • cursor does not visibly jump to the prompt while typing.

  • While a client is attached, telnetd holds a low-latency Wi-Fi lease that
  • disables modem sleep. Disconnecting restores the normal Wi-Fi power-save policy.

  • Disconnecting closes the child shell session and releases any foreground app
  • or resource it owns.

  • Remote sessions do not run /.shell/startup.
  • Telnet is unencrypted. The optional password limits access but is also sent
  • over the network in plaintext, and the start command remains in the local shell history. Use this service only on a trusted network.

ntp-sync

Network time synchronization job. It updates the SolarOS wall clock from NTP and also updates the hardware RTC when the board provides one.

Usage:

job start ntp-sync [once] [interval-sec] [server]
job stop ntp-sync
job status ntp-sync

Defaults:

SettingValue
Interval60 seconds
Serverpool.ntp.org

Examples:

job start ntp-sync once
job start ntp-sync 300 time.cloudflare.com
job start ntp-sync once 60 pool.ntp.org

Notes:

  • Wi-Fi must be connected before sync can succeed.
  • In once mode, the job retries at the interval until the first successful
  • sync, then stops itself.

  • Without once, it keeps syncing periodically.

email-sync

Receive-only IMAPS mailbox polling job. It fetches mail into the provider-local email app and publishes each new message to the universal inbox.

Usage:

job start email-sync [interval-sec] [once]
job stop email-sync
job status email-sync

The default interval is 300 seconds; accepted values are 30 through 86400 seconds. once stops the job after one attempt. The account must be configured first:

wifi on
email configure imaps://imap.example.com user@example.com app-password INBOX
job start email-sync 300

To start polling after each reboot, add the following after wifi on in /.shell/startup:

job start email-sync 300

Notes:

  • TLS certificate validation is mandatory; plaintext IMAP is not accepted.
  • The first synchronization imports up to the newest eight messages. Later
  • polls process new UIDs in batches of eight, so a busy mailbox catches up over successive intervals without overflowing the response buffer.

  • The provider-local list keeps 32 messages in volatile memory. Universal inbox
  • notifications use the mailbox as topic, the From header as sender, and the subject as title.

  • Body previews are best effort. Full MIME decoding, attachments, SMTP sending,
  • and server-side read-state synchronization remain future work.

pocsag

POCSAG pager receiver job. It configures a registered packet radio for a continuous POCSAG byte stream, frames successive 64-byte batches, filters pages to one receiver identity code (RIC), decodes alphanumeric or numeric payloads, and publishes completed messages to the universal inbox.

Usage:

job start pocsag <radio> <frequency-hz> <baud> <ric> [alpha|numeric] [normal|inverted]
job stop pocsag
job status pocsag
pocsag status
pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha|numeric] [normal|inverted] [function]

Example:

job start pocsag radio 448425000 1200 1841525 alpha
inbox list unread

job stop pocsag
pocsag send radio 448425000 1200 1841525 "SolarOS calling" alpha inverted

Notes:

  • The decoder validates POCSAG parity and BCH and corrects up to two erroneous
  • bits per codeword.

  • Messages may continue across batch boundaries; the receiver follows the sync
  • words between batches until the page is complete.

  • Identical repeated pages received within 30 seconds produce one inbox entry.
  • The default FSK polarity is normal; retry with inverted if batches remain
  • at zero while the transmitter is active.

  • pocsag status shows batch/message counts, corrections, receive errors, and
  • the RSSI of the most recent batch.

  • Stopping the job restores the radio configuration and state that were active
  • when it started.

  • Sending supports messages spanning multiple batches and restores the radio's
  • previous configuration afterward. A receiver job using the same half-duplex radio must be stopped first.

meshcore

Non-forwarding MeshCore companion provider for Contacts and Messages.

Usage:

job start meshcore <radio> <profile>
job stop meshcore
job status meshcore
meshcore status

Example:

job start meshcore radio0 meshcore-eu868
meshcore advert flood
chat

The job requires PSRAM and a packet-radio expansion capability. It claims the radio, applies the explicit regional profile, sends one zero-hop startup advert, and continuously handles adverts, direct messages, ACKs, and group messages. Its complete protocol context is allocated as external-required PSRAM; the 6144-byte worker stack remains internal and its minimum watermark is reported by meshcore status.

Stopping restores the previous radio configuration and state before releasing ownership. MeshCore and radio-link therefore report normal ownership conflicts when pointed at the same radio. See meshcore.md for identity, trust, channel, regional-profile, and security details.

espnow-link

ESP-NOW adapter for the transport-independent SolarOS Link service.

Usage:

job start espnow-link <link> [channel=auto|1..13] [phy=normal|lr500|lr250] [inbox=off|on] [chat=off|on]
job stop espnow-link
job status espnow-link
espnow status

Example:

job start espnow-link link0 channel=6 phy=lr500 chat=on
link send link0 broadcast "hello"
espnow peers

The job leases the Wi-Fi radio, creates a 250-byte-MTU Link, and moves complete Link frames through ESP-NOW. channel=auto follows an active station or AP and otherwise selects channel 6. inbox=on and chat=on have the same behavior and mutual exclusion as radio-link.

phy=normal is the default. phy=lr500 and phy=lr250 enable Espressif's proprietary 500 kbit/s or 250 kbit/s Long Range PHY for ESP-NOW peers. Every device participating in an LR link must enable LR reception; use the same mode at both ends for symmetric throughput. The service adds LR receive support while it runs, applies the selected transmit rate to configured and learned peers, and restores the previous Wi-Fi protocol selection when it stops. Use espnow status to confirm the active PHY.

Incoming frames learn volatile Link-ID-to-MAC mappings; espnow peer add stores a mapping in NVS for cold-start unicast. The service is bounded to 19 peers and four queued receive frames. Its queues and 6144-byte internal worker stack exist only while the job runs; durable state is in PSRAM. ESP-NOW is unencrypted in this release. See link.md for channel coexistence, peer conflicts, payload limits, and security constraints.

radio-link

Packet-radio adapter for the transport-independent SolarOS Link service.

Usage:

job start radio-link <link> <radio> <profile> [inbox=off|on] [chat=off|on] [repeater=off|on]
job stop radio-link
job status radio-link
link status <link>

Example:

job start radio-link link0 radio0 lora-eu868 chat=on
link send link0 broadcast "hello"
link status link0

The job claims the radio, applies the complete named profile, creates the Link instance, transmits its queued frames, and continuously receives complete radio packets. The Link service validates its own CRC, suppresses duplicates, replies to requested unicast acknowledgements, and retains accepted messages in a bounded queue. inbox=on additionally publishes accepted text messages to the universal inbox. chat=on instead registers Link as a messaging provider, creates a broadcast conversation, discovers source IDs as Contacts, and supports direct and broadcast text through Chat. Both options are off by default and cannot be enabled together.

repeater=on turns the same packet-radio adapter into a one-hop SolarOS Link range extender. It retransmits valid frames for other destinations and broadcasts, including acknowledgements and virtual-stream packets, while preserving the original Link identities. A relayed-frame marker, randomized delay, recent-frame suppression, and a bounded four-frame queue prevent loops and reduce collisions. It is off by default. job status radio-link reports forwarded, suppressed, queued, queue-drop, and invalid-frame counters.

Repeater mode can run together with local Chat and peer-bound Link streams. For example, use job start radio-link link0 radio0 lora-eu868 chat=on repeater=on to join Chat while extending its range. Direct frames for the local Link ID are consumed locally, broadcasts are consumed and repeated, and frames for other Link IDs are repeated without local delivery. A local stream can be created with link stream create link0 vser0 <peer-id> while repeating stream traffic between other devices.

Stopping restores the radio configuration and state that existed before the job started. Mutating direct radio operations are rejected while the radio is owned by the job. See link.md for commands, frame layout, IDs, queue limits, transport MTUs, and version-one exclusions.

pppd

PPP over any bidirectional SolarOS byte-stream port. A physical UART, USB CDC, or a virtual port carried by a packet-radio link can provide the bytes; the PPP negotiation mode and the interface's routing role are configured separately.

Usage:

job start pppd <port> [baud|baud=rate]
    [role=downstream|uplink|peer] [mode=passive|active]
    [local=address] [peer=address] [dns=auto|none|address]
    [priority=0..255]
job stop pppd
job status pppd

Defaults:

SettingValue
BaudKeep the port's current rate
Roledownstream
Modepassive for downstream; active otherwise
Local address192.168.8.1 for downstream
Peer address192.168.8.2 for downstream
DNSauto, copied from the preferred SolarOS uplink
Uplink priority90

Routing roles have these effects:

  • downstream enables NAPT on the PPP interface. A connected computer can use
  • whichever Wi-Fi, cellular, or WireGuard routes SolarOS currently selects.

  • uplink registers ppp-<port> as a base network path. It participates in
  • the same persistent priority selection as Wi-Fi and cellular interfaces.

  • peer creates only the point-to-point interface. It does not enable NAPT or
  • advertise the interface as a default-route candidate.

mode=active starts negotiation. mode=passive listens for the other endpoint to initiate it. Either mode can be combined with any routing role; the defaults are conveniences, not a coupling between negotiation and routing.

Examples:

# Retro computer dials into SolarOS and reaches its selected Internet route.
job start pppd uart0 baud=115200

# The same downstream service over native USB CDC with an explicit DNS server.
job start pppd cdc0 role=downstream dns=8.8.8.8

# Treat a virtual serial port over a radio link as a SolarOS uplink.
job start pppd vser0 role=uplink mode=active priority=120

# A routed point-to-point link without address translation.
job start pppd uart0 38400 role=peer mode=passive \
    local=10.20.0.1 peer=10.20.0.2 dns=none

The job exclusively claims the selected port. A baud argument is accepted only for configurable UART ports; CDC and virtual ports retain their transport rate. This initial service uses unauthenticated PPP. Use it on a physically trusted link. job status pppd reports negotiation state, assigned addresses, NAPT, traffic counters, reconnects, and the most recent PPP error. While the job runs, network and network status show ppp-<port> with its uplink, downstream, or peer role. Only the uplink role appears in the Network Settings priority list.

slip

IPv4 SLIP gateway on a byte-stream port. This is intended for retro machines, headless boards, and serial networking experiments. The active interface is named with the slip- prefix plus the selected port, for example slip-uart0. It appears as a downstream NAT link in the Network views; its traffic follows the selected SolarOS route, which can be Wi-Fi, cellular, or WireGuard.

Usage:

job start slip [port] [baud] [local-ip] [peer-ip] [netmask]
job stop slip
job status slip

Defaults:

SettingValue
Portuart0
Baud115200
Local IP192.168.7.1
Peer IP192.168.7.2
Netmask255.255.255.252

Examples:

job start slip uart0 115200
job start slip cdc0 115200
job start slip uart0 38400 192.168.7.1 192.168.7.2 255.255.255.252

Notes:

  • The peer should use the local IP as its gateway.
  • SolarOS enables NAT on the SLIP-facing interface.
  • The selected port is claimed by the SLIP job until it stops.
  • cdc0 is useful for Linux host testing; uart0 is the natural expansion
  • port path.

graffiti

Full-screen Palm Graffiti handwriting input for boards with absolute touch and PSRAM. The job does not draw an overlay or change the active application.

job start graffiti
job status graffiti
job stop graffiti

The pen-down position chooses the recognition alphabet for the complete stroke. Start in the left two-thirds of the oriented display for letters, or in the right one-third for numbers. The boundary follows the current display orientation. Relative pointer sources are ignored.

The recognizer implements the $1 unistroke pipeline with Palm Graffiti 1 alphabet and numeral templates. It preserves stroke direction so a downward vertical stroke can be I or 1, while an upward vertical stroke is Shift. The Palm editing gestures are also available:

StrokeResult
Upward verticalShift; repeat before a character for Caps Lock.
Left to rightSpace.
Right to leftBackspace.
Upper right to lower leftEnter.

While running, the job registers the pointer-observer resource. It observes absolute pointer events over the whole display but does not consume them, so the foreground application continues to receive the same press, move, and release events. Recognized characters are published by a virtual keyboard source named graffiti and therefore go to the current input focus. job status graffiti reports recognized, rejected, and dropped stroke counts plus the current case state.

gpio-keys

Maps runtime-safe GPIO inputs to SolarOS keyboard presses. The job configures each pin as an input with its internal pull-up enabled, treats a low level as pressed, and applies the same 25 ms debounce used by fixed board buttons. Each debounced transition publishes a generic SolarOS key press or release. Held keys use the system repeat rate configured by setterm keyrate.

Inline usage:

job start gpio-keys gpio17:UP gpio2:ENTER gpio3:ESCAPE
job stop gpio-keys
job status gpio-keys

Configuration-file usage:

job start gpio-keys --config /flash/gpio-keys.conf

The file contains one mapping per line. A colon or whitespace can separate the pin and key. Empty lines and text after # are ignored:

# navigation buttons, active low
gpio17:UP
gpio16 DOWN
gpio4:ENTER

Canonical pins use gpioN; the job also accepts the ioN shorthand. Key names are case-insensitive. Canonical directional names are UP, DOWN, LEFT, and RIGHT; aliases such as ARROW_UP are accepted. Other names include ENTER, SPACE, TAB, BACKSPACE, ESCAPE, HOME, END, DELETE, PAGE_UP, PAGE_DOWN, F1 through F12, and the modified SolarOS navigation keys. A single character maps that exact character.

The complete configuration is validated before any pin is changed. Up to 16 pins can be loaded from a file; the shell's argument limit allows up to seven inline mappings. Duplicate pins, fixed/reserved pins, unknown keys, and busy pins reject the complete start. Starting with a button already held does not generate a press. The file is read only at startup; restart the job to reload it.

While the job runs, every pin belongs to the gpio-keys-job expansion attachment and has an assignment such as key:UP in the io Pins and Claims views. Stopping the job detaches that device, resets the pins, releases their claims, and discards queued events from this input source. The fixed ODROID-GO button service remains independent but uses the same held-key and repeat service.

ps2-keyboard

Receives keyboard scan-code set 2 from an exclusive named PS/2 bus and publishes press and release transitions through the generic SolarOS input service. This job is a compatibility wrapper around a ps2-keyboard expansion attachment; new configurations can attach the device directly.

expansion bus create ps2 ps2kbd clock=gpio17 data=gpio18
job start ps2-keyboard ps2kbd
job status ps2-keyboard
job stop ps2-keyboard

These commands cover an expansion bus. Boards with an integrated PS/2 keyboard declare the bus and a default expansion attachment. On TTGO VGA32 v1.4, keyboard0 is attached to ps2kbd0 before the shell starts and is inspected with expansion devices and input test keyboard0.

The bus descriptor owns the CLOCK and DATA pins as bus:ps2kbd; the wrapper's ps2-keyboard-job attachment holds the exclusive lease. Normal and extended keys, modifiers, navigation keys, function keys, and keypad usages are translated to canonical USB HID identities. The configured setterm keyboard layout and setterm keyrate repeat policy apply equally to BLE and PS/2.

The receiver validates each PS/2 frame's start bit, odd parity, and stop bit in the GPIO clock-edge handler. Scan-code parsing and input publication run from the normal job tick, outside interrupt context. The current driver only receives keyboard data; it does not send LED or reset commands to the keyboard.

Use a bidirectional level shifter or another circuit that guarantees no more than 3.3 V at the ESP32 GPIOs. ESP32 inputs are not 5 V tolerant.

midi

Bidirectional MIDI transport on an exclusive named MIDI bus. The bus selects an available UART controller internally; users supply only its MIDI name, TX and RX pins, and an optional baud rate.

expansion bus create midi midi0 tx=gpio1 rx=gpio2
job start midi midi0
job status midi
midi status
job stop midi

The default rate is the MIDI DIN rate of 31250 baud. Incoming channel voice, system-common, and realtime messages are decoded with running-status support and published to subscribers such as the Synth app. Outgoing messages are queued with midi note-on, midi note-off, midi cc, midi program, or midi send. Status reports RX and TX byte/message counts, unsupported parser input, queue drops, and the last transport error.

Run midi monitor and move a controller to identify its mapping. The monitor prints CC: <channel> <controller> <value> for control changes and KEY: <channel> <note> <velocity> for note activity. Note releases use velocity zero. The app-exit key, Esc, or q returns to the shell.

Up to 16 exact incoming MIDI CC addresses can also be registered as scalar streams. This lets the controls job map a MIDI controller through the standard normalized control path to any live application parameter:

midi stream add 1 74
control create cutoff midi.cc.1.74 0 127
control bind cutoff parameter synth.filter.cutoff pickup=off
job start controls
synth

Use midi stream list, midi stream remove <channel> <controller>, and midi stream clear to inspect or remove the volatile definitions. A stream is waiting until the running MIDI job receives its first matching value.

Use a compliant electrical interface: MIDI IN requires an optoisolated receiver and MIDI OUT requires a current-limited driver. Do not connect DIN MIDI pins directly to ESP32 GPIOs.

sump

SUMP-compatible logic analyzer server on cdc0. It claims the CDC port and uses the shared logic analyzer service for acquisition. PulseView and sigrok can connect with the OpenBench Logic Sniffer/SUMP serial driver.

Usage:

job start sump [pin ...]
job start sump [pin[,pin...]]
job stop sump
job status sump

Examples:

job start sump
job start sump 1 2 3 17
job start sump 1,2,3,17

If no pins are supplied, the job uses up to eight runtime-safe GPIOs from the active board profile. Host commands select the sample rate and capture size. The current implementation supports 10 kHz to 2 MHz requests and up to 32768 one-byte samples; the status recorded with each capture reports the measured effective rate.

Notes:

  • The job requires both CDC and runtime-safe GPIO capabilities.
  • cdc0 cannot be used by a port shell, logger, bridge, or another job while
  • SUMP is active. Start SUMP from the display shell, SSH, or another port after stopping any shell attached to cdc0.

  • Basic trigger commands are accepted for host compatibility, but this first
  • implementation captures immediately instead of waiting for a trigger.

  • Captures remain available to the logic app after the job stops.

Quick reference

Use jobs to inspect state and memory requirements, job NAME start to launch a worker, job NAME status for details, and job NAME stop to release it. Jobs run in the background and may claim ports or hardware resources; this page documents every installed job's arguments and ownership rules.

Background jobs · SolarOS manual

Background jobs

Jobs are named background workers such as logging, acquisition, bridges, NTP, and HTTP serving. They are not foreground applications and continue when you switch shell or app sessions.

Inspect memory and ownership

Run jobs for the compact overview and job status NAME for details:

jobs
job status log

The status includes the worker stack size and whether it uses internal RAM or PSRAM. Detailed status also shows the current start-admission disposition and reason using the same centralized policy that launches jobs. It shows claimed resources and the last error. Check mem before starting several internal-stack jobs together.

Waiting and failed

waiting means the requested worker has not passed launch admission yet. It may run later when the required memory or resource becomes available.

failed means launch or runtime work ended with an error. Inspect last_error and the resource owner before retrying.

Start and stop

job start batmon 60
job start log file /logs/system.log info
job start midi midi0
job stop log

Only one instance of each registered job name is active. Starting it again replaces the previous invocation.

Quick reference

solaros.jobs provides list(), count(), status(name), start(name, optional_args), and stop(name). Status includes state, last_error, worker_stack_bytes, worker_stack_external, tick timing, and deadline telemetry. The shell's detailed status adds the current start-admission disposition and reason. Waiting means launch admission has not yet succeeded; failed records a terminal launch or runtime error.

The midi job owns one named MIDI bus and moves messages in both directions. Create the bus with expansion bus create midi, then use midi status to inspect its counters. midi stream add <channel> <controller> exposes an exact incoming CC as a scalar midi.cc.<channel>.<controller> stream. midi monitor prints incoming CC and key identifiers to help select that mapping.

The controls job samples configured scalar streams at 50 Hz and sends changed normalized values to native application parameters and MIDI CC targets. Create the mappings with control; see man controls for the complete workflow.

Background jobs · SolarOS manual

batmon

Battery monitor. It periodically samples battery voltage, maintains a smoothed trend, estimates power state, and can request light sleep when the configured minimum voltage is reached.

Usage:

job start batmon [interval-sec]
job stop batmon
job status batmon

Defaults:

SettingValue
Interval60 seconds

Battery limits are configured with the battery shell command:

battery capacity <mAh>
battery min_voltage <volts>
battery max_voltage <volts>

Notes:

  • Discharging trend means battery power.
  • Charging trend means external power.
  • Voltage above max_voltage is a fast external-power shortcut.
  • Three consecutive samples at or below min_voltage while on battery request
  • light sleep.

Example:

job start batmon 60

Background jobs · SolarOS manual

bridge

Bidirectional byte bridge between two byte-stream ports, or between one byte-stream port and an active SolarOS Link instance.

Usage:

job start bridge <port-a> <port-b>
job start bridge <port> <link> [broadcast|destination-id]
job stop bridge
job status bridge

Example:

job start bridge cdc0 uart0

To expose a UART byte stream over a packet-radio Link:

job start radio-link link0 radio0 lora-eu868
job start bridge uart0 link0 broadcast

That direct Link form is best-effort. For an ordered, retransmitted stream, create a peer-bound virtual port and use the normal port-to-port bridge form:

link stream create link0 vser0 0x12345678
job start bridge cdc0 vser0

The remote device creates its matching vser0 and can attach a normal shell with session create shell vser0 --term dumb. On a headless DevKit, this leaves the primary uart0 shell free for administration while Linux uses USB cdc0 for the remote terminal.

Use a decimal or 0x 32-bit Link destination instead of broadcast for acknowledged unicast:

job start bridge uart0 link0 0x12345678

Notes:

  • The two ports must be different.
  • Both ports are claimed by the bridge job until it stops.
  • Link stream ports such as vser0 are normal byte-stream ports. Their stream
  • service supplies peer filtering, segmentation, ordering, retransmission, and bounded backpressure before the bridge sees bytes.

  • In Link mode, the serial port is claimed while the already-running Link
  • instance remains active under its transport job.

  • Available serial bytes are emitted as binary Link messages, each capped at
  • the Link payload MTU. Received text and binary payloads are written to the serial port without a separator, preserving byte-stream behavior.

  • broadcast is the default destination. Explicit destinations request normal
  • Link acknowledgements.

  • The bridge consumes the Link receive queue. Do not use link receive on the
  • same Link while the bridge is running.

  • Packet radio is usually much slower than UART. The bridge uses the existing
  • bounded Link queues and does not add an unbounded SRAM buffer; sustained serial input can therefore overrun the port or produce Link queue drops.

  • If the Link disappears, the bridge releases its serial port and stops with
  • the Link error.

  • This is the clean base for USB-to-UART converter style workflows.

Background jobs · SolarOS manual

chatd

Local SolarOS chat gateway server. It is useful for testing the chat app or for small trusted local networks.

Usage:

job start chatd [port] [token] [--history path]
job start chatd [port] [token] [path]
job stop chatd
job status chatd

Defaults:

SettingValue
Port7777
Default channelgeneral
Maximum clients6
Maximum channels32
In-memory history64 events

Arguments are intentionally flexible. The first numeric argument is the port. The next non-option argument is the optional token. --history or --log selects an optional append-only history dump file.

Examples:

job start chatd
job start chatd 7777 secret
job start chatd 7777 secret --history /.shell/chatd.log

The local chat app can connect with:

chat local
chat 127.0.0.1:7777

On another SolarOS device or host on the same network, use the server IP:

chat 192.168.1.113:7777

Notes:

  • If a token is configured, clients must present the same token.
  • New clients receive the recent in-memory channel history.
  • Channel deletion is supported by the chat protocol and client.
  • The built-in server is a lightweight LAN gateway, not a hardened public chat
  • service.

Background jobs · SolarOS manual

controls

Continuous-control mapper. It samples every configured scalar-stream control at 50 Hz, applies smoothing, deadband, calibration, and inversion, then updates changed native parameter and MIDI CC bindings.

control create cutoff adc1 0 3300 smooth=40 deadband=8
control bind cutoff parameter synth.filter.cutoff pickup=on
job start controls
job status controls
job stop controls

The job takes no arguments. Controls and bindings can be added or removed while it runs. A source read error and an unavailable target are retained in control and binding status instead of stopping the worker. Native parameter bindings retry when the application resumes and publishes the path again. MIDI bindings retry while the MIDI worker is stopped.

Control definitions are runtime configuration. Put the control create, control bind, and job start controls commands in /.shell/startup to restore a hardware setup after reboot. See man controls for calibration, manual script inputs, MIDI examples, and inspection commands.

Background jobs · SolarOS manual

daq

Data acquisition job. It captures scalar and event streams to timestamped CSV, or one byte or PCM audio source directly to a raw file.

The daq shell command is usually easier to remember:

daq
daq streams
daq start <stream...> <file> [options]
daq start <file> <stream...> [options]
daq stop
daq status

Direct job usage:

job start daq <stream...> <file> [--rate seconds|--rate-ms ms] [--append|--replace]
job start daq <file> <stream...> [--rate seconds|--rate-ms ms] [--append|--replace]
job start daq <byte-stream> <file> --raw [--rate-ms ms] [--append|--replace]
job start daq <audio-stream> <file> --raw [--rate-ms ms] [--append|--replace]
job stop daq
job status daq

Defaults:

ModeDefault interval
Scalar CSV1000 ms
Raw byte stream25 ms
Raw audio streamContinuous

Examples:

daq start temperature /logs/temp.csv --rate 60
daq start /logs/env.csv temperature humidity battery --rate 60
daq start uart0 /logs/uart0.bin --raw --rate-ms 25
daq start audio0.capture /logs/microphones.pcm --raw
job start daq /logs/env.csv temperature humidity battery --rate 60

Notes:

  • Multi-stream mode supports scalar and event streams only.
  • Raw capture is single-stream only and writes byte or PCM audio data directly.
  • Raw audio files contain the native format shown by
  • stream status audio0.capture; use arecord when a WAV container is needed.

  • CSV rows include a timestamp column and one value column per stream.
  • Available streams depend on board capabilities.

Background jobs · SolarOS manual

displayd

Authenticated HTTP display and remote control. It has two modes:

  • With a physical target such as display0, it mirrors and controls the
  • active session attached to that display without allocating another display framebuffer.

  • With web0, it creates an independent monochrome virtual display and a
  • detached display shell. Its logical dimensions match the board's main display (for example 384x288 on the Freenove PAL target). A headless board uses the historical 400x300 fallback. Apps launched from that shell stay on web0 and do not replace the foreground app on a physical display.

With no target argument, displayd mirrors display0 when it exists and otherwise creates web0. The latter makes the same command useful on headless PSRAM-equipped boards.

Usage:

job start displayd [target]
job stop displayd
job status displayd

Example:

wifi on
job start displayd
job start displayd web0

Starting the job prints a random six-digit access code. Open http://<device>/display, enter that code, and click the displayed image before typing. The browser frontend polls the native 1-bit U8g2 frame up to twenty times per second and performs pixel rotation in the browser instead of the HTTP server task. It sends bounded key input through the scheduler. Ctrl+] remains the application-exit key.

API:

GET  /api/displays
GET  /api/displays/<target>/frame.pbm
GET  /api/displays/<target>/frame.raw
POST /api/displays/<target>/input

All API requests require Authorization: Bearer <code>. The access code is never accepted in the URL. The built-in frontend itself is public so that it can prompt for the code, but keeps the supplied code only in page memory.

Notes:

  • web0 is registered as source=virtual, driver=framebuffer while the job
  • is running. Its framebuffer and session exist independently of whether a browser is connected.

  • displayd creates and owns the web0 shell session itself and prints its
  • session ID. Do not run session create shell web0 afterward; the target is already attached to the browser-controlled shell.

  • Ctrl+] exits a foreground app on web0 and returns to its detached shell.
  • The physical foreground session is unaffected.

  • The physical mirror reuses the active U8g2 display and does not create
  • another display session. The built-in display shell is registered as the session attached to display0.

  • A consistent 1-bit frame snapshot and same-sized raw transmit buffer are held
  • in PSRAM while the job runs. For the 400x300 Waveshare display they consume 30,400 bytes in total. web0 additionally owns a board-sized U8g2 framebuffer (15,200 bytes on Waveshare, 13,824 bytes on the 384x288 PAL target). HTTP transmission never holds a display or registry lock.

  • The snapshot is copied into the transmit buffer and released before network
  • I/O, so a slow browser cannot prevent newer display frames from being published.

  • The browser uses frame.raw to avoid per-pixel PBM conversion on the ESP32.
  • The PBM endpoint remains available for simple external clients.

  • If a browser is still reading a frame when the display presents again, that
  • publication is skipped rather than blocking the display.

  • Input is queued by the HTTP task and dispatched only by the normal SolarOS
  • scheduler. Both physical and virtual targets receive it through their active target-addressed session; browser control does not depend on the device's globally foreground session.

  • The server is plain HTTP. The six-digit code provides convenient access
  • control on a trusted Wi-Fi network but does not encrypt frames or input and is not intended for exposure to an untrusted network.

  • displayd and httpd share one HTTP server and may run simultaneously.

Background jobs · SolarOS manual

email-sync

Receive-only IMAPS mailbox polling job. It fetches mail into the provider-local email app and publishes each new message to the universal inbox.

Usage:

job start email-sync [interval-sec] [once]
job stop email-sync
job status email-sync

The default interval is 300 seconds; accepted values are 30 through 86400 seconds. once stops the job after one attempt. The account must be configured first:

wifi on
email configure imaps://imap.example.com user@example.com app-password INBOX
job start email-sync 300

To start polling after each reboot, add the following after wifi on in /.shell/startup:

job start email-sync 300

Notes:

  • TLS certificate validation is mandatory; plaintext IMAP is not accepted.
  • The first synchronization imports up to the newest eight messages. Later
  • polls process new UIDs in batches of eight, so a busy mailbox catches up over successive intervals without overflowing the response buffer.

  • The provider-local list keeps 32 messages in volatile memory. Universal inbox
  • notifications use the mailbox as topic, the From header as sender, and the subject as title.

  • Body previews are best effort. Full MIME decoding, attachments, SMTP sending,
  • and server-side read-state synchronization remain future work.

Background jobs · SolarOS manual

espnow-link

ESP-NOW adapter for the transport-independent SolarOS Link service.

Usage:

job start espnow-link <link> [channel=auto|1..13] [phy=normal|lr500|lr250] [inbox=off|on] [chat=off|on]
job stop espnow-link
job status espnow-link
espnow status

Example:

job start espnow-link link0 channel=6 phy=lr500 chat=on
link send link0 broadcast "hello"
espnow peers

The job leases the Wi-Fi radio, creates a 250-byte-MTU Link, and moves complete Link frames through ESP-NOW. channel=auto follows an active station or AP and otherwise selects channel 6. inbox=on and chat=on have the same behavior and mutual exclusion as radio-link.

phy=normal is the default. phy=lr500 and phy=lr250 enable Espressif's proprietary 500 kbit/s or 250 kbit/s Long Range PHY for ESP-NOW peers. Every device participating in an LR link must enable LR reception; use the same mode at both ends for symmetric throughput. The service adds LR receive support while it runs, applies the selected transmit rate to configured and learned peers, and restores the previous Wi-Fi protocol selection when it stops. Use espnow status to confirm the active PHY.

Incoming frames learn volatile Link-ID-to-MAC mappings; espnow peer add stores a mapping in NVS for cold-start unicast. The service is bounded to 19 peers and four queued receive frames. Its queues and 6144-byte internal worker stack exist only while the job runs; durable state is in PSRAM. ESP-NOW is unencrypted in this release. See link.md for channel coexistence, peer conflicts, payload limits, and security constraints.

Background jobs · SolarOS manual

ftpd

Unencrypted FTP file server for one exported folder. The job supports one client at a time and passive IPv4 data connections.

Usage:

job start ftpd <folder> [port] [--user USER --password PASSWORD]
job stop ftpd
job status ftpd

Examples:

job start ftpd /shared
job start ftpd /shared 2121 --user solaros --password local-secret

Notes:

  • The default port is 21.
  • Login is anonymous by default. Anonymous clients use anonymous or ftp as
  • the username; the supplied password is ignored.

  • --user and --password must be supplied together. They provide plaintext
  • access control, not encryption. The password also remains in local shell history. Use the daemon only on a trusted network.

  • FTP / is the exported folder. Normalized paths cannot walk above that
  • folder, and the export root itself cannot be deleted, replaced, or renamed.

  • Supported operations include directory listing, download, upload, create and
  • remove directory, delete, rename, size, current directory, and passive-mode negotiation. Active mode and TLS are not supported.

Background jobs · SolarOS manual

gateway-sync

Background synchronizer for the gateway messaging provider. Start and stop it explicitly, using the same lifecycle as email-sync:

job start gateway-sync
job stop gateway-sync
job status gateway-sync

gateway-sync takes no polling interval. Unlike the periodic email-sync job, it maintains a live connection and applies its own exponential reconnect backoff. It can therefore be started before Wi-Fi has an address; it remains running and connects when the network becomes available. In /.shell/startup, use exactly:

job start gateway-sync

It owns transport connection lifetime, exponential retry, opaque resume cursors, joined-channel replay, and delivery of the gateway provider's shared outbound requests. The messaging service owns retained publication and Inbox projection. Replayed transport messages are deduplicated by the shared stable producer identity before another notification is published.

The gateway hello uses the SolarOS identity user and identity hostname values. Chat does not maintain separate user-name or device-name settings.

Stopping or closing app.chat has no effect on this job. Its worker performs transport startup, polling, and retry work outside the cooperative session/job scheduler.

The shared store retains at most 64 messages. SD-backed systems use the full-message /.messages/messages.bin ring. Systems using internal flash restore Chat history from the compact records already stored in /.inbox/messages.bin; no second ring is created, so Chat history cannot consume the remaining flash volume.

Background jobs · SolarOS manual

gesture-listener

Gesture-to-command listener. Rules are configured independently with gesture bind, while this job owns observation and dispatch:

gesture bind source=gesture0 gesture=flick direction=east -- input emit ALT+RIGHT
gesture bind source=gesture0 gesture=flick direction=west -- input emit ALT+LEFT
job start gesture-listener
job status gesture-listener
job stop gesture-listener

Stopping the job preserves the rules, prevents new gesture actions, and drops queued actions. A command already executing finishes normally. Use gesture bindings to inspect rules and counters, gesture unbind <id> to remove one, or gesture unbind all to clear the table and reset the next ID to 1. Rules and job state are volatile; recreate them in the startup script when persistence is needed. The command worker is created on demand and releases its internal stack when its queue is idle.

Background jobs · SolarOS manual

gpio-keys

Maps runtime-safe GPIO inputs to SolarOS keyboard presses. The job configures each pin as an input with its internal pull-up enabled, treats a low level as pressed, and applies the same 25 ms debounce used by fixed board buttons. Each debounced transition publishes a generic SolarOS key press or release. Held keys use the system repeat rate configured by setterm keyrate.

Inline usage:

job start gpio-keys gpio17:UP gpio2:ENTER gpio3:ESCAPE
job stop gpio-keys
job status gpio-keys

Configuration-file usage:

job start gpio-keys --config /flash/gpio-keys.conf

The file contains one mapping per line. A colon or whitespace can separate the pin and key. Empty lines and text after # are ignored:

Background jobs · SolarOS manual

graffiti

Full-screen Palm Graffiti handwriting input for boards with absolute touch and PSRAM. The job does not draw an overlay or change the active application.

job start graffiti
job status graffiti
job stop graffiti

The pen-down position chooses the recognition alphabet for the complete stroke. Start in the left two-thirds of the oriented display for letters, or in the right one-third for numbers. The boundary follows the current display orientation. Relative pointer sources are ignored.

The recognizer implements the $1 unistroke pipeline with Palm Graffiti 1 alphabet and numeral templates. It preserves stroke direction so a downward vertical stroke can be I or 1, while an upward vertical stroke is Shift. The Palm editing gestures are also available:

StrokeResult
Upward verticalShift; repeat before a character for Caps Lock.
Left to rightSpace.
Right to leftBackspace.
Upper right to lower leftEnter.

While running, the job registers the pointer-observer resource. It observes absolute pointer events over the whole display but does not consume them, so the foreground application continues to receive the same press, move, and release events. Recognized characters are published by a virtual keyboard source named graffiti and therefore go to the current input focus. job status graffiti reports recognized, rejected, and dropped stroke counts plus the current case state.

Background jobs · SolarOS manual

httpd

Static HTTP file server for a folder on mounted storage.

Usage:

job start httpd <folder>
job stop httpd
job status httpd

Example:

job start httpd /www

Notes:

  • Relative paths resolve under the default storage mount.
  • The server uses the ESP-IDF default HTTP port.
  • It shares the service-owned HTTP server with displayd.
  • It serves files and simple directory listings.
  • MIME types are provided for common text, image, audio, JSON, JavaScript, and
  • CSS files.

Background jobs · SolarOS manual

log

Runtime SolarOS log follower. It mirrors log entries to a byte-stream port or appends them to a file.

Usage:

job start log <port> [error|warn|info|debug]
job start log file <path> [error|warn|info|debug]
job stop log
job status log

Examples:

job start log cdc0
job start log uart0 debug
job start log file /.shell/log info

Notes:

  • Port targets use CRLF line endings.
  • File targets use LF line endings and are flushed periodically.
  • If no level is specified, the current runtime log level is used.
  • The log job starts from the latest entry, so it follows new logs rather than
  • dumping the whole ring.

Background jobs · SolarOS manual

meshcore

Non-forwarding MeshCore companion provider for Contacts and Messages.

Usage:

job start meshcore <radio> <profile>
job stop meshcore
job status meshcore
meshcore status

Example:

job start meshcore radio0 meshcore-eu868
meshcore advert flood
chat

The job requires PSRAM and a packet-radio expansion capability. It claims the radio, applies the explicit regional profile, sends one zero-hop startup advert, and continuously handles adverts, direct messages, ACKs, and group messages. Its complete protocol context is allocated as external-required PSRAM; the 6144-byte worker stack remains internal and its minimum watermark is reported by meshcore status.

Stopping restores the previous radio configuration and state before releasing ownership. MeshCore and radio-link therefore report normal ownership conflicts when pointed at the same radio. See meshcore.md for identity, trust, channel, regional-profile, and security details.

Background jobs · SolarOS manual

midi

Bidirectional MIDI transport on an exclusive named MIDI bus. The bus selects an available UART controller internally; users supply only its MIDI name, TX and RX pins, and an optional baud rate.

expansion bus create midi midi0 tx=gpio1 rx=gpio2
job start midi midi0
job status midi
midi status
job stop midi

The default rate is the MIDI DIN rate of 31250 baud. Incoming channel voice, system-common, and realtime messages are decoded with running-status support and published to subscribers such as the Synth app. Outgoing messages are queued with midi note-on, midi note-off, midi cc, midi program, or midi send. Status reports RX and TX byte/message counts, unsupported parser input, queue drops, and the last transport error.

Run midi monitor and move a controller to identify its mapping. The monitor prints CC: <channel> <controller> <value> for control changes and KEY: <channel> <note> <velocity> for note activity. Note releases use velocity zero. The app-exit key, Esc, or q returns to the shell.

Up to 16 exact incoming MIDI CC addresses can also be registered as scalar streams. This lets the controls job map a MIDI controller through the standard normalized control path to any live application parameter:

midi stream add 1 74
control create cutoff midi.cc.1.74 0 127
control bind cutoff parameter synth.filter.cutoff pickup=off
job start controls
synth

Use midi stream list, midi stream remove <channel> <controller>, and midi stream clear to inspect or remove the volatile definitions. A stream is waiting until the running MIDI job receives its first matching value.

Use a compliant electrical interface: MIDI IN requires an optoisolated receiver and MIDI OUT requires a current-limited driver. Do not connect DIN MIDI pins directly to ESP32 GPIOs.

Background jobs · SolarOS manual

ntp-sync

Network time synchronization job. It updates the SolarOS wall clock from NTP and also updates the hardware RTC when the board provides one.

Usage:

job start ntp-sync [once] [interval-sec] [server]
job stop ntp-sync
job status ntp-sync

Defaults:

SettingValue
Interval60 seconds
Serverpool.ntp.org

Examples:

job start ntp-sync once
job start ntp-sync 300 time.cloudflare.com
job start ntp-sync once 60 pool.ntp.org

Notes:

  • Wi-Fi must be connected before sync can succeed.
  • In once mode, the job retries at the interval until the first successful
  • sync, then stops itself.

  • Without once, it keeps syncing periodically.

Background jobs · SolarOS manual

osc

OSC 1.0 IPv4 UDP adapter for automatic incoming native-parameter writes and explicit named outbound stream, event-stream, or normalized-control bindings.

job start osc [listen=port] [target=host:port] [peer=ipv4]
job status osc
job stop osc

The listening port defaults to 9000. target= is optional for an incoming-only job and is required before an outbound binding can send. peer= accepts one exact IPv4 address and drops all other incoming sources.

The worker owns one UDP socket and a 6 KiB internal stack. It accepts packets up to 512 bytes, at most eight parameter updates per packet, immediate bundles only, and at most 100 accepted packets per second. Detailed status includes the listener, target, peer filter, inbound apply/error counters, outbound send/source errors, and the current binding count.

OSC has no authentication or encryption. Start the job only on a trusted LAN, SoftAP, or WireGuard path. Bindings are volatile and can be restored from /.shell/startup. See man osc for address mapping, binding syntax, limits, and the sampled-event caveat.

Background jobs · SolarOS manual

pocsag

POCSAG pager receiver job. It configures a registered packet radio for a continuous POCSAG byte stream, frames successive 64-byte batches, filters pages to one receiver identity code (RIC), decodes alphanumeric or numeric payloads, and publishes completed messages to the universal inbox.

Usage:

job start pocsag <radio> <frequency-hz> <baud> <ric> [alpha|numeric] [normal|inverted]
job stop pocsag
job status pocsag
pocsag status
pocsag send <radio> <frequency-hz> <baud> <ric> <message> [alpha|numeric] [normal|inverted] [function]

Example:

job start pocsag radio 448425000 1200 1841525 alpha
inbox list unread

job stop pocsag
pocsag send radio 448425000 1200 1841525 "SolarOS calling" alpha inverted

Notes:

  • The decoder validates POCSAG parity and BCH and corrects up to two erroneous
  • bits per codeword.

  • Messages may continue across batch boundaries; the receiver follows the sync
  • words between batches until the page is complete.

  • Identical repeated pages received within 30 seconds produce one inbox entry.
  • The default FSK polarity is normal; retry with inverted if batches remain
  • at zero while the transmitter is active.

  • pocsag status shows batch/message counts, corrections, receive errors, and
  • the RSSI of the most recent batch.

  • Stopping the job restores the radio configuration and state that were active
  • when it started.

  • Sending supports messages spanning multiple batches and restores the radio's
  • previous configuration afterward. A receiver job using the same half-duplex radio must be stopped first.

Background jobs · SolarOS manual

pppd

PPP over any bidirectional SolarOS byte-stream port. A physical UART, USB CDC, or a virtual port carried by a packet-radio link can provide the bytes; the PPP negotiation mode and the interface's routing role are configured separately.

Usage:

job start pppd <port> [baud|baud=rate]
    [role=downstream|uplink|peer] [mode=passive|active]
    [local=address] [peer=address] [dns=auto|none|address]
    [priority=0..255]
job stop pppd
job status pppd

Defaults:

SettingValue
BaudKeep the port's current rate
Roledownstream
Modepassive for downstream; active otherwise
Local address192.168.8.1 for downstream
Peer address192.168.8.2 for downstream
DNSauto, copied from the preferred SolarOS uplink
Uplink priority90

Routing roles have these effects:

  • downstream enables NAPT on the PPP interface. A connected computer can use
  • whichever Wi-Fi, cellular, or WireGuard routes SolarOS currently selects.

  • uplink registers ppp-<port> as a base network path. It participates in
  • the same persistent priority selection as Wi-Fi and cellular interfaces.

  • peer creates only the point-to-point interface. It does not enable NAPT or
  • advertise the interface as a default-route candidate.

mode=active starts negotiation. mode=passive listens for the other endpoint to initiate it. Either mode can be combined with any routing role; the defaults are conveniences, not a coupling between negotiation and routing.

Examples:

Background jobs · SolarOS manual

ps2-keyboard

Receives keyboard scan-code set 2 from an exclusive named PS/2 bus and publishes press and release transitions through the generic SolarOS input service. This job is a compatibility wrapper around a ps2-keyboard expansion attachment; new configurations can attach the device directly.

expansion bus create ps2 ps2kbd clock=gpio17 data=gpio18
job start ps2-keyboard ps2kbd
job status ps2-keyboard
job stop ps2-keyboard

These commands cover an expansion bus. Boards with an integrated PS/2 keyboard declare the bus and a default expansion attachment. On TTGO VGA32 v1.4, keyboard0 is attached to ps2kbd0 before the shell starts and is inspected with expansion devices and input test keyboard0.

The bus descriptor owns the CLOCK and DATA pins as bus:ps2kbd; the wrapper's ps2-keyboard-job attachment holds the exclusive lease. Normal and extended keys, modifiers, navigation keys, function keys, and keypad usages are translated to canonical USB HID identities. The configured setterm keyboard layout and setterm keyrate repeat policy apply equally to BLE and PS/2.

The receiver validates each PS/2 frame's start bit, odd parity, and stop bit in the GPIO clock-edge handler. Scan-code parsing and input publication run from the normal job tick, outside interrupt context. The current driver only receives keyboard data; it does not send LED or reset commands to the keyboard.

Use a bidirectional level shifter or another circuit that guarantees no more than 3.3 V at the ESP32 GPIOs. ESP32 inputs are not 5 V tolerant.

Background jobs · SolarOS manual

radio-link

Packet-radio adapter for the transport-independent SolarOS Link service.

Usage:

job start radio-link <link> <radio> <profile> [inbox=off|on] [chat=off|on] [repeater=off|on]
job stop radio-link
job status radio-link
link status <link>

Example:

job start radio-link link0 radio0 lora-eu868 chat=on
link send link0 broadcast "hello"
link status link0

The job claims the radio, applies the complete named profile, creates the Link instance, transmits its queued frames, and continuously receives complete radio packets. The Link service validates its own CRC, suppresses duplicates, replies to requested unicast acknowledgements, and retains accepted messages in a bounded queue. inbox=on additionally publishes accepted text messages to the universal inbox. chat=on instead registers Link as a messaging provider, creates a broadcast conversation, discovers source IDs as Contacts, and supports direct and broadcast text through Chat. Both options are off by default and cannot be enabled together.

repeater=on turns the same packet-radio adapter into a one-hop SolarOS Link range extender. It retransmits valid frames for other destinations and broadcasts, including acknowledgements and virtual-stream packets, while preserving the original Link identities. A relayed-frame marker, randomized delay, recent-frame suppression, and a bounded four-frame queue prevent loops and reduce collisions. It is off by default. job status radio-link reports forwarded, suppressed, queued, queue-drop, and invalid-frame counters.

Repeater mode can run together with local Chat and peer-bound Link streams. For example, use job start radio-link link0 radio0 lora-eu868 chat=on repeater=on to join Chat while extending its range. Direct frames for the local Link ID are consumed locally, broadcasts are consumed and repeated, and frames for other Link IDs are repeated without local delivery. A local stream can be created with link stream create link0 vser0 <peer-id> while repeating stream traffic between other devices.

Stopping restores the radio configuration and state that existed before the job started. Mutating direct radio operations are rejected while the radio is owned by the job. See link.md for commands, frame layout, IDs, queue limits, transport MTUs, and version-one exclusions.

Background jobs · SolarOS manual

slip

IPv4 SLIP gateway on a byte-stream port. This is intended for retro machines, headless boards, and serial networking experiments. The active interface is named with the slip- prefix plus the selected port, for example slip-uart0. It appears as a downstream NAT link in the Network views; its traffic follows the selected SolarOS route, which can be Wi-Fi, cellular, or WireGuard.

Usage:

job start slip [port] [baud] [local-ip] [peer-ip] [netmask]
job stop slip
job status slip

Defaults:

SettingValue
Portuart0
Baud115200
Local IP192.168.7.1
Peer IP192.168.7.2
Netmask255.255.255.252

Examples:

job start slip uart0 115200
job start slip cdc0 115200
job start slip uart0 38400 192.168.7.1 192.168.7.2 255.255.255.252

Notes:

  • The peer should use the local IP as its gateway.
  • SolarOS enables NAT on the SLIP-facing interface.
  • The selected port is claimed by the SLIP job until it stops.
  • cdc0 is useful for Linux host testing; uart0 is the natural expansion
  • port path.

Background jobs · SolarOS manual

sump

SUMP-compatible logic analyzer server on cdc0. It claims the CDC port and uses the shared logic analyzer service for acquisition. PulseView and sigrok can connect with the OpenBench Logic Sniffer/SUMP serial driver.

Usage:

job start sump [pin ...]
job start sump [pin[,pin...]]
job stop sump
job status sump

Examples:

job start sump
job start sump 1 2 3 17
job start sump 1,2,3,17

If no pins are supplied, the job uses up to eight runtime-safe GPIOs from the active board profile. Host commands select the sample rate and capture size. The current implementation supports 10 kHz to 2 MHz requests and up to 32768 one-byte samples; the status recorded with each capture reports the measured effective rate.

Notes:

  • The job requires both CDC and runtime-safe GPIO capabilities.
  • cdc0 cannot be used by a port shell, logger, bridge, or another job while
  • SUMP is active. Start SUMP from the display shell, SSH, or another port after stopping any shell attached to cdc0.

  • Basic trigger commands are accepted for host compatibility, but this first
  • implementation captures immediately instead of waiting for a trigger.

  • Captures remain available to the logic app after the job stops.

Background jobs · SolarOS manual

telnetd

Remote Telnet shell server. The listener is a background job; each accepted connection is attached to its own normal SolarOS port-shell session.

Usage:

job start telnetd [port] [--password password]
job stop telnetd
job status telnetd

Examples:

job start telnetd
job start telnetd 2323 --password local-secret

Notes:

  • The default port is 23.
  • One remote client is supported at a time. Additional clients receive a busy
  • response and are disconnected.

  • Telnet terminal-type and window-size negotiation select the terminal profile
  • and update the shell dimensions.

  • Interactive line edits use the shared port shell's coalesced redraws, so the
  • cursor does not visibly jump to the prompt while typing.

  • While a client is attached, telnetd holds a low-latency Wi-Fi lease that
  • disables modem sleep. Disconnecting restores the normal Wi-Fi power-save policy.

  • Disconnecting closes the child shell session and releases any foreground app
  • or resource it owns.

  • Remote sessions do not run /.shell/startup.
  • Telnet is unencrypted. The optional password limits access but is also sent
  • over the network in plaintext, and the start command remains in the local shell history. Use this service only on a trusted network.

Networking and security · SolarOS manual

Network interfaces, routing, Wi-Fi, WireGuard, and APIs

Network modules are package-gated. Inspect their status before assuming Wi-Fi, cellular PPP, WireGuard, MQTT, or diagnostic networking exists in the current firmware.

Network model

SolarOS separates three networking concepts:

  • An interface is a connection such as wifi-sta, modem0, wifi-ap, or
  • the WireGuard tunnel.

  • A route decides which interface carries traffic to a destination. One
  • route is the default used when no more-specific route matches.

  • The router forwards traffic for other devices from the local downstream
  • interface through the same route table SolarOS uses for its own traffic.

Run network to open a two-tab TUI. Status combines interface state, addresses, the selected default path, VPN routes, and downstream client routing. Active SLIP and PPP jobs appear by interface name and are labelled as an uplink, downstream, or routed peer; downstream rows show whether NAT is active and which selected route carries their traffic. Settings opens the installed Wi-Fi and modem control TUIs, changes each uplink's priority, and enables or disables Wi-Fi AP routing. Downstream and peer links are status-only here; their owning jobs configure their lifecycle. A transport TUI returns to the same Network tab and selection when it exits. Tab switches views. Use the arrow keys to select a setting; Left and Right lower or raise priority, and Enter opens a transport or toggles routing. Higher priority wins. Priority overrides are saved by interface name and apply again when a runtime interface such as modem0 is registered later.

For scripts and plain output, use network status for the same combined view, network interfaces for interface state and addresses, and network routes to see the automatic base path and WireGuard routes. Transport commands configure their own interfaces: wifi manages the Wi-Fi radio and SoftAP, modem manages cellular PPP, and wireguard manages the VPN tunnel.

network router on starts the saved Wi-Fi SoftAP and enables IPv4 forwarding with NAT. Packets from AP clients follow the route table; this can send ordinary traffic through Wi-Fi station mode or cellular PPP, and matching traffic through WireGuard. Router mode remains ready while no default route exists and activates when a route becomes available. network router off disables NAT and stops the downstream AP.

job start pppd <port> creates a separate serial downstream by default. That job owns NAPT for its PPP interface, while network router owns client routing for wifi-ap. With role=uplink, the same job instead adds ppp-<port> to the base-path priority list. Downstream and peer instances remain outside default route selection but appear in Network status and routing. See jobs.reference.md.

Configure the AP name and password first when the default open SolarOS-sol network is not appropriate:

wifi ap on FieldTerminal downstream-password wpa2
wifi ap off
modem connect modem0
network router on
network status

Carrier filtering and SIM-specific ACLs remain properties of the selected network path; router mode does not add destination restrictions of its own.

Wi-Fi

Wi-Fi is enabled by default. wifi disable prevents the Wi-Fi driver and its station/AP network interfaces from initializing on the next boot. wifi enable enables them again for the next boot. Both commands leave the current boot and saved network profiles unchanged. wifi on and wifi off remain live radio controls for the current boot.

The espnow-link job uses a connectionless Wi-Fi lease. In automatic mode it follows the active station or AP channel and otherwise uses channel 6. While the lease is active, scanning is rejected. A fixed ESP-NOW channel also rejects a new station connection or an AP configured for another channel. wifi off turns off station/AP networking but reports that the radio remains active until the ESP-NOW job stops. Optional phy=lr500 and phy=lr250 modes temporarily add Espressif Long Range support to the station interface; stopping the job restores the Wi-Fi protocol selection that was active before it started.

From the shell, wifi opens the display TUI and wifi status works on every shell. In the TUI, scan opens a selectable network list; select an SSID and enter its password to connect. saved stations lists remembered station profiles and can forget them. saved access points adds, edits, or removes the stored SoftAP configuration, including its password. repeater starts or stops repeating the current or preferred saved station and shows whether forwarding is waiting or active. Routing the AP through another interface is configured with network router, not the Wi-Fi controls. A script can scan before connecting:

import solaros

for network in solaros.wifi.scan():
    print(network)
print(solaros.wifi.status())

Connecting or stopping Wi-Fi can interrupt an active agent, SSH, chat, or HTTP session. Confirm disruptive changes locally.

wifi repeater on enables IPv4 layer-2 forwarding between a station and SoftAP. It uses the current upstream station or connects the preferred remembered station. The downstream SoftAP automatically uses the same SSID and saved password as that upstream profile, so repeater mode needs only an on/off control. It does not read or overwrite the independent wifi ap configuration. For example:

wifi connect HomeNetwork upstream-password
wifi repeater on
wifi repeater
wifi repeater off

The upstream DHCP server assigns downstream clients addresses on the upstream subnet; SolarOS does not run AP DHCP or NAT in this mode. Because ordinary three-address Wi-Fi cannot carry downstream client MAC addresses through a station association, SolarOS translates link-layer addresses, learns each client's IPv4-to-MAC mapping, and proxies ARP upstream. This provides same-subnet IPv4 connectivity, but is not a fully transparent WDS bridge. IPv6 and other non-IPv4 Ethernet protocols are not repeated.

The repeated SSID matches the upstream SSID; roaming decisions are made by each client. The ESP32 station and SoftAP share one 2.4 GHz radio and the upstream channel, so repeated traffic consumes airtime in both directions and throughput is lower than a dedicated dual-radio extender. wifi repeater off leaves the station connection running. Repeater and NAT modes are mutually exclusive; the lower-level wifi ap and wifi nat commands remain available for AP setup and diagnostics. While repeater mode is active, SolarOS automatically retries a lost upstream connection with bounded backoff.

Forwarded client traffic bypasses SolarOS IP services, including a SolarOS WireGuard tunnel. Configure VPN service on the clients or upstream router when repeated clients must use it.

WireGuard

WireGuard is a native ESP-IDF/lwIP client service. Python and Lua do not own the tunnel or the socket stack. Import a conventional configuration file and start the tunnel:

wireguard import /sd/vpn/solar.conf
wireguard up
wireguard status
wireguard down
wireguard forget

The supported client subset has one [Interface] section and one [Peer] section. It accepts PrivateKey, one IPv4 Address, optional ListenPort, MTU, and one numeric IPv4 DNS; the peer accepts PublicKey, optional PresharedKey, up to eight IPv4 AllowedIPs prefixes, Endpoint, and optional PersistentKeepalive. IPv6, multiple addresses or peers, hostnames in DNS, and keys such as PostUp are rejected. The endpoint can be an IPv4 address or a DNS hostname.

wireguard import validates key encoding without printing secret values. It saves the private key and optional preshared key in NVS, then wipes temporary decoded buffers. The source configuration file remains where it was imported from. wireguard forget logically removes the saved NVS profile after the tunnel is down. SolarOS does not currently enable flash or NVS encryption, so physical flash access can recover deleted NVS secrets and any retained source file.

The allowed-prefix table controls IPv4 destination routing. A 0.0.0.0/0 prefix makes the WireGuard interface the default route. The encrypted outer UDP flow stays bound to the currently preferred base interface to avoid routing it back into the tunnel. That underlay can be Wi-Fi, cellular PPP, Ethernet, SLIP, or another IPv4-capable path registered with the network service. A full tunnel uses fail-closed behavior by default. While its hostname is being resolved, only endpoint-resolution DNS and DHCP traffic may use the underlay directly. After resolution, only WireGuard endpoint UDP and DHCP remain permitted. This also blocks direct local-network and IPv6 traffic. Select wireguard up fail-open to restore direct underlay routing if the peer is down. For split tunnels the default is fail-open; fail-closed prevents matching prefixes from falling through but does not block unrelated direct underlay traffic.

The service stops its lwIP interface before light sleep and recreates it after an uplink resumes. It also tears down and reconnects when route priority or link state selects another base interface. wireguard status reports the selected underlay. Handshake timestamps prefer synchronized wall time. A persisted forward-only reservation supplies replay-safe timestamps when wall time is not synchronized.

MQTT

Connect to a broker, subscribe, then read messages with bounded timeouts. MQTT settings are stored by the service; do not embed credentials in a public script.

Quick reference

solaros.wifi provides status, status_text, start, stop, connect, connect_saved, disconnect, forget, forget_ssid, forget_all, known, scan, ap_start, ap_stop, nat, repeater_start, and repeater_stop. solaros.net provides router_start, router_stop, ping, and socket APIs. WireGuard intentionally has no Python or Lua binding. solaros.mqtt provides status, connect, disconnect, publish, subscribe, and read. solaros.net.ping(host, optional count, timeout_ms, interval_ms, data_size) returns statistics. These modules are package-gated.

Networking and security · SolarOS manual

Open Sound Control

SolarOS implements a bounded OSC 1.0 subset over IPv4 UDP. It accepts exact addresses for live native application parameters and sends explicitly named stream, event-stream, or normalized-control bindings to one configured target.

OSC has no authentication or encryption. Keep the job stopped until it is needed. Use it only on a trusted LAN or SoftAP, or carry the traffic through a trusted WireGuard tunnel. The optional peer= setting restricts incoming packets to one IPv4 address; it is an allowlist, not authentication.

Incoming parameters

Start the job with a UDP listening port:

job start osc listen=9000

The default port is 9000. Incoming addresses map automatically to parameters registered by the currently running application. Use the parameter address for native units, or append /normalized for a normalized value:

/solaros/parameter/synth/filter/cutoff
/solaros/parameter/synth/filter/cutoff/normalized
/solaros/parameter/synth/osc2/mix
/solaros/parameter/funcgen/frequency

The native address accepts exactly one OSC float32, int32, True, or False argument in the parameter's native unit. The /normalized address accepts one float32 in the range 0.0..1.0, or True and False for the endpoints. It rejects int32 arguments so integer native values cannot be mistaken for normalized values.

SolarOS converts the address after /solaros/parameter/ from slash components to the native dotted path. The parameter service applies the declared range, linear or logarithmic curve, and step quantization. Synth and Funcgen publish their parameters only while they are running, so a write to a closed application increments the unknown counter.

Immediate OSC bundles are accepted with at most two bundle levels and eight messages per packet. Future timetags, address patterns, blobs, MIDI values, automatic echo, OSCQuery, and scheduled execution are not part of this version. Packets are limited to 512 bytes and accepted traffic is limited to 100 packets per second.

Outgoing bindings

Set one destination when the job starts:

job start osc listen=9000 target=192.168.1.50:9001

Bindings have stable semantic names and user-selected OSC addresses:

osc bind ambient stream temperature /room/temperature rate=2 delta=0.1
osc bind voltage stream battery /device/battery rate=1 delta=0.01
osc bind cutoff-out control cutoff /surface/cutoff
osc bind button stream gpio17 /surface/button edge=both

Scalar streams send a float32 in the stream's native unit. Named controls send a normalized float32 in the range 0.0..1.0; calibration, smoothing, deadband, and inversion remain owned by service.controls. Event streams send int32 1 on a rising edge and int32 0 on a falling edge, filtered by edge=rising|falling|both.

rate= accepts 0.1..100 Hz and defaults to 50 Hz. Scalar and control bindings are change-only by default. delta= sets the minimum change in the source's native value, while send=always sends the current value at each rate interval. A missing source remains configured and is retried automatically.

Current GPIO event streams expose sampled state. They are not interrupt-edge queues, so OSC can miss a pulse that starts and ends between samples. OSC does not read the foreground input queue and cannot steal keyboard events from the active application.

Inspect or remove runtime bindings with:

osc bindings
osc unbind ambient
osc clear

Bindings are volatile. Put the osc bind and job start osc commands in /.shell/startup when they must be restored after reboot.

Python and Lua

Both runtimes expose the same native OSC service as solaros.osc:

solaros.osc.bind_stream(
    "ambient", "temperature", "/room/temperature", 2.0, 0.1
)
solaros.jobs.start(
    "osc", ["listen=9000", "target=192.168.1.50:9001"]
)

Lua uses the same names and positional arguments. bind_stream, bind_event, and bind_control return binding IDs. bindings() returns the complete runtime state; unbind() and clear() remove definitions. The optional rate is in Hz, scalar-stream delta uses native units, and the final Boolean selects send-always behavior for scalar and control bindings.

encode_float(address, value) and encode_int(address, value) return binary messages for managed UDP APIs. dispatch(packet) applies the same bounded parameter parser used by the OSC job and reports message, applied, unknown, and rejected counts. limits() reports the public packet, address, binding, bundle, update, and rate bounds. Socket ownership and filtering remain with the native job and solaros.jobs API.

SolarOS controller and synth devices

A normalized address lets one SolarOS device use a calibrated physical control to drive a parameter on another device without duplicating its native range or curve. On the controller device:

control create cutoff adc1 100 3200 smooth=20 deadband=8
osc bind cutoff-out control cutoff /solaros/parameter/synth/filter/cutoff/normalized
job start controls
job start osc target=192.168.1.40:9000

On the synth device:

job start osc listen=9000 peer=192.168.1.30
synth

The controller sends 0.0..1.0. The synth device converts it through the live parameter's logarithmic cutoff curve and native range.

Status and filtering

job start osc listen=9000 target=192.168.1.50:9001 peer=192.168.1.50
job status osc
job stop osc

Detailed job status reports the listening port, target, peer filter, inbound packet/application/error counters, outbound sends and failures, source errors, and binding count. osc bindings adds each source state, last value, last send time, and last error.

Quick reference

job start osc [listen=port] [target=host:port] [peer=ipv4]
job status osc
job stop osc
osc bindings
osc bind <name> stream <stream> <address> [rate=hz] [delta=value] [send=change|always]
osc bind <name> stream <event-stream> <address> edge=rising|falling|both [rate=hz]
osc bind <name> control <control> <address> [rate=hz] [send=change|always]
osc unbind <name>
osc clear

Python/Lua: solaros.osc.bindings, bind_stream, bind_event, bind_control, unbind, clear, encode_float, encode_int, dispatch, and limits.

Networking and security · SolarOS manual

SSH identity keys

SolarOS can keep a default SSH key pair for ssh and scp. The public key may be copied to remote hosts; the private key must remain protected.

Inspect or create a key

sshkey status
sshkey gen 2048
sshkey pub

Generating a key may take time and memory. Without -f, an existing key is not overwritten.

Protect private material

Do not print, copy, edit, or delete the private key unless the user explicitly requests that exact operation. The agent's storage tools reject paths below .ssh.

Quick reference

solaros.ssh_keys provides default_paths, default_exists, status, public_key, generate (optional bits and overwrite), and remove when SSH is installed. public_key() returns only the default OpenSSH public-key line. Do not inspect, expose, replace, or delete private key material unless the user explicitly requests that operation.

Hardware and expansion · SolarOS manual

Audio, input, and clipboard APIs

These services are independent even though they are often used by foreground applications. Inspect availability before calling an optional audio or BLE operation.

Keyboard input

Local hardware input uses structured press, release, and repeat events. The input service tracks held keys by source and stable physical identity, while legacy shell and text applications continue to receive translated characters. BLE, PS/2, CardKB, and the integrated CL-32 keyboard, fixed board buttons, gpio-keys, and ADC D-pads share one repeat policy. Configure it with setterm keyrate; the setting applies even on a build without BLE. Analog joysticks are different: they publish normalized axes and never synthesize key events.

Keyboard transports can additionally supply a canonical USB HID usage and modifier mask. This keeps physical controls independent of the selected text layout and lets BLE and PS/2 share the same US or German keymap.

On a board with absolute touch and PSRAM, job start graffiti turns the whole display into a Palm Graffiti handwriting keyboard without drawing an overlay. The stroke's initial X coordinate selects its alphabet: the left two-thirds is letters and the right one-third is numbers. The selection does not change while the stroke is in progress. Recognized strokes enter the normal keyboard queue, so they follow the current input focus. The job observes absolute touch without consuming it; foreground applications still receive the pointer events. See jobs.reference.md for controls and ownership.

input test <source> counters are cumulative from the time that source attached. Each accepted key press, release, or repeat increments key; it is not a count of currently held keys. Character-only devices such as CardKB emit one press and one release per character, so one tap normally adds two events. Their last event has physical=0 and usage=0; key=10 is newline/Enter.

CL-32 core0 polls the integrated AVR event FIFO every 10 ms and registers keyboard0. It preserves press and release transitions. Shift and Fn cycle through one-shot, locked, and off states. File is left Alt, Menu is left Ctrl, OK is Enter, Cancel is Escape, and Fn with keypad 1 through 0 produces F1 through F10.

PS/2 keyboard

PS/2 uses a named, exclusive CLOCK/DATA bus. Attach a keyboard device to that bus:

expansion bus create ps2 ps2kbd clock=gpio17 data=gpio18
expansion attach ps2-keyboard keyboard0 ps2=ps2kbd
input keyboard
input test keyboard0

This manual setup is for an attached expansion keyboard. A board with an integrated PS/2 keyboard capability, such as TTGO VGA32 v1.4, declares its bus and default expansion attachment automatically during boot. The compatible ps2-keyboard job remains as a wrapper for existing scripts.

The receiver supports keyboard scan-code set 2, including normal and extended press/release sequences. SolarOS supplies repeat through the generic input service, so keyboard typematic make codes do not create duplicate presses. The keyboard attachment is receive-only: keyboard LEDs and keyboard-specific host commands are not implemented.

ESP32 pins are not 5 V tolerant. Use proper level shifting, or otherwise ensure that neither PS/2 signal can be pulled above 3.3 V. Do not connect a 5 V signal directly to a GPIO merely because PS/2 uses open-collector signalling.

Pointers and axes

Use input to inspect semantic sources independently of their transport:

input touch
input mouse
input joystick
input gesture
input test touch0

Touch and other absolute pointers report positions; mice report relative deltas; analog joysticks report normalized X/Y axes; gesture sensors report recognized motions and taps. Pointer, axis, and gesture queues are allocated only when the first matching source attaches. input test retains counters and the most recent accepted event, so it also works for polling touch controllers while the shell is active.

Native foreground applications opt in to structured pointer input with SOLAR_OS_APP_FLAG_POINTER_EVENTS and to axis input with SOLAR_OS_APP_FLAG_AXIS_EVENTS. Gesture-aware applications opt in with SOLAR_OS_APP_FLAG_GESTURE_EVENTS. Their event callback then receives SOLAR_OS_EVENT_POINTER in event.data.pointer or SOLAR_OS_EVENT_AXIS in event.data.axis, or SOLAR_OS_EVENT_GESTURE in event.data.gesture. Pointer events contain the source, pointer ID, absolute or relative mode, action, coordinates, deltas, buttons, and optional display target. A non-empty target routes to the active opted-in application on that display. Its absolute coordinates and deltas follow the target's current setterm orientation; orientation 0 is the device driver's normal mounting. An empty target follows local input focus. Axis events contain the source, X/Y/Z/RX/RY/RZ axis, normalized value, and delta and follow local input focus. Gesture events contain the recognized kind, direction, flags, optional value, and original sensor word and also follow local input focus. Applications without the matching flag do not receive those structured events.

Gesture bindings observe the same events without taking them away from the foreground application. A binding can match one source or every source, one gesture kind, and optionally one direction. Its command runs on a single on-demand background worker, so two gestures never execute shell commands concurrently. The worker releases its internal stack after the command queue becomes idle. The cooldown suppresses repeated sensor reports; it defaults to 250 ms.

gesture bind source=gesture0 gesture=flick direction=east -- input emit ALT+RIGHT
gesture bind source=gesture0 gesture=flick direction=west cooldown=400 -- input emit ALT+LEFT
gesture bind source=* gesture=double-tap -- /flash/bin/toggle-light.sh
job start gesture-listener
gesture bindings
job stop gesture-listener
gesture unbind 2

input emit creates a virtual local keyboard on first use and injects a key tap into the normal input-focus path. Modifier chords use CTRL, SHIFT, ALT, GUI, or their left/right forms, for example ALT+RIGHT. It does not send USB or BLE HID reports. A chord tap releases its modifiers together with the named key. Bindings are deliberately volatile and remain configured when the gesture-listener job stops. Put the required gesture bind commands followed by job start gesture-listener in the selected startup shell script to recreate and activate them after boot. gesture unbind all removes every rule and resets the next binding ID to 1. Background commands may use shell built-ins or invoke scripts, but they cannot launch a foreground application. Stopping the job prevents new gesture actions and discards queued actions; a command already executing is allowed to finish. Use an explicit display target for display actions, for example setterm --display display0 orientation 90.

Foreground Python and Lua scripts receive the same structured pointer, axis, and gesture events through solaros.input.read([timeout_ms]). Touch events expose absolute x/y coordinates and press/move/release actions; relative mice expose delta_x/delta_y and button bits; joystick events expose their named axis, normalized value, and delta; gesture events expose names, direction, flags, value, and raw sensor data. solaros.input.sources() lists the registered semantic sources. Each runtime keeps a bounded 16-event foreground queue and reports overwritten events through solaros.input.status().dropped. Keyboard characters remain on solaros.tui.getch().

Absolute-pointer calibration maps a source's raw logical coordinates into a target extent and stores the mapping in NVS under that source name:

input calibrate touch0
input calibrate touch0 set 0 479 0 319 480 320
input calibrate touch0 reset

The device driver still owns physical orientation. Calibration only clamps and scales the already oriented X/Y values. Mouse deltas and joystick axes do not use pointer calibration.

Attach a standard relative PS/2 mouse to a named bus with:

expansion bus create ps2 ps2mouse clock=gpio17 data=gpio18
expansion attach ps2-mouse mouse0 ps2=ps2mouse
input test mouse0

The mouse attachment enables standard three-byte reporting and publishes relative motion plus the primary, secondary, and middle buttons. Confirm the connector's pinout, supply, and signal voltage before wiring it; ESP32 GPIOs are not 5 V tolerant.

An analog joystick consumes two existing scalar streams. Use stream list and adc status to find the actual stream names, then bind the measured range:

expansion attach analog-joystick joystick0 x=adc2 y=adc4 min=0 center=1650 max=3300 deadzone=100
input test joystick0

The attachment normalizes both streams as axes. Applications decide what those axes mean; SolarOS does not turn them into arrows or other keys.

Audio

Use global volume unless a diagnostic or playback command explicitly needs an override. Call deinit() or off() when a script owns output that should not remain active.

Global volume follows the selected playback device, including runtime-attached outputs on boards without built-in audio. Selecting or opening a volume-capable output applies the current global value, and Player, WebRadio, Recorder, and Synth initialize their volume controls from that shared state.

Recording and playback require enough internal/DMA memory even on boards with PSRAM. If an audio application reports no memory, stop unnecessary internal stack jobs and inspect mem.

tone_async() queues a short tone and returns a request ID without waiting for playback. Use cancel() with that ID or inspect queue_status() for the current request and completed, cancelled, dropped, and failed counters. The queue is bounded and shares exclusive output ownership with WAV playback and native synth clients. A queued tone waits for that output; a full queue reports an error to the caller.

solaros.synth gives Python and Lua scripts an eight-voice native synthesizer with two oscillators per voice, square, triangle, saw, sine, and noise waveforms, velocity, amplifier and filter ADSR envelopes, and resonant low-pass filters. The second oscillator adds octave, fine-detune, and mix controls. The interpreters send note and configuration commands; rendering stays in the native audio task. The first note claims exclusive audio output, and script shutdown releases it automatically. Synth voices use global speaker volume rather than overriding it.

BLE keyboard

The BLE service manages one remembered keyboard and publishes its HID report transitions through the generic input service. Pairing and scanning are system operations; a script can inspect state and read translated key events. BLE follows the board default when no user preference is saved. Most boards enable it; TTGO VGA32 v1.4 disables it to preserve internal heap. Use setterm ble on|off|default to select an explicit next-boot value or return to the board default. The compatible ble enable, ble disable, and ble default commands provide the same settings. These commands leave the current boot unchanged and do not forget the remembered keyboard or its bond. On a BLE-disabled boot, the unused Bluetooth controller and host memory is returned to the internal heap before normal service initialization. ble forget erases the remembered keyboard from SolarOS NVS and removes its BLE bond and cached GATT service database. This forces service rediscovery when the same keyboard address is paired again, including after keyboard firmware changes move its GATT handles. On boards with a system KEY, a long press performs that forget operation and then starts a new pairing scan. Pairing has no user cancellation path. The KEY short-press power action remains separately configurable with setterm powerkey sleep|suspend. Suspend is the default; another short press resumes the display and restores the prior power profile.

Clipboard

The clipboard stores bounded text shared by applications. Clear sensitive content after use.

Quick reference

solaros.audio provides status, deinit or off, set_volume, set_mic_gain, tone, tone_async, cancel, queue_status, level, capture, loopback, wav_info, record_wav, and play_wav. capture(frames) returns 1 through 4096 frames as interleaved little-endian signed-16 PCM plus its native sample format, rate, channel count, and sample width. solaros.synth provides status, configure, configure_oscillator2, configure_filter, configure_performance, note_on, note_off, all_notes_off, and stop. solaros.ble provides status, connected, pair, forget, layout, read. solaros.clipboard provides set, get, size, clear. Audio, synth, and BLE are package-gated. Foreground Python and Lua applications use solaros.input sources, read, clear, and status for structured pointer, axis, and gesture events; keyboard characters remain on solaros.tui.getch().

Hardware and expansion · SolarOS manual

Continuous controls

SolarOS controls turn scalar streams into named normalized values. A control can drive one or more native foreground-app parameters or MIDI Control Change messages. This keeps ADC calibration, smoothing, deadband, inversion, and pickup behavior independent of the destination.

List the scalar streams that exist on the running board:

stream

For a potentiometer whose wiper is connected to an ADC-capable expansion pin, create a control using its measured millivolt endpoints. This example uses adc1 and maps 0 through 3300 mV to the full control range:

control create cutoff adc1 0 3300 smooth=40 deadband=8

smooth= is an exponential smoothing time in milliseconds. deadband= uses the source unit, so it is millivolts for an ADC stream. Add invert when the physical direction is reversed. Use the actual endpoint readings when the potentiometer does not reach 0 or 3300 mV.

Native application parameters

A running native application can publish tunable parameters with stable paths, ranges, units, steps, and linear or logarithmic curves. Inspect the parameters that are currently available:

control parameters

The Synth app publishes parameters such as synth.filter.cutoff. Bind the potentiometer to it and start the sampler:

control bind cutoff parameter synth.filter.cutoff pickup=on
job start controls
synth

Pickup is soft takeover. The hardware knob does not change the parameter until it reaches or crosses the app's current value, preventing a sudden jump when a preset loads or the app resumes. The binding remains configured while its app is suspended or stopped and applies again when the parameter path returns.

For direct inspection or testing:

control parameter get synth.filter.cutoff
control parameter set synth.filter.cutoff 1200

MIDI CC

Control to MIDI CC

A control can drive a MIDI controller after the MIDI bus and worker are running:

expansion bus create midi midi0 tx=gpio2 rx=gpio3
job start midi midi0
control bind cutoff midi 1 74
job start controls

The normalized 16-bit control value is scaled to the MIDI range 0..127. Channel numbers are 1..16; controller numbers are 0..127.

MIDI CC to application parameter

Expose an incoming MIDI controller as a scalar stream, then use the same control-to-parameter path as an ADC or other measurement:

expansion bus create midi midi0 tx=gpio2 rx=gpio3
job start midi midi0
midi monitor
midi stream add 1 74
control create cutoff midi.cc.1.74 0 127
control bind cutoff parameter synth.filter.cutoff pickup=off
job start controls
synth

Move a controller while midi monitor is running to discover its channel and controller number. It prints mapping-oriented lines such as:

CC: 1 74 64
KEY: 1 60 100
KEY: 1 60 0

The fields are channel, controller/note, and value/velocity. Stop the monitor with the app-exit key, Esc, or q, then create the matching stream.

midi stream add <channel> <controller> registers an exact scalar stream named midi.cc.<channel>.<controller>. It reports 0..127 and retains the latest matching value while the MIDI job is running. It is non-consuming, so MIDI subscribers such as Synth still receive the original message.

Python and Lua can perform the same management with solaros.midi.streams(), stream_add(), stream_remove(), and stream_clear(). Their solaros.midi.read() API uses its own non-consuming subscription, so a script can observe messages without stealing them from Synth or another subscriber.

Up to 16 MIDI CC streams can be configured. Explicit registration avoids reserving stream-registry entries for all 2,048 possible channel/controller pairs. A new stream reports waiting until its first matching message. It returns to that state whenever the MIDI job stops or restarts.

Inspect or remove the definitions with:

midi stream list
midi stream remove 1 74
midi stream clear

MIDI CC stream definitions are volatile. Restore midi stream add commands from /.shell/startup with the related control and job commands when needed.

Manual and script controls

Use manual instead of a stream when a Python or Lua program supplies the value. Scripts can either use an existing shell configuration or create the control and its typed binding directly:

control create expression manual 0 1
control set expression 32768

Python and Lua expose values as floating-point numbers from 0.0 through 1.0:

import solaros

solaros.controls.set("expression", 0.5)
print(solaros.controls.get("expression"))
local solaros = require("solaros")

solaros.controls.set("expression", 0.5)
print(solaros.controls.get("expression"))

The equivalent complete Python setup is:

solaros.controls.create("expression")
solaros.controls.bind_parameter(
    "expression", "synth.filter.resonance", False
)
solaros.jobs.start("controls")

Lua uses the same function names and positional arguments. Both runtimes also provide controls.delete(), clear(), bindings(), bind_midi(), and unbind(). The solaros.parameters table lists dynamic native parameters and gets or sets their values without going through a control.

Inspection and removal

control list
control bindings
control get cutoff
control unbind cutoff
control delete cutoff
control clear
job stop controls

control list reports normalized and raw values, sample and update counts, and the last source error. control bindings reports target state, pickup state, application counts, and the last target error. control unbind <name> removes all parameter and MIDI targets owned by that control. Control and binding definitions are runtime configuration; place the creation, binding, and job-start commands in /.shell/startup to restore them after reboot.

Quick reference

Controls normalize scalar streams to 0..65535, apply optional smoothing, deadband, and inversion, and fan out to typed targets. Native app targets can use soft takeover and survive temporary parameter absence. MIDI targets emit CC values from 0..127. Python and Lua use normalized values from 0.0 to 1.0 and expose the complete configuration, binding, and dynamic-parameter management surface.

Hardware and expansion · SolarOS manual

Expansion drivers and attached devices

Expansion drivers turn named buses and safe GPIO slots into active displays, radios, sensors, or manual resource profiles. Drivers are package-gated, so the available list depends on the firmware and board.

Control-line bindings such as device power may use either a native ESP GPIO number or a named GPIO-controller line such as gpiox0:12. Expanded GPIO lines are digital control resources only. They cannot replace native ESP pins in SPI, I2C, UART, I2S, PWM, interrupt, or other bus/peripheral-signal bindings.

Integrated hardware uses the same composition model. A board profile declares its fixed buses and default attachments, which are created at boot, shown by expansion devices, and cannot be detached. For example, Freenove touch0 is an ft6336 attachment; Waveshare rtc0 and environment0 use pcf85063 and shtc3. Battery boards expose battery0 through battery-adc, max17048, bq27220, or an integrated controller adapter. TTGO VGA32 keyboard0 is a ps2-keyboard attachment. Built-in audio also appears as audio0: SolarTerm uses es8311-es7210, Freenove uses es8311-duplex, T-Deck Plus uses i2s-output with a separate es7210 capture device, and classic ESP32 audio boards use esp32-dac. CL-32 declares its integrated AVR as fixed core0; its polled event FIFO supplies the keyboard0 input source and its voltage and power-status registers supply battery0. Generic input, time, sensor, battery, and audio services consume the same runtime providers whether the attachment came from the board profile or the shell.

Built-in displays follow the same rule and appear as fixed display0 attachments: Waveshare uses st7305, Freenove uses st7796, ODROID-GO uses ili9341, Elecrow CrowPanel uses ssd1683, ESP32-WROVER v3.0 uses cvbs-pal, T-LoRa-Pager uses st7796, T-Deck Plus uses st7789, and TTGO VGA32 uses vga32. They attach before the splash and primary display service start.

Built-in SDMMC slots also appear as fixed storage0 attachments. SolarTerm, Waveshare ESP32-S3-SIM7670G-4G, and ESP32-WROVER v3.0 use one-bit bindings; Freenove uses four-bit bindings. The attachment claims and configures the pins early, while the normal storage phase still probes and mounts the card. The Waveshare 4G target also registers fixed battery0, pixels0, and modem0 attachments through the MAX17048, NeoPixel, and SIM7670 drivers.

Elecrow CrowPanel, CL-32, T-LoRa-Pager, and T-Deck Plus use fixed sdspi attachments instead. Their built-in SPI devices share named buses while claiming separate chip-select resources.

Named MIDI connections are created as buses rather than attached drivers. Use expansion bus create midi <name> tx=<gpio> rx=<gpio>; SolarOS chooses the UART backend and the midi background job owns the connection while it runs.

Discover what is present

expansion
expansion status
expansion drivers
expansion devices
display list

expansion opens the device manager. Its Devices view lists current attachments and opens their details; detachable runtime devices can be removed after confirmation. Its Drivers view groups drivers by category and opens an attachment form for drivers supported by the running board. Binding forms use existing named buses; create, attach, detach, or remove buses in the io app. expansion status retains the textual capabilities, buses, devices, and claims report for scripts and terminal inspection.

expansion drivers groups compiled drivers under bold Audio, Display, Input, Power, Radio, Sensor, Storage, and Utility headings, with driver names sorted inside each category. Its aligned rows also show probe support, bus type, and the driver summary. expansion devices prints each attached device in a separate block, with its name in bold followed by origin, readiness, startup mode, attachment policy, and bindings.

From a script, inspect solaros.expansion.drivers() and solaros.expansion.devices(). A driver existing in firmware does not mean a physical device is attached.

Attach deliberately

Use real bus and pin names returned by discovery:

expansion attach pcd8544 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5
display test lcd0
expansion detach lcd0

Detaching releases the resources. Do not invent a target name or copy bindings from a different board.

To restore a runtime attachment after reboot, open its device detail in expansion and press S. SolarOS reconstructs the normalized expansion attach command and adds it once to the startup script selected by setterm startup. If the device uses a runtime bus, save that bus for startup from io first so its expansion bus create command appears earlier in the script. Compiled board devices already start automatically and do not need a saved command.

When an experimental wiring arrangement becomes permanent, export its hardware description instead of copying startup commands:

expansion export /sdcard/my-hardware.toml

The versioned solaros-expansion manifest contains the current board ID plus runtime-created buses and runtime-attached catalog devices. References to board-owned buses remain references, so the base board hardware is not duplicated. Readiness, leases, service state, secrets, and shell startup commands are not part of the file. A manual attachment cannot be exported because it has no catalog driver contract that the board compiler can validate. The destination is written through a temporary file and atomically replaced.

The export format is a strict TOML contract. Schema 1 has this shape:

schema = 1
kind = "solaros-expansion"

[base]
board = "waveshare_esp32_s3_sim7670g_4g"
firmware = "4.13.1"

[[buses]]
name = "spi0"
protocol = "spi"
sharing = "shared"
host = "SPI2_HOST"
sclk = 7
mosi = 8
miso = 3
cs = [9]
max_transfer_size = 4096

[[devices]]
driver = "ssd1683"
name = "display0"
bindings = { spi = "spi0", cs = 9, dc = 10, reset = 11, busy = 12 }

base.board selects the manifest that the generated target inherits; base.firmware records export provenance and is not a compatibility gate. Only runtime-created I2C, SPI, UART, MIDI, 1-Wire, and PS/2 buses appear in buses. Each bus carries its concrete controller and protocol-specific wiring. Only runtime catalog devices appear in devices; each device carries the normalized string or integer bindings accepted by its driver. Board-owned buses may still be referenced by an exported device without being duplicated. The importer rejects unsupported schema versions, unknown fields, duplicate names, missing base boards, unavailable controllers or pins, unknown drivers, invalid bindings, and conflicts with existing board resources. Treat the file as generated interchange data; make lasting hardware changes in the resulting board manifest.

Move the file to a SolarOS source tree and run:

python3 scripts/board_config.py --expansion-manifest my-hardware.toml

The configurator loads the recorded base manifest, asks for the new board identity, validates the combined pin and bus ownership, and writes a small inherited board profile. Imported hardware then starts as board-owned after the custom target is built and flashed. Remove any matching temporary startup commands yourself; they are intentionally outside the export contract.

A Waveshare 4.2-inch V2 monochrome e-paper module uses the SSD1683 expansion driver and registers a 400x300 display target:

expansion attach ssd1683 epd0 spi=spi0 cs=gpio10 dc=gpio17 reset=gpio16 busy=gpio15
display test epd0
display mode epd0 refresh=fast
expansion detach epd0

The runtime defaults are the Waveshare V2 panel profile, 2 MHz SPI, and rotation 0. Optional power=<gpio>, clock=<khz>, rotation=<0..3>, and panel=<0..3> bindings adapt the same driver to integrated panels. Panel 0 selects Elecrow BUSY-based revision detection and defaults to rotation 2; panels 1, 2, and 3 select the legacy Elecrow, green-sticker Elecrow, and Waveshare V2 profiles.

Use the module's eight-wire SPI connector and power it from the same 3.3 V logic domain as the ESP32. The module keeps its last image after detach. If SolarOS creates a display shell on epd0, run sessions, close that session with session close <id>, and then detach the expansion.

An M5Stack Unit CardKB attaches at its fixed I2C address and becomes a shared keyboard source for the shell and foreground apps:

expansion attach cardkb cardkb0 i2c=i2c0 addr=0x5f
input test cardkb0
expansion detach cardkb0

The CardKB firmware produces characters after key release. SolarOS maps its four navigation values to the same logical arrow keys used by PS/2 and BLE keyboards. The module's Fn combinations are device-specific and are ignored.

RTC and environmental sensor modules use the same named-I2C lifecycle. Environmental adapters register named providers with the generic sensor service. A provider can supply temperature, humidity, or both, and multiple providers can be active at the same time:

expansion attach pcf85063 rtc0 i2c=i2c0 addr=0x51
expansion attach shtc3 environment0 i2c=i2c0 addr=0x70
date
temperature
humidity
temperature list
humidity list
expansion detach environment0
expansion detach rtc0

The PCF85063 irq=<gpio> binding is optional. The Waveshare board profile reserves its routed RTC interrupt on GPIO15; external modules can omit irq when only clock and calendar access is required. The generic RTC service discovers alarm, countdown, and interrupt-status support from the attached chip adapter, so applications do not depend on the PCF85063 register interface. The generic sensor service similarly exposes provider names, driver names, and temperature/humidity capabilities without exposing a chip register interface.

battery-adc takes an ADC pin and a divider ratio in thousandths. For a 2:1 resistive divider, use divider=2000:

expansion attach battery-adc battery0 adc=gpio4 divider=2000
battery
expansion detach battery0

Use a high-impedance divider suitable for the expected battery voltage. The divided voltage must remain inside the ESP32 ADC input range.

Other input devices follow the same lifecycle:

expansion attach gpio-keys keys0 key:UP=gpio17 key:ENTER=gpio2
expansion attach rotary-encoder wheel0 a=gpio17 b=gpio2
expansion bus create ps2 ps2mouse clock=gpio17 data=gpio18
expansion attach ps2-mouse mouse0 ps2=ps2mouse
expansion attach analog-joystick joystick0 x=adc2 y=adc4 min=0 center=1650 max=3300 deadzone=100
input status

Use only bindings listed by expansion drivers and resources shown on the running board. A rotary encoder decodes interrupts from its quadrature A/B signals and publishes Up/Down detents; wire its independent push switch through gpio-keys. A PS/2 mouse publishes relative pointer events. An analog joystick consumes two scalar streams and publishes axes, never keys. Foreground Python and Lua applications receive those pointer, axis, and gesture events through solaros.input; use solaros.tui.getch() for keyboard characters.

On a board without built-in SD hardware, an SPI microSD adapter can provide removable storage. The SPI bus must include MISO and declare the selected CS pin. Attaching mounts the detected FAT volume at /sdcard. Unmount it before detaching the adapter:

expansion attach sdspi card0 spi=spi0 cs=gpio4
disk lsblk
disk umount
expansion detach card0

If the adapter has a switched supply, add power=<gpio> or power=<controller:line>. The SD driver enables that line before probing and disables it after unmount and detach. The SPI bus and CS remain native ESP peripheral resources.

The sdspi attach command prints the card probe result to the invoking shell. The report includes the card identity, type, negotiated speed, capacity, CSD/SSR details, mount point, and any underlying block-I/O or FatFs mount error.

An SDMMC adapter uses direct clock, command, and data bindings. On ESP32-S3 the signals can use the GPIO matrix. Classic ESP32 accepts only the native slot-1 pinout: CLK GPIO14, CMD GPIO15, D0 GPIO2, and optionally D1 GPIO4, D2 GPIO12, D3 GPIO13.

expansion attach sdmmc card0 clk=gpio14 cmd=gpio15 d0=gpio2
disk lsblk
disk umount
expansion detach card0

An RFM95W wired to the ESP32-S3-DevKitC-1 spi0 bus with NSS on GPIO4 and reset on GPIO5 attaches as a multimode packet radio:

expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5
radio status radio0
radio profile apply radio0 lora-eu868

Connect an antenna suitable for the module band before transmitting. See the expansion reference for the complete wiring and modulation configuration.

Use rfm69 for the 13 dBm RFM69W/CW modules and rfm69h for the 20 dBm RFM69HW/HCW variants. For example:

expansion attach rfm69h radio0 spi=spi0 cs=gpio4 reset=gpio5
radio config radio0 power 20

Both variants are 3.3 V devices. High-power transmission requires a supply that can sustain the module's transmit-current peak and an antenna appropriate for the selected band.

A WS2812/NeoPixel strip uses one runtime-safe GPIO and a declared pixel count:

expansion attach neopixel pixels0 data=gpio1 count=8
neopixel fill pixels0 16 0 0
neopixel set pixels0 3 0 16 0
neopixel clear pixels0
expansion detach pixels0

SolarOS stores colors in RGB form and transmits the strip's standard GRB wire order. Attach clears all declared pixels. set and fill refresh immediately in the shell; scripting APIs buffer changes until show().

An LEDC PWM audio output uses one runtime-safe PWM pin and appears as a normal mono playback device:

expansion attach audio-pwm pwm0 pwm=gpio1
audio devices
audio default pwm0
aplay /audio/example.mp3
audio default auto
expansion detach pwm0

The output is 8-bit PWM with a 78.125 kHz carrier and a 16 kHz native PCM rate. It is a signal output, not a speaker driver. Put a reconstruction low-pass filter, DC-blocking/coupling stage, and suitable amplifier between the GPIO and a speaker. A bare speaker can overload and damage the GPIO. Only one LEDC PWM audio device can be attached at a time.

A PCM5102A module uses three runtime-safe GPIOs and appears as a normal stereo playback device:

expansion attach pcm5102 dac0 i2s=i2s1 bck=gpio1 din=gpio2 rck=gpio3
audio devices
audio default dac0
aplay /audio/example.mp3
audio default auto
expansion detach dac0

The example pins are valid on the Waveshare board. Connect the module's SCK pin to ground; the driver emits 64 BCK cycles per stereo frame so the PCM5102A can derive its system clock with the internal PLL. The driver registers dac0.playback as an exclusive 16 kHz, 16-bit PCM sink. Mono input is duplicated to both channels and device volume is applied in software. Select a runtime-safe controller explicitly with the i2s= binding, leaving other I2S controllers available to onboard audio or composite video. PCM5102A modules provide line-level output; connect an amplifier or powered input rather than a passive speaker.

A PCM1808 module uses four runtime-safe GPIOs and appears as a stereo capture device:

expansion attach pcm1808 adc0 mclk=gpio1 bck=gpio2 ws=gpio3 dout=gpio17
audio devices
arecord -d 5 -i adc0.capture /sdcard/pcm1808.wav
expansion detach adc0

The example consumes every runtime-safe GPIO on the Waveshare expansion header. Before applying power, configure the module for slave I2S mode: MD1=0, MD0=0, and FMT=0. Connect MCLK to the module's SCKI or SCK pin, WS to LRCK, and DOUT to DOUT. The driver supplies a 4.096 MHz system clock and 64 BCK cycles per 16 kHz stereo frame, receives the PCM1808's 24-bit samples, and registers adc0.capture as an exclusive signed 16-bit PCM source. Use the module-rated supply and a common ground; raw PCM1808 circuits need separate analog and digital supplies as specified by the manufacturer. On modules that expose both rails, connect +5V for the analog supply and 3.3V for the digital supply; powering only 3.3V leaves the converter's analog section unpowered.

The integrated codec and classic ESP32 DAC backends are also attachable when the target MCU and board resources support them. Codec attachments need a named I2C bus, an I2S controller, and all six audio GPIO signals:

expansion attach es8311-es7210 audio0 i2c=i2c0 i2s=i2s0 mclk=gpio38 bck=gpio14 ws=gpio13 din=gpio12 dout=gpio45 pa=gpio46
expansion attach es8311-duplex audio0 i2c=i2c0 i2s=i2s0 mclk=gpio42 bck=gpio10 ws=gpio11 din=gpio12 dout=gpio9 pa=gpio46

es8311-es7210 and es8311-duplex are ESP32-S3 drivers. esp32-dac is for the classic ESP32 internal DAC on GPIO25 or GPIO26. Its optional neg binding enables differential output, and amp plus active=0|1 controls an amplifier enable pin:

expansion attach esp32-dac audio0 pos=gpio26 amp=gpio25 active=1

Only one of these primary audio backends can be attached at a time. A fixed board-default audio0 cannot be detached.

The integrated display controller drivers can also create auxiliary display targets. Use a name other than the reserved primary name display0 for a runtime attachment. Each controller driver supports one attached instance, so its board-integrated display0 and an auxiliary instance cannot coexist:

expansion attach st7305 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5
expansion attach ili9341 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 bl=gpio6 pwm=1 active=1
expansion attach st7796 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 bl=gpio6 pwm=1 active=1
display test lcd0
expansion detach lcd0

st7305, st7796, and ili9341 are available on ESP32 and ESP32-S3 firmware. The I2S-based cvbs-pal and vga32 drivers are available on classic ESP32. Composite PAL uses I2S0 and GPIO25. VGA32 uses I2S1 plus its six RGB and two sync GPIOs; see the expansion reference for its full binding list. A fixed board-default display0 cannot be detached.

Quick reference

solaros.expansion.drivers() lists compiled drivers and devices() lists currently attached devices with normalized bindings. attach(driver, name, bindings) and detach(name) manage them. Never assume an example name such as lcd0 or oled0 exists; inspect devices() or use a name explicitly supplied by the user. Foreground scripts consume attached pointer, axis, and gesture sources through solaros.input.

Hardware and expansion · SolarOS manual

Expansion Ports

SolarOS treats an expansion port as a board-described collection of resources, not as one fixed connector standard. A board may expose individual GPIO pins, named I2C, SPI, UART, or MIDI buses, or free pins that can be routed to an approved spare peripheral host at runtime.

Use expansion layout for the physical connector arrangement, and use expansion status and gpio list on the running device for the authoritative resource view. The layout overlays live pin policy and claims; the available resources depend on the board and the compiled firmware flavor. Boards with multiple named headers can be filtered, for example with expansion layout J1.

Resource Model

TermMeaningOwnership and lifetime
Connector pinA signal physically present on an expansion header or breakout. Physical presence does not make a pin safe for runtime control.Described by the board profile.
Runtime GPIOA connector pin approved for direct gpio and 1-Wire use, and for adc or pwm where the board tables allow it.Claimed while a service or attached device uses it.
Board-defined busA named bus with fixed pins, such as i2c0 or spi0.Registered at boot and cannot be removed. A named UART can still be detached and reattached.
Runtime busA named bus routed onto approved free pins and a spare hardware host.It can be removed when idle. UART controller and pin claims follow attach/detach; other bus signals remain claimed for the descriptor lifetime.
Expansion driverCode that knows how to initialize and operate a supported external device.Listed by expansion drivers; availability is package- and capability-filtered.
Attached deviceA named driver instance bound to buses, addresses, chip-selects, or GPIO roles.Acquires resource leases on attach and releases them on detach.

Board pin policy has three levels:

PolicyDirect GPIORuntime bus routingTypical use
FreeYesYesUncommitted expansion pin.
ReleasableNoYes, after its current service releases it.UART or another default board role.
FixedNoNoBoot straps, flash/PSRAM, display, storage, USB, controls, or other board hardware.

This policy is separate from physical connector membership. For example, a strapping pin may appear on a header and in the physical connector description while remaining blocked from runtime use.

Board Resources

GPIO, ADC, and PWM

BoardPhysical expansion signalsRuntime GPIO and PWMRuntime ADCConnector restrictions
SolarTerm (Waveshare ESP32-S3-RLCD-4.2)GPIO0-GPIO3, GPIO13, GPIO14, GPIO17-GPIO20, GPIO43, GPIO44GPIO1-GPIO3, GPIO17GPIO1-GPIO3, GPIO17GPIO0 is BOOT; GPIO13/GPIO14 are I2C; GPIO18 is KEY; GPIO19/GPIO20 are native USB; GPIO43/GPIO44 belong to uart0 by default.
ESP32-S3 Display 4.0-inch (FNK0104S)GPIO2, GPIO3, GPIO14-GPIO16, GPIO21, GPIO43, GPIO44GPIO2, GPIO3, GPIO14, GPIO21GPIO2, GPIO3, GPIO14GPIO15/GPIO16 are shared I2C; GPIO43/GPIO44 belong to uart0; GPIO4 is fixed audio MCLK, not a connector GPIO.
Elecrow CrowPanel ESP32-S3 4.2-inch E-paperGPIO3, GPIO8, GPIO9, GPIO14-GPIO21, GPIO38GPIO8, GPIO9, GPIO14-GPIO21, GPIO38GPIO8, GPIO9, GPIO14-GPIO20GPIO3 is physically exposed but blocked as a strapping pin.
CL-32GPIO1-GPIO4, GPIO8, GPIO15-GPIO21, GPIO35-GPIO44, GPIO46-GPIO48GPIO4, GPIO8, GPIO15-GPIO18, GPIO21, GPIO38-GPIO42, GPIO47, GPIO48GPIO4, GPIO8, GPIO15-GPIO18GPIO1/GPIO2 are shared I2C; GPIO3 is the shared peripheral interrupt; GPIO19/GPIO20 are USB; GPIO35-GPIO37 are Octal PSRAM; GPIO43/GPIO44 belong to uart0; GPIO46 is an input-only strapping signal. EX1 uses a proprietary M.2 pinout.
T-LoRa-PagerGPIO2, GPIO3, GPIO9, GPIO33-GPIO35, GPIO43, GPIO44, and CE on XL9555 P1.1 (gpiox0:9)GPIO9GPIO9GPIO2/GPIO3 are shared I2C; GPIO33-GPIO35 are shared SPI; GPIO43/GPIO44 belong to releasable uart0; CE is an expanded digital control line, not ESP32 GPIO9.
T-Deck PlusNone; the declared connector view contains internal peripheral wiring onlyNoneNoneThe board has no general-purpose expansion connector. All declared ESP32 pins belong to fixed display, input, storage, audio, radio, GNSS, battery, USB, or bus resources.
ESP32-S3-DevKitC-1-N16R8ESP32-S3 signals broken out on the DevKitC headersGPIO1, GPIO2, GPIO4-GPIO7, GPIO10, GPIO14-GPIO18, GPIO21, GPIO39-GPIO42, GPIO47GPIO1, GPIO2, GPIO4-GPIO7, GPIO10, GPIO14-GPIO18GPIO0 is BOOT/KEY; GPIO3/GPIO45/GPIO46 are other strapping pins; GPIO19/GPIO20 are native USB; GPIO35-GPIO37 are Octal PSRAM; GPIO38/GPIO48 are reserved for either RGB LED revision; GPIO43/GPIO44 are uart0.
ESP32-S3 DevKitC-1 E-paper WorkbenchESP32-S3 signals broken out on the DevKitC headersGPIO6, GPIO7, GPIO14, GPIO18, GPIO21, GPIO39-GPIO42, GPIO47GPIO6, GPIO7, GPIO14, GPIO18GPIO1/GPIO2/GPIO4/GPIO5 belong to the fixed storage SPI bus; GPIO10/GPIO15-GPIO17 belong to the fixed e-paper display; the remaining DevKitC restrictions are unchanged.
Waveshare ESP32-S3-SIM7670G-4G V2.0GPIO0-GPIO21, GPIO39-GPIO46 on the P2/P3 headersGPIO2, GPIO3, GPIO7-GPIO14, GPIO39-GPIO42GPIO2, GPIO3, GPIO7-GPIO14Use CAM OFF. GPIO0 is BOOT/KEY; GPIO1 is the battery ADC divider; GPIO4-GPIO6 are SDMMC; GPIO15/GPIO16 are shared I2C; GPIO17/GPIO18 are the modem UART; GPIO19/GPIO20 are USB; GPIO21 is modem power control; GPIO43/GPIO44 are uart0; GPIO45/GPIO46 are strapping pins.
ODROID-GOExternal IO GPIO4 and GPIO15GPIO4, GPIO15NoneBoth pins are also the allowed external chip-select slots on the shared VSPI bus.
ESP32-WROVER v3.0GPIO0-GPIO5, GPIO12-GPIO15, GPIO18, GPIO19, GPIO21-GPIO23, GPIO25-GPIO27, GPIO32-GPIO36, GPIO39GPIO4, GPIO5, GPIO13, GPIO18, GPIO19, GPIO21-GPIO23, GPIO26, GPIO27, GPIO32-GPIO36, GPIO39; PWM excludes input-only GPIO34-GPIO36 and GPIO39GPIO32-GPIO36, GPIO39GPIO0 is BOOT/KEY; GPIO1/GPIO3 are CH340 uart0; GPIO2/GPIO14/GPIO15 are SDMMC; GPIO25 is PAL; GPIO5 is a strapping pin; GPIO34-GPIO36/GPIO39 are input-only.
TTGO VGA32 v1.4GPIO2, GPIO12-GPIO14, GPIO34, GPIO39GPIO34, GPIO39GPIO34, GPIO39GPIO34/GPIO39 are input-only. GPIO2/GPIO12-GPIO14 belong to the onboard v1.4 microSD interface; GPIO25 audio, VGA, and PS/2 signals are fixed board resources.

Power and ground pins are physical wiring resources and are not managed by the SolarOS pin-claim system. Check the board schematic and the external module's voltage and current requirements before connecting it.

Named and Runtime Buses

BoardBoard-defined busesRuntime-routable busesNotes
SolarTerm (Waveshare ESP32-S3-RLCD-4.2)i2c0: SDA GPIO13, SCL GPIO14; spi0: SCK GPIO11, MOSI GPIO12, CS GPIO40; uart0: TX GPIO43, RX GPIO44I2C on i2c1, SPI on spi3, UART on uart1/uart2, or 1-Wire, using approved free pinsspi0 is the fixed internal display bus; its pins are not expansion pins.
ESP32-S3 Display 4.0-inch (FNK0104S)i2c0: SDA GPIO16, SCL GPIO15; spi0: SCK GPIO12, MOSI GPIO11, CS GPIO10; uart0: TX GPIO43, RX GPIO44I2C on i2c1, SPI on spi3, UART on uart1/uart2, I2S on i2s1, or 1-Wire, using approved free pinsThe I2C connector shares i2c0 with touch and audio control. spi0 is the fixed internal LCD bus.
Elecrow CrowPanel ESP32-S3 4.2-inch E-paperspi0: SCK GPIO12, MOSI GPIO11, CS GPIO45; uart0: TX GPIO43, RX GPIO44I2C on i2c0/i2c1, SPI on spi3, UART on uart1/uart2, or named 1-Wire, using approved free pinsspi0 is the fixed internal SSD1683 bus. SPI3 is shared with microSD and is available for a runtime expansion bus only while the SD card is unmounted.
CL-32i2c0: SDA GPIO1, SCL GPIO2; spi0: SCK GPIO9, MISO GPIO11, MOSI GPIO10, display CS GPIO6, microSD CS GPIO7; uart0: TX GPIO43, RX GPIO44I2C on i2c1, SPI on spi3, UART on uart1/uart2, I2S on i2s1, or 1-Wire, using approved free pinsEX1, CN1, and CN2 expose the shared i2c0; EX1 exposes uart0. The internal core0, ST7305 display, and microSD attach automatically. core0 supplies keyboard0 from the AVR event FIFO and battery0 from its voltage and power-status registers; the display and microSD share spi0 with separate chip selects.
T-LoRa-Pageri2c0: SDA GPIO3, SCL GPIO2; spi0: SCK GPIO35, MISO GPIO33, MOSI GPIO34, CS GPIO38/GPIO21/GPIO36/GPIO39/GPIO9; uart0: TX GPIO43, RX GPIO44; gnss-uart: TX GPIO12, RX GPIO4External devices can share i2c0 or spi0; GPIO9 is the free SPI CS slot. uart0 is releasable.The extension header exposes the shared buses and console UART. Display, storage, radio, and NFC share spi0; GNSS owns its internal UART. There is no complete exposed pin set for another routed bus.
T-Deck Plusi2c0: SDA GPIO18, SCL GPIO8; spi0: SCK GPIO40, MISO GPIO38, MOSI GPIO41, CS GPIO12/GPIO39/GPIO9; uart0: TX GPIO43, RX GPIO44 at 9600 baudNone through an external connectorDisplay, storage, radio, keyboard, touch, audio, microphone, and GNSS use fixed internal resources. The board has no general-purpose expansion connector.
ESP32-S3-DevKitC-1-N16R8i2c0: SDA GPIO8, SCL GPIO9; spi0: SCK GPIO12, MISO GPIO13, MOSI GPIO11, CS GPIO4/GPIO10/GPIO5/GPIO6/GPIO7; uart0: TX GPIO43, RX GPIO44I2C on i2c1, SPI on spi3, UART on uart1/uart2, or 1-Wire, using approved free pinsThe board-defined spi0 is the normal expansion SPI bus.
ESP32-S3 DevKitC-1 E-paper Workbenchi2c0: SDA GPIO8, SCL GPIO9; spi0: SCK GPIO12, MISO GPIO13, MOSI GPIO11, CS GPIO10/GPIO6/GPIO7; spi1: SCK GPIO1, MISO GPIO4, MOSI GPIO2, CS GPIO5; uart0: TX GPIO43, RX GPIO44I2C on i2c1, UART on uart1/uart2, I2S on i2s1, or 1-Wire, using approved free pinskeyboard0, display0, and storage0 attach automatically. Both SPI hosts are assigned to fixed buses.
Waveshare ESP32-S3-SIM7670G-4G V2.0i2c0: SDA GPIO15, SCL GPIO16; uart0: TX GPIO43, RX GPIO44; modem-uart: TX GPIO18, RX GPIO17SPI on spi2/spi3, UART on uart2, I2S on i2s1, or 1-Wire, using approved free pinsstorage0, battery0, pixels0, and modem0 attach automatically. The SIM7670 exclusively owns UART1. Runtime SPI enables attachable packet-radio drivers without treating the cellular modem as the only radio.
ODROID-GOspi0: SCK GPIO18, MISO GPIO19, MOSI GPIO23, CS GPIO5/GPIO15/GPIO4; uart0: TX GPIO1, RX GPIO3UART on uart1/uart2, or named 1-Wire, using approved free pinsVSPI is shared with onboard TFT and SD devices; external devices use their own allowed CS slot.
ESP32-WROVER v3.0uart0: TX GPIO1, RX GPIO3I2C, SPI on spi2/spi3, UART on uart1/uart2, or 1-Wire, using free output-capable GPIO4, GPIO5, GPIO13, GPIO18, GPIO19, GPIO21-GPIO23, GPIO26, GPIO27, GPIO32, or GPIO33The rear SD slot uses the dedicated one-bit SDMMC host. GPIO34-GPIO36 and GPIO39 are available only for input signals and ADC.
TTGO VGA32 v1.4spi0: SCK GPIO14, MISO GPIO2, MOSI GPIO12, CS GPIO13; uart0: TX GPIO1, RX GPIO3; ps2kbd0: clock GPIO33, data GPIO32; ps2mouse0: clock GPIO26, data GPIO27Nonekeyboard0 attaches automatically; attach ps2-mouse to ps2mouse0 only when a mouse is connected. GPIO25 is the fixed mono audio DAC output. I2S1 and the six RGB plus two sync pins are permanently reserved for VGA DMA scanout.

I2C and SPI buses accept shared logical leases. UART, MIDI, and registered 1-Wire bus instances are exclusive. Registered 1-Wire buses appear in expansion status and can be addressed by name. Bus names are unique across protocols.

I2C, SPI, UART, MIDI, and 1-Wire buses can be created at runtime. Runtime hardware buses require an unused board-approved controller or host; all signal pins must be approved by the board's runtime pin policy. Every named UART has an explicit attached state. Attaching reserves its controller and pins; the hardware driver still starts lazily on the first consumer claim and stops after the final claim. Detaching an idle UART releases the controller and pins but preserves its name and configuration. Runtime UART descriptors may additionally be removed; board-defined UART descriptors cannot. The direct numeric form of the onewire command remains available without creating a named expansion bus.

Typical Workflow

Start by inspecting the live resource map and compiled drivers:

expansion layout
expansion status
gpio list
expansion drivers
expansion scan

If the device can use a board-defined bus, attach it directly. The device name is chosen by the user and becomes the lease owner:

expansion attach ssd1306 oled0 i2c=i2c0 addr=0x3c
expansion devices
display test oled0
expansion detach oled0

Runtime I2C and 1-Wire buses use the same lifecycle:

expansion bus create i2c i2c1 port=i2c1 sda=gpio14 scl=gpio15 speed=100000
i2c scan i2c1
expansion bus remove i2c1

expansion bus create onewire onewire0 pin=gpio16
onewire scan onewire0
expansion bus remove onewire0

expansion bus create ps2 ps2kbd clock=gpio17 data=gpio18
expansion attach ps2-keyboard keyboard0 ps2=ps2kbd
input test keyboard0
expansion detach keyboard0
expansion bus remove ps2kbd

expansion bus create uart uart1 port=uart1 tx=gpio14 rx=gpio15 baud=115200
uart status uart1
uart write uart1 AT
expansion bus detach uart1
expansion bus attach uart1
expansion bus remove uart1

expansion bus create midi midi0 tx=gpio1 rx=gpio2
job start midi midi0
midi status
midi monitor
midi note-on 1 60 100
midi note-off 1 60
midi stream add 1 74
control create cutoff midi.cc.1.74 0 127
control bind cutoff parameter synth.filter.cutoff pickup=off
job start controls
job stop midi
expansion bus remove midi0

MIDI is a user-facing bus type with an automatically selected UART backend. Its optional baud= defaults to 31250; there is no port= argument. The resolved uartN appears in status output only to help diagnose controller allocation. A standard DIN connection requires an optoisolated MIDI IN circuit and a current-limited MIDI OUT driver. Never connect DIN MIDI pins directly to ESP32 GPIOs.

An explicitly configured midi.cc.<channel>.<controller> scalar stream retains the latest matching incoming CC value while the MIDI job runs. The controls job can normalize its 0..127 range and bind it to one or more application parameters without consuming messages from MIDI subscribers.

On the Waveshare board, uart0 owns the releasable GPIO43/GPIO44 pair while it is attached. From a display or other non-uart0 shell, detach it before reusing those pins and attach it again after the temporary bus is removed:

expansion bus detach uart0
expansion bus create uart uart1 port=uart1 tx=gpio43 rx=gpio44
expansion bus remove uart1
expansion bus attach uart0

Detaching the port that carries the current shell fails as busy, so the shell cannot disconnect itself accidentally.

On a board with an approved available SPI host, create a bus before attaching the device. Creating the bus claims its controller, SCLK, MOSI, and optional MISO immediately. Each cs= option declares an allowed chip-select pin but leaves it available until a device or one-shot transfer selects it. That user claims both the GPIO and logical chip-select slot, preventing GPIO or SPI users from driving it concurrently:

expansion bus create spi spi1 host=spi3 sclk=gpio1 mosi=gpio2 miso=gpio3 cs=gpio17
expansion attach rfm69 radio0 spi=spi1 cs=gpio17
expansion detach radio0
expansion bus remove spi1

On the Elecrow CrowPanel, run disk umount before creating the runtime SPI3 bus. Remove that bus before using disk mount to make SPI3 available to microSD again.

The spi command addresses board-defined and runtime buses by name. This makes the same transfer tools available for spi0, spi1, or any other registered SPI bus:

spi status
spi status spi1
spi xfer spi1 gpio17 0 1m 0x9f 0 0 0
spi read spi1 gpio17 0 1m 4 0xff
spi write spi1 gpio17 0 1m 0xaa 0x55

The bus name and chip-select are always explicit. Transfers temporarily claim the selected chip-select and lease the bus, so they fail cleanly when an attached device already owns that chip-select.

The i2c command also accepts a named bus. Omitting it retains the i2c0 shortcut used by existing scripts:

i2c status i2c0
i2c scan i2c0
i2c probe i2c0 0x3c
i2c read i2c0 0x50 0x00 8
i2c write i2c0 0x50 0x00 0xaa 0x55

Omit miso or use miso=none for output-only peripherals. A runtime bus can only use a host and pins approved by the board profile. It cannot take fixed display, storage, I2C, USB, or strapping pins. A bus cannot be detached or removed while it has device leases, and board-defined buses can never be removed. expansion bus detach preserves the named descriptor and works for every runtime bus plus board buses whose owned pins are marked releasable. Fixed-pin board buses reject detach.

Drivers and Bindings

Run expansion drivers on the device to see the exact compiled set.

DriverDeviceRequired bindingsResult after attach
manualResource-only profileAny valid bus, address, chip-select, GPIO, ADC, or PWM bindingsClaims resources without initializing hardware.
rfm69HopeRF RFM69W/CW packet radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers a packet-radio target with PA0 power from -18 through 13 dBm.
rfm69hHopeRF RFM69HW/HCW high-power packet radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers a packet-radio target with PA_BOOST power from -2 through 20 dBm.
rfm95HopeRF RFM95W multimode radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers an FSK/GFSK/MSK/GMSK/OOK/LoRa target for the radio command.
sx1262Semtech SX1262 packet radiospi=<bus> cs=<pin> busy=<pin>; optional reset=<pin> irq=<pin>Registers a LoRa/(G)FSK packet-radio target for the radio command.
pcd854484x48 SPI LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>Registers an auxiliary display target.
ssd1683400x300 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<khz> rotation=<0..3> panel=<0..3>Registers an auxiliary target, or Elecrow's fixed display0, with auto, fast, and full refresh modes. Panel 0 auto-detects the Elecrow revision; 1 is legacy Elecrow, 2 is green-sticker Elecrow, and 3 is Waveshare V2.
ssd1306128x64 I2C OLEDi2c=<bus> addr=<address>Registers an auxiliary display target.
sh1106128x64 I2C OLED with SH1106 addressingi2c=<bus> addr=<address>Registers an auxiliary display target with the two-column offset.
st7305400x300 or 384x168 reflective LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>; optional `panel=01 rotation=13`ESP32 and ESP32-S3. Panel 0 is the native 300x400 Waveshare panel; panel 1 is the native 168x384 CL-32 panel. Both boards register their panel as fixed display0.
ili9341320x240 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> active=01 pwm=01`ESP32 and ESP32-S3; ODROID-GO registers it as fixed display0.
st7796480x320 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> active=01 pwm=01`ESP32 and ESP32-S3; Freenove registers it as fixed display0.
st7789320x240 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> power=<pin> active=01 pwm=01`ESP32-S3 driver; T-Deck Plus registers it as fixed display0 with shared peripheral power and GPIO backlight control.
cvbs-pal384x288 or 320x200 monochrome PAL composite outputi2s=i2s0 out=gpio25Classic ESP32 driver; ESP32-WROVER v3.0 registers it as fixed display0.
vga32Build-selected RGB222 VGA outputr0=<pin> r1=<pin> g0=<pin> g1=<pin> b0=<pin> b1=<pin> hsync=<pin> vsync=<pin>Classic ESP32 driver; claims I2S1 and TTGO VGA32 registers it as fixed display0.
cardkbM5Stack Unit CardKBi2c=<bus> addr=0x5fPolls released keys into the shared input service for shells and foreground apps.
tdeck-keyboardLilyGO T-Deck raw-matrix keyboardi2c=<bus> addr=0x55Polls the raw matrix and publishes keys through the shared input service. The built-in T-Deck attachment uses its board keymap; other boards can attach the same controller to a named I2C bus.
tca8418TCA8418 4x10 matrix keyboardi2c=<bus> addr=0x34; optional irq=<pin> backlight=<pwm-pin>Polls the key-event FIFO and publishes keyboard input; the optional PWM binding controls keyboard backlight brightness.
cl32-coreIntegrated CL-32 ATmega808 controlleri2c=<bus> addr=0x08Fixed CL-32-only core0; polls keyboard press/release events into keyboard0 and provides battery0 from the AVR voltage, USB-power, and charging state. It is not runtime-probeable or detachable.
gpio-keysActive-low pull-up buttonsOne or more key:<name>=<gpio> bindingsPublishes press/release keyboard events and releases all GPIO claims on detach.
ps2-keyboardPS/2 scan-code set 2 keyboardps2=<bus>Publishes canonical keyboard press/release events from an exclusive PS/2 bus.
ps2-mouseStandard three-button PS/2 mouseps2=<bus>Enables reporting and publishes relative pointer motion and button events.
analog-joystickTwo-axis analog joystickx=<scalar-stream> y=<scalar-stream> min=<value> center=<value> max=<value>; optional deadzone=<value>Normalizes two scalar streams into X/Y axis events without generating keys.
rotary-encoderQuadrature rotary encodera=<pin> b=<pin>Publishes Up or Down input for each decoded detent.
ft6336Board-integrated FT6336 touch controllerBoard-defined I2C, address, reset, and IRQ bindingsPublishes absolute pointer events as a default board attachment.
gt911GT911 capacitive touch controlleri2c=<bus> addr=0x5d irq=<pin> rotation=<0..3>; optional alt_addr=0x14Polls absolute pointer events into the shared input service. When both addresses are declared, it claims both and probes them in order; otherwise it uses only addr.
mgc3130MGC3130/Skywriter 3D gesture sensor`i2c=<bus> addr=0x420x43 transfer=<pin> reset=<pin> rotation=<0..3>; optional airwheel=01`Registers in the gesture input class and uses the open-drain transfer handshake to publish normalized X/Y/Z axes, recognized gesture/tap/AirWheel events, and buttonless display-scaled absolute hover motion. It is not registered as a touch source, because the five electrode regions do not provide a continuous drawing surface. AirWheel defaults on; set it to 0 to make the mutually exclusive circle gestures available. Use gesture bind to map its generic gesture events to local keys, scripts, or other shell commands.
pcf85063PCF85063 real-time clocki2c=<bus> addr=0x51; optional irq=<pin>Registers an RTC provider with wall-clock, alarm, timer, and optional interrupt support.
shtc3SHTC3 temperature and humidity sensori2c=<bus> addr=0x70Registers temperature and humidity providers.
battery-adcADC battery monitoradc=<pin> divider=<1000..10000>Registers a battery provider using the configured divider ratio in thousandths.
bq27220BQ27220 fuel gaugei2c=<bus> addr=0x55Registers a battery provider backed by the fuel gauge.
max17048MAX17048 fuel gaugei2c=<bus> addr=0x36Registers a battery provider with gauge-reported voltage and state of charge. The chip does not report charger or external-power state, so charging remains unknown unless another provider supplies it.
bq25896BQ25896 battery chargeri2c=<bus> addr=0x6b; optional charge_current=<0..3008> and charge_voltage=<3840..4608>Registers a charger provider; optional values configure milliamps and millivolts within the device's supported steps.
xl9555XL9555 16-line GPIO controlleri2c=<bus> addr=<0x20..0x27>; optional output=<0..65535> direction=<0..65535>Registers named digital control lines such as gpiox0:4; initial output latches are applied before output directions.
ublox-mia-m10qu-blox MIA-M10Q GNSS receiveruart=<bus>; optional power=<gpio-or-controller-line>Registers a GNSS provider and controls its optional power line around the UART-backed receiver.
sim7670SIM7670 LTE modem and GNSS receiveruart=<bus>; optional active-high power=<gpio-or-controller-line> and active-low reset=<gpio-or-controller-line>Registers modem profile, power/reset, AT/status, GNSS, and a generic IPv4 PPP service bound to the exclusively claimed UART. Without a reset line, the driver can reset through the power line. PPP-enabled targets reserve the UART for network frames until disconnected.
st25r3916ST25R3916 NFC-A readerspi=<bus> cs=<pin> irq=<pin>; optional power=<gpio-or-controller-line>Registers an NFC provider for ISO14443A discovery and card reads.
bhi260apBosch BHI260AP six-axis IMUi2c=<bus> addr=<address> irq=<pin>Uploads the sensor firmware during attachment and registers acceleration, angular-rate, and orientation measurements.
drv2605TI DRV2605 haptic controlleri2c=<bus> addr=0x5a; optional power=<gpio-or-controller-line>Registers a haptic provider for the controller's ROM effects.
sdmmcNative SD/MMC card slotclk=<pin> cmd=<pin> d0=<pin>; optional four-bit set d1=<pin> d2=<pin> d3=<pin>Built-in slots register as fixed storage0; runtime attachments mount removable FAT storage at /sdcard. Classic ESP32 uses its native slot-1 pins.
sdspiSPI microSD card adapterspi=<bus> cs=<pin>On boards without built-in SD, mounts removable FAT storage at /sdcard; run disk umount before detach.
neopixelWS2812/NeoPixel GRB stripdata=<pin> count=<1..256>Claims the data GPIO and registers a named strip for the neopixel command and script API.
audio-pwmLEDC PWM mono audio outputpwm=<pin>Claims the PWM GPIO and registers a 16 kHz mono playback device. One instance can be attached.
pcm1808PCM1808 four-wire I2S ADCmclk=<pin> bck=<pin> ws=<pin> dout=<pin>Requires expansion_i2s, claims four GPIOs and a runtime I2S controller, then registers a 16 kHz stereo capture device and stream. One instance can be attached.
es7210ES7210 I2S microphone arrayi2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin>Requires I2C and expansion_i2s, claims its bindings, then registers a 16 kHz stereo capture device and stream with microphone-gain control. One instance can be attached.
pcm5102PCM5102A three-wire I2S DACi2s=<port> bck=<pin> din=<pin> rck=<pin>Requires expansion_i2s, claims the selected runtime I2S controller and three GPIOs, then registers a 16 kHz stereo playback device and stream. One instance can be attached.
i2s-outputGeneric I2S DAC or speaker amplifieri2s=<port> bck=<pin> din=<pin> rck=<pin>Claims the selected runtime I2S controller and registers the same 16 kHz stereo playback device and stream for an integrated or external Philips-I2S receiver without assuming a specific DAC.
es8311-es7210ES8311 playback with ES7210 capturei2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin> dout=<pin> pa=<pin>ESP32-S3 primary audio backend with stereo capture and playback. Waveshare registers it as fixed audio0.
es8311-duplexES8311 duplex codeci2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin> dout=<pin> pa=<pin>ESP32-S3 primary audio backend with mono codec capture and playback. Freenove registers it as fixed audio0.
esp32-dacClassic ESP32 internal DAC`pos=gpio25gpio26; optional neg=gpio25gpio26 amp=<pin> active=01`Registers a primary playback backend. ODROID-GO and TTGO VGA32 provide fixed audio0 attachments.

Manual profiles are useful when another app or workflow operates the hardware but SolarOS still needs to prevent conflicting claims:

expansion attach manual radio0 spi0 cs=gpio10 irq=gpio4 reset=gpio5
expansion attach manual sensor0 i2c0 addr=0x40
expansion detach radio0

Binding names may be explicit (spi=spi0, i2c=i2c0) or, where unambiguous, supplied as positional bus names. ce= aliases cs= and rst= aliases reset= for common module labels.

The name display0 is reserved for the board's primary display attachment. Use another name such as lcd0 for a runtime-attached display. The seven integrated-display drivers attach before display-service initialization when a board profile declares them as fixed defaults. When a controller driver is not already attached, a later attachment registers an auxiliary target for display test, graphical sessions, and applications. Each controller driver supports one attached instance. A fixed board default cannot be detached.

Select the RFM69 driver from the module variant, not from the requested power. The rfm69h driver uses PA1 through 13 dBm, PA1+PA2 through 17 dBm, and applies the datasheet high-power OCP/TestPA settings only during 18-20 dBm transmit. Those settings are restored before standby, receive, or sleep. Both module families require 3.3 V power and a band-appropriate antenna.

WS2812/NeoPixel strip

Use a runtime-safe expansion GPIO for DIN. The driver uses an ESP32 RMT transmit channel and supports up to 256 GRB pixels per attached strip:

5V supply + -> strip 5V       supply GND -> strip GND and SolarOS board GND
GPIO1 -> level shifter -> strip DIN

expansion attach neopixel pixels0 data=gpio1 count=8
neopixel set pixels0 0 32 0 0
neopixel fill pixels0 0 0 16
neopixel clear pixels0
expansion detach pixels0

Use an external supply sized for the strip; full-white WS2812 pixels can draw roughly 60 mA each. Do not power a multi-pixel strip from a board GPIO. A 3.3 V data signal may work with short wiring when the strip supply is low enough, but a 3.3-to-5 V logic-level shifter is the reliable arrangement. Put the usual bulk capacitor across the strip supply and a small series resistor near DIN.

LEDC PWM audio output

Use a runtime-safe PWM pin. The driver updates an 8-bit, 78.125 kHz LEDC carrier from a GPTimer-paced 16 kHz mono PCM stream. It registers the attached name as an audio device and <name>.playback as its stream:

expansion attach audio-pwm pwm0 pwm=gpio1
audio device pwm0
audio default pwm0
aplay /audio/example.mp3

audio default is runtime-only because attached expansion devices are also runtime-only. Run audio default auto to return to the first compatible output. Detaching the selected device also returns selection to auto.

Do not connect a speaker directly to the GPIO. The pin provides a 3.3 V PWM signal centered near 50 percent duty during silence. Use a reconstruction low-pass filter, a DC-blocking/coupling stage, and an amplifier suitable for the speaker impedance. Keep the board and amplifier grounds common. Stop playback before detaching; detach reports busy while the playback stream is open.

PCM5102A I2S audio output

Wire the module's BCK, DIN, and RCK pins to three runtime-safe output GPIOs and connect SCK to ground. The driver is an I2S master in Philips format with 32-bit slots, so its 16-bit stereo stream supplies the 64 BCK cycles per frame needed by the PCM5102A PLL at 16 kHz. The attached name becomes an audio device and <name>.playback becomes an exclusive 16 kHz, signed 16-bit stereo PCM sink:

PCM5102A VCC -> module-rated supply   PCM5102A GND -> SolarOS GND
PCM5102A SCK -> GND                   PCM5102A BCK -> GPIO1
PCM5102A DIN -> GPIO2                 PCM5102A RCK -> GPIO3

expansion attach pcm5102 dac0 i2s=i2s1 bck=gpio1 din=gpio2 rck=gpio3
audio device dac0
audio default dac0
aplay /audio/example.mp3

The example pins are the Waveshare board's runtime-safe expansion GPIOs. Mono streams are duplicated to left and right. Volume is applied in software before samples reach I2S. Select a runtime-safe controller explicitly with the i2s= binding; this leaves other I2S controllers available to onboard audio or composite video. The PCM5102A output is line level: use a powered input or a suitable amplifier, not a passive speaker. Stop playback before detaching; detach reports busy while the playback stream is open. Run audio default auto after testing to restore automatic output selection.

PCM1808 I2S audio input

The driver makes the ESP32 the I2S clock master. Configure the PCM1808 mode pins before power-up: MD1=LOW, MD0=LOW selects slave mode and FMT=LOW selects 24-bit Philips I2S. Wire the driver MCLK signal to the module's SCKI or SCK pin. The PCM1808 mode pins are hardware straps, not runtime driver GPIOs:

PCM1808 +5V -> SolarOS 5V               PCM1808 3.3V -> SolarOS 3.3V
PCM1808 GND -> SolarOS GND
PCM1808 MD1 -> GND                      PCM1808 MD0 -> GND
PCM1808 FMT -> GND                      PCM1808 SCKI/SCK -> GPIO1
PCM1808 BCK -> GPIO2                    PCM1808 LRCK -> GPIO3
PCM1808 DOUT -> GPIO17

expansion attach pcm1808 adc0 mclk=gpio1 bck=gpio2 ws=gpio3 dout=gpio17
audio device adc0
arecord -d 5 -i adc0.capture /sdcard/pcm1808.wav

The example uses all four runtime-safe Waveshare expansion GPIOs. The driver generates 4.096 MHz MCLK and 64 BCK cycles per 16 kHz stereo frame. It receives the converter's 24-bit I2S slots, keeps the most-significant 16 bits, and publishes adc0.capture as an exclusive signed 16-bit stereo PCM source. It uses the board's runtime I2S controller (I2S1 on current ESP32-S3 profiles), so it can coexist with fixed onboard audio on I2S0 but not with another attachment that owns I2S1. Stop recording before detach. A raw PCM1808 circuit requires the analog supply, digital supply, reference components, and input network from the manufacturer reference design; follow the breakout's own supply markings when using a module. Common modules expose separate +5V analog and 3.3V digital supply pins; both rails must be powered.

Wiring Examples

PCD8544 on ESP32-S3-DevKitC-1

VCC -> 3V3        GND -> GND
CLK/SCLK -> GPIO12
DIN/MOSI -> GPIO11
CE/CS -> GPIO10   DC -> GPIO4   RST -> GPIO5

expansion attach pcd8544 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5
display test lcd0

Wire a module backlight according to the module board and use suitable current limiting when connecting it to 3V3.

Waveshare 4.2-inch V2 e-paper on ESP32-S3-DevKitC-1

This driver is for the monochrome 400x300 V2 module with the UC8176-compatible controller path, including driver-board revision 2.2. It is not the red/black/white (B) module.

VCC -> 3V3        GND -> GND
CLK -> GPIO12     DIN -> GPIO11
CS -> GPIO10      DC -> GPIO17
RST -> GPIO16     BUSY -> GPIO15

expansion attach ssd1683 epd0 spi=spi0 cs=gpio10 dc=gpio17 reset=gpio16 busy=gpio15
display test epd0
display mode epd0
display mode epd0 refresh=full
display mode epd0 refresh=fast
expansion detach epd0

BUSY is active high. refresh=auto is the default: it uses a full waveform for the first changed frame, then refreshes only the framebuffer rectangle that changed. After 19 partial updates it reinitializes the controller and performs a full cleanup refresh. After each partial waveform, the driver synchronizes the controller's current and previous RAM planes before accepting the next frame. Unchanged frames are skipped. refresh=fast remains a fast full-frame update. Detach sends the controller to deep sleep before it releases the SPI and GPIO resources. E-paper is bistable, so the last image remains visible. Keep VCC and ESP32 logic at 3.3 V even though recent Waveshare driver boards can also operate in a 5 V logic domain. If expansion detach epd0 reports that the device is busy, run sessions and close the display session that owns epd0 with session close <id> first.

M5Stack Unit CardKB on ESP32-S3-DevKitC-1

CardKB uses a fixed I2C address of 0x5f. Connect SDA and SCL to the pins of the named I2C bus; the DevKit i2c0 board definition supplies the exact pin numbers shown by expansion buses.

VCC -> 5V         GND -> GND
SDA -> I2C0 SDA   SCL -> I2C0 SCL

expansion attach cardkb cardkb0 i2c=i2c0 addr=0x5f
expansion devices
expansion detach cardkb0

Each I2C read returns one key value, or zero when no key is pending. Printable characters, Enter, Escape, Tab, Backspace, Delete, and the four arrows feed the shared SolarOS input path. CardKB reports one value after release, so host-side key repeat is not available. Its values 128 through 175 are private Fn combinations and are ignored instead of being confused with SolarOS logical keys.

RFM95W on ESP32-S3-DevKitC-1

The RFM95W is a 3.3 V device. Connect an antenna suitable for the module band before transmitting.

VCC -> 3V3        GND -> GND
SCK -> GPIO12     MISO -> GPIO13
MOSI -> GPIO11    NSS/CS -> GPIO4
RESET -> GPIO5

expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5
radio status radio0

The module and driver support FSK, GFSK, MSK, GMSK, OOK, and LoRa. The default LoRa profile is 868 MHz, 125 kHz bandwidth, SF7, coding rate 4/5, CRC enabled, explicit headers, sync word 0x12, and 13 dBm transmit power. The matching built-in profile applies those settings as one operation:

radio profile apply radio0 lora-eu868
radio send radio0 "hello from SolarOS"
radio recv radio0 5000

The built-in meshcore-eu868 profile is specifically for MeshCore companion operation in the EU868 region: 869.618 MHz, 62.5 kHz, SF8, coding rate 4/8, a 32-symbol preamble, private sync word 0x12, CRC, variable length, and 14 dBm. MeshCore always requires an explicit profile:

job start meshcore radio0 meshcore-eu868

gfsk-eu868 and ook-eu868 are also built in. Change both ends of a link to the same profile before exchanging packets. A custom set of settings can be captured in one of eight persistent NVS user profiles:

radio profile apply radio0 gfsk-eu868
radio config radio0 bitrate 9600
radio profile save radio0 gfsk-9600
radio profile show gfsk-9600

Applying a profile leaves the radio in standby and rolls back the complete configuration and prior state if the driver rejects it. User profiles preserve every common radio setting, including addressing. Built-in profiles are read-only. Selecting MSK or GMSK sets the deviation to one quarter of the bitrate, giving the required modulation index of 0.5. GFSK and GMSK enable Gaussian shaping with BT=1.0.

FSK-family and OOK packet payloads are limited to 64 bytes by the modem FIFO; LoRa payloads may contain up to 255 bytes. Fixed length zero selects the FSK/OOK unlimited FIFO-stream mode used by services such as POCSAG.

The driver polls the radio status registers, so DIO0/IRQ is optional. An IRQ binding can still be reserved for future interrupt-driven operation.

SSD1306 or SH1106 on SolarTerm

VCC -> 3V3        GND -> GND
SDA -> GPIO13     SCL -> GPIO14

i2c scan i2c0
expansion attach ssd1306 oled0 i2c=i2c0 addr=0x3c
display test oled0

Common modules answer at 0x3c or 0x3d. If the image is shifted two pixels left with two uninitialized columns on the right, reattach it as SH1106:

expansion detach oled0
expansion attach sh1106 oled0 i2c=i2c0 addr=0x3c
display test oled0

After an auxiliary display is attached, it can also host a shell session:

session create shell oled0

Quick reference

Inspect runtime-safe pins and buses before attaching hardware. Use the io application or the gpio, adc, pwm, i2c, spi, uart, bus, and expansion commands as documented here. Resource ownership prevents two drivers, jobs, or sessions from claiming the same hardware concurrently.

Hardware and expansion · SolarOS manual

GPIO, ADC, PWM, and LED APIs

SolarOS exposes only runtime-safe pins for the active board. Never copy a GPIO number from another ESP32 board or assume that a header pin is unclaimed.

Discover first

import solaros

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

Check allowed(pin) before configuring a pin. Release a claimed pin when the script is done so another service can use it.

Digital output

pin = 1
if solaros.gpio.allowed(pin):
    solaros.gpio.mode(pin, solaros.gpio.OUTPUT)
    solaros.gpio.write(pin, 1)
    solaros.gpio.release(pin)

Use the documented mode and pull constants. Do not replace them with guessed strings or integers.

Quick reference

Inspect solaros.gpio.pins() and allowed(pin) before use; never invent safe GPIO numbers. GPIO offers mode or configure, read, write, and release plus INPUT, OUTPUT and pull constants. solaros.led offers status, set, on, off, toggle. solaros.adc offers pins and read. solaros.pwm offers status, set(pin, frequency, duty_percent), and off. APIs are package-gated.

Hardware and expansion · SolarOS manual

Time, battery, and environment APIs

SolarOS distinguishes uptime, UTC, and configured local time. Sensor and battery values exist only when the board and firmware provide their services. solaros.rtc exposes optional alarm/timer hardware; solaros.schedule provides portable alarms and recurring actions whether or not that hardware exists.

Check time integrity

import solaros

print(solaros.time.uptime())
if solaros.time.is_valid():
    print(solaros.time.datetime())

Do not label data with wall-clock timestamps until the RTC or NTP-derived time is valid. Use uptime for monotonic intervals.

Read installed sensors

Inspect solaros.battery.status() and solaros.sensors.list() rather than assuming a fixed voltage, temperature, or humidity source. Read the default or a named provider with solaros.sensors.environment(), temperature(), or humidity(). On CL-32, battery0 reads the voltage measured by the integrated AVR in 25 mV steps and reports its USB-power and charging states directly. On the Waveshare ESP32-S3-SIM7670G-4G target, the MAX17048 reports voltage and state of charge but has no charger-status input. Charging therefore remains unknown. The battery service can still estimate Power from configured voltage thresholds and monitor trends; that estimate is not direct charger telemetry.

Quick reference

solaros.time provides uptime_ms, uptime, datetime, utc_datetime, set_datetime, set_utc_datetime, utc_to_local, local_to_utc, is_valid, timezone, set_timezone, and ntp_sync. solaros.battery.status and the solaros.sensors functions are package-gated.

set_timezone() accepts conventional fixed UTC offsets such as UTC-8 and UTC+5:30. Fixed offsets do not apply daylight-saving transitions. Other accepted timezone expressions use POSIX TZ syntax and its POSIX sign convention. SolarOS does not include the IANA timezone database; Europe/Berlin is a built-in daylight-saving alias.

Scripting APIs · SolarOS manual

Compatibility I/O modules

The compatibility modules expose the board's traditional single I2C, SPI, UART, and OneWire services. They are useful for simple scripts and existing code. New applications that need multiple or dynamically attached buses should prefer solaros.buses.

Safe workflow

  1. Inspect status() or allowed(pin).
  2. Use a board-supported bus, pin, chip select, address, mode, and speed.
  3. Check transfer results.
  4. Leave the service in a known state when the script exits.

For external hardware, man buses and man expansion describe the resource-owned path.

Quick reference

Compatibility modules are solaros.i2c info, probe, scan, read_reg, write_reg; solaros.spi status, xfer, read, write; solaros.uart status, baud, is_valid_baud, mode, write, read; solaros.onewire allowed, reset, scan, xfer. Inspect status or allowed first. New multi-bus code should prefer solaros.buses. Modules are package-gated.

Scripting APIs · SolarOS manual

Digital signal processing

service.dsp is a synchronous library service for signed 16-bit signal processing. Callers own FIR, decimator, and FFT contexts. The service does not run a worker task and does not retain global stream state.

The public operation selects its backend. Applications must not select SIMD directly. ESP32-S3 builds report esp32s3-pie and can use ESP-DSP PIE routines for eligible gain, window, and signed 16-bit FFT blocks. Other operations, small blocks, unaligned blocks, unavailable runtime work memory, and other targets use the portable implementation. service.engines records the operation under the dsp owner and reports cpu or simd use.

Numeric contract

  • Samples, gains, windows, and FIR coefficients are signed 16-bit values.
  • Gains, windows, and coefficients use Q15. 32767 is the largest positive
  • value and -32768 represents -1.0.

  • Products and filter sums use wide accumulators. Scaling divides by 32768,
  • rounds toward negative infinity, and saturates to the signed 16-bit range.

  • Portable and accelerated paths have the same output contract.
  • Exact in-place stateless operations are supported where the input and output
  • element counts match. Partial buffer overlap is rejected.

  • level returns unsigned peak magnitude and integer RMS magnitude.
  • FFT input is real signed 16-bit data. Output is interleaved signed 16-bit
  • (real, imaginary) data. Every radix-2 stage divides by two, so the returned exponent is log2(size) and the mathematical spectrum equals the returned values multiplied by 2 ** exponent, subject to fixed-point quantization.

FIR coefficient zero multiplies the newest sample. A decimator filters every input sample and emits the filtered value at phase zero. Its history and phase continue across calls until reset.

Native API

Include solar_os_dsp.h. Stateless operations are:

solar_os_dsp_dot_s16(a, b, count, &dot);
solar_os_dsp_gain_q15(output, input, count, gain_q15);
solar_os_dsp_mix_q15(output, a, b, count, gain_a_q15, gain_b_q15);
solar_os_dsp_clip_s16(output, input, count, minimum, maximum);
solar_os_dsp_level_s16(input, count, &level);
solar_os_dsp_window_q15(output, input, window_q15, count);

Streaming processors use caller-owned opaque contexts:

solar_os_dsp_fir_create(coefficients, taps, &fir);
solar_os_dsp_fir_process(fir, output, input, count);
solar_os_dsp_fir_reset(fir);
solar_os_dsp_fir_destroy(fir);

solar_os_dsp_decimator_create(coefficients, taps, factor, &decimator);
solar_os_dsp_decimator_process(decimator, output, capacity,
                               input, count, &produced);
solar_os_dsp_decimator_destroy(decimator);

solar_os_dsp_fft_create(size, &fft);
solar_os_dsp_fft_execute(fft, spectrum, input, &scale_exponent);
solar_os_dsp_fft_destroy(fft);

FIR filters support 1 through 1024 taps. FFT sizes must be powers of two from 2 through 4096. One context must not be used concurrently by multiple callers.

Use solar_os_dsp_backend(), solar_os_dsp_capabilities(), and solar_os_dsp_accelerated_capabilities() for diagnostics. Capability bits are:

BitOperation
0signed 16-bit dot product
1Q15 gain
2Q15 mix
3signed 16-bit clip
4signed 16-bit level
5Q15 window
6Q15 FIR
7Q15 decimator
8signed 16-bit FFT, PIE accelerated on eligible ESP32-S3 blocks

Python

Python accepts contiguous buffer objects containing native little-endian signed 16-bit samples. array('h') and bytearray objects are suitable. Stateless output functions return a new bytearray:

from array import array
import solaros

dsp = solaros.dsp
samples = array('h', [1000, -1000, 500, -500])

scaled = dsp.gain(samples, 0.5)
mixed = dsp.mix(samples, samples, 0.7, 0.3)
clipped = dsp.clip(samples, -800, 800)
peak, rms = dsp.level(samples)
energy = dsp.dot(samples, samples)

window(samples, coefficients) returns a new sample buffer. Both inputs must contain the same number of samples. Gains use floating-point values from -1.0 through 1.0 and are converted to Q15.

Stateful processors own their native context and release it during close() or garbage collection:

coefficients = array('h', [16384, 16384])
filt = dsp.fir(coefficients)
filtered = filt.process(samples)

downsample = dsp.fir(coefficients, decimation=4)
low_rate = downsample.process(samples)
downsample.reset()

fft = dsp.fft(1024)
spectrum, exponent = fft.execute(array('h', [0] * 1024))
fft.close()

The script bridge accepts at most 32768 samples per call.

Lua

Lua uses binary strings containing native little-endian signed 16-bit values. Stateless functions return a new binary string. FIR and FFT constructors return userdata that own the native context:

local dsp = solaros.dsp
local scaled = dsp.gain(samples, 0.5)
local peak, rms = dsp.level(samples)

local filt = dsp.fir(coefficients, 4)
local low_rate = filt:process(samples)
filt:reset()
filt:close()

local fft = dsp.fft(1024)
local spectrum, exponent = fft:execute(samples_1024)

Lua strings are immutable, so each output operation allocates a new string. The script bridge accepts at most 32768 samples per call.

Quick reference

Use service.dsp for synchronous fixed-point block processing. The service selects portable or ESP32-S3 PIE code without application-side board checks. Callers own streaming contexts and must destroy or close them. Python uses signed 16-bit buffer objects; Lua uses binary strings. The native Synth service uses level for its captured PCM scope blocks and publishes the resulting peak and RMS values through Synth status.

service.signal-widgets builds reusable oscilloscope and spectrum views on top of solar_os_gfx and this DSP API. Its submit calls copy recent mono or interleaved signed-16-bit PCM into thread-safe snapshots. Rendering the spectrum applies a Hann window and a 512-point FFT; the DSP service selects PIE SIMD on ESP32-S3 without widget-side board checks.

Scripting APIs · SolarOS manual

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.

API topics

Open a topic below, or use its ID with man on the device, for example man lua.network. Service availability depends on the board and flavor.

TopicServices
Storage and filessolaros.storage
Time and schedulingsolaros.time, solaros.rtc, solaros.schedule
Networkingsolaros.wifi, solaros.mqtt, solaros.http, solaros.net, solaros.ftp, solaros.sftpsync, solaros.ssh_keys
Bluetoothsolaros.ble
GPIO and peripheralssolaros.gpio, solaros.onewire, solaros.led, solaros.adc, solaros.pwm, solaros.i2c, solaros.spi, solaros.uart, solaros.neopixel, solaros.battery, solaros.sensors, solaros.gnss, solaros.nfc
Buses and expansionsolaros.buses, solaros.expansion
Audio and controlsolaros.audio, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc
Input and clipboardsolaros.input, solaros.hid, solaros.clipboard
Apps, jobs, and identitysolaros.identity, solaros.jobs, solaros.sessions, solaros.apps
Contacts and messagessolaros.contacts, solaros.messages
Text user interfacessolaros.tui
Graphicssolaros.gfx

Top-Level Helpers

  • solaros.write(text): write to the foreground terminal.
  • solaros.version(): return the firmware version.
  • solaros.should_exit(): return whether the foreground app was asked to exit.
  • solaros.tick_interval([ms]): get or set the foreground event-pump interval in milliseconds. Pass 0 to restore the 25 ms default.
  • solaros.battery_status(): short battery status table or nil when battery support is compiled.
  • solaros.wifi_status(): short Wi-Fi status table when Wi-Fi support is compiled.
  • solaros.environment(): temperature and humidity table or nil when environmental sensor support is compiled.

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 availability

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.

Conventions

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, input, TUI, and graphics. Foreground pointer and axis events use solaros.input sources, read, clear, and status; keyboard characters use solaros.tui.getch(). Lua arrays are 1-based unless an individual service explicitly exposes a native index. Close resources and keep long-running loops cooperative.

Scripting APIs · SolarOS manual

Lua apps, jobs, and identity API

API overview · Python apps, jobs, and identity

solaros.identity

  • solaros.identity: user, hostname, set_user, set_hostname, format

solaros.jobs

  • solaros.jobs: list, count, status, start, stop

solaros.sessions

  • solaros.sessions: create_shell, close

solaros.apps

  • solaros.apps: list, find

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.

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.

  • create_shell(port[, term[, cols, rows[, charset]]]): create a port shell and return its numeric session id.
  • create_shell(port, {term="auto", cols=80, rows=24, charset="utf8"}): table-options form for the same call. Use charset="ascii" for legacy terminals.
  • close(session_id): close a display/app session or stop a port shell session;
  • closing the final interactive shell is refused.

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

Quick reference

Use solaros.identity, solaros.jobs, solaros.sessions, solaros.apps for apps, jobs, and identity. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua audio and control API

API overview · Python audio and control

solaros.controls

  • solaros.controls: list, get, set, create, delete, clear, bindings, bind_parameter, bind_midi, unbind when continuous controls are compiled. Values use the normalized range 0.0..1.0.

solaros.parameters

  • solaros.parameters: list, get, set for dynamic native application parameters when continuous controls are compiled.

solaros.midi

  • solaros.midi: status, send, note_on, note_off, cc, program, read/receive, close, streams, stream_add, stream_remove, stream_clear when MIDI support is compiled.

solaros.osc

  • solaros.osc: bindings, bind_stream, bind_event, bind_control, unbind, clear, encode_float, encode_int, dispatch, limits when OSC support is compiled.

solaros.dsp

  • solaros.dsp: backend, capabilities, dot, gain, mix, clip, level, window, fir, and fft when service.dsp is compiled. Binary strings contain native little-endian signed 16-bit values; FIR and FFT constructors return caller-owned userdata.

solaros.audio

  • solaros.audio: status, deinit, off, set_volume, set_mic_gain, tone, tone_async, cancel, queue_status, level, capture, loopback, wav_info, record_wav, play_wav when audio support is compiled. capture(frames) accepts 1 through 4096 frames and returns an interleaved little-endian signed-16 binary string plus a format table with sample_format, sample_rate, channels, and bits_per_sample.

solaros.synth

  • solaros.synth: status, configure, configure_oscillator2, configure_filter, configure_performance, note_on, note_off, all_notes_off, stop when synth support is compiled. It provides eight native two-oscillator voices with polyphonic or monophonic last-note playback, portamento, per-note velocity, ADSR envelopes, and resonant low-pass filters; scripts retain the system's global speaker volume. Status includes DSP-derived pcm_peak and pcm_rms values for the captured scope block.

MIDI

The MIDI job must own a running bus before Lua transmits or receives. The following setup creates a bus, starts the worker, sends a note, and waits up to one second for a non-consuming subscriber message:

solaros.buses.create_midi("midi0", { tx = 2, rx = 3 })
solaros.jobs.start("midi", { "midi0" })
solaros.midi.note_on(1, 60, 100)
local message = solaros.midi.read(1000)

status() includes traffic, parser, drop, error, CC-stream, and script subscription state. send(status[, data1, data2]) validates raw messages; note_on, note_off, cc, and program provide channel-oriented helpers. read() and its receive() alias return a message table or nil, are bounded to 60 seconds, and are cancellation-aware. The subscription is automatically released when Lua exits; close() releases it earlier.

Use streams(), stream_add(channel, controller), stream_remove(...), and stream_clear() to manage incoming CC scalar streams. Message tables contain status, length, type, and applicable channel/data fields.

Open Sound Control

Lua configures native OSC bindings while the osc job retains UDP socket, filtering, and rate-limit ownership:

solaros.osc.bind_stream(
    "ambient", "temperature", "/room/temperature", 2.0, 0.1
)
solaros.jobs.start(
    "osc", { "listen=9000", "target=192.168.1.50:9001" }
)

bindings() returns source configuration plus availability, values, timing, send counters, and errors. bind_stream, bind_event, and bind_control return numeric IDs; unbind and clear remove definitions. Event edges are "rising", "falling", or "both"; rates are 0.1..100 Hz.

encode_float() and encode_int() return binary OSC messages that can be sent with solaros.net.udp_send(). dispatch(packet) validates a message or immediate bundle and applies the same native parameter routes as the job. limits() reports all public codec and binding bounds.

Synthesizer example

For example, this plays a short saw-wave chord without running Lua in the real-time render callback:

solaros.synth.configure("saw", 5, 80, 65, 140)
solaros.synth.configure_oscillator2("square", 0, 7, 35)
solaros.synth.configure_filter(1200, 35, 80, 5, 250, 20, 180)
solaros.synth.configure_performance(true, 80)
solaros.synth.note_on(440, 110)
solaros.synth.note_on(554, 90)
solaros.time.sleep_ms(250)
solaros.synth.all_notes_off()
solaros.time.sleep_ms(150)
solaros.synth.stop()

note_on() accepts 20 through 8000 Hz and velocity 1 through 127. Envelope times accept 0 through 10000 ms and sustain accepts 0 through 100 percent. The configure() updates active voices immediately and also sets the defaults for future notes. configure_filter() accepts cutoff from 40 through 18000 Hz, resonance and envelope amount from 0 through 100 percent, followed by its own attack, decay, sustain, and release values. The first note claims exclusive audio output lazily, and the runtime releases it automatically when the script exits or is interrupted. configure_oscillator2() accepts waveform, octave from -2 through +2, fine detune from -100 through +100 cents, and mix from 0 through 100 percent. Mix zero bypasses oscillator 2 exactly. Both oscillators share the filter and envelopes. configure_performance() selects polyphonic or monophonic last-note playback and accepts a glide time from 0 through 2500 ms.

DSP values

For the Q15 numeric contract, processor lifetime, limits, and examples, see Digital signal processing. Lua DSP operations return new binary strings because ordinary Lua strings are immutable.

Quick reference

Use solaros.audio, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc for audio and control. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua bluetooth API

API overview · Python bluetooth

solaros.ble

  • solaros.ble: keyboard status (including the latest reported keyboard
  • battery percentage when available), connected, pair, forget, layout, read; generic client functions under solaros.ble.gatt, application peripheral functions under solaros.ble.server, and typed peripheral HID under solaros.ble.hid when BLE support is compiled

BLE scan

solaros.ble.scan() returns a one-based array of device tables with the same fields and blocking behavior as Python scan. It includes non-keyboard devices, returns an empty table if none are found, and raises a Lua error on scan failure. Call it before opening GATT connections.

for _, device in ipairs(solaros.ble.scan()) do
    print(device.address, device.addr_type, device.name, device.rssi)
end

Generic BLE GATT client

solaros.ble.gatt mirrors the Python GATT client: capacity(), connect(address, addr_type, timeout_ms), disconnect(peer), status(peer), services(peer), characteristics(peer, service_index), read(peer, handle, timeout_ms), and write(peer, handle, data, with_response, timeout_ms), subscribe(peer, handle, indicate, timeout_ms), unsubscribe(peer, handle, timeout_ms), configure_queue(peer, capacity), and poll(peer). Connect returns an opaque peer handle; capacity reports the total configured generic-peer budget, not currently free slots. Optional trailing arguments may be omitted or nil; defaults are public address type, service-default timeout, and writes with response. Use dot calls, not colon method syntax.

Lua data uses binary strings, including embedded zero bytes. Discovery results are one-based Lua arrays, but each service's index field is zero-based; pass that field to characteristics(). Status and discovery fields, service limits, timeouts, retirement, and reconnect rules match Python. Errors raise Lua errors; cancellation reports BLE operation cancelled.

Subscribe discovers the CCCD and waits for its write acknowledgement. indicate defaults to false (notifications); use true for indications. poll(peer) is nonblocking and returns nil or {handle=..., data=..., indication=...}; data is a binary Lua string. No interpreter callback runs on the Bluetooth task. The per-peer queue defaults to 16 entries on first subscribe. Configure a positive capacity while connected, idle and with an empty queue. Failed allocation leaves the old queue intact. Full queues drop new events; values over 128 bytes are dropped whole. Status reports event_capacity, event_count, and the saturating events_dropped counter. Indication confirmation acknowledges protocol receipt, not app consumption. Disconnect, timeout, cancellation and sleep discard queued events; reconnect and subscribe again after resume. Successful unsubscribe discards queued events only for its characteristic. Other peers are unaffected.

Lua owns a separate session, automatically closed on interpreter exit, errors, or stop, closing all its peers. Multiple peers can coexist within the configured host/controller capacity, with one connection reserved for the keyboard. Connect peers sequentially; their subsequent operations are independent. Capacity exhaustion reports BLE connection capacity exhausted, and allocation can fail without disturbing existing peers. A caught error or completion of one REPL command does not close the interpreter's session. Use disconnect(peer) when finished with a peer.

Native BLE HID peripheral

solaros.ble.hid mirrors the Python BLE HID API. start(name) publishes a fixed encrypted and bonded composite keyboard, mouse, and gamepad service for one host. pair() explicitly forgets the next connecting peer's old HID bond before security; call it only for a user-requested new-host pairing flow. status() returns connection, security, subscription, keyboard LED, and queue state. poll() returns nil or a connected, secured, disconnected, keyboard-leds, or passkey event. Render a passkey as six digits and enter it on the remote host.

local hid = solaros.ble.hid
hid.start("SolarOS Controls")
hid.pair()

while not solaros.should_exit() do
    local event = hid.poll()
    if event then
        print(event.type, event.status)
        if event.type == "passkey" then
            print(string.format("Enter %06d on the remote host", event.passkey))
        end
    end
    if hid.status().gamepad_subscribed then
        hid.gamepad.axis(hid.AXIS_X, -12000)
        hid.gamepad.button(1, true)
        hid.gamepad.hat(hid.HAT_UP)
        hid.gamepad.send()
        break
    end
end

Keyboard calls are keyboard.press, keyboard.release, and keyboard.release_all. Mouse calls are mouse.move and mouse.button. Gamepad setters accept axes in -32768..32767, buttons 1..32, and HAT_* directions, then gamepad.send() publishes the combined state. Input operations require the corresponding encrypted host subscription. The event queue holds 16 entries; inspect events_dropped. stop() neutralizes reports and retires the service. Runtime teardown does the same automatically.

The native HID service shares the single application-peripheral lease with solaros.ble.server; the two cannot be active at once. It is separate from the dormant USB-only solaros.hid package. Scripts select behavior through typed operations and constants; report descriptors and raw report bytes stay native.

solaros.ble.server mirrors the application peripheral API: create, service, characteristic, start, stop, close, status, peers, set, send, disconnect, and poll. Values and event data use binary Lua strings; poll() returns nil when empty. Properties combine solaros.ble.READ, WRITE, WRITE_NO_RESPONSE, NOTIFY, and INDICATE. The server belongs to this runtime and is cleaned up alongside its outgoing peers on interpreter exit.

local gatt = solaros.ble.gatt
local peer = gatt.connect("aa:bb:cc:dd:ee:ff", 1) -- replace address and type
for _, service in ipairs(gatt.services(peer)) do
    print(service.uuid, service.index)
    for _, characteristic in ipairs(gatt.characteristics(peer, service.index)) do
        print(characteristic.uuid, characteristic.handle)
    end
end
-- Use a discovered handle: local data = gatt.read(peer, handle)
-- Binary write: gatt.write(peer, handle, string.char(0, 255), true)
gatt.disconnect(peer)

For an already connected peer and a discovered notification-capable handle:

gatt.configure_queue(peer, 32)
gatt.subscribe(peer, handle) -- third argument true selects indications
local event = gatt.poll(peer) -- call regularly from the application's loop
if event then print(event.handle, event.data, event.indication) end
print(gatt.status(peer).events_dropped)
-- When finished: gatt.unsubscribe(peer, handle)

Quick reference

Use solaros.ble for bluetooth. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua buses and expansion API

API overview · Python buses and expansion

solaros.buses

  • solaros.buses: constants MODE0 through MODE3, SPI2_HOST, SPI3_HOST, DEFAULT_SPEED, MAX_SPEED; functions list, get, create_spi, attach, detach, remove, spi_xfer, spi_read, spi_write when the resource service is compiled; create_i2c, i2c_probe, i2c_scan, i2c_read_reg, and i2c_write_reg are additionally present when I2C support is compiled; create_onewire, onewire_reset, onewire_scan, and onewire_xfer are additionally present when OneWire support is compiled; create_ps2 is present with PS/2 support; create_uart, create_midi, uart_write, and uart_read are additionally present when UART support is compiled

solaros.expansion

  • solaros.expansion: drivers, devices, attach, detach when the expansion service is compiled

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.

  • list() returns every bus table.
  • get(name) returns one bus table.
  • create_i2c(name, config) creates a runtime I2C bus and returns its table.
  • create_onewire(name, config) creates a runtime 1-Wire bus and returns its table.
  • create_spi(name, config) creates a runtime SPI bus and returns its table.
  • create_uart(name, config) creates a lazy runtime UART bus and returns its table.
  • create_midi(name, config) creates an exclusive MIDI bus and automatically selects its UART backend.
  • attach(name) attaches a named detachable bus and reserves its endpoint and pins.
  • detach(name) detaches an idle named bus without deleting its descriptor.
  • remove(name) removes an idle runtime bus.
  • i2c_probe(bus, address), i2c_scan(bus),
  • i2c_read_reg(bus, address, reg, length), and i2c_write_reg(bus, address, reg, data) operate on a selected named I2C bus when both the resource and I2C services are compiled.

  • onewire_reset(bus), onewire_scan(bus), and
  • onewire_xfer(bus, read_len[, data]) operate on a selected registered OneWire bus when both the resource and OneWire services are compiled.

  • uart_write(bus, data) and uart_read(bus[, length[, timeout_ms]]) operate
  • on a selected named UART when both the resource and UART services are compiled.

  • spi_xfer(bus, cs, data[, mode[, speed_hz]]),
  • spi_read(bus, cs, length[, fill[, mode[, speed_hz]]]), and spi_write(bus, cs, data[, mode[, speed_hz]]) transfer on a selected named bus. Each raw transfer takes and releases a temporary lease automatically.

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.

create_midi requires tx and rx; optional baud_rate defaults to 31250. SolarOS selects an unused board-approved UART controller. The returned port is diagnostic backend information, not an input to the MIDI API.

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 with their categories. devices() lists active devices with name, driver, origin (board or runtime), ready, autostart, detachable, and normalized bindings. Each binding contains kind, role, target, value, and aux. attach(driver, name, bindings) and detach(name) mirror the shell lifecycle. Binding tables accept spi, cs (or ce), i2c, addr, alt_addr, uart, ps2, gpio, irq, reset (or rst), dc, busy, data, bck, din, rck, mclk, ws, dout, adc, pwm, count, keys, x, y, min, center, max, and deadzone. ps2 names an existing PS/2 bus; x and y name scalar streams; keys maps logical key names to GPIO numbers. cs requires spi; addr and alt_addr require i2c; 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_*.

Quick reference

Use solaros.buses, solaros.expansion for buses and expansion. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua contacts and messages API

API overview · Python contacts and messages

solaros.contacts

  • solaros.contacts: list, get when provider-neutral messaging is compiled

solaros.messages

  • solaros.messages: conversations, list, send, mark_read, cancel when provider-neutral messaging is compiled

Sending messages

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.

Quick reference

Use solaros.contacts, solaros.messages for contacts and messages. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua gpio and peripherals API

API overview · Python gpio and peripherals

solaros.battery

  • solaros.battery: status with voltage, percentage, external-power,
  • charging, and charging_known fields when battery support is compiled

solaros.sensors

  • list(): return registered providers with name, driver, temperature,
  • and humidity fields.

  • environment([name]): return temperature and humidity from one named
  • provider. Without a name, use a combined provider when possible, then the default provider for each value.

  • temperature([name]): return degrees Celsius from the default or named
  • provider, or nil when unavailable.

  • humidity([name]): return relative humidity as a percentage from the default
  • or named provider, or nil when unavailable.

for _, sensor in ipairs(solaros.sensors.list()) do
    print(sensor.name, sensor.driver)
end

print(solaros.sensors.temperature())
print(solaros.sensors.humidity())

solaros.gnss

  • list(): return registered receivers with name, driver, power_control,
  • and powered.

  • power(enabled[, name]): change a driver-managed receiver power rail,
  • defaulting to the first receiver, and return the requested state.

  • fix([name[, timeout_ms]]): poll a receiver, defaulting to the first one and
  • a 1000 ms timeout. The result includes scaled-integer position, accuracy, motion, fix, satellites_valid, satellite count, and UTC fields.

local fix = solaros.gnss.fix()
if fix.valid then
    print(fix.latitude_deg_e7, fix.longitude_deg_e7)
end

solaros.haptic

  • list(): return registered haptic devices with name, driver, and the
  • number of supported numbered effects.

  • play(effect[, name]): play an effect from 1 through the device's
  • reported effect count, defaulting to the first haptic device.

  • stop([name]): stop the active effect, defaulting to the first device.
solaros.haptic.play(15)

solaros.charger

  • list(): return registered chargers, concrete drivers, and supported ranges.
  • status([name]): return charger state, input flags, configured values, and
  • the raw fault byte.

  • enable(enabled[, name]): enable or disable charging.
  • set_input_limit(mA[, name]), set_current(mA[, name]), and
  • set_voltage(mV[, name]): set an exact value in the advertised range.

OTG/boost mode and battery-chemistry policy are not exposed.

local status = solaros.charger.status()
print(status.state)

solaros.nfc

  • list(): return registered readers with name, driver, power_control,
  • and powered.

  • power(enabled[, name]): change a driver-managed reader power rail,
  • defaulting to the first reader, and return the requested state.

  • scan([name[, timeout_ms]]): discover one collision-free NFC-A tag. The
  • returned uid and atqa strings are binary-safe; sak is numeric and technology is "nfca".

solaros.imu

  • list(): return registered motion sensors with name, driver, and boolean
  • acceleration, angular_velocity, and orientation capabilities.

  • sample([name[, timeout_ms]]): read one sample, defaulting to the first
  • sensor and a 1000 ms timeout. The result contains timestamp_us and any available acceleration_m_s2, angular_velocity_rad_s, and orientation tables. Vectors use x, y, and z; orientation is a unit quaternion with w, x, y, and z.

local sample = solaros.imu.sample()
if sample.acceleration_m_s2 then
    print(sample.acceleration_m_s2.x,
          sample.acceleration_m_s2.y,
          sample.acceleration_m_s2.z)
end

solaros.gpio

  • solaros.gpio: constants INPUT, OUTPUT, PULL_NONE, PULL_UP, PULL_DOWN; functions pins, allowed, mode, configure, read, write, release when GPIO support is compiled. Pin tables include expansion, allowed, available, claimed, owner, and policy (free, releasable, or fixed).

solaros.onewire

  • solaros.onewire: allowed, reset, scan, xfer for the direct-pin compatibility API when OneWire support is compiled

solaros.led

  • solaros.led: status, set, on, off, toggle when GPIO support is compiled

solaros.adc

  • solaros.adc: pins, read when ADC support is compiled

solaros.pwm

  • solaros.pwm: constants FREQ_MIN, FREQ_MAX; functions status, set, off when PWM support is compiled

solaros.neopixel

  • solaros.neopixel: list, set, fill, show, clear when the NeoPixel expansion package is compiled

solaros.i2c

  • solaros.i2c: info, probe, scan, read_reg, write_reg when I2C support is compiled

solaros.spi

  • solaros.spi: constants MODE0 through MODE3, DEFAULT_SPEED, and MAX_SPEED; functions status, xfer, read, write when SPI support is compiled

solaros.uart

  • solaros.uart: status, baud, is_valid_baud, mode, write, read when UART support is compiled

Direct-pin OneWire

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.

Default UART

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.

Quick reference

Use solaros.gpio, solaros.onewire, solaros.led, solaros.adc, solaros.pwm, solaros.i2c, solaros.spi, solaros.uart, solaros.neopixel, solaros.battery, solaros.charger, solaros.sensors, solaros.gnss, solaros.haptic, solaros.imu, and solaros.nfc for gpio and peripherals. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua graphics API

API overview · Python graphics

solaros.gfx draws through the display owned by the current foreground application. The module is already available as solaros; it can also be loaded with require("solaros").

Graphics ownership does not deliver input implicitly. Use solaros.input.read() for touch coordinates, mouse deltas, and joystick axes.

Draw safely

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

gfx.begin()
local ok, err = pcall(function()
    local width = gfx.width()
    local height = gfx.height()
    gfx.clear(gfx.WHITE)
    gfx.color(gfx.BLACK)
    gfx.fill_circle(math.floor(width / 2), math.floor(height / 2),
                    math.floor(math.min(width, height) / 4))
    gfx.present()
end)
gfx["end"]()
if not ok then error(err) end

Lua uses gfx["end"]() because end is a language keyword. The cleanup must run even when drawing fails.

Attached displays

A port shell has no current foreground display. Discover a ready target with display list and pass its real name to gfx.begin(name). Never assume that an example such as oled0 exists.

Bitmaps and sprites

gfx.bitmap(x, y, width, height, data) draws packed 1-bit XBM data in the current color. gfx.sprite(...) is an alias intended for transparent pixel-art objects. Rows contain (width + 7) // 8 bytes, least-significant bit first. Set bits are drawn and clear bits leave the existing framebuffer unchanged. Pass the data as a binary string of exactly the required size. One call accepts at most 128 packed bytes.

local person = string.char(0x18, 0x3c, 0x18, 0x7e, 0x18, 0x24, 0x42, 0x00)
gfx.sprite(20, 20, 8, 8, person)

Icons

gfx.icon(x, y, name, size) draws an Open Iconic symbol in the current color. Names are lowercase and hyphenated, such as folder, tablet, and musical-note. Size must be 8, 16, 32, 48, or 64 pixels.

gfx.icon(20, 20, "tablet", 32)

Colors

Use gfx.WHITE, gfx.LIGHT, gfx.DARK, gfx.BLACK, gfx.gray(level), or gfx.rgb(red, green, blue). RGB components are 0..255. On color TFTs, the named colors and gray(level) span the setterm foreground and background theme, while rgb(...) stays literal in the lazily allocated indexed canvas. One-bit displays keep the existing luminance and dither path.

solaros.gfx

  • solaros.gfx: foreground graphics drawing functions

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, gray(level) with 0..GRAY_MAX, and rgb(red, green, blue) with 0..255 components. On color TFT targets, the named colors and gray(level) span the setterm foreground and background theme, while rgb(...) remains literal. One-bit targets keep the existing luminance and ordered-dither path. 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:

  • begin([target]), end()
  • width(), height(), size()
  • clear([color])
  • gray(level)
  • rgb(red, green, blue)
  • color([color]), set_color(color)
  • font([font]), set_font(font)
  • pixel(x, y), line(x0, y0, x1, y1)
  • rect(x, y, width, height), fill_rect(x, y, width, height)
  • circle(x, y, radius), fill_circle(x, y, radius)
  • icon(x, y, name, size) with a lowercase, hyphenated Open Iconic name such
  • as tablet; size is 8, 16, 32, 48, or 64

  • bitmap(x, y, width, height, data), sprite(...) alias
  • text(x, baseline_y, text)
  • refresh(), present()
  • getch([timeout_ms])

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

Quick reference

Lua: use the preloaded solaros table or local solaros = require("solaros"), then assign local gfx = solaros.gfx. gfx.begin() uses the current foreground display and errors from a port/headless shell where there is none. For an attached display, the agent must call display_list and pass a returned ready name; absent names raise ESP_ERR_NOT_FOUND. Use width, height or size; clear; color; pixel, line, rect, fill_rect, circle, fill_circle, icon, text; refresh or present. Use bitmap(x, y, width, height, data) or its sprite alias for transparent packed 1-bit XBM data, with at most 128 bytes per call. Colors are gfx.WHITE, gfx.LIGHT, gfx.DARK, gfx.BLACK, and gfx.gray(level), and gfx.rgb(red, green, blue); pass these values to clear and color, never color-name strings or guessed integers. Call gfx["end"]() because end is a Lua keyword. Required attached-display pattern (replace the quoted target with a ready display_list name):

local solaros = require("solaros")
local gfx = solaros.gfx
gfx.begin("verified-ready-target")
local ok, err = pcall(function()
    gfx.clear(gfx.WHITE)
    gfx.color(gfx.BLACK)
    -- draw here
    gfx.present()
end)
gfx["end"]()
if not ok then error(err) end

Scripting APIs · SolarOS manual

Lua input and clipboard API

API overview · Python input and clipboard

solaros.hid

  • solaros.hid: typed keyboard, mouse, and gamepad tables when service.hid is compiled

solaros.clipboard

  • solaros.clipboard: set, get, size, clear

solaros.input

  • solaros.input: sources, read, clear, status for foreground pointer, axis, and gesture events

Generic pointer, axis, and gesture input

solaros.input.sources() lists registered input sources with their numeric source, name, class, class name, capability bits, and ready state. read([timeout_ms]) returns the next pointer, axis, or gesture event table, or nil; the maximum timeout is 60000 ms. clear() discards queued events, and status() reports available, queued, capacity, and cumulative dropped counts.

Pointer events contain source metadata, pointer_id, numeric and named mode/action, x, y, delta_x, delta_y, buttons, and target. Absolute touch sources use the coordinates; relative mice use the deltas. Axis events contain source metadata, numeric and named axis, value, and delta. Gesture events contain numeric and named gesture and direction, flags, a source-specific value, and the original sensor raw word. AirWheel values are signed counter steps; 32 steps approximate one revolution.

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

input.clear()
while not solaros.should_exit() do
    local event = input.read(100)
    if event and event.type == "pointer" then
        if event.mode == input.MODE_ABSOLUTE then
            print("touch", event.action_name, event.x, event.y)
        else
            print("mouse", event.delta_x, event.delta_y, event.buttons)
        end
    elseif event and event.type == "axis" then
        print("axis", event.axis_name, event.value, event.delta)
    elseif event then
        print("gesture", event.gesture_name, event.direction_name)
    end
end

The foreground queue holds 16 events and discards the oldest event when full. Agent and other headless source runners report available=false and return nil. Keyboard characters and navigation keys remain available through solaros.tui.getch().

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.

Quick reference

Use solaros.input, solaros.hid, solaros.clipboard for input and clipboard. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua networking API

API overview · Python networking

solaros.wifi

  • solaros.wifi: status, status_text, start, stop, connect, connect_saved, disconnect, forget, forget_ssid, forget_all, known, scan, ap_start, ap_stop, nat, repeater_start, repeater_stop when Wi-Fi support is compiled

solaros.mqtt

  • solaros.mqtt: status, connect, disconnect, publish, subscribe, read when network.mqtt is compiled

solaros.http

  • solaros.http: bounded requests, retained same-origin sessions, and streaming handles when network.http-client is compiled

solaros.ftp

  • solaros.ftp: passive-mode list, download, upload, directory, delete, and rename operations when network.ftp is compiled

solaros.sftpsync

  • solaros.sftpsync: incremental file and directory synchronization over SFTP when network.sftpsync is compiled

solaros.net

  • solaros.net.router_start() and router_stop() control OS router mode.
  • solaros.net: ping, managed tcp_connect, tcp_send, tcp_receive, udp_open, udp_send, udp_receive, websocket_connect, websocket_send, websocket_receive, close, close_all, and limits when network.base is compiled

solaros.ssh_keys

  • solaros.ssh_keys: default_paths, default_exists, status, public_key, generate, remove when network.ssh is compiled

Managed TCP, UDP, and WebSocket clients

solaros.net mirrors the Python managed-network API:

  • tcp_connect(host, port[, timeout_ms]), tcp_send(handle, data[, timeout_ms]), and tcp_receive(handle[, max_bytes[, timeout_ms]])
  • udp_open([local_port]), udp_send(handle, host, port, data[, timeout_ms]), and udp_receive(handle[, max_bytes[, timeout_ms]])
  • websocket_connect(url[, subprotocol[, timeout_ms]]), websocket_send(handle, data[, text[, timeout_ms]]), and websocket_receive(handle[, max_bytes[, timeout_ms]])
  • close(handle), close_all(), and limits()

Calls are synchronous. Connect defaults to 10000 ms; send and receive default to 1000 ms. The accepted range is 0 through 60000 ms. Receive returns nil on timeout. TCP peer closure returns an empty string. Other failures raise a Lua error. UDP results contain data, address, port, truncated, and datagram_bytes; WebSocket results contain data, type, final, closed, truncated, and frame_bytes.

Each Lua app or runner invocation owns its handles exclusively. Handles cannot be shared with Python or another invocation, are generation checked, and close automatically before interpreter teardown on normal exit, error, cancellation, or forced cleanup. The combined per-invocation limit is four TCP, UDP, and WebSocket handles, with eight script handles globally. Transfers and WebSocket frames are limited to 65536 bytes and UDP datagrams to 65507 bytes. Oversized messages return the retained prefix and discard the remainder of that message.

TCP and UDP waits check cancellation within 50 ms polling slices. DNS checks cancellation before and after the platform resolver. WebSocket DNS, TCP/TLS, upgrade, and frame operations check before and after their bounded transport call, so cancellation can take up to the remaining call timeout. TCP and UDP use one end-to-end deadline per public operation. A WebSocket public call can contain multiple platform transport stages; each stage is separately bounded by the supplied timeout, and cancellation is checked between stages. WebSocket receive polling, reading, and truncated-frame draining share the remaining SolarOS-layer deadline. ws:// and certificate-validated wss:// clients are supported; listener/server sockets, multicast, custom WebSocket headers, custom certificate stores, URL credentials, fragments, and IPv6 literals are not.

local handle = solaros.net.udp_open()
solaros.net.udp_send(handle, "example.com", 9000, "hello")
local packet = solaros.net.udp_receive(handle, 4096, 1000)
if packet then
    print(packet.address, packet.port, packet.data)
end
solaros.net.close(handle)

HTTP requests

solaros.http uses the shared bounded SolarOS HTTP client. HTTPS uses the firmware certificate bundle. The mirrored call forms are:

  • request(method, url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • get(url[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]])
  • head(url[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]])
  • post, put, patch, and delete use
  • (url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])

  • session_open(origin)
  • session_request(handle, method, url[, body[, headers[, timeout_ms[, max_bytes]]]])
  • session_close(handle) and session_close_all()
  • stream_open(method, url[, body[, headers[, timeout_ms[, follow_redirects]]]])
  • stream_read(handle[, timeout_ms]), stream_close(handle), and
  • stream_close_all()

URLs must use http:// or https://. Headers are a table of up to 16 string pairs and 8192 bytes total; names and values cannot contain line breaks. Defaults are 10000 ms, a 65536-byte response body, and redirect following. max_bytes accepts 0 through 262144. The response table contains status_code, binary body, headers, content_length, bytes_received, duration_ms, truncated, and headers_truncated. A body over the limit returns its retained prefix with truncated=true. HTTP 4xx and 5xx statuses are normal responses; request, cancellation, deadline, DNS, TLS, and transport failures raise Lua errors. Exiting or interrupting Lua cancels an active request.

session_open retains one same-origin HTTP/TLS client. Its origin contains only scheme and authority. session_request rejects redirects and cross-origin URLs, clears previous request headers, and returns the same bounded response table as request. A stale connection is retried once only for GET or HEAD before any response starts; writes are never retried. Each runtime can retain two sessions, with four globally. Session handles close automatically at interpreter teardown, but scripts should close them explicitly.

stream_open runs the HTTP operation in a native worker without an end-to-end deadline. Its timeout bounds each transport operation and accepts 0 through 60000 ms; zero selects the 10000 ms service default. stream_read returns nil on wait timeout. Otherwise it returns an ordered header, response, data, complete, or error event. Data events contain up to 1024 binary bytes. Terminal events include status, content length, received byte count, duration, cancellation flags, and ESP error details. The limits are two streams per runtime and four globally, with eight queued events per stream. A full queue terminates the stream instead of dropping bytes. Streams close at interpreter teardown; close them explicitly to release resources promptly. Protocol records such as SSE messages can cross data-event boundaries and must be reassembled by the script.

local response = solaros.http.get("https://example.com/")
print(response.status_code, #response.body)

response = solaros.http.post(
    "https://example.com/api",
    '{"state":"online"}',
    { ["Content-Type"] = "application/json" }
)
print(response.status_code, response.body)
local handle = solaros.http.session_open("https://example.com")
local first = solaros.http.session_request(handle, "GET", "https://example.com/a")
local second = solaros.http.session_request(handle, "GET", "https://example.com/b")
solaros.http.session_close(handle)

FTP operations

solaros.ftp uses synchronous, unencrypted IPv4 FTP. Each call connects, performs one operation with passive data connections, and disconnects:

  • list(host[, path[, username[, password[, port]]]])
  • download(host, remote_path, local_path[, username[, password[, port]]])
  • upload(host, local_path, remote_path[, username[, password[, port]]])
  • mkdir, rmdir, and remove use
  • (host, path[, username[, password[, port]]])

  • rename(host, old_path, new_path[, username[, password[, port]]])

The defaults are /, anonymous, solaros@, and port 21. Listings contain name, is_directory, and size. Local paths use SolarOS storage resolution. Failures raise a Lua error. FTP does not encrypt credentials or content; use it only on a trusted network.

for _, item in ipairs(solaros.ftp.list("fileserver", "/incoming")) do
    print(item.name, item.size)
end
solaros.ftp.download("fileserver", "/incoming/report.txt", "/notes/report.txt")

SFTP synchronization

solaros.sftpsync.sync(source, destination[, recursive[, dry_run[, port[, password]]]]) synchronizes one local path with one [user@]host:path endpoint. The direction follows the operand order. Local paths use the SolarOS storage resolver. The username defaults to the SolarOS identity, the port defaults to 22, and an empty password allows SSH key authentication.

The call blocks while it transfers, but remains cancellation-aware. It skips regular files whose size and modification time match, never deletes destination-only files, and limits recursive traversal to eight directory levels. Set recursive for directories or dry_run to report what would change. The returned table contains files_changed, bytes_transferred, and dry_run; failures raise a Lua error.

local result = solaros.sftpsync.sync(
    "/notes", "backup@example.com:/srv/terminal/notes", true
)
print(result.files_changed, result.bytes_transferred)

A script-driven continuous control uses the same target mappings as an ADC potentiometer. Lua can create, bind, inspect, and remove controls directly:

solaros.controls.create("expression")
solaros.controls.bind_parameter(
    "expression", "synth.filter.resonance", false
)
solaros.jobs.start("controls")
solaros.controls.set("expression", 0.5)
print(solaros.controls.get("expression"))

solaros.controls.create(name[, source, input_min, input_max, smoothing_ms, deadband, inverted]) omits source for manual controls. bindings() includes pickup, application, and error state. bind_parameter() and bind_midi() return binding IDs; unbind() returns the number removed.

Dynamic app parameters are available through solaros.parameters.list(), get(path), and set(path, value). set() returns the authoritative native-unit value after the parameter's range and step handling.

Quick reference

Use solaros.wifi, solaros.mqtt, solaros.http, solaros.net, solaros.ftp, solaros.sftpsync, solaros.ssh_keys for networking. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua storage and files API

API overview · Python storage and files

solaros.storage

  • solaros.storage: status, is_mounted, mount, unmount, mount_point, usage, resolve, read_file, rescan, blocks, block_count, block, usage_for_block, mkdir, rmdir, remove, rename, copy, mount_volume, unmount_volume

Quick reference

Use solaros.storage for storage and files. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Lua text user-interface API

API overview · Python text user interfaces

Use the shared layout on displays and cursor-addressable port shells:

local tui = solaros.tui
local _, _, body = table.unpack(tui.layout())
tui.title("Example")
tui.cell(body[1], 0, body[4], "Shared layout")
tui.help("Enter open  Esc exit")

solaros.tui

  • solaros.tui: curses-like terminal drawing functions

TUI

solaros.tui draws into one buffered foreground frame. refresh() atomically commits the changed cells. It exposes constants NORMAL, BOLD, INVERSE, plus common key constants such as KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_CTRL_LEFT, KEY_CTRL_RIGHT, KEY_ESCAPE, KEY_PAGE_UP, and KEY_PAGE_DOWN.

Functions:

  • rows(), cols(), size()
  • clear(), refresh()
  • move(row, col), write(text[, attr]), addstr(row, col, text[, attr])
  • putch(row, col, ch[, attr])
  • hline(row, col, width[, attr]), vline(row, col, height[, attr]), vrule(row, col, height[, width[, attr]])
  • box(row, col, height, width[, attr])
  • fill(row, col, height, width[, ch[, attr]])
  • getch([timeout_ms])

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

Quick reference

High-level: layout, cell, title, help, tab, list_move, input_edit, input. Rectangles are {row, col, height, width}. The low-level API remains.

input(row, col, width, label, text, cursor, view[, attr[, masked]]) draws an editable input row. Set masked to true to draw one * per UTF-8 character. The mask is render-only: text and the value returned by input_edit() remain unchanged, so a script must still avoid logging secrets and discard them when they are no longer needed.

Use tui.getch() for keyboard characters and navigation keys. Use solaros.input.read() for foreground touch, mouse, and joystick events.

Scripting APIs · SolarOS manual

Lua time and scheduling API

API overview · Python time and scheduling

solaros.time

  • solaros.time: uptime_ms, sleep_ms, uptime, datetime, utc_datetime, set_datetime, set_utc_datetime, utc_to_local, local_to_utc, is_valid, timezone, set_timezone, ntp_sync. sleep_ms is cancellation-aware and accepts delays up to one hour.

solaros.rtc

  • solaros.rtc: status, set_alarm, clear_alarm, set_timer, clear_timer, pending, and ack; constants INTERRUPT_ALARM and INTERRUPT_TIMER. Direct slots are leased to Lua and released when the runtime exits.

solaros.schedule

  • solaros.schedule: list, add_in, add_every, add_at, add_daily, add_weekly, enable, remove, run, and stop_alarm; weekday bit constants SUN through SAT. Actions are "alarm" or "run", with an absolute shell-script path for "run".

Quick reference

Use solaros.time, solaros.rtc, solaros.schedule for time and scheduling. See man lua for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Named runtime buses

Named buses let scripts and expansion drivers share one description of a physical I2C, SPI, UART, MIDI, OneWire, or PS/2 connection. The registry records pins, readiness, sharing policy, and active leases.

Inspect before creating

import solaros

for bus in solaros.buses.list():
    print(bus)

Reuse a suitable registered bus instead of creating a duplicate on the same pins. Use attach and detach for leases, and remove only a runtime bus that has no users.

Change the clock of a board-defined or runtime-created named I2C bus without recreating its descriptor:

i2c speed i2c0 400000
i2c status i2c0

The change is serialized with transfers and applies to subsequent transactions, including when attached devices retain shared leases. The io app exposes the same operation as Set I2C speed on a selected I2C bus.

Transfer through the registry

Use the family matching the descriptor: i2c_scan, spi_xfer, uart_read, or onewire_scan. Inspect the result and release any lease even when a transfer fails.

Quick reference

Prefer solaros.buses for named hardware. list and get inspect registered buses; create_i2c, create_onewire, create_ps2, create_spi, create_uart, and create_midi create runtime buses; attach, detach, remove manage lifecycle. Transfer families are i2c_probe or scan or read_reg or write_reg, onewire_reset or scan or xfer, spi_xfer or read or write, and uart_read or write. Inspect descriptors before choosing names, pins, chip selects, or hosts.

A MIDI bus is a first-class, exclusive bus backed internally by an available UART controller. create_midi(name, {"tx": ..., "rx": ...}) does not take a controller number and defaults to 31250 baud. Start job midi to receive and transmit MIDI messages. Use optoisolated MIDI IN and a current-limited MIDI OUT driver; do not wire a DIN MIDI connector directly to ESP32 GPIOs.

Scripting APIs · SolarOS manual

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

API topics

Open a topic below, or use its ID with man on the device, for example man python.network. Service availability depends on the board and flavor.

TopicServices
Storage and filessolaros.storage
Time and schedulingsolaros.time, solaros.rtc, solaros.schedule
Networkingsolaros.wifi, solaros.mqtt, solaros.http, solaros.net, solaros.ftp, solaros.sftpsync, solaros.ssh_keys
Bluetoothsolaros.ble
GPIO and peripheralssolaros.gpio, solaros.onewire, solaros.led, solaros.adc, solaros.pwm, solaros.i2c, solaros.spi, solaros.uart, solaros.neopixel, solaros.battery, solaros.sensors, solaros.gnss, solaros.nfc
Buses and expansionsolaros.buses, solaros.expansion
Audio and controlsolaros.audio, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc
Input and clipboardsolaros.input, solaros.hid, solaros.clipboard
Apps, jobs, and identitysolaros.identity, solaros.jobs, solaros.sessions, solaros.apps
Contacts and messagessolaros.contacts, solaros.messages
Text user interfacessolaros.tui
Graphicssolaros.gfx

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 MicroPython's size-conscious EXTRA language profile. This adds common language features such as f-strings, sets, properties, descriptors, enumerate(), filter(), reversed(), memoryview, and frozenset. The importable runtime modules are array, binascii, cmath, collections, errno, gc, hashlib, io, json, math, micropython, random, struct, and sys.

input(), execfile(), and upstream extmod modules outside this selected set remain disabled. Use the typed solaros service APIs instead.

The selected modules include json.loads() and json.dumps(), hexadecimal and Base64 conversions in binascii, SHA-256 in hashlib, and the usual non-cryptographic random helpers. SolarOS seeds random from the ESP32 hardware random source when the module is first imported. Use hashlib for hashing and an appropriate SolarOS security service, not random, for security-sensitive values.

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

Service availability

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:

  • service.wifi: top-level wifi_status and solaros.wifi
  • network.mqtt: solaros.mqtt
  • network.http-client: solaros.http
  • network.ftp: solaros.ftp
  • network.sftpsync: solaros.sftpsync
  • network.base: solaros.net
  • network.ssh: solaros.ssh_keys
  • service.ble: solaros.ble
  • service.hid: solaros.hid
  • service.gpio: solaros.gpio and solaros.led
  • service.onewire: solaros.onewire
  • service.messaging: solaros.contacts and solaros.messages
  • service.adc, service.pwm, service.i2c, service.spi, and
  • service.uart: their matching submodules

  • service.audio, service.synth, service.battery, and service.sensors:
  • their matching helpers and submodules

  • service.gnss and service.nfc: solaros.gnss and solaros.nfc
  • service.dsp: solaros.dsp fixed-point block operations and caller-owned
  • FIR, decimator, and FFT processors

Top-Level Helpers

  • solaros.write(text): write text to the SolarOS terminal.
  • solaros.version(): return the SolarOS firmware version string.
  • solaros.should_exit(): return True when the app is being asked to stop.
  • solaros.tick_interval([ms]): get or set the foreground event-pump interval in milliseconds. Pass 0 to restore the 25 ms default.
  • solaros.battery_status(): shortcut for solaros.battery.status() when battery support is compiled.
  • solaros.wifi_status(): compact Wi-Fi status shortcut when Wi-Fi support is compiled.
  • solaros.environment(): shortcut for solaros.sensors.environment() when environmental sensor support is compiled.

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.

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, input, TUI, and graphics. Foreground pointer and axis events use solaros.input sources, read, clear, and status; keyboard characters use solaros.tui.getch(). 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.

Scripting APIs · SolarOS manual

Python apps, jobs, and identity API

API overview · Lua apps, jobs, and identity

solaros.identity

Identity functions read the SolarOS user and hostname service.

  • user(): return the configured username used by default for SSH and SCP.
  • hostname(): return the configured hostname.
  • set_user(name): validate and save the username in NVS.
  • set_hostname(name): validate and save the hostname in NVS. Reboot before
  • expecting an already initialized Wi-Fi interface to advertise the new name.

  • format(): return user@hostname.

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

Job functions control SolarOS background jobs.

  • list(): return all jobs.
  • count(): return number of jobs.
  • status(name): return one job status.
  • start(name[, args]): start a job; args is a list or tuple of strings.
  • stop(name): stop a job.

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.

  • create_shell(port[, term[, cols, rows[, charset]]]): create a port shell session and return its numeric session id.
  • create_shell(port, term="auto", cols=80, rows=24, charset="utf8"): keyword form for the same call. Use charset="ascii" for legacy terminals.
  • close(session_id): close a display/app session or stop a port shell session;
  • closing the final interactive shell is refused.

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.

  • list(): return registered apps with name and summary.
  • find(name): return one app dictionary or None.

Example:

import solaros

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

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

Quick reference

Use solaros.identity, solaros.jobs, solaros.sessions, solaros.apps for apps, jobs, and identity. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python audio and control API

API overview · Lua audio and control

solaros.controls

Continuous controls are named normalized values. Python can configure them, inspect their runtime counters, and supply manual values without knowing whether their targets are native app parameters or MIDI CC messages.

  • list(): return complete control configuration, normalized value, source
  • value, generation, sample/update counters, read errors, and last error.

  • get(name): return the current normalized value from 0.0 through 1.0.
  • set(name, value): set a manual control to a normalized value from 0.0
  • through 1.0.

  • `create(name[, source, input_min, input_max, smoothing_ms, deadband,
  • inverted]): create a manual or scalar-stream control and return its dictionary. Omit source or pass None` for a manual control.

  • delete(name) and clear(): remove one or all controls. clear() returns
  • the number removed.

  • bindings(): return parameter/MIDI targets and their pickup, application,
  • and error state.

  • bind_parameter(name, path[, pickup]) and
  • bind_midi(name, channel, controller): add a target and return its numeric binding ID.

  • unbind(name): remove all targets for a control and return the count.

Create and bind a manual control directly:

import solaros

solaros.controls.create("expression")
solaros.controls.bind_parameter(
    "expression", "synth.filter.resonance", False
)
solaros.jobs.start("controls")
solaros.controls.set("expression", 0.5)
print(solaros.controls.get("expression"))

solaros.parameters

Native applications publish parameters only while they are active.

  • list(): return path, owner, name, label, unit, range, step, curve, current
  • value, readability, and error fields for every published parameter.

  • get(path): read a native-unit value.
  • set(path, value): set a native-unit value and return the authoritative
  • value after range/step handling.

solaros.midi

The MIDI job must own a running MIDI bus before scripts transmit or receive. Create the bus with solaros.buses.create_midi() and start it with solaros.jobs.start("midi", ["midi0"]).

  • status(): return running state, bus name, RX/TX byte and message counts,
  • parser/subscriber/queue drops, last error, CC-stream count, and whether this interpreter has an active receive subscription.

  • send(status[, data1, data2]): validate and queue one raw MIDI message. The
  • argument count must match the status byte.

  • note_on(channel, note[, velocity]), note_off(channel, note[, velocity]),
  • cc(channel, controller, value), and program(channel, program): queue channel messages. Channels are 1..16; MIDI data is 0..127.

  • read([timeout_ms]) or receive([timeout_ms]): lazily create a non-consuming
  • interpreter subscription and return the next message dictionary, or None. Timeout is bounded to 60 seconds and is cancellation-aware.

  • close(): release the receive subscription early. Interpreter shutdown also
  • releases it automatically.

  • streams(), stream_add(channel, controller),
  • stream_remove(channel, controller), and stream_clear(): manage bounded incoming CC scalar streams.

Received and transmitted message dictionaries contain status, length, type, optional channel, and the applicable data1/data2 bytes.

solaros.buses.create_midi("midi0", {"tx": 2, "rx": 3})
solaros.jobs.start("midi", ["midi0"])
solaros.midi.note_on(1, 60, 100)
message = solaros.midi.read(1000)

solaros.osc

OSC bindings configure the native osc job; start and stop that worker through solaros.jobs. The scripting API does not replace its bounded UDP transport, peer filtering, or rate limiting.

  • bindings(): return complete source configuration and runtime availability,
  • value, timing, send, and error telemetry.

  • bind_stream(name, source, address[, rate_hz, delta, send_always]): publish
  • a scalar stream and return its binding ID.

  • bind_event(name, source, address[, edge, rate_hz]): publish sampled event
  • edges. edge is "rising", "falling", or "both".

  • bind_control(name, control, address[, rate_hz, send_always]): publish a
  • normalized named control and return its binding ID.

  • unbind(name) and clear(): remove one or all bindings. clear() returns
  • the number removed.

  • encode_float(address, value) and encode_int(address, value): return a
  • bounded OSC message as bytes, suitable for solaros.net.udp_send().

  • dispatch(packet): validate a message or immediate bundle and apply its
  • native parameter routes; return message, applied, unknown, and rejected counts.

  • limits(): return packet, address, binding, bundle/update, and rate limits.
solaros.osc.bind_stream(
    "ambient", "temperature", "/room/temperature", 2.0, 0.1
)
solaros.jobs.start(
    "osc", ["listen=9000", "target=192.168.1.50:9001"]
)

solaros.audio

Available when the firmware includes the audio service.

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

  • status(): return codec/sample/pin status.
  • deinit(): turn audio hardware off.
  • off(): alias for deinit().
  • set_volume(volume): set speaker volume.
  • set_mic_gain(gain_db): set microphone gain.
  • tone(frequency_hz, duration_ms[, volume]): play a tone.
  • tone_async(frequency_hz, duration_ms[, volume]): queue a tone and return its request ID.
  • cancel(request_id): cancel a queued or playing asynchronous tone.
  • queue_status(): return asynchronous tone worker, queue, and result counters.
  • level(duration_ms): measure input level and return samples, peak, and average percent.
  • capture(frames): capture 1 through 4096 native input frames and return
  • (pcm, format). pcm contains interleaved little-endian signed-16 samples. format contains sample_format, sample_rate, channels, and bits_per_sample.

  • loopback(duration_ms[, volume]): run microphone-to-speaker loopback.
  • wav_info(path): inspect a WAV file.
  • record_wav(path, duration_ms): record a native WAV file.
  • play_wav(path[, volume]): play a native WAV file.

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))
pcm, format = solaros.audio.capture(1024)
print(len(pcm), format)

solaros.synth

Available when the firmware includes the synth service. The native engine has eight voices and renders continuously without running Python in the real-time audio callback. It uses the system's global speaker volume.

  • status(): return ownership, both oscillator configurations, amplifier and filter envelopes, mono and glide settings, voice, sample-rate, render-deadline, and captured-PCM telemetry. The pcm_peak and pcm_rms fields come from the shared DSP service.
  • configure(waveform[, attack_ms[, decay_ms[, sustain_percent[, release_ms]]]]): configure active voices immediately and set the defaults for future notes. Waveforms are square, triangle, saw, sine, and noise; envelope times are 0 through 10000 ms and sustain is 0 through 100 percent.
  • configure_oscillator2(waveform[, octave[, detune_cents[, mix_percent]]]): configure the second oscillator. Octave is -2 through +2, detune is -100 through +100 cents, and mix is 0 through 100 percent. A zero mix is an exact oscillator-1 bypass.
  • configure_performance([mono[, glide_ms]]): select polyphonic or monophonic last-note playback and set portamento from 0 through 2500 ms.
  • configure_filter(cutoff_hz[, resonance_percent[, envelope_amount_percent[, attack_ms[, decay_ms[, sustain_percent[, release_ms]]]]]]): configure the resonant low-pass filter. Cutoff is 40 through 18000 Hz; percentages are 0 through 100; times are 0 through 10000 ms.
  • note_on(frequency_hz[, velocity]): start or retrigger a note from 20 through 8000 Hz. Velocity defaults to 100 and ranges from 1 through 127.
  • note_off(frequency_hz): release the matching note.
  • all_notes_off(): release all active notes through their configured release envelopes.
  • stop(): stop immediately and release audio ownership.

The first note_on() claims the exclusive audio output lazily. A script also releases that ownership automatically when it exits or is interrupted.

import solaros

solaros.synth.configure("saw", 5, 80, 65, 140)
solaros.synth.configure_oscillator2("square", 0, 7, 35)
solaros.synth.configure_filter(1200, 35, 80, 5, 250, 20, 180)
solaros.synth.configure_performance(True, 80)
solaros.synth.note_on(440, 110)
solaros.synth.note_on(554, 90)
solaros.time.sleep_ms(250)
solaros.synth.all_notes_off()
solaros.time.sleep_ms(150)
solaros.synth.stop()

solaros.dsp

solaros.dsp accepts native little-endian signed 16-bit buffers and provides backend, capabilities, dot, gain, mix, clip, level, window, fir, and fft. Stateless output operations return a new bytearray. Streaming constructors return objects with explicit reset() and close() methods. See Digital signal processing for the fixed-point contract, limits, and examples.

Quick reference

Use solaros.audio, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc for audio and control. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python bluetooth API

API overview · Lua bluetooth

solaros.ble

Top-level BLE functions expose keyboard pairing and layout controls. Generic characteristic I/O is separate, under solaros.ble.gatt.

  • status(): return human-readable BLE keyboard status, including the latest
  • reported keyboard battery percentage when available.

  • connected(): return whether a keyboard is connected.
  • pair(): start keyboard pairing.
  • forget(): remove remembered keyboard pairing.
  • layout([name]): get or set keyboard layout, currently us or de.
  • read([max_bytes]): read pending decoded keyboard bytes.

Example:

import solaros

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

solaros.ble.hid

Available when BLE support is compiled. This typed peripheral API makes the SolarOS device advertise as one composite BLE keyboard, mouse, and gamepad. The OS owns the fixed HID-over-GATT report map, descriptors, encryption, bonding, and teardown; scripts cannot publish arbitrary HID descriptors or report bytes.

hid = solaros.ble.hid
hid.start("SolarOS Controls")
# Call only after an explicit user action to pair a new host.
hid.pair()

while not solaros.should_exit():
    event = hid.poll()
    if event is not None:
        print(event)
        if event["type"] == "passkey":
            print("Enter %06d on the remote host" % event["passkey"])
    if hid.status()["keyboard_subscribed"]:
        hid.keyboard.press(hid.KEY_LEFT_CTRL, hid.KEY_A)
        hid.keyboard.release_all()
        break
  • start(name): acquire the application-peripheral lease and start connectable
  • advertising. The name is 1..26 bytes without an embedded NUL. One remote host can connect at a time.

  • pair(): enter explicit new-host pairing mode. SolarOS removes the next
  • connecting peer's stored HID bond before starting security. Omit this on normal startup so a remembered host reconnects without pairing again.

  • stop(): send best-effort neutral keyboard, mouse, and gamepad reports, stop
  • advertising, disconnect the host, and retire the service.

  • status(): return registered, advertising, closing, connected,
  • encrypted, bonded, keyboard_subscribed, mouse_subscribed, gamepad_subscribed, keyboard_leds, and event queue counters.

  • poll(): nonblocking; return None when empty or an event dictionary. Event
  • types are connected, secured, disconnected, keyboard-leds, and passkey. Render a passkey as six digits and enter it on the remote host.

  • keyboard.press(*keys), keyboard.release(*keys), and
  • keyboard.release_all() use the KEY_* constants. Up to six ordinary keys plus modifiers can be held.

  • mouse.move(x, y) sends signed relative movement, chunking large deltas;
  • mouse.button(mask, pressed) uses the MOUSE_* masks.

  • gamepad.axis(axis, value), gamepad.button(number, pressed), and
  • gamepad.hat(direction) update state; gamepad.send() publishes it. Axes use -32768..32767, buttons are 1..32, and hats use the HAT_* constants.

Input report operations require an encrypted host subscription for that report type and otherwise raise OSError("ESP_ERR_INVALID_STATE"). Keyboard LED bits are available as LED_NUM_LOCK, LED_CAPS_LOCK, LED_SCROLL_LOCK, LED_COMPOSE, and LED_KANA. Call poll() regularly and inspect events_dropped; the native queue holds 16 events and drops new events when full. The runtime owns this service and cleans it up on normal exit, uncaught exceptions, interruption, or forced stop.

solaros.ble.hid and solaros.ble.server share one connectable-advertising lease, so a runtime cannot use both simultaneously. Outgoing solaros.ble.gatt peers remain separate subject to the shared controller connection budget. This BLE API is independent of the dormant USB-only solaros.hid package described in Python input.

solaros.ble.scan()

Available when BLE support is compiled. Returns a list of up to 32 device dictionaries, including non-keyboard devices. Each record contains address (colon-separated hex), name (possibly empty), integer addr_type, rssi (dBm), appearance, and booleans hid_service, keyboard_like, remembered, and connected. Address types are 0 public, 1 random, 2 public identity, and 3 random identity.

Uses the same blocking scan as shell ble scan, with no arguments. Scan errors raise OSError; no devices returns an empty list. Scanning is rejected during sleep preparation or while a generic GATT link is active or retiring. Scan before connecting. Script cancellation is checked before and after the blocking service call; it does not interrupt the radio scan.

for device in solaros.ble.scan():
    print(device["address"], device["addr_type"], device["name"], device["rssi"])

solaros.ble.gatt

Available when BLE support is compiled. This synchronous client owns one session per Python runtime; Lua and the shell have separate owners. Each runtime may own multiple peers, with independent operations and connection state.

  • capacity(): total configured generic-peer capacity, not currently free slots.
  • One additional connection is reserved for the OS keyboard. Capacity is set by firmware host/controller configuration, not an application peer-count limit.

  • connect(address, addr_type=0, timeout_ms=0): connect, discover services, and
  • return an opaque peer handle. Use a colon-separated address such as aa:bb:cc:dd:ee:ff; address types are 0 public, 1 random, 2 public identity, and 3 random identity. Use solaros.ble.scan() to find the address and type.

  • disconnect(peer): invalidate the handle and request asynchronous disconnect.
  • It cannot disconnect another runtime's or the shell's peer.

  • status(peer): return owner, address, addr_type, status, connected,
  • busy, retiring, mtu, service_count, max_value_bytes, event_capacity, event_count, and events_dropped.

  • services(peer): return dictionaries with index, uuid, primary,
  • start_handle, and end_handle.

  • characteristics(peer, service_index): return dictionaries with uuid, handle,
  • and the numeric Bluetooth properties bitmask. Pass the service's returned zero-based index.

  • read(peer, handle, timeout_ms=0): return characteristic data as bytes.
  • write(peer, handle, data, with_response=True, timeout_ms=0): write binary data.
  • data must support the buffer protocol, for example bytes or bytearray. Without response, completion confirms local submission, not peer receipt.

  • subscribe(peer, handle, indicate=False, timeout_ms=0): discover the
  • characteristic's CCCD and enable notifications, or indications when True. The characteristic must advertise the requested property. Completion waits for the CCCD write acknowledgement; delivery starts after that acknowledgement.

  • unsubscribe(peer, handle, timeout_ms=0): disable delivery and discard queued
  • events for this characteristic after the CCCD write succeeds.

  • configure_queue(peer, capacity): allocate a queue shared by this peer's
  • subscriptions. Capacity must be positive; allocation may fail. The peer must be connected, with no pending operation and an empty queue. Failure preserves the old queue. First subscribe allocates 16 entries unless configured earlier.

  • poll(peer): nonblocking; return None when empty or a dictionary with
  • handle, binary data (bytes), and indication (bool). It may be called while another task is performing an operation on this peer.

Arguments are positional. Timeouts accept 0..60000 milliseconds; zero selects 12 seconds for connect or 5 seconds for read/write/subscribe/unsubscribe. Errors raise OSError; cancellation reports BLE operation cancelled. Capacity exhaustion reports BLE connection capacity exhausted; allocation can also fail. Existing peers remain connected when another connection cannot be admitted. Connect peers sequentially: concurrent connection establishment can report ESP_ERR_INVALID_STATE. Timeout or cancellation retires only the affected connection. Release its peer handle and connect again after teardown finishes. Rediscover characteristic handles after every reconnect, including after sleep.

The runtime closes its session and all peers on normal exit, uncaught exceptions, and stop. Waiting operations check script stop/deadline state every 50 ms. In the REPL, the session lasts until the interpreter exits; use disconnect(peer) when done. An explicitly caught error does not end the runtime or release its session.

The current service retains at most 24 services and 64 characteristics per service. Reads return at most the first 128 bytes; writes accept 1..128 bytes and must fit within the negotiated MTU minus three bytes. mtu reports the negotiated value; connection setup performs MTU exchange before discovery. Notification queues preserve arrival order. Full queues drop new events; payloads exceeding 128 bytes are dropped whole, never truncated. Both increment the saturating events_dropped counter. NimBLE confirms indications at the protocol layer; confirmation does not mean the application consumed the event. Poll and monitor loss counters regularly. Disconnect, cancellation, timeout and sleep release queue storage and discard queued events. Loss counters remain readable for the peer handle's lifetime. Reconnect and resubscribe after sleep. No user callback runs on the Bluetooth task. There is no script MTU setter or automatic write chunking. Application services and advertising use 2, independently of these client peers.

import solaros

gatt = solaros.ble.gatt
peers = []
try:
    # Replace both addresses and address types with your peripherals.
    peers.append(gatt.connect("aa:bb:cc:dd:ee:01", 1))
    peers.append(gatt.connect("aa:bb:cc:dd:ee:02", 1))
    for peer in peers:
        print(gatt.status(peer))
        for service in gatt.services(peer):
            print(service)
            print(gatt.characteristics(peer, service["index"]))
        # Use a readable handle from this peer: print(gatt.read(peer, handle))
finally:
    for peer in peers:
        gatt.disconnect(peer)

For an already connected peer and a discovered notification-capable handle:

gatt.configure_queue(peer, 32)
gatt.subscribe(peer, handle)       # pass True as third argument for indications
event = gatt.poll(peer)           # call regularly from the application's loop
if event is not None:
    print(event["handle"], event["data"], event["indication"])
print(gatt.status(peer)["events_dropped"])
# When finished: gatt.unsubscribe(peer, handle)

Quick reference

Use solaros.ble for bluetooth. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python buses and expansion API

API overview · Lua buses and expansion

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.

  • Constants: MODE0 through MODE3, SPI2_HOST, SPI3_HOST,
  • DEFAULT_SPEED, and MAX_SPEED.

  • list(): return all named bus dictionaries.
  • get(name): return one named bus dictionary or raise OSError when absent.
  • create_i2c(name, config): create a runtime I2C bus and return its dictionary.
  • create_onewire(name, config): create a runtime 1-Wire bus and return its dictionary.
  • create_ps2(name, config): create an exclusive PS/2 bus from clock and data pins.
  • create_spi(name, config): create a runtime SPI bus and return its dictionary.
  • create_uart(name, config): create a lazy runtime UART bus and return its dictionary.
  • create_midi(name, config): create an exclusive MIDI bus and automatically select its UART backend.
  • attach(name): attach a named detachable bus and reserve its endpoint and pins.
  • detach(name): detach an idle named bus without deleting its descriptor.
  • remove(name): remove an idle runtime bus. Board-defined or leased buses
  • cannot be removed.

  • i2c_probe(bus, address): probe an address on a named I2C bus.
  • i2c_scan(bus): return detected addresses on a named I2C bus.
  • i2c_read_reg(bus, address, reg, length): read bytes from an 8-bit register.
  • i2c_write_reg(bus, address, reg, data): write bytes to an 8-bit register.
  • onewire_reset(bus): reset a named 1-Wire bus and return device presence.
  • onewire_scan(bus): return ROM-address dictionaries found on a named bus.
  • onewire_xfer(bus, read_length[, data]): reset, write, and read a named bus.
  • uart_write(bus, data): write bytes through a named UART and return the number written.
  • uart_read(bus[, length[, timeout_ms]]): read bytes from a named UART.
  • spi_xfer(bus, cs, data[, mode[, speed_hz]]): perform a full-duplex named-bus
  • transfer and return received bytes.

  • spi_read(bus, cs, length[, fill[, mode[, speed_hz]]]): clock in bytes using
  • the optional fill byte.

  • spi_write(bus, cs, data[, mode[, speed_hz]]): write bytes and return the
  • number written.

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 and MIDI 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_midi requires tx and rx; optional baud_rate defaults to 31250. SolarOS selects an unused board-approved UART controller. The returned port is diagnostic backend information, not an input to the MIDI API.

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.

  • drivers(): return compiled driver dictionaries with name, summary,
  • category, required_capabilities, probe_supported, and supported.

  • devices(): return active device dictionaries with name, driver,
  • origin (board or runtime), ready, autostart, detachable, and bindings. Each normalized binding contains kind, role, target, value, and aux.

  • attach(driver, name, bindings): attach a driver using a binding dictionary.
  • detach(name): detach a device and release its resource claims and bus leases.

Binding dictionaries accept spi, cs (or ce), i2c, addr, alt_addr, uart, ps2, gpio, irq, reset (or rst), data, bck, din, rck, dc, mclk, ws, dout, busy, adc, pwm, count, keys, x, y, min, center, max, and deadzone. ps2 names an existing PS/2 bus; x and y name scalar streams; keys maps logical key names to GPIO numbers. cs requires spi; addr and alt_addr require 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")

Quick reference

Use solaros.buses, solaros.expansion for buses and expansion. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python contacts and messages API

API overview · Lua contacts and messages

solaros.contacts and solaros.messages

Provider-neutral messaging builds expose:

  • solaros.contacts.list(): bounded contact summaries.
  • solaros.contacts.get(contact_id): one contact with endpoint IDs, or None.
  • solaros.messages.conversations(): bounded conversation summaries.
  • solaros.messages.list(conversation_id): retained messages.
  • solaros.messages.send(conversation_id, body, allow_untrusted=False): queue
  • a message and return its stable hexadecimal ID.

  • solaros.messages.mark_read(conversation_id): mark linked message state read.
  • solaros.messages.cancel(message_id): cancel a queued message using the
  • hexadecimal string returned by send() or list().

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

Quick reference

Use solaros.contacts, solaros.messages for contacts and messages. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python gpio and peripherals API

API overview · Lua gpio and peripherals

solaros.battery

Available when the firmware includes the battery service.

  • status(): return battery status with voltage_mv, percent,
  • percent_estimated, adc_calibrated, external_power, charging, and charging_known. When charging_known is false, charging is only a trend estimate.

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.

  • list(): return registered providers with name, driver, temperature,
  • and humidity fields.

  • environment([name]): return temperature_c and humidity_percent from a
  • named provider. Without a name, prefer one provider that supplies both values, then fall back to the default provider for each value.

  • temperature([name]): return a temperature in degrees Celsius from the
  • default or named provider, or None when unavailable.

  • humidity([name]): return relative humidity as a percentage from the
  • default or named provider, or None when unavailable.

Example:

import solaros

for sensor in solaros.sensors.list():
    print(sensor["name"], sensor["driver"])

print(solaros.sensors.temperature())
print(solaros.sensors.humidity())

solaros.gnss

Available when the firmware includes a GNSS receiver service.

  • list(): return registered receivers with name, driver, power_control,
  • and powered.

  • power(enabled[, name]): change a driver-managed receiver power rail,
  • defaulting to the first receiver, and return the requested state.

  • fix([name[, timeout_ms]]): poll a receiver, defaulting to the first one and
  • a 1000 ms timeout. The result contains fix and UTC validity, date and time, fix type, satellites_valid and satellite count, longitude and latitude in degrees times 10^7, MSL height and accuracy in millimeters, ground speed in millimeters per second, heading in degrees times 10^5, and position DOP times 100.

import solaros

fix = solaros.gnss.fix()
if fix["valid"]:
    print(fix["latitude_deg_e7"], fix["longitude_deg_e7"])

solaros.haptic

Available when the firmware includes the driver-agnostic haptic service.

  • list(): return registered haptic devices with name, driver, and the
  • number of supported numbered effects.

  • play(effect[, name]): play an effect from 1 through the device's
  • reported effect count, defaulting to the first haptic device.

  • stop([name]): stop the active effect, defaulting to the first device.
import solaros

solaros.haptic.play(15)

solaros.charger

Available when the firmware includes the driver-agnostic charger service.

  • list(): return registered chargers, concrete drivers, and the supported
  • minimum, maximum, and step for each configurable value.

  • status([name]): return charging state, input and power-good flags, enabled
  • state, configured limits, and the driver's raw fault byte.

  • enable(enabled[, name]): enable or disable charging.
  • set_input_limit(mA[, name]), set_current(mA[, name]), and
  • set_voltage(mV[, name]): set an exact value in the range reported by list(). Values are rejected instead of silently rounded.

OTG/boost mode and battery-chemistry policy are intentionally not exposed.

import solaros

print(solaros.charger.status())

solaros.nfc

Available when the firmware includes an NFC reader service.

  • list(): return registered readers with name, driver, power_control,
  • and powered.

  • power(enabled[, name]): change a driver-managed reader power rail,
  • defaulting to the first reader, and return the requested state.

  • scan([name[, timeout_ms]]): discover one collision-free NFC-A tag,
  • defaulting to the first reader and a 1000 ms timeout. The result contains binary uid and atqa values, numeric sak, and technology "nfca".

import binascii
import solaros

tag = solaros.nfc.scan()
print(binascii.hexlify(tag["uid"]))

solaros.imu

Available when the firmware includes the motion-sensor service.

  • list(): return registered motion sensors with name, driver, and boolean
  • acceleration, angular_velocity, and orientation capabilities.

  • sample([name[, timeout_ms]]): read one sample, defaulting to the first
  • sensor and a 1000 ms timeout. The result contains timestamp_us, acceleration_m_s2, angular_velocity_rad_s, and orientation. Unavailable measurements are None. Vectors use x, y, and z fields; orientation is a unit quaternion with w, x, y, and z fields.

import solaros

sample = solaros.imu.sample()
accel = sample["acceleration_m_s2"]
if accel is not None:
    print(accel["x"], accel["y"], accel["z"])

solaros.gpio

GPIO functions expose only runtime-safe expansion pins. Use solaros.gpio.pins() to inspect the active board. On SolarTerm (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.

  • Constants: INPUT, OUTPUT, PULL_NONE, PULL_UP, PULL_DOWN.
  • pins(): return board GPIO dictionaries with pin, expansion, allowed,
  • available, claimed, owner, policy, role, configured, mode, pull, level, and level_valid. Pin policy is free, releasable, or fixed; releasable pins report allowed=True but become available only when their board bus is detached.

  • allowed(pin): return whether a pin can be controlled by runtime apps.
  • mode(pin): return one pin dictionary.
  • mode(pin, mode[, pull]): configure an allowed pin. mode may be INPUT, OUTPUT, "in", "input", "out", or "output".
  • configure(pin, mode[, pull]): alias for mode(pin, mode[, pull]).
  • read(pin): read an allowed pin and return 0 or 1.
  • write(pin, value): set an allowed pin low or high. If needed, the pin is configured as output first.
  • release(pin): reset the pin and release its direct-GPIO claim.

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.

  • allowed(pin): return whether the pin is available for OneWire operations.
  • reset(pin): reset the bus and return whether a presence pulse was detected.
  • scan(pin): return device dictionaries containing a 16-digit hexadecimal address and numeric family code.
  • xfer(pin, read_len[, data]): reset the bus, write a bytes-like object, then read and return read_len bytes. Either read_len or data must be non-empty.

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.

  • status(): return whether the status LED is currently on.
  • set(on): set the status LED and return the resulting boolean state.
  • on(): turn the status LED on and return True.
  • off(): turn the status LED off and return False.
  • toggle(): toggle the status LED and return the resulting boolean state.

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.

  • pins(): return dictionaries with pin, allowed, adc_capable, unit, and channel.
  • read(pin): return pin, raw, voltage_mv, unit, channel, and calibrated.

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.

  • Constants: FREQ_MIN, FREQ_MAX.
  • status(): return dictionaries with pin, allowed, active, channel, freq_hz, and duty_percent.
  • set(pin, freq_hz, duty_percent): start or update PWM on a pin. Duty is 0..100.
  • off(pin): stop PWM on a pin.

Example:

import solaros

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

solaros.neopixel

Available when the NeoPixel expansion package is compiled.

  • list(): return attached strip dictionaries with name, data_pin, and count.
  • set(name, index, red, green, blue): update one buffered pixel.
  • fill(name, red, green, blue): update every buffered pixel.
  • show(name): transmit the buffered colors in GRB wire order.
  • clear(name): clear the buffer and transmit it immediately.
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.

  • info(): return bus speed and SDA/SCL pins.
  • probe(address): raise on missing device, return None on success.
  • scan(): return detected addresses.
  • read_reg(address, reg, length): read bytes from an 8-bit register.
  • write_reg(address, reg, data): write bytes to an 8-bit register.

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_*.

  • Constants: MODE0, MODE1, MODE2, MODE3, DEFAULT_SPEED, MAX_SPEED.
  • status(): return the bus name, host, pins, speed, transfer limit, and configured CS slots.
  • xfer(cs, data[, mode[, speed_hz]]): perform a full-duplex transfer and return the received bytes.
  • read(cs, length[, fill[, mode[, speed_hz]]]): transmit the fill byte, default 0xff, while reading.
  • write(cs, data[, mode[, speed_hz]]): write bytes and return the number written.

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.

  • status(): return UART name, attached, port, pins, baud rate, mode, 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.
  • baud([rate]): get or set baud rate.
  • is_valid_baud(rate): return whether a baud rate is accepted.
  • mode([name]): get or set raw or line mode.
  • write(data): write bytes and return bytes written.
  • read([length[, timeout_ms]]): read bytes.

Example:

import solaros

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

Quick reference

Use solaros.gpio, solaros.onewire, solaros.led, solaros.adc, solaros.pwm, solaros.i2c, solaros.spi, solaros.uart, solaros.neopixel, solaros.battery, solaros.charger, solaros.sensors, solaros.gnss, solaros.haptic, solaros.imu, and solaros.nfc for gpio and peripherals. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python graphics API

API overview · Lua graphics

solaros.gfx draws through the display owned by the current foreground application. A script started from a display shell can use that display without naming it. A script started from a port shell must use a ready attached display name.

Graphics ownership does not deliver input implicitly. Use solaros.input.read() for touch coordinates, mouse deltas, and joystick axes.

Draw on the current display

import solaros
from solaros import gfx

gfx.begin()
try:
    width = gfx.width()
    height = gfx.height()
    gfx.clear(gfx.WHITE)
    gfx.color(gfx.BLACK)
    gfx.fill_circle(width // 2, height // 2, min(width, height) // 4)
    gfx.present()
finally:
    gfx.end()

Always put gfx.end() in finally so an exception releases the display.

Draw on an attached display

First run display list or inspect solaros.expansion.devices(). Pass only a ready target returned by discovery:

gfx.begin("oled0")

An absent name raises ESP_ERR_NOT_FOUND. Calling gfx.begin() without a name from a port or headless shell raises RuntimeError because that session has no foreground display.

Colors and dimensions

Use gfx.WHITE, gfx.LIGHT, gfx.DARK, gfx.BLACK, gfx.gray(level), or gfx.rgb(red, green, blue). RGB components are 0..255. On color TFTs, the named colors and gray(level) span the setterm foreground and background theme, while rgb(...) stays literal in the lazily allocated indexed canvas. One-bit targets keep the existing luminance and dither path. Do not use color-name strings or guessed integer values. Read dimensions with width(), height(), or size() rather than assuming a panel size.

Bitmaps and sprites

gfx.bitmap(x, y, width, height, data) draws packed 1-bit XBM data in the current color. gfx.sprite(...) is an alias intended for transparent pixel-art objects. Rows contain (width + 7) // 8 bytes, least-significant bit first. Set bits are drawn and clear bits leave the existing framebuffer unchanged. The data must be a bytes-like object of exactly the required size, with a maximum of 128 packed bytes per call.

person = bytes((0x18, 0x3C, 0x18, 0x7E, 0x18, 0x24, 0x42, 0x00))
gfx.sprite(20, 20, 8, 8, person)

Icons

gfx.icon(x, y, name, size) draws an Open Iconic symbol in the current color. Names are lowercase and hyphenated, such as folder, tablet, and musical-note. Size must be 8, 16, 32, 48, or 64 pixels.

gfx.icon(20, 20, "tablet", 32)

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:

  • WHITE
  • LIGHT
  • DARK
  • BLACK
  • GRAY_MAX: maximum grayscale level accepted by gray(level), currently 16.

gray(level) returns a semantic shade from the setterm foreground color at level 0 to the setterm background color at GRAY_MAX; BLACK, DARK, LIGHT, and WHITE use the same theme range. rgb(red, green, blue) returns an explicit RGB color from three 0..255 components. Color-capable TFT targets preserve explicit RGB values in an indexed-color canvas. One-bit targets keep the existing luminance and ordered-dither path.

Fonts:

  • FONT_SMALL
  • FONT_MONO
  • FONT_BOLD
  • FONT_MONO_12, FONT_MONO_14, FONT_MONO_16, FONT_MONO_18, FONT_MONO_20
  • FONT_BOLD_12, FONT_BOLD_14, FONT_BOLD_16, FONT_BOLD_18, FONT_BOLD_20
  • FONT_ITALIC_12, FONT_ITALIC_14, FONT_ITALIC_16, FONT_ITALIC_18, FONT_ITALIC_20
  • FONT_BOLD_ITALIC_12, FONT_BOLD_ITALIC_14, FONT_BOLD_ITALIC_16, FONT_BOLD_ITALIC_18, FONT_BOLD_ITALIC_20

Italic constants currently map to the closest upright face in the trimmed firmware font set.

Functions:

  • begin([target]): enter foreground graphics mode; without a target, require
  • the current shell to have a display framebuffer; when target is provided, claim and draw to that named display target.

  • end(): leave graphics mode and redraw the terminal.
  • width(): return graphics width in pixels.
  • height(): return graphics height in pixels.
  • size(): return (width, height).
  • clear([color]): clear the graphics buffer, defaulting to WHITE.
  • gray(level): return a grayscale color value from 0 to GRAY_MAX.
  • rgb(red, green, blue): return an RGB color; each component is 0..255.
  • color([color]): get or set current drawing color.
  • set_color(color): alias for color(color).
  • font([font]): get or set current text font.
  • set_font(font): alias for font(font).
  • pixel(x, y): draw one pixel.
  • line(x0, y0, x1, y1): draw a line.
  • rect(x, y, width, height): draw a rectangle outline.
  • fill_rect(x, y, width, height): draw a filled rectangle.
  • circle(x, y, radius): draw a circle outline.
  • fill_circle(x, y, radius): draw a filled circle.
  • icon(x, y, name, size): draw a named Open Iconic symbol; use a lowercase,
  • hyphenated name such as tablet; size is 8, 16, 32, 48, or 64.

  • bitmap(x, y, width, height, data): draw a transparent packed 1-bit XBM.
  • sprite(x, y, width, height, data): alias for bitmap().
  • text(x, baseline_y, text): draw UTF-8 text.
  • refresh(): present the graphics buffer.
  • present(): alias for refresh().
  • getch([timeout_ms]): return a key code or None.

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()

Quick reference

Python: import solaros; from solaros import gfx. gfx.begin() uses the current foreground display and raises RuntimeError from a port/headless shell where there is none. For an attached display, the agent must call display_list and pass a returned ready name to gfx.begin(name); scripts can verify names with solaros.expansion.devices(). An absent name raises ESP_ERR_NOT_FOUND. Use width(), height(), or size(); clear(color); color(color); pixel, line, rect, fill_rect, circle, fill_circle, icon, text; refresh() or present(); then end(). Use bitmap(x, y, width, height, data) or its sprite alias for transparent packed 1-bit XBM data, with at most 128 bytes per call. Standard min() and max() are available. Colors are gfx.WHITE, gfx.LIGHT, gfx.DARK, gfx.BLACK, gfx.gray(level), and gfx.rgb(red, green, blue); pass these values to clear() and color(), never color-name strings or guessed integers. Required attached-display pattern (replace the quoted target with a ready display_list name):

import solaros
from solaros import gfx
gfx.begin("verified-ready-target")
try:
    gfx.clear(gfx.WHITE)
    gfx.color(gfx.BLACK)
    # draw here
    gfx.present()
finally:
    gfx.end()

Scripting APIs · SolarOS manual

Python input and clipboard API

API overview · Lua input and clipboard

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()
  • status() returns initialized and connected.
  • keyboard.press(*keys) and keyboard.release(*keys) preserve each accepted
  • keyboard state transition; up to six ordinary keys plus modifiers can be held. keyboard.release_all() releases every key.

  • mouse.move(x, y) accumulates signed deltas until transmitted.
  • mouse.button(mask, pressed) changes one or more standard button bits.

  • Gamepad setters update coalesced state. Axes use -32768..32767, buttons are
  • numbered 1..32, hats use HAT_CENTERED or one of eight directions, and gamepad.send() queues the current state.

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.

  • set(data): set clipboard bytes.
  • get(): return clipboard bytes.
  • size(): return clipboard size in bytes.
  • clear(): clear the clipboard.

Example:

import solaros

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

solaros.input

Foreground scripts can receive generic pointer, axis, and gesture events routed to their active session. sources() lists registered input sources with source, name, source_class, source_class_name, capabilities, and ready.

  • read([timeout_ms]): return the next pointer, axis, or gesture event dictionary, or
  • None. The maximum timeout is 60000 ms.

  • clear(): discard queued pointer, axis, and gesture events and return the number
  • discarded.

  • status(): return available, queued, capacity, and cumulative
  • dropped counters.

Pointer dictionaries have type="pointer", source metadata, pointer_id, numeric and named mode/action, x, y, delta_x, delta_y, buttons, and target. Touch and other absolute sources use x/y; relative mice use the deltas. Axis dictionaries have type="axis", source metadata, numeric and named axis, value, and delta. Gesture dictionaries have type="gesture", numeric and named gesture and direction, flags, a source-specific value, and the original sensor raw word. AirWheel values are signed counter steps; 32 steps approximate one revolution.

import solaros
from solaros import input as device_input

device_input.clear()
while not solaros.should_exit():
    event = device_input.read(100)
    if event is None:
        continue
    if event["type"] == "pointer":
        if event["mode"] == device_input.MODE_ABSOLUTE:
            print("touch", event["action_name"], event["x"], event["y"])
        else:
            print("mouse", event["delta_x"], event["delta_y"], event["buttons"])
    elif event["type"] == "axis":
        print("axis", event["axis_name"], event["value"], event["delta"])
    else:
        print("gesture", event["gesture_name"], event["direction_name"])

The queue holds 16 events. When it is full, the oldest event is discarded so the script receives current pointer state; inspect status()["dropped"] when loss matters. Event reads are available only to a foreground Python app. Agent or other headless source runners report available=False and return None. Keyboard characters and navigation keys remain available through solaros.tui.getch().

Quick reference

Use solaros.input, solaros.hid, solaros.clipboard for input and clipboard. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python networking API

API overview · Lua networking

solaros.wifi

Wi-Fi functions expose station, SoftAP, scan, low-level NAT, and L2 IPv4 repeater controls.

  • status(): return detailed Wi-Fi status.
  • status_text(): return the same compact status text used by the shell.
  • start(): start Wi-Fi and reconnect saved station config if present.
  • stop(): stop Wi-Fi.
  • connect(ssid[, password]): connect to a station network and save it.
  • connect_saved(): connect using remembered station profiles.
  • disconnect(): disconnect station mode.
  • forget(): remove the active or preferred station profile.
  • forget_ssid(ssid): remove one remembered station profile.
  • forget_all(): remove all remembered station profiles.
  • known(): return remembered station profiles as dictionaries with ssid and preferred.
  • scan(): return visible APs as dictionaries with ssid, auth, rssi, channel, and hidden.
  • ap_start([ssid[, password[, auth]]]): start SoftAP, reusing saved AP config when no arguments are supplied.
  • ap_stop(): stop SoftAP.
  • nat(enabled): advanced control for NAT on the SoftAP interface. Prefer solaros.net.router_start() and router_stop().
  • repeater_start(): connect the preferred remembered upstream when needed, repeat its saved SSID and password, and bridge upstream DHCP plus IPv4/ARP traffic without NAT.
  • repeater_stop(): stop L2 forwarding and the SoftAP while retaining the upstream station.

Example:

import solaros

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

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

solaros.net

  • router_start(): start the saved downstream Wi-Fi AP and route its clients through the SolarOS route table using IPv4 NAT.
  • router_stop(): disable router mode and stop the downstream AP.
  • ping(host[, count[, timeout_ms[, interval_ms[, data_size]]]]): send bounded ICMP echo requests.

The remaining solaros.net functions provide bounded TCP, UDP, and WebSocket handles.

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.

  • status(): return MQTT status, saved broker URL, client ID, auth flags, counters, queued message count, and last error.
  • connect([url[, username[, password]]]): connect to mqtt://... or mqtts://...; supplied values are saved in NVS. With no arguments, reconnect using saved settings.
  • disconnect(): stop the MQTT client.
  • publish(topic, payload[, qos[, retain]]): publish bytes or text and return the message ID.
  • subscribe(topic[, qos]): subscribe and return the message ID.
  • read([timeout_ms]): return the next queued message dictionary, or None on timeout. Message payloads are returned as bytes.

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

solaros.http provides bounded synchronous HTTP and HTTPS requests through the shared SolarOS HTTP client. It is present when network.http-client is compiled. HTTPS uses the firmware certificate bundle; no socket, TLS, or upstream MicroPython networking module is exposed.

  • request(method, url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • get(url[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]])
  • head(url[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]])
  • post(url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • put(url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • patch(url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • delete(url[, body[, headers[, timeout_ms[, max_bytes[, follow_redirects]]]]])
  • session_open(origin)
  • session_request(handle, method, url[, body[, headers[, timeout_ms[, max_bytes]]]])
  • session_close(handle)
  • session_close_all()
  • stream_open(method, url[, body[, headers[, timeout_ms[, follow_redirects]]]])
  • stream_read(handle[, timeout_ms])
  • stream_close(handle)
  • stream_close_all()

Methods are case-insensitive in request(). Request bodies accept text or any readable buffer. URLs must use http:// or https://. Headers are a dictionary of up to 16 string pairs and 8192 bytes total; names and values cannot contain line breaks. The defaults are a 10000 ms end-to-end timeout, a 65536-byte response-body limit, and redirect following. max_bytes accepts 0 through 262144; zero collects metadata without retaining a body.

The result is a dictionary containing status_code, binary body, headers, content_length, bytes_received, duration_ms, truncated, and headers_truncated. Header names retain the server's spelling and duplicate names use the last received value. content_length is -1 when the server did not supply it. When a body exceeds max_bytes, SolarOS stops that response, returns the retained prefix, and sets truncated=True.

HTTP error statuses such as 404 and 500 are normal results. Invalid requests, allocation failures, cancellation, deadlines, DNS failures, and transport errors raise OSError("ESP_ERR_..."). Exiting the Python app cancels an active request.

session_open() retains one same-origin HTTP/TLS client and returns an interpreter-owned opaque handle. origin contains only the scheme and authority, for example https://example.com; credentials, paths, queries, and fragments are rejected. session_request() has the same response and body limits as request(), but redirects are always disabled and the full request URL must match the opened origin. Per-request headers are removed before every request. A stale retained connection is retried once only for GET or HEAD and only before response headers or body data arrive. Writes are never retried. Each runtime can retain two sessions, with four sessions globally. Close handles in finally; all retained clients also close at interpreter teardown.

stream_open() starts a native worker and returns an interpreter-owned opaque handle. Unlike request(), it has no end-to-end deadline: timeout_ms bounds each connect, header, write, or body-read operation and accepts 0 through 60000; zero selects the 10000 ms service default. stream_read() returns None when its wait expires. Otherwise it returns the next ordered event dictionary:

  • header: status_code, name, value, and truncated
  • response: status_code and content_length
  • data: status_code and up to 1024 binary bytes in data
  • complete or error: final status, content length, received byte count,
  • duration, cancellation/deadline flags, and ESP error details

Each runtime can open two streams, with four streams globally. Each stream has an eight-event queue. If a script does not drain it, SolarOS terminates the stream with ESP_ERR_NO_MEM; it never silently drops body bytes. Handles close automatically at interpreter teardown. Always close them explicitly in finally so a completed stream releases its queue and handle immediately.

The data boundary is a transport chunk, not an application record. For SSE, retain an incomplete line across data events and dispatch a message only at the blank line that terminates the SSE record.

import solaros

response = solaros.http.get("https://example.com/")
print(response["status_code"], len(response["body"]))

response = solaros.http.post(
    "https://example.com/api",
    b'{"state":"online"}',
    {"Content-Type": "application/json"},
)
print(response["status_code"], response["body"])
handle = solaros.http.session_open("https://example.com")
try:
    first = solaros.http.session_request(handle, "GET", "https://example.com/a")
    second = solaros.http.session_request(handle, "GET", "https://example.com/b")
finally:
    solaros.http.session_close(handle)
handle = solaros.http.stream_open(
    "GET",
    "https://example.com/events",
    None,
    {"Accept": "text/event-stream"},
)
pending = b""
try:
    while not solaros.should_exit():
        event = solaros.http.stream_read(handle, 250)
        if event and event["type"] == "data":
            pending += event["data"]
            while b"\n" in pending:
                line, pending = pending.split(b"\n", 1)
                print(line.rstrip(b"\r"))
        elif event and event["type"] in ("complete", "error"):
            break
finally:
    solaros.http.stream_close(handle)

solaros.net

  • ping(host[, count[, timeout_ms[, interval_ms[, data_size]]]]): ping a host and return transmit/receive statistics.
  • tcp_connect(host, port[, timeout_ms]): open an IPv4 TCP client and return a managed handle.
  • tcp_send(handle, data[, timeout_ms]): send the complete binary buffer.
  • tcp_receive(handle[, max_bytes[, timeout_ms]]): receive bytes, None on timeout, or b"" when the peer closes.
  • udp_open([local_port]): open an IPv4 UDP endpoint; zero or an omitted port selects an ephemeral local port.
  • udp_send(handle, host, port, data[, timeout_ms]): send one complete datagram.
  • udp_receive(handle[, max_bytes[, timeout_ms]]): receive one datagram dictionary or None on timeout.
  • websocket_connect(url[, subprotocol[, timeout_ms]]): connect to a ws:// or certificate-validated wss:// URL and return a managed handle.
  • websocket_send(handle, data[, text[, timeout_ms]]): send one final binary frame, or one final text frame when text is true.
  • websocket_receive(handle[, max_bytes[, timeout_ms]]): receive one frame dictionary or None on timeout.
  • close(handle): close one managed handle.
  • close_all(): close every handle owned by this interpreter run.
  • limits(): return current ownership, usage, limits, and blocking-policy fields.

The TCP, UDP, and WebSocket calls are synchronous. They do not start a worker or invoke callbacks. The connect default is 10000 ms, the send and receive default is 1000 ms, and each call accepts a timeout from 0 through 60000 ms. A receive timeout is a normal None result. Connect, send, cancellation, DNS, TLS, protocol, and other transport failures raise OSError.

Handles belong only to the current Python app or runner invocation. They cannot be shared with Lua, another Python invocation, or a background job. Handles are generation checked, so a closed handle does not become valid when its slot is reused. Normal exit, an exception, cancellation, and forced interpreter cleanup close all remaining handles before the VM is destroyed. Explicit close() in a finally block is still recommended when the script continues after an error.

One interpreter run can hold four TCP, UDP, and WebSocket handles in total. SolarOS allows eight of these script handles globally. A send, receive buffer, or WebSocket frame is limited to 65536 bytes; one UDP datagram is limited to 65507 bytes. limits() reports these constants and the current per-run and global handle counts. Opening past a limit raises an allocation-style OSError without evicting another owner.

TCP and UDP socket waits check cancellation in slices of at most 50 ms. DNS resolution is a platform call and checks cancellation before and after it. WebSocket DNS, TCP/TLS setup, upgrade, and frame I/O use the supplied bounded transport deadline and check cancellation before and after each transport stage; cancellation can therefore take up to that stage's timeout. TCP and UDP use one end-to-end deadline per public operation, including DNS where SolarOS controls it. A WebSocket public call can contain multiple transport stages; each stage is separately bounded by the supplied timeout. Receive polling, reading, and draining share the remaining public-call deadline at the SolarOS layer.

UDP receive dictionaries contain data, address, port, truncated, and datagram_bytes. WebSocket receive dictionaries contain data, type, final, closed, truncated, and frame_bytes. When a datagram or frame is larger than max_bytes, its retained prefix is returned with truncated=True; the rest of that message is discarded so the next receive starts at the next message. WebSocket types are continuation, text, binary, close, ping, pong, or unknown.

These APIs are clients only. They do not expose TCP listen/accept, UDP multicast, custom WebSocket headers, custom certificate stores, or a raw socket object. WebSocket URLs support DNS names or IPv4 hosts, optional ports, paths, and query strings; URL credentials, fragments, and IPv6 literals are rejected.

Example:

import solaros

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

handle = solaros.net.websocket_connect("wss://example.com/events")
try:
    solaros.net.websocket_send(handle, b'{"ready":true}', True)
    frame = solaros.net.websocket_receive(handle, 4096, 5000)
    if frame is not None:
        print(frame["type"], frame["data"])
finally:
    solaros.net.close(handle)

solaros.ftp

The package-gated FTP client uses synchronous, unencrypted IPv4 FTP with passive data connections. Each call connects, logs in, performs one operation, and disconnects:

  • list(host[, path[, username[, password[, port]]]])
  • download(host, remote_path, local_path[, username[, password[, port]]])
  • upload(host, local_path, remote_path[, username[, password[, port]]])
  • mkdir(host, path[, username[, password[, port]]])
  • rmdir(host, path[, username[, password[, port]]])
  • remove(host, path[, username[, password[, port]]])
  • rename(host, old_path, new_path[, username[, password[, port]]])

The defaults are path /, username anonymous, password solaros@, and port 21. list() returns dictionaries with name, is_directory, and size. Local paths use the normal SolarOS storage resolver. Calls raise OSError on DNS, login, protocol, filesystem, cancellation, or transfer failure. FTP does not encrypt credentials or file content; use it only on a trusted network.

import solaros

for item in solaros.ftp.list("fileserver", "/incoming"):
    print(item["name"], item["size"])

solaros.ftp.upload("fileserver", "/notes/todo.txt", "/incoming/todo.txt")

solaros.sftpsync

sync(source, destination[, recursive[, dry_run[, port[, password]]]]) synchronizes one local path with one [user@]host:path endpoint through SFTP. The direction follows the operand order. Local paths use the SolarOS storage resolver. The username defaults to the SolarOS identity, the port defaults to 22, and an empty password allows SSH key authentication.

The call blocks while it transfers, but remains cancellation-aware. It skips regular files whose size and modification time match, never deletes destination-only files, and limits recursive traversal to eight directory levels. Set recursive=True for directories or dry_run=True to report what would change. The returned dictionary contains files_changed, bytes_transferred, and dry_run; failures raise OSError.

import solaros

result = solaros.sftpsync.sync(
    "/notes", "backup@example.com:/srv/terminal/notes", True
)
print(result["files_changed"], result["bytes_transferred"])

solaros.ssh_keys

SSH key functions manage the default SolarOS SSH key pair.

  • default_paths(): return private and public key paths.
  • default_exists(): return whether both default key files exist.
  • status(): return key existence, sizes, and paths.
  • public_key(): return the default OpenSSH public-key line without its newline.
  • generate([bits[, overwrite]]): generate RSA keys.
  • remove(): remove the default 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())

Quick reference

Use solaros.wifi, solaros.mqtt, solaros.http, solaros.net, solaros.ftp, solaros.sftpsync, solaros.ssh_keys for networking. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python storage and files API

API overview · Lua storage and files

Files and imports

open(path[, mode]) opens a file through SolarOS storage. Text mode is the default; add b for bytes. The supported base modes are r, w, a, and exclusive-create x, and each can use + for updating. File objects support read, readinto, readline, readlines, write, seek, tell, flush, and close, and can be used as context managers.

with open("/notes/example.txt", "w") as output:
    output.write("hello from Python\n")

with open("/notes/example.txt") as source:
    print(source.read())

Paths use the same preferred-storage and explicit-mount rules as solaros.storage. They cannot bypass SolarOS mounts to reach an unrelated host filesystem. Long reads and writes remain responsive to app cancellation.

External imports support .py, .mpy, and package directories containing __init__.py or __init__.mpy. A file-run starts its search in the script's directory and then searches the other entries in sys.path. The REPL and source-runner start relative imports at the preferred storage root.

/apps/weather/main.py
/apps/weather/sensors.py
/apps/weather/display/__init__.py

From main.py, both import sensors and import display resolve beside the script. Imported files pass through the same SolarOS path resolver as open().

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

solaros.storage

Storage functions expose SD mount and filesystem service operations.

  • status(): return a human-readable status string for the preferred mounted
  • persistent storage (SD when mounted, otherwise internal flash).

  • is_mounted(): return whether the default storage volume is mounted.
  • mount(): mount the default storage volume.
  • unmount(): unmount the default storage volume.
  • mount_point(): return the preferred mounted persistent-storage path (SD
  • when mounted, otherwise internal flash).

  • usage([path]): return disk usage for the default volume or the volume containing path.
  • resolve(path): return the internal resolved path.
  • read_file(path[, max_bytes]): return up to max_bytes bytes from a regular
  • file. The default is 4096 and the maximum is 65536.

  • rescan(): rescan SD block devices and partitions.
  • blocks(): return a list of block device and partition dictionaries.
  • block_count(): return the number of known blocks.
  • block(index): return one block dictionary.
  • usage_for_block(index): return usage for one mounted block.
  • mkdir(path): create a directory.
  • rmdir(path): remove an empty directory.
  • remove(path): remove a file.
  • rename(old_path, new_path): rename or move a file or directory.
  • copy(source_path, dest_path): copy a file.
  • mount_volume(name[, mount_point]): mount a named block or partition.
  • unmount_volume(target): unmount by volume name or mount point.

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

Quick reference

Use solaros.storage for storage and files. See man python for runtime conventions and service availability.

Scripting APIs · SolarOS manual

Python text user-interface API

API overview · Lua text user interfaces

Use the shared layout on displays and cursor-addressable port shells:

from solaros import tui
_, _, body, _, _, _ = tui.layout()
tui.title("Example")
tui.cell(body[0], 0, body[3], "Shared layout")
tui.help("Enter open  Esc exit")

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:

  • NORMAL
  • BOLD
  • INVERSE

Functions:

  • rows(): return terminal rows.
  • cols(): return terminal columns.
  • size(): return (rows, cols).
  • clear(): clear the terminal.
  • refresh(): atomically commit the buffered TUI frame and flush it.
  • move(row, col): move the terminal cursor.
  • write(text[, attr]): write at the current cursor.
  • addstr(row, col, text[, attr]): move and write text.
  • putch(row, col, ch[, attr]): draw one character or codepoint.
  • hline(row, col, width[, attr]): draw a horizontal line.
  • vline(row, col, height[, attr]): draw a vertical line.
  • vrule(row, col, height[, width[, attr]]): draw a continuous pixel vertical rule.
  • box(row, col, height, width[, attr]): draw a box.
  • fill(row, col, height, width[, ch[, attr]]): fill a rectangle.
  • getch([timeout_ms]): return a key code or None.

Common key constants include KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_CTRL_LEFT, KEY_CTRL_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

Quick reference

High-level: layout, cell, title, help, tab, list_move, input_edit, input. Rectangles are (row, col, height, width). The low-level API remains.

input(row, col, width, label, text, cursor, view[, attr[, masked]]) draws an editable input row. Set masked=True to draw one * per UTF-8 character. The mask is render-only: text and the value returned by input_edit() remain unchanged, so a script must still avoid logging secrets and discard them when they are no longer needed.

Use tui.getch() for keyboard characters and navigation keys. Use solaros.input.read() for foreground touch, mouse, and joystick events.

Scripting APIs · SolarOS manual

Python time and scheduling API

API overview · Lua time and scheduling

Datetime values

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})

solaros.time

Time functions use the SolarOS RTC/time service.

  • uptime_ms(): return uptime in milliseconds.
  • sleep_ms(ms): delay for up to one hour while remaining responsive to script cancellation.
  • uptime(): return formatted uptime text.
  • datetime(): return the local wall-clock datetime.
  • utc_datetime(): return UTC datetime.
  • set_datetime(datetime): set the local wall-clock datetime.
  • set_utc_datetime(datetime): set the UTC wall-clock datetime.
  • utc_to_local(datetime): convert UTC datetime to local time.
  • local_to_utc(datetime): convert local datetime to UTC.
  • is_valid(datetime): validate a datetime.
  • timezone(): return {"name": ..., "posix": ...}.
  • set_timezone(timezone): set timezone by SolarOS-supported name or POSIX TZ string.
  • ntp_sync([server[, timeout_ms]]): sync wall-clock time from NTP and return {"utc": ..., "local": ...}.

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

Direct access to optional RTC alarm and countdown hardware:

  • status(): return availability, provider, capabilities, interrupt GPIO and active level, and current slot owners.
  • set_alarm(hour, minute[, second[, day[, weekday]]]); clear_alarm().
  • set_timer(seconds[, repeat]); clear_timer().
  • pending(); ack(mask), using INTERRUPT_ALARM and INTERRUPT_TIMER.

Direct slots are leased to the Python runtime and released when it exits. A busy error means the scheduler or another client owns that hardware slot.

solaros.schedule

Named schedules remain available after the creating script exits:

  • list() returns entry dictionaries.
  • add_in(name, seconds[, action[, value[, persistent]]]).
  • add_every(name, seconds[, action[, value]]).
  • add_at(name, year, month, day, hour, minute, second[, action[, value]]).
  • add_daily(name, hour, minute, second[, action[, value]]).
  • add_weekly(name, weekday_mask, hour, minute, second[, action[, value]]).
  • enable(name, enabled), remove(name), run(name), and stop_alarm().

The action is "alarm" or "run"; a run action requires an absolute shell script path. Combine SUN through SAT with bitwise OR for weekly schedules.

Quick reference

Use solaros.time, solaros.rtc, solaros.schedule for time and scheduling. See man python for runtime conventions and service availability.

System services · SolarOS manual

Cellular modem

SolarOS exposes cellular configuration through a common modem service. Each hardware driver translates the common profile into its own command set. The Waveshare ESP32-S3-SIM7670G-4G V2.0 registers its SIM7670 as modem0 on modem-uart. The UART starts at the module's 115200-baud boot rate, then the driver defaults to auto, which temporarily raises both ends to the SIM7670 maximum of 460800 baud. It verifies the selected rate after every power-on or reset and falls back to the boot rate if negotiation fails; the module's persistent IPREX setting is not changed. Show or persist another supported rate while packet data is disconnected:

modem baud modem0
modem baud modem0 230400
modem baud modem0 auto

Baud selection is an optional capability of the common modem service. USB and other non-serial modem transports do not expose it.

Open the modem TUI to inspect status, control power/data, and edit the saved profile and optional baud setting. Use the textual commands when scripting or diagnosing:

modem
modem list
modem status

Use Tab to switch between Status and Settings. The arrow keys select settings and change enumerated values; Enter edits text or runs the selected action. Profile fields remain a draft until save profile is selected. Password values are represented only as set or none, and password input is masked.

If the driver has a hardware power or reset binding, control it without using module-specific AT commands:

modem power off modem0
modem power on modem0
modem reset modem0

modem list reports whether power and reset control are available. On the Waveshare V2.0 board, set CAM OFF, HUB ON, 4G OFF, and USB OFF. SolarOS owns the active-high GPIO21 modem rail when the 4G switch is OFF. The module RESET pin is not routed to the ESP32, so modem reset performs a full rail power cycle. An expansion attachment can instead provide both a power line and a dedicated active-low reset line.

Create and save a packet-data profile without entering AT commands:

modem profile set modem0 --apn internet --ip ipv4v6 --auth none
modem profile show modem0

Use --dns <ipv4> when the SIM or APN requires a specific resolver. Omit it, or use --dns auto, to accept the DNS server negotiated by PPP. For example:

modem profile set modem0 --apn internet --dns 8.8.8.8 --ip ipv4v6 --auth none

The profile stores the APN, optional IPv4 DNS override, IP family, authentication mode, and optional username/password in NVS. profile show never prints the password. PAP and CHAP require both --user and --password; none and auto reject explicit credentials. auto asks the backend to use its default authentication policy. The SIM7670 backend currently resolves auto to no authentication.

Bring the saved cellular connection up or down:

modem connect modem0
modem status modem0
modem disconnect modem0

modem connect reapplies the saved profile, attaches packet service, dials the SIM7670 data connection, and waits for its PPP interface to obtain an IPv4 address. The cellular interface becomes the preferred route for new SolarOS traffic while it is connected. modem status reports network=up and prints the interface, IPv4 address, gateway, and DNS server. The Waveshare target accepts an ipv4v6 modem profile but currently brings up IPv4 over PPP.

The SIM7670 UART is exclusive. While PPP is connected, SolarOS uses that UART for network frames, so raw modem at commands, SIM unlock, profile changes, and GNSS operations return ESP_ERR_INVALID_STATE. modem status remains available using the registration and signal values cached immediately before dialing. Run modem disconnect to stop PPP and return the UART to AT/GNSS operation. Concurrent PPP and AT/GNSS access would require a later CMUX stage.

Remove both the stored profile and the modem-side context configuration:

modem profile clear modem0

If a SIM is waiting for its PIN, unlock it separately:

modem sim unlock 1234 modem0

The PIN is sent to the modem but is not stored by SolarOS. Shell commands can remain in terminal history, so clear sensitive command history after entering a PIN or APN password.

Raw AT access remains available for diagnostics. Quote the command as one shell argument; the optional device name and timeout follow it:

modem at "AT+CGMM"
modem at "AT+COPS?" modem0 10000

The command accepts only text that starts with AT and rejects embedded CR or LF characters. It prints the modem response, including an ERROR, before it prints the SolarOS error name.

The modem also registers a GNSS receiver with the common service. Turn it on before requesting a fix:

gnss power on modem0
gnss fix modem0 5000
gnss power off modem0

gnss status [name] reports the selected logical receiver's power, fix, fix type, and satellite state. gnss fix reports latitude, longitude, altitude, and UTC when the receiver has a valid fix. The SIM7670 backend uses AT+CGNSSINFO, including its fix mode and satellite count. Some SIM7670 firmware omits the satellite fields until it has a fix; SolarOS reports that state as satellites=unknown, not as a measured count of zero.

Because this SIM7670 contains both functions, gnss power on enables the shared modem rail before it starts the GNSS engine, and gnss power off shuts that rail down, disconnecting an active cellular/PPP session first. Modem power-off or reset also marks the co-located GNSS receiver off. Turning modem power back on does not start the GNSS engine; use gnss power on again. This relationship belongs to the combined SIM7670 driver only. A separately registered modem and GNSS receiver have independent power state and controls.

The modem profile API and PPP network service are shared. The PPP service owns network negotiation, authentication, DNS, routing, and interface state over a generic byte-stream transport. The SIM7670 adapter supplies UART reads and writes plus its modem-specific packet attach, dial, escape, and hang-up steps. Other PPP users can supply another pull-based byte stream or feed received frames from a push-based transport without depending on the modem service.

Quick reference

modem list shows registered devices, concrete drivers, and power, reset, and baud capabilities. modem status [name] reads power, SIM, LTE registration, signal, packet-context, and IP-interface state. modem power on|off [name] controls an optional hardware rail and modem reset [name] uses the driver's reset method. modem baud [name] [auto|rate] manages an optional serial transport rate. modem profile set|show|clear manages persistent cellular settings. modem connect|disconnect brings the PPP network interface up or down. modem sim unlock enters a SIM PIN. modem at remains an advanced diagnostic escape hatch. Use gnss status [name], gnss power on [name], gnss fix [name] [timeout-ms], and gnss power off [name] for the integrated SIM7670 GNSS receiver.

System services · SolarOS manual

Device identity

The device user and hostname are stored in NVS, so they do not depend on an SD card. The shell prompt uses both values. Gateway Chat uses both values when it connects, and network services use the hostname when advertising the device.

Inspect and change identity

From the shell:

identity status
identity user nils
identity hostname solarterm

From Python:

import solaros

print(solaros.identity.format())
solaros.identity.set_hostname("solarterm")

If Wi-Fi is already initialized, reboot before expecting every advertisement or DHCP hostname to use a changed value.

If Gateway Chat is already connected, run gateway connect to reconnect with the changed identity.

Quick reference

solaros.identity provides user(), hostname(), set_user(name), set_hostname(name), and format(). Values are NVS-backed. Reboot before expecting an already initialized Wi-Fi interface to advertise a changed hostname.

System services · SolarOS manual

Foreground sessions and applications

A session is a foreground application or shell attached to a display or byte stream. Each display or port has one active view, but can retain several suspended applications. Sessions let you switch away and return without discarding their state.

Inspect and switch

session list
session fg 3
session send 3 wifi status
session close 3

session send ID COMMAND runs a command on an active display shell from any other shell. The command is echoed and added to the target shell's history, and its output stays on the target display. SolarOS refuses the request if the target is an application, is suspended, has a partially typed command, or is the calling shell.

Alt+Tab or Alt+Right switches to the next session on the locally focused display. Alt+Left switches to the previous session. Either Alt key is accepted, including AltGr on compact keyboards. Both directions follow wrapped session-ID order, and the display shell remains in the cycle. session fg explicitly foregrounds its session and moves local input focus to that session's display. Closing an application returns to that display's shell.

On UART, USB CDC, Telnet, and other port shells, press Ctrl+Z to suspend a resumable foreground application and return to the prompt. Run fg to restore the most recently suspended application on that port, or fg ID to restore a specific port-owned session. Ctrl+] still closes the current application. Port sessions never migrate: a command from another shell can foreground or close one, but it changes the application shown on the owning port.

Applications currently remain single-instance across the device. If an app is already retained on another display or port, SolarOS reports its session owner instead of creating a second copy.

SolarOS always keeps at least one interactive shell. exit, close, and session close refuse to close the final remaining display or port shell.

Native applications receive cooperative tick events every 25 ms by default. An application that needs a faster best-effort update rate can set its descriptor's tick_interval_ms below 25; the runtime raises its scheduler cadence while that application is active. A zero value keeps the default.

Create another shell

session create shell cdc0 --term auto
session create shell uart0 --term ansi --charset ascii --size 80x25
session create shell lcd0

Use port list or display list to discover real targets first. A manually created port session does not rerun /.shell/startup. A display shell is created on its target without changing local input focus; use session focus when the BLE keyboard or board controls should move to it.

Terminal control and character encoding are separate. --term selects cursor and screen-control behavior. Port shells use --charset utf8 by default; add --charset ascii for DOS or another legacy terminal so TUI line art and symbols are rendered with ASCII fallbacks. The setting does not change framebuffer display sessions.

Cursor-addressable port shells echo ordinary end-of-line typing directly and send required line edits as one redraw. This keeps the cursor stable on UART, USB CDC, and network-backed terminals such as Telnet.

Start an app on a display

An app can be launched onto a named display from any shell:

session create files display0
session create reader oled0 /manual.md
session create writer display0 /notes.md

The invoking port shell immediately returns to its own prompt. The new app receives input belonging to its display; serial input remains with the serial shell. App arguments use the invoking shell's current directory for the same path-aware apps as a direct launch.

This cross-shell display control remains available while a port shell retains its own applications. session close ID can close either kind of session.

Child applications

Foreground applications can temporarily open another app on the same terminal. Files uses this for Reader, Less, Edit, and View; Playground uses it to run an installed Python or Lua application. The parent is suspended while the child is active. Closing the child resumes the parent. Suspending the child with Ctrl+Z returns to the shell and retains both sessions for later fg.

Local input focus

Keyboard, board-button, and D-pad key events share one local display focus. Pointer events without an explicit target use that focus too. Analog joystick axes remain semantic axis input and are not converted to focus keys. The primary board display is selected at boot. On a headless board, the first display shell created becomes the default.

session focus
session focus oled0

Changing focus does not move or restart a session. It only assigns local input and target-scoped session-cycle shortcuts. Browser input from displayd remains assigned to the display being controlled.

Quick reference

solaros.sessions.create_shell(port, optional term, cols, rows, charset) returns a session id; close(id) closes it. The Lua options table and Python keyword form also accept charset="utf8" or charset="ascii". Script-created port shells do not run /.shell/startup. solaros.apps.list() and find(name) inspect registered foreground apps.

System services · SolarOS manual

MeshCore companion messaging

SolarOS implements a non-forwarding MeshCore companion node. It exchanges signed adverts, end-to-end encrypted direct messages, acknowledgements, and shared-key group messages through the provider-neutral Contacts and Messages services. It can also carry a peer-bound SolarOS virtual serial port through ordinary encrypted MeshCore direct packets. SolarOS does not repeat traffic or include MeshCore's Arduino interface, companion protocol, room server, remote administration, sensors, or telemetry. SolarOS Link remains a separate protocol; the virtual serial feature only reuses its reliable stream framing.

Quick start

Attach a packet-capable LoRa radio and start the worker with an explicit regional profile:

expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5
job start meshcore radio0 meshcore-eu868
meshcore status
chat

meshcore-us915 is the MeshCore "USA/Canada (Recommended)" preset: 910.525 MHz, 62.5 kHz, SF7, coding rate 4/5. meshcore-eu868 is 869.618 MHz, 62.5 kHz, SF8, coding rate 4/8, a 32-symbol preamble, sync word 0x12, CRC, variable packet length, and 14 dBm. It is an EU868 profile; do not use it outside regions where that frequency and transmit behavior are legal. The profile argument is mandatory so SolarOS never silently chooses a region.

The job claims the radio, applies the complete profile, enters receive mode, and emits one zero-hop advert. It restores the previous radio configuration and state and releases ownership when stopped or when startup fails. Starting radio-link on the same radio fails with the normal ownership error.

job status meshcore
job stop meshcore

Identity, name, and adverts

The first start generates an Ed25519 identity from the SolarOS RNG and stores it as an opaque Credentials record. A MeshCore-specific advertised-name override is optional; without one, SolarOS uses the truncated device user@hostname identity.

meshcore identity show
meshcore identity generate
meshcore identity generate --force
meshcore identity import PRIVATE_KEY_HEX
meshcore identity export --private
meshcore name
meshcore name field-unit
meshcore advert zero
meshcore advert flood

Generating with --force, importing, or editing channels is rejected while the job runs. Private export is deliberately explicit and prints a warning. Treat the output as a password: current NVS is not encrypted, and physical flash access can recover the stored identity and group keys.

No periodic advert is sent. meshcore advert zero reaches only local receivers; meshcore advert flood deliberately requests a network-wide flood.

Contacts, trust, and messages

A valid advert proves that its sender controls the advertised public key. It does not prove the person's identity, so the endpoint enters Contacts as discovered. Review it with contacts show, then use contacts trust or contacts block. Blocked endpoints cannot send or receive direct messages.

Direct messages use MeshCore's public-key/ECDH path. An outbound message waits for its cryptographic acknowledgement and retries twice after the initial attempt. A valid ACK marks it delivered; the final timeout marks it failed. Discovered recipients require Chat confirmation or messages send ... --allow-untrusted.

The standard Public group is enabled by default. Group messages are encrypted with a shared key but their embedded sender name is not authenticated, so Chat always labels them sender-unverified. A group transmission becomes sent after the radio finishes because groups have no recipient ACK.

meshcore channel list
meshcore channel public off
meshcore channel public on
meshcore channel add '#hansemesh'
meshcore channel add team BASE64_PSK
meshcore channel remove team

There are eight total group slots including Public. Custom keys must decode to 16 or 32 bytes and remain confined to Credentials; they are not shown by Inbox, logs, autocomplete, agent tools, Python, or Lua.

A channel name that starts with # is a public hashtag channel. SolarOS derives its 16-byte key from the first 16 bytes of the SHA-256 digest of the exact channel name, including the leading #. Hashtag names are case-sensitive and do not take a pre-shared-key argument. Anyone who knows or guesses the name can derive the same key, so hashtag channels are not private. Use the explicit Base64 key form only for private channels without the # prefix.

Reliable virtual serial ports

A MeshCore stream is a normal SolarOS byte-stream port carried in encrypted MeshCore direct-request packets. Standard MeshCore repeaters can route those opaque packets between two SolarOS endpoints; the repeaters do not need SolarOS support. Create the stream explicitly on both devices and bind each side to the other device's trusted MeshCore endpoint.

First start MeshCore on both devices. Use flood adverts when the endpoints are not in direct radio range, then review and trust the discovered endpoint on each side:

job start meshcore radio0 meshcore-eu868
meshcore advert flood
contacts list
contacts show <contact-id>
contacts trust <contact-id> <endpoint-id>
meshcore stream create mser0 <endpoint-id>
meshcore stream status mser0

Endpoint IDs are local database IDs and can differ between the two devices. The endpoint must be a trusted MeshCore endpoint with the exact peer public key. A discovered, blocked, group, or changed identity is rejected. Configure only one stream for a peer identity.

For example, expose a shell on the internet-connected ground station:

session create shell mser0 --term dumb

On the pocket terminal, open it as a serial connection:

com mser0

To expose another machine instead, connect its serial interface to the ground station and bridge the UART port:

job start bridge uart0 mser0

The stream is a slow, stop-and-wait serial link, not an IP or Telnet tunnel. It carries up to 82 data bytes per MeshCore packet, uses 2048-byte transmit and receive queues, retries after 12 to 16 seconds, and treats a peer as absent after 180 seconds. Interactive command shells are practical; full-screen TUIs, bulk transfers, and chatty terminal negotiation are poor fits. Use the dumb terminal mode where possible.

At most two Link-backed streams can exist at once, including MeshCore streams. Stream definitions remain in RAM if the MeshCore job stops and reconnect after it starts again, but they do not persist across a reboot. Remove an unclaimed port with meshcore stream remove <port>.

Implementation and limits

The complete provider context and its 16-packet pool use external-required PSRAM. Runtime measurement tuned the worker to a 7168-byte internal stack. meshcore status reports packet usage, traffic, retries, duplicate counts, whether the context is in PSRAM, and the measured minimum stack watermark. Firmware packages omit MeshCore unless the board has PSRAM and packet-radio expansion capability.

SolarOS vendors the audited protocol subset from MeshCore commit 03b6ef4b0de98fc70b49ef10a6d0d61f8381fb7a. Updates are explicit source reviews; firmware builds never download protocol code.

Quick reference

meshcore status
meshcore identity show
meshcore identity generate [--force]
meshcore identity import <private-key-hex>
meshcore identity export --private
meshcore name [name]
meshcore advert zero|flood
meshcore channel list
meshcore channel add <#hashtag>
meshcore channel add <name> <base64-psk>
meshcore channel remove <name>
meshcore channel public on|off
meshcore stream list
meshcore stream status [port]
meshcore stream create <port> <trusted-endpoint-id>
meshcore stream remove <port>
job start meshcore <radio> <profile>
job status meshcore
job stop meshcore

System services · SolarOS manual

Messaging identities and security

SolarOS separates messaging identity from transport. A contact is one local address-book entry, while each gateway, MeshCore, or SolarOS Link address is an endpoint with its own trust and capabilities. Linking endpoints does not copy trust between providers.

Trust has three states:

  • discovered: a provider supplied a valid address or advert;
  • trusted: the user explicitly accepts that endpoint identity;
  • blocked: direct inbound and outbound messaging is denied.

A signed MeshCore advert proves ownership of its public key, not the human identity using the device. New adverts therefore remain discovered until the user trusts them.

Contacts keeps at most 64 contacts and 80 endpoints in PSRAM. Its bounded, CRC-checked store uses alternating headers and data copies under /.contacts/contacts.bin. Persistence writes and fsync occur after releasing the service lock. If the filesystem is unavailable, the service stays usable in volatile mode and reports the error through contacts status.

Credentials keeps at most 12 opaque NVS records with at most 128 secret bytes each. Supported record kinds are asymmetric identities, shared keys, and tokens. Public listing exposes only the record ID, provider, kind, and label. Secret reads name one record and copy into a caller-owned buffer; temporary buffers are wiped.

Credentials are never exposed through Inbox, logs, autocomplete, native-agent tools, Python, or Lua. The current SolarOS NVS configuration is not encrypted, so someone with physical flash access can recover the stored secrets. Flash and NVS encryption provisioning are outside this release.

SSH keys, native-agent API keys, and existing provider tokens remain in their current stores and are not migrated automatically.

See contacts for the TUI and mutation commands. chat is the unified conversation UI and works without Wi-Fi; provider jobs connect transports when available. The messages shell API exposes the same bounded conversation store. Stable 64-bit message IDs are printed as hexadecimal strings so they survive shell and scripting runtimes without precision loss.

Direct messages to a blocked endpoint are always rejected. A discovered endpoint requires an explicit second confirmation in Chat or --allow-untrusted in the shell. That opt-in applies only to the one send and does not change the endpoint's trust state.

Quick reference

contacts
contacts status
contacts list [all|discovered|trusted|blocked]
contacts show CONTACT_ID
contacts rename CONTACT_ID NAME
contacts trust CONTACT_ID [ENDPOINT_ID]
contacts block CONTACT_ID [ENDPOINT_ID]
contacts remove CONTACT_ID
contacts link TARGET_CONTACT_ID SOURCE_CONTACT_ID
messages status
messages conversations
messages list CONVERSATION_ID
messages send CONVERSATION_ID TEXT [--allow-untrusted]
messages read CONVERSATION_ID
messages delete MESSAGE_ID
messages clear gateway|meshcore|link|all
messages outbox
messages cancel MESSAGE_ID
outbox [list]
outbox cancel MESSAGE_ID

The three user-facing collections have distinct roles: Conversations contain retained message history, Inbox is a universal notification view of inbound items from messaging and other applications, and Outbox contains only pending sends. Provider packet queues and event rings are internal bounded transport mechanisms, not additional mailboxes. On internal flash, Messaging physically shares Inbox's compact persistent records but still owns the conversation API.

messages clear also removes the selected provider's Inbox projections, including stale projections whose retained ring slot has already wrapped. It does not remove unrelated Inbox sources; use inbox clear for the entire universal Inbox.

For packet-radio operation, see meshcore and link. MeshCore direct messages use the same conversations and trust states as gateway messages; shared-key groups remain visibly sender-unverified. A radio-link or espnow-link job started with chat=on adds unencrypted, packet-sized Link broadcast and direct conversations and discovers source device IDs as Contacts.

System services · SolarOS manual

SolarOS Link

SolarOS Link is a small transport-independent message layer for packet-sized connections. The Link service owns framing, protocol CRC, sequence numbers, acknowledgements, duplicate suppression, and bounded receive/transmit queues. A transport adapter moves complete Link frames over a specific medium.

The base packet layer deliberately does not provide routing, fragmentation, encryption, or mesh forwarding. The packet-radio adapter can optionally repeat each frame by one hop. One text or binary message must fit the selected transport MTU. The optional Link stream layer segments a byte stream into those packets and adds ordered delivery, retransmission, and backpressure.

Radio Quick Start

Attach a packet radio, then start the adapter with a complete radio profile:

expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5
job start radio-link link0 radio0 lora-eu868
link status link0
link send link0 broadcast "hello"

To copy received text messages into the universal inbox:

job start radio-link link0 radio0 lora-eu868 inbox=on

inbox=off is the default. Accepted text and binary messages remain available through the bounded Link receive queue in either mode; inbox=on additionally publishes accepted text messages to the inbox. When this diagnostic queue is full, Link evicts its oldest queued copy while continuing to deliver the newest accepted frame to Inbox or Chat. Read one queued message with:

link receive link0
link receive link0 1000

The optional timeout is in milliseconds and is limited to 1000 in the shell.

To use Link text in the unified Chat and Messages interfaces instead:

job start radio-link link0 radio0 lora-eu868 chat=on
chat

Chat shows a link provider section with a link0 broadcast conversation. Each received 32-bit source ID creates a discovered Contact and direct conversation. Rename, trust, or block it with the normal contacts commands. The Chat projection observes accepted frames without consuming the Link receive queue, so link receive remains available as a bounded recent-frame diagnostic. Its capacity does not limit Chat delivery. chat=on and inbox=on are mutually exclusive because generic messaging already publishes received Chat messages to Inbox.

ESP-NOW Quick Start

Start the ESP-NOW adapter on two SolarOS devices using the same channel:

job start espnow-link link0 channel=6 phy=lr500
link status link0
link send link0 broadcast "hello"

The default channel=auto follows an active station or access-point channel. When neither exists, it starts on channel 6. A fixed channel prevents a new station connection or an AP on another channel. Wi-Fi scanning is rejected while ESP-NOW is active because a scan leaves the transport channel. wifi off stops station and AP networking but retains the radio while espnow-link owns its connectionless lease.

The default phy=normal uses the normal ESP-NOW PHY selection. Use phy=lr500 or phy=lr250 on every participating SolarOS device to select Espressif's proprietary 500 kbit/s or 250 kbit/s Long Range PHY. LR mode trades throughput for receive sensitivity and range. SolarOS enables LR receive support for the lifetime of the job, assigns the selected rate to configured and newly learned peers, and restores the previous Wi-Fi protocol selection on stop. espnow status reports the active PHY.

An accepted incoming frame learns its Link source-ID-to-MAC mapping for the current boot. This permits unicast replies after the peer has sent a frame. Add a persistent mapping when this device must initiate unicast after boot:

espnow peers
espnow peer add 0x12345678 24:6f:28:11:22:33
espnow peer remove 0x12345678

There are 19 peer slots. Configured peers are stored in NVS; learned peers are volatile and the oldest learned entry is evicted when necessary. SolarOS rejects a learned mapping that conflicts with an existing source ID or MAC instead of silently redirecting traffic.

Commands

CommandDescription
`link status\list`List active Link instances and their queue/protocol counters.
link status <link>Show one Link instance, local ID, MTU, queue depths, ACK state, duplicates, CRC errors, and drops.
`link send <link> <broadcast\destination-id> <text>`Queue one text message. Unicast requests an acknowledgement.
`link send-binary <link> <broadcast\destination-id> <byte...>`Queue one binary message from decimal or 0x byte values.
link receive <link> [timeout-ms]Remove and print the oldest accepted text or binary message.
link stream listList Link-backed virtual serial ports.
link stream status [port]Show peer, connection, MTU, queues, traffic, retry, reconnect, drop, and error state.
link stream create <link> <port> <peer-id>Register a peer-bound Link stream as a normal bidirectional SolarOS port.
link stream remove <port>Remove an unclaimed Link stream port.

Destination IDs accept decimal or 0x notation. broadcast is the reserved destination 0xffffffff. link status prints this device's stable 32-bit local ID as hexadecimal; it is derived from the ESP32 base MAC.

The displayed frame MTU comes from the active transport and radio profile. The payload limit is the MTU minus the 12-byte header and 2-byte protocol CRC. For example, the 255-byte LoRa packet profile carries at most 241 Link payload bytes, a 64-byte FSK profile carries at most 50, and the 250-byte ESP-NOW transport carries at most 236.

radio-link Job

Usage:

job start radio-link <link> <radio> <profile> [inbox=off|on] [chat=off|on] [repeater=off|on]
job status radio-link
job stop radio-link

The job claims the radio as job:radio-link, applies the named radio profile, creates the Link instance, and continuously alternates queued transmission and packet reception. While it runs, mutating radio config, radio state, radio send, radio recv, and profile-apply operations are rejected; read-only radio status remains available and shows the owner.

Stopping the job destroys its Link queues, restores the radio configuration and state that existed at startup, and releases the radio. Only one instance of the radio-link job can run at a time, matching the normal SolarOS job registry.

repeater=on makes the device a one-hop store-and-forward range extender on the selected radio and profile. It repeats valid Link frames that are not addressed to the local device, including broadcasts, acknowledgements, and virtual-stream traffic. The forwarded copy preserves the original source, destination, sequence, type, and payload. A Link header flag marks that copy as already relayed, so another repeater does not forward it again.

Each repeater waits for a short randomized interval before transmission. If it hears another repeater forward the same frame first, it suppresses its pending copy. A direct acknowledgement also cancels the corresponding pending text or binary frame while the acknowledgement itself remains eligible for repeating back toward the sender. Recent-frame caching and a bounded four-frame relay queue prevent loops and unbounded memory use. Inspect its counters with job status radio-link:

job start radio-link link0 radio0 lora-eu868 repeater=on
job status radio-link

Repeater mode does not make the device a relay-only endpoint. It can participate in Chat at the same time:

job start radio-link link0 radio0 lora-eu868 chat=on repeater=on

Direct traffic addressed to this device is consumed locally and is not repeated. Broadcast traffic is consumed locally and repeated, while traffic addressed to other Link IDs is repeated without being delivered locally. The same Link instance can also own a peer-bound virtual stream created with link stream create link0 vser0 <peer-id> while it repeats stream frames between other devices.

Repeating doubles the packet-radio airtime used for traffic that crosses the repeater and increases acknowledgement latency. All devices must use the same frequency, modulation, packet profile, and Link framing. Repeater mode does not add encryption or authentication; it forwards any structurally valid Link frame received on that profile. It is off by default.

The Link queues have four entries each and use PSRAM when available. They are created only when a Link starts, so the compiled service has no idle queue allocation. Receive-queue overflow evicts the oldest queued copy and increments the drop counter rather than rejecting the accepted frame or growing without bound. Transmit-queue overflow still rejects a new send.

With chat=on, outgoing broadcast text becomes sent after the radio accepts the frame. Direct text remains sending until the matching Link acknowledgement arrives and becomes failed after ten seconds without one. There is no automatic retry. Text that exceeds the active Link payload MTU fails with the exact byte limit. Link contacts remain discovered until explicitly trusted, and Link messages carry no encrypted or transport-secured security flag. Each messaging-adapter session scopes Link source/sequence identities with a fresh local epoch, so restarting radio-link does not make new packets collide with retained messages from the previous sequence cycle.

espnow-link Job

Usage:

job start espnow-link <link> [channel=auto|1..13] [phy=normal|lr500|lr250] [inbox=off|on] [chat=off|on]
job status espnow-link
job stop espnow-link

The job acquires the Wi-Fi radio through the connectionless lease, initializes ESP-NOW, creates a Link instance with a 250-byte frame MTU, and moves complete frames between the two services. Inbox and Chat behavior is the same as for radio-link; both options default to off and cannot be enabled together. The PHY defaults to normal; every LR peer must enable LR reception. Use the same lr500 or lr250 mode at both ends for symmetric throughput.

The transport state and peer table live in PSRAM. Its bounded four-frame receive queue, two-entry send-completion queue, and 6144-byte time-critical worker stack are allocated only while the job runs and released when it stops. A stopped compiled service therefore has no queues or worker stack reserved for a future ESP-NOW session.

radio-link and espnow-link can run at the same time with different Link names. Only one can use chat=on because the Link messaging projection is a single provider. Direct Link queues and inbox=on do not share that provider.

ESP-NOW frames are unencrypted in this release. Link IDs and learned MAC mappings are not cryptographic identities, so peers can be spoofed. Do not expose a privileged Link stream over an untrusted ESP-NOW channel.

Version 1 Frame

All multi-byte values use network byte order:

OffsetSizeField
01Version in the high nibble, flags in the low nibble
11Message type
22Sequence number
44Source ID
84Destination ID
12variablePayload
final 22CRC-16/CCITT-FALSE over header and payload

Message types are 1 text, 2 binary, 3 acknowledgement, and 4 stream. Unicast text and binary frames set the acknowledgement-requested flag. An acknowledgement has no payload, swaps source/destination, and echoes the acknowledged sequence number. Broadcast frames never request acknowledgements, avoiding an ACK storm. Stream frames use their own ordered acknowledgement and retry protocol and therefore do not request the base Link acknowledgement.

The receiver remembers the 12 most recent source/sequence/type tuples. A duplicate is not delivered or copied to the inbox, but is acknowledged again when requested so a sender can recover from a lost ACK. Up to eight outstanding unicast sequence/destination pairs are tracked for status; version one does not automatically retransmit an unacknowledged frame.

The protocol CRC is present even when the transport also supplies a hardware CRC. This keeps integrity checking consistent across packet radio, serial, infrared, or future transports.

Serial byte bridge

The existing bridge job can connect one bidirectional byte-stream port to an active Link:

job start radio-link link0 radio0 lora-eu868
job start bridge uart0 link0 broadcast

Serial input is sent as binary Link messages capped to the active payload MTU. Received text and binary payloads are written back to the serial stream without separators. Replace broadcast with a decimal or 0x destination ID for acknowledged unicast. The bridge claims the serial port and consumes the Link receive queue until stopped.

This is a bounded, best-effort stream adapter rather than Link fragmentation or flow control. Packet radio can be much slower than a serial producer, so sustained input can overrun the serial driver or the four-entry Link queue.

Virtual serial ports

For an ordered interactive stream, create the same peer-bound Link stream on both devices. The stream name becomes a normal SolarOS byte-stream port and is shown by both link stream list and port list:

# Device A; Device B reports local ID 0xde63d29e
job start radio-link link0 radio0 lora-eu868
link stream create link0 vser0 0xde63d29e

# Device B; Device A reports local ID 0x7b1fdb02
job start radio-link link0 radio0 lora-eu868
link stream create link0 vser0 0x7b1fdb02

The port is initially closed. It starts negotiating when a normal SolarOS consumer claims it. To expose a Waveshare shell over the radio:

# Waveshare
session create shell vser0 --term dumb

# DevKit ground station with display and keyboard
com vser0

# Or bridge a Linux USB shell through the headless DevKit
job start bridge cdc0 vser0

Claiming the virtual port with com, session, or bridge opens the stream automatically; there is no separate stream-open command. Linux can open the DevKit USB CDC serial device after starting the bridge and interact with the Waveshare's normal port shell. On a headless DevKit, uart0 remains available for local administration while cdc0 is owned by the bridge. --term dumb avoids expensive terminal probes and escape traffic; VT100 remains available when the radio profile has enough throughput.

Each direction uses a fresh random session epoch whenever the port opens, 16-bit byte-frame sequence numbers, ordered stop-and-wait delivery, piggybacked stream acknowledgements, and jittered retransmission after 0.8 to 1.2 seconds. Stream data is split to the active Link MTU and reassembled into a 2048-byte RX queue. A separate 2048-byte TX queue applies backpressure to the shell or bridge instead of silently overflowing the four-entry packet queue. Fifteen-second open frames also detect a disconnected or restarted peer.

Stream protocol v2 carries an acknowledgement field in every frame. Pending acknowledgements are piggybacked on outbound data, and a standalone ACK is delayed by 40 milliseconds to give an interactive port shell time to queue its echo first. A normal keystroke and its echo therefore need two radio frames on the fast path instead of waiting for a separate ACK between them.

Stream packets are dispatched before the normal Link receive queue, so they do not appear in link receive and cannot evict Chat or diagnostic packet copies. Only packets whose Link source matches the configured peer ID enter the virtual port. The peer binding prevents accidental cross-talk, but Link IDs can be spoofed: Link streams are not encrypted or cryptographically authenticated. Do not expose a privileged shell over an untrusted radio channel.

Close every shell or bridge that owns the port before removing it:

session close <id>
link stream remove vser0

Stopping and restarting radio-link or espnow-link with the same Link name leaves the virtual port registered. Buffered users see a disconnected stream until the transport returns; the stream epochs then resynchronize without reusing stale bytes.

Quick reference

Start a packet-radio link with job start radio-link link0 radio0 lora-eu868 [inbox=off|on] [chat=off|on] [repeater=off|on]. Use link status link0, link send link0 broadcast "text", and link receive link0. Use chat=on for unified Link broadcast/direct conversations and discovered Contacts. Use link stream create link0 vser0 PEER_ID when a reliable virtual serial port is required. For ESP-NOW, use job start espnow-link link0 [channel=auto|1..13] [phy=normal|lr500|lr250] and configure cold-start unicast peers with espnow peer add. Unicast packet messages request acknowledgements. The base packet layer has no routing, fragmentation, encryption, mesh forwarding, or automatic retransmission; radio-link repeater=on adds one-hop packet repetition, and the peer-bound stream layer adds segmentation, ordering, retransmission, and backpressure for byte-stream consumers.

Boards and firmware · SolarOS manual

Defining SolarOS Boards

New board profiles use one versioned TOML manifest in boards/manifests/<target>.toml. The manifest contains identity, MCU family, build capabilities, buses, pins, connector metadata, and automatic fixed expansion devices. CMake generates its private board configuration and C header from that file. Do not edit generated files in .pio/build/.../generated.

boards/expansion_drivers.toml is the desktop configuration catalog. It states which MCU families, packages, board capabilities, and wiring bindings each expansion driver supports. The same catalog is used by the board compiler and the configuration TUI.

All built-in boards use manifests. A compatibility loader remains for out-of-tree legacy boards/<target>.cmake profiles, but new and maintained profiles must use TOML. Runtime capability bits remain derived from the generated SOLAR_OS_BOARD_HAS_* defines.

scripts/validate_board_metadata.py checks every board during CMake configuration. It verifies board registration and identity, capability registry coverage and dependencies, pin masks against their named lists and free-pin slots, static bus protocol gates, and the GPIO/bus tables in expansion.reference.md. Run it directly after changing board metadata:

python3 scripts/validate_board_metadata.py

File Layout

The source and generated paths are:

boards/manifests/<target>.toml                 source board profile
boards/expansion_drivers.toml                  selectable driver catalog
scripts/board_config.py                        desktop TUI
scripts/os_builder.py                          flavor build/flash TUI
scripts/generate_board_profile.py              manifest validator/compiler
.pio/build/<env>/generated/solar_os/            generated CMake and C metadata

A custom profile normally extends one of the neutral MCU-family base profiles:

esp32_s3_devkitc1_n16r8    ESP32-S3
esp32_devkitc_v4_wrover    classic ESP32 with PSRAM

It does not need a new CMake file, C header, selector branch, or PlatformIO environment. Use the base profile's PlatformIO environment and select the custom manifest with SOLAR_OS_BOARD.

Reusable CMake fragments in boards/drivers/ remain the package adapters for concrete board services. The manifest compiler selects them from driver-catalog metadata. Driver implementation and resource ownership stay in the packaged expansion driver, so the profile does not introduce driver-specific #ifdef branches.

Creating A Custom Board With The TUI

Run the tool from the SolarOS source directory:

python3 scripts/board_config.py

The workflow is MCU-first:

  1. Select ESP32-S3 or Classic ESP32.
  2. Select a compatible neutral base board.
  3. Enter the board identity.
  4. Select the permanently wired expansion drivers.
  5. Select existing buses or create new buses, then enter addresses, pins, chip
  6. selects, ports, and optional driver parameters.

  7. Confirm the generated profile.

The tool rejects unsupported MCU/driver combinations, duplicate device names, invalid addresses and parameters, unavailable chip-select pins, and GPIO conflicts. Pins used by a fixed device or a new bus are removed from the runtime user-pin surface automatically. The output is one inherited TOML file under boards/manifests/.

Promoting A Tested Runtime Configuration

You can start from any supported board, create buses in io, attach catalog drivers in expansion, and test the real wiring before defining a custom target. Export the settled hardware configuration on the device:

expansion export /sdcard/my-hardware.toml

Then copy the file into the SolarOS source tree and pass it to the same board configurator:

python3 scripts/board_config.py --expansion-manifest my-hardware.toml

The snapshot names its original board manifest. The configurator uses that as the inherited base, promotes exported buses and devices to board-owned entries, fixes their GPIOs, and removes their peripheral controllers from the runtime pool. If the base has no primary display, storage, audio, pointer, battery, or RTC provider and exactly one imported driver supplies that role, its board capabilities, driver fragment, and required defines are promoted too. Existing primary roles remain primary; ambiguous new primary roles are rejected instead of selected silently.

The export contains hardware topology only. It does not include startup jobs, network settings, credentials, readiness, or resource leases. Runtime manual attachments make export fail because a custom board must reference a validated driver in boards/expansion_drivers.toml. Edit or remove temporary startup commands separately after flashing the generated target.

For example, devkitc1_epaper_workbench.toml extends the S3 DevKitC profile and declares CardKB, SSD1683, and SDSPI as fixed devices. It also declares the second SPI bus used by the SD card. Build an inherited profile through its base environment:

SOLAR_OS_BOARD=devkitc1_epaper_workbench \
  pio run -e esp32_s3_devkitc1_n16r8

The configurator creates a SolarOS board profile, not a new PlatformIO environment. Use the base environment shown by the configurator, and keep SOLAR_OS_BOARD set for every build or upload invocation. To build and flash the example profile in one command:

SOLAR_OS_BOARD=devkitc1_epaper_workbench \
  pio run -e esp32_s3_devkitc1_n16r8 -t upload

The upload target builds first when necessary. Add --upload-port <serial-device> if PlatformIO does not select the correct port. If you select a firmware flavor explicitly, keep it on the upload invocation as well:

SOLAR_OS_BOARD=devkitc1_epaper_workbench SOLAR_OS_FLAVOR=core \
  pio run -e esp32_s3_devkitc1_n16r8 -t upload

You can edit the generated TOML after leaving the TUI. Validate it before a build:

python3 scripts/generate_board_profile.py \
  --manifest boards/manifests/devkitc1_epaper_workbench.toml \
  --manifest-dir boards/manifests \
  --drivers boards/expansion_drivers.toml \
  --validate-only

Fixed devices attach automatically and report origin=board. They cannot be detached at runtime. Use the established names when the device provides a primary board service:

Hardware roleFixed device name
Primary displaydisplay0
Removable storagestorage0
Audio input/outputaudio0
Primary pointertouch0
Battery monitorbattery0
Real-time clockrtc0
Environmental sensorenvironment0

The profile declares hardware, not policy. Keep job autostart commands in .shell/startup. Do not keep expansion attach, expansion bus create, or session create commands there for hardware that is now fixed in the profile.

After flashing, verify the automatic devices and their services:

status
pkg
port list
expansion drivers
expansion devices

A successful manifest validation and build do not prove wiring, polarity, power control, or peripheral behavior. Test the generated profile on its physical target.

Built-In Targets

The current tree includes these board targets:

TargetPlatformIO envHardwareHighlights
solar_termsolar_termSolarTerm, built from the Waveshare ESP32-S3-RLCD-4.2Primary ST7305 reflective display target with SDMMC, CDC, UART, RTC, SHTC3, battery ADC, ES8311/ES7210 audio, expansion I2C/SPI/UART/GPIO/ADC/PWM, and runtime-routable SPI3 on GPIO1/GPIO2/GPIO3/GPIO17. SolarOS refers to this hardware configuration as SolarTerm.
freenove_esp32_s3_display_4_0freenove_esp32_s3_display_4_0Freenove ESP32-S3 Display 4.0-inch (FNK0104S)Integrated 480x320 ST7796 display, FT6336 capacitive pointer, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, native USB CDC, and UART/I2C/GPIO expansion connectors.
elecrow_crowpanel_esp32_s3_4_2_epaperelecrow_crowpanel_esp32_s3_4_2_epaperElecrow CrowPanel ESP32-S3 4.2-inch E-paperESP32-S3-WROOM-1-N8R8 target with a 400x300 SSD1683 e-paper display, microSD over SDSPI, CH340C/UART console, rotary/menu/exit controls, status LED, Wi-Fi, BLE, and expansion I2C/SPI/UART/1-Wire/GPIO/ADC/PWM.
cl_32cl_32CL-32ESP32-S3-WROOM-1-N16R8 target with a 384x168 ST7305 reflective LCD, an ATmega808-backed keyboard and battery monitor, native USB CDC, UART, microSD over SDSPI, PCF85063 RTC, onboard PWM buzzer, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S.
odroid_goodroid_goHardkernel ODROID-GOClassic ESP32 target with ILI9341 display, SD over VSPI/SDSPI, battery ADC, ESP32 DAC speaker, buttons, ADC D-pad, status LED, display brightness, expansion SPI/UART/GPIO/PWM, and runtime GPIO4/GPIO15.
freenove_esp32_wrover_v3freenove_esp32_wrover_v3Freenove ESP32-WROVER v3.0 (FNK0060)Classic ESP32 target with 8 MB PSRAM, CH340/UART console, one-bit SDMMC, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, and a 384x288 monochrome PAL composite display on GPIO25.
ttgo_vga32_v14ttgo_vga32_v14LilyGO TTGO VGA32 v1.4ESP32-PICO-D4 desktop target with 8 MB external PSRAM, build-selectable 320x200@70Hz, 320x240@60Hz, 640x400@70Hz, or 640x480@60Hz VGA output through the onboard RGB222 resistor DAC, GPIO25 mono DAC audio, a default-attached PS/2 keyboard, v1.4 microSD wiring over HSPI, USB-UART, Wi-Fi, BLE disabled by default, and two input-only expansion GPIOs.
esp32_s3_devkitc1_n16r8esp32_s3_devkitc1_n16r8Espressif ESP32-S3-DevKitC-1-N16R8Headless ESP32-S3 target with CDC, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM, graphics through attachable display targets, and no primary display or onboard sensors.
esp32_devkitc_v4_wroveresp32_devkitc_v4_wroverEspressif ESP32-DevKitC V4 with ESP32-WROVER-EHeadless classic ESP32 target with PSRAM, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S, graphics through attachable display targets, and no built-in peripherals.
devkitc1_epaper_workbenchesp32_s3_devkitc1_n16r8 with SOLAR_OS_BOARD=devkitc1_epaper_workbenchESP32-S3 DevKitC-1 E-paper WorkbenchManifest-generated development target with fixed CardKB, 400x300 SSD1683 display, and SDSPI storage attachments.
t_lora_pagert_lora_pagerLilyGO T-LoRa-Pager (SX1262)ESP32-S3-WROOM-1-N16R8 target with a 480x222 ST7796 display, a TCA8418 4x10 matrix keyboard, an SX1262 LoRa radio, PCF85063 RTC, microSD over SDSPI, ES8311 audio, a BQ27220 battery gauge, a BQ25896 charger, a rotary encoder, an XL9555 I2C GPIO controller for integrated power rails, a MIA-M10Q GNSS receiver, an ST25R3916 NFC-A reader, a BHI260AP IMU, a DRV2605 haptic controller, native USB CDC, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM. SD power and its external pull-up start enabled; GNSS and NFC power start disabled and are enabled with gnss power on and nfc power on. Use haptic play EFFECT for DRV2605 ROM effects 1 through 117. The Pager fixes BQ25896 fast-charge current at 704 mA during attachment; inspect or explicitly change charger settings with charger.
t_deck_plust_deck_plusLilyGO T-Deck PlusESP32-S3FN16R8 target with a 320x240 ST7789 display, GT911 touch, I2C keyboard, five-way trackball, separate I2S speaker output and ES7210 microphone capture, SX1262 LoRa radio, microSD over SDSPI, battery ADC, internal GPS UART, native USB CDC, Wi-Fi, and BLE.
waveshare_esp32_s3_sim7670g_4gwaveshare_esp32_s3_sim7670g_4gWaveshare ESP32-S3-SIM7670G-4G V2.0Headless ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, CH343/UART console, SIM7670G AT, GNSS, and IPv4 PPP access over UART1, MAX17048 battery gauge, one-bit SDMMC, one WS2812B, Wi-Fi, BLE, and expansion GPIO/ADC/PWM/SPI/I2S. Set the DIP switches to CAM OFF, HUB ON, 4G OFF, USB OFF. With CAM off, the unused camera signal pins are available as expansion GPIOs and can form runtime SPI or I2S buses; GPIO15/GPIO16 remain the shared MAX17048 I2C bus and GPIO46 remains blocked as an input-only strapping pin. SolarOS drives GPIO21 active-high to control the modem VBAT rail; leave the 4G switch OFF to avoid contention and permit software power-off/reset. The modem UART belongs exclusively to PPP while connected; disconnect it before AT or GNSS use. Camera support is not included.
waveshare_esp32_s3_sim7670g_4g_epaperwaveshare_esp32_s3_sim7670g_4g with SOLAR_OS_BOARD=waveshare_esp32_s3_sim7670g_4g_epaperWaveshare ESP32-S3-SIM7670G-4G E-paper workbenchExpansion-export reference target with a fixed CardKB keyboard on the board-owned I2C bus at address 0x5f and a fixed primary 400x300 SSD1683 display on SPI2 using GPIO3/GPIO7 through GPIO12.

Generated Build Interface

The manifest compiler converts build.drivers into reusable CMake driver fragments and converts build.capabilities into SOLAR_OS_BOARD_HAS_* definitions. Fixed devices add their driver packages and board-service capabilities through boards/expansion_drivers.toml. Packages whose requirements are not satisfied are pruned.

The generated CMake and header files are private build artifacts. Inspect them when diagnosing a profile, but make source changes in the TOML manifest or the driver catalog.

Capability Flags

The current capability flags are:

FlagMeaning
PSRAMExternal PSRAM is present and configured. SOLAR_OS_BOARD_PSRAM_BYTES gives the expected capacity.
SIMDCPU vector/SIMD instructions are available for bulk data engines such as image, audio, DSP, or accelerated math paths.
DISPLAYA board-integrated primary display driver and boot-time display target are available. Requires GFX.
GFXThe firmware can host drawable display targets, including targets registered later by expansion drivers. It does not imply that a display exists at boot.
CDCUSB Serial/JTAG CDC byte-stream port cdc0. The dormant TinyUSB composite driver can add keyboard, mouse, and gamepad HID reports when explicitly enabled.
UARTHardware UART service is supported. Named UART buses may be board-defined or created at runtime.
SDSD/MMC storage and filesystem mounting.
I2CHardware I2C service is supported. Named I2C buses may be board-defined or created at runtime.
SPIHardware SPI service is supported. Named SPI buses may be board-defined or created at runtime.
RTCRTC attached to the board I2C bus.
BATTERYBattery voltage monitor is available.
AUDIOSpeaker/audio-output path is available.
AUDIO_INPUTMicrophone/audio-input path is available. Usually paired with AUDIO on codec boards.
WIFIWi-Fi station/AP services.
BLEBLE keyboard and BLE/GATT services.
PS2_KEYBOARDA board-integrated PS/2 keyboard bus is available. Requires GPIO.
GPIORuntime-safe GPIO service.
ADCRuntime-safe ADC service.
PWMRuntime-safe PWM service.
EXPANSION_GPIOExpansion connector has runtime-safe GPIO pins for external hardware.
EXPANSION_I2CExpansion hardware may use a static or runtime-created named I2C bus. Requires I2C.
EXPANSION_SPIExpansion hardware may use a static or runtime-created named SPI bus. Requires SPI.
EXPANSION_UARTExpansion hardware may use a static or runtime-created named UART bus. Requires UART.
EXPANSION_ADCExpansion connector has ADC-capable runtime pins.
EXPANSION_PWMExpansion connector has PWM-capable runtime pins.
EXPANSION_I2SBoard has a spare I2S controller and at least three runtime-safe output GPIOs for an external three-wire I2S device.
KEYBuilt-in board key for sleep/pairing control.
BUTTONSBuilt-in digital buttons are available for keyboard/app input.
JOYSTICKBuilt-in analog joystick axes are available as generic axis input.
ADC_DPADBuilt-in ADC D-pad axes are available for keyboard/app input.
STATUS_LEDBoard status LED output is available.
DISPLAY_BRIGHTNESSDisplay backlight or brightness control is available.
POINTERBoard-integrated absolute or relative pointing input. Events carry a source, pointer ID, display target, coordinates, deltas, buttons, and press/move/release action.
STREAMING_DISPLAYThe board's primary display has a bounded-cadence raster-frame presenter suitable for animation or games. This is separate from ordinary terminal and GUI drawing.
TEMPERATURETemperature sensor service.
HUMIDITYHumidity sensor service.

src/CMakeLists.txt validates that every enabled driver-backed capability has a matching selector, then consumes direct board sources and required packages. It does not know which concrete source files belong to ST7305, SDMMC, PCF85063, or any future driver.

Expansion capabilities are compile-time gates for external hardware packages. Use them when a package needs connector resources rather than an internal board peripheral. A driver that can use either a static expansion SPI descriptor or a runtime-routed bus may accept either expansion_spi or expansion_gpio; a driver that also requires independent control pins must still require expansion_gpio. Do not gate these packages on plain spi and gpio, because those capabilities can refer only to internal display or storage hardware. Use expansion_i2s for external I2S packages; it is intentionally stricter than expansion_gpio and prevents unusable drivers from entering a board build. The user-facing connector tables and attachment workflow live in Expansion Ports.

Generated Board Metadata

The manifest compiler emits the C-visible identity, pin, bus, connector, and fixed-device tables. The examples below describe that generated runtime contract. Do not create or edit include/boards/<target>.h for a manifest profile.

#pragma once

#define SOLAR_OS_BOARD_ID "my_board"
#define SOLAR_OS_BOARD_NAME "My SolarOS Board"
#define SOLAR_OS_BOARD_VENDOR "Vendor"
#define SOLAR_OS_BOARD_MODULE_NAME "ESP32-S3-WROOM-1-N16R8"

Add board-specific compatibility macros to [defines] only when existing code requires them. Normal identity, capability, bus, connector, pin, and device metadata comes from dedicated manifest fields.

UART example:

#include "driver/gpio.h"
#include "driver/uart.h"

#define SOLAR_OS_BOARD_UART_PORT UART_NUM_0
#define SOLAR_OS_BOARD_PIN_UART_TX GPIO_NUM_43
#define SOLAR_OS_BOARD_PIN_UART_RX GPIO_NUM_44

Key example:

#include "driver/gpio.h"

#define SOLAR_OS_BOARD_PIN_KEY GPIO_NUM_18
#define SOLAR_OS_BOARD_KEY_ACTIVE_LEVEL 0
#define SOLAR_OS_BOARD_KEY_PULL_UP 1
#define SOLAR_OS_BOARD_KEY_PULL_DOWN 0

Runtime GPIO example:

#define SOLAR_OS_BOARD_EXPANSION_GPIO_MASK ((1ULL << GPIO_NUM_1) | \
                                            (1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_USER_GPIO_MASK ((1ULL << GPIO_NUM_1) | \
                                       (1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_EXPANSION_GPIO_LIST "1 2"
#define SOLAR_OS_BOARD_USER_GPIO_LIST "1 2"
#define SOLAR_OS_BOARD_GPIO_SLOTS { \
    {.pin = 1, .policy = SOLAR_OS_PIN_POLICY_FREE, .role = "expansion"}, \
    {.pin = 2, .policy = SOLAR_OS_PIN_POLICY_FREE, .role = "expansion"}, \
}

Pin policy is separate from physical connector membership:

  • SOLAR_OS_PIN_POLICY_FREE: available for direct GPIO and future routed buses.
  • SOLAR_OS_PIN_POLICY_RELEASABLE: has a default board role but may be routed
  • after its current service releases it. The board bus descriptor remains registered; releasing the service only stops the hardware and frees its pins.

  • SOLAR_OS_PIN_POLICY_FIXED: never available to runtime pin routing.

Keep the user GPIO list conservative. Do not mark boot strapping, flash/PSRAM, display, SD, system I2C, or key pins free. A releasable pin remains unavailable to direct GPIO until a resource-aware service explicitly takes ownership.

Describe the physical placement of every exposed connector contact separately. The io app and expansion layout [connector] render this metadata and combine GPIO contacts with the live pin policy and claim registry:

#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_TITLE "J1 / J3 pin headers"
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_VIEW \
    "component side; antenna at top, USB connectors at bottom"
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_ROWS 22
#define SOLAR_OS_BOARD_CONNECTOR_LAYOUT_COLUMNS 2
#define SOLAR_OS_BOARD_CONNECTOR_PIN_COUNT 44
#define SOLAR_OS_BOARD_CONNECTOR_PINS { \
    {.connector = "J1", .position = 1, .row = 0, .column = 0, \
     .pin = -1, .kind = SOLAR_OS_CONNECTOR_PIN_POWER, .label = "3V3"}, \
    {.connector = "J3", .position = 1, .row = 0, .column = 1, \
     .pin = -1, .kind = SOLAR_OS_CONNECTOR_PIN_GROUND, .label = "GND"}, \
}

row and column are zero-based display coordinates. position is the connector manufacturer's pin number and need not increase in screen order. Use GPIO, power, ground, control, and NC kinds as appropriate; only GPIO entries participate in live resource lookup. Keep the view description explicit about which board side is shown and its orientation. A board without this metadata still builds, but reports that no physical connector map is available.

Static board bus example:

#include "solar_os_bus_types.h"

#define SOLAR_OS_BOARD_BUSES { \
    { \
        .name = "i2c0", \
        .protocol = SOLAR_OS_BUS_PROTOCOL_I2C, \
        .origin = SOLAR_OS_BUS_ORIGIN_BOARD, \
        .sharing = SOLAR_OS_BUS_SHARED, \
        .config.i2c = { \
            .port = I2C_NUM_0, \
            .sda_pin = GPIO_NUM_8, \
            .scl_pin = GPIO_NUM_9, \
            .speed_hz = SOLAR_OS_BUS_I2C_DEFAULT_SPEED_HZ, \
        }, \
    }, \
    { \
        .name = "spi0", \
        .protocol = SOLAR_OS_BUS_PROTOCOL_SPI, \
        .origin = SOLAR_OS_BUS_ORIGIN_BOARD, \
        .sharing = SOLAR_OS_BUS_SHARED, \
        .config.spi = { \
            .host = SPI2_HOST, \
            .sclk_pin = GPIO_NUM_12, \
            .miso_pin = GPIO_NUM_13, \
            .mosi_pin = GPIO_NUM_11, \
            .max_transfer_size = 4096, \
            .cs_count = 2, \
            .cs = { \
                {.name = "gpio10", .pin = GPIO_NUM_10}, \
                {.name = "gpio5", .pin = GPIO_NUM_5}, \
            }, \
        }, \
    }, \
}
#define SOLAR_OS_BOARD_RUNTIME_SPI_HOST_MASK (1U << SPI3_HOST)
#define SOLAR_OS_BOARD_EXPANSION_ADC_MASK ((1ULL << GPIO_NUM_1) | \
                                           (1ULL << GPIO_NUM_2))
#define SOLAR_OS_BOARD_EXPANSION_PWM_MASK SOLAR_OS_BOARD_USER_GPIO_MASK

SOLAR_OS_BOARD_BUSES is the canonical static-bus table consumed directly by the protocol-neutral named bus registry. It includes board buses exposed to OS services and expansion management, such as the Waveshare i2c0. Bus names are unique across protocols. I2C and SPI buses accept shared logical leases; UART and 1-Wire bus instances are exclusive. Attaching an expansion device acquires a lease under the device name and detaching it releases that lease.

Protocol capabilities describe whether the service can exist; they do not imply a static bus. A board may therefore enable UART, SPI, or I2C with no matching entry in SOLAR_OS_BOARD_BUSES when it supports only runtime-created buses. Expansion capability flags authorize that runtime-facing path, while the runtime controller masks and pin policy constrain the instances that may be created. Configuration checks reject expansion capabilities without their base protocol and non-empty SPI/UART runtime masks without matching base and expansion capabilities. SOLAR_OS_BOARD_RUNTIME_I2S_PORT_MASK similarly names the controller reserved for attachable I2S hardware and must accompany EXPANSION_I2S.

The registry distinguishes immutable board descriptors from runtime-created buses. Board buses cannot be unregistered. Every protocol uses the same named bus attach/detach lifecycle. Runtime descriptors are detachable and removable; board descriptors are detachable only when all signal pins are marked releasable, and otherwise remain fixed. Detachment releases the hardware endpoint and signal pins while preserving the name and configuration. Runtime I2C uses an unregistered hardware controller plus approved SDA/SCL pins. Runtime 1-Wire uses one approved pin. Runtime SPI is supported on hosts explicitly allowed by SOLAR_OS_BOARD_RUNTIME_SPI_HOST_MASK; CS entries are bus-owned GPIO slots, while their logical chip-select use is claimed per device. Runtime bus signal pins and hardware endpoints are claimed atomically and released when an idle bus is detached or removed. Runtime UART controllers are limited by SOLAR_OS_BOARD_RUNTIME_UART_PORT_MASK. An attached UART reserves its controller and pins, while its driver starts on first lease and stops on final release.

PlatformIO Environment

Inherited custom profiles normally use their base profile's environment, as shown in the TUI procedure. Add an environment only when an in-tree standalone target needs its own PlatformIO hardware, partition, or SDK configuration:

[env:my_board]
board = esp32-s3-devkitc1-n16r8
board_build.cmake_extra_args = -DSOLAR_OS_BOARD=my_board

board is the PlatformIO hardware definition. SOLAR_OS_BOARD is the SolarOS manifest name under boards/manifests/<target>.toml.

When the PlatformIO environment name and SolarOS board profile name are the same, the CMake argument is still preferred because it removes ambiguity and makes alias environments possible.

For an inherited custom profile, build and upload through its base environment while selecting the profile explicitly:

SOLAR_OS_BOARD=my_board pio run -e esp32_s3_devkitc1_n16r8
SOLAR_OS_BOARD=my_board pio run -e esp32_s3_devkitc1_n16r8 -t upload

For a standalone target with its own [env:my_board] entry, the environment already selects the profile:

pio run -e my_board
pio run -e my_board -t upload
pio device monitor -b 115200

Classic ESP32 boards can use a board-specific SDK defaults file when the common defaults are not appropriate for the target:

[env:odroid_go]
board = odroid_esp32
board_build.cmake_extra_args = -DSOLAR_OS_BOARD=odroid_go -DSDKCONFIG_DEFAULTS=sdkconfig.defaults.odroid_go

Update Layouts

os_builder selects an update layout after board selection. This choice fixes the partition table and application-image limit; internal storage size is not a separate user setting.

FlashOTA updatesSerial updates
16 MiBTwo 0x700000-byte (7 MiB) app slots and 0x1F0000 bytes of internal storageOne 0xE00000-byte (14 MiB) app slot and 0x1F0000 bytes of internal storage
8 MiBTwo 0x3E0000-byte (3.875 MiB) app slots and 0x20000 bytes (128 KiB) of internal storageOne 0x700000-byte (7 MiB) app slot and 0xF0000 bytes (960 KiB) of internal storage
4 MiBNot availableOne 0x3D0000-byte app slot and 0x20000 bytes (128 KiB) of internal storage

The OTA layout also includes the internal OTA service and its dependencies. The service is not a flavor group. The serial layout does not add it, although another selected feature can still depend on the shared OTA implementation. Changing layouts changes the partition table, so install the first image for a new layout through serial flashing. Do not switch a deployed device's layout with an ordinary OTA update.

For a direct PlatformIO build outside os_builder, set SOLAR_OS_LAYOUT:

SOLAR_OS_LAYOUT=single pio run -e solar_term

If it is omitted, 8 MiB and 16 MiB environments use the OTA layout and 4 MiB environments use their fixed serial layout.

Freenove ESP32-WROVER v3.0

The freenove_esp32_wrover_v3 target covers the FNK0060 v3.0 board with an ESP32-WROVER-E-N4R8 module, 4 MB flash, 8 MB physical PSRAM, a CH340 USB-to-UART bridge, and the rear microSD slot. It uses uart0 on GPIO1/GPIO3 as the boot shell and a fixed storage0 one-bit SDMMC attachment on GPIO14 clock, GPIO15 command, and GPIO2 data. The active-low BOOT button on GPIO0 is also the SolarOS KEY. A short press uses the configured sleep or suspend action; another short press resumes from suspend. A long press forgets the remembered BLE keyboard and starts pairing. Do not hold the button during reset or power-up, because GPIO0 low selects the ESP32 serial download boot mode.

The target deliberately leaves the OV2640 camera unsupported. Remove or disconnect it before using this target. GPIO25 is reserved for PAL composite output because it conflicts with the camera's VSYNC signal. The other former camera signals are available as runtime expansion GPIOs; GPIO34, GPIO35, GPIO36, and GPIO39 are input-only and also support ADC. GPIO5 is a boot strapping pin, so external circuitry must not force it to the wrong level while the ESP32 resets.

The cvbs_pal backend produces monochrome PAL through the original ESP32's DAC1 and I2S0 DMA hardware. Its default SolarOS canvas is 384x288 with PAL 625/50 timing. The U8g2 draw buffer lives in PSRAM, while two scanout buffers in internal RAM swap only at PAL field boundaries so applications keep the normal display service and do not tear the active field. The timing and low-level peripheral setup are adapted from LovyanGFX Panel_CVBS; SolarOS keeps its own one-bit graphics stack instead of linking LovyanGFX's separate color framebuffer.

Small composite displays that do not handle the full PAL raster well can use a centered 320x200 safe-area mode. Select it when building; the main and virtual display dimensions both change to 320x200:

SOLAR_OS_FLAVOR=rover SOLAR_OS_CVBS_MODE=320x200 \
  pio run -e freenove_esp32_wrover_v3

Omit SOLAR_OS_CVBS_MODE (or set it to 384x288) to build the default full PAL mode. Composite scanout requires the ESP32's full 240 MHz clock, so SolarOS clamps all power profiles to that board-specific CPU floor on this target.

Use os_builder with the rover baseline to make a specialized Game Boy or Synth image. Add the required group, remove unrelated groups until the measured image fits, then build and flash from the same TUI:

python3 scripts/os_builder.py \
  --input flavors/rover.toml \
  --board freenove_esp32_wrover_v3 \
  --layout single

The default full PAL mode gives Game Boy the largest image. The 320x200 safe-area mode also supports Game Boy; its presenter selects a smaller centered raster. For serial diagnostics, run:

job start log uart0 debug

It is silent because composite scanout owns I2S0. For a custom Synth image, retain BLE, MIDI, controls, storage, basic file tools, Synth, and the PWM audio driver while removing unrelated stacks. Attach an LEDC PWM audio output at runtime because PAL scanout owns I2S0:

expansion attach audio-pwm audio pwm=gpio26
synth

Connect GPIO25 to the composite input's center conductor and a board GND to its shield/ground. Keep both leads short and use a PAL-capable input with its normal 75-ohm termination. The backend continuously owns I2S0 and the APLL while the display is active, so this board cannot use an I2S0 audio backend at the same time.

Runtime GPIO is available on GPIO4, GPIO5, GPIO13, GPIO18, GPIO19, GPIO21-GPIO23, GPIO26, GPIO27, GPIO32-GPIO36, and GPIO39. GPIO34-GPIO36 and GPIO39 are input-only and support runtime ADC; the other runtime pins support PWM and can form runtime I2C, SPI, UART, or 1-Wire buses. UART0 remains registered on the CH340 pins and cannot be detached by the shell using it.

The board uses partitions_4mb.csv, with one 0x3D0000-byte factory application slot and a 0x20000-byte (128 KiB) flash filesystem. A dual-OTA layout is not available on 4 MB flash. Install firmware through the CH340 serial connection; this partition layout does not support on-device OTA updates.

LilyGO TTGO VGA32 v1.4

The ttgo_vga32_v14 target covers the ESP32-PICO-D4 VGA32 revision 1.4 with 8 MB external PSRAM. It uses the onboard RGB222 resistor DAC for VGA, the USB-UART bridge on UART0, mono ESP32 DAC audio on GPIO25, a PS/2 keyboard on GPIO32/GPIO33, and the revision 1.4 microSD wiring on HSPI: MOSI GPIO12, MISO GPIO2, clock GPIO14, and chip select GPIO13. These SD pins differ from the older revision 1.2 board.

VGA scanout continuously streams a short line ring through I2S1 DMA. A level-3 IRAM interrupt on CPU1 refills completed line groups. Frame submissions are coalesced, and a CPU1 presentation worker converts the newest monochrome snapshot into the scanout buffer at a bounded rate. The SolarOS canvas stays monochrome, while a lookup table expands the configured foreground and background colors to the board's two-bit-per-channel VGA output. I2S1 and the VGA GPIOs remain fixed resources while the display is active.

The onboard audio path takes the ESP32 DAC1 signal from GPIO25 and routes the same mono output to the 3.5 mm jack and the NS4150 speaker amplifier. SolarOS uses the shared ESP32-DAC backend at 16 kHz. This is output-only hardware; the board does not advertise microphone or audio-input support. Test it with audio tone 880 500.

The default mode is 640x480@60 Hz. Select another mode at build time with SOLAR_OS_VGA_MODE:

pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=320x200 pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=320x240 pio run -e ttgo_vga32_v14
SOLAR_OS_VGA_MODE=640x400 pio run -e ttgo_vga32_v14

The 320x200 and 320x240 modes use double scan and two internal monochrome scanout buffers. The 320x240 mode derives its 60 Hz timing from 640x480 VGA. The 640x400@70 Hz and 640x480@60 Hz modes use the standard 25.175 MHz VGA pixel clock and one internal monochrome scanout buffer to preserve heap for SolarOS. Updating a high-resolution frame can therefore produce a brief tear while the new image is copied. Changing SOLAR_OS_VGA_MODE causes PlatformIO to reconfigure CMake automatically.

The board has 4 MB flash, so its PlatformIO environment defaults to the focused rover flavor instead of full:

pio run -e ttgo_vga32_v14

To create a compact Game Boy or another specialized build, start from rover in os_builder, adjust the granular groups, and build against this board's single-image limit:

python3 scripts/os_builder.py \
  --input flavors/rover.toml \
  --board ttgo_vga32_v14 \
  --layout single

The board profile declares ps2kbd0 and creates the default keyboard0 ps2-keyboard expansion attachment before the shell. Its topology and input state remain visible through the generic commands:

expansion devices
input keyboard
input test keyboard0

BLE remains available but defaults to off on this board to preserve internal heap. Use setterm ble on and reboot to enable it. setterm ble default clears the user override and restores the board default.

The onboard PS/2 mouse connector is registered as the fixed ps2mouse0 bus. Attach a mouse when one is connected with expansion attach ps2-mouse mouse0 ps2=ps2mouse0; it is not a default attachment because the connector can be empty. The microSD signals are fixed board resources; GPIO34 and GPIO39 are the only header pins available for runtime GPIO/ADC, and both are input-only.

ODROID-GO

The built-in odroid_go target covers the classic ESP32 Hardkernel ODROID-GO. It uses an ESP32-WROVER module with 4 MiB PSRAM, the ILI9341 display driver, SDSPI storage on the VSPI bus, battery ADC, ESP32 DAC speaker output, digital buttons, ADC D-pad input, status LED, PWM display brightness, Wi-Fi, and BLE.

The board does not have CDC, I2C, RTC, onboard temperature/humidity sensors, or audio input enabled. It boots into the display shell, and uart0 on GPIO1/GPIO3 is available as the serial byte-stream port.

The ILI9341 backend plans changed monochrome tile runs, INDEX8 GUI tiles, and INDEX2 game rasters separately, then sends all three through the same queued two-line DMA pump. Game Boy uses a centered 240x216 raster at 30 presentation frames per second so RGB565 transfer stays below the shared-SPI pixel budget.

ODROID-GO uses the shared VSPI bus for the TFT, SD card, and external chip selects:

  • GPIO18: VSPI SCLK
  • GPIO19: VSPI MISO
  • GPIO23: VSPI MOSI
  • GPIO5: TFT chip select
  • GPIO22: SD card chip select
  • GPIO4 and GPIO15: external IO and runtime-safe SPI chip-select slots

Runtime GPIO access is intentionally limited to GPIO4 and GPIO15. Other visible or board-significant pins are reserved: GPIO2 is the status LED, GPIO14 is the LCD backlight, GPIO25 is speaker amplifier enable, GPIO26 is the DAC sample output, GPIO34/GPIO35 are the ADC D-pad axes, GPIO36 is battery ADC, GPIO39 is the board key input, and GPIO32/GPIO33/GPIO13/GPIO27/GPIO0 are built-in buttons.

GPIO25 is amplifier enable/shutdown wiring, not a second SolarOS DAC channel. Treat GPIO26 as the only DAC sample output for ODROID-GO audio.

Freenove ESP32-S3 Display 4.0-inch

The freenove_esp32_s3_display_4_0 target supports the capacitive-touch FNK0104S board. SolarOS runs its portrait 320x480 ST7796 panel as a 480x320 landscape primary display and controls the active-high GPIO45 backlight with PWM. The FT6336 touch controller shares i2c0 with the ES8311 codec and emits generic absolute pointer events targeted at display0. Native apps opt in with SOLAR_OS_APP_FLAG_POINTER_EVENTS; pointer events include press, move, and release state rather than exposing FT6336 registers to applications. Foreground Python and Lua apps receive the same routed events through solaros.input. The board profile creates this as the default ft6336 expansion attachment named touch0; inspect it with expansion devices, input touch, and input test touch0. Optional logical-range correction is stored with input calibrate touch0 ....

The ST7796 backend uses the same changed-region planner and queued two-line DMA pump for monochrome terminal, INDEX8 GUI, and INDEX2 game content. A full 320x288 Game Boy image would leave too little margin on the 40 MHz RGB565 SPI link, so the presenter selects a centered 240x216 raster at 25 frames per second.

The ES8311 is configured as one duplex codec: GPIO8 carries playback data to the codec, GPIO6 carries microphone data to the ESP32-S3, and GPIO1 controls the active-low speaker amplifier. The SD slot is the fixed four-bit SDMMC attachment storage0. Battery voltage is measured on GPIO9 through the board divider. The TP4054 circuit is an analog charger, not a digitally addressable fuel gauge or battery manager.

Expansion wiring is divided across three connectors. uart0 uses GPIO43 and GPIO44. The I2C connector exposes GPIO16 SDA and GPIO15 SCL. The GPIO connector exposes runtime-safe GPIO2, GPIO3, GPIO14, and GPIO21. Current Freenove FNK0104S documentation identifies the third GPIO signal as GPIO14; GPIO4 is fixed as the ES8311 MCLK signal. The BOOT button on GPIO0 becomes the SolarOS KEY after startup; do not hold it during reset because it selects download mode.

Build the target with:

pio run -e freenove_esp32_s3_display_4_0

Elecrow CrowPanel ESP32-S3 4.2-inch E-paper

The built-in elecrow_crowpanel_esp32_s3_4_2_epaper target covers Elecrow's V1.0 400x300 monochrome CrowPanel. It uses an ESP32-S3-WROOM-1-N8R8 module, the SSD1683 e-paper driver, microSD over a dedicated SDSPI bus, five digital controls, a status LED, Wi-Fi, BLE, and selectable OTA or serial-update layouts.

Elecrow has shipped both the original panel and a newer panel identified by a green circular sticker on the back. The newer revision keeps the same GPIO pinout but requires a different reset, initialization, refresh, and sleep sequence. SolarOS detects the revision from its post-reset BUSY behavior and selects the matching command and waveform path automatically.

The board's USB-C data lines terminate at a CH340C USB-to-UART bridge. SolarOS therefore uses uart0 on GPIO43/GPIO44 for the serial console and does not claim native USB Serial/JTAG CDC. The BAT connector supplies the board but the published schematic does not provide a battery-voltage ADC path, so the target does not advertise the BATTERY capability.

The display shell defaults to the board's landscape orientation, rotated 90 degrees clockwise from the controller's portrait orientation. The onboard controls are mapped as follows:

  • GPIO1: EXIT, mapped to the foreground app-exit key.
  • GPIO2: MENU, used as the SolarOS KEY for sleep/suspend control and BLE pairing.
  • GPIO6: rotary counter-clockwise/previous, mapped to Down.
  • GPIO4: rotary clockwise/next, mapped to Up.
  • GPIO5: rotary press, mapped to Enter.

The 20-pin GPIO header exposes GPIO3, GPIO8, GPIO9, GPIO14-GPIO21, and GPIO38. GPIO3 is a strapping pin and is listed as a physical connector pin but blocked from runtime control. Runtime GPIO/PWM is allowed on GPIO8, GPIO9, GPIO14-GPIO21, and GPIO38. ADC is available on the ADC-capable subset GPIO8, GPIO9, and GPIO14-GPIO20.

The same runtime-safe pins can be routed to named I2C buses on i2c0 or i2c1, named UART buses on uart1 or uart2, and named 1-Wire buses. They can also be routed to a named SPI bus on spi3 after the SD card is unmounted. SPI3 is arbitrated as one resource: mounting the SD card while an expansion SPI bus is attached, or creating an expansion SPI bus while SD is mounted, is rejected.

The panel and storage wiring remains internal board wiring:

  • SSD1683: GPIO12 SCK, GPIO11 MOSI, GPIO47 reset, GPIO46 D/C, GPIO45 chip
  • select, GPIO48 BUSY, and GPIO7 display-power enable.

  • microSD: GPIO39 SCK, GPIO40 MOSI, GPIO13 MISO, GPIO10 chip select, and GPIO42
  • SD-power enable.

  • GPIO41: active-high status LED.

The default OTA layout uses partitions_8mb.csv, with two 0x3E0000-byte OTA application slots and a 0x20000-byte flash filesystem partition. The serial layout uses partitions_8mb_single.csv, with one 0x700000-byte factory slot and a 0xF0000-byte flash filesystem partition.

Headless Boards

A headless board is a valid SolarOS target as long as it has a byte-stream port. For boards without DISPLAY, SolarOS starts the primary shell on uart0 when UART is enabled. If UART is not available, it falls back to cdc0 when CDC is enabled.

Recommended minimal capability set for a generic ESP32-S3 board:

set(SOLAR_OS_BOARD_HAS_PSRAM ON)
set(SOLAR_OS_BOARD_PSRAM_BYTES 8388608)
set(SOLAR_OS_BOARD_HAS_SIMD ON)
include("${CMAKE_CURRENT_LIST_DIR}/drivers/cdc_usb_serial_jtag.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/drivers/uart_esp_idf.cmake")
set(SOLAR_OS_BOARD_HAS_CDC ON)
set(SOLAR_OS_BOARD_HAS_UART ON)
set(SOLAR_OS_BOARD_HAS_WIFI ON)
set(SOLAR_OS_BOARD_HAS_BLE ON)

With uart0 as the primary shell, cdc0 remains clean for logs, a later shell job, bridge jobs, or host-side tooling.

The built-in esp32_s3_devkitc1_n16r8 target keeps this headless shell model and also enables expansion GPIO, ADC, PWM, I2C, and SPI. The default I2C bus is GPIO8 SDA and GPIO9 SCL. The default SPI bus is FSPI on GPIO12 SCK, GPIO13 MISO, and GPIO11 MOSI, with chip-select slots on GPIO4, GPIO10, GPIO5, GPIO6, and GPIO7. Its active-low BOOT button on GPIO0 is also the SolarOS KEY for the configured short-press sleep/suspend action, light-sleep wake, and long-press BLE keyboard replacement. GPIO0 remains reserved from runtime routing. Do not hold the button during reset or power-up, because that selects download boot mode. The N16R8 target defaults to the common 16 MiB OTA partitions.csv layout. Its serial alternative uses partitions_16mb_single.csv. Both retain the same 0x1F0000-byte internal volume for durable agent conversations and normal file workflows without an SD card. The board also permits runtime routing on the spare SPI3 host. Static spi0 remains the usual choice; the runtime host is useful for isolated experiments on another set of routable expansion pins. Auxiliary SPI displays can use that expansion SPI bus through expansion drivers. For example, a PCD8544 84x48 LCD module can attach as lcd0 with expansion attach pcd8544 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 and then be exercised with display test lcd0. An RFM95W multimode radio wired with NSS on GPIO4 and reset on GPIO5 attaches with expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5. Auxiliary I2C displays can use i2c0 as well. A common 128x64 SSD1306 OLED at address 0x3c can attach with expansion attach ssd1306 oled0 i2c=i2c0 addr=0x3c; use display test oled0 or session create shell oled0 after attachment. Modules whose image is shifted two pixels left use the SH1106 profile instead: expansion attach sh1106 oled0 i2c=i2c0 addr=0x3c.

The Waveshare target has no static SPI bus on its expansion connector, but its spare SPI3 host may be routed over the four free header pins. A full-duplex bus using GPIO1/GPIO2/GPIO3 plus GPIO17 as its device-select slot is created with:

expansion bus create spi spi1 host=spi3 sclk=gpio1 mosi=gpio2 miso=gpio3 cs=gpio17

The bus remains idle until a device attaches. After detaching all devices, expansion bus remove spi1 releases the three data/clock pins and its configured chip-select pins. The board-defined I2C bus on GPIO13/GPIO14 is fixed and is never remapped by this operation.

The spare I2C/UART controllers and free pins can instead form runtime I2C, UART, or named 1-Wire buses:

expansion bus create i2c i2c1 port=i2c1 sda=gpio1 scl=gpio2
expansion bus create onewire onewire0 pin=gpio3
expansion bus create uart uart1 port=uart1 tx=gpio1 rx=gpio2

The board-defined uart0 on GPIO43/GPIO44 is non-removable but detachable. From a display or other non-uart0 shell, expansion bus detach uart0 releases those pins for a temporary runtime bus. Remove the temporary descriptor and run expansion bus attach uart0 to restore the board UART. A UART carrying an active port owner cannot be detached.

For the N16R8 module, GPIO35, GPIO36, and GPIO37 are reserved by Octal PSRAM and must not be exposed as runtime GPIO. The generic DevKitC target also reserves GPIO38 and GPIO48 because the onboard RGB LED moved between board revisions. Use a revision-specific board profile if one of those pins must be exposed.

Display Boards

For a board-integrated primary display, enable both DISPLAY and GFX, include the display fragment, and declare the packaged driver as an immutable early display0 attachment in the board header:

include("${CMAKE_CURRENT_LIST_DIR}/drivers/display_st7305.cmake")
set(SOLAR_OS_BOARD_HAS_DISPLAY ON)
set(SOLAR_OS_BOARD_HAS_GFX ON)

A headless board that supports attachable graphical displays enables GFX without enabling DISPLAY. Its graphics applications are compiled, but they require a ready named display target at runtime.

The board header then provides metadata and pins. The built-in Waveshare target uses the ST7305 reflective LCD driver:

SOLAR_OS_BOARD_DISPLAY_WIDTH and SOLAR_OS_BOARD_DISPLAY_HEIGHT always describe the logical SolarOS drawing area after rotation. A controller driver that needs the unrotated panel address space uses SOLAR_OS_BOARD_DISPLAY_NATIVE_WIDTH and SOLAR_OS_BOARD_DISPLAY_NATIVE_HEIGHT separately. Display targets register the dimensions of their initialized U8g2 logical coordinate space; registration rejects a target whose advertised dimensions disagree with that space.

#define SOLAR_OS_BOARD_DISPLAY_CONTROLLER "ST7305"
#define SOLAR_OS_BOARD_DISPLAY_WIDTH 400
#define SOLAR_OS_BOARD_DISPLAY_HEIGHT 300

#define SOLAR_OS_BOARD_PIN_LCD_DC GPIO_NUM_5
#define SOLAR_OS_BOARD_PIN_LCD_CS GPIO_NUM_40
#define SOLAR_OS_BOARD_PIN_LCD_SCK GPIO_NUM_11
#define SOLAR_OS_BOARD_PIN_LCD_MOSI GPIO_NUM_12
#define SOLAR_OS_BOARD_PIN_LCD_RST GPIO_NUM_41
#define SOLAR_OS_BOARD_PIN_LCD_TE GPIO_NUM_6

The built-in ODROID-GO target uses the ILI9341 TFT driver on the board VSPI bus:

#define SOLAR_OS_BOARD_DISPLAY_CONTROLLER "ILI9341"
#define SOLAR_OS_BOARD_DISPLAY_WIDTH 320
#define SOLAR_OS_BOARD_DISPLAY_HEIGHT 240
#define SOLAR_OS_BOARD_DISPLAY_NATIVE_WIDTH 240
#define SOLAR_OS_BOARD_DISPLAY_NATIVE_HEIGHT 320

#define SOLAR_OS_BOARD_PIN_LCD_DC GPIO_NUM_21
#define SOLAR_OS_BOARD_PIN_LCD_CS GPIO_NUM_5
#define SOLAR_OS_BOARD_PIN_LCD_SCK GPIO_NUM_18
#define SOLAR_OS_BOARD_PIN_LCD_MOSI GPIO_NUM_23
#define SOLAR_OS_BOARD_PIN_LCD_MISO GPIO_NUM_19
#define SOLAR_OS_BOARD_PIN_LCD_BL GPIO_NUM_14

The Elecrow CrowPanel target exposes its SSD1683 e-paper controller as a fixed early display0 expansion attachment on the board-defined spi0 bus:

#define SOLAR_OS_BOARD_DEFAULT_EXPANSION_DEVICES { \
    { \
        .driver = "ssd1683", \
        .name = "display0", \
        .binding_count = 8, \
        .bindings = { \
            {SOLAR_OS_EXPANSION_BINDING_SPI_BUS, .target = "spi0"}, \
            {SOLAR_OS_EXPANSION_BINDING_SPI_CS, .target = "spi0", .value = 45}, \
            {SOLAR_OS_EXPANSION_BINDING_GPIO, "dc", .value = 46}, \
            {SOLAR_OS_EXPANSION_BINDING_GPIO, "reset", .value = 47}, \
            {SOLAR_OS_EXPANSION_BINDING_GPIO, "busy", .value = 48}, \
            {SOLAR_OS_EXPANSION_BINDING_GPIO, "power", .value = 7}, \
            {SOLAR_OS_EXPANSION_BINDING_PARAMETER, "clock", .value = 10000}, \
            {SOLAR_OS_EXPANSION_BINDING_PARAMETER, "panel", .value = 0}, \
        }, \
    }, \
}

The fixed bindings select GPIO12 SCK, GPIO11 MOSI, GPIO45 CS, GPIO46 D/C, GPIO47 reset, GPIO48 BUSY, GPIO7 display power, a 10 MHz SPI clock, and the automatic Elecrow panel profile. That profile detects the original or green-sticker revision from BUSY behavior and defaults to rotation 2. Runtime Waveshare V2 attachments use panel profile 3, rotation 0, and 2 MHz by default.

Its refresh=auto default performs fast updates, skips unchanged frames, and inserts a full waveform on the first update and after every 19 fast updates to limit ghosting. display mode display0 refresh=fast forces fast updates and display mode display0 refresh=full forces the full waveform.

Different display controllers should get a separate driver and board display binding instead of overloading the ST7305 or ILI9341 macros.

The runtime path is:

main.c
  -> early fixed expansion attachments
    -> services/solar_os_<controller>_display.c
      -> drivers/<concrete_display_driver>.c
      -> generic board-display backend for display0
  -> solar_os_display target display0

main.c, terminal, and graphics services do not include concrete display driver headers. A board panel is listed as a fixed expansion device and is registered with the display service as display0, source=board, and role=primary. When that controller driver is not already attached, a runtime attachment uses another name and registers an auxiliary target with source=expansion. Each of these controller drivers supports one attached instance at a time.

Storage, I2C, Sensors, RTC, And Audio

Declare built-in peripherals as fixed devices using the same catalog entries as runtime expansions. Add the protocol backends that their static buses need and use storage_expansion for either SDMMC or SDSPI:

[build]
drivers = ["storage_expansion", "i2c_esp_idf", "spi_esp_idf"]
capabilities = ["sd", "i2c", "spi", "expansion_i2c", "expansion_spi"]

[[buses]]
name = "i2c0"
protocol = "i2c"
sharing = "shared"
port = "I2C_NUM_0"
sda = 13
scl = 14

[[devices]]
driver = "sdmmc"
name = "storage0"
bindings = { clk = 38, cmd = 21, d0 = 39 }

SDSPI uses a named SPI bus and bindings = { spi = "spi0", cs = 22 } instead. The package driver consumes the fixed-device bindings and is not selected through board-pin #ifdef branches. Boards with a switched card supply can additionally define SOLAR_OS_BOARD_PIN_SD_POWER and SOLAR_OS_BOARD_SD_POWER_ACTIVE_LEVEL; the shared storage adapter enables that rail before probing or mounting the card.

RTC, sensor, battery, and audio devices follow the same rule. See the Waveshare manifest for PCF85063, SHTC3, battery ADC, and ES8311/ES7210 examples. See the ODROID-GO manifest for ESP32 DAC output and amplifier-enable bindings. The Waveshare PCF85063 interrupt is a fixed optional device binding on GPIO15; other PCF85063 attachments can omit it when the interrupt output is not wired.

RTC chip adapters register with the generic RTC service. That service presents their clock/calendar operations to the shared time service and retains generic RTC topology such as an optional interrupt GPIO. Providers advertise optional alarm, countdown, and interrupt-status operations; an RTC that only keeps time remains a valid provider. The PCF85063 adapter implements all three optional operations. Countdown periods from 1 to 255 seconds are supported directly; longer exact-minute periods are supported through 4 hours 15 minutes. Power, alarm, shell, and script features must use the RTC service rather than including a concrete RTC driver.

The runtime path follows the same pattern as display:

services/solar_os_<service>.c
  -> solar_os_board_<class>_*
    -> board/solar_os_board_<class>_<driver>.c
      -> drivers/<concrete_driver>.c

Services and applications should include the board abstraction headers, not concrete driver headers such as sd_card.h, rtc_pcf85063.h, audio_codec_board.h, shtc3.h, or battery_adc.h.

Validation Checklist

Before committing a new board target:

  1. Build the new environment:

``sh pio run -e my_board ``

  1. Rebuild the Waveshare environment to catch shared regressions:

``sh pio run -e solar_term ``

For changes touching ESP32 classic support, ILI9341 display, SD-SPI, ESP32-DAC audio, buttons, or ADC D-pad input, also build ODROID-GO:

``sh pio run -e odroid_go ``

  1. Check the compile log for low-level drivers. A headless board should not
  2. compile display, SD, audio, battery, sensor, or GPIO drivers unless those capabilities were explicitly enabled.

  1. Flash and verify boot:

``sh pio run -e my_board -t upload ``

  1. On the device, run:

``text status port list pkg ``

  1. Try unsupported hardware commands and confirm they fail cleanly, for example:

``text disk status audio status battery status ``

  1. If the board has no display, confirm the primary shell starts on uart0 and
  2. that cdc0 can still be claimed by a job when needed.

Quick reference

Select the PlatformIO environment matching the physical target. Board profiles declare capabilities and drivers; flavors select packages within those capabilities. The built-in target table, pin rules, display/storage/audio details, porting procedure, and validation checklist are maintained here.

Boards and firmware · SolarOS manual

Firmware Packages and Flavors

SolarOS package selection is declared in packages/solar_os_packages.toml. Flavor files normally select granular groups. The generator translates those groups into internal packages, resolves package dependencies, and removes packages unsupported by the target board. A maintainer can use individual package overrides for unusual build work, but packages are not part of the normal flavor-configurator workflow.

service.ppp owns PPP network negotiation, authentication, DNS, routing, and ESP-NETIF state independently of any modem. A consumer supplies byte-stream write access and either a read callback or received byte chunks. Optional link start and stop hooks let a modem dial and hang up around the same generic PPP session. expansion.sim7670 composes this service with its UART and AT-command adapter; other serial, USB, or radio transports can reuse it without registering a cellular modem.

service.streams owns the dynamic typed endpoint registry. Sensor, port, and audio providers register their endpoints there at runtime. service.audio also owns audio-device discovery; devices refer to their capture and playback stream IDs instead of exposing a board-specific global data path. It has no board-audio capability requirement. Concrete audio driver packages publish devices and endpoints when attached, including immutable board-default attachments for built-in hardware. The independent service.audio-codecs package owns incremental compressed-audio decoding, so file players and network sources can share the same decoder without owning an audio device.

expansion.audio-pwm depends on the generic audio and expansion services. On a board with expansion PWM it can therefore add a runtime playback device even when no built-in codec or DAC exists. expansion.pcm5102 follows the same ownership model on boards with the expansion_i2s capability and provides both the PCM5102-specific and generic i2s-output playback drivers. expansion.pcm1808 uses that model for a four-GPIO I2S capture device, while expansion.es7210 adds I2C-controlled ES7210 stereo capture. The capability guarantees a spare I2S controller and runtime-safe GPIOs; individual driver binding validation enforces the required signal count. These packages are pruned from boards such as ODROID-GO that cannot expose an I2S controller for expansion use. driver.audio-es8311 provides the es8311-es7210 and es8311-duplex drivers only for ESP32-S3 targets with I2C and expansion I2S resources. driver.audio-esp32-dac provides esp32-dac only for classic ESP32 targets. Both use the generic audio backend; a board with built-in audio declares a fixed default attachment instead of compiling a separate board adapter. The driver.display-st7305, driver.display-st7796, driver.display-st7789, driver.display-ili9341, driver.display-cvbs-pal, driver.display-vga32, and expansion.ssd1683 packages use the same model. Each package registers an expansion driver and a board with that integrated panel declares an immutable early display0 attachment. SSD1683, ST7305, ST7796, and ILI9341 are available on both ESP32 and ESP32-S3; ST7789 is currently ESP32-S3-only. The I2S-based CVBS PAL and VGA32 implementations remain specific to classic ESP32. Generic services do not select these implementations with driver-specific preprocessor branches. expansion.ssd1683 uses a named SPI bus and claimed CS, D/C, reset, BUSY, and optional power pins. Runtime attachments register an auxiliary display target; Elecrow declares the same driver as its fixed primary display. Automatic mode uses changed-frame partial windows. expansion.cardkb polls the M5Stack Unit CardKB at its fixed I2C address and publishes its character taps and navigation keys through the shared input service used by shells and foreground apps. board.cl32-core is required only by the CL-32 profile. Its fixed core0 attachment polls the integrated ATmega808 keyboard FIFO and publishes press/release transitions through keyboard0. It also publishes the AVR's battery-voltage, USB-power, and charging measurements through battery0; it is not a selectable expansion hardware group. expansion.gpio-keys, expansion.ps2-keyboard, and expansion.ps2-mouse compose physical buttons, keyboards, and relative mice through the same device lifecycle. expansion.analog-joystick consumes two scalar streams and publishes normalized axes without synthesizing key events. Board-integrated devices use the same drivers as default board-selected attachments. expansion.sdspi adds removable SPI microSD storage to boards that do not have built-in SD hardware. It uses a named expansion SPI bus and mounts at /sdcard without changing the internal-flash root filesystem. expansion.sdmmc provides the native SD/MMC host on ESP32 and ESP32-S3. Boards with an integrated slot declare a fixed early storage0 attachment; boards without one can attach the same driver at runtime. Classic ESP32 accepts only its native slot-1 pinout, while ESP32-S3 can route the signals through its GPIO matrix.

Ownership Rules

  • bootstrap is immutable and contains only the runtime and shell needed to
  • start SolarOS. A flavor cannot disable its members.

  • Groups are selection shortcuts only. They cannot own source files or ESP-IDF
  • component requirements.

  • Every source file and component requirement belongs to a package.
  • A driver package can declare its compatible ESP-IDF MCU targets. Target
  • pruning occurs before board-capability pruning, so classic ESP32 and ESP32-S3 implementations are selected before connector and peripheral capabilities are considered.

  • Expansion-driver symbols belong to their driver packages. The flavor
  • generator emits the registry from that metadata; the generic expansion service does not include individual drivers behind package #if blocks.

  • A package lists other packages it needs with depends. Enabling an app or job
  • automatically enables its transitive dependencies. Explicitly disabling a required package is an error.

  • Board capability pruning is applied to the resolved graph. If a dependency is
  • unavailable, its dependants are removed as well.

  • A board can require packages that implement inseparable onboard hardware.
  • These packages and their dependencies are enabled in every flavor before capability pruning; generation fails if the board cannot support them. For example, TTGO VGA32 v1.4 requires the PS/2 keyboard expansion driver used by its default keyboard0 attachment. Built-in defaults are fixed attachment instances, not a separate copy of the controller driver.

Groups are the user-facing feature units shared by flavor TOML files and the configurator. They are deliberately granular: examples include http_client, ftp, logging, gameboy, pcm1808, and st7305. Categories such as Networking, Maintenance, Games, and Expansion hardware only organize the configurator; categories cannot be selected and do not appear in a flavor. Dormant engineering packages such as USB HID are not exposed as groups.

Groups can contain more than one package when users reasonably perceive one feature. For example, ssh includes the SSH and SCP clients, while ftp includes the client and its server job. Package dependencies can add services and other internal support automatically. Those packages remain implementation details rather than becoming additional choices.

The launcher group selects the native graphical launcher and its JSON parser. Board capability pruning removes it from builds without graphics. Its mutable configuration and item state remain cold until the app starts.

The image_viewer and sketch groups select the two media applications. View requires graphics and PSRAM for large decoded images. Sketch requires graphics but has no pointer or PSRAM capability gate: it uses a compact two-bit canvas and discovers absolute or relative pointers at runtime. Its PNG decoder and shared storage browser are package dependencies, while mutable app and canvas state remain cold until the app starts. The four-shade canvas is converted into a cold-allocated monochrome XBM buffer for one opaque graphics blit per redraw.

The separate invaders and gameboy groups select those games independently. Game Boy additionally requires PSRAM, SD storage, and the streaming_display board capability. Its fixed-frequency emulator submits compact INDEX2 frames to the shared bounded- cadence presenter. When service.synth is present, Game Boy also uses the MiniGB APU through the shared synth and audio services.

The writerdeck flavor targets the Elecrow e-paper board with the reader, writer, and notes groups plus selected system, maintenance, and network tools. It excludes general utilities and hardware-diagnostics jobs to stay focused and fit the board's smaller OTA slot.

The rover flavor targets the 4 MiB classic-ESP32 boards. It includes the expansion framework and drivers, networking, media viewing, writing and general utilities, Files, logging, Bridge, and GPIO Keys. It omits the battery monitor, DAQ, SUMP, Logic, games, scripting, Agent, and remote manual synchronization to fit the factory application slot and preserve internal-memory margin. The embedded docs application remains available. Specialized Rover images are created from this portable baseline with os_builder instead of being kept as separate checked-in rover-* flavors.

Network ownership is intentionally split. network.base, service.osc, job.osc, network.wireguard, network.mqtt, network.ssh, network.mail, messaging.gateway, network.http-client, and network.http-server own their individual implementations. Image and document decoding are separate media.image and media.document packages, so selecting app.curl, for example, does not pull MQTT, SSH, mail, or image dependencies.

service.osc owns the bounded OSC 1.0 codec, incoming native-parameter mapping, named volatile outbound bindings, and the osc shell command. job.osc owns the IPv4 UDP socket, peer filtering, rate limit, source sampling, and counters. It consumes the existing stream and control services without making either core service depend on networking.

network.wireguard depends on the generic network service and lwIP. It owns the native tunnel, its bounded route table, persistent client profile, preferred underlay changes, and light-sleep lifecycle. The network service owns the shared lwIP route hook used by Wi-Fi, PPP, WireGuard, and other IP paths. WireGuard is not part of either embedded scripting runtime.

network.http-client owns the shared TLS-enabled HTTP transport used by curl, webradio, and web. It exposes request headers and bodies, redirects, streaming response events, cross-task cancellation, per-I/O timeouts, and an end-to-end deadline. Callers continue to own their worker task and response consumer; see HTTP Client Service for the native API and lifecycle.

service.webradio owns the disk-backed user station catalog on the current storage root. app.webradio combines that catalog with the shared HTTP client, MP3 codec, generic audio output service, and service.signal-widgets. The signal-widget package owns reusable signed-16-bit oscilloscope and DSP spectrum components; it depends on service.dsp, whose eligible ESP32-S3 FFT path uses PIE SIMD. WebRadio requires Wi-Fi but does not require the board-audio capability: a headless board can include the app and later gain a default audio output from a runtime-attached expansion. The foreground app owns separate network/decode and steady playback workers joined by a PSRAM-preferred PCM jitter buffer. Suspending its UI leaves those workers running; closing the app stops them and releases their resources.

app.player combines the shared audio and codec services with a persistent playlist on the current storage root, the reusable storage browser, signal widgets, and the reusable cassette widget. Its file browser selects existing WAV/MP3 files. Player has no built-in-audio capability requirement, so a runtime-attached output device can satisfy playback.

app.recorder is the interactive counterpart to arecord. It combines the generic audio and stream services with the same storage browser, cassette, oscilloscope, and spectrum packages as Player. It records PCM WAV from a runtime-selected capture stream and does not require built-in board audio. Its monitor transport loops the selected capture stream to the current default output and visualization widgets without opening a recording file. Recorder allocates its app-lifetime state in PSRAM on PSRAM boards and releases it when the application closes. Its browser, widgets, conversion buffers, and PCM queue also use PSRAM. Its 8 KiB capture/file worker and the reusable audio-player sink are admitted as user-started foreground transports with internal stacks, and continue while the Recorder UI is suspended. Capture and sink timing, plus cache-disabled filesystem access, therefore do not depend on a PSRAM stack. Recorder stores its selected input, destination folder, format, device gain, volume, and visualizer under .recorder on the current storage root. Its cassette animates only for record and playback; scope and spectrum continue to present monitored input at the app's nominal 25 Hz graphical cadence. Hardware input gain is exposed through an optional audio-device operation; streams without a gain-capable owning device remain valid recording inputs and show gain as unavailable.

app.funcgen combines service.synth with the shared oscilloscope widget. It has no built-in-audio capability requirement and discovers runtime output devices when opened. Its foreground and widget state prefer PSRAM; the bounded oscillator state and Synth worker remain internal for deterministic rendering. Every displayed control has a corresponding native parameter for physical and MIDI control bindings.

The agent group selects app.agent; its service.agent dependency is added automatically. service.agent owns provider-neutral events, NVS-backed provider configuration, bounded tool-loop policy, a declarative typed-tool registry, and the OpenAI Responses/Chat-Completions adapter. The registry owns provider schemas, output schemas, risk and availability metadata, and shared execution for the system, storage, jobs, and policy-gated script tools. Its NVS-backed off, readonly, confirm, and all policy filters advertised schemas and is enforced again at execution time. It depends on the shared HTTP and JSON services and is pruned from targets without both Wi-Fi and PSRAM. Python and Lua are not dependencies. See Native Agent Service.

app.python and app.lua each depend on service.script_runner. That service defines the common source/file request, bounded output, cancellation, deadline, and completion-status contract. Each interpreter owns its language adapter and single-owner guard. app.agent supplies installed adapters to both the manual agent script command and the typed agent registry without making either interpreter a dependency of the agent package.

Messaging is split into explicit layers: service.messaging owns provider-neutral conversations, history, delivery state, and the pending outbox; messaging.gateway owns gateway configuration and room commands; chat.transport.gateway owns the gateway wire protocol; and job.gateway-sync owns connection lifetime, retries, cursors, and gateway delivery. The bounded store persists full messages on SD and uses the Inbox's compact persistent records as its internal-flash fallback. Stable producer IDs suppress transport replays before they reach either UI. app.chat depends only on the provider-neutral Messaging service and is a foreground view over that shared state; job.chatd remains the independent local gateway server.

Inbox storage and presentation are also separate. Producers such as mail and POCSAG depend only on service.inbox; it owns the bounded persistent ring under /.inbox/, durable read state, producer-key replay suppression, and the persisted opt-in notification-sound policy. Audio-capable builds enqueue the sound through service.audio; Inbox remains available without audio hardware. app.inbox adds the foreground browser and its shell command.

service.synth depends on the device-independent service.audio and provides exclusive real-time PCM rendering for reusable synthesizers and emulated sound hardware. It opens the default playback endpoint at runtime, so firmware without built-in board audio can use a subsequently attached output expansion. The selected audio provider retains hardware-codec, global-volume, and output-serialization ownership; the synth service owns a bounded render block, dedicated worker, client ownership, and deadline/error counters. Native apps can supply an independent signed 16-bit stereo render callback.

SolarOS Builder

Use the host-side os_builder to create or modify a selection, build it, and flash it without editing TOML by hand:

python3 scripts/os_builder.py

The builder starts with board selection and then asks for the update layout. The board establishes the MCU target, capabilities, flash and PSRAM limits, and the drivers that must be included. The layout establishes the application-image limit: 8 MiB and 16 MiB targets can keep two images for OTA updates or use one larger image for serial updates. A 4 MiB target has one fixed serial-update layout. Internal filesystem size follows the layout and is not a user setting. Flavors remain portable because neither the board nor the layout is stored in the flavor TOML.

The main screen contains selectable groups under collapsible category headings. The same group names are used in the TOML file. Categories are headings only, and services and internal packages are never listed as choices. The resolver adds dependencies automatically. [!] marks a group required by the selected board, [+] marks a group whose feature is already present through another selection, and [-] marks a group that the board cannot support.

Each group shows its marginal estimated contribution to the current image. The detail line separates the group's own contribution from additional automatic support. Shared objects and components are counted once, so the number can change with the surrounding selections. Before the first build, the top bar compares the estimated image with the selected layout's image limit.

When available, estimates use text and initialized-data sizes from cached PlatformIO objects and the ESP-IDF component archives declared by package requirements. Otherwise, the builder identifies that it is using a source-size fallback. A specific board, layout, and cached build can be selected from the command line:

python3 scripts/os_builder.py \
  --input flavors/core.toml \
  --output flavors/my-flavor.toml \
  --board solar_term \
  --layout ota

Press b to build the current selection. The builder writes a private working flavor under .pio/os_builder/; it does not overwrite the selected input or the output flavor. During the build it shows a progress bar and the current compiler stage. A successful build replaces the total estimate with the exact firmware.bin size and recalibrates marginal group estimates from that board's artifacts. Change groups and press b again to iterate.

Press f to flash. Flashing is enabled only after a successful build whose board, layout, and groups still match the current screen. Any selection change makes the build stale until it is rebuilt. The upload has its own progress bar and confirmation; --upload-port can select a serial device explicitly. Build and flash failures open a concise error view, with a toggling the full output. Complete logs are replaced on each operation at .pio/os_builder/build.log and .pio/os_builder/flash.log. Press s whenever you want to save the portable group selection as TOML; saving does not end the build/flash loop.

Custom Flavor Example

This flavor selects only the user-facing HTTP client group. The generator adds the internal HTTP transport and other dependencies automatically:

[flavor]
name = "curl-only"
description = "Bootstrap plus the HTTP client app."

[groups]
http_client = true

For maintainer experiments, an optional [packages] table can override individual internal packages. The builder does not expose that table as another selection layer.

Use pkg on the device to inspect the resolved package list.

Quick reference

Select a board, an update layout, and then granular groups. Build to replace the total estimate with a measured image size; adjust, rebuild, and flash when it fits. A flavor stores only portable groups. The board supplies required hardware drivers and removes unsupported groups, while the layout defines the image limit. Internal packages and services are resolved automatically. Use pkg on-device to inspect the resolved firmware.

Join us on: