SolarOS

SolarOS 4.17.7 documentation

User manual.

Start with the SolarOS overview, or browse commands, apps, jobs, hardware, and scripting below. On the device, use help to browse or man -k QUERY to search. Availability depends on your board and firmware flavor.

Getting started4 topics
Shell and storage2 topics
Commands119 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.camera commandInitialize the camera on first use and show the detected sensor, JPEG configuration, orientation, current owner and frame lease, capture count, and last error.can commandList the attached classic CAN controller and bitrate. See Classic CAN.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.controller commandInspect feedback controller configuration, state, output, and errors.cp commandCopy a file or matched set.daq commandPrint DAQ usage.date commandShow or set the local date.deepsleep commandTurn off the ESP32 radios and enter deep sleep. KEY, an armed RTC or scheduled timer, or RESET starts a fresh boot.df commandShow free space on mounted storage volumes.disk commandShow persistent-storage status.display commandList targets and layouts, draw a test pattern, or change driver-specific display settings.dlp commandReceive DLP images on a named display through an exclusive duplex byte stream. See DLP.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.encoder commandConfigure PPR, transmission, polarity or filtering for enc.<name> velocity feedback.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 sensors and touchscreens, 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.irc commandSave an IRC server profile on mounted storage.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.load commandValidate, relocate, run, and unload one native ELF module from storage.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 and managed PSRAM 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 commandOpen the tabbed instance, connection, and message manager.midi commandShow MIDI worker, traffic, parser, and queue status.mkdir commandCreate directories.model commandCheck engine availability and inspect resident models, interfaces, memory, mode, and references.modem commandOpen the modem status and settings TUI.motor commandConfigure the generic motor service and mot.<name> drive sink while unclaimed.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.pipeline commandList OS-owned native image pipelines.pkg commandOpen the native-module package manager.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 commandList registered PWM controllers.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.robot commandInspect joints in the optional external robot library.rtc commandShow the RTC provider, capabilities, interrupt wiring, and alarm/timer owners. Reports unavailable when no RTC is present.rtsp-auth commandList stored RTSP client accounts (host, port, username; never the password).say commandQueue text for offline speech, or stream a plain UTF-8 text file aloud with live progress. Pitch and speed default to 100. File mode submits bounded chunks through one continuous PicoTTS/audio session, completes paths with Tab, and remains responsive; press Esc or Ctrl+C to stop. Start speechd with a PicoTTS voice directory first.sc2 commandShow this device's SC2 end-to-end identity (device ID, fingerprint, prekeys) and the service state. Creates the identity on first use. See man python.sc2.schedule commandList persistent alarms and scheduled shell scripts. Entries show their enabled state, trigger, and action.screen-keyboard commandInspect or control the running screen-keyboard job. Touch or arrow keys select keys; Enter activates the selection.servo commandShow names, outputs, enabled state, and owners in a compact list.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.shutdown commandStop apps and jobs, flush storage, and power down. Only the physical power/KEY button, RESET, or a power cycle starts a fresh boot.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 or a keyboard key to resume.temperature commandList temperature providers or read the default or named sensor.time commandShow or set the local time.top commandPrint a one-shot cumulative FreeRTOS task snapshot. Use ltop for live interval CPU, memory, and stack monitoring.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.
Applications61 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 referenceConfigure providers, inspect tools, and understand request limitsamiga applicationRun Amiga 500 software with the external foreground moduleaplay 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.Apple II Plus applicationRun Apple II Plus software with the external foreground moduleApplication 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 groups conversations by named instance, including IRC, Gateway, MeshCore, and Link. 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.dlp applicationUse a named SolarOS display as a Display Link Protocol receiver. The dlp package requires graphics support. One receiver may run at a time.DOOM applicationRun DOOM IWAD files with the external PrBoom foreground appedit 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.Game Boy applicationRun original Game Boy ROMs with the external foreground modulehelp applicationForeground browser for the package-aware SolarOS manual. The foldable tree groups the topics available in the current firmware and shows whether it is using the setup and recovery guide or a verified downloaded revision. Full guides and scripting API references require the downloaded manual; use help update to install it on devices with Wi-Fi, PSRAM, and SD storage. 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. The playfield scales to the active display; ship movement, shields and collision detection use the same game coordinates. Small displays use bitmap text for the score and lives.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.ltop applicationLive text task monitor for display and port terminals. Unlike the top command's cumulative snapshot, ltop samples the FreeRTOS runtime counters once per second and reports the interval deltas. One progress bar shows the busy share of each CPU core, derived from that core's idle-task delta. Each task percentage is its share of one core, so the task total can approach 200% on a dual-core target. The title shows the current aggregate CPU load instead of the fixed maximum capacity.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.Macintosh Plus applicationRun Macintosh Plus software with the external foreground modulemessages applicationManage messaging instances, connections, and local message history on the display or a VT100 terminal. Run messages without arguments to open the TUI. Existing messages subcommands remain available for shell and scripting use.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.rfscan applicationGraphical receiver-energy monitor for attached radios that report the live_rssi capability. Requires a graphical display and the radio_scanner group. Radio drivers provide live energy readings and validate tuning and bandwidth settings through the common radio API; the app does not select support by radio model.rtsp applicationPlay a trusted-LAN RTSP stream using UDP RTP/JPEG video and RTP/L16 audio.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.sftp applicationTwo-pane SFTP file manager with the same panes, file operations, progress, and key bindings as ftp. The left pane is local mounted storage and the right pane is a remote SSH server's SFTP subsystem. It uses the shared SSH transport, known-host policy, host aliases, and password or public-key authentication.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. Runtime rotation preserves the drawing and its saved dimensions. The canvas fits inside the new viewport, and pointer input follows the fitted image. Gray margins mark space outside the drawable canvas after rotation.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.Tiny386 applicationBoot PC floppy and hard-disk images with the external 386 emulatorview 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.vplay applicationGraphical MPEG-1 media player with optional MP2 audio. The bottom controls show playback status and elapsed time above the volume bar; full screen hides the controls and time. The middle Stop/Play icon indicates pause while paused.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. Direct MJPEG URLs are shown as live video using the same JPEG renderer.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. The GUI uses compact controls, scrolling channels and fitted edit dialogs on small display targets. 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.zoo applicationBrowse the hosted model zoo and install complete model bundles on mounted SD or flash storage. The text interface works on the display and VT100 port shells. It requires the Zoo package, ESP32-S3, PSRAM, and networking.
Background jobs36 topics
Background job referenceConfiguration, ownership, and examples for every background jobBackground jobsStart, inspect, and stop background workersbatmon jobBattery monitor. It periodically samples battery voltage, maintains a smoothed trend, estimates power state, and can request sleep, deep sleep, or shutdown 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.cam-webd jobHTTP access to the fitted camera as one-shot JPEG images or a single-client MJPEG stream.chatd jobLocal SolarOS chat gateway server. It is useful for testing the chat app or for small trusted local networks.controllers jobFeedback-controller worker for named PID and hysteresis instances.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:gestures 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.irc-sync jobMaintains enabled native IRC instances over verified TLS. Each instance has independent reconnect and channel membership state. Configure profiles with irc, then run job start irc-sync. The job accepts no arguments. Use irc status for each connection and job stop irc-sync to stop all IRC connections. Chat and retained messages remain available. See Native IRC.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.meshcore-ble jobMeshCore companion-protocol client for a separate BLE radio device. The job connects to one explicit peer, synchronizes its contacts and channels, drains queued messages, and carries Chat messages through the standard MeshCore messaging provider.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 incoming native-parameter writes, named scalar stream receive bindings, and outbound stream, event-stream or 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.pointer-arrows jobConsume one named pointer source and hold arrow keys while it moves: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.rtspd jobPublish explicitly selected sources as a standard single-client RTSP session. Video uses RTP/JPEG; audio uses RTP/L16 PCM. Each enabled track has its own RTP/RTCP UDP port pair, SSRC, and sender reports, with a shared RTCP CNAME and session start clock.screen-keyboard jobThe screen keyboard supplies touch and keyboard input above the shell, TUIs, and graphical applications on a selected display. Start the background job to arm its activation actions: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.speechd jobOffline text-to-speech queue. The optional job loads a PicoTTS voice from storage into PSRAM and accepts asynchronous requests from native applications, Python, and Lua.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 expansion14 topics
Scripting APIs42 topics
Compatibility I/O modulesUse the legacy single-bus I2C, SPI, UART, and OneWire APIsComputer visionNative QR decoding, statistics, segmentation, filtering, morphology, image difference, and blobsDigital signal processingPortable fixed-point DSP operations, streaming contexts, and script APIsEmotion engineAnimated facial expressions and optional reaction sounds for Python and LuaJet 3D rendererScript-defined 3D scenes using the resident Jet libraryLua API overviewRuntime basics, conventions, and service API topic indexLua audio and control APIAudio and control: audio, synth, dsp, controls, parameters, midi, oscLua bluetooth APIBluetooth: bleLua board, apps, jobs, and identity APIBoard information, apps, jobs, and identity: board, identity, jobs, sessions, appsLua buses and expansion APIBuses and expansion: buses, expansionLua camera, streams, and RTSP APIOwned local stream frames, camera snapshots, native images, and asynchronous RTSP sessionsLua contacts and messages APIContacts and messages: contacts, messagesLua gpio and peripherals APIGPIO and peripherals: gpio, onewire, led, adc, pwm, servo, motor, encoder, 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 SC2 end-to-end chat security APISC2 end-to-end encryption: sc2Lua storage and files APIStorage and files: storageLua text user-interface APIBuild terminal applications from LuaLua time and scheduling APITime and scheduling: time, rtc, scheduleModel bundles and pipelinesChecked model manifests, reusable adapters, and resident file or stream inferenceModel inferenceLoad resident ESP-DL models from storage and execute named tensor inputs and outputsNamed runtime busesCreate and use resource-owned I2C, SPI, UART, MIDI, OneWire, and PS/2 busesNative foreground appsInstall native apps, switch sessions, and use the foreground module APINative image pipelinesOS-owned QR and model processing jobs with bounded latest results and frame timingsNative librariesInstall reusable native libraries and manage their lifetimeNative media and inferenceVersioned image, camera/RTSP, QR, imlib and resident-model interfaces for ELF modulesPython API overviewRuntime basics, conventions, and service API topic indexPython audio and control APIAudio and control: audio, synth, dsp, controls, parameters, midi, oscPython bluetooth APIBluetooth: blePython board, apps, jobs, and identity APIBoard information, apps, jobs, and identity: board, identity, jobs, sessions, appsPython buses and expansion APIBuses and expansion: buses, expansionPython camera, streams, and RTSP APIOwned local stream frames, camera snapshots, native images, and asynchronous RTSP sessionsPython contacts and messages APIContacts and messages: contacts, messagesPython gpio and peripherals APIGPIO and peripherals: gpio, onewire, led, adc, pwm, servo, motor, encoder, 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 SC2 end-to-end chat security APISC2 end-to-end encryption: sc2Python 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 services7 topics
Boards and firmware2 topics

Getting started · SolarOS manual

Browse and refresh documentation

Run help to browse topics, or search and open them from the shell:

man -k connect wifi
man sessions.apps
man --list

In Help, use arrows or Enter to expand a group, then Enter to read a topic. Graphic displays use Reader; text terminals use Less. Closing a page returns to the same place in Help.

Firmware includes setup help and a topic directory. Install the full manual for detailed guides and scripting APIs, or read it at solar-os.eu.

Install or refresh the manual

This requires Wi-Fi, PSRAM, and mounted SD storage. Connect Wi-Fi using wifi on a display or wifi connect SSID PASSWORD from a text shell. Then:

disk lsblk
disk mount
help update
help status

disk mount mounts the default removable volume; use man command.disk to select another volume. help update downloads and verifies the manual for your running firmware version. If the download fails or is interrupted, the previous manual stays active. Once installed, it works offline while the storage is mounted.

After an OS upgrade, a previously downloaded manual remains readable but is marked outdated. Run help update to refresh it. help status shows the firmware and manual versions, download state, and last error.

Return to built-in help

help reset

This stops using the downloaded manual and restores setup help. Cached files may remain on SD. To read the shell's status command page, use help command.status; help status reports the manual's own status.

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 setup and recovery guide. Full topic guides and scripting API references require the downloaded manual. Refreshing requires Wi-Fi, PSRAM, and SD; the installed manual can be read offline while its storage is mounted.

Getting started · SolarOS manual

Navigation and keys

SolarOS accepts input from keyboards, board buttons, wheels, and trackballs. Use the common keys below, or find your device's built-in controls in the target tables. These tables describe the default mappings; custom keyboard profiles can change them.

Alt+Enter means hold Alt, press Enter, then release both keys. A tap means press and release a key. A hold keeps a modifier or layer active while you press another key.

Basic navigation

KeyAction
Up / DownMove between menu items or scroll, depending on the app.
Left / RightMove across the launcher grid; move the cursor or change a value where supported.
EnterSelect an item, open it, or confirm a prompt.
EscapeGo back or cancel; exits apps that support Escape.
Page Up / Page DownMove a page at a time where supported.
Home / EndMove to the start or end where supported.
Alt+EnterToggle full screen in shared text menus. Small menus start in full screen automatically.

Run launcher for the graphical app grid, or type an app's command name at the shell prompt. App-specific controls appear in the app's help and the Application reference. Escape is handled by each app; use the app-close shortcut below when you need to close the foreground app.

Sessions and app shortcuts

ActionLocal display keyboardPort terminal (USB CDC, UART, Telnet)
Close the foreground appCtrl+Alt+DelCtrl+]
Next retained sessionAlt+Tab or Alt+RightCtrl+Z suspends a resumable app and returns to the prompt; use fg to resume it.
Previous retained sessionAlt+LeftUse fg ID to resume a specific retained app.
Move local keyboard focus to another displayCtrl+Alt+ArrowRun session focus TARGET.

Either Alt key works for session switching and display focus, including AltGr on compact keyboards. Switching sessions retains resumable apps; closing an app returns to its display's shell or resumes its parent app. Use session list to see session IDs and Sessions for more controls.

At the shell prompt

KeyAction
EnterRun the typed command.
Up / DownBrowse command history.
Left / RightMove the insertion cursor.
Backspace / DeleteDelete before / at the cursor.
EscapeClear the typed command without running it.
TabComplete a command, subcommand, or path.
Tab twiceList matching completions; at an empty prompt, list commands.

LilyGO T-LoRa-Pager (T-Pager)

ControlAction
Wheel clockwiseDown — next menu item or scroll down.
Wheel counterclockwiseUp — previous item or scroll up.
Press wheelEnter — select/open; emitted when the wheel is released.
Keyboard EnterEnter.
Hold Space + EnterEscape — back/cancel; exits apps that support Escape.
Tap SpaceType a space.
Hold Space + another keyType its symbol; Q–P give digits 1–0.
Hold the Caps-labelled keyShift — use with another key for its shifted character.
Hold AltAlt modifier.
BackspaceDelete the character before the cursor.

The default keyboard profile has no Left, Right, Tab, or Ctrl mappings. The wheel supplies Up/Down, so it cannot move across launcher columns. Use a custom keyboard profile or a compatible external keyboard for those keys, horizontal launcher navigation, and the usual session-switch and app-close combinations. You can also launch apps by typing their command names. Space+Enter remains Escape rather than the universal app-close key.

LilyGO T-Deck Plus

ControlAction
Roll trackball up / down / left / rightOne arrow key in the dominant direction after movement stops.
Tap trackballEnter — select/open by default.
Hold trackballSuspend by default.
Keyboard EnterEnter.
Tap microphone-icon key (Mic)Tab on release — complete a shell command or argument; tap twice to list matches.
Shift + microphone-icon keyCtrl — keep Mic held while pressing the other key.
Sym + microphone-icon keyType 0.
Shift + trackball arrowsExtend the text selection in the editor.
Shift + Mic + trackball left / rightMove the cursor one word left / right.
Alt+CEscape — back/cancel where supported.
Alt+QClose the foreground app.
Alt+BToggle the keyboard backlight for the current boot; the shortcut does not type a character.
Alt + trackball up / downPage Up / Page Down.
Alt + trackball left / rightPrevious / next retained session on the focused display.
Alt + MicNext retained session on the focused display.
Alt+EnterToggle full screen in shared text menus.
Hold Sym + another keyType the symbol from the keyboard's symbol layer.
Hold Shift + another keyType its shifted character.

The trackball press uses the system KEY settings. Change a tap with setterm powerkey short <action> and a hold with setterm powerkey long <action>, or edit the two rows in setterm. Saved choices override these defaults. Any KEY press wakes suspend or sleep; the wake press is consumed until release. Deep-sleep wake starts a fresh boot.

Trackball movement is evaluated in fixed 80 ms windows. The first evaluation presses an arrow; continued rolling keeps it held, using setterm keyrate for repeat. Changing direction releases the old arrow before pressing the new one. The key is released after 80 ms without movement events. Short rolls that meet the minimum vector length produce a press and release. The direction follows the greater net horizontal or vertical movement within each window; opposite movements cancel. Equal diagonals use the most recently moved axis. Each evaluation uses the modifiers from its last movement pulse.

The board attaches the trackball expansion driver as trackball0 and starts the pointer-arrows job. The driver owns the four direction pins and publishes relative pointer movement. The job consumes that source and converts movement to the arrow gestures described above. To use relative pointer input directly:

job stop pointer-arrows

Restore arrow navigation with:

job start pointer-arrows source=trackball0

To reduce sensitivity, stop the job and restart it with, for example, job start pointer-arrows source=trackball0 min-vector=4. min-vector accepts 1..65535 and defaults to 2. Higher values require a longer net movement vector within each 80 ms window. Movement below the minimum releases the arrow and does not accumulate into the next window.

The push switch continues using the system KEY settings in either case. The board starts arrow navigation again at boot; a startup script can stop the job to select relative pointer input, or stop and restart it with a tuned min-vector, on every boot.

For Ctrl, hold Shift and Mic in either order, then press another key or roll the trackball. Shift selects the Ctrl chord and is consumed, so this does not also select text. Shift can be released while Mic continues acting as Ctrl; releasing Mic releases Ctrl. A plain Mic emits Tab on release, allowing the Ctrl chord to be recognized without triggering completion. Sym+Mic retains its digit 0 mapping.

Keyboard keys report their actual presses and releases, supporting held controls in foreground apps. Trackball arrows also report presses and releases; setterm keyrate controls repeat for typing and held trackball arrows.

The keyboard light starts disabled on every boot, retaining its saved brightness. Alt+B toggles the light for the current boot. Suspend and light sleep turn it off temporarily, then restore the user's on/off choice on wake. An explicit setterm backlight level or follow applies that setting for the current boot. Use setterm backlight 0 to turn off the keyboard light, or setterm backlight 100 for full brightness. Values from 0 to 100 set it independently of display brightness; setterm backlight follow makes it track the display. Alt+B does not save changes across reboots. When the configured brightness is zero, toggling on uses 50%. The setterm backlight query shows the configured brightness, including while the light is toggled off.

Pi0cket CL-32

ControlAction
Arrow keysCorresponding arrow key.
OKEnter — select/open.
CancelEscape — back/cancel where supported.
Hold FileLeft Alt modifier.
Hold MenuLeft Ctrl modifier.
File + Left / RightPrevious / next retained session on the focused display.
File+OKToggle full screen in shared text menus.
Hold ShiftShift modifier.
Hold FnSelect the alternate symbol and function-key layer.
Fn + keypad 1–9 / 0F1–F9 / F10.
Fn + Up / DownPage Up / Page Down.
Fn + Left / RightHome / End.
Fn + Yes (OK) / No (Cancel)Previous / next retained session on the focused display, once per key press.

Shift and Fn stay active only while held. Release them to return to normal typing.

The built-in keyboard resumes suspend and wakes sleep or deepsleep. Wake from deepsleep starts a fresh boot. Keyboard, USB-power, and charging notifications share the wake interrupt, so power changes can also wake sleep.

ODROID-GO

ControlAction
D-padCorresponding arrow key, when ADC D-pad support is included in the firmware.
AEnter — select/open.
BSpace.
SELECTEscape — back/cancel where supported.
MENUClose the foreground app.
VOLUMEToggle audio mute.

The built-in buttons do not provide a text keyboard or Alt/Ctrl modifiers. Use a compatible external keyboard or a port terminal for typing commands and combinations that need those keys.

See Boards for target capabilities and Input for input sources and keyboard layout settings. To customize a supported keyboard profile, use the website's Tools → Keyboard configurator.

Quick reference

Arrows navigate, Enter selects, and Escape goes back where supported. Close a local display app with Ctrl+Alt+Del; in a port terminal use Ctrl+]. Alt+Tab or Alt+Right selects the next local session; Alt+Left selects the previous one. Ctrl+Z suspends a resumable port app; fg resumes it. T-Pager: wheel is Up/Down, press is Enter, Space+Enter is Escape, held Space selects symbols, and held Caps is Shift. Its default profile lacks Left, Right, Tab, and Ctrl. T-Deck Plus: trackball supplies arrows and Enter, Mic is Tab, Shift+Mic is Ctrl, Alt+C is Escape, and Alt+Q closes the app. Pi0cket CL-32: OK is Enter, Cancel is Escape, File is Alt, and Menu is Ctrl. ODROID-GO: D-pad supplies arrows, A is Enter, B is Space, SELECT is Escape, and MENU closes the app.

Getting started · SolarOS manual

SolarOS overview

SolarOS turns an ESP32 board into a pocket computer or a small terminal. Use the shell to manage files and hardware, open apps, run background jobs, or write Python and Lua programs. A board can have its own screen and keyboard, or you can use it through a serial or network terminal.

How the parts fit together

LayerRoleExample
Board profileDescribes the hardware, pins, and built-in devices.A screen, SD slot, and keyboard.
DriversCommunicate with particular hardware.A display controller or temperature sensor.
ServicesGive programs a shared way to use hardware and OS features.Storage, audio, time, input, and networking.
Commands, apps, jobs, and scriptsUse services to do useful work.Read a file, play music, or record sensor data.

For example, a sensor driver reads a chip, the sensor service exposes its temperature, and a command or script reads that value. Programs can use the same service with built-in hardware or an attached expansion device.

Boards, capabilities, and packages

A board profile describes a supported hardware target. Capabilities describe what it can support, such as graphics, Wi-Fi, or expansion I2C. A firmware flavor selects a set of compiled packages: services, drivers, apps, and jobs. Features also depend on the hardware being present.

Check your device before following an example:

board
pkg system
apps
jobs

Boards lists targets and wiring. Packages and flavors explains how to choose or build an image.

SolarWriter combines the CrowPanel 5.79-inch wide e-paper display with INMP441 capture, shared PCM5102/MAX98357 playback, and an LC709203F battery gauge. Its fixed attachments use the same reusable expansion drivers available on other compatible boards.

Reusable runtime drivers and buses

A bus is a named connection such as i2c0 or spi0. A driver knows how to use a particular device on that connection. An attachment is a configured instance of the driver, with a device name and wiring.

Built-in devices attach at boot and are fixed. Where the board and firmware allow it, you can attach more devices at runtime using the same drivers. Attaching hardware publishes it to the relevant services: an added display becomes a display target, for example.

expansion bus list
expansion drivers
expansion devices

Pins, buses, ports, and devices can have an owner while in use. If a resource is busy, inspect its owner and stop that user before reusing it. See Named buses and Expansion devices.

Shells, apps, sessions, and jobs

  • A shell accepts commands on a display or terminal connection. Paths, history, and typed input belong to that shell.
  • An app is an interactive foreground program, such as Files or Reader. Start it by name, for example files.
  • A session holds a shell or app on a particular display or port. Resumable apps keep their state when you switch away.
  • A job works in the background, such as a logger or HTTP server. It continues when you switch apps. Start and stop jobs explicitly.
session list
job start batmon 60
job status batmon
job stop batmon

On a display, Alt+Tab switches sessions and Ctrl+Alt+Del closes the app. On a port terminal, Ctrl+Z suspends a resumable app, fg resumes it, and Ctrl+] closes it. See Sessions, Apps, and Jobs.

Displays, terminals, and input

A display target is a named screen or canvas. It can be a physical panel, joined panels, a split-screen viewport, or a virtual display. Each target has its own active session. A port carries bytes, for example over UART, USB CDC, or a Link radio connection; port shells use terminal text.

Local input focus selects which display receives keyboard and board-control input. Changing focus does not move an app. Touch and browser input can target a specific display. TUI apps use text menus; GUI apps use graphics. Check an app's requirements before launching it from a port shell.

display list
port list
session focus

See Navigation and keys for common shortcuts and target control mappings. See Display commands, Sessions, and Input and audio.

Streams, parameters, and controls

A stream is a named source or destination for data. Scalar streams carry measurements such as millivolts; other streams carry bytes, audio, or video. Use stream list to discover them. Apps and jobs can plot, record, or process the types they support.

A parameter is a named setting published by a running native app, such as synth.filter.cutoff. A control maps an input stream or script value to an app parameter or MIDI controller. For example, a potentiometer can adjust Synth's filter through the controls job.

control parameters
control list
control bindings

See Continuous controls for a complete example. Image pipelines process camera or RTSP streams in the background and retain their latest result.

Files, settings, and automation

Shell paths use / for the default storage volume. Other mounted volumes can include /flash and /sdcard. Use disk status and df to check storage. Files hold documents, scripts, captures, and installed apps.

Many system settings are saved across reboots in NVS, the device's settings store. Runtime buses, attachments, control mappings, and job starts can be restored with the startup script. Run setterm startup to find its path. Schedules run actions at a time or interval; they do not launch foreground apps.

Python and Lua use the solaros APIs to access these services. Playground installs script apps. On supported ESP32-S3 boards with PSRAM, native modules add compiled apps, jobs, or drivers. Install only code you trust.

See Storage, Scripting, Playground, Native apps, and Scheduling.

Connections, power, and updates

The network service manages connections and routes; network status shows the current path. power status shows the power profile and sleep settings. Suspending the display keeps jobs running; deep sleep starts a fresh boot on wake. Firmware updates use ota on boards with an OTA layout. See Networking and Power and updates.

Finding documentation

help
man -k connect wifi
man sessions.apps

The website has the full manual. Device firmware includes setup help and a topic directory. With Wi-Fi, PSRAM, and SD storage, help update installs the manual for your firmware version; it then works offline while the card is mounted. See Documentation setup.

Quick reference

Board profiles describe hardware; capabilities and firmware flavors determine which packages are available. Drivers attach devices to named buses; services expose them to commands, apps, jobs, and Python/Lua. Apps run in foreground sessions on displays or ports; jobs run in the background. Local input focus selects a display. Streams carry data; controls map values to app parameters or MIDI. Files hold user data, NVS holds persistent settings, and startup scripts restore runtime configuration. Discover names with board, pkg, apps, jobs, display list, port list, expansion status, and stream list. Use help or man to find usage; help update installs the full manual on supported devices.

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 are available only when their services are included in the firmware. Start with board, pkg, display list, and expansion status.

Keep scripts responsive

Long-running loops should check solaros.should_exit() and sleep briefly so the OS can handle input. For example:

import solaros

while not solaros.should_exit():
    print(solaros.time.uptime())
    solaros.time.sleep_ms(1000)

Close opened files and release displays, buses, and GPIOs when finished, even after an error. Use try/finally in Python and pcall with 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

Tables fit the current shell width on display and port shells. Long rows are clipped at the right edge; trailing values or columns can be omitted. Widen the terminal to see more columns. One cell is reserved to prevent automatic wrapping.

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

When the path displayed in the shell prompt exceeds half the terminal width, the prompt shows only the last directory, such as /logs/. The working directory and relative path resolution remain unchanged.

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.

On display shells and VT100 port shells, Backspace removes the character before the command-line cursor. Delete removes the character under the cursor without moving it; it does nothing at the end of the line.

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.
inboxinbox 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.
inboxinbox 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.
contactscontacts 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.
messagesmessagesOpen the tabbed instance, connection, and message manager.
messagesmessages statusShow archive, 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.
messagesmessages clear <instance|instance-id|all>Clear retained history and owned Inbox projections, including for removed instances. Unrelated Inbox sources remain.
messagesmessages remove <instance|instance-id>Remove a provider instance, connection credentials, pending sends, history and Inbox notifications.
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.
ircirc add <instance> <host> <port> <nick>; irc set <instance> <host> <port> <nick>Save an IRC server profile on mounted storage.
ircirc status; irc connect|disconnect <instance>Inspect or enable individual IRC connections maintained by irc-sync.
ircirc remove <instance>Remove an IRC instance, its credentials, history and associated Inbox notifications.
ircirc join|query <instance> <channel|nick>; irc leave <conversation-id>Open rooms and private conversations through shared Messaging.
ircirc sasl <instance> <account> <password>; irc sasl <instance> off; irc pass <instance> <password|off>; irc ca <instance> <PEM-path|default>Set optional authentication and TLS trust. See man messaging.irc.
sc2sc2 id; sc2 statusShow this device's SC2 end-to-end identity (device ID, fingerprint, prekeys) and the service state. Creates the identity on first use. See man python.sc2.
sc2sc2 safety <peer>Show the safety number shared with a stored peer, to compare out of band. <peer> is a 32-hex device ID, a unique prefix (6+ hex digits) of a known peer, or an alias.
sc2sc2 verify <peer>; sc2 unverify <peer>Mark the safety number with a peer as compared (or take that back). Chat shows sc2 DMs as (verified) or (unverified); the record is kept in the SC2 store (up to 64 peers). Verifying needs the peer's keys (message it first).
sc2sc2 alias; sc2 alias <name> <peer>; sc2 alias <name> offList, save or delete peer nicknames (up to 16, in the SC2 store) for sc2 safety and messages query <sc2-instance> <peer>.
sc2sc2 selftest; sc2 bench [n]Run the SC2 known-answer and two-device session tests in RAM, or time the primitives and a DM/room round trip. Neither touches stored keys.
sc2sc2 room create ["name"] / sc2 room invite <room-id|name> / sc2 room join <sc2i:...|@file> / sc2 roomsCreate a private SC2 room, print its invite again, join with an invite, or list names, IDs and state. Quote names containing spaces (up to 48 UTF-8 bytes). Admission requires a valid end-to-end invite proof. Then messages send <conv> <text>; messages leave <conv> leaves. SC2 commands, peers and rooms support Tab completion. See man messaging.
gatewaygateway configure <url> [token]Save gateway connection settings. The gateway uses the global SolarOS user identity.
messagesmessages classes / messages instancesList available provider classes or configured instances.
messagesmessages add gateway <name> <url> [token]Save an independent Gateway instance. Enable it with messages connect <name> and start gateway-sync.
messagesmessages configure <name> <url> [token]Change a Gateway instance's connection settings.
messagesmessages add irc <name> <host> <port> <nick>Save an independent native IRC connection.
messagesmessages add meshcore <name> radio <radio> <profile>Save a MeshCore instance on a local radio.
messagesmessages add meshcore <name> ble <address> <type> [pin]Save a MeshCore companion binding.
messagesmessages add link <name> <transport>Bind an independent Chat projection to a named Link transport.
messagesmessages add sc2 <name> <domain> [admit] [key=<b64>]Add an SC2 relay from its signed descriptor at https://<domain>/.well-known/solaros-relay (CA and host name checked; never plain HTTP). The relay key (and the fallback relays and their keys) are pinned; every login must prove the key, a different key is refused. key= must match the descriptor when given. messages configure <name> key <b64>|off changes the pin. See man messaging.
messagesmessages add sc2 <name> chats://<host>:<port>[#pin=sha256/<b64>] [admit]Save a disabled SC2 end-to-end relay instance. Enable it with messages connect <name> and start its worker with job start sc2-sync; messages disconnect <name> disables it. messages configure <name> admit <code>|off or pin <pin>|off change it; fallback <url> [<url> <url>]|off sets up to three fallback relays (failover and return to the primary; sc2 relay shows which is in use). With a pin, the pinned key alone authenticates the relay (self-signed certificates work); without one the CA bundle is used. See man messaging.
gatewaygateway connect [url] [token]Enable gateway synchronization, optionally updating settings.
gatewaygateway disconnectDisable gateway synchronization.
gatewaygateway roomsList known and joined gateway rooms.
gatewaygateway 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.
rtsp-authrtsp-authList stored RTSP client accounts (host, port, username; never the password).
rtsp-authrtsp-auth set <host[:port]> <user> <password>Save a Basic/Digest account used when an rtsp URL has no inline userinfo. Four accounts.
rtsp-authrtsp-auth clear [host[:port]]Remove one stored account, or every account when the host is omitted.
pocsagpocsag statusShow POCSAG receiver configuration, counters, correction statistics, and RSSI.
pocsagpocsag 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 and remaining service settings, then reboots. Migrated Chat credentials, radio profiles, and ESP-NOW peers are files on mounted storage. 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. Firmware carries a setup and recovery guide and a searchable topic directory; full guides and scripting API references require the downloaded manual. help reset returns immediately to the setup and recovery guide.

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.
pkgpkgOpen the native-module package manager.
pkgpkg systemPrint compiled package groups and build units.
pkgpkg availableVerify the signed native-module catalog and print its compact module list.
pkgpkg installedList installed application, job, driver, and library modules.
pkgpkg install <module>Download, verify, validate, and atomically install a native module; Ctrl+C, Esc, or the app-exit key cancels.
pkgpkg remove <module>Remove an installed native module.
pkgpkg unload <library>Unload an unused library while keeping its installed file.
loadload <file.elf> [args...]Validate, relocate, run, and unload one native ELF module from storage.
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.
engineengine [status|reset]Print or reset generic engine utilization counters for CPU/SIMD-style backends and vector bulk operations.
displaydisplay [list]; display layouts; display test <target>; display mode <target> [mode]List targets and layouts, draw a test pattern, or change driver-specific display settings.
displaydisplay orientation <target> [0|90|180|270]Show or change orientation for text, graphics and touch. Runtime only; does not change NVS.
displaydisplay scale <target> [1..4]Show or change runtime integer pixel scaling without changing NVS.
displaydisplay relax [<target> [seconds]]Run timed native black/white recovery cycles on supported EPDs, then restore the saved image and refresh mode. Without arguments, use the first supported display for 1 second.
dlpdlp serve <display> stream=<byte-stream>; dlp status; dlp stop <display>Receive DLP images on a named display through an exclusive duplex byte stream. See DLP.
displaydisplay join <name> --horizontal|--vertical <target> <target> [target ...]; display unjoin <name>Join two to four physical targets into one logical display, or remove the join.
displaydisplay split <target> --horizontal|--vertical <first> <second>; display unsplit <target>Split one physical target into two equal logical viewports, or remove the split.
inputinput [status|keyboard|touch|mouse|joystick|dpad|buttons|gesture]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.
inputinput 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.
inputinput emit <key|chord>Emit a local key tap, such as RIGHT, ALT+RIGHT, ENTER, or one literal character, through the normal input-focus path.
inputinput keymap [<source> [show|load <path>|reset]]List source mapping support, inspect a map, load a validated JSON file, or restore a registered source profile. See source keymaps.
gesturegesture [status]List gesture-capable sensors and touchscreens, readiness, and the gesture kinds each source advertises.
gesturegesture bind source=<name|*> gesture=<name> [direction=<name|*>] [cooldown=<ms>] -- <command> [args...]Configure a volatile gesture-to-command rule for the gestures job.
gesturegesture bindingsShow listener state and list volatile gesture bindings, trigger counts, queue drops, cooldowns, and commands.
gesturegesture unbind <id|all>Remove one volatile gesture binding or all of them.
screen-keyboardscreen-keyboard [status]; screen-keyboard show|hide|toggle; screen-keyboard mode full|symbolsInspect or control the running screen-keyboard job. Touch or arrow keys select keys; Enter activates the selection.
statusstatusPrint a compact system summary, including the last foreground-app exit code.
uptimeuptimePrint elapsed time since boot.
memmem [policy]Print heap and managed PSRAM status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.
memmem reclaimAsk registered owners to release rebuildable idle data, compact eligible buffers, and show recovered bytes and the largest free block before and after.
memmem compactCompact unpinned movable buffers and show the largest free block before and after. Raw pointers, DMA and stacks stay fixed.
memmem ownersShow managed-region bytes, allocation counts, movable bytes and largest object by owner tag. SDK-region totals remain in mem policy.
toptopPrint a one-shot cumulative FreeRTOS task snapshot. Use ltop for live interval CPU, memory, and stack monitoring.
sleepsleepEnter explicit light sleep.
deepsleepdeepsleepTurn off the ESP32 radios and enter deep sleep. KEY, an armed RTC or scheduled timer, or RESET starts a fresh boot.
shutdownshutdownStop apps and jobs, flush storage, and power down. Only the physical power/KEY button, RESET, or a power cycle starts a fresh boot.
suspendsuspendTurn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY or a keyboard key to resume.
powerpower [status]Show the selected and effective profiles, suspend state, sleep policy, and wake statistics.
powerpower profile [performance|balanced|battery|lowpower]Show or save the power profile.
powerpower idle [off|seconds]Show or configure the display-shell idle light-sleep timeout.
powerpower key [short|long] [action]Show or configure the dedicated KEY short- and long-press actions.
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 or a keyboard 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.
rtcrtc 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.
scheduleschedule add <name> in <duration> <alarm|run script>Add a one-shot monotonic schedule. Durations use an s, m, h, or d suffix.
scheduleschedule add <name> every <duration> <alarm|run script>Add a recurring monotonic interval schedule.
scheduleschedule 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.
scheduleschedule add <name> daily HH:MM[:SS] <alarm|run script>Add a daily schedule in configured local time.
scheduleschedule 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.
settermsetterm cursor [block|underscore] [blink|steady]Show or save the global local-terminal cursor style. Defaults to block blink.
settermsetterm --display <target> [orientation|scale|font|textsize|palette|statusbar] [value]Show or change a named display's profile; orientation and scale are persistent, other targeted settings are volatile.
settermsetterm scale [1..4]Set and save scaling for the current display, or display0 from a port shell.
settermsetterm orientation [0|90|180|270]Set and save orientation for the current display, or display0 from a port shell.
settermsetterm font [mono|compact]; setterm textsize [8|10|12|14|16|18|20]Show or set the terminal font and text size.
settermsetterm 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.
settermsetterm statusbar [show|hide]Show or hide the graphical shell status bar.
settermsetterm brightness [0..100]Show or set display brightness.
settermsetterm backlight [0..100|follow]Show or set auxiliary lighting, such as the keyboard backlight, independently or following display brightness.
settermsetterm profile [vt100|ansi|dumb]; setterm charset [utf8|ascii]Configure escape sequences and TUI glyph output for the current port shell.
settermsetterm keyboard [us|de]; setterm powerkey [short|long] [action]Show or set the keyboard layout and dedicated KEY action.
settermsetterm keyrate [off|1..60 [delay-ms]]Show or set the shared keyboard and button repeat policy.
settermsetterm ble [default|on|off]Show or set the BLE preference for the next boot.
settermsetterm timezone [UTC|UTC+/-offset|Europe/Berlin|POSIX-TZ]Show or set the timezone used for local time.
settermsetterm 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.

Native ELF modules

load is the low-level maintainer interface for the native-module runtime. It is compiled only for ESP32-S3 boards with PSRAM. The command resolves the ELF path through the calling shell's current directory, reads at most 2 MiB into PSRAM, validates every ELF header and table boundary, checks for an Xtensa ELF32 dynamic object, relocates it into executable PSRAM, and calls its entry point with the supplied arguments.

Native modules import the single versioned solar_os_native_host_v1 symbol. The returned function table reports the ABI version, target, firmware version, and provides UTF-8 output through the shell that invoked load. Its optional get_service extension discovers versioned image, video, vision and inference interfaces; see Native media and inference. Existing modules using the original output-only prefix remain compatible. Arbitrary SolarOS internals are not exported through this interface. Native ELF code is not sandboxed; load is for trusted, maintainer-produced modules only.

ELFs declaring lifecycle-2 app metadata launch through the foreground app runtime, retaining their image while suspended. Installed apps launch by module name. Ctrl+Alt+Del, Ctrl+] or closing the session requests cooperative shutdown. The host restarts the device if shutdown does not finish within five seconds.

The ordinary load runner is for short-lived command-style modules. A module must stop all of its work and release every callback and resource before its entry point returns, because the runner immediately unloads its code and data. Installed jobs, drivers, and libraries use separate resident lifecycle ABIs instead of this runner.

The standalone modules/examples/hello ESP-IDF project builds a small acceptance module.

Official native-module catalogs are versioned with the host firmware. pkg accepts a catalog only when its ECDSA signature, SolarOS version, ESP target, and native ABI match the running firmware. Each downloaded ELF must match the signed size and SHA-256, pass the same ELF validation as load, and is staged before atomic activation. The signed type selects the storage and lifecycle boundary: application modules use /modules/apps/<module>.elf, jobs use /modules/jobs, drivers use /modules/drivers, and libraries use /modules/libraries. Every entry separately declares its lifecycle ABI and its type-specific native host API size. Application lifecycle ABI 1 is the synchronous main(argc, argv) run-and-unload contract. Job lifecycle ABI 1 registers start, stop, and tick callbacks and keeps the ELF resident until the stopped job is removed. Driver lifecycle ABI 1 registers a zero-binding utility driver contract and keeps the ELF resident until all devices are detached and the module is removed. Library lifecycle ABI 1 registers a versioned API table. Consumers acquire leases that prevent unloading while calls, objects, or workers use that API. Libraries load on demand from installed storage; pkg unload ID unloads an unused library and retains its file. See Native libraries. Application lifecycle ABI 2 declares a foreground app manifest and runs main(argc, argv) on its declared worker. Display, input, storage, timing and optional audio are provided by the versioned foreground service. Resumable apps retain their ELF and guest state across session switches.

Run pkg without arguments to open the package manager. It loads the saved signed catalog without a network request and checks its signature and host compatibility again. Apps, Jobs, Libraries and Drivers appear as collapsible folders, including when empty. Arrows navigate the tree, Enter opens a folder or module details, and Left returns to the parent. Installed entries show [I] and incompatible entries show [!].

Press R to refresh and save the signed catalog, I to install or replace the selected module, or U to remove it. Install and removal require confirmation. Operations show progress popups and a result popup; Esc cancels a download or closes a popup. Confirmation defaults to No. The catalog and signature are stored under /modules/.catalog/ on the preferred storage volume. A failed refresh leaves the previous catalog available. A catalog for a different host firmware version is not used; press R after a firmware update. After a successful catalog check, pkg install completion uses that verified catalog. pkg remove completion and pkg installed read the installed module directories directly.

Installed application modules are shell commands: the module ID resolves under /modules/apps after built-in commands, compiled applications, and aliases. Command completion lists installed applications. load remains available for explicit paths and diagnostics. Reinstalling an application from the schema-v1 layout moves it into /modules/apps; pkg remove also recognizes the legacy flat application path during this transition.

The job and driver acceptance modules exercise their resident paths. Use pkg install hello-job, then job start hello-job, job status hello-job, and job stop hello-job. Use pkg install hello-driver, then expansion attach hello-driver hello0 and expansion detach hello0. The hello driver claims no GPIO or bus. pkg remove rejects a running job or a driver with an attached device.

For example:

pkg available
pkg installed
pkg install hello
hello SolarOS
pkg remove hello

Power, RTC, schedules, and terminal settings

power usage:

power status
power profile [performance|balanced|battery|lowpower]
power idle [off|seconds]
power key [short|long] [action]
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 suspended. It temporarily uses the lowpower profile and prevents the idle policy from entering explicit light sleep. Another press of KEY, a new keypress from a built-in or attached keyboard, or a BLE keyboard reconnect resumes the display and restores the selected profile. Releasing a held key or automatic key repeat does not resume the display. power status shows the selected profile, effective profile, and suspend state.

power key is an alias for the KEY settings exposed by setterm powerkey. Without short or long, an action changes the short press for compatibility.

deepsleep is a standalone command. It turns off the ESP32 radios and enters ESP32 deep sleep after configuring the same board wake input, compatible RTC interrupt, and next scheduled timer wake sources used by explicit light sleep. KEY, an armed RTC or scheduled timer, and RESET start a normal fresh boot; running jobs, sessions, and RAM state are not restored. BLE and Wi-Fi are off and cannot wake the device. This is not a hardware power-off: the board remains powered, and a separate board power control must still be used to remove its supply.

On Pi0cket CL-32, the built-in keyboard resumes suspend and wakes light or deep sleep through the companion controller's shared interrupt. USB-power and charging changes can also wake the board. Release held keys before sleeping. Keyboard-equipped boards allow up to three seconds for held keys and the wake interrupt to settle; sleep is cancelled if the input remains active.

suspend shows the SolarOS splash with Suspended; sleep and deepsleep show Sleeping. The splash replaces the current contents on every attached display before the transition, including e-paper displays that retain their image. Waking from suspend or light sleep restores the previous session. The CL-32 reflective LCD turns off during suspend after presenting the splash and restores the selected display power policy on resume. Unlike e-paper, it cannot retain the image after its supply is removed.

shutdown works from display and port shells on every target. It stops new app and job launches, closes sessions, stops background jobs and radios, flushes and unmounts persistent storage, and shows Powered off before power-down. Save unsaved documents before shutdown; closing an app does not automatically save edits. RAMFS contents and running sessions are discarded. Cleanup failures are reported and prevent power-down. Use RESET or cycle power to restart after a cleanup failure.

Shutdown uses the board's declared hardware power-off provider when available: the Pi0cket CL-32 controller, AXP2101 on Waveshare ESP32-S3-ePaper-3.97, and BQ25896 battery disconnect on T-LoRa-Pager. Xteink X4 Pro drops its peripheral latch; T-Deck Plus and ThinkNode M9 turn off their shared display/peripheral rail. Other targets enter ESP32 deep sleep. USB power can keep a board alive after its battery is disconnected; deep sleep is the fallback in that case. Shutdown disables RTC and scheduled timer wake. Pressing the physical power/KEY button starts a normal fresh boot, with apps and jobs initialized from saved settings. A target without a usable KEY wake pin needs RESET or a power cycle. Shutdown never resumes the old session.

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|scale|font|textsize|palette|statusbar] [value]
setterm scale [1..4]
setterm orientation [0|90|180|270]
setterm font [mono|compact]
setterm textsize [8|10|12|14|16|18|20]
setterm cursor [block|underscore] [blink|steady]
setterm palette [normal|inverted]
setterm foreground [#RRGGBB]
setterm background [#RRGGBB]
setterm statusbar [show|hide]
setterm brightness [0..100]
setterm backlight [0..100|follow]
setterm profile [vt100|ansi|dumb]
setterm charset [utf8|ascii]
setterm keyboard [us|de]
setterm powerkey [short|long] [default|off|enter|app-exit|sleep|suspend|deepsleep|shutdown|ble-pair]
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 cursor saves the global local CLI/TUI style across reboots. Default: block blink, 500 ms per phase. Omitting the mode preserves it. Typing or moving makes the cursor visible. Blocks invert the cell to keep text readable. Apps can hide the cursor. Graphical apps and external terminal clients keep their own cursors.

setterm backlight controls all registered auxiliary lights. It accepts a percentage from 0 to 100 (0 turns them off), or follow to track the primary display's configured brightness. Independent brightness defaults to 50%; both the numeric setting and follow mode are saved across reboots. Follow mode requires display brightness control. The keyboard backlights on T-Deck Plus, T-LoRa-Pager, ThinkNode M9, and Pi0cket CL-32 use this control. Unsupported hardware reports unsupported. Suspend and explicit sleep temporarily turn auxiliary lighting off; resume restores the selected setting. Shutdown also turns it off. The Pi0cket CL-32 companion controller retains its keyboard-activity timeout and fade. T-Deck Alt+B toggles the light for the current boot without typing a character or saving changes across reboots. Suspend and light sleep preserve this on/off choice. Toggling on from a configured level of zero uses 50%. The query shows the configured brightness, including while the light is toggled off.

backlight is no longer an alias for display brightness. Scripts that used setterm backlight for the screen must use setterm brightness instead.

setterm keyrate configures the shared repeat policy for BLE, PS/2, CardKB, and the Pi0cket 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 shows both effective KEY actions. Configure the gestures independently, or use the powerkey short and powerkey long rows in the setterm TUI:

setterm powerkey short enter
setterm powerkey long suspend
setterm powerkey long sleep
setterm powerkey long deepsleep
setterm powerkey long shutdown
setterm powerkey long ble-pair
setterm powerkey long default

The action choices are default, off, enter, app-exit, sleep, suspend, deepsleep, shutdown, and ble-pair. off disables the awake action; it does not cut power or disable KEY wake. sleep enters explicit light sleep. suspend turns off the display while jobs and services continue. deepsleep enters deep sleep and starts a fresh boot on wake. shutdown performs cleanup and powers off where supported. ble-pair forgets the remembered BLE keyboard and starts pairing when BLE is enabled for the current boot.

A hold is recognized after 1.2 seconds; sleep and shutdown begin after KEY is released. Any KEY press resumes suspend or wakes sleep, regardless of its duration. The wake press is consumed until release, so it cannot also type Enter, start pairing, or put the device back to sleep.

default restores the board's mapping. T-Deck defaults to Enter for a tap and suspend for a hold. Other boards retain their normal short-press mapping; holds default to suspend on targets with a board-owned wired keyboard and BLE pairing on targets without one. Connecting a BLE keyboard does not change these defaults. Sticky defaults to light sleep for a tap and shutdown for a hold. Valid saved choices take precedence over defaults, including older short-press settings. Changes persist and apply immediately. setterm powerkey sleep remains shorthand for setterm powerkey short sleep; setterm key is also accepted.

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. Terminal settings (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 profile of a named runtime display target, even when a TUI application rather than a shell owns that display. The target profile uses global defaults and the scale saved for that display name. 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. Orientation and scale are saved per display and restored after reboot. Other targeted changes apply to current and future sessions until reboot or display removal. Without a setting, the command prints the target's complete profile. For example:

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

display orientation display0 90 rotates the shell, running graphical apps and touch coordinates together. Shell output, history and unfinished input are preserved. Both display orientation and display scale are runtime-only, including changes made by gesture bindings. setterm orientation, setterm scale and the settings TUI use the same display operations and save their values in NVS.

display scale display0 shows the display's integer pixel scale. Values 1 through 4 apply to the entire display canvas, including text, graphics, icons and spacing. The default is 1, except Sticky's primary display defaults to 2 on both controller revisions. Saved per-display settings take precedence. On an 800x480 panel, scale 2 gives applications a 400x240 logical canvas, with each pixel enlarged to a 2x2 block. Scale 3 uses a 266x160 canvas; unused edge pixels do not add logical drawing space. Touch coordinates follow the same scale and orientation.

Use setterm scale 2 or setterm --display display0 scale 2 to save scale for that display name. display scale display0 2 changes only the current runtime. Close graphical applications, dismiss display overlays, and remove layouts using that target before changing scale. Text and graphical applications opened afterward use the new logical dimensions. Use display scale display0 1 to return to native resolution. External VT100 terminal geometry is unchanged.

Text size 8 selects a fixed bitmap face with 6×8 pixel cells for both font families. It uses the same upright glyphs for regular, bold, and italic text. On a 240×64 display, hiding the status bar gives 38 columns and eight rows:

setterm --display lcd1 textsize 8
setterm --display lcd1 statusbar hide

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.
agentagent config endpoint|model|key|reasoning|tools|max-tools VALUEConfigure the provider and tool policy.
agentagent 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.
sessionsession create shell <port> [--term auto|vt100|ansi|dumb] [--charset utf8|ascii] [--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 gestures
job start httpd /www
job start meshcore-ble 68:ee:8f:69:5f:35 public 123456
job start displayd [display-target]   # display0 by default, web0 when headless
job start cam-webd [qvga|vga] [fps] [auth=none|required]  # QVGA, 5 fps, no auth by default
job start rtspd [video=<stream>|none] [audio=<stream>|none] [size=qvga|vga] [fps=0..30] [port=<port>]
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.
diskdisk mount [flash|sd0pN] [mount]Mount the default volume or an explicit persistent volume.
diskdisk mount <image.img> <mount> [partition]Mount a raw FAT image; select an MBR primary partition from 1 to 4 if needed.
diskdisk create <image.img> <MiB>Create a new MBR/FAT hard-disk image without overwriting an existing file.
diskdisk umount [flash|sd0pN|imgN|mount]Unmount the default volume or an explicit volume/image/mount point.
diskdisk format <flash|sd0|sd0pN> --forceCreate 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.
lsls [-a] [-h] [path|pattern]List files. Hidden files are shown only with -a; sizes are human-readable with -h.
catcat <path|pattern>Print a small text file.
mkdirmkdir <path> [path...]Create directories.
rmrm [-f|-rf] <path|pattern> [path|pattern...]Remove files. -f allows directories; -rf removes directories recursively.
mvmv <source|pattern> <dest>Rename or move a file or matched set.
cpcp <source|pattern> <dest>Copy a file or matched set.
zipzip [-0] <archive.zip> <path|pattern> [path|pattern...]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

stream write accepts up to eight distinct scalar stream/value pairs. Without --rate it writes each value once. With --rate 1..100, it keeps the writers open and publishes all values each cycle until Ctrl+C, a disconnect, or an error. --duration limits publishing to 0.001..86400 seconds and requires --rate. All values are validated and all streams opened before the first sample is written. Writes within a cycle are sequential, not an atomic transaction; a runtime failure can leave a partially written cycle. Missed periods are skipped rather than replayed. Timing is best effort. On exit, writers close without sending an extra value; each receiver applies its own hold or timeout policy.

CommandUsageDescription
streamstream or stream listList dynamic typed stream endpoints.
streamstream status <id>Show type, direction, provider, format, owner, counters, and any declared scalar command range.
streamstream echo <source> [--once] [--rate 1..100]Print scalar samples with acquisition time, age and native unit, or byte/event source CSV records. IMU records contain one timestamp and all axes. Default display rate is 10 Hz; Ctrl+C stops.
streamstream write <scalar-sink> <value> [<scalar-sink> <value> ...] [--rate 1..100] [--duration seconds]Publish native float32 scalar values once or periodically. Values outside a declared command range are rejected with the stream ID, value, allowed range, and unit. Ctrl+C ends periodic publishing.
streamstream tcp listen <name> <port>Register an exclusive duplex byte stream backed by a raw TCP listener. Requires TCP byte-stream support.
streamstream tcp connect <name> <host> <port>Register an exclusive duplex byte stream for an outbound TCP connection.
streamstream tcp remove <name>Remove an unclaimed TCP listener or outbound stream.
streamstream ble listen <name>Register an exclusive duplex BLE L2CAP CoC stream. Advertises when claimed. Requires BLE byte-stream support.
streamstream ble connect <name> <address> <public|random|rpa_public|rpa_random>Register an outbound CoC stream. Connects when claimed. Use the address and type reported by ble scan.
streamstream ble status <name>Show connection state, negotiated SDU sizes, queued bytes, and connection interval.
streamstream ble remove <name>Remove an unclaimed BLE stream.
modelmodel backend; model list; model info <handle>Check engine availability and inspect resident models, interfaces, memory, mode, and references.
modelmodel load <file.espdl> [timeout_ms]; model bundle <bundle.json> [timeout_ms]Load a resident raw model or a checked model bundle from storage.
modelmodel mode <handle> <single|auto|dual>Select execution mode without reloading the model.
modelmodel unload <handle|all>Explicitly unload models; active operations and retained model references report busy.
pipelinepipeline listList OS-owned native image pipelines.
pipelinepipeline start qr <source> [limit] [interval_ms]Decode QR codes from an image file, video stream ID, or JPEG RTSP URL in a native background job.
pipelinepipeline start model <source> <model> [limit] [interval_ms]Run a resident bundle with one image input. The job retains the model until processing ends or is stopped.
pipelinepipeline status <id>Show job state, frame counts, last error, and the latest frame timings.
pipelinepipeline result <id>Print the latest result as JSON.
pipelinepipeline stop <id>Cancel and join native workers, release the source and model reference, and keep the latest result.
pipelinepipeline destroy <id>Stop and remove the job, configuration, and latest result.
daqdaq helpPrint DAQ usage.
daqdaq statusShow DAQ job status.
daqdaq streamsList stream IDs.
daqdaq 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.
daqdaq 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.
loglog follow [error|warn|info|debug]Follow logs in the current shell.
loglog clearClear the runtime log ring.
loglog level [error|warn|info|debug]Show or change runtime log level.
loglog sink cdc [on|off]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.
xferxfer 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.
xferxfer recv <port> <file> --zmodem [--append|--replace]Receive a file with ZMODEM and append or replace the destination.

TCP byte streams:

stream tcp listen tcp0 7777
stream status tcp0
stream tcp remove tcp0

The optional TCP byte-stream package is included in the full flavor. It supports up to four IPv4 TCP endpoints. Listeners bind to all network interfaces; stream tcp connect <name> <host> <port> configures an outbound endpoint. Hostnames resolve when the endpoint is registered; recreate it to resolve again. The outbound socket connects lazily when a consumer first performs I/O. Each exposes one exclusive duplex byte stream and port, without Telnet negotiation or newline translation. TCP connections are unencrypted and unauthenticated; use a trusted network. Endpoints are runtime settings and are not saved across reboot.

Start the stream consumer before connecting the client. I/O is nonblocking; partial writes and backpressure are preserved. A disconnected peer causes an I/O error for the current owner. Release and reopen the stream before attaching a new peer; a new connection never continues the old owner's protocol session.

BLE byte streams

The service.ble-stream package provides native BLE L2CAP Connection-Oriented Channels (CoC). Up to four runtime endpoint configurations share at most two active channels, subject to the host's connection budget. The OS reserves a connection for its BLE keyboard. Configuration does not connect or advertise; claiming the stream starts the connection, and releasing it closes the owned BLE session and frees its data buffers. Endpoints remain until removed or rebooted.

stream ble listen ble0
dlp serve display0 stream=ble0
stream ble status ble0

On another device, run ble scan, then configure the discovered address and type (public, random, rpa_public, or rpa_random):

stream ble connect ble0 aa:bb:cc:dd:ee:ff public
expansion attach stream-dlp remote stream=ble0

The listener advertises SolarOS <stream-name> using the shared generic BLE peripheral lease. Only one listener can be claimed at a time; it cannot run alongside another generic GATT server or the HID peripheral role. The keyboard input client can remain connected. Central/peripheral roles do not determine which device sends data or serves a display.

Streams carry exact bytes in both directions, with no line conversion or Link framing. Reads and writes are nonblocking after connection setup; a full transmit queue returns a partial write. A paused reader withholds channel credits instead of dropping data. The receiver and transmitter each have a bounded 4096-byte queue per channel. CoC SDU boundaries are hidden from consumers. Output queued before a peer connects stays with the current claim, allowing a listening shell to send its initial banner when the central arrives. The receive MTU fits one CoC PDU, so short SDUs cannot leave outstanding credits without a receive buffer. stream ble status shows negotiated sizes and queues.

A disconnect fails the current claim. Release and reopen to create a fresh connection; bytes from separate connections are never joined. DLP serving and the stream-dlp driver handle this through their existing reconnect paths. Open may wait for GAP/GATT connection setup. A central checks the SolarOS stream service and version descriptor before opening PSM 0x0080. This is native CoC, not the GATT UART profile used by BLE serial applications. Streams currently accept open connections and do not impose pairing, bonding, or peer authorization.

After releasing the consumer:

stream ble remove ble0

Closing the stream closes its peer; the endpoint remains registered until removed. An outbound endpoint attempts a new connection when reopened and used. Remove refuses a stream that is still claimed. Connected peers request low-latency Wi-Fi and release that request when the connection closes.

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 routernetwork 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.
wifiwifi forget [ssid|all]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 apwifi ap on [ssid [password [open|wpa|wpa2|wpa/wpa2]]]Start and save SoftAP settings.
wifi apwifi ap offStop SoftAP.
wifi natwifi nat [status|on|off]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.
wireguardwireguard 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.
bleble keepalive [on|off]Show or save the best-effort BLE keyboard keepalive setting.
bleble scanScan nearby BLE devices.
bleble pairForget the remembered keyboard and its bond, then start pairing a replacement.
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 gattble 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 pair <six-digit-pin>Secure and bond the connected GATT peripheral with its six-digit PIN.
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.
netscannetscan <host|range> [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.

Discovery through ble scan, the BLE Inspector, or the scripting APIs temporarily pauses automatic keyboard reconnect attempts. Reconnect resumes after the scan. An existing keyboard connection remains open.

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 inspector TUI

Run ble without arguments to open the BLE Inspector TUI. Its Devices, Services, Chars, and Settings tabs support scanning, connecting, service and characteristic inspection, characteristic reads and hexadecimal writes, and BLE keyboard settings. Press Tab to move between tabs. The inspector owns its GATT connection and disconnects it when the TUI closes. Existing ble status, ble scan, and ble gatt ... commands remain available for scripts and plain text use. On a BLE-disabled boot, the Settings tab remains available so BLE can be enabled for the next boot.

BLE keepalive and GATT

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 pair <six-digit-pin>
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.
otaota boot 0|1Select an OTA slot and reboot.
sshkeysshkey [status]Show default SSH key status.
sshkeysshkey gen [-f] [2048|3072|4096]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.
batterybattery min_voltage [V|mV]Show or set low-voltage threshold.
batterybattery max_voltage [V|mV]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.
audioaudio 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 muteSet global speaker level to zero while playback continues. Restore sound with audio level <1-100>.
audioaudio mic [ms]Sample microphone level.
audioaudio loopback [ms] [volume]Run microphone-to-speaker loopback.
audioaudio offStop audio output.
saysay [-v <0..100>] [--volume <0..100>] [--pitch <50..200>] [--speed <20..500>] [--drop-if-busy] (--file <path> | [--] <text...>)Queue text for offline speech, or stream a plain UTF-8 text file aloud with live progress. Pitch and speed default to 100. File mode submits bounded chunks through one continuous PicoTTS/audio session, completes paths with Tab, and remains responsive; press Esc or Ctrl+C to stop. Start speechd with a PicoTTS voice directory first.
ledled [status|on|off|toggle]Inspect or control the built-in status LED when available.
cameracamera [status]Initialize the camera on first use and show the detected sensor, JPEG configuration, orientation, current owner and frame lease, capture count, and last error.
cameracamera capture <path> [qvga|vga]Capture one JPEG into a caller-selected SolarOS path. The default is QVGA; changing size reinitializes the idle camera. Existing files are replaced.
cameracamera orientation [normal|mirror|flip|180]Show or set sensor orientation: normal, horizontal mirror, vertical flip, or 180° rotation (both). Changes apply to future frames, including active RTSP/web streams, and survive camera stop/start and size changes until reboot or camera detach. No setting is saved. The JPEG path does not support 90°/270° rotation.
cameracamera offDeinitialize an idle camera and release its framebuffer and driver resources.
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 registered expansion drivers.
expansionexpansion devicesList fixed board and runtime-attached expansion devices with origin, readiness, startup mode, policy, and bindings.
expansionexpansion 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.
expansionexpansion 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.
expansionexpansion 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 registered expansion driver or manual resource profile.
expansionexpansion detach <name>Detach an active expansion device and release its resource claims.
expansionexpansion attach <board-device-name>Restore a detached board device with its manifest bindings and board origin.
expansionexpansion export <path>Atomically export runtime buses and catalog-backed device attachments as a portable expansion manifest for custom-board generation.
neopixelneopixel [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.
neopixelneopixel 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.
midimidi 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.
midimidi 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.
controlcontrol list|parameters|bindingsInspect 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.
controlcontrol 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.
controlcontrol get|set <name> [value]Read or set a normalized 0..65535 control value.
controlcontrol parameter get|set <path> [value]Read or set an available native parameter in its declared unit. Both commands complete registered parameter paths.
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.
encoderencoder configure <name> <key> <value>Configure PPR, transmission, polarity or filtering for enc.<name> velocity feedback.
motormotor configure <name> polarity|max_drive|brake <value>Configure the generic motor service and mot.<name> drive sink while unclaimed.
controllercontroller list or controller status <name>Inspect feedback controller configuration, state, output, and errors.
controllercontroller create <name> pid|hysteresis or controller delete <name>Create or remove a named feedback controller.
controllercontroller configure <name> <key> <value>Set gains, setpoint, output limits, direction, timing, or safe output; see Feedback controllers.
controllercontroller bind <name> <stream>|manual parameter:<path>|pwm:<provider>:<output>|stream:<sink>Select timestamped measurement input and an owned parameter, PWM pulse, or scalar sink output.
controllercontroller feed <name> <measurement>Publish a manual-source measurement acquired now.
controllercontroller step <name> <measurement> <dt-seconds>Calculate output for a stopped controller without writing an actuator.
controllercontroller manual <name> <output> or controller auto <name>Select a fixed output or automatic computation.
controllercontroller tune <name> [noise-floor [points [timeout-ms]]]Automatically find motion, measure step responses and validate candidate PID gains without applying them. Starts the worker when needed; component-owned controllers reject tuning. Inspect with status, abort with stop.
controllercontroller start|stop|reset <name>Enable a bound controller, stop and release its output, or reset a stopped instance.
robotrobot joint list or robot joint status <joint>Inspect joints in the optional external robot library.
robotrobot model load <file.urdf> or robot model unloadLoad a validated URDF model without activating hardware, or release it.
robotrobot start or robot stopStart URDF-declared velocity controllers or release them while retaining the model. Stop before replacing or unloading it.
robotrobot model info|links|joints|metadataInspect the resident model and retained SolarOS extension.
robotrobot model devices or robot model frame <device-or-stream>List URDF device/frame bindings or resolve a device or registered stream to its frame.
robotrobot model set <joint> <position> [<joint> <position> ...]Supply model joint positions as one validated batch.
robotrobot model transform <target> <source>Query the transform mapping source-frame coordinates into target.
robotrobot model point|vector <target> <source> <x> <y> <z>Transform a point or direction; see Robot models and transforms.
robotrobot joint create <joint> velocity|servoCreate an inactive joint without claiming hardware.
robotrobot joint configure <joint> <key> <value>Configure PPR, gearbox ratio, polarities, limits, timing or control gains.
robotrobot joint motor <joint> <motor> <encoder>Bind named devices to a velocity joint.
robotrobot joint servo <joint> <provider> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz]Bind a calibrated input-shaft servo output.
robotrobot joint command <joint> <target>Refresh an joint target in radians/second or radians.
robotrobot joint enable|disable|close <joint>Activate, stop and release, or destroy an joint; see Robot joints.
oscosc bindingsInspect named outbound OSC bindings and their live source, value, send, and error state.
oscosc send <host:port> <address> <value> [value ...] [--rate 1..100] [--duration seconds]Send one to eight float32 arguments in one UDP message, once or periodically; Ctrl+C stops. No running OSC job is required.
oscosc bind <name> stream <stream> <address> [rate=hz] [delta=value] [send=change|always]Publish one scalar stream as OSC float32 values in its native unit.
oscosc bind <name> stream <event-stream> <address> edge=rising|falling|both [rate=hz]Publish sampled boolean transitions as OSC int32 0 or 1.
oscosc 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 bind <name> receive <scalar-sink> <address> [arg=0..7]Map one incoming OSC argument to a scalar sink in its native unit. Several bindings may map one message to multiple streams. See Open Sound Control.
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.
radioradio scan [name]Open the graphical rfscan app when included in the firmware.
radioradio status|listList 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.
meshcoremeshcore identity show|generate|import|exportInspect or explicitly manage the private MeshCore identity.
meshcoremeshcore name [name]Show or set the MeshCore-specific advertised name.
meshcoremeshcore advert zero|floodQueue a local or explicitly network-wide advert.
meshcoremeshcore channel list|add|remove|publicJoin 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.
radioradio state <name> [sleep|standby|rx|tx]Show or change radio operating state.
radioradio send <name> <text|byte...>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.
gnssgnss enable [name] / gnss disable [name]Enable or silence NMEA output on a receiver with recognized software configuration support. Power and UART ownership remain unchanged.
gnssgnss 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.
modemmodem 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.
modemmodem baud [name] [auto|rate]Show or persist an optional serial modem transport rate. auto selects the driver's preferred rate.
modemmodem 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.
modemmodem profile show|clear [name]Inspect a saved profile with its password redacted, or remove it.
modemmodem 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.
cancan [list]List the attached classic CAN controller and bitrate. See Classic CAN.
cancan status <name>Show CAN state, queues, and error counters.
cancan send <name> <id> <hex-or-> [ext] [rtr]Queue a standard, extended, or remote CAN frame with up to eight bytes.
cancan recv <name> [timeout-ms]Read one CAN frame, waiting at most 1000 ms.
cancan recover <name> / can start <name>Start bus-off recovery, or restart after recovery reaches stopped state.
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.
encoderencoder [list]List named encoders, providers, and counting modes.
encoderencoder status <name>Read the mode and signed count of one encoder.
encoderencoder read <name>Print the signed count without clearing it.
encoderencoder reset <name>Set the encoder count to zero.
motormotor [list]List registered motor endpoints, drivers, and supported operations.
motormotor status <name>Read mode, normalized applied drive, common fault masks and optional driver diagnostics.
motormotor drive <name> <value>Apply a normalized signed demand from -1.0 to 1.0; zero coasts.
motormotor coast|brake|clear <name>Coast, brake, or coast and clear fault latches.
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.
chargercharger 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.
nfcnfc 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.
espnowespnow peers|listList 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.
linklink status|listList active SolarOS Link instances and their queue/protocol counters.
linklink status <link>Show local ID, transport MTU, queues, acknowledgements, duplicates, CRC errors, and drops.
linklink send <link> <broadcast|destination-id> <text>Queue a text message. Unicast requests an acknowledgement.
linklink 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.
pocsagpocsag 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.
uartuart 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.
gpiogpio mode <pin> <in|out> [none|up|down]Configure a runtime GPIO.
gpiogpio read <pin>Read a runtime GPIO.
gpiogpio write <pin> <0|1>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.
onewireonewire reset <bus|pin>Reset a named bus or direct runtime GPIO and report presence.
onewireonewire scan <bus|pin>Discover and list 1-Wire ROM addresses.
onewireonewire 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 providersList registered PWM controllers.
pwmpwm statusShow named outputs and their frequency groups.
pwmpwm set <output> <freq-hz> <duty-percent>Start PWM on provider:number, or a native GPIO number.
pwmpwm pulse <output> <freq-hz> <pulse-us>Command a pulse width in microseconds.
pwmpwm off <output>Stop a general PWM output.
servoservo listShow names, outputs, enabled state, and owners in a compact list.
servoservo status [name]Show calibration, frequency, inversion, and the last accepted command for all servos or one named servo.
servoservo open <name> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz [inverted:0|1]]Reserve an output without pulses; see Servo outputs.
servoservo set <name> <angle-rad> / servo pulse <name> <pulse-us>Command a calibrated angle in radians or a pulse width in microseconds.
servoservo disable <name> / servo close <name>Stop pulses, retaining or releasing the reservation.
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.
temperaturetemperature [list|sensor]List temperature providers or read the default or named sensor.
humidityhumidity [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.

display relax runs black/white recovery on the first registered display that supports it for 1 second. It reports an error if no display supports recovery. display relax display0 runs recovery for 60 seconds. Supply a duration from 1 to 86400 seconds, for example display relax display0 3600 for one hour. It alternates solid black and white using synchronous native full refreshes, with up to one second of hold after each refresh. It completes a black/white pair when the time expires; a hardware refresh can extend the run beyond the requested duration. The app-exit key (normally Ctrl+]), Ctrl+C, or Escape stops it between refreshes. The command returns to the shell after restoring the saved image and previous refresh mode, including pulse tuning. Sleep and shutdown interrupt recovery before the power transition.

Recovery is available on all built-in EPD drivers: every SSD1683 profile, SSD1685, SSD1677, UC8179, and UC8279, including the X4 display wrapper and runtime-attached panels. LCD, RLCD, and other display types return unsupported. It claims the whole physical target; close apps or remove layouts that own it first. A display-shell session can recover its own target without closing that session. Ordinary presentation and pulse scans are suspended during recovery. Completion reports black/white cycles, not a measurement of ghost removal; inspect the physical panel afterward.

display join wall0 --horizontal display0 lcd0 claims two to four backing targets and registers wall0 as one logical target. Horizontal joins place targets from left to right; vertical joins place them from top to bottom. The largest cross-axis dimension defines the logical canvas and unused backing area is cleared. display split display0 --horizontal left0 right0 claims one backing target and registers two equal logical viewports. On a 792x272 CrowPanel this creates two 396x272 targets. Each viewport has its own display buffer, terminal profile, session ownership, and frame-export surface; updating one viewport preserves the other.

Layouts are runtime-only and are shown by display layouts. Backing targets remain visible in display list with an owner such as display-layout:display0, while logical targets have source layout and role joined or viewport. Layout targets work with normal commands such as session create shell left0 and session create files right0. A layout cannot use another layout as backing, does not scale content, and cannot be removed while any logical target is owned or exported. When the built-in display shell creates a split, session 0 automatically moves to the first viewport; when it creates a join containing its current display, it moves to the joined target. Removing that layout moves session 0 back to its original board display. Shell state and scrollback are preserved while terminal geometry is recalculated for the new target. Layout updates are coalesced and backing targets are refreshed sequentially, which is especially important for e-paper panels. display unjoin wall0 and display unsplit display0 release the backing targets.

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 starts with a full waveform and skips unchanged frames. Waveshare V2 expansion targets use the changed framebuffer rectangle and the controller's partial-window waveform, with a full cleanup after every 19 partial updates. The Elecrow 5.79-inch dual controller transfers both RAM halves with its 0xDC partial waveform after the initial cleanup and does not schedule periodic full refreshes. refresh=fast forces a fast full-frame waveform and refresh=full forces the full cleanup waveform on every changed frame.

SSD1685 targets default to asynchronous refresh=pulse and also provide factory refresh=auto|partial|full modes. In auto mode, cleanup-every=<1..128> sets the number of partial updates before a full OTP cleanup (default 64). refresh=partial bypasses periodic cleanup; refresh=fast is an alias for it.

SSD2677 Sticky V2 starts in asynchronous refresh=pulse / waveform=fastwave with its own 535-byte RAM waveform, shared pixel budgets, background settling, and adaptive native cleanup. Defaults are drive-cycles=8, pulse-frames=4, waveform-fr=5, pulse-period=100, pulse-power=hold, and cleanup-every=64. Pulse tuning follows SSD1677, except waveform-fr accepts 1..5 (25, 50, 65, 75, 85 Hz). pulse-frames and drive-cycles accept 1..32. pulse-scan=sync waits for settling; pulse-scan=async coalesces new targets and continues settling after the application stops painting.

SSD2677 also exposes native refresh=auto|fast|partial|full and waveform=otp. refresh=partial sends a full-frame old/new differential image using factory scan timing. refresh=fast uses an 85 Hz scan rate for partial updates. Auto and fast modes perform a full cleanup after 19 changed partial updates; explicit partial mode bypasses periodic cleanup. Full refreshes retain factory scan timing and also restore the baseline after errors, sleep, or display recovery. Sticky identifies its controller at startup; display list reports the selected SSD1677 or SSD2677 driver. SSD2677 mode output includes the SPI rate and last refresh, transfer, and drive times in microseconds.

SSD1677 also provides refresh=pulse / waveform=fastwave, with native refresh=auto|partial|full and refresh=fast as an alias for partial. Sticky SSD1677 (panel=1) starts in asynchronous pulse mode at 20 MHz; other SSD1677 profiles start in native auto mode. The Sticky panel profile uses the manufacturer's Mode 1 OTP waveform with identical monochrome data in both RAM planes for full cleanup. Generic profiles retain their differential full-frame sequence. Pulse mode uses the controller's 105-byte LUT, nominal vendor voltages, and neutral masks for settled pixels. Pulse defaults are asynchronous, 100 ms between new images, five drive cycles on Sticky (three on other profiles), eight scan frames, waveform-fr=8, retained supplies during settling, and adaptive cleanup with baseline 64. waveform-fr=<1..8> selects a nibble repeated across its five frame-rate bytes. It supports pulse-period=<67..1000>, pulse-spi-khz=<100..20000>, pulse-power=<cycle,hold>, and invert=<false,true>; it retains the native booster ramp. waveform=otp restores native auto. Pulse memory is allocated in PSRAM on demand.

The Elecrow 5.79-inch dual-SSD1683, Elecrow 4.2-inch green-sticker, Waveshare 4.2-inch V2/revision-2.2, SSD1677, and SSD1685 GDEY029T71H profiles provide the refresh=pulse monochrome mode. display mode <target> drive-cycles=<1..32> sets the per-pixel budget, pulse-frames=<1..32> sets each drive phase's scan duration, waveform-fr=<1..12> selects a raw frame-rate code (the dual SSD1683 accepts only 1–5 or 9–12; SSD1677 accepts 1–8), and cleanup-every=<1..128> sets the cleanup baseline. cleanup-policy=adaptive tracks actual successful drives per pixel. It cleans up when the most-driven pixel reaches cleanup-every * drive-cycles activations, or after 4 * cleanup-every changed images, whichever comes first. cleanup-policy=fixed cleans up every cleanup-every changed images. Synchronous mode counts completed changed presentations; asynchronous mode counts changed targets consumed by the worker. Identical targets do not advance the image count. At a limit, the asynchronous worker schedules a native cleanup without waiting for another presentation or for all pending pixels to settle. Mode output includes the policy, peak/limit drive cycles, and changed-image age. These counters estimate accumulated activity and do not measure ghosting. These runtime settings apply to pulse mode, with SSD1685's cleanup-every also controlling its native auto-mode cleanup interval. They are listed by display mode <target>. pulse-scan=sync completes a presentation's pending cycles before returning. pulse-scan=async runs one cycle per scan in a persistent worker and accepts new targets while earlier pixels settle. pulse-period=<10..1000> requests the minimum new-image interval in milliseconds; the dual-SSD1683, Elecrow green-sticker, SSD1677, and SSD1685 profiles instead accept 67–1000 ms. Settling drives run as BUSY permits without consuming new-image slots. A new target can replace the current target between drives when its image slot is due; unchanged pixels keep their remaining budgets. Identical submissions do not consume image slots. Synchronous operation still completes all pending drives before returning and applies the requested interval between changed targets. Mode output separates frame-count and frame-interval-us for distinct target snapshots from scan-count and scan-interval-us for physical drives. The frame count includes native baseline snapshots; submitted/coalesced counts also appear. pulse-power=<cycle,hold> selects supply cycling or retention during settling, pulse-ramp=<normal,fast> selects booster duration, and pulse-spi-khz=<100..20000> overrides the SPI clock only for pulse operation. SSD1685 limits pulse SPI to 100–10000 kHz and also exposes invert=<false,true> for polarity testing; its custom waveform requires panel validation. See the SSD1685 reference for defaults. Elecrow 5.79-inch defaults are asynchronous, 67 ms new-image interval, retained supplies, fast booster ramp, 20000 kHz pulse SPI, 6 drive cycles, 8 scan frames, FR code 4, and adaptive cleanup with a baseline of 64. This profile uses the Fastwave waveform builder on both controllers, reported as waveform=fastwave, with no reset kick and one VSS settle frame after the configured drive phase. Grayscale images use the existing dithering path. CrowPanel 5.79-inch and SolarWriter start in pulse mode, with native auto as the fallback if the worker cannot start. Requested image cadence is not a guarantee of achieved cadence or optical settling. Waveshare 4.2-inch defaults are asynchronous, 100 ms new-image interval, supply cycling, normal ramp, the configured SPI clock, 3 drive cycles, 8 scan frames, FR code 4, and adaptive cleanup with baseline 64. Native and pulse timing logs support comparison on that panel. The Elecrow green-sticker profile adapts the Fastwave phase builder to its UC8276 OLD/NEW RAM and programmable waveform registers, reported as waveform=fastwave-uc8276. It retains the controller's voltage and PLL setup, uploads both full RAM masks, and leaves unbudgeted pixels undriven. Its defaults are asynchronous, 67 ms new-image interval, supply cycling, fast ramp, 20000 kHz pulse SPI, 8 drive cycles, 8 scan frames, raw PLL code 12, and fixed cleanup every 8 changed images. The Elecrow 4.2-inch board starts this revision in pulse mode. For this profile, waveform-fr selects a raw PLL code; internal scan frequency is separate from image update rate. The mode requires physical panel calibration; other EPD profiles reject it until they have a matching waveform adapter. See the SolarWriter display controls and Waveshare 4G E-paper for behavior and recovery.

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

rx-gain controls receiver gain policy. radio status <name> lists the modes supported by that radio; the radio TUI's rx gain row cycles through those choices with Left/Right. Changes leave the radio in standby, like other radio configuration changes.

DriverRX gain modesDefault
SX1262power-saving, boostedpower-saving
RFM95Wpower-saving, boostedboosted
RFM69 / RFM69Hauto, g1 through g6auto
radio config radio0 rx-gain boosted
radio state radio0 rx

On SX1262 and RFM95W, boosted enables the higher-current RX/LNA mode; power-saving disables that boost. Automatic gain control remains active. On RFM69, auto enables AGC; g1 selects maximum LNA gain and g2 through g6 reduce it by 6, 12, 24, 36, and 48 dB respectively. Fixed gain can reduce the receiver's ability to handle strong signals. These settings are not transmit power or a common numeric gain scale across radio families. default restores the driver's default policy. Unsupported modes are rejected.

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, including RX gain, are saved in mounted-storage settings and consume no idle profile cache. Existing profiles without RX gain use the driver's default:

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
hello SolarOS
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.

Browse and manage files

pwd
ls
mkdir /notes
edit /notes/todo.txt
cp /notes/todo.txt /notes/backup.txt
df

Relative paths start in the current shell directory. A leading slash starts at the default volume root. Use /flash or /sdcard to select a particular mounted volume. Check free space with df before a large copy or capture.

Startup script

Run setterm startup to see the script path. The selection is saved and takes effect at the next boot:

  • auto: use mounted board-owned SD, otherwise flash. This is the default.
  • flash: use /flash/.shell/startup, or /.shell/startup on flash-root boards.
  • sd: use /sdcard/.shell/startup.

Explicit flash and sd selections do not fall back. If SD is mounted but has no startup file, auto does not run the flash script.

Mounts

Use disk for 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.

After replacing or reinserting a card, close files that used it and run disk mount to recover storage. The command checks the cached card, clears stale SD mounts, and initializes the inserted card again. A board-defined SDSPI or SDMMC slot also supports expansion attach storage0 for recovery.

Disk images

Mount a raw .img with a FAT12, FAT16, or FAT32 filesystem to edit its files:

disk mount /sdcard/tiny386/harddisk.img /mnt/dos
ls /mnt/dos
cp /sdcard/notes.txt /mnt/dos/notes.txt
disk umount /mnt/dos

SolarOS accepts whole-volume images, such as FAT floppy images, and MBR disks. It selects the first FAT partition by default. Add a partition number from 1 to 4 to select another primary partition, for example disk mount /sdcard/disk.img /mnt/dos 2. Other filesystems and compressed disk formats are not supported. Mounting never formats an existing image.

To create a new hard-drive image with one FAT partition:

disk create /sdcard/tiny386/harddisk.img 32

The size is in MiB, from 1 to 2047. Creation allocates the full size and refuses to overwrite an existing file. The new disk is formatted but is not bootable; install a guest operating system separately. Images must use 512-byte sectors and be smaller than 2 GiB. FAT mounts share the firmware's available volume slots with SD and flash; disk lsblk lists mounted images as imgN.

Close files before unmounting. Unmount an image before giving it to Tiny386; close the guest before mounting it again. Suspending a guest keeps its disk claim. SolarOS refuses a second mount or guest using the same image, and keeps the backing storage mounted while an image is in use. Do not edit or replace the raw image file through another program while it is mounted or running.

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 erased internal storage on first use. If a non-empty volume fails to mount, it leaves the contents intact. Format it only when you can discard those contents.

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.

Scripts should use solaros.storage for SolarOS paths and file operations. See Python storage or Lua storage.

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 endpoint|model|key|reasoning|tools|max-tools VALUEConfigure 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 muteSet global speaker level to zero while playback continues. Restore sound with audio level <1-100>.
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 [V|mV]Show or set low-voltage threshold.
battery max_voltage [V|mV]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 pairForget the remembered keyboard and its bond, then start pairing a replacement.
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 pair <six-digit-pin>Secure and bond the connected GATT peripheral with its six-digit PIN.
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

camera

Initialize the camera on first use and show the detected sensor, JPEG configuration, orientation, current owner and frame lease, capture count, and last error.

Usage

UsageDescription
camera [status]Initialize the camera on first use and show the detected sensor, JPEG configuration, orientation, current owner and frame lease, capture count, and last error.
camera capture <path> [qvga|vga]Capture one JPEG into a caller-selected SolarOS path. The default is QVGA; changing size reinitializes the idle camera. Existing files are replaced.
camera orientation [normal|mirror|flip|180]Show or set sensor orientation: normal, horizontal mirror, vertical flip, or 180° rotation (both). Changes apply to future frames, including active RTSP/web streams, and survive camera stop/start and size changes until reboot or camera detach. No setting is saved. The JPEG path does not support 90°/270° rotation.
camera offDeinitialize an idle camera and release its framebuffer and driver resources.

Commands · SolarOS manual

can

List the attached classic CAN controller and bitrate. See Classic CAN.

Usage

UsageDescription
can [list]List the attached classic CAN controller and bitrate. See Classic CAN.
can status <name>Show CAN state, queues, and error counters.
can send <name> <id> <hex-or-> [ext] [rtr]Queue a standard, extended, or remote CAN frame with up to eight bytes.
can recv <name> [timeout-ms]Read one CAN frame, waiting at most 1000 ms.
can recover <name> / can start <name>Start bus-off recovery, or restart after recovery reaches stopped state.

Commands · SolarOS manual

cat

Print a small text file.

Usage

UsageDescription
cat <path|pattern>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|bindingsInspect 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. Both commands complete registered parameter paths.
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

controller

Inspect feedback controller configuration, state, output, and errors.

Usage

UsageDescription
controller list or controller status <name>Inspect feedback controller configuration, state, output, and errors.
controller create <name> pid|hysteresis or controller delete <name>Create or remove a named feedback controller.
controller configure <name> <key> <value>Set gains, setpoint, output limits, direction, timing, or safe output; see Feedback controllers.
controller bind <name> <stream>|manual parameter:<path>|pwm:<provider>:<output>|stream:<sink>Select timestamped measurement input and an owned parameter, PWM pulse, or scalar sink output.
controller feed <name> <measurement>Publish a manual-source measurement acquired now.
controller step <name> <measurement> <dt-seconds>Calculate output for a stopped controller without writing an actuator.
controller manual <name> <output> or controller auto <name>Select a fixed output or automatic computation.
controller tune <name> [noise-floor [points [timeout-ms]]]Automatically find motion, measure step responses and validate candidate PID gains without applying them. Starts the worker when needed; component-owned controllers reject tuning. Inspect with status, abort with stop.
controller start|stop|reset <name>Enable a bound controller, stop and release its output, or reset a stopped instance.

Commands · SolarOS manual

cp

Copy a file or matched set.

Usage

UsageDescription
cp <source|pattern> <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

deepsleep

Turn off the ESP32 radios and enter deep sleep. KEY, an armed RTC or scheduled timer, or RESET starts a fresh boot.

Usage

UsageDescription
deepsleepTurn off the ESP32 radios and enter deep sleep. KEY, an armed RTC or scheduled timer, or RESET starts a fresh boot.

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 mount <image.img> <mount> [partition]Mount a raw FAT image; select an MBR primary partition from 1 to 4 if needed.
disk create <image.img> <MiB>Create a new MBR/FAT hard-disk image without overwriting an existing file.
disk umount [flash|sd0pN|imgN|mount]Unmount the default volume or an explicit volume/image/mount point.
disk format <flash|sd0|sd0pN> --forceCreate a FAT filesystem, permanently erasing the unmounted target.

Commands · SolarOS manual

display

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

Usage

UsageDescription
display [list]; display layouts; display test <target>; display mode <target> [mode]List targets and layouts, draw a test pattern, or change driver-specific display settings.
display orientation <target> [0|90|180|270]Show or change orientation for text, graphics and touch. Runtime only; does not change NVS.
display scale <target> [1..4]Show or change runtime integer pixel scaling without changing NVS.
display relax [<target> [seconds]]Run timed native black/white recovery cycles on supported EPDs, then restore the saved image and refresh mode. Without arguments, use the first supported display for 1 second.
display join <name> --horizontal|--vertical <target> <target> [target ...]; display unjoin <name>Join two to four physical targets into one logical display, or remove the join.
display split <target> --horizontal|--vertical <first> <second>; display unsplit <target>Split one physical target into two equal logical viewports, or remove the split.

Commands · SolarOS manual

dlp

Receive DLP images on a named display through an exclusive duplex byte stream. See DLP.

Usage

UsageDescription
dlp serve <display> stream=<byte-stream>; dlp status; dlp stop <display>Receive DLP images on a named display through an exclusive duplex byte stream. See DLP.

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

encoder

Configure PPR, transmission, polarity or filtering for enc.<name> velocity feedback.

Usage

UsageDescription
encoder configure <name> <key> <value>Configure PPR, transmission, polarity or filtering for enc.<name> velocity feedback.
encoder [list]List named encoders, providers, and counting modes.
encoder status <name>Read the mode and signed count of one encoder.
encoder read <name>Print the signed count without clearing it.
encoder reset <name>Set the encoder count to zero.

Commands · SolarOS manual

engine

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

Usage

UsageDescription
engine [status|reset]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|listList 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 registered 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 registered expansion driver or manual resource profile.
expansion detach <name>Detach an active expansion device and release its resource claims.
expansion attach <board-device-name>Restore a detached board device with its manifest bindings and board origin.
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 sensors and touchscreens, readiness, and the gesture kinds each source advertises.

Usage

UsageDescription
gesture [status]List gesture-capable sensors and touchscreens, 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 gestures 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 enable [name] / gnss disable [name]Enable or silence NMEA output on a receiver with recognized software configuration support. Power and UART ownership remain unchanged.
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> <in|out> [none|up|down]Configure a runtime GPIO.
gpio read <pin>Read a runtime GPIO.
gpio write <pin> <0|1>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 [status|keyboard|touch|mouse|joystick|dpad|buttons|gesture]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.
input keymap [<source> [show|load <path>|reset]]List source mapping support, inspect a map, load a validated JSON file, or restore a registered source profile. See source keymaps.

Commands · SolarOS manual

irc

Save an IRC server profile on mounted storage.

Usage

UsageDescription
irc add <instance> <host> <port> <nick>; irc set <instance> <host> <port> <nick>Save an IRC server profile on mounted storage.
irc status; irc connect|disconnect <instance>Inspect or enable individual IRC connections maintained by irc-sync.
irc remove <instance>Remove an IRC instance, its credentials, history and associated Inbox notifications.
irc join|query <instance> <channel|nick>; irc leave <conversation-id>Open rooms and private conversations through shared Messaging.
irc sasl <instance> <account> <password>; irc sasl <instance> off; irc pass <instance> <password|off>; irc ca <instance> <PEM-path|default>Set optional authentication and TLS trust. See man messaging.irc.

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 [status|on|off|toggle]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|listList 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

load

Validate, relocate, run, and unload one native ELF module from storage.

Usage

UsageDescription
load <file.elf> [args...]Validate, relocate, run, and unload one native ELF module from storage.

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 [error|warn|info|debug]Follow logs in the current shell.
log clearClear the runtime log ring.
log level [error|warn|info|debug]Show or change runtime log level.
log sink cdc [on|off]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] [path|pattern]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 and managed PSRAM status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.

Usage

UsageDescription
mem [policy]Print heap and managed PSRAM status; policy also shows allocation-class counters, guarded fallback limits, and the last tagged failure.
mem reclaimAsk registered owners to release rebuildable idle data, compact eligible buffers, and show recovered bytes and the largest free block before and after.
mem compactCompact unpinned movable buffers and show the largest free block before and after. Raw pointers, DMA and stacks stay fixed.
mem ownersShow managed-region bytes, allocation counts, movable bytes and largest object by owner tag. SDK-region totals remain in mem policy.

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|exportInspect or explicitly manage the private MeshCore identity.
meshcore name [name]Show or set the MeshCore-specific advertised name.
meshcore advert zero|floodQueue a local or explicitly network-wide advert.
meshcore channel list|add|remove|publicJoin 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

Open the tabbed instance, connection, and message manager.

Usage

UsageDescription
messagesOpen the tabbed instance, connection, and message manager.
messages statusShow archive, 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 <instance|instance-id|all>Clear retained history and owned Inbox projections, including for removed instances. Unrelated Inbox sources remain.
messages remove <instance|instance-id>Remove a provider instance, connection credentials, pending sends, history and Inbox notifications.
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.
messages classes / messages instancesList available provider classes or configured instances.
messages add gateway <name> <url> [token]Save an independent Gateway instance. Enable it with messages connect <name> and start gateway-sync.
messages configure <name> <url> [token]Change a Gateway instance's connection settings.
messages add irc <name> <host> <port> <nick>Save an independent native IRC connection.
messages add meshcore <name> radio <radio> <profile>Save a MeshCore instance on a local radio.
messages add meshcore <name> ble <address> <type> [pin]Save a MeshCore companion binding.
messages add link <name> <transport>Bind an independent Chat projection to a named Link transport.
messages add sc2 <name> <domain> [admit] [key=<b64>]Add an SC2 relay from its signed descriptor at https://<domain>/.well-known/solaros-relay (CA and host name checked; never plain HTTP). The relay key (and the fallback relays and their keys) are pinned; every login must prove the key, a different key is refused. key= must match the descriptor when given. messages configure <name> key <b64>|off changes the pin. See man messaging.
messages add sc2 <name> chats://<host>:<port>[#pin=sha256/<b64>] [admit]Save a disabled SC2 end-to-end relay instance. Enable it with messages connect <name> and start its worker with job start sc2-sync; messages disconnect <name> disables it. messages configure <name> admit <code>|off or pin <pin>|off change it; fallback <url> [<url> <url>]|off sets up to three fallback relays (failover and return to the primary; sc2 relay shows which is in use). With a pin, the pinned key alone authenticates the relay (self-signed certificates work); without one the CA bundle is used. See man messaging.

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

model

Check engine availability and inspect resident models, interfaces, memory, mode, and references.

Usage

UsageDescription
model backend; model list; model info <handle>Check engine availability and inspect resident models, interfaces, memory, mode, and references.
model load <file.espdl> [timeout_ms]; model bundle <bundle.json> [timeout_ms]Load a resident raw model or a checked model bundle from storage.
model mode <handle> <single|auto|dual>Select execution mode without reloading the model.
model unload <handle|all>Explicitly unload models; active operations and retained model references report busy.

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

motor

Configure the generic motor service and mot.<name> drive sink while unclaimed.

Usage

UsageDescription
motor configure <name> polarity|max_drive|brake <value>Configure the generic motor service and mot.<name> drive sink while unclaimed.
motor [list]List registered motor endpoints, drivers, and supported operations.
motor status <name>Read mode, normalized applied drive, common fault masks and optional driver diagnostics.
motor drive <name> <value>Apply a normalized signed demand from -1.0 to 1.0; zero coasts.
motor coast|brake|clear <name>Coast, brake, or coast and clear fault latches.

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 <source|pattern> <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 <host|range> [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 send <host:port> <address> <value> [value ...] [--rate 1..100] [--duration seconds]Send one to eight float32 arguments in one UDP message, once or periodically; Ctrl+C stops. No running OSC job is required.
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 bind <name> receive <scalar-sink> <address> [arg=0..7]Map one incoming OSC argument to a scalar sink in its native unit. Several bindings may map one message to multiple streams. See Open Sound Control.
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 0|1Select 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

pipeline

List OS-owned native image pipelines.

Usage

UsageDescription
pipeline listList OS-owned native image pipelines.
pipeline start qr <source> [limit] [interval_ms]Decode QR codes from an image file, video stream ID, or JPEG RTSP URL in a native background job.
pipeline start model <source> <model> [limit] [interval_ms]Run a resident bundle with one image input. The job retains the model until processing ends or is stopped.
pipeline status <id>Show job state, frame counts, last error, and the latest frame timings.
pipeline result <id>Print the latest result as JSON.
pipeline stop <id>Cancel and join native workers, release the source and model reference, and keep the latest result.
pipeline destroy <id>Stop and remove the job, configuration, and latest result.

Commands · SolarOS manual

pkg

Open the native-module package manager.

Usage

UsageDescription
pkgOpen the native-module package manager.
pkg systemPrint compiled package groups and build units.
pkg availableVerify the signed native-module catalog and print its compact module list.
pkg installedList installed application, job, driver, and library modules.
pkg install <module>Download, verify, validate, and atomically install a native module; Ctrl+C, Esc, or the app-exit key cancels.
pkg remove <module>Remove an installed native module.
pkg unload <library>Unload an unused library while keeping its installed file.

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 [short|long] [action]Show or configure the dedicated KEY short- and long-press actions.
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 or a keyboard key to resume.

Commands · SolarOS manual

pwm

List registered PWM controllers.

Usage

UsageDescription
pwm providersList registered PWM controllers.
pwm statusShow named outputs and their frequency groups.
pwm set <output> <freq-hz> <duty-percent>Start PWM on provider:number, or a native GPIO number.
pwm pulse <output> <freq-hz> <pulse-us>Command a pulse width in microseconds.
pwm off <output>Stop a general PWM output.

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 scan [name]Open the graphical rfscan app when included in the firmware.
radio status|listList 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> [sleep|standby|rx|tx]Show or change radio operating state.
radio send <name> <text|byte...>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] <path|pattern> [path|pattern...]Remove files. -f allows directories; -rf removes directories recursively.

Commands · SolarOS manual

robot

Inspect joints in the optional external robot library.

Usage

UsageDescription
robot joint list or robot joint status <joint>Inspect joints in the optional external robot library.
robot model load <file.urdf> or robot model unloadLoad a validated URDF model without activating hardware, or release it.
robot start or robot stopStart URDF-declared velocity controllers or release them while retaining the model. Stop before replacing or unloading it.
robot model info|links|joints|metadataInspect the resident model and retained SolarOS extension.
robot model devices or robot model frame <device-or-stream>List URDF device/frame bindings or resolve a device or registered stream to its frame.
robot model set <joint> <position> [<joint> <position> ...]Supply model joint positions as one validated batch.
robot model transform <target> <source>Query the transform mapping source-frame coordinates into target.
robot model point|vector <target> <source> <x> <y> <z>Transform a point or direction; see Robot models and transforms.
robot joint create <joint> velocity|servoCreate an inactive joint without claiming hardware.
robot joint configure <joint> <key> <value>Configure PPR, gearbox ratio, polarities, limits, timing or control gains.
robot joint motor <joint> <motor> <encoder>Bind named devices to a velocity joint.
robot joint servo <joint> <provider> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz]Bind a calibrated input-shaft servo output.
robot joint command <joint> <target>Refresh an joint target in radians/second or radians.
robot joint enable|disable|close <joint>Activate, stop and release, or destroy an joint; see Robot joints.

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

rtsp-auth

List stored RTSP client accounts (host, port, username; never the password).

Usage

UsageDescription
rtsp-authList stored RTSP client accounts (host, port, username; never the password).
rtsp-auth set <host[:port]> <user> <password>Save a Basic/Digest account used when an rtsp URL has no inline userinfo. Four accounts.
rtsp-auth clear [host[:port]]Remove one stored account, or every account when the host is omitted.

Commands · SolarOS manual

say

Queue text for offline speech, or stream a plain UTF-8 text file aloud with live progress. Pitch and speed default to 100. File mode submits bounded chunks through one continuous PicoTTS/audio session, completes paths with Tab, and remains responsive; press Esc or Ctrl+C to stop. Start speechd with a PicoTTS voice directory first.

Usage

UsageDescription
say [-v <0..100>] [--volume <0..100>] [--pitch <50..200>] [--speed <20..500>] [--drop-if-busy] (--file <path> | [--] <text...>)Queue text for offline speech, or stream a plain UTF-8 text file aloud with live progress. Pitch and speed default to 100. File mode submits bounded chunks through one continuous PicoTTS/audio session, completes paths with Tab, and remains responsive; press Esc or Ctrl+C to stop. Start speechd with a PicoTTS voice directory first.

Commands · SolarOS manual

sc2

Show this device's SC2 end-to-end identity (device ID, fingerprint, prekeys) and the service state. Creates the identity on first use. See man python.sc2.

Usage

UsageDescription
sc2 id; sc2 statusShow this device's SC2 end-to-end identity (device ID, fingerprint, prekeys) and the service state. Creates the identity on first use. See man python.sc2.
sc2 safety <peer>Show the safety number shared with a stored peer, to compare out of band. <peer> is a 32-hex device ID, a unique prefix (6+ hex digits) of a known peer, or an alias.
sc2 verify <peer>; sc2 unverify <peer>Mark the safety number with a peer as compared (or take that back). Chat shows sc2 DMs as (verified) or (unverified); the record is kept in the SC2 store (up to 64 peers). Verifying needs the peer's keys (message it first).
sc2 alias; sc2 alias <name> <peer>; sc2 alias <name> offList, save or delete peer nicknames (up to 16, in the SC2 store) for sc2 safety and messages query <sc2-instance> <peer>.
sc2 selftest; sc2 bench [n]Run the SC2 known-answer and two-device session tests in RAM, or time the primitives and a DM/room round trip. Neither touches stored keys.
sc2 room create ["name"] / sc2 room invite <room-id|name> / sc2 room join <sc2i:...|@file> / sc2 roomsCreate a private SC2 room, print its invite again, join with an invite, or list names, IDs and state. Quote names containing spaces (up to 48 UTF-8 bytes). Admission requires a valid end-to-end invite proof. Then messages send <conv> <text>; messages leave <conv> leaves. SC2 commands, peers and rooms support Tab completion. See man messaging.

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

screen-keyboard

Inspect or control the running screen-keyboard job. Touch or arrow keys select keys; Enter activates the selection.

Usage

UsageDescription
screen-keyboard [status]; screen-keyboard show|hide|toggle; screen-keyboard mode full|symbolsInspect or control the running screen-keyboard job. Touch or arrow keys select keys; Enter activates the selection.

Commands · SolarOS manual

servo

Show names, outputs, enabled state, and owners in a compact list.

Usage

UsageDescription
servo listShow names, outputs, enabled state, and owners in a compact list.
servo status [name]Show calibration, frequency, inversion, and the last accepted command for all servos or one named servo.
servo open <name> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz [inverted:0|1]]Reserve an output without pulses; see Servo outputs.
servo set <name> <angle-rad> / servo pulse <name> <pulse-us>Command a calibrated angle in radians or a pulse width in microseconds.
servo disable <name> / servo close <name>Stop pulses, retaining or releasing the reservation.

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 auto|vt100|ansi|dumb] [--charset utf8|ascii] [--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 cursor [block|underscore] [blink|steady]Show or save the global local-terminal cursor style. Defaults to block blink.
setterm --display <target> [orientation|scale|font|textsize|palette|statusbar] [value]Show or change a named display's profile; orientation and scale are persistent, other targeted settings are volatile.
setterm scale [1..4]Set and save scaling for the current display, or display0 from a port shell.
setterm orientation [0|90|180|270]Set and save orientation for the current display, or display0 from a port shell.
setterm font [mono|compact]; setterm textsize [8|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]Show or set display brightness.
setterm backlight [0..100|follow]Show or set auxiliary lighting, such as the keyboard backlight, independently or following display brightness.
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 [short|long] [action]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

shutdown

Stop apps and jobs, flush storage, and power down. Only the physical power/KEY button, RESET, or a power cycle starts a fresh boot.

Usage

UsageDescription
shutdownStop apps and jobs, flush storage, and power down. Only the physical power/KEY button, RESET, or a power cycle starts a fresh boot.

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] [2048|3072|4096]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, counters, and any declared scalar command range.
stream echo <source> [--once] [--rate 1..100]Print scalar samples with acquisition time, age and native unit, or byte/event source CSV records. IMU records contain one timestamp and all axes. Default display rate is 10 Hz; Ctrl+C stops.
stream write <scalar-sink> <value> [<scalar-sink> <value> ...] [--rate 1..100] [--duration seconds]Publish native float32 scalar values once or periodically. Values outside a declared command range are rejected with the stream ID, value, allowed range, and unit. Ctrl+C ends periodic publishing.
stream tcp listen <name> <port>Register an exclusive duplex byte stream backed by a raw TCP listener. Requires TCP byte-stream support.
stream tcp connect <name> <host> <port>Register an exclusive duplex byte stream for an outbound TCP connection.
stream tcp remove <name>Remove an unclaimed TCP listener or outbound stream.
stream ble listen <name>Register an exclusive duplex BLE L2CAP CoC stream. Advertises when claimed. Requires BLE byte-stream support.
stream ble connect <name> <address> <public|random|rpa_public|rpa_random>Register an outbound CoC stream. Connects when claimed. Use the address and type reported by ble scan.
stream ble status <name>Show connection state, negotiated SDU sizes, queued bytes, and connection interval.
stream ble remove <name>Remove an unclaimed BLE stream.

Commands · SolarOS manual

suspend

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

Usage

UsageDescription
suspendTurn off the primary display and temporarily use the lowpower profile while services and jobs continue. Press KEY or a keyboard 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 a one-shot cumulative FreeRTOS task snapshot. Use ltop for live interval CPU, memory, and stack monitoring.

Usage

UsageDescription
topPrint a one-shot cumulative FreeRTOS task snapshot. Use ltop for live interval CPU, memory, and stack monitoring.

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 [ssid|all]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 [open|wpa|wpa2|wpa/wpa2]]]Start and save SoftAP settings.
wifi ap offStop SoftAP.
wifi nat [status|on|off]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> <path|pattern> [path|pattern...]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
job start speechd /voices/en-US
agent --tts
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.

agent --tts, agent --tts new, agent --tts resume SLOT, and agent --tts ask PROMPT keep the written answer and also queue the successful final model response through the running local PicoTTS speechd. Start speechd with a voice directory first. Status and retry messages, tool calls, tool results, confirmations, and intermediate text before a tool call are not spoken.

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 allows 16 tool calls per request by default. Set a limit from 1 to 32 with agent config max-tools COUNT. When the budget is used, it gives a final answer from the results already collected. Answers are limited to 16 KiB.

agent script supports Python or Lua with a 30-second deadline and up to 4095 output bytes. Model-generated scripts are limited to 640 source bytes and 383 output bytes. Esc or the app-exit key cancels execution.

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 agent supports Responses and compatible Chat Completions endpoints. Set the complete endpoint URL and a model supported by that provider:

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.

Responses resumes with the saved provider response ID. Chat Completions uses local message history. Only completed turns are saved. Each slot holds up to 10 KiB on flash or 48 KiB on SD; a new conversation replaces the oldest slot when full. API keys stay in NVS and are not copied into conversation files.

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.

With Python or Lua included, run a source string or file:

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. The tool returns up to three matching topics' Quick references from the signed downloaded manual. Without that manual, it returns the built-in setup references or download instructions. 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 uses topic names, aliases, summaries, keywords, and Quick references. Downloaded graphics references 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"](). Install or refresh the manual with help update before using APIs whose topics return download instructions. A retained manual from an earlier firmware version is marked as potentially outdated by help status.
  • 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 topic Quick references per lookup, subject to the 4095-byte tool-result limit. Firmware does not embed full scripting guides or a scripting-section excerpt index.
  • Manual search: fixed stack storage for at most 12 results. Built-in setup text and download notices remain in flash. Full verified SD pages are loaded into PSRAM while being read; the signed catalog supplies Quick references for the agent.
  • 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 requires Wi-Fi and PSRAM. Python and Lua execution is available only when the corresponding runtime is included in the firmware. See agent tools for available operations and their policy.

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

amiga

amiga emulates an Amiga 500 with 1 MiB RAM, keyboard, mouse, joystick, audio and one read-only floppy drive. It requires an ESP32-S3 board with PSRAM, graphics and native-module support.

Quick reference

Copy solaros_native_amiga.app.elf to modules/apps/amiga.elf on the preferred storage volume. Boards with 8 MiB PSRAM are recommended; the app needs at least 3 MiB free PSRAM after loading.

cd /sdcard/amiga
amiga kick13.rom workbench.adf

Supply your own raw 256 KiB Kickstart 1.3 ROM and, optionally, an 880 KiB AmigaDOS ADF image. Omit the ADF argument to start without a disk. Neither file is included, and disk images are not modified.

Paths can be absolute or relative to the current directory. Tab completes both filenames.

To launch on the display from a port shell:

session create amiga display0 /sdcard/amiga/kick13.rom /sdcard/amiga/workbench.adf
session close ID

Display and controls

The Amiga screen fits the display with black borders where needed.

ControlAction
KeyboardType into the guest; default mode
F6Select keyboard mode (default)
F7Select keyboard mouse emulation
F8Select keyboard joystick emulation
ArrowsGuest arrows, mouse movement or joystick direction by mode
SpaceGuest Space, left mouse button or joystick fire by mode
EnterGuest Return; right mouse button in mouse/joystick modes
F2Save a PPM snapshot beside the ROM (.ppm)
F10Pause / continue
Ctrl+]Close

Mouse and touch input work in every mode. On touchscreens, slide to move, tap to click, or hold still briefly and then slide to drag. Lift to release.

Audio uses the SolarOS volume setting when audio hardware is available. Switch sessions to suspend and resume emulation; close the session to stop it.

Compatibility

The app supports standard AmigaDOS floppy images. Disk swapping, hard disks, networking, HAM, dual playfield, interlace, AGA and overscan are not supported. Programs that require precise Copper timing may display incorrectly.

amiga --self-test checks the emulator without ROM or disk files. See Native foreground apps for module installation and sessions.

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

Apple II Plus

Apple II Plus is an external foreground app for ESP32-S3 devices with PSRAM, graphics, and native-module support. It emulates a 6502, 64 KiB RAM including the language card, 40-column text, lores/hires and mixed graphics, two Disk II drives, keyboard, paddles, and speaker. Undocumented CPU opcodes act as NOPs. Apple II Plus and Disk II firmware ROMs are bundled. Supply disk images separately.

Quick reference

pkg install apple2
apple2
apple2 /sdcard/apple2/dos33.nib
session create apple2 display0 /sdcard/apple2/dos33.nib

Installation and disks

Install from a compatible module catalog with pkg install apple2, or copy solaros_native_apple2.app.elf to modules/apps/apple2.elf in the preferred storage volume. The app appears in the installed app registry.

No separate ROM files are needed. NIB disk images must be exactly 232960 bytes. Mount one or two disks at launch, or run apple2 without arguments to start Applesoft BASIC. For example, type PRINT 123+456 and press Enter.

Launch

apple2
apple2 drive1.nib [drive2.nib]
apple2 /sdcard/apple2/dos33.nib
load /sdcard/solaros_native_apple2.app.elf /sdcard/apple2/dos33.nib

Disk paths are relative to the launching shell's current directory, or absolute. The older apple2 <disk-directory> [drive1.nib] [drive2.nib] form also works: from /sdcard, use apple2 apple2 dos33.nib. Relative disk names then use that directory. Files opens .nib images with the installed module.

Text and status overlays follow the terminal theme; graphics use Apple II palettes. Changed frames are presented at up to 25 Hz. On native color displays, the aspect-ratio bars are black. Speaker output is optional; emulation continues when audio is unavailable.

Controls

  • F1 saves drive 1; Alt+F1 saves drive 2.
  • F2 toggles guest speed and performance information.
  • F3 toggles disk acceleration and F4 toggles mute.
  • F10 pauses; F11 resets the guest; F12 toggles help.
  • Numeric keypad 1/3 controls the horizontal paddle and 5/2 the vertical paddle. Releasing a key centers that paddle.
  • Ctrl, Alt, and Shift control the three paddle buttons.
  • Ctrl+Alt+Del, Ctrl+], or session close ID closes the app.

Letters are converted to uppercase for the guest. Arrow keys, Backspace, Enter, and Ctrl combinations are translated to Apple II keyboard input.

Sessions and disk saves

Use Alt+Tab or Alt+Right for the next SolarOS session and Alt+Left for the previous session. session fg ID also selects a session. Switching retains guest CPU, RAM, mounted disks, unsaved writes, and the pause state while releasing display and audio. Resume clears held controls and rebases timing.

Disk writes remain in memory until saved with F1 or Alt+F1. Saving atomically replaces the corresponding NIB file and requires writable storage. Closing the app discards unsaved disk changes.

See the foreground app contract and module build instructions.

Applications · SolarOS manual

SolarOS Embedded Apps

Run apps to list the applications available in your firmware. Start an app by its command name, with any arguments shown below.

Common controls

ActionDisplayPort terminal
Close the current appCtrl+Alt+DelCtrl+]
Switch to a retained sessionAlt+Tab or Alt+Right; previous with Alt+LeftCtrl+Z to suspend, then fg or fg ID to resume
Move local input between displaysCtrl+Alt+Arrowsession focus TARGET

Either Alt key works, including AltGr. Apps may also provide Esc or Q to exit. In shared text menus, Alt+Enter toggles full screen; on Pi0cket CL-32 use File+OK. Small text menus start in full screen automatically.

Closing returns to the shell. Python/Lua stdout, errors, and tracebacks remain in shell scrollback; app screens are discarded. Errors return to the shell with a diagnostic. Run status to check the most recent app exit code: 0 means success. See Sessions for switching and display focus.

messages

Manage messaging instances, connections, and local message history on the display or a VT100 terminal. Run messages without arguments to open the TUI. Existing messages subcommands remain available for shell and scripting use.

Press Tab to switch between Instances, Connections, and Messages. Use the arrow keys to select a row, Enter to open it, a for actions, and ? for help. f selects an instance or All instances. The selected instance is shared between tabs. Esc returns to the previous view; q closes the TUI.

  • Instances: n adds an instance using a provider form, e edits its settings, and d removes it after confirmation. Select a field and press Enter to edit it in place with a visible insertion cursor; s saves the form. The other fields remain visible. Unchanged secret fields keep their saved values. Submitting an empty secret field clears it. Removal deletes local history, queued sends, settings, and credentials for that instance. A Deleting... popup stays visible while removal runs. Choose SC2 to enter a relay address such as chat.solar-os.eu:1337 (TLS), a full chats:// or chat:// URL, or a bare domain for signed HTTPS discovery. Domain discovery fills in the endpoint, TLS pin, relay signing key, and fallback relays. An admission code remains optional and must be supplied by the operator when required. The other optional fields are the TLS pin and relay signing key (base64). New SC2 instances start disabled. Changing the relay URL resets its TLS and signing-key pins unless you also edit those fields.
  • Connections: c enables the selected instance and x disables it. s starts its background worker; the same action is in the a menu and connection details. Workers that are already running or waiting for memory are left active. A shared worker serves the enabled instances of its provider. Enter shows connection state, worker state, and errors. Enabling an instance and starting its worker are separate actions. Link transports prompt for the worker name and settings, as used after job start, for example radio-link mylink radio0 PROFILE chat=on or espnow-link mylink chat=on. Use the transport name saved in the Link instance.
  • Messages: v cycles through conversations, unread messages, and the pending outbox. Enter opens conversation history or a message's full text. Page Up loads older records and Page Down loads newer records. c opens Chat for the selected conversation; closing Chat returns to this browser. r marks the selected conversation read. d deletes one selected local message after confirmation. A pending send must first be cancelled with x before its history can be deleted. Local deletion does not retract a remote message.

Message browsing uses the persistent Messaging archive. The universal Inbox also contains notifications from other applications; see inbox for those. See Messaging for provider settings, trust, and storage behavior.

dlp

Use a named SolarOS display as a Display Link Protocol receiver. The dlp package requires graphics support. One receiver may run at a time.

dlp serve display0 stream=cdc0
dlp status
dlp stop display0

Run these commands from the local display shell or a separate port/network shell. The selected stream must provide exclusive, ordered, duplex bytes. Release any shell, log, or bridge owner on that stream first; DLP needs both directions for commands and replies. DLP does not configure baud rates, establish network connections, or add a BLE/radio transport.

The receiver runs as a foreground session on the named display, suspending its previous session. It advertises the current display orientation and MONO1 pixels (black=1, least-significant pixel first). Targets with direct RGB565 presentation also advertise RGB565 with big-endian pixel bytes. The host must use an advertised format. Updates are staged; the display changes on PRESENT. Changed rectangles and RLE are supported. A new connection/session must send a full image before partial updates. The receiver also supports negotiated upload windows of up to 16 packets. Uploads receive cumulative progress acknowledgements, and the final reply waits for presentation completion. Lost or corrupt packets are retransmitted within the bounded window; retries do not repeat accepted writes or PRESENT.

MONO1 uses direct presentation where supported. Displays with an INDEX2 raster backend use batched MONO1 conversion and send changed regions, without rebuilding a graphics canvas. This path needs an extra two-bit-per-pixel conversion buffer (38,400 bytes at 480×320). Initial frames and resume redraws repaint the full display. Other targets use the regular graphics canvas.

Ctrl+], Ctrl+Alt+Delete, Escape, or dlp stop <display> closes the receiver, releases the stream, and restores the previous session, including while waiting for a host. Suspension pauses protocol processing; resuming redraws the last completed image. Provider disconnects and I/O errors leave the app waiting for a new connection, retaining the last completed image. The receiver retries opening an unavailable stream. Each reopened transport starts a fresh protocol session and requires a full image. Serial links with no disconnect indication remain listening until stopped.

The desktop test sender requires Python, pyserial, and Pillow:

python3 -m pip install pyserial Pillow
python3 scripts/dlp_image.py --port /dev/ttyACM0 image.png
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --frames 20 --stats
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --fps 30 --frames 100 --stats
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --frames 6 --reconnect-every 2

Start the receiver before running the sender. The script discovers geometry, fits images on a white background, and sends MONO1 frames. --legacy-cells tests fixed-cell framing; compact DLP framing is the default. This is an image test tool; desktop virtual-monitor integration belongs to a host project. Windowed uploads are negotiated by default, with automatic fallback for older receivers. --no-window forces the original stop-and-wait behavior for comparison; it is independent of --legacy-cells. After display suspension invalidates retained content, the sender starts a new session and repaints fully.

For Wi-Fi, create a raw TCP stream from a separate device console:

stream tcp listen tcp0 7777
dlp serve display0 stream=tcp0

Then run the desktop sender, replacing the example address with the device IP:

python3 scripts/dlp_image.py --tcp 192.0.2.10:7777 --demo --fps 30 --frames 100 --stats

TCP needs Pillow but does not need pyserial. Use the raw TCP listener, rather than a Telnet shell port. Closing the TCP sender leaves DLP waiting on the same listener. Connect a new sender without running dlp serve again. To remove the endpoint, stop DLP first if it is running, then run stream tcp remove tcp0.

For a BLE connection between two SolarOS devices, configure a CoC listener on the receiving device and start DLP:

stream ble listen ble0
dlp serve display0 stream=ble0

On the sending device, use ble scan to find the advertised SolarOS ble0 peripheral. Copy its address and address type into the connect command:

stream ble connect ble0 aa:bb:cc:dd:ee:ff public
expansion attach stream-dlp remote stream=ble0
display test remote

The central/peripheral roles belong to the BLE connection; either side can serve a display or send frames. Release the consumer to close the connection. DLP serving waits for a new connection after a disconnect. An attached stream-dlp display reconnects on a subsequent presentation attempt. To remove an endpoint, stop DLP or detach the display, then stream ble remove ble0. See BLE stream commands.

--stats prints actual completion rate, byte counts, and host wall times per input frame: prep generates/loads and converts the image; upload plans and sends the update, including intermediate window checkpoints; present_wait runs from PRESENT through the completion reply. With windows it also includes sending the final buffered upload batch. total includes preparation and any requested reconnect, and excludes the intentional frame-rate sleep. The final summary counts presented frames and unchanged images separately; unchanged images do not increase completed FPS. The default rate limit is 2 FPS; --fps 30 sets a 30 FPS ceiling and includes image preparation in that budget.

Run dlp status from a separate shell for receiver-side last/average/maximum timings in microseconds. render measures MONO1 canvas preparation or changed-area detection and batched INDEX2 conversion; display measures the display-service call, including driver conversion, rotation, tile checks and I/O. Unconverted direct rasters have zero separate render time. All driver work is included under display. total is their sum. These measurements exclude wire upload, startup and resume/overlay redraws; they measure service completion, not panel scanout. Completion rate averages the intervals between the first and last DLP frame, including host pacing. The statistics remain available after stopping, until the next receiver start. Host present_wait includes stream transfer, round trips, scheduling and legacy status polling, so it is not a device-only or SPI-only measurement. Diagnostics never enter the DLP byte stream.

rtsp

Play a trusted-LAN RTSP stream using UDP RTP/JPEG video and RTP/L16 audio.

rtsp rtsp://192.168.1.238/media
rtsp 192.168.1.238/media
rtsp user:secret@192.168.1.192:8554/youtube
rtsp --audio-only rtsp://192.168.1.113/media
rtsp --stats rtsp://192.168.1.192:8554/youtube
rtsp --audio-only --stats rtsp://192.168.1.192:8554/youtube
rtsp-auth set 192.168.1.50 admin camera-password
rtsp rtsp://192.168.1.50/media

The rtsp:// prefix is optional. The RTSP control connection uses TCP port 554 unless a port is supplied; RTP media uses negotiated UDP ports. Addresses without a path are accepted; include the publisher's path when required.

RTSP playback keeps Wi-Fi awake while connecting, playing, and reconnecting to avoid delayed or dropped live media. Normal power saving resumes after playback stops, unless another active service still needs low-latency Wi-Fi.

Inline credentials use user:password@host; percent-encode spaces and reserved characters in the username or password. rtsp-auth set <host[:port]> <user> <password> saves up to four accounts for URLs without inline credentials. Run rtsp-auth to list accounts, or rtsp-auth clear [host[:port]] to remove one account or all accounts when the host is omitted. The player hides passwords.

Graphical sessions show aspect-fit JPEG video when a video track is selected. Audio-only sessions use the common audio GUI oscilloscope, fed by playback PCM, filling the available area between the header and player controls. Port shells select audio only and print playback status. --audio-only skips video negotiation and video buffers even when the publisher offers both tracks. Audio uses the selected default output and global volume; Up/Down adjust volume (+/- remain aliases). The graphical player uses the common header, volume bar, and Stop/Play button. Enter, Space, or a tap on the button stops playback without closing the app; Play reconnects to the same URL. Stopping releases the network, decoder, audio output, and media buffers once their workers finish. RTSP URLs are single sources, so there are no Previous/Next buttons. F toggles fullscreen, hiding the header and controls while retaining aspect-fit video or expanding the audio-only oscilloscope to the whole screen. D toggles the separate frame diagnostic overlay: received, displayed, network/app drops, and displayed frames per second. Q, Escape, or the normal application-exit key closes playback. Suspending keeps audio playing and pauses video decoding until you return. Closing releases the audio output and network connection.

Temporary connection failures retry up to six times. Unsupported codecs, failed or unsupported authentication, and local memory or audio failures stop immediately. The shell diagnostic identifies the failing operation. Use --stats and log show for frame counts, drops, timings, audio buffering, and retry details. The D overlay works without logging; --stats also enables it initially.

Requirements and limits:

  • IPv4 RTSP 1.0 with unicast UDP media. Client authentication supports Basic or Digest MD5 (qop=auth or no qop); the rtspd publisher remains unauthenticated.
  • One JPEG video track and/or one L16 audio track (8–48 kHz, mono or stereo).
  • JPEG types 0/1 with explicit 8-bit quantization tables (Q=255); restart markers are supported.
  • TCP interleaving, multicast, encrypted RTSP, compressed audio, and redirects are unsupported.
  • Five seconds without media triggers reconnection. Retry exhaustion returns an error to the shell.
  • rtspd allows one receiver at a time; stop another receiver before connecting.
  • Microphone capture cannot be opened while playback is active.

If playback reports no memory, use mem to check internal memory and its largest free block, then stop unused jobs. PSRAM alone does not meet audio and DMA memory requirements.

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
job start speechd /voices/en-US
agent --tts
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.

agent --tts, agent --tts new, agent --tts resume SLOT, and agent --tts ask PROMPT keep the written answer and also queue the successful final model response through the running local PicoTTS speechd. Start speechd with a voice directory first. Status and retry messages, tool calls, tool results, confirmations, and intermediate text before a tool call are not spoken.

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 allows 16 tool calls per request by default. Set a limit from 1 to 32 with agent config max-tools COUNT. When the budget is used, it gives a final answer from the results already collected. Answers are limited to 16 KiB.

agent script supports Python or Lua with a 30-second deadline and up to 4095 output bytes. Model-generated scripts are limited to 640 source bytes and 383 output bytes. Esc or the app-exit key cancels execution.

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.

On displays narrower than 240 pixels or shorter than 180 pixels, the graphical media controls use an 8-pixel bitmap font and smaller transport buttons. Short displays show volume as a percentage beside the filename instead of a volume bar. Lists scroll to keep the selected entry visible, and edit dialogs fit the display. When the cassette cannot fit, the visualizer uses a scope.

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.

Small graphical displays show one selected control at a time, with its position in the eight-control sequence. The waveform remains visible when space allows. Short terminals scroll the controls to keep the selection visible. Long output names are clipped to the available width.

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.

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. The graphical controls and lists adapt to small displays, including 240×64 and 128×64 targets.

On a graphical session, Tab switches between Play and Playlist. The Play tab uses the space above the shared bottom controls 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. </> seeks backward/forward by ten seconds without changing tracks; rewind/forward buttons provide the same actions. Seeking preserves pause state and discards queued audio. The controls show the track name, playback status and time above the volume bar. The middle button stops or starts playback and indicates pause while paused. 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, </> seeks backward/forward ten seconds, 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.

Player shows SEEKING while preparing a new position. MP3 seeking can take longer on slow storage, including with variable-bitrate files. Stop playback to cancel a seek.

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 groups conversations by named instance, including IRC, Gateway, MeshCore, and Link. 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.

/leave removes the current channel or room from Chat's Channels tab and returns to that tab. Its local history is retained. Use /join instance channel to show the room and its previous messages again. Explicit departures survive restart; disconnecting a provider keeps its channels visible. Messages still provides access to retained history while a room is left.

MeshCore channel labels distinguish [radio] and [companion]. Entries marked (history) retain readable messages but cannot accept sends. After submitting a message, the footer follows its delivery state through queued, sending, sent, delivered, or failed.

Usage:

chat [instance|class|conversation-id]

With no selector, Chat opens a unified view and initially selects the newest unread conversation. An instance name or provider class 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.

Additional Gateway connections use messages add gateway NAME URL [TOKEN], messages connect NAME, and messages join NAME ROOM. The same gateway-sync job maintains every enabled instance independently. In Chat, use /add gateway NAME URL [TOKEN], /connect NAME, and /join NAME ROOM. /remove NAME closes the instance and deletes its settings, pending sends, and local history.

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 instance's state. Connection providers use disconnected, connecting, and connected; connectionless MeshCore and Link providers use stopped, starting, ready, and error.

In-app commands:

/help
/new contact-id
/join instance channel
/query instance nickname
/leave
/connect instance
/disconnect instance
/remove instance
/server instance host port nickname
/sasl instance account password
/sasl instance off
/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. The GUI uses compact controls, scrolling channels and fitted edit dialogs on small display targets. 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. Playlists, HLS, and AAC are unsupported.

Controls:

  • Tab switches between the Player and Channels tabs in the GUI.
  • The Player tab gives the space above the shared bottom controls to a live PCM oscilloscope or spectrum analyzer. V switches visualizers. The spectrum analyzer shows the frequencies in the playing audio.
  • On the Player tab, Left and Right play the previous or next catalog channel. The catalog wraps as a ring. Enter stops playback or reconnects the channel; Space pauses/resumes, and Up/Down changes global volume in five-percent steps.
  • The bottom controls show the channel name, playback state and elapsed output time above the volume bar, followed by Previous, Stop/Play, and Next buttons. The buttons support clicking or tapping; the middle icon indicates pause while paused. Live-channel pause uses bounded audio buffers; a server may disconnect during a long pause, requiring reconnection.
  • 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.

Playback continues when you switch sessions. Closing WebRadio stops reception and playback and releases the output device. The visualizer follows played audio, so it does not run ahead of sound while the stream buffers.

help

Foreground browser for the package-aware SolarOS manual. The foldable tree groups the topics available in the current firmware and shows whether it is using the setup and recovery guide or a verified downloaded revision. Full guides and scripting API references require the downloaded manual; use help update to install it on devices with Wi-Fi, PSRAM, and SD storage. 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 app registry, including installed external foreground apps. Associations include images to view, WAV/MP3 to player, MPEG-1 .mpg/.mpeg files to vplay, CSV to sheet, Python and Lua scripts to their runtimes, documents to reader (or writer when Reader is unavailable), and .gb ROMs to the installed Game Boy module. NIB disk images open with the installed Apple II Plus module. 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/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.
  • Space toggles a mark on the current file or directory and advances to the next item.
  • 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
ftp HOST [PORT] [--user USER --password PASSWORD] [--remote PATH] [--local PATH]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage.

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. Starting without arguments opens the disconnected file manager; F2/n opens the connection form. The command-line form remains available as a direct connection shortcut. If login is rejected, FTP reopens the form on the password field with the entered host, port, user, remote path, and masked password preserved. Connection failures are reported as refused, timed out, host unreachable, or network unreachable when the socket supplies that distinction, and as server unavailable otherwise.

Controls:

  • Up/Down selects an item. PgUp/PgDn moves by one visible page, and Home/End selects the first/last item. Tab, Left, and Right switch panes.
  • Space toggles a mark on the current file or directory and advances to the next item, as in Files. Marked items show *; each pane shows its mark count. Copy, move, and delete use the active pane's marked items, or the current item when none are marked. The parent entry cannot be marked.
  • F2/n opens the connection form. Tab or Up/Down selects a field, Enter connects, and Esc cancels the form.
  • Enter opens a directory; Backspace opens its parent. r refreshes the remote and local listings. h toggles hidden files and directories in both panes. Hidden items are shown initially; the title shows hidden:on or hidden:off.
  • F3/V 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/C copies the current or marked files and directories to the other pane. Directory transfers are recursive. Copy and move show the same preparing/transfer progress popup used by Files.
  • F6/M moves the current or marked files and directories to the other pane by completing each item's copy before deleting its source.
  • F7/k/K creates a directory in the active pane.
  • F8/D recursively deletes the current or marked items after confirmation. The help bar emphasizes the same operation mnemonics used by Files and SFTP.
  • 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. Marks survive refreshes in the same directory and pane switching. Opening another directory clears that pane's marks; hiding hidden items clears their marks. Completed items are unmarked. A failure stops the batch and leaves remaining items marked.

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.

sftp

Two-pane SFTP file manager with the same panes, file operations, progress, and key bindings as ftp. The left pane is local mounted storage and the right pane is a remote SSH server's SFTP subsystem. It uses the shared SSH transport, known-host policy, host aliases, and password or public-key authentication.

Usage:

sftp
sftp [user@]HOST[:PATH] [PORT] [--user USER] [--password PASSWORD] [--remote PATH] [--local PATH]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage. Completion preserves a supplied user@ prefix.

Starting without arguments opens the disconnected file manager. Press F2/n to enter the host, port, user, optional password, and initial remote path. Leave the password empty to use the configured SSH key. When user@ and --user are omitted from the command-line shortcut, SFTP uses the SolarOS identity user. A remote path can be appended to the host, for example sftp nils@remote:/directory; this form connects immediately and opens that directory. --remote PATH overrides an appended path. A command-line password remains in shell history; the connection form masks it. The active form field keeps a visible text cursor. If passwordless key authentication fails, SFTP reopens the form on the password field instead of showing a low-level error.

Controls are identical to FTP: F2/n opens connection setup; F3/V views, F5/C copies, F6/M moves, F7/k/K creates a directory, and F8/D deletes after confirmation. PgUp/PgDn moves by one visible page, Backspace opens the parent directory, and h toggles hidden files and directories in both panes. Space marks or unmarks files and directories and advances the cursor, as in Files. Copy, move, and delete operate on marked items in the active pane, or the current item if none are marked. Transfers are recursive, show progress, retain both pane positions after refresh, and stage downloaded and uploaded files before replacement. q, F10, or the app-exit key closes the SSH connection and exits.

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. The footer shows a Actions and ? Help, including on compact screens.

When Chat is included, select a contact and press c to open their direct conversation. Closing Chat returns to Contacts. Chat uses the contact's preferred direct endpoint, favoring trusted endpoints; sending to a discovered endpoint still requires confirmation. If its saved provider instance is unavailable, Contacts names that instance in the error. Check it in Messages before opening a chat. A running worker does not by itself mean its radio or companion binding is ready.

Press a in the list or details to manage the selected contact:

  • c: open Chat for the selected contact, when Chat is included.
  • t: trust all endpoints or choose one endpoint.
  • b: block all endpoints or choose one endpoint.
  • r: edit the contact name, then Enter to save or Esc to cancel.
  • l: choose another contact to link into the selected contact. Confirm with y to move its endpoints and remove its source record.
  • d: remove the contact and its endpoints after confirming with y.

These shortcuts also work directly from the list and details. Use arrows and Enter in menus, and Esc to cancel. Confirmation pages require y; Enter does not confirm removal or linking. Press f in the list to filter by endpoint trust (all, discovered, trusted, or blocked), and s for store status. ? opens a scrollable key guide. Filters match contacts with any endpoint in that trust state; linking can select any other contact regardless of the current filter or name search.

The same operations are available from the shell:

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 512-contact store is full, radio discovery may evict the oldest unpinned contact whose endpoints are all still discovered; trusted and blocked records are never automatically evicted. BLE companion imports preserve existing records and report overflow instead of evicting contacts. The shared store supports up to 576 endpoints.

The versioned store is CRC checked, uses two alternating headers and data copies, remains below 192 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. Narrow screens show fewer columns and keep the selected pin visible.
  • Pins, Buses, and Claims fit their columns to the screen width. Bus state stays ahead of routing details; trailing columns are omitted when space is limited.
  • 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, 1-Wire, PS/2, or CAN bus, depending on the installed drivers.
  • 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.
  • CAN creation selects TX/RX GPIOs, bitrate in kbit/s, listen-only or normal mode, and an optional transceiver-select GPIO with its active level. The default is 500 kbit/s in listen-only mode. CAN controllers appear in Buses; runtime controllers support removal and Autostart, which saves an expansion attach twai-can ... command. A compatible CAN transceiver is required; see manual hardware.can.
  • 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. The playfield scales to the active display; ship movement, shields and collision detection use the same game coordinates. Small displays use bitmap text for the score and lives.

Usage:

invaders

Controls:

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

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.

ltop

Live text task monitor for display and port terminals. Unlike the top command's cumulative snapshot, ltop samples the FreeRTOS runtime counters once per second and reports the interval deltas. One progress bar shows the busy share of each CPU core, derived from that core's idle-task delta. Each task percentage is its share of one core, so the task total can approach 200% on a dual-core target. The title shows the current aggregate CPU load instead of the fixed maximum capacity.

The IRAM and ERAM progress bars show the currently used share of the internal and external heaps. ERAM is shown as unavailable on boards without external RAM.

The task table is sorted by current CPU load and shows task state, priority, interval CPU percentage, and peak stack consumption since the task started. Peak stack consumption is the allocated stack size minus the minimum remaining stack reported by FreeRTOS. Idle tasks are omitted from the table because their counters drive the per-core progress bars. Columns expand across wide terminals and the priority column is hidden on narrow terminals.

Usage:

ltop

Controls:

  • Up/Down or j/k scroll one row.
  • Page Up/Page Down, Home/End, or g/G move through the task list.
  • r resets the sampling baseline.
  • q, Esc, or the app-exit key exits.

rfscan

Graphical receiver-energy monitor for attached radios that report the live_rssi capability. Requires a graphical display and the radio_scanner group. Radio drivers provide live energy readings and validate tuning and bandwidth settings through the common radio API; the app does not select support by radio model.

rfscan
rfscan radio0
radio scan radio0

Select the radio at launch with rfscan <name>; it remains selected for that app session. With no argument the app selects the first attached radio. The radio configuration TUI also opens the scanner with s on a graphical display. From a text-only shell, use session create rfscan display0 radio0 to open it on the device display.

Channel plots live RSSI against time. Time cycles through 10 seconds, 1 minute, 10 minutes and 1 hour. Sampling intervals increase with the time window; short bursts can be missed. The graph shows the latest level and the minimum and peak since starting or clearing. When another service is already receiving, the scanner observes without retuning, consuming packets or taking its radio claim. Transmit and sleep intervals produce no energy samples. An idle radio is claimed and placed in receive mode until scanning stops.

PgUp zooms into the RSSI plot's Y axis and PgDown zooms out. Available spans are 5, 10, 25, 50, 100 and 200 dB; the default is 100 dB. In Channel, the plot centers on the average RSSI of samples inside the displayed time window. In Band, it centers on the average of the current frequency-bin readings. Empty bins and held peaks do not affect the average. Each zoom key centers the axis on that average, then holds the axis steady until the next zoom key. The Avg readout continues updating; values outside the selected span are clipped to the plot edges. Zoom leaves scanning and the alert threshold unchanged and does not change waterfall shading. A threshold outside the visible Y range keeps working but its horizontal marker is hidden.

Channel controls are Start/Stop, Time, Step, Freq and Thresh. Step selects a frequency increment of 25, 62.5, 125 or 250 kHz without interrupting monitoring. With Freq selected, Up/Down increases/decreases the requested frequency by that increment; the frequency is shown to 1 Hz. Changing frequency applies it and resumes acquisition automatically when monitoring was running. Tuning requires an unclaimed radio and preserves the current receiver bandwidth. A busy radio reports its owner. Stop, suspension or exit restores the previous frequency and operating state.

Enter on a setting opens a prefilled input popup. Type to replace the current value, use Left/Right, Home/End, Backspace or Delete to edit, or Ctrl+U to clear the field. Enter applies and Esc cancels. Frequency uses MHz; Step and BW use kHz, including decimal values. A BW of 0 keeps the current receiver filter. Range accepts start and end frequencies in MHz separated by a space; Time accepts 1–3600 seconds. Values that would exceed the supported number of scan bins are rejected or require a larger step. Radio drivers validate frequency and bandwidth when scanning starts. Opening or cancelling a popup leaves the current scan running.

Thresh controls an audible energy alert in both tabs. The default is −90 dBm; Up/Down changes it by 1, 1, 1, 2, 5 or 10 dB for Y spans of 5, 10, 25, 50, 100 or 200 dB, respectively, and cycles through Off at the ends of the −130 to −30 dBm range. Enter accepts an exact dBm value or off. Changing the threshold leaves scanning running. A dashed horizontal line marks the threshold; fresh samples at or above it request a short beep at most once per second, including brief hits between graphical refreshes. Alerts use the selected audio output and global volume/mute, skip busy audio, and are cancelled on Stop, suspension or exit. Audio failures are reported without ending the scan. The center frequency of the strongest sample responsible for the last alert is shown beside the threshold as Threshold: >=-90dBm Triggered: 868.375000MHz. Before the first alert, Triggered shows --. The frequency remains visible while scanning moves on; the next alert updates it.

Band shows received energy against frequency, a dashed peak trace and a waterfall with the newest completed sweep at the top. Darker pixels mean stronger energy. The waterfall uses dithering on 1-bit displays; buttons use solid backgrounds, outlines and inverse selection. Range cycles through ±250 kHz, ±1 MHz, ±2 MHz and a preset band: 433.05–434.79, 863–870 or 902–928 MHz, selected from the current frequency. Step cycles through 25, 62.5, 125 and 250 kHz; wide ranges increase the step when necessary to stay within 129 frequency bins. BW selects the current receiver bandwidth or 62.5, 125 or 250 kHz. Drivers can round the requested bandwidth up to a supported filter; the running scan shows the applied bandwidth. Unsupported settings are rejected by the radio driver. The final frequency shown is the last sampled bin.

Three status lines above the graph show tuning settings, RSSI readings and the alert threshold with the last alert frequency. Errors replace the RSSI readings. The top header shows LIVE with owns RX, observe RX or RX paused for a band sweep; stopped acquisition shows STOPPED. In Channel, the header also shows the time window. The tuning row shows the frequency or band range, with BW: and Step: labels. Sampling runs independently of graphical refreshes. Radios advertising fast_tune use lightweight frequency changes and driver-defined settling times; other live_rssi radios remain supported through normal configuration. Sweep speed depends on the driver, bandwidth, range and frequency step.

Band scans require exclusive ownership of the selected radio. A busy radio shows its owner; stop that owner explicitly before scanning. Normal packet reception on that radio is paused during a sweep. Band scanning has no idle timeout. Stop, app suspension, app exit or a scan error ends the sweep. The scanner restores the previous configuration and operating state and releases its claim; a restoration failure is reported. Changing settings or switching tabs automatically resumes acquisition if it was running; a stopped scan stays stopped. Rejected settings restore the previous selection and resume it when radio ownership permits. Returning to a suspended app leaves it stopped until Start is selected.

ControlAction
Tab, or tap a tabSwitch between Channel and Band
Left/RightSelect a bottom function
PgUp/PgDownZoom the RSSI Y axis in/out, centered on the displayed average
Up/DownNext/previous value of the selected function; Up starts and Down stops when Start/Stop is selected
Enter, or tap a bottom buttonToggle Start/Stop, or open the selected setting's input popup
SpaceStart or stop
r, s, tTime/range, step, threshold
f in Channel, b in BandIncrease frequency by Step, or advance bandwidth
cClear history and peak hold
Ctrl+Alt+Del, q or EscExit

RSSI measures energy inside the receiver filter without decoding its source. Frequency bins are sampled sequentially; receiver bandwidth limits frequency detail. Scan only within the attached module's supported RF band and antenna range. This display does not provide raw I/Q samples.

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]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage, including in raw mode.

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.

vplay

Graphical MPEG-1 media player with optional MP2 audio. The bottom controls show playback status and elapsed time above the volume bar; full screen hides the controls and time. The middle Stop/Play icon indicates pause while paused.

The viewport and controls adapt to small targets, including 240×64 displays. Full screen uses the entire display for video. The minimum target size is 84×48 pixels.

Usage:

vplay [-fit|-actual] <file.mpg>

Supports MPEG-1 program streams (.mpg and .mpeg) containing one MPEG-1 video track and optionally one MPEG-1 Layer II (MP2) audio track. MPEG-2, MPEG-4/H.264, AVI, transport streams, and MP3/AAC audio are not supported. The source limit is 640x480; lower resolutions and frame rates are recommended for smooth playback. Playback speed depends on source complexity, display transfer speed, and output size.

Controls:

  • Space pauses/resumes.
  • Up/Down changes global volume.
  • Enter stops playback or restarts the selected file.
  • </> seeks backward/forward ten seconds, preserving pause state.
  • Left/Right selects the previous/next MPEG file in the same folder, in filename order. At either end, the selection stays unchanged.
  • The shared Previous, Rewind, Stop/Play, Forward, and Next buttons provide the same actions when clicked or tapped.
  • f toggles full screen, hiding the header, volume strip, and transport buttons.
  • 0 selects actual size and 1 selects fit. Actual-size video larger than the viewport is cropped centrally.
  • d toggles frame/timing log diagnostics.
  • Esc or app-exit key exits.

Playback pauses while its session is suspended and returns to the shell at the end of the file. Audio uses the selected SolarOS output and acts as the playback clock when present; a file with audio requires an available playback output. Video dimensions and frame rate must remain constant, and the initial audio/video timestamp difference must be no more than ten seconds.

SEEKING means the player is preparing a new position. Files without usable seek timestamps take longer. Stop or close playback to cancel. Positions beyond the end select the last frame.

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. Runtime rotation preserves the drawing and its saved dimensions. The canvas fits inside the new viewport, and pointer input follows the fitted image. Gray margins mark space outside the drawable canvas after rotation.

Usage:

sketch
sketch file.png

Sketch requires at least 200×200 pixels. On smaller targets it reports that the display resolution is too low and returns to the shell.

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. A failed save preserves the previous document.
  • 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. Direct MJPEG URLs are shown as live video using the same JPEG renderer.

Usage:

web http://host/
web https://host/path
web http://camera-host/camera.mjpeg

MJPEG playback uses a bounded 512 KiB assembly buffer and a single pending decoded frame. If decoding or display presentation falls behind the source, web drops the stale pending frame and keeps the newest one instead of growing latency or memory use. Reload, Back, Forward, and app exit cancel the active HTTP stream. The stream inherits the security properties of its URL; plain HTTP provides no encryption or peer authentication.

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.

zoo

Browse the hosted model zoo and install complete model bundles on mounted SD or flash storage. The text interface works on the display and VT100 port shells. It requires the Zoo package, ESP32-S3, PSRAM, and networking.

zoo
zoo refresh
zoo source [HTTPS-CATALOG-URL|reset]
zoo storage [sd|flash]
zoo install ID VERSION

The default source is https://solar-os.eu/zoo/catalog.json. The first launch fetches it; later launches use the cached catalog. r refreshes the cache. Up/Down and Page Up/Page Down select entries, Enter opens details, Alt+S searches, and d downloads the selected version. Arrows and page keys scroll long details. * marks an installed bundle and ! marks an incompatible entry. c or Esc cancels a running operation. q or Esc returns from details or exits the browser; the app-exit key exits from either view.

Source and storage preferences persist. Without an explicit storage preference, Zoo uses mounted SD, then flash. Files are stored under dl/zoo/ on that volume; each installed bundle is in models/ID/VERSION/. Downloads are streamed to storage, checked against archive, manifest, model, and asset hashes, and installed through a staged directory swap. Interrupted downloads preserve the previous installation.

The browser shows task, modality, result adapter, license, validation status, and reported free-memory requirements. Runtime compatibility is not a device inference test, and unreported memory remains unknown. The device accepts catalogs up to 512 KiB and 256 entries; larger catalogs fail without replacing the cache. Installation also needs enough storage for the ZIP and extracted files.

Installation does not load a model. Use the displayed bundle.json path with model bundle PATH or the native, Python, or Lua inference APIs. Loaded models remain resident until explicitly unloaded or the device reboots. See Model bundles for inputs, results, and execution, and Native image pipelines for a Zoo-to-camera workflow and a Python detection example. Catalog task names describe the model; check its result adapter before selecting a script. Raw tensor bundles need model-specific interpretation.

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 groups conversations by named instance, including IRC, Gateway, MeshCore, and Link. 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.

/leave removes the current channel or room from Chat's Channels tab and returns to that tab. Its local history is retained. Use /join instance channel to show the room and its previous messages again. Explicit departures survive restart; disconnecting a provider keeps its channels visible. Messages still provides access to retained history while a room is left.

MeshCore channel labels distinguish [radio] and [companion]. Entries marked (history) retain readable messages but cannot accept sends. After submitting a message, the footer follows its delivery state through queued, sending, sent, delivered, or failed.

Usage:

chat [instance|class|conversation-id]

With no selector, Chat opens a unified view and initially selects the newest unread conversation. An instance name or provider class 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.

Additional Gateway connections use messages add gateway NAME URL [TOKEN], messages connect NAME, and messages join NAME ROOM. The same gateway-sync job maintains every enabled instance independently. In Chat, use /add gateway NAME URL [TOKEN], /connect NAME, and /join NAME ROOM. /remove NAME closes the instance and deletes its settings, pending sends, and local history.

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 instance's state. Connection providers use disconnected, connecting, and connected; connectionless MeshCore and Link providers use stopped, starting, ready, and error.

In-app commands:

/help
/new contact-id
/join instance channel
/query instance nickname
/leave
/connect instance
/disconnect instance
/remove instance
/server instance host port nickname
/sasl instance account password
/sasl instance off
/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. The footer shows a Actions and ? Help, including on compact screens.

When Chat is included, select a contact and press c to open their direct conversation. Closing Chat returns to Contacts. Chat uses the contact's preferred direct endpoint, favoring trusted endpoints; sending to a discovered endpoint still requires confirmation. If its saved provider instance is unavailable, Contacts names that instance in the error. Check it in Messages before opening a chat. A running worker does not by itself mean its radio or companion binding is ready.

Press a in the list or details to manage the selected contact:

  • c: open Chat for the selected contact, when Chat is included.
  • t: trust all endpoints or choose one endpoint.
  • b: block all endpoints or choose one endpoint.
  • r: edit the contact name, then Enter to save or Esc to cancel.
  • l: choose another contact to link into the selected contact. Confirm with y to move its endpoints and remove its source record.
  • d: remove the contact and its endpoints after confirming with y.

These shortcuts also work directly from the list and details. Use arrows and Enter in menus, and Esc to cancel. Confirmation pages require y; Enter does not confirm removal or linking. Press f in the list to filter by endpoint trust (all, discovered, trusted, or blocked), and s for store status. ? opens a scrollable key guide. Filters match contacts with any endpoint in that trust state; linking can select any other contact regardless of the current filter or name search.

The same operations are available from the shell:

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 512-contact store is full, radio discovery may evict the oldest unpinned contact whose endpoints are all still discovered; trusted and blocked records are never automatically evicted. BLE companion imports preserve existing records and report overflow instead of evicting contacts. The shared store supports up to 576 endpoints.

The versioned store is CRC checked, uses two alternating headers and data copies, remains below 192 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

dlp

Use a named SolarOS display as a Display Link Protocol receiver. The dlp package requires graphics support. One receiver may run at a time.

dlp serve display0 stream=cdc0
dlp status
dlp stop display0

Run these commands from the local display shell or a separate port/network shell. The selected stream must provide exclusive, ordered, duplex bytes. Release any shell, log, or bridge owner on that stream first; DLP needs both directions for commands and replies. DLP does not configure baud rates, establish network connections, or add a BLE/radio transport.

The receiver runs as a foreground session on the named display, suspending its previous session. It advertises the current display orientation and MONO1 pixels (black=1, least-significant pixel first). Targets with direct RGB565 presentation also advertise RGB565 with big-endian pixel bytes. The host must use an advertised format. Updates are staged; the display changes on PRESENT. Changed rectangles and RLE are supported. A new connection/session must send a full image before partial updates. The receiver also supports negotiated upload windows of up to 16 packets. Uploads receive cumulative progress acknowledgements, and the final reply waits for presentation completion. Lost or corrupt packets are retransmitted within the bounded window; retries do not repeat accepted writes or PRESENT.

MONO1 uses direct presentation where supported. Displays with an INDEX2 raster backend use batched MONO1 conversion and send changed regions, without rebuilding a graphics canvas. This path needs an extra two-bit-per-pixel conversion buffer (38,400 bytes at 480×320). Initial frames and resume redraws repaint the full display. Other targets use the regular graphics canvas.

Ctrl+], Ctrl+Alt+Delete, Escape, or dlp stop <display> closes the receiver, releases the stream, and restores the previous session, including while waiting for a host. Suspension pauses protocol processing; resuming redraws the last completed image. Provider disconnects and I/O errors leave the app waiting for a new connection, retaining the last completed image. The receiver retries opening an unavailable stream. Each reopened transport starts a fresh protocol session and requires a full image. Serial links with no disconnect indication remain listening until stopped.

The desktop test sender requires Python, pyserial, and Pillow:

python3 -m pip install pyserial Pillow
python3 scripts/dlp_image.py --port /dev/ttyACM0 image.png
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --frames 20 --stats
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --fps 30 --frames 100 --stats
python3 scripts/dlp_image.py --port /dev/ttyACM0 --demo --frames 6 --reconnect-every 2

Start the receiver before running the sender. The script discovers geometry, fits images on a white background, and sends MONO1 frames. --legacy-cells tests fixed-cell framing; compact DLP framing is the default. This is an image test tool; desktop virtual-monitor integration belongs to a host project. Windowed uploads are negotiated by default, with automatic fallback for older receivers. --no-window forces the original stop-and-wait behavior for comparison; it is independent of --legacy-cells. After display suspension invalidates retained content, the sender starts a new session and repaints fully.

For Wi-Fi, create a raw TCP stream from a separate device console:

stream tcp listen tcp0 7777
dlp serve display0 stream=tcp0

Then run the desktop sender, replacing the example address with the device IP:

python3 scripts/dlp_image.py --tcp 192.0.2.10:7777 --demo --fps 30 --frames 100 --stats

TCP needs Pillow but does not need pyserial. Use the raw TCP listener, rather than a Telnet shell port. Closing the TCP sender leaves DLP waiting on the same listener. Connect a new sender without running dlp serve again. To remove the endpoint, stop DLP first if it is running, then run stream tcp remove tcp0.

For a BLE connection between two SolarOS devices, configure a CoC listener on the receiving device and start DLP:

stream ble listen ble0
dlp serve display0 stream=ble0

On the sending device, use ble scan to find the advertised SolarOS ble0 peripheral. Copy its address and address type into the connect command:

stream ble connect ble0 aa:bb:cc:dd:ee:ff public
expansion attach stream-dlp remote stream=ble0
display test remote

The central/peripheral roles belong to the BLE connection; either side can serve a display or send frames. Release the consumer to close the connection. DLP serving waits for a new connection after a disconnect. An attached stream-dlp display reconnects on a subsequent presentation attempt. To remove an endpoint, stop DLP or detach the display, then stream ble remove ble0. See BLE stream commands.

--stats prints actual completion rate, byte counts, and host wall times per input frame: prep generates/loads and converts the image; upload plans and sends the update, including intermediate window checkpoints; present_wait runs from PRESENT through the completion reply. With windows it also includes sending the final buffered upload batch. total includes preparation and any requested reconnect, and excludes the intentional frame-rate sleep. The final summary counts presented frames and unchanged images separately; unchanged images do not increase completed FPS. The default rate limit is 2 FPS; --fps 30 sets a 30 FPS ceiling and includes image preparation in that budget.

Run dlp status from a separate shell for receiver-side last/average/maximum timings in microseconds. render measures MONO1 canvas preparation or changed-area detection and batched INDEX2 conversion; display measures the display-service call, including driver conversion, rotation, tile checks and I/O. Unconverted direct rasters have zero separate render time. All driver work is included under display. total is their sum. These measurements exclude wire upload, startup and resume/overlay redraws; they measure service completion, not panel scanout. Completion rate averages the intervals between the first and last DLP frame, including host pacing. The statistics remain available after stopping, until the next receiver start. Host present_wait includes stream transfer, round trips, scheduling and legacy status polling, so it is not a device-only or SPI-only measurement. Diagnostics never enter the DLP byte stream.

Applications · SolarOS manual

DOOM

DOOM is an external foreground app for ESP32-S3 devices with PSRAM, graphics, and native-module support. It uses Espressif's PrBoom port. Game data is supplied separately as a DOOM or DOOM II IWAD. Music, networking, savegames, persistent configuration, PWAD overlays and DeHackEd patches are unsupported.

Quick reference

doom /sdcard/doom/doom1.wad
session create doom display0 /sdcard/doom/doom1.wad
load /sdcard/solaros_native_doom.app.elf /sdcard/doom/doom1.wad
session close ID

Installation and launch

Copy solaros_native_doom.app.elf to modules/apps/doom.elf in the preferred storage volume. The app requires at least 4 MiB free PSRAM and a 24 KiB worker stack. Large WADs or other resident apps can exhaust available memory.

Supply exactly one IWAD path. Keep its storage mounted and the file unchanged until the app closes. WAD files are read through SolarOS storage services; the app does not require a dedicated flash partition. The game renders at 320x240 with a fixed RGB332 output palette and uses the host's display fitting and monochrome conversion.

Sound effects

Sound effects play through the selected SolarOS audio output, using the global speaker level. The game's sound-volume setting controls the effects mix. Busy or unavailable audio output leaves the game running silently.

The IWAD must contain digital sound-effect lumps (DS*). Espressif's doom1-cut.wad has no sound-effect data; use an uncut shareware or owned full-game IWAD for sound. Music is disabled. Session suspension stops audio; resume reopens the output and continues the retained sample positions.

Controls and sessions

KeyAction
Arrow keysMove forward/backward and turn
CtrlFire
SpaceOpen doors and use switches
ShiftRun
AltStrafe with arrow keys
Comma / periodStrafe left / right
1–7Select weapon
TabToggle map
EscapeOpen menu
EnterSelect menu item

Held movement requires a keyboard source that sends key releases. Text-only sources provide taps. Normal SolarOS session shortcuts suspend the game while retaining its state. Resume clears held keys and continues the game clock from where it paused. Ctrl+] or session close ID closes the app; progress is not saved to disk.

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 app registry, including installed external foreground apps. Associations include images to view, WAV/MP3 to player, MPEG-1 .mpg/.mpeg files to vplay, CSV to sheet, Python and Lua scripts to their runtimes, documents to reader (or writer when Reader is unavailable), and .gb ROMs to the installed Game Boy module. NIB disk images open with the installed Apple II Plus module. 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/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.
  • Space toggles a mark on the current file or directory and advances to the next item.
  • 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.

Close Flash when finished to release its working memory. The programming operation owns the selected UART and optional control pins until it completes.

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
ftp HOST [PORT] [--user USER --password PASSWORD] [--remote PATH] [--local PATH]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage.

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. Starting without arguments opens the disconnected file manager; F2/n opens the connection form. The command-line form remains available as a direct connection shortcut. If login is rejected, FTP reopens the form on the password field with the entered host, port, user, remote path, and masked password preserved. Connection failures are reported as refused, timed out, host unreachable, or network unreachable when the socket supplies that distinction, and as server unavailable otherwise.

Controls:

  • Up/Down selects an item. PgUp/PgDn moves by one visible page, and Home/End selects the first/last item. Tab, Left, and Right switch panes.
  • Space toggles a mark on the current file or directory and advances to the next item, as in Files. Marked items show *; each pane shows its mark count. Copy, move, and delete use the active pane's marked items, or the current item when none are marked. The parent entry cannot be marked.
  • F2/n opens the connection form. Tab or Up/Down selects a field, Enter connects, and Esc cancels the form.
  • Enter opens a directory; Backspace opens its parent. r refreshes the remote and local listings. h toggles hidden files and directories in both panes. Hidden items are shown initially; the title shows hidden:on or hidden:off.
  • F3/V 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/C copies the current or marked files and directories to the other pane. Directory transfers are recursive. Copy and move show the same preparing/transfer progress popup used by Files.
  • F6/M moves the current or marked files and directories to the other pane by completing each item's copy before deleting its source.
  • F7/k/K creates a directory in the active pane.
  • F8/D recursively deletes the current or marked items after confirmation. The help bar emphasizes the same operation mnemonics used by Files and SFTP.
  • 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. Marks survive refreshes in the same directory and pane switching. Opening another directory clears that pane's marks; hiding hidden items clears their marks. Completed items are unmarked. A failure stops the batch and leaves remaining items marked.

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.

Small graphical displays show one selected control at a time, with its position in the eight-control sequence. The waveform remains visible when space allows. Short terminals scroll the controls to keep the selection visible. Long output names are clipped to the available width.

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.

Applications · SolarOS manual

Game Boy

Game Boy is an external foreground app for ESP32-S3 devices with PSRAM, graphics, and native-module support. It runs original Game Boy (DMG) ROMs up to 4 MiB. Game Boy Color-only ROMs and unsupported cartridge controllers are rejected. No ROM images are bundled.

Quick reference

pkg install gameboy
gameboy /sdcard/roms/game.gb
session create gameboy display0 /sdcard/roms/game.gb

Installation and launch

Install from a compatible module catalog with pkg install gameboy, or copy solaros_native_gameboy.app.elf to modules/apps/gameboy.elf in the preferred storage volume. The app appears in the installed app registry and handles .gb files opened from Files. ROM paths can be relative to the launching shell's current directory.

You can also launch an ELF directly:

load /sdcard/gameboy.app.elf /sdcard/roms/game.gb

Video uses a four-shade 160x144 image, scaled to the display. The guest runs at its native frame frequency; presentation is bounded to 25 frames per second. Audio uses the shared synth service at 16 kHz stereo when available. Emulation continues without audio if the firmware omits synth support or no compatible speaker output is available.

Controls

  • Arrows control the D-pad.
  • The physical US-Z key position is A; on German QWERTZ keyboards it is labeled Y. The physical X key is B.
  • Enter is Start; Backspace or Delete is Select.
  • p pauses and r resets.
  • q, Esc, or the app-exit key exits.
  • For board-button input, Enter is A, Space is B, the app-exit button is Start, and Escape is Select. Pressing Start and Select together exits.

Held-key input supports combinations such as diagonals and direction plus A/B. Character-only input uses short button pulses. Analog joystick axes do not become Game Boy keys.

Sessions and saves

Session switching retains guest CPU, RAM, cartridge RAM, pause state, and audio synthesizer state. Display and audio are released while suspended. Resume clears held inputs and rebases guest timing. Closing writes modified cartridge RAM to a .sav file beside the ROM using atomic replacement. Existing saves are loaded on launch; a size mismatch is reported. Reset preserves cartridge RAM.

See the foreground app contract and the module build instructions.

Applications · SolarOS manual

help

Foreground browser for the package-aware SolarOS manual. The foldable tree groups the topics available in the current firmware and shows whether it is using the setup and recovery guide or a verified downloaded revision. Full guides and scripting API references require the downloaded manual; use help update to install it on devices with Wi-Fi, PSRAM, and SD storage. 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. The playfield scales to the active display; ship movement, shields and collision detection use the same game coordinates. Small displays use bitmap text for the score and lives.

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. Narrow screens show fewer columns and keep the selected pin visible.
  • Pins, Buses, and Claims fit their columns to the screen width. Bus state stays ahead of routing details; trailing columns are omitted when space is limited.
  • 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, 1-Wire, PS/2, or CAN bus, depending on the installed drivers.
  • 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.
  • CAN creation selects TX/RX GPIOs, bitrate in kbit/s, listen-only or normal mode, and an optional transceiver-select GPIO with its active level. The default is 500 kbit/s in listen-only mode. CAN controllers appear in Buses; runtime controllers support removal and Autostart, which saves an expansion attach twai-can ... command. A compatible CAN transceiver is required; see manual hardware.can.
  • 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

ltop

Live text task monitor for display and port terminals. Unlike the top command's cumulative snapshot, ltop samples the FreeRTOS runtime counters once per second and reports the interval deltas. One progress bar shows the busy share of each CPU core, derived from that core's idle-task delta. Each task percentage is its share of one core, so the task total can approach 200% on a dual-core target. The title shows the current aggregate CPU load instead of the fixed maximum capacity.

The IRAM and ERAM progress bars show the currently used share of the internal and external heaps. ERAM is shown as unavailable on boards without external RAM.

The task table is sorted by current CPU load and shows task state, priority, interval CPU percentage, and peak stack consumption since the task started. Peak stack consumption is the allocated stack size minus the minimum remaining stack reported by FreeRTOS. Idle tasks are omitted from the table because their counters drive the per-core progress bars. Columns expand across wide terminals and the priority column is hidden on narrow terminals.

Usage:

ltop

Controls:

  • Up/Down or j/k scroll one row.
  • Page Up/Page Down, Home/End, or g/G move through the task list.
  • r resets the sampling baseline.
  • q, Esc, or the app-exit key exits.

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

Macintosh Plus

Mac is an external foreground app for ESP32-S3 boards with PSRAM, graphics and native-module support. It emulates a Macintosh Plus with 4 MiB RAM, a monochrome screen matching the selected display, keyboard, mouse and one disk image. Audio and networking are not supported.

Quick reference

Copy solaros_native_mac.app.elf to modules/apps/mac.elf on the preferred storage volume. The app requires at least 5 MiB free PSRAM after loading its ELF, including a contiguous 4 MiB guest RAM allocation, and a 16 KiB worker stack. Close other large apps if allocation fails.

mac /sdcard/mac/macplus.rom
mac /sdcard/mac/macplus.rom /sdcard/mac/hd.img
mac /sdcard/mac/macplus.rom --floppy /sdcard/mac/system.img
mac /sdcard/mac/macplus.rom --pointer-speed 2
session create mac display0 /sdcard/mac/macplus.rom --floppy /sdcard/mac/system.img
session close ID

Tab completes ROM and disk paths. After the ROM path, type --fl and press Tab to complete --floppy, then use Tab to complete its image path. --pointer-speed and its suggested values also complete with Tab.

Supply your own Macintosh Plus ROM and guest disks. A ROM must start with a valid 128 KiB Plus v1, v2 or v3 image. Trailing dump data is ignored after checksum validation. Without a disk, the guest waits for a boot disk. Plus v3 skips its long power-on RAM test after the host clears guest RAM.

The optional SCSI hard disk uses 512-byte sectors and needs a bootable Apple partition map and disk driver. --floppy instead accepts a raw 400 or 800 KiB floppy and requires a Plus v3 ROM. The app patches its in-memory ROM copy with a replacement Sony driver. Disk Copy containers and nibble images are not supported. Only one image can be attached in this version.

Display and controls

The app queries the selected display and adapts the guest resolution, mapping one guest pixel to one display pixel without scaling or cropping. This requires a Plus v3 ROM for resolutions other than 512×342. Display dimensions must each be 64..1024 pixels, subject to available PSRAM. The ROM file is unchanged. Software with fixed-size windows or dialogs may not fit smaller displays. The app checks for screen changes up to 25 times per second and redraws only changed frames, plus a complete frame after resume. Keyboard letters, punctuation and modifiers go to the guest. Mouse and trackball movement controls the guest mouse; the primary button clicks and drags. Touch acts as a touchpad: drag to move and hold the mouse button. Relative mouse and trackball movement defaults to 5× speed. Set --pointer-speed N to an integer from 1 to 10; 1 gives the original speed. The option can appear before or after the ROM or disk arguments. Touch movement and keyboard mouse controls keep their normal speed.

ControlAction
Keypad 4, 6, 8, 2Move the mouse left, right, up, down
Keypad 5Hold the mouse button
F8Toggle arrows-and-Space mouse mode
Arrows, Space in mouse modeMove the mouse, hold its button
F2Save a guest screen snapshot beside the ROM as .pbm
F10Pause or continue the guest
Ctrl+]Close the app

Character-only input sources move six pixels per arrow event; Space toggles the button because those sources do not send key releases.

mac --self-test runs a striped diagnostic screen without a Macintosh ROM or disk. It checks the emulator path and does not run Macintosh software.

Sessions and storage

SolarOS session shortcuts suspend and resume the guest. CPU, RAM, peripheral state, pause state and disk claims remain resident while suspended. Resume clears held input and redraws the screen.

Guest disk writes are saved and synced immediately. Use a copy of an image to preserve the original. A suspended guest still owns its image. Close it before mounting that image elsewhere or removing storage. PRAM is saved in a 32-byte .pram file beside the ROM when the app suspends or closes. Serial-port settings use offline mode because LocalTalk is not implemented.

See the foreground module contract and module sources and build instructions.

Applications · SolarOS manual

messages

Manage messaging instances, connections, and local message history on the display or a VT100 terminal. Run messages without arguments to open the TUI. Existing messages subcommands remain available for shell and scripting use.

Press Tab to switch between Instances, Connections, and Messages. Use the arrow keys to select a row, Enter to open it, a for actions, and ? for help. f selects an instance or All instances. The selected instance is shared between tabs. Esc returns to the previous view; q closes the TUI.

  • Instances: n adds an instance using a provider form, e edits its settings, and d removes it after confirmation. Select a field and press Enter to edit it in place with a visible insertion cursor; s saves the form. The other fields remain visible. Unchanged secret fields keep their saved values. Submitting an empty secret field clears it. Removal deletes local history, queued sends, settings, and credentials for that instance. A Deleting... popup stays visible while removal runs. Choose SC2 to enter a relay address such as chat.solar-os.eu:1337 (TLS), a full chats:// or chat:// URL, or a bare domain for signed HTTPS discovery. Domain discovery fills in the endpoint, TLS pin, relay signing key, and fallback relays. An admission code remains optional and must be supplied by the operator when required. The other optional fields are the TLS pin and relay signing key (base64). New SC2 instances start disabled. Changing the relay URL resets its TLS and signing-key pins unless you also edit those fields.
  • Connections: c enables the selected instance and x disables it. s starts its background worker; the same action is in the a menu and connection details. Workers that are already running or waiting for memory are left active. A shared worker serves the enabled instances of its provider. Enter shows connection state, worker state, and errors. Enabling an instance and starting its worker are separate actions. Link transports prompt for the worker name and settings, as used after job start, for example radio-link mylink radio0 PROFILE chat=on or espnow-link mylink chat=on. Use the transport name saved in the Link instance.
  • Messages: v cycles through conversations, unread messages, and the pending outbox. Enter opens conversation history or a message's full text. Page Up loads older records and Page Down loads newer records. c opens Chat for the selected conversation; closing Chat returns to this browser. r marks the selected conversation read. d deletes one selected local message after confirmation. A pending send must first be cancelled with x before its history can be deleted. Local deletion does not retract a remote message.

Message browsing uses the persistent Messaging archive. The universal Inbox also contains notifications from other applications; see inbox for those. See Messaging for provider settings, trust, and storage behavior.

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

Set your provider endpoint, model name, and API key. For a Responses endpoint:

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.

Use agent --tts for a spoken conversation, or add --tts before new, resume, or ask. Start speechd with the desired PicoTTS voice first:

job start speechd /voices/en-US
agent --tts
agent --tts ask What is the battery status?

The answer remains written in the terminal. Speech is queued only after a successful final model answer. Connection status, retries, tool calls, tool results, confirmation prompts, and intermediate model text preceding a tool call remain silent. --tts currently selects the local PicoTTS speech service.

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 reports memory, resource owners, and job errors. A job shown as ready has enough declared worker-stack memory at that moment; it may still need more buffers or an available resource when it 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 Quick references from the signed downloaded manual. If a topic directs you to install the manual, run help update before using its APIs.
  • 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. Add --tts immediately after agent to speak final answers through a running local speechd. agent list inspects saved slots, agent resume SLOT restores one, and agent delete SLOT removes 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. The graphical controls and lists adapt to small displays, including 240×64 and 128×64 targets.

On a graphical session, Tab switches between Play and Playlist. The Play tab uses the space above the shared bottom controls 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. </> seeks backward/forward by ten seconds without changing tracks; rewind/forward buttons provide the same actions. Seeking preserves pause state and discards queued audio. The controls show the track name, playback status and time above the volume bar. The middle button stops or starts playback and indicates pause while paused. 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, </> seeks backward/forward ten seconds, 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.

Player shows SEEKING while preparing a new position. MP3 seeking can take longer on slow storage, including with variable-bitrate files. Stop playback to cancel a seek.

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.

On displays narrower than 240 pixels or shorter than 180 pixels, the graphical media controls use an 8-pixel bitmap font and smaller transport buttons. Short displays show volume as a percentage beside the filename instead of a volume bar. Lists scroll to keep the selected entry visible, and edit dialogs fit the display. When the cassette cannot fit, the visualizer uses a scope.

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

rfscan

Graphical receiver-energy monitor for attached radios that report the live_rssi capability. Requires a graphical display and the radio_scanner group. Radio drivers provide live energy readings and validate tuning and bandwidth settings through the common radio API; the app does not select support by radio model.

rfscan
rfscan radio0
radio scan radio0

Select the radio at launch with rfscan <name>; it remains selected for that app session. With no argument the app selects the first attached radio. The radio configuration TUI also opens the scanner with s on a graphical display. From a text-only shell, use session create rfscan display0 radio0 to open it on the device display.

Channel plots live RSSI against time. Time cycles through 10 seconds, 1 minute, 10 minutes and 1 hour. Sampling intervals increase with the time window; short bursts can be missed. The graph shows the latest level and the minimum and peak since starting or clearing. When another service is already receiving, the scanner observes without retuning, consuming packets or taking its radio claim. Transmit and sleep intervals produce no energy samples. An idle radio is claimed and placed in receive mode until scanning stops.

PgUp zooms into the RSSI plot's Y axis and PgDown zooms out. Available spans are 5, 10, 25, 50, 100 and 200 dB; the default is 100 dB. In Channel, the plot centers on the average RSSI of samples inside the displayed time window. In Band, it centers on the average of the current frequency-bin readings. Empty bins and held peaks do not affect the average. Each zoom key centers the axis on that average, then holds the axis steady until the next zoom key. The Avg readout continues updating; values outside the selected span are clipped to the plot edges. Zoom leaves scanning and the alert threshold unchanged and does not change waterfall shading. A threshold outside the visible Y range keeps working but its horizontal marker is hidden.

Channel controls are Start/Stop, Time, Step, Freq and Thresh. Step selects a frequency increment of 25, 62.5, 125 or 250 kHz without interrupting monitoring. With Freq selected, Up/Down increases/decreases the requested frequency by that increment; the frequency is shown to 1 Hz. Changing frequency applies it and resumes acquisition automatically when monitoring was running. Tuning requires an unclaimed radio and preserves the current receiver bandwidth. A busy radio reports its owner. Stop, suspension or exit restores the previous frequency and operating state.

Enter on a setting opens a prefilled input popup. Type to replace the current value, use Left/Right, Home/End, Backspace or Delete to edit, or Ctrl+U to clear the field. Enter applies and Esc cancels. Frequency uses MHz; Step and BW use kHz, including decimal values. A BW of 0 keeps the current receiver filter. Range accepts start and end frequencies in MHz separated by a space; Time accepts 1–3600 seconds. Values that would exceed the supported number of scan bins are rejected or require a larger step. Radio drivers validate frequency and bandwidth when scanning starts. Opening or cancelling a popup leaves the current scan running.

Thresh controls an audible energy alert in both tabs. The default is −90 dBm; Up/Down changes it by 1, 1, 1, 2, 5 or 10 dB for Y spans of 5, 10, 25, 50, 100 or 200 dB, respectively, and cycles through Off at the ends of the −130 to −30 dBm range. Enter accepts an exact dBm value or off. Changing the threshold leaves scanning running. A dashed horizontal line marks the threshold; fresh samples at or above it request a short beep at most once per second, including brief hits between graphical refreshes. Alerts use the selected audio output and global volume/mute, skip busy audio, and are cancelled on Stop, suspension or exit. Audio failures are reported without ending the scan. The center frequency of the strongest sample responsible for the last alert is shown beside the threshold as Threshold: >=-90dBm Triggered: 868.375000MHz. Before the first alert, Triggered shows --. The frequency remains visible while scanning moves on; the next alert updates it.

Band shows received energy against frequency, a dashed peak trace and a waterfall with the newest completed sweep at the top. Darker pixels mean stronger energy. The waterfall uses dithering on 1-bit displays; buttons use solid backgrounds, outlines and inverse selection. Range cycles through ±250 kHz, ±1 MHz, ±2 MHz and a preset band: 433.05–434.79, 863–870 or 902–928 MHz, selected from the current frequency. Step cycles through 25, 62.5, 125 and 250 kHz; wide ranges increase the step when necessary to stay within 129 frequency bins. BW selects the current receiver bandwidth or 62.5, 125 or 250 kHz. Drivers can round the requested bandwidth up to a supported filter; the running scan shows the applied bandwidth. Unsupported settings are rejected by the radio driver. The final frequency shown is the last sampled bin.

Three status lines above the graph show tuning settings, RSSI readings and the alert threshold with the last alert frequency. Errors replace the RSSI readings. The top header shows LIVE with owns RX, observe RX or RX paused for a band sweep; stopped acquisition shows STOPPED. In Channel, the header also shows the time window. The tuning row shows the frequency or band range, with BW: and Step: labels. Sampling runs independently of graphical refreshes. Radios advertising fast_tune use lightweight frequency changes and driver-defined settling times; other live_rssi radios remain supported through normal configuration. Sweep speed depends on the driver, bandwidth, range and frequency step.

Band scans require exclusive ownership of the selected radio. A busy radio shows its owner; stop that owner explicitly before scanning. Normal packet reception on that radio is paused during a sweep. Band scanning has no idle timeout. Stop, app suspension, app exit or a scan error ends the sweep. The scanner restores the previous configuration and operating state and releases its claim; a restoration failure is reported. Changing settings or switching tabs automatically resumes acquisition if it was running; a stopped scan stays stopped. Rejected settings restore the previous selection and resume it when radio ownership permits. Returning to a suspended app leaves it stopped until Start is selected.

ControlAction
Tab, or tap a tabSwitch between Channel and Band
Left/RightSelect a bottom function
PgUp/PgDownZoom the RSSI Y axis in/out, centered on the displayed average
Up/DownNext/previous value of the selected function; Up starts and Down stops when Start/Stop is selected
Enter, or tap a bottom buttonToggle Start/Stop, or open the selected setting's input popup
SpaceStart or stop
r, s, tTime/range, step, threshold
f in Channel, b in BandIncrease frequency by Step, or advance bandwidth
cClear history and peak hold
Ctrl+Alt+Del, q or EscExit

RSSI measures energy inside the receiver filter without decoding its source. Frequency bins are sampled sequentially; receiver bandwidth limits frequency detail. Scan only within the attached module's supported RF band and antenna range. This display does not provide raw I/Q samples.

Applications · SolarOS manual

rtsp

Play a trusted-LAN RTSP stream using UDP RTP/JPEG video and RTP/L16 audio.

rtsp rtsp://192.168.1.238/media
rtsp 192.168.1.238/media
rtsp user:secret@192.168.1.192:8554/youtube
rtsp --audio-only rtsp://192.168.1.113/media
rtsp --stats rtsp://192.168.1.192:8554/youtube
rtsp --audio-only --stats rtsp://192.168.1.192:8554/youtube
rtsp-auth set 192.168.1.50 admin camera-password
rtsp rtsp://192.168.1.50/media

The rtsp:// prefix is optional. The RTSP control connection uses TCP port 554 unless a port is supplied; RTP media uses negotiated UDP ports. Addresses without a path are accepted; include the publisher's path when required.

RTSP playback keeps Wi-Fi awake while connecting, playing, and reconnecting to avoid delayed or dropped live media. Normal power saving resumes after playback stops, unless another active service still needs low-latency Wi-Fi.

Inline credentials use user:password@host; percent-encode spaces and reserved characters in the username or password. rtsp-auth set <host[:port]> <user> <password> saves up to four accounts for URLs without inline credentials. Run rtsp-auth to list accounts, or rtsp-auth clear [host[:port]] to remove one account or all accounts when the host is omitted. The player hides passwords.

Graphical sessions show aspect-fit JPEG video when a video track is selected. Audio-only sessions use the common audio GUI oscilloscope, fed by playback PCM, filling the available area between the header and player controls. Port shells select audio only and print playback status. --audio-only skips video negotiation and video buffers even when the publisher offers both tracks. Audio uses the selected default output and global volume; Up/Down adjust volume (+/- remain aliases). The graphical player uses the common header, volume bar, and Stop/Play button. Enter, Space, or a tap on the button stops playback without closing the app; Play reconnects to the same URL. Stopping releases the network, decoder, audio output, and media buffers once their workers finish. RTSP URLs are single sources, so there are no Previous/Next buttons. F toggles fullscreen, hiding the header and controls while retaining aspect-fit video or expanding the audio-only oscilloscope to the whole screen. D toggles the separate frame diagnostic overlay: received, displayed, network/app drops, and displayed frames per second. Q, Escape, or the normal application-exit key closes playback. Suspending keeps audio playing and pauses video decoding until you return. Closing releases the audio output and network connection.

Temporary connection failures retry up to six times. Unsupported codecs, failed or unsupported authentication, and local memory or audio failures stop immediately. The shell diagnostic identifies the failing operation. Use --stats and log show for frame counts, drops, timings, audio buffering, and retry details. The D overlay works without logging; --stats also enables it initially.

Requirements and limits:

  • IPv4 RTSP 1.0 with unicast UDP media. Client authentication supports Basic or Digest MD5 (qop=auth or no qop); the rtspd publisher remains unauthenticated.
  • One JPEG video track and/or one L16 audio track (8–48 kHz, mono or stereo).
  • JPEG types 0/1 with explicit 8-bit quantization tables (Q=255); restart markers are supported.
  • TCP interleaving, multicast, encrypted RTSP, compressed audio, and redirects are unsupported.
  • Five seconds without media triggers reconnection. Retry exhaustion returns an error to the shell.
  • rtspd allows one receiver at a time; stop another receiver before connecting.
  • Microphone capture cannot be opened while playback is active.

If playback reports no memory, use mem to check internal memory and its largest free block, then stop unused jobs. PSRAM alone does not meet audio and DMA memory requirements.

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

sftp

Two-pane SFTP file manager with the same panes, file operations, progress, and key bindings as ftp. The left pane is local mounted storage and the right pane is a remote SSH server's SFTP subsystem. It uses the shared SSH transport, known-host policy, host aliases, and password or public-key authentication.

Usage:

sftp
sftp [user@]HOST[:PATH] [PORT] [--user USER] [--password PASSWORD] [--remote PATH] [--local PATH]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage. Completion preserves a supplied user@ prefix.

Starting without arguments opens the disconnected file manager. Press F2/n to enter the host, port, user, optional password, and initial remote path. Leave the password empty to use the configured SSH key. When user@ and --user are omitted from the command-line shortcut, SFTP uses the SolarOS identity user. A remote path can be appended to the host, for example sftp nils@remote:/directory; this form connects immediately and opens that directory. --remote PATH overrides an appended path. A command-line password remains in shell history; the connection form masks it. The active form field keeps a visible text cursor. If passwordless key authentication fails, SFTP reopens the form on the password field instead of showing a low-level error.

Controls are identical to FTP: F2/n opens connection setup; F3/V views, F5/C copies, F6/M moves, F7/k/K creates a directory, and F8/D deletes after confirmation. PgUp/PgDn moves by one visible page, Backspace opens the parent directory, and h toggles hidden files and directories in both panes. Space marks or unmarks files and directories and advances the cursor, as in Files. Copy, move, and delete operate on marked items in the active pane, or the current item if none are marked. Transfers are recursive, show progress, retain both pane positions after refresh, and stage downloaded and uploaded files before replacement. q, F10, or the app-exit key closes the SSH connection and exits.

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. Runtime rotation preserves the drawing and its saved dimensions. The canvas fits inside the new viewport, and pointer input follows the fitted image. Gray margins mark space outside the drawable canvas after rotation.

Usage:

sketch
sketch file.png

Sketch requires at least 200×200 pixels. On smaller targets it reports that the display resolution is too low and returns to the shell.

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. A failed save preserves the previous document.
  • 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]

Press Tab after a host prefix to complete aliases from .ssh/hosts on the default storage, including in raw mode.

Controls:

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

Applications · SolarOS manual

Tiny386

Tiny386 is an external foreground app for ESP32-S3 devices with PSRAM, graphics, and native-module support. It emulates a 386 PC with 2 MiB RAM, 256 KiB VGA RAM, PS/2 keyboard, timers, RTC, a floppy drive, and an IDE hard drive. SeaBIOS and the VGA BIOS are embedded in the module. Guest disk images are supplied separately. Networking, mouse, and audio are not supported.

Quick reference

tiny386 /sdcard/tiny386/dos.img
tiny386 /sdcard/tiny386/dos.img --hdd /sdcard/tiny386/harddisk.img
tiny386 --hdd /sdcard/tiny386/bootable-harddisk.img
session create tiny386 display0 /sdcard/tiny386/dos.img
session close ID

Installation and images

Copy solaros_native_tiny386.app.elf to modules/apps/tiny386.elf in the preferred storage volume. The firmware must support the foreground disk-image service methods. The app requires at least 5 MiB free PSRAM and reserves a 16 KiB worker stack; startup can fail if memory is fragmented or other apps retain large allocations.

Use a bootable raw floppy .img of 720 KiB, 1.2 MiB, 1.44 MiB, or 2.88 MiB. An .img extension alone does not distinguish a floppy from a hard-disk image. Use --hdd to attach one raw IDE disk with 512-byte sectors, smaller than 2 GiB. Disk data is accessed directly from storage. Keep an original copy when using a guest that modifies its disks. A bootable hard disk can start without a floppy; with both attached, SeaBIOS tries the floppy first.

Create a formatted 32 MiB hard disk with disk create /sdcard/tiny386/harddisk.img 32. It is ready for files but does not contain a bootable OS. Boot a DOS floppy to install DOS onto the hard drive. For FreeDOS, run these commands from the booted floppy:

FDISK /MBR
SYS C:

FDISK /MBR installs the master boot code while keeping the existing partition and files. SYS C: installs the DOS boot sector and system files. The partition created by disk create is already marked active. Close the emulator, then launch it with only --hdd to boot from the hard disk. See disk images to copy files into it from SolarOS.

Display and controls

The app queries the selected foreground display and allocates a matching frame. Text uses the guest VGA font, character attributes, and cursor. Cell size depends on the number of guest columns and rows: an 80-column screen on a 480-pixel display has six pixels per character. Graphics retain their aspect ratio. Guest RTC starts at 2000-01-01.

Letters, punctuation, Ctrl and Shift combinations, function keys, and navigation keys go to the guest PS/2 keyboard. SolarOS session shortcuts retain their normal function. Ctrl+] or session close ID closes the app.

Sessions and disk saves

Alt+Tab, Alt+Right, Alt+Left, and session fg ID switch sessions. Suspend retains the CPU, guest RAM, VGA state, and exclusive disk claims, pauses guest time, and releases presentation resources. Resume clears held keys and redraws the screen.

Guest writes update and sync the image file before the disk operation returns; storage must be writable. Read, write, sync, or close failures are reported to the launching shell. Close the guest cleanly before removing storage or mounting its image in SolarOS. A suspended guest still owns its disks. Power loss during a write or a guest's incomplete filesystem update can lose data.

See the foreground app contract and module build and BIOS source instructions.

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

vplay

Graphical MPEG-1 media player with optional MP2 audio. The bottom controls show playback status and elapsed time above the volume bar; full screen hides the controls and time. The middle Stop/Play icon indicates pause while paused.

The viewport and controls adapt to small targets, including 240×64 displays. Full screen uses the entire display for video. The minimum target size is 84×48 pixels.

Usage:

vplay [-fit|-actual] <file.mpg>

Supports MPEG-1 program streams (.mpg and .mpeg) containing one MPEG-1 video track and optionally one MPEG-1 Layer II (MP2) audio track. MPEG-2, MPEG-4/H.264, AVI, transport streams, and MP3/AAC audio are not supported. The source limit is 640x480; lower resolutions and frame rates are recommended for smooth playback. Playback speed depends on source complexity, display transfer speed, and output size.

Controls:

  • Space pauses/resumes.
  • Up/Down changes global volume.
  • Enter stops playback or restarts the selected file.
  • </> seeks backward/forward ten seconds, preserving pause state.
  • Left/Right selects the previous/next MPEG file in the same folder, in filename order. At either end, the selection stays unchanged.
  • The shared Previous, Rewind, Stop/Play, Forward, and Next buttons provide the same actions when clicked or tapped.
  • f toggles full screen, hiding the header, volume strip, and transport buttons.
  • 0 selects actual size and 1 selects fit. Actual-size video larger than the viewport is cropped centrally.
  • d toggles frame/timing log diagnostics.
  • Esc or app-exit key exits.

Playback pauses while its session is suspended and returns to the shell at the end of the file. Audio uses the selected SolarOS output and acts as the playback clock when present; a file with audio requires an available playback output. Video dimensions and frame rate must remain constant, and the initial audio/video timestamp difference must be no more than ten seconds.

SEEKING means the player is preparing a new position. Files without usable seek timestamps take longer. Stop or close playback to cancel. Positions beyond the end select the last frame.

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. Direct MJPEG URLs are shown as live video using the same JPEG renderer.

Usage:

web http://host/
web https://host/path
web http://camera-host/camera.mjpeg

MJPEG playback uses a bounded 512 KiB assembly buffer and a single pending decoded frame. If decoding or display presentation falls behind the source, web drops the stale pending frame and keeps the newest one instead of growing latency or memory use. Reload, Back, Forward, and app exit cancel the active HTTP stream. The stream inherits the security properties of its URL; plain HTTP provides no encryption or peer authentication.

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. The GUI uses compact controls, scrolling channels and fitted edit dialogs on small display targets. 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. Playlists, HLS, and AAC are unsupported.

Controls:

  • Tab switches between the Player and Channels tabs in the GUI.
  • The Player tab gives the space above the shared bottom controls to a live PCM oscilloscope or spectrum analyzer. V switches visualizers. The spectrum analyzer shows the frequencies in the playing audio.
  • On the Player tab, Left and Right play the previous or next catalog channel. The catalog wraps as a ring. Enter stops playback or reconnects the channel; Space pauses/resumes, and Up/Down changes global volume in five-percent steps.
  • The bottom controls show the channel name, playback state and elapsed output time above the volume bar, followed by Previous, Stop/Play, and Next buttons. The buttons support clicking or tapping; the middle icon indicates pause while paused. Live-channel pause uses bounded audio buffers; a server may disconnect during a long pause, requiring reconnection.
  • 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.

Playback continues when you switch sessions. Closing WebRadio stops reception and playback and releases the output device. The visualizer follows played audio, so it does not run ahead of sound while the stream buffers.

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.

Applications · SolarOS manual

zoo

Browse the hosted model zoo and install complete model bundles on mounted SD or flash storage. The text interface works on the display and VT100 port shells. It requires the Zoo package, ESP32-S3, PSRAM, and networking.

zoo
zoo refresh
zoo source [HTTPS-CATALOG-URL|reset]
zoo storage [sd|flash]
zoo install ID VERSION

The default source is https://solar-os.eu/zoo/catalog.json. The first launch fetches it; later launches use the cached catalog. r refreshes the cache. Up/Down and Page Up/Page Down select entries, Enter opens details, Alt+S searches, and d downloads the selected version. Arrows and page keys scroll long details. * marks an installed bundle and ! marks an incompatible entry. c or Esc cancels a running operation. q or Esc returns from details or exits the browser; the app-exit key exits from either view.

Source and storage preferences persist. Without an explicit storage preference, Zoo uses mounted SD, then flash. Files are stored under dl/zoo/ on that volume; each installed bundle is in models/ID/VERSION/. Downloads are streamed to storage, checked against archive, manifest, model, and asset hashes, and installed through a staged directory swap. Interrupted downloads preserve the previous installation.

The browser shows task, modality, result adapter, license, validation status, and reported free-memory requirements. Runtime compatibility is not a device inference test, and unreported memory remains unknown. The device accepts catalogs up to 512 KiB and 256 entries; larger catalogs fail without replacing the cache. Installation also needs enough storage for the ZIP and extracted files.

Installation does not load a model. Use the displayed bundle.json path with model bundle PATH or the native, Python, or Lua inference APIs. Loaded models remain resident until explicitly unloaded or the device reboots. See Model bundles for inputs, results, and execution, and Native image pipelines for a Zoo-to-camera workflow and a Python detection example. Catalog task names describe the model; check its result adapter before selecting a script. Raw tensor bundles need model-specific interpretation.

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 shows state, worker-stack bytes, and resource counts. Use job status NAME for errors, resource owners, memory placement, and timings.

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 report which ports, files, streams, and listeners they own. If a resource is busy, stop its current user before starting another job on it.

  • waiting: the start request is retained and retries automatically when enough worker memory is available.
  • failed: starting or running failed. Check job status NAME, fix the cause, and restart. Jobs may need buffers beyond their declared stack size.
  • running: the job started successfully.
  • stopped: there is no active job or pending start 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.

Timing fields show the effective interval/deadline, last and maximum execution time, and missed deadlines. A missed deadline is recorded; it does not forcibly stop a handler.

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 stop at reboot. Run setterm startup to find the selected startup script and add the commands you want to run at boot. For 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 sleep, deep sleep, or shutdown when the configured minimum voltage is reached.

Usage:

job start batmon [interval-sec] [sleep|deepsleep|shutdown]
job stop batmon
job status batmon

Defaults:

SettingValue
Interval60 seconds
Low-voltage actionsleep

The action can also be supplied without an interval, for example job start batmon shutdown. The selected action appears in the startup log and the job's battery resource details.

ActionBehavior
sleepRequest the same light sleep as the sleep command.
deepsleepRequest the same deep sleep as the deepsleep command.
shutdownRun the normal shutdown sequence, including storage cleanup and board power-off when supported.

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.
  • Three consecutive samples at or below min_voltage, with an established discharging trend and no inferred external power, request the selected action.
  • The voltage trend needs at least five minutes of history. This check runs only while batmon is running.
  • Set min_voltage above the board's minimum operating voltage, with margin for voltage drop under load. Changing the action does not change the threshold or bypass the trend and consecutive-sample checks.

Example:

job start batmon 60
job start batmon 30 shutdown

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.

irc-sync

Maintains enabled native IRC instances over verified TLS. Each instance has independent reconnect and channel membership state. Configure profiles with irc, then run job start irc-sync. The job accepts no arguments. Use irc status for each connection and job stop irc-sync to stop all IRC connections. Chat and retained messages remain available. See Native IRC.

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] [--sc2 on|off] [--v1 on|off] [--sc2-id ID]
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

SC2 mini-relay

On builds with SC2 messaging, chatd is also a small SC2 relay on the same port, so a device can be a relay and a client at once. The first line a client sends decides: an SC2 hello goes to the mini-relay, a v1 hello to the v1 server. Legacy v1 stays on by default, as on the self-hosted relay; --v1 off refuses v1 clients (426) and --sc2 off turns the mini-relay off. The token, when set, is also the SC2 admission code for new devices. Rate limits are off, as on the self-hosted relay.

The mini-relay is a fixed-size subset of the relay protocol:

FeatureLimit
Login and auth (signed welcome, pinned relay key)6 connections
Registered devices16 (then 503 server_full)
Prekeys per device (bundle publish and claim)8 one-time prekeys
Offline DM mailbox (store and forward)24 messages, 12 per device, 3200-byte envelopes
Public room pub:generallast 24 posts

The six connections are held for a bounded time, as on the self-hosted relay:

  • One session per device: when a device logs in again, its older session gets 409 replaced and is closed (this also clears a connection left half-open by a network change).
  • Keepalive pings are not activity. A session that sends nothing else for 5 minutes gets 408 idle and is closed; the client reconnects and picks up waiting DMs at its next sync. A login has to finish within 30 seconds.
  • When all six connections are taken, a new one takes the slot of the session that has been idle longest, if that one has been idle for at least a minute (503 evicted); otherwise the newcomer gets 503 server_full.

Private rooms and other public rooms are refused (501/404); use a full relay for those. All stores live in one PSRAM block (about 135 KiB), so the mailbox and history are lost when chatd stops. Without PSRAM the mini-relay stays off and v1 keeps working. The relay key is created once and kept in the SC2 store (mr/relay), so pinned clients keep trusting it across restarts. chatd logs it at start:

chatd: SC2 relay chatd-1a2b3c4d key=B64 (134 KiB PSRAM), v1 too

Pin it on clients with messages add sc2 NAME chat://IP:7777 [TOKEN] key=B64. The device running chatd can use its own relay through chat://127.0.0.1:7777.

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.

controllers

Feedback-controller worker for named PID and hysteresis instances.

The job takes no arguments and owns one 4 KiB worker stack. Configure instances with controller before enabling their outputs:

job start controllers
controller start level
controller status level
controller stop level
job stop controllers

Each instance has its own period, timestamped input, output limits, and safe output. Faults latch until explicit stop/start. Stopping this job stops all enabled instances and attempts to release their outputs. Failed cleanup retains claims for controller stop <name> to retry. See Feedback controllers for input freshness, output adapters, configuration, and Python/Lua APIs.

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.

gestures

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 gestures
job status gestures
job stop gestures

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 incoming native-parameter writes, named scalar stream receive bindings, and outbound stream, event-stream or 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 or mapped stream 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.

Receive bindings acquire scalar writers on the first valid packet. Stop releases those writers without sending another value; each target retains its own timeout or hold policy. Bindings remain configured for the next start.

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.

cam-webd

HTTP access to the fitted camera as one-shot JPEG images or a single-client MJPEG stream.

job start cam-webd [qvga|vga] [fps] [auth=none|required]
job status cam-webd
job stop cam-webd

The defaults are QVGA JPEG at five frames per second with no authentication. The optional frame rate is 1..30; JPEG quality is fixed at 12. Options can be given in any order. The job leases the camera as job:cam-webd until it stops. The shell camera command and other camera users report that owner while the lease is active.

API:

GET /api/camera
GET /camera.jpg
GET /camera.mjpeg

With the default auth=none, the endpoints can be opened directly:

http://device/camera.mjpeg

Use auth=required to require Authorization: Bearer <code> on all three endpoints. Starting in that mode prints a random six-digit access code. For example:

curl -H 'Authorization: Bearer 123456' http://device/camera.jpg -o frame.jpg
curl -H 'Authorization: Bearer 123456' http://device/camera.mjpeg -o stream.mjpeg

The MJPEG endpoint uses the shared HTTP server's asynchronous request path. Only one stream client is admitted. The worker captures and transmits one camera framebuffer at a time and releases it on every success or error path; there is no frame queue or JPEG copy. A slow client therefore reduces the capture rate through socket backpressure instead of consuming more memory. Snapshot requests are rejected while the stream owns the capture path.

The server is plain HTTP. With the default auth=none, anyone who can reach the device can view the camera. Use it only on a trusted Wi-Fi network. The optional access code limits casual access but does not encrypt images and is not intended for exposure to an untrusted network.

rtspd

Publish explicitly selected sources as a standard single-client RTSP session. Video uses RTP/JPEG; audio uses RTP/L16 PCM. Each enabled track has its own RTP/RTCP UDP port pair, SSRC, and sender reports, with a shared RTCP CNAME and session start clock.

job start rtspd [video=<stream>|none] [audio=<stream>|none] [size=qvga|vga] [fps=0..30] [port=<port>]
job start rtspd video=camera0 audio=none size=qvga fps=10
job start rtspd video=none audio=audio0.capture
job start rtspd video=camera0 audio=audio0.capture
job status rtspd
job stop rtspd

Defaults are video=camera0 audio=none size=qvga fps=5 port=554. video=camera is a compatibility alias for camera0. Video selects a typed JPEG frame source from stream list; opening it holds the same exclusive hardware lease as the camera service. At least one source must be enabled. fps= is a maximum video publication rate, not a capture timer; fps=0 removes the cap. size= and fps= are invalid with video=none. JPEG quality is fixed at 12.

The publisher keeps Wi-Fi awake while the job runs. Stopping it releases that request; other active low-latency services can continue to keep Wi-Fi awake.

Use stream list to find available video and audio source IDs. audio0.capture is the board codec's PCM microphone endpoint when available; mic0 is a scalar level sensor, not a PCM source. Audio must be a source or duplex S16LE PCM endpoint with 16-bit samples, 1..8 channels, and a native rate of 8000..192000 Hz. The job advertises the source's native rate and channels; it does not resample. Only selected hardware is leased as job:rtspd. Tab completion after job start rtspd offers unused option keys. After audio= or video=, double Tab lists compatible registered source IDs and none; partial IDs complete normally. Selection does not open or lease the source. Audio-only publishing requires neither a camera nor the camera package. For playback on another SolarOS device, use JPEG video and L16 audio at 8000..48000 Hz with one or two channels. A 16000 Hz mono source is the recommended low-bandwidth audio format; native 16000 Hz stereo capture also works. The publisher's wider native-format support does not imply that every receiver supports those formats, and selecting a source never resamples it. External publishers should use baseline JPEG with both quantization tables; 160x120 at 10 fps is a useful initial ESP32 receiver workload. RTP packets must fit the path MTU; SolarOS publishes packets of at most 1200 bytes. Selecting an absent, busy, sink-only, or incompatible source fails startup; there is no silent fallback to another source.

Reader workers block on camera/audio availability and immediately publish or discard the completed frame/block. They continuously drain idle sources, so connecting does not expose a stale camera image or audio backlog. The video cap drops newly captured images rather than holding them until a timer expires. The publisher leases one PSRAM camera framebuffer at a time and keeps no additional JPEG copy or video queue. OV5640 capture uses two PSRAM framebuffers in latest-frame mode so capture can continue while a frame is being sent; OV2640 capture uses one. There is at most one MTU-sized PCM block. Disabled sources allocate no reader stack. Temporary UDP transmit pressure drops the current frame/block and newly captured data during a short backoff instead of disconnecting or building a queue. Audio sample timestamps advance across dropped blocks. Status reports congestion drops and the last transmit errno separately from fatal send errors. Each enabled source uses an additional 4096-byte internal worker stack, shown by job status rtspd, and is closed by its owning reader during cancellation. Source stack minimum-free values are reported in bytes, including microphone startup and publishing. Packet scratch and RTSP session storage are allocated only when the job starts, using the PSRAM-preferred memory policy; disabled sources allocate no scratch. Audio scratch is one 1188-byte PCM block plus one 1200-byte RTP packet, off the reader's stack. I2S DMA buffers and task stacks remain internal. Runtime buffers are freed after workers and leases have closed, including failed startup; a pending stop retains them safely. An unused/stopped job reserves no session or media buffers, only its small internal control state and lock. job status rtspd reports runtime buffer sizes.

Open the single RTSP client session:

vlc rtsp://device/media
rtsp rtsp://device/media
ffplay -rtsp_transport udp -fflags nobuffer -probesize 32 -analyzeduration 1 -max_delay 100000 rtsp://device/media

FFplay's default probing/playback buffers can add seconds of latency; the example reduces client-side probing and UDP reordering delay. It does not guarantee a particular glass-to-glass latency. Capture timestamps determine video RTP timing; audio timestamps advance by sample frames. Images and audio blocks predating each PLAY are discarded.

The camera lease excludes cam-webd, shell camera capture, and other camera owners only when a camera source is selected. Each packet is at most 1200 bytes. Unsupported JPEG modes are counted and dropped, not sent using private payloads. Transport is UDP; RTSP-over-TCP interleaving is unsupported. VLC requires a build with Live555: if its log reports satip or access_realrtsp failures, check for --disable-live555 and use FFplay or a compatible VLC build.

The stream is unauthenticated and unencrypted; use it only on a trusted LAN or protected network path.

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 native RGB565 frames on color targets and 1-bit U8g2 frames on monochrome targets up to twenty times per second. It 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
GET  /api/displays/<target>/frame.rgb565
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.

Display metadata reports pixel_format as rgb565 or mono1 and supplies frame_rgb565 for color targets. The color endpoint contains native row-major RGB565 pixels, most-significant byte first, with native_width * 2 bytes per row and the metadata's rotation. frame.raw retains the native 1-bit U8g2 layout; frame.pbm remains a monochrome image. Color targets provide both formats. The color endpoint returns 404 for monochrome targets.

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 frame snapshot and raw transmit buffer are held in PSRAM while the job runs. Color targets keep an additional RGB565 snapshot and use a color-sized transmit buffer: together these require four bytes per native pixel plus the 1-bit snapshot. For the monochrome 400x300 Waveshare display, the snapshot and transmit buffer 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.
  • Indexed graphics and full-screen, unscaled RGB565 frames, including Jet scenes, preserve their original colors in the browser. The monochrome endpoints provide dithered snapshots. Partial or scaled RGB565 raster updates are not mirrored.
  • 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.rgb565 for color targets and frame.raw for monochrome targets. The PBM endpoint remains available for 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 7168-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.

meshcore-ble

MeshCore companion-protocol client for a separate BLE radio device. The job connects to one explicit peer, synchronizes its contacts and channels, drains queued messages, and carries Chat messages through the standard MeshCore messaging provider.

Usage:

job start meshcore-ble <address> <public|random|rpa_public|rpa_random> [six-digit-pin]
job status meshcore-ble
job stop meshcore-ble

Example:

job start meshcore-ble 68:ee:8f:69:5f:35 public 123456
contacts
chat

The job requires BLE and PSRAM. The optional PIN requests Secure Connections pairing; inspect job status meshcore-ble and confirm encrypted=yes and bonded=yes. The PIN is retained only while the job runs, but the start command can remain in shell history. The worker reconnects with exponential backoff and coexists with the BLE keyboard; a concurrent keyboard connection attempt can delay a reconnect but does not tear down either established link.

meshcore and meshcore-ble are alternative transports for the same messaging provider and cannot run at the same time. Contacts, direct conversations, Chat, and Inbox remain provider-neutral. Channels and the external device's radio and identity are configured on the companion device; the meshcore shell command continues to configure only SolarOS's local packet-radio implementation. See meshcore.md for transport 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.

screen-keyboard

The screen keyboard supplies touch and keyboard input above the shell, TUIs, and graphical applications on a selected display. Start the background job to arm its activation actions:

job start screen-keyboard mode=full
job start screen-keyboard mode=symbols display=display0
job start screen-keyboard mode=deck
job status screen-keyboard
screen-keyboard show
screen-keyboard hide
screen-keyboard toggle
screen-keyboard mode full
screen-keyboard mode symbols
screen-keyboard mode deck
job stop screen-keyboard

Start the job once, choosing one mode. By default, Ctrl+Space or an inward upward swipe from the bottom edge toggles visibility. Tap a key to enter it, or use the physical arrows to move the selection and Enter to activate it. Escape hides the keyboard. A downward swipe that starts inside its panel also hides it. Touch strokes reserved for activation or keyboard selection do not reach the application underneath; touches outside these regions remain normal application input.

Full mode provides letters, numbers, one-shot Shift/Ctrl/Alt, Caps Lock, a symbol table, and a navigation page with Tab, Escape, arrows, Home/End, Page Up/Down, Delete, and Backspace. Physical Tab cycles the full-mode pages. Symbols mode opens the punctuation palette directly, with = selected first. Deck mode provides two rows containing only punctuation absent from T-Deck's normal and Sym layers: = < > [ ] { } % & plus backslash, vertical bar, caret, tilde, and backtick. It has no control-key row; use the configured key or gesture, or Escape, to hide it. Closing the keyboard clears its modifiers and Caps Lock. Physical typing can continue while it is visible; unmodified arrows, Enter, Tab, and Escape control the keyboard. Desktop modifier shortcuts retain their normal behavior.

Start arguments:

ArgumentMeaning
mode=full|symbols|deckInitial mode; default full.
layout=gridLayout provider.
display=<target>Display and input destination; default the focused display.
trigger=<key-or-chord>Visibility toggle; default ctrl+space. Use none to disable it.
source=<keyboard>Restrict the key trigger to one registered input source.
key-id=<id>Use a source-local physical key ID instead of a translated key. Requires source=; without trigger=, no modifiers are required.
gesture=edge-swipe|noneEdge activation; default edge-swipe.
direction=north|south|east|westInward edge-swipe motion; default north from the bottom edge.
touch-source=<source>Restrict touch interaction to one registered touch source.
auto-hide=on|offHide after emitting a key; default off.
visible=on|offShow immediately on start; default off.

For a keyboard without a convenient activation chord, select a key ID from input test <source> and bind it with source= and key-id=. No long-press activation is used. A display must be ready and at least 80 by 64 logical pixels. The keyboard hides on session/focus changes, display geometry changes, and sleep. Status reports visibility, layout, mode, emitted-key count, and the last error. Configuration is volatile; use the selected startup script to start the job automatically after boot.

pointer-arrows

Consume one named pointer source and hold arrow keys while it moves:

job start pointer-arrows source=trackball0
job status pointer-arrows
job stop pointer-arrows

Set min-vector=1..65535 to require a minimum net vector length in each evaluation window. The default is 2; larger values reduce sensitivity. For example, restart T-Deck navigation with:

job stop pointer-arrows
job start pointer-arrows source=trackball0 min-vector=4

Length is sqrt(dx*dx + dy*dy) in the source's movement units. For trackball0, one interrupt edge contributes one unit on its axis. Movement below the minimum releases a held arrow and is discarded at the end of the window; it does not accumulate across windows. The option also applies to the last partial window when movement stops or a touch is released.

The job exclusively captures that source's pointer events, including contact and button boundaries. They are withheld from foreground pointer consumers and the pointer filter while the job runs. Other pointer sources remain available. Pointer buttons do not produce keyboard keys. Release any held button or touch before starting the job.

Movement is evaluated in fixed 80 ms windows, starting with the first movement event. Each window that meets the minimum selects the dominant net direction and presses that arrow. Continued movement in the same direction keeps the key held, using the normal setterm keyrate repeat settings. A direction or modifier change releases the old key before pressing the new one. Opposite movement cancels within a window; equal diagonals use the most recently moved axis. A zero net vector releases the arrow. Trackball windows retain the modifiers from their last interrupt edge. Other pointer sources use keyboard modifiers when samples are processed.

After 80 ms without a movement event, the held key is released. A short roll that meets the minimum produces a press and release. Touch release ends an absolute-pointer gesture immediately. Stopping the job, source loss, or a capture reset also releases held keys and cancels repeat.

Relative pointers supply deltas directly. Absolute pointers are measured from each contact's initial position; a new contact starts a new baseline. Only one contact is tracked at a time. Job status reports the captured source, minimum vector length, direction evaluations that meet it, capture resets, and evaluation and inactivity intervals.

Stopping releases capture and discards unevaluated movement. If stopped during a held button or contact, pointer delivery resumes after its release. Source closure or loss of readiness stops the job and releases its resources. T-Deck starts this job for trackball0 before the shell and startup script run.

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.

speechd

Offline text-to-speech queue. The optional job loads a PicoTTS voice from storage into PSRAM and accepts asynchronous requests from native applications, Python, and Lua.

job start speechd /voices/en-US
say "Solar O S is ready"
say --pitch 85 --speed 120 "Solar O S is ready"
say --file /documents/announcement.txt
say --file /books/novel.txt
job status speechd
job stop speechd

say --file <path> validates and reads a plain UTF-8 text file in bounded chunks. It keeps the shell in a foreground playback mode, but advances from shell events so the display and progress bar refresh while speech is running. The percentage is submitted-file progress: it advances whenever the next bounded chunk is accepted by speechd. The complete file is one streaming speech request: PicoTTS and the audio player stay open between chunks, while a single bounded handoff slot prevents the file from being buffered in memory. Press Esc or Ctrl+C to cancel the current speech request and stop reading. The foreground command returns only after speechd confirms cancellation, so a new say request cannot race the previous stream's cleanup. Cancellation discards queued text and soft-resets PicoTTS instead of synthesizing the unspoken remainder of the file. There is no file-size limit because only one bounded chunk is held and submitted at a time. Tab completion after --file lists filesystem paths. Ordinary say <text...> remains asynchronous and returns the queued request ID. --pitch 50..200 and --speed 20..500 use PicoTTS's native controls; both default to 100 and apply to ordinary text and file playback.

The voice directory must contain ta.bin and sg.bin. No voice blobs are compiled into firmware.bin. The repository's top-level picotts_voices/ directory contains ready-to-copy voices for en-GB, en-US, de-DE, es-ES, fr-FR, and it-IT. Builds also mirror them below .pio/build/<environment>/picotts_voices/. Copy one or more complete locale directories to SD or flash storage, then select the language when starting the job:

job start speechd /voices/de-DE

Restarting speechd with another directory changes the language at runtime. The directory name is used as the voice label in job status speechd; PicoTTS validates the TA and SG resource contents while starting. Missing, truncated, or incompatible resources make the job fail instead of accepting requests.

The job acquires the selected audio output only while an utterance is active and releases it between requests. Normal requests wait for another audio owner; callers can mark disposable notifications drop_if_busy.

The queue holds eight requests of at most 512 UTF-8 bytes. Request IDs support polling and cancellation through solaros.speech. Stopping the job cancels pending speech and releases the roughly 1.1 MiB PicoTTS engine allocation plus the selected TA and SG buffers.

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.

Start, inspect, and stop

jobs
job start batmon 60
job status batmon
job stop batmon

This starts battery monitoring every 60 seconds. Available jobs depend on the board and firmware. Only one instance of each job name can run; starting it again replaces the previous invocation.

Use job status NAME to see errors and the ports, files, or streams it owns. For example, a logger can write to a file:

job start log file /logs/system.log info
job status log
job stop log

Create the destination directory first. Jobs stop at reboot; add their start commands to the selected startup script to restore them. Run setterm startup to find that script. See Job reference for each job's arguments.

Understand the state

StateWhat to do
runningThe job started successfully. Inspect its status for progress.
waitingSolarOS retained the start request and will retry when enough worker memory is available.
failedA start or runtime error occurred. Check job status NAME, resolve the error, then restart.
stoppedNo job or pending start request is active.

The STACK column reports worker-stack bytes, not total memory use. Use mem when a job waits or reports no memory. Stop unused jobs to free their resources.

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 sleep, deep sleep, or shutdown when the configured minimum voltage is reached.

Usage:

job start batmon [interval-sec] [sleep|deepsleep|shutdown]
job stop batmon
job status batmon

Defaults:

SettingValue
Interval60 seconds
Low-voltage actionsleep

The action can also be supplied without an interval, for example job start batmon shutdown. The selected action appears in the startup log and the job's battery resource details.

ActionBehavior
sleepRequest the same light sleep as the sleep command.
deepsleepRequest the same deep sleep as the deepsleep command.
shutdownRun the normal shutdown sequence, including storage cleanup and board power-off when supported.

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.
  • Three consecutive samples at or below min_voltage, with an established discharging trend and no inferred external power, request the selected action.
  • The voltage trend needs at least five minutes of history. This check runs only while batmon is running.
  • Set min_voltage above the board's minimum operating voltage, with margin for voltage drop under load. Changing the action does not change the threshold or bypass the trend and consecutive-sample checks.

Example:

job start batmon 60
job start batmon 30 shutdown

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

cam-webd

HTTP access to the fitted camera as one-shot JPEG images or a single-client MJPEG stream.

job start cam-webd [qvga|vga] [fps] [auth=none|required]
job status cam-webd
job stop cam-webd

The defaults are QVGA JPEG at five frames per second with no authentication. The optional frame rate is 1..30; JPEG quality is fixed at 12. Options can be given in any order. The job leases the camera as job:cam-webd until it stops. The shell camera command and other camera users report that owner while the lease is active.

API:

GET /api/camera
GET /camera.jpg
GET /camera.mjpeg

With the default auth=none, the endpoints can be opened directly:

http://device/camera.mjpeg

Use auth=required to require Authorization: Bearer <code> on all three endpoints. Starting in that mode prints a random six-digit access code. For example:

curl -H 'Authorization: Bearer 123456' http://device/camera.jpg -o frame.jpg
curl -H 'Authorization: Bearer 123456' http://device/camera.mjpeg -o stream.mjpeg

The MJPEG endpoint uses the shared HTTP server's asynchronous request path. Only one stream client is admitted. The worker captures and transmits one camera framebuffer at a time and releases it on every success or error path; there is no frame queue or JPEG copy. A slow client therefore reduces the capture rate through socket backpressure instead of consuming more memory. Snapshot requests are rejected while the stream owns the capture path.

The server is plain HTTP. With the default auth=none, anyone who can reach the device can view the camera. Use it only on a trusted Wi-Fi network. The optional access code limits casual access but does not encrypt images and is not intended for exposure to an untrusted network.

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] [--sc2 on|off] [--v1 on|off] [--sc2-id ID]
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

SC2 mini-relay

On builds with SC2 messaging, chatd is also a small SC2 relay on the same port, so a device can be a relay and a client at once. The first line a client sends decides: an SC2 hello goes to the mini-relay, a v1 hello to the v1 server. Legacy v1 stays on by default, as on the self-hosted relay; --v1 off refuses v1 clients (426) and --sc2 off turns the mini-relay off. The token, when set, is also the SC2 admission code for new devices. Rate limits are off, as on the self-hosted relay.

The mini-relay is a fixed-size subset of the relay protocol:

FeatureLimit
Login and auth (signed welcome, pinned relay key)6 connections
Registered devices16 (then 503 server_full)
Prekeys per device (bundle publish and claim)8 one-time prekeys
Offline DM mailbox (store and forward)24 messages, 12 per device, 3200-byte envelopes
Public room pub:generallast 24 posts

The six connections are held for a bounded time, as on the self-hosted relay:

  • One session per device: when a device logs in again, its older session gets 409 replaced and is closed (this also clears a connection left half-open by a network change).
  • Keepalive pings are not activity. A session that sends nothing else for 5 minutes gets 408 idle and is closed; the client reconnects and picks up waiting DMs at its next sync. A login has to finish within 30 seconds.
  • When all six connections are taken, a new one takes the slot of the session that has been idle longest, if that one has been idle for at least a minute (503 evicted); otherwise the newcomer gets 503 server_full.

Private rooms and other public rooms are refused (501/404); use a full relay for those. All stores live in one PSRAM block (about 135 KiB), so the mailbox and history are lost when chatd stops. Without PSRAM the mini-relay stays off and v1 keeps working. The relay key is created once and kept in the SC2 store (mr/relay), so pinned clients keep trusting it across restarts. chatd logs it at start:

chatd: SC2 relay chatd-1a2b3c4d key=B64 (134 KiB PSRAM), v1 too

Pin it on clients with messages add sc2 NAME chat://IP:7777 [TOKEN] key=B64. The device running chatd can use its own relay through chat://127.0.0.1:7777.

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

controllers

Feedback-controller worker for named PID and hysteresis instances.

The job takes no arguments and owns one 4 KiB worker stack. Configure instances with controller before enabling their outputs:

job start controllers
controller start level
controller status level
controller stop level
job stop controllers

Each instance has its own period, timestamped input, output limits, and safe output. Faults latch until explicit stop/start. Stopping this job stops all enabled instances and attempts to release their outputs. Failed cleanup retains claims for controller stop <name> to retry. See Feedback controllers for input freshness, output adapters, configuration, and Python/Lua APIs.

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 native RGB565 frames on color targets and 1-bit U8g2 frames on monochrome targets up to twenty times per second. It 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
GET  /api/displays/<target>/frame.rgb565
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.

Display metadata reports pixel_format as rgb565 or mono1 and supplies frame_rgb565 for color targets. The color endpoint contains native row-major RGB565 pixels, most-significant byte first, with native_width * 2 bytes per row and the metadata's rotation. frame.raw retains the native 1-bit U8g2 layout; frame.pbm remains a monochrome image. Color targets provide both formats. The color endpoint returns 404 for monochrome targets.

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 frame snapshot and raw transmit buffer are held in PSRAM while the job runs. Color targets keep an additional RGB565 snapshot and use a color-sized transmit buffer: together these require four bytes per native pixel plus the 1-bit snapshot. For the monochrome 400x300 Waveshare display, the snapshot and transmit buffer 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.
  • Indexed graphics and full-screen, unscaled RGB565 frames, including Jet scenes, preserve their original colors in the browser. The monochrome endpoints provide dithered snapshots. Partial or scaled RGB565 raster updates are not mirrored.
  • 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.rgb565 for color targets and frame.raw for monochrome targets. The PBM endpoint remains available for 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

gestures

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 gestures
job status gestures
job stop gestures

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

irc-sync

Maintains enabled native IRC instances over verified TLS. Each instance has independent reconnect and channel membership state. Configure profiles with irc, then run job start irc-sync. The job accepts no arguments. Use irc status for each connection and job stop irc-sync to stop all IRC connections. Chat and retained messages remain available. See Native IRC.

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 7168-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

meshcore-ble

MeshCore companion-protocol client for a separate BLE radio device. The job connects to one explicit peer, synchronizes its contacts and channels, drains queued messages, and carries Chat messages through the standard MeshCore messaging provider.

Usage:

job start meshcore-ble <address> <public|random|rpa_public|rpa_random> [six-digit-pin]
job status meshcore-ble
job stop meshcore-ble

Example:

job start meshcore-ble 68:ee:8f:69:5f:35 public 123456
contacts
chat

The job requires BLE and PSRAM. The optional PIN requests Secure Connections pairing; inspect job status meshcore-ble and confirm encrypted=yes and bonded=yes. The PIN is retained only while the job runs, but the start command can remain in shell history. The worker reconnects with exponential backoff and coexists with the BLE keyboard; a concurrent keyboard connection attempt can delay a reconnect but does not tear down either established link.

meshcore and meshcore-ble are alternative transports for the same messaging provider and cannot run at the same time. Contacts, direct conversations, Chat, and Inbox remain provider-neutral. Channels and the external device's radio and identity are configured on the companion device; the meshcore shell command continues to configure only SolarOS's local packet-radio implementation. See meshcore.md for transport 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 incoming native-parameter writes, named scalar stream receive bindings, and outbound stream, event-stream or 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 or mapped stream 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.

Receive bindings acquire scalar writers on the first valid packet. Stop releases those writers without sending another value; each target retains its own timeout or hold policy. Bindings remain configured for the next start.

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

pointer-arrows

Consume one named pointer source and hold arrow keys while it moves:

job start pointer-arrows source=trackball0
job status pointer-arrows
job stop pointer-arrows

Set min-vector=1..65535 to require a minimum net vector length in each evaluation window. The default is 2; larger values reduce sensitivity. For example, restart T-Deck navigation with:

job stop pointer-arrows
job start pointer-arrows source=trackball0 min-vector=4

Length is sqrt(dx*dx + dy*dy) in the source's movement units. For trackball0, one interrupt edge contributes one unit on its axis. Movement below the minimum releases a held arrow and is discarded at the end of the window; it does not accumulate across windows. The option also applies to the last partial window when movement stops or a touch is released.

The job exclusively captures that source's pointer events, including contact and button boundaries. They are withheld from foreground pointer consumers and the pointer filter while the job runs. Other pointer sources remain available. Pointer buttons do not produce keyboard keys. Release any held button or touch before starting the job.

Movement is evaluated in fixed 80 ms windows, starting with the first movement event. Each window that meets the minimum selects the dominant net direction and presses that arrow. Continued movement in the same direction keeps the key held, using the normal setterm keyrate repeat settings. A direction or modifier change releases the old key before pressing the new one. Opposite movement cancels within a window; equal diagonals use the most recently moved axis. A zero net vector releases the arrow. Trackball windows retain the modifiers from their last interrupt edge. Other pointer sources use keyboard modifiers when samples are processed.

After 80 ms without a movement event, the held key is released. A short roll that meets the minimum produces a press and release. Touch release ends an absolute-pointer gesture immediately. Stopping the job, source loss, or a capture reset also releases held keys and cancels repeat.

Relative pointers supply deltas directly. Absolute pointers are measured from each contact's initial position; a new contact starts a new baseline. Only one contact is tracked at a time. Job status reports the captured source, minimum vector length, direction evaluations that meet it, capture resets, and evaluation and inactivity intervals.

Stopping releases capture and discards unevaluated movement. If stopped during a held button or contact, pointer delivery resumes after its release. Source closure or loss of readiness stops the job and releases its resources. T-Deck starts this job for trackball0 before the shell and startup script run.

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

rtspd

Publish explicitly selected sources as a standard single-client RTSP session. Video uses RTP/JPEG; audio uses RTP/L16 PCM. Each enabled track has its own RTP/RTCP UDP port pair, SSRC, and sender reports, with a shared RTCP CNAME and session start clock.

job start rtspd [video=<stream>|none] [audio=<stream>|none] [size=qvga|vga] [fps=0..30] [port=<port>]
job start rtspd video=camera0 audio=none size=qvga fps=10
job start rtspd video=none audio=audio0.capture
job start rtspd video=camera0 audio=audio0.capture
job status rtspd
job stop rtspd

Defaults are video=camera0 audio=none size=qvga fps=5 port=554. video=camera is a compatibility alias for camera0. Video selects a typed JPEG frame source from stream list; opening it holds the same exclusive hardware lease as the camera service. At least one source must be enabled. fps= is a maximum video publication rate, not a capture timer; fps=0 removes the cap. size= and fps= are invalid with video=none. JPEG quality is fixed at 12.

The publisher keeps Wi-Fi awake while the job runs. Stopping it releases that request; other active low-latency services can continue to keep Wi-Fi awake.

Use stream list to find available video and audio source IDs. audio0.capture is the board codec's PCM microphone endpoint when available; mic0 is a scalar level sensor, not a PCM source. Audio must be a source or duplex S16LE PCM endpoint with 16-bit samples, 1..8 channels, and a native rate of 8000..192000 Hz. The job advertises the source's native rate and channels; it does not resample. Only selected hardware is leased as job:rtspd. Tab completion after job start rtspd offers unused option keys. After audio= or video=, double Tab lists compatible registered source IDs and none; partial IDs complete normally. Selection does not open or lease the source. Audio-only publishing requires neither a camera nor the camera package. For playback on another SolarOS device, use JPEG video and L16 audio at 8000..48000 Hz with one or two channels. A 16000 Hz mono source is the recommended low-bandwidth audio format; native 16000 Hz stereo capture also works. The publisher's wider native-format support does not imply that every receiver supports those formats, and selecting a source never resamples it. External publishers should use baseline JPEG with both quantization tables; 160x120 at 10 fps is a useful initial ESP32 receiver workload. RTP packets must fit the path MTU; SolarOS publishes packets of at most 1200 bytes. Selecting an absent, busy, sink-only, or incompatible source fails startup; there is no silent fallback to another source.

Reader workers block on camera/audio availability and immediately publish or discard the completed frame/block. They continuously drain idle sources, so connecting does not expose a stale camera image or audio backlog. The video cap drops newly captured images rather than holding them until a timer expires. The publisher leases one PSRAM camera framebuffer at a time and keeps no additional JPEG copy or video queue. OV5640 capture uses two PSRAM framebuffers in latest-frame mode so capture can continue while a frame is being sent; OV2640 capture uses one. There is at most one MTU-sized PCM block. Disabled sources allocate no reader stack. Temporary UDP transmit pressure drops the current frame/block and newly captured data during a short backoff instead of disconnecting or building a queue. Audio sample timestamps advance across dropped blocks. Status reports congestion drops and the last transmit errno separately from fatal send errors. Each enabled source uses an additional 4096-byte internal worker stack, shown by job status rtspd, and is closed by its owning reader during cancellation. Source stack minimum-free values are reported in bytes, including microphone startup and publishing. Packet scratch and RTSP session storage are allocated only when the job starts, using the PSRAM-preferred memory policy; disabled sources allocate no scratch. Audio scratch is one 1188-byte PCM block plus one 1200-byte RTP packet, off the reader's stack. I2S DMA buffers and task stacks remain internal. Runtime buffers are freed after workers and leases have closed, including failed startup; a pending stop retains them safely. An unused/stopped job reserves no session or media buffers, only its small internal control state and lock. job status rtspd reports runtime buffer sizes.

Open the single RTSP client session:

vlc rtsp://device/media
rtsp rtsp://device/media
ffplay -rtsp_transport udp -fflags nobuffer -probesize 32 -analyzeduration 1 -max_delay 100000 rtsp://device/media

FFplay's default probing/playback buffers can add seconds of latency; the example reduces client-side probing and UDP reordering delay. It does not guarantee a particular glass-to-glass latency. Capture timestamps determine video RTP timing; audio timestamps advance by sample frames. Images and audio blocks predating each PLAY are discarded.

The camera lease excludes cam-webd, shell camera capture, and other camera owners only when a camera source is selected. Each packet is at most 1200 bytes. Unsupported JPEG modes are counted and dropped, not sent using private payloads. Transport is UDP; RTSP-over-TCP interleaving is unsupported. VLC requires a build with Live555: if its log reports satip or access_realrtsp failures, check for --disable-live555 and use FFplay or a compatible VLC build.

The stream is unauthenticated and unencrypted; use it only on a trusted LAN or protected network path.

Background jobs · SolarOS manual

screen-keyboard

The screen keyboard supplies touch and keyboard input above the shell, TUIs, and graphical applications on a selected display. Start the background job to arm its activation actions:

job start screen-keyboard mode=full
job start screen-keyboard mode=symbols display=display0
job start screen-keyboard mode=deck
job status screen-keyboard
screen-keyboard show
screen-keyboard hide
screen-keyboard toggle
screen-keyboard mode full
screen-keyboard mode symbols
screen-keyboard mode deck
job stop screen-keyboard

Start the job once, choosing one mode. By default, Ctrl+Space or an inward upward swipe from the bottom edge toggles visibility. Tap a key to enter it, or use the physical arrows to move the selection and Enter to activate it. Escape hides the keyboard. A downward swipe that starts inside its panel also hides it. Touch strokes reserved for activation or keyboard selection do not reach the application underneath; touches outside these regions remain normal application input.

Full mode provides letters, numbers, one-shot Shift/Ctrl/Alt, Caps Lock, a symbol table, and a navigation page with Tab, Escape, arrows, Home/End, Page Up/Down, Delete, and Backspace. Physical Tab cycles the full-mode pages. Symbols mode opens the punctuation palette directly, with = selected first. Deck mode provides two rows containing only punctuation absent from T-Deck's normal and Sym layers: = < > [ ] { } % & plus backslash, vertical bar, caret, tilde, and backtick. It has no control-key row; use the configured key or gesture, or Escape, to hide it. Closing the keyboard clears its modifiers and Caps Lock. Physical typing can continue while it is visible; unmodified arrows, Enter, Tab, and Escape control the keyboard. Desktop modifier shortcuts retain their normal behavior.

Start arguments:

ArgumentMeaning
mode=full|symbols|deckInitial mode; default full.
layout=gridLayout provider.
display=<target>Display and input destination; default the focused display.
trigger=<key-or-chord>Visibility toggle; default ctrl+space. Use none to disable it.
source=<keyboard>Restrict the key trigger to one registered input source.
key-id=<id>Use a source-local physical key ID instead of a translated key. Requires source=; without trigger=, no modifiers are required.
gesture=edge-swipe|noneEdge activation; default edge-swipe.
direction=north|south|east|westInward edge-swipe motion; default north from the bottom edge.
touch-source=<source>Restrict touch interaction to one registered touch source.
auto-hide=on|offHide after emitting a key; default off.
visible=on|offShow immediately on start; default off.

For a keyboard without a convenient activation chord, select a key ID from input test <source> and bind it with source= and key-id=. No long-press activation is used. A display must be ready and at least 80 by 64 logical pixels. The keyboard hides on session/focus changes, display geometry changes, and sleep. Status reports visibility, layout, mode, emitted-key count, and the last error. Configuration is volatile; use the selected startup script to start the job automatically after boot.

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

speechd

Offline text-to-speech queue. The optional job loads a PicoTTS voice from storage into PSRAM and accepts asynchronous requests from native applications, Python, and Lua.

job start speechd /voices/en-US
say "Solar O S is ready"
say --pitch 85 --speed 120 "Solar O S is ready"
say --file /documents/announcement.txt
say --file /books/novel.txt
job status speechd
job stop speechd

say --file <path> validates and reads a plain UTF-8 text file in bounded chunks. It keeps the shell in a foreground playback mode, but advances from shell events so the display and progress bar refresh while speech is running. The percentage is submitted-file progress: it advances whenever the next bounded chunk is accepted by speechd. The complete file is one streaming speech request: PicoTTS and the audio player stay open between chunks, while a single bounded handoff slot prevents the file from being buffered in memory. Press Esc or Ctrl+C to cancel the current speech request and stop reading. The foreground command returns only after speechd confirms cancellation, so a new say request cannot race the previous stream's cleanup. Cancellation discards queued text and soft-resets PicoTTS instead of synthesizing the unspoken remainder of the file. There is no file-size limit because only one bounded chunk is held and submitted at a time. Tab completion after --file lists filesystem paths. Ordinary say <text...> remains asynchronous and returns the queued request ID. --pitch 50..200 and --speed 20..500 use PicoTTS's native controls; both default to 100 and apply to ordinary text and file playback.

The voice directory must contain ta.bin and sg.bin. No voice blobs are compiled into firmware.bin. The repository's top-level picotts_voices/ directory contains ready-to-copy voices for en-GB, en-US, de-DE, es-ES, fr-FR, and it-IT. Builds also mirror them below .pio/build/<environment>/picotts_voices/. Copy one or more complete locale directories to SD or flash storage, then select the language when starting the job:

job start speechd /voices/de-DE

Restarting speechd with another directory changes the language at runtime. The directory name is used as the voice label in job status speechd; PicoTTS validates the TA and SG resource contents while starting. Missing, truncated, or incompatible resources make the job fail instead of accepting requests.

The job acquires the selected audio output only while an utterance is active and releases it between requests. Normal requests wait for another audio owner; callers can mark disposable notifications drop_if_busy.

The queue holds eight requests of at most 512 UTF-8 bytes. Request IDs support polling and cancellation through solaros.speech. Stopping the job cancels pending speech and releases the roughly 1.1 MiB PicoTTS engine allocation plus the selected TA and SG buffers.

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 for the interactive view:

  • Status shows connections, addresses, the default route, VPN routes, and downstream links. It also shows whether NAT is active for clients.
  • Settings opens Wi-Fi or modem controls, sets uplink priorities, and toggles Wi-Fi AP routing. Higher priority wins; saved priorities survive reboot.

Use Tab to switch views, arrows to select, Left/Right to change priority, and Enter to open a transport or toggle routing. SLIP and PPP jobs manage their own downstream or peer connections; Network shows their status.

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())

Changing Wi-Fi can interrupt agent, SSH, chat, or HTTP connections.

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 explicitly bound scalar stream sinks. It sends 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 or mapped updates 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.

Incoming stream bindings

Map an exact OSC address and a zero-based argument index to a writable float32 scalar stream:

osc bind base-linear receive diff_drive.base.cmd_linear /base/cmd_vel arg=0
osc bind base-angular receive diff_drive.base.cmd_angular /base/cmd_vel arg=1
job start osc listen=9000 peer=192.168.1.50

The example maps a message with type tags ,ff and arguments 0.02, 0.0 to the drive's linear and angular command streams. Start the robot to make its streams available. The same binding syntax works for standalone controller setpoints, servo commands and other scalar sinks. arg= defaults to zero and accepts 0..7. Messages accept up to eight float32, int32 or Boolean arguments. Values retain the target stream's native unit, without normalization or scaling.

A desktop Python sender can send both values in one UDP packet:

import socket
import struct
import time

def osc_string(text):
    data = text.encode("ascii") + b"\0"
    return data + b"\0" * (-len(data) % 4)

packet = (osc_string("/base/cmd_vel") + osc_string(",ff")
          + struct.pack(">ff", 0.02, 0.0))
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sender:
    for _ in range(40):
        sender.sendto(packet, ("192.168.1.40", 9000))
        time.sleep(0.05)

This publishes at about 20 Hz for two seconds. Each received packet updates the mapped streams once. The receiver never repeats a cached command between packets, so stopping the sender allows the target's command watchdog to expire. Commands use local receipt/write time; remote-clock synchronization and out-of-order packet detection are not provided.

All mapped arguments and target types, directions and declared ranges are checked before any stream in that message is written. Writers are acquired together on the first valid message and held until job stop osc, unbind or clear. Configuration alone does not claim outputs. A missing stream is retried on the next packet. A target already owned by another writer is rejected. Only one receive binding may target a given stream.

Writes within a message or bundle are sequential, not an atomic transaction. A provider failure can leave a partial update. Malformed packets are rejected before any update; immediate bundles share the eight-update limit. Extra numeric arguments are allowed, but every mapped argument must be present. The /solaros/parameter/ namespace remains reserved for parameter writes.

Stopping OSC releases writers without sending an extra value; the target's hold/stop/watchdog policy applies. Stop OSC before stopping a robot whose streams it holds. Failed closes retain the binding and handle for retry with osc unbind or osc clear.

Sending from the shell

Send one OSC message directly, without bindings or a running OSC job:

osc send 192.168.1.238:9000 /base/cmd_vel 0.02 0

The destination is an IPv4 address or hostname followed by a UDP port from 1 to 65535. One to eight finite numeric values are encoded as float32 arguments in a single message, in the order supplied. The example sends type tags ,ff; the receive bindings above map its values to linear and angular velocity.

Use the same repeat options as stream write when the receiver needs timely updates:

osc send 192.168.1.238:9000 /base/cmd_vel 0.02 0 --rate 20 --duration 2

--rate accepts integer rates from 1 to 100 Hz. Without --duration, publishing continues until Ctrl+C, Esc, app exit, or disconnection. --duration accepts 0.001..86400 seconds and requires --rate. Missed periods are skipped rather than followed by a catch-up burst. The socket closes on completion, cancellation or failure. No extra zero command is sent on exit; the receiver's watchdog or hold policy applies. Sending does not change the OSC job or its bindings. Tab completes send and the unused repeat options after the values.

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, bind_control and bind_receive 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.

bind_receive(name, stream, address[, argument]) creates an incoming binding; the argument index is zero-based in both runtimes. Receive records include direction="receive", source (the local target stream), argument, target_available, target_claimed, received, write_errors, last_receive_ms and last_error. clear() reports cleanup errors rather than discarding a writer that could not be closed.

encode_float(address, value) and encode_int(address, value) return binary messages for managed UDP APIs. dispatch(packet) applies the same bounded parameter/stream 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 shows outgoing source state and send counters, or incoming argument mappings, target claims, receive/write-error counters, last receive 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 send <host:port> <address> <value> [value ...] [--rate 1..100] [--duration seconds]
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 bind <name> receive <scalar-sink> <address> [arg=0..7]
osc unbind <name>
osc clear

Python/Lua: solaros.osc.bindings, bind_stream, bind_event, bind_control, bind_receive, 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

Copy only the public key to the remote host's authorized_keys. Keep the private key on the device. The agent's storage tools cannot access .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. Share that line with the remote host; keep the private key on the device.

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

BLE, PS/2, CardKB, integrated keyboards, board buttons, gpio-keys, and ADC D-pads feed the same input service. Use setterm keyboard us or de to choose a text layout, and setterm keyrate to set key repeat. Analog joysticks report axes; they do not generate arrow keys.

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.

On Pi0cket CL-32, hold Shift for shifted keys and Fn for alternate symbols. Both modifiers stop applying when released. File is left Alt, Menu is left Ctrl, OK is Enter, Cancel is Escape, and holding Fn with keypad 1 through 0 produces F1 through F10. Hold Fn and press Yes (OK) or No (Cancel) to switch to the previous or next retained session on the focused display, once per key press.

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. input test shows counters and the latest event while the shell is active.

The input service recognizes single-finger tap, double-tap, hold, swipe, and edge-swipe gestures from any registered absolute touch source. Producers submit ordinary pointer samples; the common input pump handles gesture timing. gesture status lists touch sources and their supported gestures. Recognition preserves ordinary pointer events and pauses while an exclusive pointer filter or capture owns the source, including handwriting input.

A tap lasts at most 250 ms with no more than 12 pixels of movement on either axis. A single tap waits 300 ms after release so a second tap can form one double-tap event instead of two taps. The second press must start within that interval and within 24 pixels on each axis of the first release. A stationary contact produces one hold after 600 ms; release does not produce another gesture. A swipe completes within 1000 ms, moves at least 40 pixels, and has a dominant axis at least 1.5 times the other axis. Its direction is west, east, north, or south relative to the target display's orientation when the contact starts. Without a target display, calibrated source directions apply. IMU directions remain board-relative. Other touch gestures use direction none.

An edge-swipe starts at a screen edge and moves inward using the same distance, duration, and dominant-axis thresholds as a swipe. The edge band is 5 percent of the shorter native display side, rounded up and limited to 8 through 24 pixels. Its direction describes motion: north is an upward swipe from the bottom edge, south is downward from the top, east is rightward from the left, and west is leftward from the right, relative to display orientation. Display bounds and orientation are fixed when contact starts. Pointer calibration bounds take precedence over display bounds. Without either set of bounds, recognition produces ordinary swipes only. An edge swipe produces one edge-swipe event instead of an additional ordinary swipe, so those bindings can be configured independently. Raw pointer events remain available; a gesture binding does not reserve or consume the touch stroke.

Touch gesture value is 1 for tap, 2 for double-tap, 600 milliseconds for hold, or the dominant-axis distance in pixels for either swipe kind, capped at 32767. raw is zero; double-tap sets the double flag and edge-swipe sets the edge flag. Pinch and rotation are not supported.

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 display 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. Commands run one at a time. The cooldown suppresses repeated reports and defaults to 250 ms. The command after -- accepts separate arguments or one quoted command line.

An LSM6DS3TR-C attached with irq=<pin> registers a gesture source alongside its IMU measurements. It reports tap, double-tap, tilt, wake, and free-fall, with direction none. Its hardware 4D detector also reports tilt with directions north, east, south, and west and values 0, 90, 180, and 270 degrees. The optional rotation=0..3 binding corrects sensor mounting in quarter-turns; these directions stay fixed when the display rotates. Sticky uses rotation=1. Hold the screen upright and turn it; flat poses do not select an orientation. The detector uses a filtered 60-degree threshold and does not repeat an unchanged orientation. Orientation state is checked every 50 ms independently of the interrupt pin. Single taps wait 650 ms so a double tap produces one double-tap event. Values are 1, or 2 for double-tap. The raw word contains WAKE_UP_SRC in bits 0..7, TAP_SRC in bits 8..15, and FUNC_SRC1 in bits 16..23, and D6D_SRC in bits 24..31. wake is a motion event; power behavior requires an explicit binding. No gesture commands are assigned automatically.

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 gestures
gesture bindings
job stop gestures
gesture unbind 2

To rotate Sticky's display when held in each of its four orientations:

gesture bind source=imu0 gesture=tilt direction=north -- display orientation display0 0
gesture bind source=imu0 gesture=tilt direction=east -- display orientation display0 90
gesture bind source=imu0 gesture=tilt direction=south -- display orientation display0 180
gesture bind source=imu0 gesture=tilt direction=west -- display orientation display0 270
job start gestures

To use horizontal swipes for previous/next foreground sessions and vertical swipes for page navigation in the focused app:

gesture bind source=* gesture=swipe direction=west -- input emit ALT+LEFT
gesture bind source=* gesture=swipe direction=east -- input emit ALT+RIGHT
gesture bind source=* gesture=swipe direction=north -- input emit PAGE_UP
gesture bind source=* gesture=swipe direction=south -- input emit PAGE_DOWN
job start gestures

An upward swipe from the bottom edge can have its own action, for example:

gesture bind source=* gesture=edge-swipe direction=north -- input emit PAGE_UP

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 gestures job stops. Put the required gesture bind commands followed by job start gestures 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 display orientation display0 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.

The screen-keyboard background job provides a full virtual keyboard or a symbol palette above display applications. Start it with job start screen-keyboard mode=full or mode=symbols, then toggle it with Ctrl+Space or an upward swipe from the bottom edge. See screen-keyboard for controls and bindings.

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. Disabling BLE also frees internal memory for other work on the next boot. 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. ble pair performs that forget operation before it scans for a replacement, so switching keyboards does not retain the previous keyboard bond. If a keyboard still has an orphaned bond from an older SolarOS release, explicit pairing replaces that conflicting bond. On boards with a system KEY, setterm powerkey long ble-pair assigns this operation to a hold. It is the default on targets without a board-owned wired keyboard; targets with one default to suspend. Pairing has no user cancellation path. The short and long actions are independently configurable with setterm powerkey short <action> and setterm powerkey long <action>. A press of any duration resumes suspend, with the wake press consumed until release. See the KEY settings for actions and board defaults.

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

Classic CAN

The twai-can expansion driver exposes one ESP32 or ESP32-S3 classic CAN controller. Connect TX and RX to a compatible external CAN transceiver. The transceiver provides the CANH/CANL electrical interface; ESP32 pins do not connect directly to those bus wires. Use the same bitrate on all nodes and terminate the bus at its two ends.

expansion attach twai-can can0 tx=16 rx=17 bitrate=500 listen=0
can list
can status can0
can send can0 0x123 01020304
can recv can0 100
expansion detach can0

Choose available pins on your board. Bitrates are 25, 50, 100, 125, 250, 500, 800, or 1000 kbit/s. Default listen=1 receives without acknowledging or transmitting. Set listen=0 for normal bus participation. Optional select=<gpio-or-controller:line> controls a transceiver enable or signal multiplexer; active=0|1 selects its enabled level (default 1). Detaching releases selection and all resource claims.

In the io app, press n, select can, and set the controller name, TX/RX GPIOs, bitrate, and mode. An optional Select GPIO and Active level control a transceiver enable pin. CAN controllers appear in the Buses view, where runtime controllers have Autostart and Remove CAN bus actions. The CAN option is available when the twai-can driver is installed and supported by the board. Use expansion attach for selection through an external GPIO controller.

Frames carry up to eight data bytes. IDs default to standard 11-bit form; add ext for a 29-bit ID. Hex data contains two digits per byte, without spaces; - sends a zero-length frame. Add rtr for a remote request; its DLC is the length of the supplied hex argument, and its data bytes are ignored. The command reports when a frame is queued. Check controller status for transmission failures; queue admission is not a delivery acknowledgment. CAN FD and application protocols such as CANopen are not provided.

can recv waits up to 1000 ms, with zero as the default. The C service API also bounds send and receive waits to 1000 ms. Operations and detach are serialized. A receive timeout returns without producing a frame.

After bus-off, can recover can0 starts the controller's bus-free recovery. Poll can status can0 until its state becomes stopped, then run can start can0. Detach is refused while recovery is in progress.

Quick reference

can [list] lists the attached controller. can status <name> shows its state, queue depths, error counters, failed transmissions, and missed receives. can send <name> <id> <hex-or-> [ext] [rtr] queues a frame. can recv <name> [timeout-ms] reads one frame. can recover <name> begins bus-off recovery; can start <name> restarts a stopped controller.

Hardware and expansion · SolarOS manual

Continuous controls

A control maps a measurement or script value to an app setting or MIDI controller. For example, a potentiometer can change Synth's filter cutoff. You can calibrate its range, smooth noisy readings, or reverse its direction.

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

Parameters are named settings published by running native apps, with defined ranges and units. List the currently available settings:

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.

You can configure up to 16 MIDI CC streams. A new stream reports waiting until its first matching message. It returns to waiting when 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 the startup script selected by setterm startup, together with the related control and job commands.

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 last until reboot. Run setterm startup to find the script in which to save their creation, binding, and job-start commands.

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

Encoder counts

The encoder service exposes named counters through the shell, Python, Lua, and C. The pcnt-encoder expansion driver uses the ESP32 pulse-counter peripheral to count A/B signals in hardware. Multiple encoders can run independently, subject to available GPIOs and PCNT units.

Select the encoder package group for the common service and pcnt_encoder for the hardware driver. Selecting the driver also includes the service. Both are included in the full flavor on supported boards. The provider supports ESP32 and ESP32-S3; it requires no Arduino or external encoder library.

Quick reference

Use available native GPIOs for A and B. These example pins must be checked against the board's pin table and current resource claims:

expansion attach pcnt-encoder left a=1 b=2 mode=4
expansion attach pcnt-encoder right a=3 b=4 mode=2 invert=1
encoder list
encoder status left
encoder read left
encoder reset left
expansion detach left
expansion detach right

Counting modes

mode is required at attachment. There is no implicit counting mode.

ArgumentCounting behavior
mode=1x1: count rising edges of A; B determines direction.
mode=2x2, half quadrature: count both edges of A; B determines direction.
mode=4x4, full quadrature: count both edges of A and B.

All modes require both A and B. For a complete cycle starting at A=B=0, the sequence A rises, B rises, A falls, B falls produces a positive count. The reverse sequence produces a negative count. invert=1 reverses the sign. One signal cycle produces 1, 2, or 4 counts according to the selected mode; counts are not automatically converted into detents, revolutions, or distance. Changing mode requires detaching and attaching again.

The current PCNT provider returns signed 32-bit accumulated counts. ESP-IDF compensates for the 16-bit hardware counter rolling over at its configured limits. Keep the total between -2147483648 and 2147483647; reset before exceeding that range. Reading does not clear the count. Attachment starts from zero; reset stops, clears, and restarts the counter, so edges during that brief interval are not retained.

Wiring and options

Connect A/B to distinct, available native GPIOs and share ground. Input levels must be compatible with the ESP32. GPIO-expander lines cannot serve as PCNT inputs. Both GPIOs are exclusively claimed for the attachment.

Optional argumentMeaning and default
invert=0|1Reverse counting direction when 1; default 0.
pull=0|1|2Internal pulls: 0 none, 1 up, 2 down; default 0. Pin support depends on the chip.
filter_ns=<integer>Reject pulses shorter than this duration in nanoseconds; default 1000. Zero disables the filter. Values beyond hardware support are rejected.

Choose a filter shorter than the valid signal pulses at the highest expected speed. The hardware glitch filter does not replace suitable electrical debouncing for mechanical contacts. The existing rotary-encoder driver provides debounced knob navigation events instead of this raw count API.

Each attachment uses one PCNT unit: x1/x2 use one channel, x4 uses two channels within that unit. Resource limits come from the chip; exhausting the pool returns an error. Detach stops counting, releases the PCNT unit, and resets the pins before releasing their claims. Failed detach retains the endpoint and claims for retry; if teardown partially completed, reads can return a not-ready error. Counting across explicit sleep is not guaranteed.

Python and Lua

Both runtimes expose solaros.encoder when the common service is compiled:

CallResult
list()Metadata records containing name, driver, integer mode, and can_reset.
status(name)The same fields plus signed integer count.
read(name)The signed count.
reset(name)Set count to zero when supported; no return value.

Names are always required. Service errors raise runtime errors. The constants MODE_X1, MODE_X2, and MODE_X4 equal 1, 2, and 4. All providers use this API; a provider without reset support reports can_reset=false and rejects reset requests.

Python:

import solaros

solaros.expansion.attach("pcnt-encoder", "left",
                        {"a": 1, "b": 2, "mode": solaros.encoder.MODE_X4})
print(solaros.encoder.status("left"))
print(solaros.encoder.read("left"))
solaros.encoder.reset("left")
solaros.expansion.detach("left")

Lua:

solaros.expansion.attach("pcnt-encoder", "left", {
    a = 1, b = 2, mode = solaros.encoder.MODE_X4,
    invert = 0, pull = 0, filter_ns = 1000,
})
print(solaros.encoder.read("left"))
solaros.encoder.reset("left")
solaros.expansion.detach("left")

An encoder measures motion independently of the motor service. Attaching one does not enable automatic speed or position control.

Calibrated feedback

Attachment publishes enc.<name>, a timestamped scalar source in rad/s, and parameters enc.<name>.ppr, .gear_ratio, .polarity, .encoder_on_output, .velocity_tau and .position_zero. Use encoder configure <name> <key> <value> or the parameter service to configure them while unclaimed. PPR defaults to zero (uncalibrated), gear ratio to 1, polarity to +1, velocity filter time constant to 0.1 s, and output-shaft placement to false.

A positive PPR is required for velocity control. Counts per output revolution are derived from PPR, the actual driver decoding mode, and gear ratio for a motor-shaft encoder. The same service accumulates signed count wrap into relative position and filters velocity using acquisition intervals. Position starts at position_zero when the stream opens; it is not an absolute reference.

The source permits one open handle, retains its acquisition history while open, and prevents calibration changes, reset and detach until close. Raw reads and encoder status remain available. With positive PPR, status reports fresh calibrated feedback even when no controller or stream reader is running. An idle query measures velocity over a 20 ms window; its first query is valid without an earlier status call. Position accumulates from the first observation after configuration/reset, or from the current stream reference. Raw read remains an immediate count acquisition. Without calibration, feedback remains invalid.

When a stream is active, status previews the current count using the owner's estimator history without changing its filter, position reference or acquisition timestamp. The returned status has its own fresh acquisition time.

Use stream echo enc.<name> [--once] [--rate 1..100] to inspect the velocity source. An idle echo opens the calibrated source and maintains its filter history until it stops. Alongside a controller, echo observes the controller's existing timestamped samples without taking its claim or advancing its estimator. See Feedback controllers for a standalone encoded-motor loop and tuning example.

C providers

Consumers include solar_os_encoder.h and use solar_os_encoder_get(), solar_os_encoder_status(), solar_os_encoder_read(), and solar_os_encoder_reset(). solar_os_encoder_configure() configures the service calibration. Providers register a name, driver, counting mode, context, and read callback. Reset and shutdown callbacks are optional.

Native reference claims prevent reset and detach while allowing shared reads. Claimed sampling returns a fresh count, the x1/x2/x4 decoding factor and a monotonic acquisition timestamp. Release the claim after dependent control has stopped. Tokens are invalid after release. See Robot axes. Callbacks are serialized per endpoint outside the registry lock. In-flight operations prevent unregister; failed shutdown keeps the registration for retry. The provider owns its context until unregister succeeds.

The configure(name, key, value) function is also available in Python and Lua.

Hardware and expansion · SolarOS manual

Expansion drivers and attached devices

Expansion drivers let you add displays, keyboards, sensors, audio devices, and other hardware while SolarOS is running. A driver can serve both built-in and added hardware. Its availability depends on the board and firmware.

Built-in devices attach at boot and appear in expansion devices with board origin. Most are fixed; a board manifest can explicitly make an individual device detachable. Use expansion detach <name> to release it and expansion attach <name> to restore its board bindings. Detachment lasts until restoration or reboot and does not imply that the hardware is powered off. Runtime attachments use named buses and available pins; detach them when finished to release those resources.

Digital control lines, such as a device's power switch, may use an ESP32 GPIO or a GPIO-expander line such as gpiox0:12. Bus signals, PWM, interrupts, and other peripheral signals still require suitable native ESP32 pins.

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 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. Press Enter on a field to choose or edit its value. While editing the device name, Enter accepts the name and returns to the attachment menu. Display panel choices list their native resolutions and module names. Optional panel fields include a default choice. Rotation has its own selection list with degree labels and only the orientations supported by the driver. Leave its default choice selected to use the driver's default rotation. expansion status retains the textual capabilities, buses, devices, and claims report for scripts and terminal inspection.

expansion drivers lists the included drivers by category. expansion devices shows attached instances, their wiring, readiness, and whether they start at boot.

On ESP32-S3 boards with graphics, the full flavor includes the concrete ssd1677 and ssd2677 (Sticky V2) drivers. Custom flavors can enable their respective groups. Drivers omitted by the flavor or pruned for the target's capabilities do not appear in the list.

The cl32-display, sticky-display, and x4pro adapters are selected by their board manifests and detect the built-in display controller at startup. They are excluded from the manual driver list and cannot be attached manually.

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

Run expansion attach <driver> to list that driver's required and optional bindings. After entering a driver and device name, Tab completion offers its bindings and values, including display panel and rotation options.

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 file records runtime buses and driver attachments, plus the base board identity. It omits secrets, service state, and startup commands. Attachments using the manual resource profile cannot be exported.

Copy it to a SolarOS source tree and run:

python3 scripts/board_config.py --expansion-manifest my-hardware.toml

The configurator asks for a new board identity and checks the combined wiring. After you build and flash that target, the imported devices attach at boot. Remove matching temporary startup commands yourself. See Promoting a runtime configuration for the manifest format and build steps.

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..4> 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. Panel 4 selects Elecrow's 792x272 cascaded dual-SSD1683 profile and defaults to rotation 0.

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.

DS3231 real-time clock

Use a named I2C bus running at 100 or 400 kHz. Connect SDA, SCL, ground, and 3.3 V power. For alarm interrupts, also connect INT/SQW to an available ESP32 GPIO with a pull-up to 3.3 V, and add irq=<pin> to the attachment command.

expansion attach ds3231 rtc0 i2c=i2c0 addr=0x68
rtc status
date
ntp
rtc alarm set 07:30:15
rtc pending
rtc ack alarm
rtc alarm clear
expansion detach rtc0

Only one RTC/time provider can be attached. A board with a fixed onboard RTC already occupies that slot. The ds3231 group enables this driver; the full flavor includes it on boards with I2C.

The calendar uses 2000–2099 with the century bit cleared and Sunday numbered as zero in SolarOS. Oscillator-stop status leaves the clock unreliable until time is set, for example with ntp on a connected device. Existing 12-hour clocks are read correctly; setting time writes 24-hour format.

Alarm 1 supports seconds, seconds and minutes, time of day, or time of day with either a date of the month or weekday. Other match combinations are unsupported. Alarm 2 interrupts are disabled, and rtc timer is unsupported. The existing Python and Lua time/RTC APIs use this provider too. The alarm interrupt is available to the existing sleep policy when the selected GPIO supports waking the ESP32.

Other I2C sensors

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. Use rtc to inspect the attached clock's alarm and timer support. Use temperature list and humidity list to find available sensor providers.

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.

After replacing or reinserting an SD card, run disk mount to initialize and mount it again. For a board-defined SDSPI or SDMMC device, repeating expansion attach storage0 also recovers the card without releasing its bus bindings. These commands check the cached card and discard stale SD mounts before probing again. Unmount disk images backed by the card first.

GT911 attachments can additionally bind reset=gpioN, power=gpioN, and active=0|1 for reset/address selection and touch-rail control. The default power active level is high. Optional home_key=<numeric-key> maps the capacitive Home pad to a SolarOS key tap on release; zero disables it. Touch state is retained until the controller provides a fresh frame.

An SDMMC adapter uses direct clock, command, and data bindings. Optional power=gpioN active=0|1 bindings control a slot rail, with an 80 ms off / 120 ms on cycle before each card initialization. The default active level is high. 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.

Display Link Protocol over streams

stream-dlp connects a DLP peripheral through an existing exclusive duplex byte stream. It registers the supplied name as a display target for text, graphics, and foreground applications. The stream_dlp firmware group is included in the full flavor, including boards without a built-in display. The stream owns transport setup; the driver owns DLP discovery, format selection, uploads, presentation completion, and recovery.

stream tcp connect pico0 192.0.2.10 7777
expansion attach stream-dlp lcd1 stream=pico0
display test lcd1
expansion detach lcd1
stream tcp remove pico0

Replace the address with a reachable DLP endpoint. For UART, create a UART port with the required pins and fixed baud first, then select its byte-stream ID. For example, on approved free pins and an unused controller:

expansion bus create uart dlp-uart port=uart1 tx=<host-tx-gpio> rx=<host-rx-gpio> baud=921600
expansion attach stream-dlp lcd1 stream=dlp-uart

Cross host TX to peripheral RX and host RX to peripheral TX, with common ground. Radio-backed streams follow the same contract. There is no baud negotiation or UART READY signal. Optional hardware reset is separate from the stream driver.

stream= completes compatible registered streams. Attachment claims the stream exclusively; it remains claimed until detach. Stop applications and release display claims before detaching. Detach frees the local DLP image, conversion and protocol buffers and releases the stream claim. The stream itself remains registered; a Link stream's RX/TX queues are released by link stream remove <port>.

timeout-ms=100..60000 sets discovery and the minimum command timeout, default 5000 ms. Peripheral presentation limits may require longer waits. Discovery retries transient stream failures within its connection deadline. A failed presentation returns an error; the next presentation reopens the stream, discovers a fresh protocol session, and uploads a full frame. Dimensions and supported formats must remain fixed for the attachment; detach and attach again after changing the peripheral.

Both DLP drivers use the same host core, including negotiated window uploads, compression, retained-state checks, and changed-region uploads. Private Python and Lua connections use that core without registering a named display.

Add compressed=1 to either DLP attachment to enable negotiated lossless LZ4 compression. For example:

expansion attach stream-dlp lcd1 stream=pico0 compressed=1
expansion attach spi-dlp lcd2 spi=<bus> cs=<allowed-cs-gpio> compressed=1

The setting belongs to that attachment and is preserved when you save it for startup. Omitted or compressed=0 retains the existing raw/RLE behavior. With the option enabled, the sender chooses raw, RLE, or LZ4 per block; unsupported endpoints fall back to raw/RLE. Compression preserves every pixel and works with all negotiated pixel formats. The receiver needs no separate compression switch.

Display Link Protocol over SPI

spi-dlp attaches a Display Link Protocol (DLP) peripheral under the supplied display name. The peripheral owns its physical panel. SolarOS reads its native dimensions and accepted pixel formats, selects a transfer format, and registers a display target for text, graphics, and foreground applications. A receive-capable SPI bus with MISO is required. The spi_dlp firmware group supplies the driver and is selected by the full flavor on compatible boards.

expansion attach spi-dlp lcd1 spi=<bus> cs=<allowed-cs-gpio>
expansion attach spi-dlp lcd1 spi=<bus> cs=<allowed-cs-gpio> ready=<allowed-input-gpio> reset=<allowed-output-gpio>

READY and RESET are independently optional. Without READY, transfers use polling and the peripheral's minimum spacing. RESET targets the peripheral controller, and defaults to active low with a 10 ms pulse and a 5 s boot deadline. Optional parameters are clock=<kHz>, reset-ms=<ms>, boot-ms=<ms>, and reset-active=0|1. The clock ceiling defaults to 10000 kHz and is limited by the peripheral's reported maximum. Width and height are discovered rather than supplied as bindings.

The host accepts dimensions up to 2040 pixels per axis and 1,048,576 total pixels, subject to available memory. It keeps a monochrome text buffer, an RGB565 composition buffer, and one conversion row. When memory permits, two additional native-format buffers (each at most 256 KiB) track changes. The driver uploads changed rectangles, uses optional byte RLE when useful, and checks retained state before skipping an unchanged frame. It sends a full replacement after content loss or when partial updates cost more. Larger images and cache allocation failures use bounded full-frame uploads. Color and indexed graphics are converted to the negotiated format. Changing geometry requires detaching and recreating the target after releasing display claims.

The DLP SPI profile retains SDLP v1 mode 1 and 256-byte full-duplex cells. The portable protocol also supports ordered byte streams; this driver uses SPI. Its negotiated format describes wire pixels; it does not require any particular MCU or panel bus. See the driver reference for bindings.

Quick reference

solaros.expansion.drivers() lists registered 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.

spi-dlp attaches a DLP endpoint using spi and cs, with independent optional ready and reset GPIOs. Native dimensions and wire formats are discovered during attachment. Optional clock is a ceiling in kHz; reset-ms, boot-ms, and reset-active configure reset and discovery.

stream-dlp attaches a DLP endpoint through stream=<existing-exclusive-duplex-byte-stream>. Optional timeout-ms=100..60000 defaults to 5000. Configure UART, TCP, BLE, or radio through its stream provider first; there is no baud negotiation. Attachment claims the stream until detach; release display claims before detaching. stream tcp connect <name> <host> <port> creates an outbound TCP stream. stream ble connect <name> <address> <address-type> creates an outbound native CoC stream. A stream ble listen <name> endpoint can also host a DLP client; BLE central/peripheral roles are independent of DLP direction.

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.
ES3C28P 2.8-inch ESP32-S3 Touch DisplayGPIO2, GPIO3, GPIO14-GPIO16, GPIO21, GPIO43, GPIO44GPIO2, GPIO3, GPIO14, GPIO21GPIO2, GPIO3, GPIO14GPIO15/GPIO16 are shared by touch, audio control, and the I2C connector; GPIO17/GPIO18 belong to touch; GPIO43/GPIO44 belong to uart0.
ES3N28P 2.8-inch ESP32-S3 DisplayGPIO2, GPIO3, GPIO14-GPIO16, GPIO21, GPIO43, GPIO44GPIO2, GPIO3, GPIO14, GPIO21GPIO2, GPIO3, GPIO14GPIO15/GPIO16 are shared I2C; GPIO43/GPIO44 belong to uart0; GPIO17/GPIO18 are reserved for the unpopulated touch footprint.
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.
Elecrow CrowPanel ESP32-S3 5.79-inch E-paperGPIO3, GPIO8, GPIO9, GPIO14-GPIO21, GPIO38GPIO8, GPIO9, GPIO14-GPIO21, GPIO38GPIO8, GPIO9, GPIO14-GPIO20GPIO3 is physically exposed but blocked as a strapping pin.
SolarWriterGPIO3, GPIO8, GPIO9, GPIO14-GPIO21, GPIO38GPIO18, GPIO19, GPIO20, GPIO38GPIO18, GPIO19, GPIO20GPIO8/9 are shared fuel-gauge/RTC I2C, GPIO14–17 are shared audio, GPIO21 is reserved for jack detect, and GPIO3 is a strapping pin.
Waveshare ESP32-S3-ePaper-3.97GPIO41-GPIO44 on the I2C/UART padsNoneNoneGPIO41/GPIO42 belong to shared i2c0; GPIO43/GPIO44 belong to uart0. The board exposes no unmanaged general-purpose signal.
Pi0cket 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.
ThinkNode M9None; 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, radio, GNSS, RTC, battery, buzzer, power, 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.
GOOUUU ESP32-S3CAMESP32-S3 signals broken out on the 2x20 side headersGPIO1-GPIO3, GPIO14, GPIO21, GPIO41, GPIO42, GPIO45-GPIO47GPIO1-GPIO3, GPIO14GPIO2 may carry an onboard LED load; GPIO3/GPIO46 share switch/shutter signals; GPIO3/GPIO45/GPIO46 are usable at runtime but external wiring must preserve their reset strapping levels. GPIO0 is BOOT/KEY; GPIO4-GPIO13 and GPIO15-GPIO18 belong to the camera; GPIO19/GPIO20 are native USB; GPIO35-GPIO37 are Octal PSRAM; GPIO38-GPIO40 are SDMMC; GPIO43/GPIO44 are uart0; GPIO48 drives the onboard WS2812.
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.
ES3C28P 2.8-inch ESP32-S3 Touch Displayi2c0: SDA GPIO16, SCL GPIO15; spi0: SCK GPIO12, MISO GPIO13, 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 FT6336 touch and ES8311 control. spi0 is the fixed internal LCD bus.
ES3N28P 2.8-inch ESP32-S3 Displayi2c0: SDA GPIO16, SCL GPIO15; spi0: SCK GPIO12, MISO GPIO13, 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 ES8311 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.
Elecrow CrowPanel ESP32-S3 5.79-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 dual-SSD1683 bus. SPI3 is shared with microSD and is available for a runtime expansion bus only while the SD card is unmounted.
SolarWriteri2c0: SDA GPIO8, SCL GPIO9; inherited CrowPanel SPI/UART busesI2C on i2c1, UART on uart1/uart2, or 1-Wire, using GPIO18/19/20/38; SPI3 while microSD is unmountedFixed audio0 owns I2S1 and GPIO14–17. battery0 (0x0b) and DS3231 rtc0 (0x68) share i2c0. RTC INT/SQW is unbound. GPIO21 is reserved for jack detect.
Waveshare ESP32-S3-ePaper-3.97i2c0: SDA GPIO41, SCL GPIO42; spi0: SCK GPIO11, MOSI GPIO12, CS GPIO10; uart0: TX GPIO43, RX GPIO44External I2C devices can share i2c0; external UART devices can use uart0 when the console releases it.spi0 is the fixed internal SSD1677 bus. All other GPIOs belong to fixed peripherals, native USB, controls, or power-management signals.
Pi0cket 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 pinscl_32: EX1, CN1, and CN2 expose the shared i2c0; EX1 exposes uart0. The internal core0, 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 Plusuart0: TX GPIO43, RX GPIO44, GNSS detection at 9600/38400 baud; internal i2c0 and spi0Grove UART after expansion detach gnss0; restore with expansion attach gnss0Grove is the only expansion connector and shares its UART signals with always-on GNSS. Use gnss disable gnss0 to silence supported receivers before detaching; reattachment restores output. Software release does not electrically disconnect GNSS TX, so an external transmitter needs suitable isolation. The I/O map shows Grove; display, storage, radio, keyboard, touch, and audio use internal wiring.
ThinkNode M9i2c0: SDA GPIO20, SCL GPIO21; i2c1: SDA GPIO7, SCL GPIO6; spi0: SCK GPIO40, MISO GPIO38, MOSI GPIO47, CS GPIO16/GPIO48/GPIO39; uart0: TX GPIO43, RX GPIO44; uart1: TX GPIO3, RX GPIO2 at 115200 baudNone through an external connectorDisplay, storage, keyboard, RTC, battery, buzzer, and GNSS use fixed internal resources. i2c0 is the keyboard bus and i2c1 the peripheral bus; uart0 is the CH340K USB console; uart1 belongs to the GNSS receiver. 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.
GOOUUU ESP32-S3CAMuart0: TX GPIO43, RX GPIO44I2C on i2c0/i2c1, SPI on spi2/spi3, UART on uart1/uart2, I2S on i2s1, or 1-Wire, using approved free pinsstorage0 and pixels0 attach automatically. Camera SIOD/SIOC remain camera-owned and are not exposed as a general I2C 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 GPIO3I2C on i2c0/i2c1, UART on uart1/uart2, or named 1-Wire, using approved free pinsGPIO4/GPIO15 can form an I2C bus when unused. Those pins are also external VSPI chip-select slots and cannot be shared with I2C. VSPI is shared with onboard TFT and SD devices.
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 registered 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 set available for manual attachment. The board-only cl32-display, sticky-display, and x4pro adapters detect the built-in display controller at startup and are excluded from this list.

DriverDeviceRequired bindingsResult after attach
manualResource-only profileAny valid bus, address, chip-select, GPIO, ADC, or PWM bindingsClaims resources without initializing hardware.
axp2101AXP2101 PMIC, fuel gauge, and battery chargeri2c=<bus> addr=0x34; optional input_current=<mA> charge_current=<mA> charge_voltage=<mV>Registers the board battery provider and charger0; also supplies the reusable ALDO3 control used by the Waveshare SSD1677 display.
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. Sleep retains configuration; radio state <name> standby wakes the radio without receiving, and radio state <name> rx enables continuous reception.
lr11xxSemtech LR11xx packet radio (LR1110/LR1120/LR1121)spi=<bus> cs=<pin> busy=<pin>; optional reset=<pin> irq=<pin> freq=<mhz> tcxo=<mv>Registers a sub-GHz LoRa packet-radio target for the radio command. The part reports which family member it is, so one driver covers all three and the summary names the one fitted. freq is the board's SKU in MHz and tcxo its TCXO supply in millivolts, or 0 for a crystal. A board with an antenna switch also declares its DIO table as SOLAR_OS_BOARD_LR11XX_RF_SWITCH. GFSK, 2.4 GHz, GNSS scanning, and Wi-Fi scanning are not covered.
pcd854484x48 SPI LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>Registers an auxiliary display target.
stream-dlpDisplay Link Protocol byte-stream endpointstream=<id> [timeout-ms=<ms>] [compressed=0|1]Claims an existing exclusive duplex byte stream.
spi-dlpDisplay Link Protocol SPI endpointspi=<bus> cs=<pin> [ready=<pin>] [reset=<pin>] [compressed=0|1]Discovers geometry and negotiates a wire pixel format.
ssd1683SSD1683 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<khz> rotation=<0..3> panel=<0..4> startup-pulse=<0|1>Registers an auxiliary target, or Elecrow's fixed display0, with auto, fast, and full refresh modes. Panel 0 auto-detects the 4.2-inch Elecrow revision; 1 is legacy Elecrow, 2 is green-sticker Elecrow, 3 is Waveshare V2, and 4 is Elecrow's 792x272 dual-controller panel. Panels 2, 3, and 4 also accept pulse mode and its runtime tuning controls. startup-pulse=1 selects pulse at attach when supported; the default is native auto.
uc8279UC8279 800x480 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin> panel=<1..2>; optional power=<pin> clock=<khz> rotation=<0..3>Panel 1 selects X4 Pro glass and sets its PLL; panel 2 selects X4 Classic glass and preserves the panel's programmed PLL. Both address 600 gates with the visible rows at gates 120..599 and require programmed OTP/MTP waveforms. Active-low BUSY, automatic full cleanup every 20 changed frames, and selectable partial/full refresh. Controller and panel selection are explicit; UC8279 X3 792x528 and other variants require separate profiles.
ssd1680SSD1680 122x250 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<100..4000 khz> rotation=<0..3> panel=0Default panel 0 selects 122x250 V4-style monochrome glass with full and partial waveforms in OTP. Active-high BUSY, native auto/partial/full refresh, and full cleanup after five partial updates. Rotation 1 or 3 gives 250x122 landscape.
ssd1685SSD1685 168x384 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin> panel=1; optional power=<pin> clock=<khz> rotation=<0..3>Panel 1 selects GDEY029T71H glass, including its source offset and OTP waveforms. Active-high BUSY, 10 MHz maximum SPI clock, full/partial refresh, and automatic full cleanup after five partial updates. Rotation 1 or 3 gives 384x168 landscape.
uc8179UC8179 800x480 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin> panel=1; optional power=<pin> clock=<khz> rotation=<0..3>Panel 1 selects Xteink 800x480 glass with 600 addressed gates and programmed OTP waveforms. Active-low BUSY, automatic full cleanup every 20 changed frames, and selectable partial/full refresh. No controller autodetection; other panel geometries and waveforms need their own profile.
ssd1677SSD1677 800x480 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<khz> rotation=<0..3> panel=<0,1> or paired power_i2c=<bus> power_addr=0x34 through an attached AXP2101 servicePanel 0 preserves the generic differential sequence; panel 1 selects Sticky's matching-plane full OTP refresh. Native auto/partial/full modes and Fastwave pulse mode with per-pixel settling, adaptive cleanup, and native recovery. Native mode releases pulse buffers and its worker. Holds the PMIC attachment while using ALDO3.
ssd2677Sticky 3D97 V2 SSD2677 800x480 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<100..20000 khz> rotation=<0..3> panel=<0,1> or paired power_i2c=<bus> power_addr=0x34Both panel values select the Sticky monochrome profile. panel=1 starts in asynchronous Fastwave/pulse; panel=0 starts in native auto. Supports shared pixel budgets, background settling, adaptive cleanup, native auto/fast/partial/full, temperature waveforms, MONO1 raster, recovery, and sleep/resume. Generic color panels are unsupported.
ssd1306SSD1306 I2C OLEDi2c=<bus> addr=<address>; optional panel=<0..7> rotation=<0..3>Registers an auxiliary display target with the selected panel's geometry and addressing. Defaults to 128x64, rotation 0.
sh1106SH1106 I2C OLEDi2c=<bus> addr=<address>; optional panel=<0..3> rotation=<0..3>Registers an auxiliary display target with panel-specific initialization and column offsets. Defaults to 128x64, rotation 0.
st7305400x300 or 384x168 reflective LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>; optional panel=0|1 rotation=1|3ESP32 and ESP32-S3. Panel 0 is the native 300x400 Waveshare panel; panel 1 is the native 168x384 Pi0cket 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=0|1 pwm=0|1ESP32 and ESP32-S3; ODROID-GO registers it as fixed display0.
st7796480x320 color TFTspi=<bus> cs=<pin> dc=<pin>; optional reset=<pin> bl=<pin> active=0|1 pwm=0|1ESP32 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=0|1 pwm=0|1ESP32-S3 driver; T-Deck Plus and the ThinkNode M9 register it as fixed display0 with shared peripheral power and backlight control. A board with an active-low power rail sets SOLAR_OS_BOARD_LCD_POWER_ACTIVE_LEVEL in its manifest.
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.
inputronic-keyboardSoldered Inputronic KEYBOARD (SKU 333360)i2c=<bus> addr=0x34 [reset=<gpio>] [irq=<gpio>]Polls the 8x10 matrix FIFO and publishes press/release events through the shared input service. Optional active-low reset and interrupt pins.
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. Registers its keyboard backlight for setterm backlight, using the keyboard firmware's brightness command.
stc8h-keyboardElecrow STC8H companion-MCU keypadi2c=<bus> addr=0x6c|0x6d; optional alt_addr=0x6c|0x6d backlight=<pwm-pin>Reads finished key codes from the STC8H scanner and publishes them through the shared input service. The address identifies the ThinkNode M9 board revision; with alt_addr declared, the driver probes both. Arrow, enter, and printable codes follow the CardKB map. Keycaps map Home to app exit, Back to escape, Messages/Maps/Pin to F1/F2/F3, and long-press Pin to F4. The scanner cannot emit tab, pipe, backslash, brackets, or several other characters, so the Message key arms a one-shot compose layer for the next key: space or t gives tab, p gives |, b backslash, e =, 5 %, 6 ^, 9/0 [/], (/) {/}, ,/. </>, g a backtick, and n ~. Composed arrows navigate instead: up and down page through the terminal scrollback, and right is tab, which the keypad cannot send any other way. Composed left sends a plain left arrow and disarms the layer. Up, down, and right keep the layer armed so repeated presses keep working. A second Message press or a two-second timeout disarms it. The optional backlight binding registers PWM illumination for setterm backlight.
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.
tab5-keyboardM5Stack Tab5 70-key keyboardi2c=<bus> addr=0x6d [irq=<pin>]Normal-mode press/release events, runtime keymaps, optional active-low interrupt with polling fallback. Accepts an already configured address from 0x08 to 0x77.
tca8418Generic TCA8418 matrix keyboardi2c=<bus> addr=0x34 [rows=1..8] [cols=1..10] [reset=<pin>] [irq=<pin>]Defaults to an 8x10 reference map; supports user mappings through input keymap.
lilygo-pager-keyboardLilyGO T-LoRa-Pager keyboardi2c=<bus> addr=0x34 [reset=<pin>] [irq=<pin>] [backlight=<pwm-pin>]Fixed 4x10 Pager wiring and symbol layer, with optional PWM backlight controlled by setterm backlight.
cl32-coreIntegrated Pi0cket CL-32 ATmega808 controlleri2c=<bus> addr=0x08Fixed Pi0cket CL-32-only core0; polls keyboard press/release events into keyboard0, provides battery0 from the AVR voltage, USB-power, and charging state, and controls keyboard illumination through setterm backlight. The AVR retains its keyboard-activity timeout and fade. 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.
trackballFour-channel GPIO trackballup=<gpio> down=<gpio> left=<gpio> right=<gpio> [pull=0|1|2]Counts both interrupt edges on each direction input and publishes relative pointer deltas. Pull defaults to 1 (up); 0 is none and 2 is down. Pins must be distinct. The push switch is independent.
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=0x14 reset=<pin> power=<pin> active=<0|1> home_key=<numeric-key> swap_xy=<0|1>Polls absolute pointer events into the shared input service and retains state between fresh frames. Axis swapping precedes rotation and is off by default. Optional reset performs interrupt-pin address selection; optional power controls the touch rail (default active high). Both declared addresses are claimed. The Home pad can emit a key tap on release.
mgc3130MGC3130/Skywriter 3D gesture sensori2c=<bus> addr=0x42|0x43 transfer=<pin> reset=<pin> rotation=<0..3>; optional airwheel=0|1Registers 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.
pcf8563PCF8563 / BM8563 real-time clocki2c=<bus> addr=0x51; optional irq=<pin>Registers an RTC provider with calendar, minute-resolution alarms, countdown, and optional active-low interrupt support. Calendar dates use 2000–2099 with the century bit cleared. Low-voltage status marks the clock unreliable until the date is set. A new or unpowered RTC may have an invalid calendar; set it with date YYYY-MM-DD then time HH:MM:SS, or ntp after connecting Wi-Fi. Valid saved time is preserved on boot. Nonzero alarm seconds are unsupported; countdown accepts 1–255 seconds or whole minutes up to 255 minutes.
ds3231DS3231 real-time clocki2c=<bus> addr=0x68; optional irq=<pin>Registers an RTC provider with a 2000–2099 calendar, second-resolution Alarm 1, and optional active-low INT/SQW interrupt. Oscillator-stop status marks time unreliable until it is set. Reads accept 12-hour or 24-hour format; writes use 24-hour format. Countdown is unsupported; Alarm 2 interrupts are disabled.
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=0x55Reads battery voltage and gauge-reported state of charge; charging state is inferred from the discharge flag.
lc709203fLC709203F single-cell Li-ion fuel gaugei2c=<bus> addr=0x0b; optional capacity=<mAh> profile=0|1 temp=<decic> apa=<value>CRC-checked voltage and state of charge; follows configured battery capacity. No charger-state measurement.
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.
cw2017CW2017 fuel gaugei2c=<bus> addr=0x63Registers a battery provider with cell voltage and state of charge. Preserves the resident cell-specific BATINFO profile. Without a loaded profile or valid SOC, battery percentage uses the service's voltage estimate. Charging and external-power state are not reported by the gauge.
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.
nmeaNMEA 0183 GNSS receiveruart=<bus>; optional power=<gpio-or-controller-line> active=0|1 (both or neither) and alt_baud=<4800..921600>Parses streamed RMC/GGA in a background reader; gnss fix uses sentences at most 2.5 s old. Detection listens at the configured rate, then alt_baud, restoring the configured rate if neither works. Switched receivers require explicit power polarity. gnss enable / gnss disable use recognized UBX UART1 or CASIC UART0 configuration, read back the output setting, and observe 2.6 s without received bytes before reporting disabled. Binary control replies remain enabled; unexpected periodic output fails the disable check. Baud, sentence rates, power, and UART ownership remain unchanged. Muted receivers are restored on attachment. RMC/GGA provide no position accuracy or PDOP. Failed power-off retains registration for retry.
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.
mpu6050MPU6050 six-axis IMUi2c=<bus> addr=0x68|0x69; optional irq=<pin>Registers SI-unit acceleration and angular velocity at 100 Hz, ±2 g and ±250°/s. Polls data-ready status without IRQ; an IRQ binding checks the active-high latched data-ready input. Reads are on demand; no sampling worker or orientation estimate.
qmi8658QMI8658 six-axis IMUi2c=<bus> addr=0x6a|0x6bPolls data-ready status and registers SI-unit acceleration and angular-rate measurements. The Waveshare board uses fixed imu0 at 0x6a.
lsm6ds3tr-cLSM6DS3TR-C six-axis IMUi2c=<bus> addr=0x6a|0x6b; optional irq=<pin>Registers SI-unit acceleration and angular velocity at 104 Hz, ±2 g and ±250°/s. An IRQ binding enables hardware tap, double-tap, tilt, wake, and free-fall gestures and raises acceleration to 416 Hz. Sticky binds imu0 at 0x6a with IRQ GPIO7.
drv2605TI DRV2605 haptic controlleri2c=<bus> addr=0x5a; optional power=<gpio-or-controller-line>Registers a haptic provider for the controller's ROM effects.
pcnt-encoderHardware quadrature encodera=<gpio> b=<gpio> mode=<1|2|4>; optional invert=<0|1> pull=<0|1|2> filter_ns=<integer>Registers a named encoder with explicit x1/x2/x4 counting. See Encoder counts.
drv8830TI DRV8830 brushed DC motor driveri2c=<bus> addr=<0x60..0x68>; optional int=<gpio> max_mv=<482..5060>Registers a motor endpoint for normalized drive, coast, brake and fault handling. Attaches in coast. See Motor control.
pca9685NXP PCA9685 16-channel PWM controlleri2c=<bus> addr=<0x40..0x77 except 0x70>; optional oe=<gpio-or-controller-line> osc_hz=<20000000..30000000>Registers PWM outputs <device>:0 through <device>:15, initially off. All channels share one frequency; reserved or active outputs prevent detach. See Servo outputs.
sdmmcNative SD/MMC card slotclk=<pin> cmd=<pin> d0=<pin>; optional four-bit set d1=<pin> d2=<pin> d3=<pin> and power=<pin> active=<0|1>Built-in slots register as fixed storage0; runtime attachments mount removable FAT storage at /sdcard. Classic ESP32 uses its native slot-1 pins. An optional power rail cycles off for 80 ms, then on for 120 ms before card initialization (default active high).
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.
audio-pdmPDM microphone input on ESP32-S3i2s=i2s0 clk=<pin> din=<pin>; optional power=<pin> slot=0|1Registers <name>.capture as exclusive 16 kHz, signed 16-bit mono PCM. Power and DMA are enabled on stream open and released on close; slot 0 selects the left microphone channel. Sticky binds mic0.
esp32-cameraDVP JPEG camerad0=<pin> through d7=<pin>, vsync=<pin> href=<pin> pclk=<pin> xclk=<pin>; either i2c=<bus> or siod=<pin> sioc=<pin>; optional pwdn=<pin> reset=<pin>ESP32-S3 with PSRAM; claims GPIOs and the single DVP capture peripheral. Named SCCB buses can be shared with other devices; direct-pin SCCB reserves the configured I2C controller. Registers the device name as an exclusive typed video source. Capture starts only on use. Busy owners or frames prevent detach.
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.
inmp441Three-wire I2S microphone, optional shared playbacki2s=<port> bck=<pin> ws=<pin> din=<pin>; optional slot=0|1 dout=<pin>Registers mono 16 kHz capture. With dout, also registers stereo playback on the same clocks and controller. 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 DACpos=gpio25|gpio26; optional neg=gpio25|gpio26 amp=<pin> active=0|1Registers 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

For camera SCCB control, use either an existing named I2C bus or both SIOD/SIOC GPIO bindings. Supplying both modes, only one SCCB pin, or neither mode is rejected. With i2c=i2c0, the bus supplies SDA/SCL wiring and retains its other devices when the camera stops. The camera keeps a bus lease until detach; d0..d7, vsync, href, pclk, and xclk remain GPIO bindings. On Pi0cket CL-32, the expansion connector's camera control signals use the board-owned i2c0, so select i2c=i2c0 alongside the camera's DVP pin assignments. After attachment, camera status initializes and probes the camera, and camera capture <path> captures a JPEG. The direct-pin mode retains siod=<pin> sioc=<pin> and reserves the SCCB controller selected by the firmware.

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.

LC709203F battery fuel gauge

Connect the Adafruit breakout's VIN to 3.3 V for ESP32 logic, GND to common ground, and SDA/SDI and SCL to a named I2C bus. Connect the actual single-cell Li-ion battery to the breakout's battery sockets. Its two battery sockets are parallel: use one for the cell and the other for the board's battery connection. The battery powers the gauge chip; VIN supplies the I2C pull-ups and level shifters. The fixed 7-bit address is 0x0b.

expansion attach lc709203f battery0 i2c=i2c0 addr=0x0b capacity=3000
battery status
battery capacity 3000

The driver wakes the gauge, checks each read's CRC, and registers the battery service's voltage and gauge-reported percentage. It does not measure charger status or USB power presence. Successful samples are cached for two seconds; I2C and CRC failures return errors rather than estimated percentages.

capacity is the attachment's fallback in mAh (default 500). A nonzero battery capacity setting takes precedence and updates the gauge's APA setting on the next sample. APA uses the closest characterized Type-01/Type-03 cell capacity: 100, 200, 500, 1000, 2000, or 3000 mAh. Other cell/IC variants can specify apa=1..255 to use their manufacturer's adjustment value instead. profile=1 is the default Adafruit 3.7 V nominal / 4.2 V charge profile; profile=0 selects the other profile stored in the chip.

Without a thermistor, temp=250 supplies an assumed 25.0 °C to the gauge's algorithm. This is compensation input, not a temperature measurement. temp accepts -200 through 600 in tenths of a degree Celsius. No interrupt wire is required. Detach removes the provider but leaves the gauge operating so battery tracking continues. Select the lc709203f firmware group on boards with expansion I2C.

INMP441 I2S microphone

Power the microphone from 3.3 V, connect its SCK/WS/SD to bck/ws/din, and tie L/R to ground or 3.3 V. It needs no MCLK or I2C connection. slot=0 selects the first received stereo slot; slot=1 selects the second. The default is 0. The driver reads 24-bit microphone data in 32-bit Philips-I2S slots and exports its most significant 16 bits as mono PCM at 16 kHz. Startup samples are discarded before capture opens.

expansion attach inmp441 mic0 i2s=i2s1 bck=gpio14 ws=gpio15 din=gpio17 slot=1
arecord -d 5 -i mic0.capture /sdcard/microphone.wav

Add dout=gpio16 to send playback data to an I2S DAC or MAX98357 amplifier. Both receivers can share that data line and the same BCK/WS clocks:

expansion attach inmp441 audio0 i2s=i2s1 bck=gpio14 ws=gpio15 din=gpio17 slot=1 dout=gpio16
audio default audio0

This attachment provides audio0.capture (mono) and audio0.playback (stereo). Capture and playback can run together at 16 kHz. Mono playback is duplicated to both outputs; volume is applied in software. The output remains silent when only capture is open. Use arecord, recorder, or typed streams for this expansion input; audio mic measures the board audio backend.

Detach an existing pcm5102/i2s-output attachment before attaching this combined configuration. The clocks, pins, and I2S controller have one owner. Close both capture and playback streams before detaching. Select the inmp441 firmware group on boards with expansion_i2s.

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.

Stream Display Link

stream-dlp requires the stream_dlp firmware group. It accepts an existing exclusive duplex byte stream with stream=<id>. The optional timeout-ms=<ms> is 100..60000, default 5000; it sets discovery and the minimum command timeout. The stream provider handles UART, TCP, or radio setup. The supplied device name becomes the display target name. Four named DLP endpoints can be attached across the SPI and stream drivers.

Both drivers accept optional compressed=0|1, default 0. Set it to 1 to enable negotiated lossless LZ4 blocks with raw/RLE fallback. Unsupported servers retain raw/RLE operation. Saving the attachment preserves this parameter, and reconnects renegotiate server support. Pixel values and display backend selection are unchanged.

The attachment claims the stream until detach. Release display claims before detaching; a claimed stream cannot be removed. On a failed presentation, the next presentation reopens the stream and starts a fresh DLP session with a full repaint. Changed geometry or format capabilities require a new attachment. No UART baud negotiation, READY, or reset binding is provided here.

SPI Display Link

spi-dlp requires the spi_dlp firmware group, a graphics-capable runtime, and expansion SPI/GPIO resources. The SPI bus must include MISO and support transfers of at least 256 bytes. The supplied device name becomes the display target name. A maximum of four named DLP endpoints can be attached across both drivers.

BindingRequirementMeaning
spi=<bus>RequiredNamed host SPI bus
cs=<pin>RequiredAllowed native GPIO chip select, active low
ready=<pin>OptionalActive-high peripheral transport-ready input
reset=<pin>OptionalPeripheral-controller reset output
clock=<kHz>OptionalHost clock ceiling, 100..40000; default 10000
reset-ms=<ms>OptionalReset assertion time, 1..1000; default 10
boot-ms=<ms>OptionalDiscovery deadline, 100..60000; default 5000
reset-active=0|1OptionalAsserted reset level; default 0
compressed=0|1OptionalEnable negotiated lossless LZ4; default 0 retains raw/RLE

READY and RESET may be wired independently. A READY binding requires the peripheral's READY capability. Without it, the host polls with the advertised minimum CS-high interval. Optional reset supports recovery without depending on a working SPI response. Keep the shared SPI bus available while the peripheral boots or refreshes.

Wire formats are MONO1, GRAY2, GRAY4, GRAY8, and RGB565 with the high byte first. Each endpoint advertises the subset it accepts and its preferred format. The native resolution stays fixed for an attachment. A bridge that continuously scans a raw LCD maintains that scanout independently of host image uploads.

The DLP SPI profile retains the SDLP v1 fixed 256-byte cells. Optional RLE and retained-state queries are negotiated after configuration. The host sends changed rectangles when its native-format image caches are available, and falls back to full replacements after content loss or when deltas cost more. Each cache is limited to 256 KiB; allocation failure uses full-frame streaming. This driver exposes SPI; the portable protocol library also supports ordered byte streams.

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.

122x250 monochrome e-paper (SSD1680)

Use ssd1680 for a four-wire SPI panel with active-high BUSY. The default panel=0 profile is 122 × 250 pixels, with rotation 0 and a 4 MHz SPI clock. It uses the 2.13-inch V4-style monochrome panel's OTP full and partial waveforms. Other glass sizes, tri-color panels, and panels requiring a custom RAM LUT need their own panel profile.

For example, on an ESP32-S3 DevKitC SPI bus:

expansion attach ssd1680 epd0 spi=spi0 cs=gpio10 dc=gpio17 reset=gpio16 busy=gpio15
display mode epd0 refresh=auto

Add rotation=1 or rotation=3 for 250 × 122 landscape. An optional power GPIO controls an active-high panel supply. clock accepts 100 through 4000 kHz. The full firmware flavor includes the driver; custom builds can select the ssd1680 group. The same bindings can define a fixed primary display0 in a board manifest or an auxiliary display attached at runtime.

refresh=auto starts with a full refresh, uses the native partial waveform for changed images, and performs a full cleanup after five partial updates. refresh=partial and its refresh=fast alias use the same cleanup policy; refresh=full uses full refreshes. Unchanged visible images are skipped. MONO1 raster frames, display relax, and display sleep/resume use the common display services. The controller's byte padding stays outside the visible 122 × 250 geometry.

GDEY029T71H e-paper (SSD1685)

Use the ssd1685 driver with panel=1 for this 2.9-inch black-and-white panel. Supply 3.3 V and connect BUSY as an active-high input. The optional power GPIO controls an active-high module supply enable. SPI defaults to 10 MHz; clock accepts 100 through 10000 kHz.

On a named SPI bus with the following free control pins:

expansion attach ssd1685 epd0 spi=spi0 cs=gpio10 dc=gpio17 reset=gpio16 busy=gpio15 panel=1 rotation=3
display test epd0
display mode epd0 refresh=full
display mode epd0 refresh=auto
expansion detach epd0

The native framebuffer is 168x384. Rotations 1 and 3 expose a 384x168 target. Primary and runtime SSD1685 displays accept MONO1 raster frames, including rotation, palette inversion, and background borders. The shared frame presenter converts indexed app frames to monochrome, including Game Boy's four-shade video. Pulse mode submits raster frames to the same asynchronous scan worker used for graphics drawing.

All supported EPD drivers provide display relax [<target> [seconds]], including SSD1683, SSD1685, SSD1677, UC8179, UC8279, and the X4 display wrapper. Recovery uses native full black/white refreshes, then restores the saved image and refresh settings. For example, display relax display0 60 runs for about a minute; cancel between refreshes with Escape or Ctrl+C.

In refresh=auto, the factory waveform starts with a full refresh, skips unchanged frames, and performs 64 partial updates before the next full cleanup by default. cleanup-every=1..128 changes this OTP cleanup interval. refresh=partial (also accepted as refresh=fast) requests partial updates while a valid previous frame is available; initialization, sleep/resume, and transfer recovery always begin with a full refresh. refresh=full uses the panel's temperature-dependent full waveform.

The default refresh=pulse uses an experimental SSD1685 Fastwave adapter with the shared per-pixel budgeting and scan worker used by the Elecrow boards. It retains factory source/gate voltages and sets VCOM to -0.2 V. Contrast, polarity, ghosting, and timing need validation on the installed panel. Repeated custom pulses can leave persistent staining.

CL-32 EPD starts with these settings; inspect them with the last command:

display mode display0 cleanup-every=64
display mode display0 cleanup-policy=adaptive
display mode display0 drive-cycles=3
display mode display0 pulse-period=100
display mode display0 refresh=pulse
display mode display0

drive-cycles=1..32 sets successful activations per changed pixel (default 3). pulse-frames=1..32 sets the duration of each drive phase (default 10), and waveform-fr=1..12 selects a frame-rate code (default 5, nominal 125 Hz). Identical frames do not refill budgets. pulse-scan=async is the default; settling pulses continue in the background, and newer targets can replace older ones between drives. pulse-scan=sync finishes the pending drives before returning. pulse-period=67..1000 sets the minimum new-image interval in milliseconds (default 100), independently of settling pulses.

In pulse mode, cleanup-policy=fixed schedules a full OTP cleanup every cleanup-every changed images. The default cleanup-policy=adaptive cleans up when a pixel reaches cleanup-every * drive-cycles successful drives or after 4 * cleanup-every changed images. The default cleanup baseline is 64. Mode output reports cleanup age, peak drive exposure, pending pixels, and separate image and scan counters.

pulse-power=hold|cycle selects retained supplies or cycling between drives (default hold). pulse-ramp=normal|fast changes booster startup duration (default normal). pulse-spi-khz=100..10000 overrides the clock in pulse mode, initially using the attachment's SPI clock. invert=true swaps drive polarity for panel testing (default false). Use one setting per command.

To return to the factory setup:

display mode display0 refresh=auto

The aliases waveform=fastwave and waveform=otp select refresh=pulse and refresh=auto, respectively. Initialization, resume, and mode recovery establish a full-refresh baseline. An asynchronous failure returns to refresh=auto and attempts a full refresh of the latest accepted image. Pulse mode requires PSRAM for pixel budgets and worker snapshots, allocated during attachment. If they cannot be allocated, attachment fails. A later failed mode change leaves the previous refresh mode selected.

CL-32's e-paper variant uses this panel with SCK GPIO9, MOSI GPIO10, CS GPIO6, DC GPIO13, reset GPIO12, and BUSY GPIO14. Build cl_32 for either display. Its board-owned cl32-display adapter detects SSD1685 or ST7305 at startup and attaches the matching primary driver on these shared SPI/control pins.

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.

M5Stack Tab5 Keyboard

The Tab5 Keyboard (A164) is a 3.3 V, 70-key I2C keyboard with a default address of 0x6d. Connect 3.3 V, GND, SDA, and SCL to the corresponding board supply and named bus. INT is optional; connect it to a free, input-capable GPIO and pass irq=<pin>. Use expansion status and expansion layout to find the board's buses and pins.

The waveshare_esp32_s3_sim7670g_4g_epaper profile attaches this keyboard automatically as fixed keyboard0 on i2c0 at 0x6d, with INT on GPIO41. GPIO40 is available for expansion. Use input test keyboard0 to inspect its event count and last event. Fixed board attachments cannot be detached or attached a second time from the shell.

On boards without a fixed Tab5 attachment, attach a connected keyboard with:

expansion attach tab5-keyboard keyboard0 i2c=i2c0 addr=0x6d
input sources
input keymap keyboard0 show
expansion detach keyboard0

For interrupt operation, append an available GPIO binding, for example irq=gpio1 when GPIO1 is free on the board. Falling INT wakes the keyboard worker. It also polls every 50 ms to cover missed edges; without INT it polls every 10 ms. These are worker intervals, not guaranteed input latency.

The driver selects Normal mode and maps both press and release events into the common input service, including host key repeat. Aa acts as held Shift, Ctrl and Alt are held modifiers, and Sym selects the printed symbol layer while held. Character-mode latch and double-click behavior is not used. Letters and navigation keys carry HID usages. Printed punctuation uses logical character mappings without HID usages; raw HID forwarding requires a mapping that supplies the appropriate usages and modifiers.

Physical IDs are 1 + row * 14 + column, with rows 0..4 and columns 0..13. Use input keymap keyboard0 load <path> and input keymap keyboard0 reset to customize or restore the built-in map. This driver uses runtime keymaps; it is not a SOLAR_OS_KEYMAP_PROFILE desktop/build profile.

The firmware has a 32-event FIFO and no overflow status flag. A detected full queue, malformed event, input queue failure, or I2C error releases tracked keys and triggers reconfiguration with a cleared FIFO. Release and press keys again after recovery. A controller reset or an undetected dropped event cannot be reconstructed from the event-only protocol.

Detach stops the worker and removes its GPIO handler, releases keys, and restores the previous keyboard mode and interrupt configuration. It leaves the named I2C bus available to other devices. If restoring configuration fails, ownership is retained so detach can be retried. Attaching clears old queued events; RGB settings and the persistent I2C address are not changed.

Custom flavors can enable tab5_keyboard = true in [groups] or select expansion_tab5_keyboard in [packages]. The full flavor includes it on ESP32 and ESP32-S3 boards with expansion I2C support. addr selects the current device address; it does not reprogram the keyboard.

TCA8418 matrix keyboards and mappings

tca8418, inputronic-keyboard, and lilygo-pager-keyboard share the same controller backend, FIFO recovery, and matrix-to-input mapper. Each attachment owns one controller and input source. The fixed controller address is 0x34; multiple controllers require separate named I2C buses. RESET and INT bindings are optional. RESET is active low and is pulsed before the first probe. FIFO polling works with or without INT.

The generic tca8418 driver accepts rows=1..8 and cols=1..10, defaulting to 8 rows and 10 columns. The branded drivers select their own fixed geometry and mapping. Generic TCA8418 has no PWM dependency; the Pager profile owns its optional backlight.

expansion attach tca8418 keyboard1 i2c=i2c0 addr=0x34 rows=4 cols=10
input keymap keyboard1
input keymap keyboard1 load /sd/keymap.json
input keymap keyboard1 reset

The generic default is a reference wiring map. These are the keys under the US keyboard layout; HID usages pass through the active SolarOS keyboard layout for translation.

RowColumns 0 through 9
0Q W E R T Y U I O P
1A S D F G H J K L Enter
2Left Shift Z X C V B N M Backspace Left Control
3Space Tab Escape Caps Lock Left Alt Left Down Up Right Delete
41 2 3 4 5 6 7 8 9 0
5F1 F2 F3 F4 F5 F6 F7 F8 F9 F10
6Minus Equals Left bracket Right bracket Backslash Semicolon Quote Grave Comma Period
7Left GUI Right Control Right Shift Right Alt Right GUI F11 F12 Insert Home End

Rows and columns are zero-based in mapping files. The physical event ID is 1 + row * 10 + col, even when fewer than ten columns are enabled. The controller's matrix geometry is selected at attachment; mapping files cannot change it.

The input service manages each source's mapping, modifier state, symbol layer, and tap/hold selector. input keymap lists mapping support; input keymap keyboard1 show inspects the active map. See source keymaps for physical-key selectors, compatible matrix selectors, JSON fields, and Python/Lua capability queries. The existing row/column mapping files remain valid for these three profiles.

The Pager's built-in profile uses Space as a tap/hold symbol selector and its Caps-labelled key as a momentary Shift modifier. Unlabelled cells produce no logical input. The Pager board manifest initializes lilygo-pager-keyboard; scripts that previously selected tca8418 for the Pager should use this name to retain its wiring, symbol layer, and backlight binding. The generic tca8418 name now selects the reference map. Custom flavors can select the lilygo_pager_keyboard hardware group for the Pager profile.

Soldered Inputronic KEYBOARD

The standalone Inputronic KEYBOARD (SKU 333360) uses a TCA8418 at the fixed I2C address 0x34. Connect its easyC/Qwiic connector to a 3.3 V supply, ground, and the SDA/SCL pins of a named I2C bus. Use expansion bus to identify the bus and its pins. A board-owned bus can be shared with other devices at different addresses. Enable the inputronic_keyboard hardware group in custom flavors; it is included in full.

expansion attach inputronic-keyboard keyboard1 i2c=i2c0 addr=0x34
input keyboard
expansion detach keyboard1

If RESET is wired to a host GPIO, add reset=<gpio> to pulse the active-low reset and hold it high before probing the keyboard. Otherwise RESET must have a pull-up to the keyboard supply. If INT is wired, irq=<gpio> reserves that input with a pull-up; FIFO polling still runs without a host interrupt. The custom 4G/e-paper profile binds RESET to GPIO41 and INT to GPIO40.

Letters use the active SolarOS keyboard layout. Caps Lock starts off and toggles on each Caps press; Shift reverses letter case. Number and punctuation keys retain the Inputronic symbol map: Shift+0 through Shift+9 produce = ! " # $ % & / ( ), and Shift+semicolon, Shift+comma, and Shift+period produce colon, semicolon, and colon. Control/Alt chords and modified arrows use the common keyboard translation. Enter, Escape, Tab, Backspace, Delete, arrows, Space, and F1 through F10 feed shells and foreground apps. Held keys use the shared input repeat settings.

The six FN keys publish physical press/release events with logical key and HID usage zero for applications to interpret. Their physical IDs are FN1=78, FN2=79, FN3=17, FN4=18, FN5=19, and FN6=20. They do not emit text. FIFO overflow or I2C failure clears held keys and modifiers; after recovery, release and press any still-held key again. Detaching closes the keyboard source and releases the expansion's bus/address claims. Multiple keyboards can use separate named buses, each at 0x34.

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

Both drivers accept panel and rotation. Panel numbers are specific to the controller; select the profile that matches the module:

PanelSSD1306SH1106
0 (default)Generic 128x64Generic 128x64
1Univision 128x32Visionox 128x32
2EastRising 64x4864x32
3Generic 64x32Wise 72x40
4EastRising 96x16
596x40
6EastRising 72x40
7Winstar 48x64

Rotations 0, 1, 2, and 3 select 0, 90, 180, and 270 degrees. Rotations 1 and 3 swap the target's width and height. For example, a 128x32 SSD1306 module can expose a 32x128 target:

expansion attach ssd1306 oled0 i2c=i2c0 addr=0x3c panel=1 rotation=1

OLED attachments use the named I2C bus for the address probe, initialization, display updates, and shutdown. This also supports runtime-created buses on headless boards. Other devices at different addresses can share the bus. Use the wiring of the selected bus; the SolarTerm example below uses its default i2c0 pins.

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

Classic CAN

Classic CAN controllers attach through twai-can; see Classic CAN for transceiver wiring, bitrate, frame, and recovery controls.

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

Feedback controllers

Controllers compare a measurement with a setpoint and calculate an output. The controllers package group includes the service and its background job. It is enabled in the full flavor. Up to eight named instances are available. Standalone names use 1 through 22 lowercase letters, digits, underscores, or hyphens. Definitions are volatile; restore them from a startup script when required.

Setpoint streams

Each standalone controller publishes controller.<name>.setpoint, a scalar f32 command input with readback. Values use the measurement source's native unit: for enc.<name>, that is output-shaft rad/s. stream status shows the unit after the feedback source is bound. A setpoint command updates the target; it does not start a controller or its background job.

For an already configured controller:

stream write controller.left_pid.setpoint 0.5
stream echo controller.left_pid.setpoint --once

stream write sends one value and closes its writer. Applications can keep a sink handle open and write native float32 values repeatedly. One writer owns the input at a time; readback and echo remain available alongside it. The input uses the same writer claim as controller.<name>.setpoint in the parameter service, so shell configuration, parameter/OSC/control writes and a stream producer cannot overwrite each other. Gain/limit changes, rebinding and tuning are rejected while an input writer is attached.

Each accepted write renews command_timeout_ms when it is nonzero. Rejected values do not renew it. Closing a writer leaves the last command in effect: with timeout zero it stays latched; otherwise it expires normally. Readback reports the last accepted command and its original acquisition timestamp, even after expiry stops the output. Controller status reports the current waiting state and active setpoint separately. Close open stream handles before deleting the controller. Setpoint streams are command inputs/readback, not PID feedback sources or targets for another PID instance.

Python and Lua use the existing stream API. For example, Python can open solaros.streams.open("controller.left_pid.setpoint", {"direction": "sink"}) and write struct.pack("<f", value) through solaros.streams.write(handle, data). Lua uses the same sink option and string.pack("<f", value). Close the handle when the producer stops. The ordinary setpoint parameter and convenience controller configure <name> setpoint <value> remain available when no stream writer owns the input.

Controllers owned by components

Components can create PID instances through native controllers ABI 1. These share the same eight-slot registry and PID implementation. Motor velocity joints appear under their joint names with owner=robot; owned names fit 23 bytes. controller list/status and Python/Lua solaros.controllers expose their state, including owner, waiting and feedforward. Output includes feedforward.

Motor velocity joints bind the same calibrated encoder sources and motor sinks available to standalone controllers. Encoder scaling, filtering and relative position belong to the encoder service. PID, ramping, deadband, friction feedforward and command freshness belong to the controller service; motor polarity, drive limits and stop behavior belong to the motor service. A joint supplies commands and ticks its owned OS controller on its sampling task. The generic controller job skips owned instances to avoid duplicate stepping. claimed includes the bound device streams. No motor feedback/control loop implementation is linked into the robot library.

Owned instances are configured and released through their owner's API. Generic configure, bind, feed, step, manual, auto, reset, start, tune and delete operations are rejected. controller stop (or stopping all controllers) revokes further evaluation and stops/releases its bound endpoints. A robot joint observes this on its next sample and latches a fault. Use robot stop/start or joint disable/enable to recover. The owner retains the controller handle until physical cleanup succeeds. Servo-position joints use the OS servo service and have no PID instance.

Owned instances receive setpoints through their owner rather than registering an independently writable controller.<name>.setpoint parameter. Standalone controllers retain the parameter, stream and PWM interfaces described below. Owned instances expose controller.<name>.setpoint as read-only command observation. Stream writers cannot bypass the robot API or its lifecycle. Robot startup claims endpoint configuration, applies URDF settings, and restores the previous settings before releasing the endpoints. Failed cleanup retains ownership for a later stop retry.

Algorithms

pid provides proportional, integral, and derivative terms in parallel form. Set unused gains to zero for P, PI, or PD operation. Defaults are kp=1, ki=0.1, and kd=0, so a new PID instance starts as PI. Gains are nonnegative. reverse=0 means increasing output increases the measurement. Use reverse=1 when increasing output decreases the measurement.

The error is setpoint minus measurement, with its sign reversed for reverse operation. The integral uses elapsed seconds and conditional integration: integration pauses when it would drive an already saturated output farther beyond its limit. The derivative acts on the measurement, with a first-order filter using backward Euler and the tau time constant in seconds. Setpoint changes do not create a derivative kick. A nonzero derivative gain requires a positive tau.

hysteresis switches between output_min and output_max. With normal direction, output becomes maximum at or below setpoint - hysteresis/2 and minimum at or above setpoint + hysteresis/2. Between these thresholds it retains the previous output level. Reverse direction exchanges the actions. The band width must be positive.

Setpoint and measurement use the same physical unit. Output limits and gains must match the output adapter's unit. Values are not normalized automatically.

Manual stepping

An instance can run without an output binding or a job:

controller create test pid
controller configure test setpoint 0.8
controller configure test kp 0.5
controller configure test ki 0.2
controller step test 0.3 0.02
controller status test
controller delete test

step takes the measurement and elapsed seconds, returns the output, and never writes an actuator. It is available only while the instance is stopped and owns no output. Zero, negative, non-finite, or excessive elapsed times are rejected without advancing algorithm state. max_dt defaults to one second.

controller manual <name> <output> selects a fixed output within the limits. controller auto <name> resumes automatic computation. PI/PID instances with nonzero integral gain track the manual output for a smooth first automatic step. P and PD instances cannot compensate their proportional/derivative terms in this way. Hysteresis resumes its two-level switching behavior. reset clears algorithm history and the fault indication while stopped and unclaimed, using safe_output as the initial output. It also clears tuning results.

Standalone PID tuning

controller tune <name> [noise-floor [points [timeout-ms]]]
controller status <name>
controller stop <name>

tune starts an automatic experiment on a stopped, unclaimed standalone PID controller. It works without the robot library installed. Component-owned controllers, including owner=robot, reject tuning without changing their state. Hysteresis instances cannot be tuned.

Configure direction, permitted output limits, safe_output, measurement timing, and the permitted measurement range (setpoint_min/max) first. The current setpoint is ignored; the tuner chooses its own operating points. It measures a one-second baseline, increases drive in 2% increments of the available output span until motion is detected, then measures several five-second step responses above the detected breakaway output. Between steps it returns to safe_output and waits for the baseline to recover.

The default is four step-response points. Optional points accepts 3 through

  1. noise-floor is the minimum meaningful response in feedback units, not
  2. motor power. Omitted or zero selects 0.02; measured baseline noise can raise the threshold to six standard deviations. The total timeout defaults to 120000 ms and accepts 1000 through 300000. The former relay band/cycles arguments now select the response floor and number of step-response points.

One invocation tests one direction: increasing output from safe_output, or decreasing output if increasing output has no headroom or the measurement range excludes it. To characterize the opposite motor direction, use a separate run with output limits that permit only that direction. Output stays within the configured limits. Feedback outside the measurement range, with a noise allowance, stops the test; this is a monitored limit, not a guarantee against transient mechanical overshoot.

Use tune instead of start. The shell starts the controllers worker if needed for a bound controller. API callers start the job separately. Tuning claims the configured parameter, PWM output, or scalar stream sink and uses the normal timestamped stream or feed input. It makes no output write until a valid measurement is processed. Continue supplying fresh samples; cached timestamps do not count as new identification samples. The ordinary feedback timeout and max_dt checks remain active. Configuration and setpoint writes, including parameter/control/OSC writes, are rejected during tuning.

Each response estimates process gain, delay and time constant using its 10% and 63.2% crossings. Flat, unresolved, wrongly signed, or still drifting responses fail with a specific reason. The per-point PID candidates are averaged, then exercised privately at 80% of each measured speed change from baseline, leaving output headroom for corrections. Each target must track continuously for one second within the larger of twice the response floor and 5% of its change from baseline, within ten seconds per target. Validation uses the PID output directly, without command ramps or friction feedforward. Configured gains, setpoint and bias/ramp settings remain unchanged. Status shows phase, point counts, test setpoint, breakaway output, noise and fitted response data. A test that cannot produce motion within its output limit fails with no-motion-within-output-limit.

On completion, the service writes safe_output, releases the output, and reports candidate kp, ki, kd, and tau in status. Configured gains are never changed automatically. A failed final cleanup invalidates the candidate and retains the claim for stop to retry. stop aborts a running experiment and performs the same safe-output cleanup. Input, timing, or I/O faults stop updates and retain claims for explicit cleanup, as in periodic operation. Results are volatile and remain visible until reset, deletion, or another tuning attempt.

The step-response identification and IMC PID equations are adapted from Tortuga's PidTuner. The smoothing time is at least the fitted time constant and four sample intervals. The derivative filter is at least one sample interval or one tenth of the fitted delay. These are heuristic candidates; validation covers the tested targets and load, not every operating condition.

With no source or output binding, tune instead advances through controller step or solaros.controllers.step. Each call returns the next test output, or safe_output on completion. No background job or actuator write occurs. The caller supplies acquisition timing, applies the returned output, and stops the physical actuator on failure, abort, or completion. After this experiment ends, further step calls are rejected until reset or an explicit periodic start; this prevents an accidental return to the old PID loop.

To tune a robot wheel independently, stop the robot, configure the encoder and motor through their OS services, and bind a standalone controller as below. Use the same calibration, filtering and permitted drive/speed limits as the joint. Copy reviewed candidate kp, ki, kd and tau into the URDF, export and reload it, then explicitly start the robot. Tuning does not edit model files.

Standalone encoded motor

An attached motor and encoder can run closed-loop velocity control without the robot library or a script. Attachment defines wiring and counting mode; service configuration defines calibration and policy. For Solbot's already attached left motor and left_enc encoder:

encoder configure left_enc ppr 70
encoder configure left_enc gear_ratio 30
encoder configure left_enc polarity 1
encoder configure left_enc velocity_tau 0.1
motor configure left max_drive 0.2
motor configure left polarity 1
controller create left_pid pid
controller configure left_pid output_min -0.2
controller configure left_pid output_max 0.2
controller configure left_pid kp 0.04
controller configure left_pid ki 0.03
controller configure left_pid setpoint 0.3
controller bind left_pid enc.left_enc stream:mot.left
job start controllers
controller start left_pid
controller status left_pid
controller stop left_pid

The setpoint and feedback are output-shaft rad/s. Output is normalized motor drive, not rad/s. Both PID output limits must fit the motor's max_drive and motor bindings require safe_output=0. The encoder must have a positive PPR. Stop the robot first if it owns these devices.

For tuning, leave the controller stopped and use:

controller tune left_pid
controller status left_pid

The preceding 0.2 drive cap is an example control limit, not a useful tuning envelope for a motor whose breakaway output is already 0.2. Tuning needs permitted headroom above breakaway to measure distinct operating speeds. Choose that envelope for the assembly before running the experiment.

The controller job executes the experiment, stops output and releases the streams on completion. controller stop left_pid aborts it. Existing gains remain unchanged. No Python/Lua sampling loop is required.

acceleration_limit limits setpoint change per second; zero disables ramping. velocity_deadband maps small requested velocities to zero. Both default to zero. friction_bias_forward and friction_bias_reverse default to zero and supply continuous direction-dependent feedforward within the output limits. Feedforward participates in PID anti-windup. Zero command removes feedforward but continues feedback control. Manual output mode does not add feedforward.

command_timeout_ms=0 (default) keeps a standalone setpoint until it changes or the controller stops. A positive timeout requires fresh setpoint writes, including through controller.<name>.setpoint. Expiry resets the ramp/PID, stops output and enters waiting, while continuing feedback checks and retaining claims. A fresh setpoint resumes operation. It is distinct from timeout_ms, which limits feedback age. Both timeouts, when enabled, must cover the controller period. Relay tuning uses its own bounded experiment timeout instead of requiring repeated setpoint writes.

Periodic operation

Bind a measurement source and an output before enabling an instance:

controller bind <name> <stream>|manual parameter:<path>|pwm:<provider>:<output>|stream:<sink>
job start controllers
controller start <name>
controller status <name>
controller stop <name>
job stop controllers

start claims the output without writing a command. The job uses one worker for all instances, checks them approximately every 10 ms, and steps each at its configured period. period_ms defaults to 20 and accepts integer values from 10 through 60000. Actual elapsed time is used; missed periods are not replayed. This worker provides background control, without a hard real-time deadline guarantee. Fast peripheral control belongs in a dedicated driver or native application with suitable timing.

The worker accepts only scalar streams with an acquisition timestamp. Built-in ADC streams supply this timestamp. Other providers must implement the timestamped scalar-sample interface; ordinary scalar reads are not used as a substitute. The provider must honor bounded read requests and report when a cached sample was acquired, rather than when it was read.

Use manual as the source when a native application or script supplies measurements. controller feed <name> <measurement> timestamps a new measurement with current OS uptime. Feed a first measurement before starting the instance, and continue feeding within timeout_ms. Only use this command for a measurement acquired now. The C API also accepts the original monotonic acquisition timestamp for delayed delivery.

Measurements must be finite, must not move backward in acquisition time, and must not be in the future. A sample becomes stale after timeout_ms, which defaults to 200. This timeout must be at least the period. max_dt must also cover the period and limits the permitted interval between completed steps.

Outputs and ownership

stream:<sink> claims a scalar f32 sink exclusively for the controller's lifetime. stream:mot.<name> writes normalized signed motor drive through the motor service. Encoder sources enc.<name> remain open for the same lifetime, locking calibration and preventing reset/detach. Ordinary motor commands and competing output opens are rejected while claimed. Motor sinks check local feedback/command deadlines after status acquisition and before applying a new drive. Closing the sink coasts or brakes according to its stop policy. Failed closes retain handles for retry.

parameter:<path> writes a registered native parameter in its declared unit. Both output limits must fit the parameter's range. The controller claims the parameter exclusively: shell, scripting, OSC, and control-mapping setters cannot write it while claimed. Parameter quantization remains in effect. The algorithm reports its requested output; provider quantization or other downstream behavior may change the applied value.

Provider removal invalidates the claim. A replacement parameter with the same path cannot inherit it. Restart the instance explicitly after repairing the binding. Controller setpoint parameters cannot be used as controller output targets.

pwm:<provider>:<output> reserves an output through the PWM service. Its values are pulse widths in microseconds, rounded to whole microseconds. frequency_hz defaults to 50 and accepts 1 through 40000, subject to provider support. Output limits must be nonnegative and their rounded maximum must be shorter than the PWM period. Use pwm providers and pwm status to discover available endpoints. Existing PWM or servo reservations can prevent a controller from claiming an output. Calibrated servo angles are not output adapters in this service. Use a motor stream for normalized drive.

For an available GPIO2 and an ADC input on GPIO1, this example maps ADC millivolts to PWM pulse widths. Select gains, ranges, and frequency for the connected process:

controller create level pid
controller configure level output_max 1000
controller configure level kp 0.2
controller configure level ki 0.1
controller configure level setpoint 1500
controller configure level safe_output 0
controller bind level adc1 pwm:native:2
job start controllers
controller start level
controller status level
controller stop level
job stop controllers

Missing or stale input, invalid arithmetic, excessive elapsed time, or a failed output write latches a fault and stops automatic updates. The service attempts safe_output immediately and retains the claim. It does not restart automatically when a provider recovers. status reports the last error and fault count. The safe write can itself fail; the status cannot prove the physical output reached its requested value.

If closing a measurement stream fails, its handle and the output claim are retained so that stopping the controller can retry cleanup.

stop writes safe_output and then releases ownership. Closing a PWM output also disables its signal. A failed safe write or release retains the claim for a later stop retry. Stopping the job stops all enabled instances and attempts the same cleanup. Stop an instance successfully before deleting it or changing its bindings or tuning.

Configuration and inspection

controller configure <name> <key> <value>

Keys are setpoint, setpoint_min, setpoint_max, kp, ki, kd, tau, output_min, output_max, hysteresis, max_dt, reverse (0 or 1), safe_output, period_ms, timeout_ms, frequency_hz, command_timeout_ms, acceleration_limit, velocity_deadband, friction_bias_forward, and friction_bias_reverse. Period, timeouts and frequency use integer values; timeout_ms accepts 1 through 60000. Only setpoint can change while claimed. Limits default to 0 through 1, tau to 0.05 seconds, hysteresis width to 1, and safe output to 0. Safe output must lie within the limits before starting.

Each instance registers controller.<name>.setpoint in the parameter service, initially accepting values from -1e20 through 1e20 in the measurement's unit. Set setpoint_min and setpoint_max while stopped to define a useful range for normalized control bindings. The current setpoint must fit the new range. Shell, scripts, OSC, and existing controls can write this parameter. For example:

controller configure level setpoint_min 0
controller configure level setpoint_max 3300
control create level-knob adc2 0 3300 smooth=40
control bind level-knob parameter controller.level.setpoint
job start controls

This maps the knob's full travel to setpoints from 0 through 3300. Configure the range before starting level. Removing the instance removes its setpoint parameter. list and status expose configuration, mode, claim and fault state, measurement timestamp, output, error, P/I/D contributions, saturation, and step/fault counts.

Python and Lua

Both expose solaros.controllers with matching functions:

FunctionResult or behavior
create(name[, kind])Create pid (default) or hysteresis; return status.
list() / status(name)Return all status records or one record.
configure(name, key, value)Set one numeric option.
bind(name, source, target)Configure stream/manual input and an output adapter.
feed(name, measurement)Publish a measurement acquired now.
step(name, measurement, dt)Step a stopped instance; return its computed output.
manual(name, output) / auto(name)Select manual or automatic mode.
start(name) / stop(name)Claim and enable, or stop and release, the output.
tune(name[, noise_floor[, points[, timeout_ms]]])Start automatic standalone PID tuning; return status. Defaults: 0, 4, 120000.
reset(name) / delete(name)Reset or remove a stopped, unclaimed instance.
import solaros

solaros.controllers.create("test")
solaros.controllers.configure("test", "setpoint", 0.8)
print(solaros.controllers.step("test", 0.3, 0.02))
print(solaros.controllers.status("test"))
solaros.controllers.delete("test")

Lua uses the same names and positional arguments after local solaros = require("solaros"). Periodic instances belong to the OS service and continue after the script exits; stop them explicitly when the script owns their lifecycle.

Status records include tune_state (idle, running, complete, aborted, or failed), tune_reason, tune_phase, tune_point, tune_points, tune_test_setpoint, tune_drive, tune_breakaway, tune_noise, tune_process_gain, tune_dead_time, tune_time_constant, tune_elapsed, and candidate tune_kp, tune_ki, tune_kd, tune_tau. Candidate fields are valid only when tune_state is complete. Legacy tune_cycles/tune_required_cycles count identified response points; relay-only tune_period, tune_amplitude and tune_ultimate_gain remain zero.

Native C

solar_os_controller_math.h provides allocation-free configuration and state objects, reset, manual/automatic selection, and solar_os_controller_math_step(config, state, setpoint, measurement, dt). The library depends only on the C standard library. Callers own state, timing, output application, and fault handling. Invalid steps return false without changing state.

solar_os_controllers.h exposes the named service. Timestamped stream providers use solar_os_stream_scalar_sample_t with OS uptime in milliseconds. Output and stream callbacks invoked by this service must not re-enter it.

Quick reference

Use controller create name pid|hysteresis, configure the setpoint and tuning, then call controller step name measurement dt-seconds for manual stepping. Periodic instances bind a timestamped scalar stream or manual feed to an exclusive parameter, PWM pulse output, or scalar stream sink. For encoded motors, configure encoder and motor, then bind enc.<encoder> to stream:mot.<motor>. This path needs no robot library. Start job controllers, then start each instance. Faults latch and attempt the configured safe output. Stop instances to release claims; failed cleanup is retryable. Python and Lua expose the same operations under solaros.controllers. Native applications can use the allocation-free controller math library directly. Component-owned PID instances share this registry; inspect their owner and waiting fields and configure/release them through their owner's API. Use controller tune name [noise-floor [points [timeout-ms]]] on a stopped standalone PID to report candidate gains without applying them. Inspect results with status, abort with stop, and use reset before resuming manual stepping. Robot-owned controllers reject tuning; copy reviewed standalone results into the URDF and reload it.

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)
    try:
        solaros.gpio.write(pin, 1)
    finally:
        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 providers, status, set(output, frequency, duty_percent), pulse(output, frequency, pulse_us), and off. Outputs use provider:number; integers select native GPIOs. Providers enforce their shared-frequency constraints. For calibrated positional outputs, use the servo service. APIs are package-gated.

Hardware and expansion · SolarOS manual

Motor control

The motor service exposes named motor endpoints through the shell, Python, Lua, and C. Drivers register endpoints with this service, so applications use the same API across controllers. The current hardware backend is DRV8830.

Select the motor package group for the common service and the drv8830 group for that controller. Selecting a driver also includes its required service. Both groups are included in the full flavor where supported. motor list reports each endpoint's driver and drive, brake, and clear capabilities. Optional operations return an unsupported-operation error when the provider does not implement them.

Quick reference

An active native consumer can claim a motor output exclusively. While claimed, ordinary drive, coast, brake and clear commands and device detach are rejected. Status remains available. The owning consumer must stop and release the output; failed cleanup retains the claim for retry. See Robot axes.

expansion attach drv8830 motor0 i2c=i2c0 addr=0x60 max_mv=3000
motor list
motor status motor0
motor drive motor0 0.5
motor drive motor0 -0.5
motor coast motor0
motor brake motor0
motor clear motor0
expansion detach motor0

Normalized control

drive takes a finite value from -1.0 through 1.0. The sign selects direction; the magnitude requests a fraction of the backend's configured full-scale output. Zero coasts. This value is a control demand, not a measured speed, torque, voltage, or universal PWM duty cycle. Each driver maps it to its hardware and reports the quantized applied command when available.

Values outside the range, NaN, and infinity are rejected. brake requests active braking when supported. coast releases the bridge output. clear is an explicit fault-recovery operation that leaves the motor coasting. Commands remain active after the shell or API call returns. Neither coast nor brake switches off the motor supply.

Parameters and command stream

Attachment publishes the exclusive scalar sink mot.<name> and parameters mot.<name>.polarity, .max_drive and .brake. Configure these through motor configure <name> <key> <value> or the parameter service while unclaimed. Defaults are polarity +1, maximum drive 1 and coast on stream close. Configuration changes first coast the motor; a failed stop leaves configuration unchanged. Polarity maps logical drive to physical direction. Maximum drive limits accepted demand before backend quantization.

The sink accepts normalized signed drive, not velocity. Opening it claims the motor and locks configuration until a successful close. Direct commands and competing opens are rejected while claimed. Stream zero/close requests use the configured coast/brake policy; raw motor drive ... 0 continues to mean coast. Scalar payloads are native float; OS feedback controllers also supply a local monotonic deadline, checked after status acquisition and before actuation. See Feedback controllers for binding and tuning.

Python and Lua

Both runtimes expose solaros.motor when the motor service is compiled:

  • list(): return motor metadata records.
  • status(name): read one named motor's state and diagnostics.
  • drive(name, value): apply a normalized signed command.
  • coast(name): release the motor output.
  • brake(name): request active braking, when supported.
  • configure(name, key, value): set service polarity, maximum drive or stop policy.
  • clear(name): coast and clear driver-supported fault latches.

Names are required, including when only one motor is present. Successful control calls return no value; service errors raise runtime errors.

Metadata records contain name, driver, can_drive, can_brake, and can_clear. Status records also contain:

FieldMeaning
modeunknown, coast, forward, reverse, or brake.
drive_valid, driveWhether the applied normalized command is known, and its value. This is not measured shaft motion.
faultsNormalized present or recorded fault conditions.
blocking_faultsConditions that currently prevent drive. Recorded history can remain after recovery.
fault_input_present, fault_input_assertedAvailability and sampled assertion state of an optional hardware fault input.
voltage_valid, voltage_mvOptional nominal voltage-setting diagnostic, in millivolts. This is not a measured output voltage.
raw_valid, raw_control, raw_faultOptional driver-specific register diagnostics. Their bit layout is not part of the common motor contract.

Both modules export normalized fault-mask constants FAULT_UNKNOWN, FAULT_OVERCURRENT, FAULT_UNDERVOLTAGE, FAULT_OVERTEMPERATURE, and FAULT_CURRENT_LIMIT. Test these against faults or blocking_faults; do not interpret driver register bits as common fault masks.

Python, after attachment:

import solaros

print(solaros.motor.list())
try:
    solaros.motor.drive("motor0", 0.5)
    print(solaros.motor.status("motor0"))
finally:
    solaros.motor.coast("motor0")

Lua, after attachment:

local ok, err = pcall(function()
    solaros.motor.drive("motor0", -0.5)
    local state = solaros.motor.status("motor0")
    print(state.mode, state.drive, state.blocking_faults)
end)
solaros.motor.coast("motor0")
if not ok then error(err) end

DRV8830 backend

One controller provides one motor endpoint. Multiple controllers can share a named I2C bus at distinct seven-bit addresses, 0x60 through 0x68. Connect SDA/SCL to that bus, share ground, and supply the controller and motor according to their ratings. The bus must run at 400 kHz or less.

Optional max_mv=482..5060 sets the backend's full-scale voltage limit; the default is 5060 mV. It is rounded down to a supported VSET code. A normalized magnitude of one selects that code. Other magnitudes select the nearest fraction of that code. Codes below the minimum valid VSET code 6 map to coast. With the default full scale, commands whose magnitude is below approximately 0.0873 coast; the first nonzero applied command is 6/63, approximately 0.0952. Quantization and this deadband are backend-specific.

The chip internally regulates the average voltage across the motor winding. If the target exceeds the available supply, it saturates at full duty. SolarOS reads the programmed setting, not a motor or supply-voltage measurement. Current limiting is configured by the external sense resistor.

FAULTn is optional. Add int=<available-native-gpio> when wired; this is an active-low, open-drain signal. Use a pull-up compatible with the ESP32 input; the driver also enables the native input pull-up. SolarOS claims the address and optional GPIO; the named bus owns SDA/SCL. The fault register also works without the interrupt wire. The input is sampled on status and drive calls; there is no asynchronous callback.

Python and Lua attachment dictionaries/tables accept i2c, addr, optional integer int, and optional integer max_mv. For example, Python can attach with:

solaros.expansion.attach("drv8830", "motor0",
                        {"i2c": "i2c0", "addr": 0x60, "max_mv": 3000})

Attach and successful detach coast the bridge. Reversal passes through standby. Active FAULT, a latched overcurrent or extended-current-limit condition, or an asserted fault input blocks drive. Recovered undervoltage and overtemperature history remains visible without requiring clear. A failed control write attempts to coast. A communication failure cannot guarantee a physical stop. Failed detach retains the service registration and expansion resource claims for retry.

Electrical behavior and register diagnostics follow the TI DRV8830 datasheet.

C providers and consumers

Applications include solar_os_motor.h and use solar_os_motor_get(), solar_os_motor_status(), solar_os_motor_drive(), solar_os_motor_coast(), solar_os_motor_brake(), and solar_os_motor_clear().

Drivers register one endpoint per motor channel using solar_os_motor_register(). Status and coast callbacks are required; normalized drive, brake, clear, and shutdown callbacks are optional. Callbacks are serialized per endpoint and execute outside the registry lock. Providers own their hardware mapping, initial coast state, and fault policy. Successful unregister stops the motor before allowing the driver to release its context. In-flight operations and failed shutdown prevent unregister and preserve the endpoint for retry.

Hardware and expansion · SolarOS manual

Robot joints

The optional robot native library provides rotary servo-position and motor/encoder-velocity joints through the shell, Python and Lua. Velocity joints configure the OS encoder, motor and controller services; standalone controllers can use those services without the robot library. Previous device settings are restored on disable or stop. Robot joints work without URDF or a display. The same library also provides URDF models, transforms and differential drive. Native libraries require ESP32-S3, executable PSRAM and the native-module service.

An expanded URDF can also declare motor/encoder velocity controllers in its solar_os block. Load the model, then use robot start to create and enable those controllers and robot stop to close them. They use the same calibration and command watchdog as manually created joints. See model controller bindings.

Enabled motor joints appear under their joint names in controller list and controller status <joint>, with owner=robot. Their controller state and gains are also visible through solaros.controllers. Configure them through the joint API or URDF while disabled. robot stop removes the model's controller instances after hardware cleanup succeeds. A failed stop retains the instance for retry. controller stop <joint> stops and releases the bound endpoints and causes a joint fault. Recover with joint disable/enable, or model stop/start. Servo-position joints use the OS servo service and do not create a PID instance.

Install robot from a compatible signed module catalog before creating a joint. Creating the first joint loads the installed library on demand; it does not download code. Loading, creating, configuring and binding do not energize outputs. enable explicitly claims hardware and starts control. Python/Lua bindings are included on supported native-module builds.

Calibration

Each joint declares this configuration, applied through shared OS services:

  • ppr: A-channel pulse periods per encoder-shaft revolution, independent of decoding. Configure this explicitly for motor joints.
  • gear_ratio: motor/input revolutions per output/joint revolution. Configure this explicitly for motor joints, including 1 for direct drive.
  • motor_polarity: +1 or -1, mapping positive joint demand to actuator direction.
  • encoder_polarity: +1 or -1, mapping encoder counts to positive joint motion.
  • encoder_on_output: 0 for a motor/input-shaft encoder (default), 1 for a gearbox-output encoder.
  • position_zero: joint coordinate in radians at the initial reference.

The encoder provider reports its actual x1/x2/x4 decoding factor. Counts per joint revolution are ppr * decoding * gear_ratio for a motor-shaft encoder, or ppr * decoding for an output-shaft encoder. Do not enter already decoded counts or output-shaft counts as PPR. Gearbox ratio and quadrature decoding are applied once. Both polarities must be checked for the mounted mechanism; they are independent of each other and of the positive direction in a driver.

encoder_counts_per_rev is read-only status derived from these parameters. It cannot be configured independently. The value remains pending until the encoder's decoder mode has been sampled. For example, PPR 70, x1 decoding and a ratio of 30 derive 2100 counts per joint revolution. A manually measured full revolution validates this result; correct PPR, ratio or encoder placement when the measurement disagrees with it.

Velocity uses acquisition timestamps and radians per second. Signed 32-bit encoder wrap is accumulated in a 64-bit relative count, assuming fewer than 2^31 counts between samples. velocity_tau is the first-order low-pass time constant in seconds; 0 disables filtering. The default is 0.1 seconds.

Motor joints expose relative displacement since enable in position, but position_valid and referenced remain false because there is no homing or absolute position reference. URDF-bound continuous joints use this relative position for live model transforms while timestamped feedback is valid; bounded model joints still require a referenced position. Servo joints expose commanded_position; their measured position and velocity remain invalid. Motor drive is normalized demand, not torque or measured effort.

Shell

Joint operations live under robot joint.

robot joint list
robot joint create <name> velocity|servo
robot joint configure <name> <key> <value>
robot joint motor <name> <motor> <encoder>
robot joint servo <name> <provider> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz]
robot joint command <name> <target>
robot joint enable <name>
robot joint status <name>
robot joint disable <name>
robot joint close <name>

For an encoder calibrated at 70 A-channel periods per shaft revolution and a 30:1 gearbox, prepare a joint using already registered devices:

robot joint create wheel velocity
robot joint motor wheel wheel_motor wheel_encoder
robot joint configure wheel ppr 70
robot joint configure wheel gear_ratio 30
robot joint configure wheel motor_polarity 1
robot joint configure wheel encoder_polarity 1
robot joint configure wheel max_drive 0.2
robot joint status wheel

This only configures the joint. enable first claims the motor, applies the configured coast/brake behavior, claims the encoder reference, acquires a baseline sample and starts the loop. Without a fresh queued command, it keeps the configured coast/brake behavior while waiting for the first command. Feedback and device fault monitoring continue, but the command watchdog is not armed and the joint does not regulate velocity yet. An expired queued velocity command is discarded on enable. The first fresh command arms the watchdog, including an explicit zero command. Refresh targets in radians/second before their expiry. disable stops control and releases the devices after applying the configured stop behavior. close disables and destroys the joint.

For a positional servo, servo specifies its input-shaft calibration. The adapter converts joint position to servo-shaft angle as motor_polarity * (target - position_zero) * gear_ratio. Supply a fresh position command in radians before enabling. Servo enable reserves PWM without pulses; the first control update commands the position. Measured joint position is unavailable without a feedback sensor. Disabling closes PWM and stops the signal; it does not remove actuator power.

Control and lifecycle

Velocity joints use the OS PI/PID controller service with calibrated encoder feedback, setpoint ramping and normalized motor output. motor_polarity is applied after the controller; encoder_polarity is applied to feedback. Saturation, anti-windup and filtered derivative use the common controller implementation. Joint control runs independently of job controllers, which can run other controller instances alongside it. Timing depends on device access and OS scheduling, without a hard real-time guarantee.

Motor claims exclude ordinary drive, coast, brake and clear writes and prevent detach. Encoder reference claims permit shared reads but prevent reset and detach. Servo outputs reserve their PWM output until disabled.

Command expiry stops the output, resets the controller and enters waiting while retaining device claims and feedback monitoring. A fresh command resumes control immediately; disable/enable is not required. A failed stop latches a fault instead. Failed/stale/nonmonotonic feedback, excessive scheduling gaps, blocking motor faults, arithmetic failure and output failures latch a fault. The worker attempts the configured stop behavior and exits. Claims remain held until disable or close completes cleanup. Failed stopping retains both motor and encoder claims for retry. A failed stop command does not prove the physical actuator stopped. Repair the cause and disable successfully before re-enabling.

Python/Lua-owned joints are closed on interpreter teardown. Failed cleanup keeps the joint and its library lease available for shell inspection and retry. Shell-owned joints persist until explicitly closed or restart. After the first command, expiry also applies when a script exits or a transport disconnects. Library unload is refused while a joint still holds a lease.

Configuration and state

Configuration is accepted only while the joint owns no hardware and has no worker. Changes invalidate any queued command. Keys include ppr, gear_ratio, motor_polarity, encoder_polarity, encoder_on_output, position_zero, position_min, position_max, velocity_limit, acceleration_limit, max_drive, velocity_tau, friction_bias_forward, friction_bias_reverse, velocity_deadband, kp, ki, kd, tau, period_ms, command_timeout_ms, feedback_timeout_ms, max_gap_ms and brake. brake=0 selects coast (default); brake=1 requires a brake-capable motor.

Defaults are a 20 ms period, 300 ms command timeout, 100 ms feedback timeout, 200 ms maximum scheduling gap, 10 rad/s velocity limit, 10 rad/s² setpoint acceleration limit and 0.2 maximum normalized drive. PI defaults are kp=0.02, ki=0.01, kd=0, with derivative filter tau=0.05 seconds. These are initial settings, not gains tuned for a particular mechanism. Configure both PPR and gearbox ratio before enabling a velocity joint. Unconfigured motor joints report zero for PPR and gearbox ratio; zero cannot be configured as a valid PPR or ratio. A direct drive using PPR-based conversion still requires an explicit ratio of 1.

Velocity targets use radians/second; they are not normalized motor-drive commands. kp converts velocity error to normalized drive, and ki integrates that error over seconds. For example, a stationary joint commanded to 0.5 rad/s with kp=0.02 initially requests only 0.01 proportional drive. Such a request can fall below the driver's output deadband or the mechanism's breakaway drive. The default gains do not guarantee rotation for a particular motor. Configure gains while disabled, keep a bounded max_drive, and verify encoder motion and overshoot before using the joint continuously.

The command timeout defaults to 300 ms and starts on the first command. For interactive shell checks, configure command_timeout_ms while disabled to allow enough time for typing. A target is valid only for that duration; continuous operation requires refreshed commands from a script or another client. After ordinary command expiry, a fresh command resumes control from the stopped state. Configuration changes still require disable. Hardware, feedback and scheduling faults require successful disable/recovery before re-enabling. status reports the command timeout, whether the watchdog is armed, and the recovery command.

The joint owns actuator calibration and the velocity-command deadband. friction_bias_forward and friction_bias_reverse are nonnegative normalized feedforward magnitudes for joint-positive and joint-negative motion, each at most max_drive. Both default to zero. The sign of the ramped velocity setpoint selects a continuous drive bias; a zero setpoint adds none. Calibrate this bias while the mechanism is rotating at the intended speed and load. Feedback may reduce or cancel it. PID output bounds account for compensation so the final motor command respects max_drive and anti-windup remains effective. Polarity is applied after summing these terms.

Breakaway drive is the normalized actuator command at which a stationary mechanism begins to rotate. For example, a breakaway drive of 0.15 means rotation starts at about 15% of full-scale drive in that direction. This is an observed threshold; it does not measure power, torque, or the drive required to sustain rotation. It can differ by direction and load. There is no separate breakaway threshold or startup boost. A breakaway observation alone does not calibrate friction_bias_forward or friction_bias_reverse.

velocity_deadband is a command tolerance in rad/s, default zero and strictly less than velocity_limit. Commands with absolute value at or below this tolerance become a zero target before ramping. Encoder feedback is never zeroed by this tolerance. A zero velocity target remains closed-loop and may apply corrective drive; use disable to stop and release the motor. Mechanical backlash and servo pulse deadband are not compensated.

Periods accept 10 through 1000 ms. Timeouts and maximum gaps accept the period through 60000 ms. PPR, ratio, velocity and acceleration limits must be positive; maximum drive is greater than zero and at most 1. Position limits are radians and constrain servo targets. Velocity targets outside velocity_limit are rejected. This version does not control bounded-travel motor position or provide trajectory planning, homing, force control, or torque measurement.

status reports configuration, state (ready, active, waiting, fault), claims, command expiry, acquisition time, raw count, decoding, validity, target, rate-limited setpoint, velocity, relative position, commanded servo position, last output command, friction feedforward, P/I/D contributions, saturation and counters. The library error uses solar_os_native_service_result_v1_t, with zero for success.

Python and Lua

Both runtimes expose solaros.robot:

FunctionBehavior
joints() / joint_status(name)Return all records or one record.
joint_create(name[, kind])Create velocity (default) or servo; return status.
joint_configure(name, key, value)Set one numeric option while inactive.
joint_bind_motor(name, motor, encoder)Select named devices for a velocity joint.
joint_bind_servo(name, provider, output, min_us, max_us, min_rad, max_rad[, frequency_hz])Set input-shaft servo calibration; frequency defaults to 50.
joint_command(name, target)Refresh a velocity or position command.
joint_enable(name) / joint_disable(name)Activate or stop and release hardware.
joint_close(name)Stop, release and destroy a joint.
import solaros

solaros.robot.joint_create("wheel")
try:
    solaros.robot.joint_bind_motor("wheel", "wheel_motor", "wheel_encoder")
    solaros.robot.joint_configure("wheel", "ppr", 70)
    solaros.robot.joint_configure("wheel", "gear_ratio", 30)
    solaros.robot.joint_enable("wheel")
    for _ in range(10):
        solaros.robot.joint_command("wheel", 1.0)
        solaros.time.sleep_ms(50)
    solaros.robot.joint_disable("wheel")
finally:
    solaros.robot.joint_close("wheel")

This example commands movement and assumes the specified calibration matches the connected mechanism. Lua uses the same names and arguments after local solaros = require("solaros").

Native API and build instructions are in the robot library README.

Quick reference

Install the optional robot library, create a joint, configure PPR, gearbox ratio and polarities, and bind named motor/encoder or calibrated servo outputs. Counts per joint revolution are derived and exposed as read-only status. Enable explicitly and refresh commands within their timeout. Expiry stops output and waits for a fresh command; hardware and feedback faults require recovery. Status distinguishes commanded, relative and measured quantities. Disable stops outputs before releasing hardware; failed cleanup is retryable. Close joints before unloading the library. Enabled motor joints also appear in controller list under their joint names. Python and Lua expose the same operations in solaros.robot.

Hardware and expansion · SolarOS manual

Robot models and transforms

The optional robot native library loads expanded URDF into a link/joint tree. It supplies rigid-transform math, explicit joint-position updates, forward kinematics, and frame-to-frame point and direction conversion. It works without connected motors, controllers, ROS, or a display. Install a compatible robot library from a signed module catalog before loading a description.

Author the description in a desktop ROS 2 package if convenient. Expand Xacro on the desktop and copy the resulting URDF to the device:

xacro solbot.urdf.xacro -o solbot.urdf

The device reads the flat XML file. It does not run Xacro, resolve ROS package paths, or load mesh files. Visual, collision, inertial, material, transmission, Gazebo and ros2_control elements are accepted as metadata and omitted from the in-memory kinematic model. This interface does not render or simulate geometry.

Device frames

Associate an OS device with a URDF link in the existing extension:

<link name="imu_link"/>
<joint name="imu_joint" type="fixed">
  <parent link="base_footprint"/>
  <child link="imu_link"/>
  <origin xyz="0 0 0.02" rpy="0 0 0"/>
</joint>
<solar_os version="1">
  <device name="imu0" frame="imu_link"/>
</solar_os>

The example pose is a temporary frame 20 mm above base_footprint with zero rotation. Replace it with the physical mounting pose when the IMU is mounted. Use one solar_os block per robot, adding device bindings alongside joint and drive declarations. Device names must be unique; frames must name existing links. Up to 16 devices can be associated, and several devices can share a frame.

expansion attach mpu6050 imu0 i2c=i2c0 addr=0x68
robot model load solbot.urdf
robot model devices
robot model frame imu0
robot model frame imu.imu0
robot model transform base_footprint imu_link
imu sample imu0
stream echo imu.imu0 --once

Frame associations are queryable after model load and require no robot start. Loading does not attach drivers, claim sensors, or start sampling. Unattached devices may be declared so geometry is usable without hardware. A device-name query works while detached; a stream-name query resolves a registered stream's device. Camera, IMU and future range devices use the same binding syntax. Samples remain in the sensor's local coordinates, with no frame ID added to the source. Consumers resolve the binding and apply the existing model transform; the service does not rotate samples or use measured attitude to move the frame. Dynamic paths require current joint state, as other transform queries do.

Python and Lua expose solaros.robot.model_frame(device_or_stream), which returns the frame name. Pass it to solaros.robot.model_transform(target, frame) for the transform. C clients use solar_os_robot_model_frame() in the OS frontend or the native library's appended model_device / model_device_frame API entries. Older robot libraries can still load models but do not support frame lookup; update the library to use these entries.

Load and inspect

robot model load solbot.urdf
robot model info
robot model links
robot model joints

Tab completes model subcommands and the path after model load. With a model loaded, it also completes both frame arguments of transform, point and vector, and movable joint names in each joint/value pair after model set. Fixed joints and joints already supplied in the batch are omitted from set suggestions. Completion does not load a model or acquire hardware.

Loading parses and validates the document before replacing the resident model. It checks unique link and joint names, parent/child references, a single root, connectivity, cycles, duplicate fields, supported joint types, finite numeric values, axes and limits. A failed read or parse leaves the previous model intact. XML errors identify the line, column and offending field, for example:

line 6, column 5: Unknown param attribute 'valule' on <param name='kd'>

Here the attribute must be value. Loading does not create controllers, attach devices, claim hardware or enable outputs. Existing independent joint controllers keep running.

There is one device-wide resident model shared by the shell, Python and Lua. Loading another valid model replaces it and clears the old model's joint states. The model remains available until explicit unload:

robot model unload

Ending a script closes its hardware joints but leaves the shared model available. Unload the model before unloading the robot library.

Joint states and forward kinematics

Fixed joints have known positions immediately. Continuous, revolute and prismatic joints initially have unknown positions. An unset joint is not assumed to have a measured zero position.

After robot start, URDF-bound continuous joints use measured encoder positions automatically. Transform, point and vector queries and model inspection all use the latest joint feedback. Positions are radians relative to the encoder count at enable, plus the configured position_zero; this does not reset the hardware counter or establish an absolute wheel angle. The first valid sample makes the position available even while the controller waits for its first command. Queries do not command movement.

Missing, stale or faulted feedback makes the bound position unavailable. Stopping the joint also clears its model position; restarting establishes a new relative reference. Revolute and prismatic joints require referenced position feedback; relative encoder counts alone cannot establish their absolute pose. A servo command is not measured position feedback.

Supply model state explicitly for unbound joints or while the robot is stopped, for example:

robot model set left_wheel_joint 0 right_wheel_joint 0
robot model transform base_footprint left_wheel_link
robot model set left_wheel_joint 1.57079632679
robot model transform base_footprint left_wheel_link

Rotary positions are radians; prismatic positions are metres. Bounded joints require valid URDF limits. Supplied state may exceed those limits so the model can still describe an actual mechanism beyond its permitted travel. Control components own command-limit enforcement. Continuous joints accept finite unwrapped angles. A set command validates every name and value before updating any joint. Duplicate names in one batch are rejected. These updates describe a pose; they never command a motor or servo. Manual updates to runtime-bound joints are rejected so they cannot replace measured state. Stop the robot before supplying a manual pose for those joints.

URDF origins use Rz(yaw) * Ry(pitch) * Rx(roll). Joint motion follows the origin in the joint's local axis. The axis defaults to +X and supplied nonzero axes are normalized. Transform queries walk the path between the requested links. Unknown joints on that path return an invalid-state error naming an affected joint in the shell. Unknown joints above their common ancestor do not prevent a local lookup. A frame-to-itself lookup returns identity even when that frame's pose relative to the root is unknown.

This is current-state forward kinematics. There is no transform history, timestamp interpolation, ROS topic publication, external-frame insertion or inverse solver in the model interface. The separate diff_drive component below supplies measured planar odometry.

Transform direction

The first frame is the target; the second is the source. A lookup returns T_target_source, mapping coordinates expressed in source into target:

robot model transform base_link left_wheel_link
robot model point base_link left_wheel_link 0 0 0
robot model vector base_link left_wheel_link 1 0 0

point applies rotation and translation. vector applies rotation only. Transform output contains translation in metres and a normalized quaternion in x y z w order. Composition A * B applies B first, then A.

For the supplied Solbot model at zero wheel angles, the left wheel centre is [0, 0.0350641, 0] in base_link, and the caster centre is [-0.0747, 0, 0.006] in base_footprint. These are nominal model coordinates.

SolarOS extension

One optional <solar_os version="1"> child of <robot> declares rotary motor velocity controllers and components alongside the standard model:

<solar_os version="1">
  <joint name="left_wheel_joint" command="velocity">
    <motor device="left" polarity="1"/>
    <encoder device="left_enc" ppr="70" placement="motor" polarity="1"/>
    <transmission gear_ratio="30"/>
    <param name="kp" value="0.04"/>
    <param name="ki" value="0.03"/>
    <param name="kd" value="0"/>
    <param name="max_drive" value="0.2"/>
    <param name="velocity_limit" value="3"/>
    <param name="command_timeout_ms" value="150"/>
  </joint>
</solar_os>

Add a matching right-wheel declaration with devices right and right_enc. The loader validates the tree, joint references, controller parameters, calibration, duplicate declarations and exclusive motor/encoder bindings. It retains the extension's XML verbatim for inspection:

robot model metadata

Loading creates no controllers and claims no hardware. Start them explicitly:

robot model load /sdcard/solbot.urdf
robot start
robot joint list
robot stop
robot model unload

robot start creates, configures and enables the declared controllers and components. Device availability is checked at startup. Motor joints create named PID instances in the OS controller service. Inspect them with controller list or controller status <joint> (owner=robot). Calibration and bindings apply to the OS encoder, motor and controller services. Their previous settings are restored before release; failed cleanup retains the settings/claims for a stop retry. Outputs stay stopped until fresh commands arrive; the configured command watchdog applies. A startup failure closes every controller created by that attempt. If a device stop or lease release fails, ownership is retained: retry robot stop before starting, replacing or unloading the model.

robot stop releases only model-owned controllers and leaves the model loaded. Separately created joints are unaffected. Commands, status and disable remain available through robot joint; model-owned joints must be stopped together before changing bindings or configuration. They survive interpreter cleanup.

Each binding requires command="velocity", a motor, an encoder, positive PPR and an explicit gear ratio. Encoder placement is motor (default) or output; decoder factor comes from the actual provider. Optional <param name="..." value="..."/> elements use the existing robot joint configure keys. Defaults match manually created joints. Parameters are validated together, in any order; duplicate keys, unknown keys/attributes and malformed values are rejected. Do not repeat calibration or polarity as both an attribute and a parameter. Up to eight bindings are allowed; bound joint names must fit 23 bytes.

Verify example calibration, gains and polarities for the mounted hardware. Servo controllers remain available through the manual joint API; position and prismatic bindings are unsupported here.

Differential drive

Declare one drive after its two velocity-controller bindings:

<diff_drive name="base" left_joint="left_wheel_joint" right_joint="right_wheel_joint">
  <geometry wheel_radius="0.0145" wheel_separation="0.0701282"/>
  <odometry frame="odom" base_frame="base_footprint"/>
  <param name="command_timeout_ms" value="300"/>
</diff_drive>

Place this block inside <solar_os version="1">. One drive is supported per model. Loading validates geometry, controller references and frame names. Both wheels must have velocity bindings; a wheel cannot belong to another active drive. robot start activates the drive after enabling its joints. It starts waiting for a body command. robot stop stops both outputs, releases the joints, and removes the component streams and parameters. Close stream readers and writers before stopping. If cleanup fails, close those handles and retry robot stop.

Commands use forward linear X velocity in m/s and left-turn angular Z velocity in rad/s. Wheel commands are (linear - angular * separation / 2) / radius and (linear + angular * separation / 2) / radius, in joint rad/s. If either wheel would exceed its velocity limit, both commands are scaled together to preserve curvature. Existing joint PID, acceleration, polarity and drive limits still apply. Direct commands/disable on a drive-owned wheel are rejected; use the body-command streams or robot stop.

stream status diff_drive.base.cmd_linear
control parameter get diff_drive.base.linear_velocity_limit
stream write diff_drive.base.cmd_linear 0.02 diff_drive.base.cmd_angular 0 --rate 20 --duration 2
stream echo diff_drive.base.odom_x --once

This publishes both body-command axes at 20 Hz for two seconds. Without --rate, stream write updates each axis only once. Commands expire after the configured timeout; 300 ms is the default. Ending publishing sends no extra value, and the drive stops when its command timeout expires. A timeout pauses both wheel controllers with output zero, retaining ownership and feedback. Fresh body commands resume operation. A wheel feedback/control fault stops the pair and latches a drive fault; stop/start the robot to recover.

Two float32 command streams are published:

StreamUnit
diff_drive.base.cmd_linearm/s
diff_drive.base.cmd_angularrad/s

Both are duplex: writing supplies a command, and reading returns the last accepted command. Readback is not measured motion; use the odometry streams below for feedback. Each accepts one writer and any number of observers. stream status shows each command's allowed range; an out-of-range stream write reports the rejected value, range and unit without renewing the command timestamp. Both axes must have fresh accepted samples before wheel commands are produced. Each axis has its own acquisition timestamp: publishing linear repeatedly does not renew a stale angular command. Publish both periodically, as in the example above. Opening or closing a writer does not itself activate output. Invalid values do not renew freshness. These two scalar inputs are not an atomic vector transport: a writer can briefly combine a new axis with the previous fresh axis.

Owned parameters appear under diff_drive.base: wheel_radius, wheel_separation, linear_velocity_limit, angular_velocity_limit and command_timeout_ms. Change them in URDF, then stop/load/start. Optional velocity-limit params use m/s and rad/s. Without them, body limits are derived from the smaller wheel velocity limit and the geometry. For example, a wheel radius of 0.0145 m and a wheel velocity limit of 4 rad/s give a default linear limit of 0.058 m/s. control parameter get and set complete registered paths; robot-owned configuration rejects direct changes while the component exists. Streams and parameters are created on start, not model load. Component names fit 23 bytes.

Odometry uses measured wheel displacement with the configured encoder scaling and polarities. Absolute joint referencing is unnecessary. Both wheel samples must advance before an update; their average acquisition time timestamps the result. Stale or invalid wheel feedback faults the drive. Pose integration follows the circular arc between samples, with +X forward and counterclockwise yaw. Each start resets pose to zero using the current wheel positions as its baseline.

Read-only odometry streams share that acquisition timestamp:

Stream suffix under diff_drive.baseUnit
odom_x, odom_ym
odom_yawrad
odom_linearm/s
odom_angularrad/s

The scripting status API includes frames, pose, measured velocities, validity and sample time. The default odometry frame is odom; the base defaults to the URDF root. The base frame should describe the axle-centred, forward-facing body reference. These outputs provide a planar odom-to-base estimate for a bridge; they do not insert an external frame into robot model transform, publish ROS messages, estimate covariance or compensate wheel slip.

Python and Lua

Both runtimes expose these functions on solaros.robot:

FunctionResult
model_load(path)Load a URDF file; return model info. Use an absolute device path.
model_unload()Close the shared model and release its lease.
model_info()Name, root, link-name list, joint records, known-joint count and extension size.
model_set_joint(name, position)Supply one model joint position.
model_transform(target, source)translation and rotation arrays; quaternion order is x,y,z,w.
model_point(target, source, x, y, z)Three coordinates after rotation and translation.
model_vector(target, source, x, y, z)Three coordinates after rotation only.
start()Start the loaded model's controllers and components.
stop()Stop model-owned controllers and components; retain the model.
diff_drive_command(name, linear, angular)Update both body-command axes atomically, in m/s and rad/s. Rejects competing writes while either command stream has a writer.
diff_drive_status(name)Configuration, state, errors, body commands, wheel targets, measured pose and velocity, validity and sample timestamps.

Joint records include name, parent, child, numeric kind, position and position_known. Kinds are 0 fixed, 1 revolute, 2 continuous and 3 prismatic. Python returns lists/dictionaries; Lua returns tables with 1-based arrays.

import solaros

model = solaros.robot.model_load("/solbot.urdf")
solaros.robot.model_set_joint("left_wheel_joint", 0.0)
pose = solaros.robot.model_transform("base_link", "left_wheel_link")
centre = solaros.robot.model_point("base_link", "left_wheel_link", 0, 0, 0)
solaros.robot.model_unload()

Lua uses the same names after local solaros = require("solaros").

URDF limits

Install compatible firmware and a matching robot library to use model and component commands.

Limits are 128 KiB XML, 64 links, 63 joints, 63-byte names, 32 XML nesting levels, 24 attributes per element, 255-byte decoded attribute values, and 32 KiB retained SolarOS XML. Oversized input is rejected rather than truncated. The parser accepts UTF-8, XML comments, processing instructions and predefined/numeric entities. DTD and external entities are unsupported. Floating, planar and mimic joints are explicitly unsupported. The model uses float transform math and has no dependency on ROS libraries. Native API details are in the robot library README.

Quick reference

Load expanded URDF with robot model load <file.urdf>. Inspect the model and supply joint positions with robot model set before querying transforms. robot start activates declared controllers and components; robot stop releases them while retaining the model. Publish component commands through streams, using stream write <stream> <value> [...] --rate <Hz> for periodic updates. Both differential-drive command axes must stay fresh. Read odometry with stream echo. Stop the robot before replacing or unloading its model.

Hardware and expansion · SolarOS manual

Servo outputs

The servo service binds calibrated positional servos to named PWM outputs. It is included in the full flavor and can be selected through the servo package group in other flavors. Use pwm providers to discover controllers and pwm status to discover their outputs.

Outputs use provider:number. The built-in ESP32 provider is native; its output numbers are runtime-safe GPIO numbers, so native:1 selects GPIO1. The pwm group includes this backend on boards with PWM and GPIO support. Expansion drivers can register additional PWM providers. Their output numbers identify controller channels rather than ESP32 pins. A bare GPIO number is accepted as shorthand for a native output.

Each named servo has explicit pulse and angle endpoints. Choose these values from the servo's specification and the mounted mechanism's usable range. All servo angles use radians: calibration, commands, status, streams and the C/Python/Lua APIs. Pulse widths use microseconds and frequency uses hertz. The default frequency is 50 Hz; supported configuration frequencies are 1 through 1000 Hz, subject to the hardware timer and servo's requirements. Pulse endpoints must be positive, ordered, and shorter than the PWM period. Angle endpoints must be finite and ordered. Commands outside either calibrated range are rejected.

Shell

servo list
servo status [name]
servo open <name> <output> <min-us> <max-us> <min-rad> <max-rad> [freq-hz [inverted:0|1]]
servo set <name> <angle-rad>
servo pulse <name> <pulse-us>
servo disable <name>
servo close <name>

servo list (or bare servo) shows one line per open servo: its name, output, enabled state, and owner. servo status adds calibration, frequency, inversion, and the last accepted command for all open servos. servo status <name> shows those details for one servo. Tab completes open servo names after set, status, pulse, disable, and close.

Names contain 1 through 15 letters, digits, underscores, or hyphens. The following example assumes an available GPIO1 and a servo calibrated for 1000 through 2000 microseconds over approximately -π/4 through π/4 radians:

servo open arm native:1 1000 2000 -0.785398 0.785398
servo set arm 0
servo status arm
servo disable arm
servo close arm

open reserves the output with no pulses. The first set or pulse command starts output. set linearly maps the angle endpoints to the pulse endpoints; inversion reverses this mapping. pulse commands a pulse within the same calibrated limits.

disable stops pulses while keeping the pin and timer reservation. Another set or pulse resumes output. close stops pulses and releases the output. These operations control the signal; they do not switch the servo's power rail. Shell-opened servos remain reserved until explicitly closed or the OS restarts.

Scalar stream

Opening a standalone servo also publishes servo.<name>. Its scalar f32 command input uses radians, matching robot geometry. It applies the same calibration, inversion and pulse limits as the servo API. Opening a stream sends no pulses; the first valid write starts output.

servo open arm native:1 1000 2000 -0.785398 0.785398
stream write servo.arm 0.5
stream echo servo.arm --once
servo disable arm
servo close arm

Readback is the last commanded angle with its acquisition timestamp; it is not measured position feedback. One stream writer is allowed alongside source readers and echo. While that writer is attached, direct servo set and pulse commands are rejected. Closing the writer preserves the last commanded angle and its PWM pulses. servo disable stops pulses and revokes the attached writer until it closes and opens again. servo close stops/releases PWM first; if stream handles still exist, close them and retry servo close to finish removing the named endpoint. Status exposes input_claimed, stream_writable and closing through the shell and Python/Lua.

Servos opened through the native component-owned motion API, including robot joints, expose read-only servo.<name> command observation. Their commands remain restricted to the owning component API.

Python and Lua

Both runtimes expose solaros.servo when the servo service is compiled:

  • list(): return all open servo records.
  • status(name): return one record; fail if the name is not open.
  • open(name, output, min_us, max_us, min_rad, max_rad[, freq_hz[, inverted]]): reserve a calibrated output. Frequency defaults to 50; inversion defaults to false. Use a boolean for inversion.
  • set(name, angle_rad): command an angle within the calibrated range.
  • pulse(name, pulse_us): command an integer pulse width within that range.
  • disable(name): stop pulses and retain the reservation.
  • close(name): stop pulses and release the reservation.

Records contain name, owner, output, freq_hz, pulse_min_us, pulse_max_us, angle_min_rad, angle_max_rad, inverted, enabled, has_command, commanded_pulse_us, and commanded_angle_rad. Command fields are meaningful after has_command becomes true. They describe the last accepted command and remain available while disabled. pwm status reports the quantized hardware pulse width separately. Neither interface measures the servo's physical position.

Python example using the calibration assumed above:

import solaros

solaros.servo.open("arm", "native:1", 1000, 2000, -0.785398, 0.785398)
try:
    solaros.servo.set("arm", 0)
    print(solaros.servo.status("arm"))
finally:
    solaros.servo.close("arm")

Lua uses the same argument order:

solaros.servo.open("arm", "native:1", 1000, 2000, -0.785398, 0.785398)
solaros.servo.set("arm", 0)
solaros.servo.close("arm")

Each output belongs to the shell, Python, or Lua runtime that opened it. Other runtimes can inspect it but cannot command or close it. Python and Lua close their servo outputs when the interpreter exits, including after an error or interruption. A hardware close failure is logged and retains the reservation for retry by the owning runtime.

PCA9685 expansion controller

The optional pca9685 package group adds an I2C controller with 16 PWM outputs. It is enabled in full on boards with expansion I2C. The expansion device name becomes the PWM provider name, and output numbers run from 0 through 15. With an existing i2c0 bus and a controller at address 0x40:

expansion attach pca9685 servos0 i2c=i2c0 addr=0x40
pwm providers
servo open arm servos0:0 1000 2000 -0.785398 0.785398
servo set arm 0
servo close arm
expansion detach servos0

pwm set and pwm pulse also accept these outputs. Python and Lua use the same names through solaros.pwm and solaros.servo. Attachment initializes every channel to full-off; opening a servo does not start pulses. Closing every servo and stopping every general PWM output permits detach. Disabled servos still reserve their channels and the shared frequency.

All 16 channels share one prescaler. A frequency change is rejected while another channel is active or reserved, unless both requests select the same prescaler. Different PCA9685 devices have independent frequencies. With the nominal 25 MHz internal oscillator, requests from 24 through 1526 Hz are supported; the servo abstraction additionally limits requests to 1000 Hz. The hardware has 12-bit resolution, approximately 4.88 microseconds per tick at a requested 50 Hz. PWM status reports quantized timing.

Optional oe=<gpio-or-controller-line> controls the active-low output-enable input. The driver holds it high during initialization and low after all outputs are off. Detach raises it before shutting down the chip. Without this binding, wire OE low. osc_hz=<20000000..30000000> changes the timing calculation to match a measured internal oscillator frequency; the default is 25000000. External-clock mode is not supported. Use an individual 7-bit address from 0x40 through 0x77, excluding the shared All Call address 0x70.

Failed transfers attempt to restore the previous frequency and output values. If restoration also fails, further commands are rejected until outputs are closed and the controller is detached and reattached. Disable/close remain available for cleanup. If detach cannot complete its hardware shutdown, its context and expansion resources remain held; retry detach after restoring I2C.

The controller supplies control signals. Supply servo power from a suitable rail and connect its ground to the controller and MCU ground.

PWM resources

The service has a bounded pool of 16 named servos, independent of the backend's channel capacity. PWM supports four registered providers and 32 active or reserved outputs across those providers. Limits do not preallocate servo objects for every controller channel.

Each provider manages its shared frequency groups. Outputs in the same group must use compatible frequencies, including while disabled. A conflicting request is rejected. Independent providers can use different frequencies. An expansion provider cannot be detached while any output is active, reserved, or executing a command. Close servo objects and stop general PWM outputs first.

The native backend has four hardware channels shared with general PWM outputs and board peripherals using the same driver. They share one timer. A board peripheral already using that timer at another frequency can prevent opening a native servo.

Pulse commands update hardware duty without creating a background task. Hardware PWM continues independently of script scheduling. Commands to an external controller may require a bus transfer. Calls to one provider are serialized; a transfer to one provider does not hold the global registry lock or prevent commands to another servo on an independent provider.

Native timer resolution is 14 bits at servo frequencies: at 50 Hz, one tick is approximately 1.22 microseconds. Other providers report their own resolution and frequency group in pwm status. Pulse widths are rounded to hardware ticks.

See GPIO and analog APIs, Python hardware API, and Lua hardware API.

Quick reference

Use servo open name output min-us max-us min-rad max-rad [freq-hz [inverted]] with explicit calibration and a named PWM output such as native:1. Open reserves without pulses; set and pulse start output, disable retains the reservation, close releases it. Python and Lua expose the same operations under solaros.servo, plus list and status, and close their outputs on interpreter exit. Status describes commanded position. Providers enforce their own shared-frequency constraints. The service supports 16 servos; native PWM has four hardware channels.

Hardware and expansion · SolarOS manual

Source keymaps

A source keymap translates physical keys into HID usages, modifiers, layers, and logical key bytes before the common input service handles repeat and routing. Each registered source starts with its built-in profile and maintains its own modifier, Caps, and layer state.

Source mapping and keyboard language layout are separate settings. setterm keyboard us|de selects how HID usages become characters. Its setterm keymap alias selects the same language layout; it does not load a source mapping file.

Discover and control mappings

input keymap
input keymap keyboard0 show
input keymap keyboard0 load /keymap.json
input keymap keyboard0 reset

The first command lists registered input sources and their mapping support. show reports physical IDs, optional matrix coordinates, and both mapping layers. Omitting show also displays the map. load validates a file before applying it; reset restores the source's built-in profile.

tca8418, inputronic-keyboard, and lilygo-pager-keyboard register physical mapping profiles with modifier, layer, and tap/hold support. Other drivers currently appear as unsupported until they register a profile. A character tap alone does not identify a physical key or provide a real held state.

Mapping files

Files are JSON objects of at most 16 KiB, with "schema": 1 and a keys array, a symbols array, or both. Each load begins with the built-in map and replaces the listed entries. Sources can register up to 256 physical IDs. The IDs are source-local integers from 1 through 65535; they need not be consecutive or match HID usages. Use the IDs reported by show.

For example, on Inputronic, physical ID 52 is the A position. This maps it to the B HID usage:

{"schema": 1, "keys": [{"physical": 52, "usage": 5}]}

A source with matrix metadata also accepts zero-based row and col selectors in place of physical. An entry must use exactly one selector form; two entries that address the same physical key are rejected even if their selector forms differ. Matrix coordinates are converted using the source's registered first ID and row stride. TCA8418 profiles use physical = 1 + row * 10 + col, including matrices narrower than ten columns. Mapping files cannot alter the set of physical IDs or the matrix geometry.

For a generic TCA8418 reference map, this changes Q to A and makes Space a symbol selector when held; Space+A emits an exclamation mark:

{
  "schema": 1,
  "keys": [
    {"row": 0, "col": 0, "usage": 4},
    {"row": 3, "col": 0, "usage": 44, "layer_tap": true}
  ],
  "symbols": [{"row": 0, "col": 0, "key": 33}]
}

keys defines base entries; symbols defines overrides while the layer selector is held. Unlisted entries retain their built-in mappings. Positions without a symbol mapping use the base layer. Optional output fields are:

  • usage: canonical USB HID keyboard usage, expressed as a decimal integer.
  • key and shift_key: SolarOS logical key bytes, 0 through 255. Nonzero values override character translation; Control/Alt chords use the HID usage.
  • raw: true: publish physical press/release events without a logical key.
  • layer_tap: true: hold to select symbols; an unused tap emits the specified usage or key on release. Only one base-layer selector is allowed.
  • alt_block: true: suppress logical output when pressed with Alt. The Pager profile uses this for its Alt+B reservation.

An entry with only a selector clears that entry in the selected layer. An empty symbol entry falls back to the base layer. Raw entries cannot specify logical output. Duplicate keys, unknown fields, invalid usages, and unknown physical IDs are rejected without changing the active map.

A successful apply releases held keys, clears queued controller events, and resets modifier, Caps, and layer state. Release and press any still-held keys again. If hardware preparation fails, the previous map stays active while the driver recovers; held keys have been released.

Mappings last until detach or reboot. Load a saved file from a startup script to reuse it after startup.

Desktop keymap tool

From a SolarOS source checkout, use the Python CLI to create, edit, inspect, or validate a mapping. It uses only the Python standard library.

python3 scripts/keymap.py profiles
python3 scripts/keymap.py show --profile inputronic-keyboard
python3 scripts/keymap.py create --profile inputronic-keyboard \
  --set FN1=F1 --set FN2=Escape --output my-keymap.json
python3 scripts/keymap.py validate --profile inputronic-keyboard --input my-keymap.json
python3 scripts/keymap.py edit --profile inputronic-keyboard \
  --input my-keymap.json --set FN3=Tab

create --interactive prompts for the six Inputronic FN keys. On the other profiles it accepts named assignments until an empty line finishes. An interactive terminal also enters this mode when no assignments are supplied. show lists the profile's key names, physical IDs, and both layers; add --input PATH to preview a mapping. Editing retains existing assignments. Creation requires --force to replace an existing output file. Editing updates its input file unless a separate --output PATH is supplied.

Assignment names are case-insensitive. Use a key name from show or physical:ID, for example --set physical:78=F1. Actions include HID names such as F1, Escape, Ctrl, Shift, and Space; char:! for a logical byte; usage:0x3a or key:27 for numeric output; raw for physical events; and none to clear an entry. tap:Space selects the symbol layer when held and emits Space on an unused tap.

For a generic TCA8418 with a smaller matrix, add --rows N --cols N to match its configured geometry. Physical IDs retain the controller's ten-column stride; keys outside the selected geometry are rejected. The Inputronic and Pager profiles have fixed geometry.

Use --symbol KEY=ACTION to change a symbol entry. For example:

python3 scripts/keymap.py create --profile inputronic-keyboard \
  --set FN3=tap:Space --symbol A=char:! --output symbols.json

Only one layer selector is permitted. The Pager already uses Space as its selector; replace that assignment before choosing a different selector. The tool writes ordinary schema-1 JSON containing only selected overrides. Explicit assignments are retained even when they match the standard profile. Profiles describe the standard mappings, so previews do not automatically include custom defaults compiled into a particular firmware image.

Compile custom defaults

Select a keyboard profile and a mapping file when building firmware:

SOLAR_OS_KEYMAP_PROFILE=inputronic-keyboard \
SOLAR_OS_KEYMAP_FILE=my-keymap.json \
pio run -e waveshare_esp32_s3_sim7670g_4g

The supported profiles are inputronic-keyboard, lilygo-pager-keyboard, and tca8418. Profile definitions in keymaps/profiles/ supply shared key names and standard mappings to the desktop tool and firmware generator. Custom files replace their listed entries in the standard profile. The resulting tables are compiled into firmware; a device-side JSON file is not needed at startup. The selection applies to all attachments of that profile. For a generic TCA8418 configured with fewer rows or columns, only its active matrix positions are used.

Both variables must be supplied together. Relative paths are resolved from the SolarOS project directory. The same selections can be placed in a PlatformIO environment as custom_solaros_keymap_profile and custom_solaros_keymap_file, or supplied as CMake definitions with the SOLAR_OS_KEYMAP_PROFILE and SOLAR_OS_KEYMAP_FILE names.

For several custom profiles, set SOLAR_OS_KEYMAPS_FILE to an index file:

{
  "schema": 1,
  "profiles": {
    "inputronic-keyboard": "inputronic.json",
    "lilygo-pager-keyboard": "pager.json"
  }
}

Paths within the index are relative to the index's directory. Use SOLAR_OS_KEYMAPS_FILE by itself; it cannot be combined with the single-profile variables. Its PlatformIO option is custom_solaros_keymaps_file. Every selected profile's package must be enabled in the build. Missing files, unknown profiles, and invalid maps stop the build. File edits trigger regeneration; removing the selection restores the standard tables on rebuild.

Boot and input keymap SOURCE reset use the compiled default, including custom assignments. Runtime loads also start from that compiled default, so omitted entries retain it. To return to the standard firmware default, rebuild without the custom selection. This does not alter the selected US/DE layout.

Python and Lua

When service.input-keymap is included, both runtimes provide:

  • solaros.input.keymap_info(name) returns mapping capability information.
  • solaros.input.load_keymap(name, path) loads a validated file.
  • solaros.input.reset_keymap(name) restores the built-in profile.

keymap_info returns a dictionary in Python and a table in Lua. Fields are source, capabilities, key_count, rows, cols, first, stride, and boolean supported, physical, modifiers, layers, and tap_hold. Unsupported sources have supported=false; non-matrix profiles have zero rows and columns. An unknown source raises an error. Load/reset return None in Python and no values in Lua; failures raise an exception or error.

Quick reference

  • input keymap lists source mapping support.
  • input keymap SOURCE [show|load PATH|reset] controls a registered profile.
  • Files require "schema":1 and keys or symbols; each entry selects physical or both row and col, with optional usage, key, shift_key, raw, layer_tap, or alt_block output fields.
  • solaros.input.keymap_info(name) reports supported features.
  • solaros.input.load_keymap(name,path) and reset_keymap(name) apply changes.
  • Mappings last until detach or reboot. Release and press held keys again after applying a map. setterm keyboard us|de selects language layout.
  • python3 scripts/keymap.py create --profile PROFILE --set KEY=ACTION --output PATH creates a sparse mapping from a desktop source checkout.
  • SOLAR_OS_KEYMAP_PROFILE with SOLAR_OS_KEYMAP_FILE, or SOLAR_OS_KEYMAPS_FILE, selects compiled firmware defaults; reset restores those defaults.

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.

Use the clock

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

IMU samples and streams

The IMU service exposes each attached device as imu.<device>, a shared source. It reads through the existing driver when a consumer requests a sample; opening the stream does not create a periodic sampling task. The chip still measures at its configured hardware rate. Readers obtain current measurements, not a lossless history, and multiple readers may sample at different times.

For an MPU6050 on a named I2C bus:

expansion attach mpu6050 imu0 i2c=i2c0 addr=0x68
imu list
imu sample imu0
stream status imu.imu0
stream echo imu.imu0 --once
stream echo imu.imu0 --rate 20

The driver accepts addresses 0x68 and 0x69. Add irq=<pin> only when INT is wired to an available native GPIO. The optional input is active high and latched until status is read. Without it, the driver polls status over I2C. The default measurements are 100 Hz, ±2 g and ±250 degrees/s, with the hardware low-pass filter enabled. Reported units are m/s² and rad/s. The driver neither estimates orientation nor subtracts gravity or gyro bias.

stream echo prints one CSV record containing an acquisition timestamp, a validity mask, acceleration, angular velocity and optional orientation. A zero orientation field is not an orientation estimate: validity bit 0 means acceleration, bit 1 angular velocity, and bit 2 orientation. IRQ-free MPU6050 timestamps mark host sample read completion, without a hardware clock or FIFO history. Other IMU providers retain their own sample timestamps.

Native byte reads return one complete 52-byte imu-v1-le record. It contains little-endian uint64 timestamp_us, uint32 valid, then ten IEEE float32 values: acceleration x/y/z, gyro x/y/z, and orientation w/x/y/z. There is no padding. Buffers smaller than 52 bytes fail without acquiring a sample. Open readers prevent detachment; close them before expansion detach <device>. Python and Lua can also read structured values with solaros.imu.sample().

URDF device/frame associations belong to the robot model. The IMU source carries no frame ID and remains usable without the robot library.

Scripting APIs · SolarOS manual

Compatibility I/O modules

Use solaros.i2c, solaros.spi, solaros.uart, and solaros.onewire for simple scripts on the board's default connections. Use named buses with solaros.buses when you need multiple connections or runtime wiring.

The default I2C bus uses the board's primary controller and SDA/SCL pins. Starting another named I2C bus does not change this default. Board-defined buses on different controllers can operate independently through 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

Computer vision

Python media API · Lua media API

Quick reference

  • Enable the vision group (service.vision and service.imlib) on a board with PSRAM. The full flavor enables it on supported boards. A camera or display is optional.
  • solaros.vision.qrcodes(image[, options]) reads a native solaros.image handle and returns QR payloads, four corners, and processing statistics.
  • Options: x, y, width, height, output_width, output_height.
  • Processing dimensions are limited to 640 by 480. At most eight decoded codes are returned. truncated indicates additional decoded codes were omitted.
  • Python payloads are bytes; Lua payloads are binary strings. Inspect eci before interpreting a payload as text.
  • Decode a camera/RTSP frame with image.from_frame, then release the frame before QR processing. Close the decoded image when finished.
  • service.imlib adds the image operations below. Each package can be selected independently. Native code and Python/Lua use the same backend.

Images and regions

Use solaros.image.open(path) for a stored JPEG, PNG, GIF, or WebP image, or solaros.image.from_frame(frame) for a leased camera/RTSP JPEG. QR processing uses the decoded image without modifying its pixels. The image remains valid after processing and can also be drawn or presented.

x and y select the crop origin, defaulting to zero. Zero or omitted width and height select the remaining image from that origin. The crop must fit within the image. Zero or omitted output dimensions select the crop dimensions. Larger images require an explicit crop or resize to fit the processing limit. Resize uses nearest-neighbour sampling; preserve aspect ratio for best results.

Corners are reported in original-image coordinates, including the crop offset and inverse resize. Their order follows the QR orientation: top left, top right, bottom right, bottom left. For mirrored codes, orientation reflects the decoder's original corner order. Python uses tuples of (x, y) pairs; Lua uses arrays of two-element arrays. Payloads do not execute any action automatically.

Results and limits

The returned dictionary/table contains:

FieldMeaning
codesDecoded QR entries; empty when no valid code is found
width, heightOriginal image dimensions
processed_width, processed_heightDimensions passed to recognition
candidatesRecognized QR candidates, including unsuccessful decodes
decode_failuresCandidates whose payload could not be decoded before the result limit
truncatedA further valid code was found after eight results
preprocess_usRecognition-buffer allocation and grayscale conversion time
detect_usRecognition time, including cooperative scheduling
decode_usPayload decode and result-copy time
elapsed_usTotal native processing time, excluding source decode and task launch/cleanup

Each QR entry contains payload, corners, version, ecc_level, data_type, and eci. Payload bytes and metadata are copied into the interpreter and remain valid after the image is closed. ECC values are M=0, L=1, H=2, Q=3; data types include numeric=1, alphanumeric=2, byte=4, and Kanji=8.

Only one QR request runs at a time. A competing request returns a busy error. Allocation failures return an error without falling back to internal RAM for QR buffers. Processing uses a five-second cooperative deadline and checks app cancellation. Cancellation waits for the current decode step, so return may take longer than the deadline.

Python example

import solaros

picture = solaros.image.open("/qr.jpg")
try:
    result = solaros.vision.qrcodes(picture)
    for code in result["codes"]:
        print(code["payload"], code["corners"])
finally:
    solaros.image.close(picture)

For a camera frame, retain the capture timestamp separately so that a result can be associated with its source:

frame = solaros.camera.snapshot()
try:
    timestamp = solaros.streams.frame_info(frame)["timestamp_us"]
    picture = solaros.image.from_frame(frame)
finally:
    solaros.streams.release_frame(frame)

try:
    result = solaros.vision.qrcodes(picture)
    print(timestamp, result["codes"])
finally:
    solaros.image.close(picture)

For repeated captures, open the source once, acquire/decode/release one frame, process it, close its image, and call solaros.time.sleep_ms between iterations. Do not collect an unbounded queue of frames or decoded images. A script cannot open the exclusive local camera while rtspd or cam-webd owns it.

Lua example

local picture = solaros.image.open("/qr.jpg")
local ok, result = pcall(solaros.vision.qrcodes, picture)
solaros.image.close(picture)
if not ok then error(result) end
for _, code in ipairs(result.codes) do
    print(code.payload, code.corners[1][1], code.corners[1][2])
end

The vision module follows its native package gate. Enabling Python or Lua alone does not enable vision. Neural inference is not included in this API.

Image processing with imlib

The selected OpenMV imlib routines accept existing solaros.image handles. Source images remain unchanged. Transformations return new image handles; close them through image.close. Analyses return owned dictionaries/tables. Interpreter exit closes its image handles. Stored pictures and decoded stream frames use the same API.

Python and Lua expose the same positional calls:

CallReturn
vision.histogram(image[, options])Normalized histogram fractions in channels
vision.statistics(image[, options])Region statistics in channels
vision.binary(image, options)New thresholded black/white image
vision.invert(image[, options])New image with inverted pixel values
vision.mean(image[, options])New image with a box filter
vision.gaussian(image[, options])New image with a Gaussian filter
vision.median(image[, options])New image with a median filter
vision.erode(image[, options])New image with erosion
vision.dilate(image[, options])New image with dilation
vision.opening(image[, options])New image, erode then dilate
vision.closing(image[, options])New image, dilate then erode
vision.difference(image, reference[, options])New absolute per-channel difference image
vision.blobs(image, options)Connected regions in blobs
vision.process(image, operation[, options[, reference]])Analysis or transformation record with native timing and scratch usage

operation uses the table's function names, such as "gaussian" or "blobs". For transformations, process returns the new handle in image. Difference requires a reference with the same original dimensions as the source; both images receive the same crop/resize.

Options

Options are a Python dictionary or Lua table. Unknown keys, invalid types, out-of-range values, and options belonging to another operation fail before worker execution.

Common optionMeaning
format"gray" default, or "rgb565" for color processing
x, y, width, heightSource crop; zero dimensions select the remaining image
output_width, output_heightNearest-neighbour resize; zero selects crop size
timeout_msCooperative deadline, 1–60000 ms; default 5000

Processed dimensions must fit 640 by 480. Larger pictures require an explicit crop or resize. A transformed image contains the processed crop at the selected output size. Processing uses grayscale or RGB565 temporary pixels and copies transformed pixels into a new immutable RGB888 SolarOS image. Color processing therefore includes RGB565 quantization.

  • Histogram/statistics: optional bins (2–256). Defaults are 256 for grayscale, 101 for LAB lightness, and 256 for each LAB chromatic channel. Fewer bins make statistics approximate. Optional thresholds and Boolean invert restrict measurements; an empty selection produces zero histogram fractions.
  • Binary/blobs: required thresholds, up to eight inclusive ranges. Grayscale uses [min,max] in 0–255. RGB565 uses [Lmin,Lmax,Amin,Amax,Bmin,Bmax], with L in 0–100 and A/B in -128–127. Boolean invert inverts each range's match.
  • Filters/morphology: ksize is the kernel radius, 1–3, default 1. A radius of one selects a 3 by 3 neighborhood. Median uses the middle percentile; Gaussian uses Pascal coefficients. Threshold an image first for binary-mask morphology. Filters retain the upstream edge handling.
  • Blobs: x_stride, y_stride (default 1), area_threshold and pixels_threshold (default 10), Boolean merge (default false), margin (0–640, default 0) and max_blobs (1–64, default 32). Area checks the bounding box; pixel threshold checks matching pixels.

Results and resources

Measurements contain channels.gray, or channels.l, channels.a, and channels.b for RGB565/LAB. Histogram channels are arrays of fractions. Statistics channels contain mean, median, mode, stdev, min, max, lower_quartile, and upper_quartile.

Blobs contain x, y, width, height, cx, cy, rotation in radians, pixels, code (threshold bitmask), and count (merged components). Bounds and centroids use original source-image coordinates, including crop/resize. Pixel count uses the processed resolution. Rotation is mapped back using both resize scale factors. The record includes coordinates="source_pixels" and truncated; truncation means the output limit or the 512-candidate admission limit omitted regions. Candidate admission happens before merging.

Records also include operation, format, source roi, processed_width, processed_height, preprocess_us, process_us, output_us, elapsed_us, and workspace_peak_bytes. Timing excludes image decoding, task launch/cleanup, and interpreter object construction. Scratch usage includes converted images and tracked algorithm allocations; it excludes the worker stack, result record, returned image, and interpreter copies.

One imlib request runs at a time; competing calls report busy. Its 12 KiB worker stack and scratch exist only while processing. Scratch uses PSRAM, with a 2 MiB request limit including allocation tracking. Blob flood-fill storage is bounded to 128 KiB; exhaustion reports an allocation error. Temporary storage is released on every return. Constant lookup tables occupy firmware flash. Cancellation and timeout join the worker before releasing buffers. Deadlines are cooperative, so an individual step can exceed the requested time.

Python filtering example

from solaros import image, vision

source = image.open("/picture.jpg")
try:
    filtered = vision.gaussian(source, {"output_width": 320, "output_height": 240})
    try:
        mask = vision.binary(filtered, {"thresholds": [[128, 255]]})
        try:
            result = vision.blobs(mask, {"thresholds": [[128, 255]]})
            print(result["blobs"], result["process_us"])
        finally:
            image.close(mask)
    finally:
        image.close(filtered)
finally:
    image.close(source)

Lua color example

local picture = solaros.image.open("/picture.jpg")
local ok, result = pcall(solaros.vision.blobs, picture, {
    format = "rgb565", output_width = 320, output_height = 240,
    thresholds = {{20, 80, 30, 127, 0, 127}}, pixels_threshold = 30
})
solaros.image.close(picture)
if not ok then error(result) end
for _, blob in ipairs(result.blobs) do
    print(blob.cx, blob.cy, blob.pixels)
end

The native pipeline service currently accepts a single QR or model processor. The examples above sequence synchronous image operations in the interpreter.

Scripting APIs · SolarOS manual

Digital signal processing

Use solaros.dsp to scale, mix, filter, or analyze signed 16-bit samples from Python or Lua. Create a FIR, decimator, or FFT processor for repeated work, and close it when finished. Native C APIs are listed below.

SolarOS selects acceleration automatically where available on ESP32-S3. The same API works with the portable backend on other boards.

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

Emotion engine

The emotion engine draws procedural eyes on a supported display target and can play short reaction sounds. A script chooses the expression; a separate worker advances the animation while the script performs other work.

The optional emotion native library requires ESP32-S3 with executable PSRAM. The firmware must include the emotion runtime and Python or Lua. The standard Python and Lua package groups select the runtime on compatible boards. Sound also requires the audio service and an available output device.

Start a face

pkg available
pkg install emotion
display list

Install emotion when it is available in the configured signed module catalog. Opening a face loads the installed library; it does not download code.

import solaros

with solaros.emotion.open(sound=True) as face:
    face.set("happy", transition_ms=200)
    face.look(x=0.4, y=-0.2)
    face.react("surprised", hold_ms=700, sound="chirp")
    solaros.time.sleep_ms(2000)

From a port shell or a headless script, supply a ready target returned by display list, for example solaros.emotion.open("oled0").

Display and lifetime

open(display=None, sound=False, fg=None, bg=None, fps=0, motion=None) returns a face object. Python accepts positional or keyword arguments. Lua accepts the same arguments positionally; use nil for omitted optional values.

  • Omit display to use the calling foreground application's display. An explicit name matching that display uses the same foreground lifetime.
  • A different target is claimed exclusively until the face closes. A busy or absent target raises an error; the engine does not replace another owner.
  • A foreground face pauses its animation clock and stops its sound when its session is suspended. It restores its frame on resume. A face on a separately claimed target continues until closed or its owning interpreter exits.
  • close() stops the worker and presenter before releasing the display and library. Repeated successful closes are harmless. If shutdown reports a timeout, the instance retains its resources and can be closed again.
  • Interpreter teardown closes remaining faces after normal completion, exceptions, or cancellation. Use Python's with or Lua's <close> for a shorter, explicit lifetime.

At most four faces may be open across all consumers, with one instance per display target. Each face has independent expression and gaze state. Close faces before using the script graphics or TUI APIs. Drawing over an active face is not supported. The library cannot be unloaded while a face holds it.

Appearance and refresh

When the face opens, it uses the configured terminal foreground color for the eyes and background color for the background (setterm foreground and setterm background). Optional fg and bg values override those colors for this face. Each accepts an integer from 0x000000 to 0xFFFFFF in 0xRRGGBB format; omitted values or None/nil independently use the terminal setting. The colors are captured at open; reopen the face to change them. Monochrome targets convert the selected colors through the display service.

face = solaros.emotion.open(fg=0x00FFFF, bg=0x000000)

The equivalent Lua call is solaros.emotion.open(nil, false, 0x00FFFF, 0x000000).

Faces preserve their proportions and fit the target's oriented dimensions. fps=0 uses the target's preferred rate, capped at 25 frames per second. An explicit value accepts 1..30 and is capped by the same runtime and target limits. This is a submission limit, not a guarantee of physical refresh speed.

motion=None enables idle movement and automatic blinking when the target advertises at least 20 frames per second. Slower targets default to reduced motion and at most two submissions per second. motion=False selects that mode explicitly; True enables animation within the target's rate limit. Unchanged images are not resubmitted. Short transitions or blinks may be skipped by slow displays; use longer-held poses on e-paper.

SSD1306 and SH1106 expansion displays advertise 25 fps, so automatic motion is enabled by default. Actual refresh speed depends on the named I2C bus speed and other devices sharing that bus. Animation follows elapsed time and skips intermediate images when presentation takes longer than a frame interval.

Face methods

Python methodBehaviour
set(pose, transition_ms=200)Change the persistent expression and cancel the current reaction and sound.
react(pose, hold_ms=700, transition_ms=120, sound=None)Transition to a temporary pose, hold it, then return to the persistent expression over 200 ms. A new reaction replaces the previous one.
look(x=0, y=0)Set gaze offsets from -1.0 to 1.0. Negative y looks up. Gaze remains independent of expression.
blink()Request a 180 ms blink.
status()Inspect dimensions, rate, motion/audio availability, frame counters, and output errors.
close()Stop and release this instance.

Durations accept 0..60000 ms. set, react, look, and blink enqueue work without waiting for presentation. Each instance holds up to eight pending commands; a full queue raises a timeout error. Pending commands are processed in order. Commands sent during foreground suspension wait for resume within that same queue limit. Opening a new foreground face while suspended raises an invalid-state error.

status() returns width, height, fps, motion, sound, frames, dropped, audio_skipped, error, and audio_error. frames counts accepted frame submissions, not physical display refreshes. dropped counts rejected submissions. Error fields retain the last reported error; audio errors reset when a later cue successfully opens the output.

Poses and sounds

The 21 named poses are:

neutral       blink_high    happy         glee          blink_low
sad_down      sad_up        worried       focused       annoyed
surprised     skeptic       bored         unimpressed   sleepy
suspicious    squint        angry         furious       scared
awe

The pose names describe the supplied Cozmo-style eye reference. Gaze and blinking can also be controlled independently. The engine uses authored geometry and timing rather than stored animation images.

Reaction sounds are chirp, giggle, sigh, and alert. Omit sound, pass None/nil, or use "silent" for a silent reaction. Sound must also be enabled when opening the face. Ordinary expression and gaze changes do not produce sound. Synthesized cues respect global volume and release the output after playback. If another consumer owns the speaker, the cue is skipped while the face continues; inspect audio_skipped and audio_error.

Lua example

local solaros = require("solaros")
local face <close> = solaros.emotion.open(nil, true)
face:set("happy", 200)
face:look(0.4, -0.2)
face:react("surprised", 700, 120, "chirp")
solaros.time.sleep_ms(2000)

Lua uses the same face methods with colon syntax and positional arguments. For a dedicated display, replace nil with a verified target name.

Native consumers

Native applications and jobs can acquire library emotion, ABI 1, through the native library service, using solar_os_emotion_abi.h. The engine accepts caller-owned state, INDEX2 output buffers, and monotonic timestamps. It also generates mono S16 sound samples. Native consumers own their scheduling and use their available host services for presentation and playback. The library itself acquires no hardware.

A foreground application can use its existing foreground video/audio service. Use its text_colors method to obtain the terminal foreground and background in RGB565. solar_os_emotion_palette_rgb565 builds the four-entry presentation palette, with background at index 0 and eyes at index 3. Supply different RGB565 endpoints to override either color:

uint16_t fg, bg, palette[4];
int err = foreground->text_colors(&fg, &bg);
if (err != SOLAR_OS_NATIVE_OK) return err;
/* Optional override: fg = 0x07ff; bg = 0x0000; */
solar_os_emotion_palette_rgb565(fg, bg, palette);

Pass this palette to the native foreground video service with INDEX2 frames. Firmware consumers of solar_os_emotion_open can set foreground_set and/or background_set alongside the corresponding foreground_rgb888 and background_rgb888 options; unset colors use terminal settings.

The scripting runtime adapter is a firmware service; it is not a general display/audio ABI for external jobs. An independent emotion job is not required to use a face from a robot script.

Quick reference

Requires ESP32-S3, executable PSRAM, the emotion runtime, Python or Lua, and the installed emotion native library. pkg available lists catalog modules; pkg install emotion installs it when published. display list lists targets.

solaros.emotion.open(display=None, sound=False, fg=None, bg=None, fps=0, motion=None)
face.set(pose, transition_ms=200)
face.react(pose, hold_ms=700, transition_ms=120, sound=None)
face.look(x=0, y=0)
face.blink()
face.status()
face.close()

Lua uses positional arguments, nil, colon methods, and <close>; Python supports keywords and with. Omit the display only in a foreground application. Use a verified explicit target for port shells and background scripts.

fg colors the eyes and bg colors the background. Both accept 0xRRGGBB integers; omitted values use the terminal colors at open. Sound cues: silent, chirp, giggle, sigh, alert; sound also needs sound=True and an available audio output. A busy speaker skips the cue.

Gaze is -1..1, negative y up. Durations are 0..60000 ms. fps=0 selects the target rate; 1..30 requests a rate, capped at 25 and the target preference. Automatic motion is enabled only for targets advertising at least 20 fps; reduced motion is limited to 2 fps. Slow panels can skip brief transitions.

Four instances maximum, one per target, eight queued commands per instance. Foreground faces pause with their session; dedicated displays continue. Close faces before graphics/TUI drawing. Close joins workers before releasing ownership and the library; a shutdown timeout retains resources for retry. Interpreter exit closes remaining faces. Frame counters report submissions, not physical refreshes. Native consumers use solar_os_emotion_abi.h, ABI 1.

Scripting APIs · SolarOS manual

Jet 3D renderer

solaros.jet renders script-defined scenes using the installed jet resident library. The firmware includes the service.jet frontend, not the renderer. The Python and Lua package groups include this frontend on supported boards; custom flavors can select the jet group separately. It requires ESP32-S3, PSRAM and a display with RGB565 or MONO1 presentation.

Install a compatible library from your configured signed package catalog:

pkg install jet

Installation and rendering are separate operations. jet.open() uses the installed file; it does not download code. A missing or incompatible library produces an error. Only one scene may be open across all scripts and native consumers. Close it before pkg unload jet or pkg remove jet.

Python

import solaros

with solaros.jet.open() as scene:
    scene.background(0xffffff)
    scene.camera(z=-500, fov=60)
    cube = scene.cube(150, 150, 150, color=0x2192ff)
    for angle in range(360):
        if solaros.should_exit():
            break
        scene.transform(cube, z=100, rx=angle, ry=angle * 2)
        scene.render()
        solaros.time.sleep_ms(1)

open(display=None, fps=0) returns a scene. The default target is the calling foreground display. From a port shell, provide a ready target name from display list, for example solaros.jet.open("display0"). fps is 0 through 30; zero uses the target's preferred stream rate on monochrome targets and 30 on color targets. Presentation may coalesce queued frames. Jet keeps its current orientation while active. Display and gesture rotation changes are rejected until the scene is suspended or closed.

Scene methodArguments and result
background(color)Set RGB888 clear color; default white.
camera(x=0, y=0, z=-500, rx=0, ry=0, rz=0, fov=60)Camera position, integer-degree rotation and horizontal field of view, 10–150 degrees. Default near/far planes are 16/4096.
light(ambient=0x5a5a5a, color=0xfff5e6, intensity=220, azimuth=35, elevation=40)Ambient RGB888 and directional light; intensity is 0–255, direction is in integer degrees.
cube(width, height, depth, color=0xffffff)Create a shaded cube; each dimension is 1–4096. Return an integer object handle.
mesh(vertices, triangles, color=0xffffff)Copy vertices and indexed triangles; return an object handle. Vertices are (x,y,z) sequences. Python indices are zero-based. Normals are generated separately for each triangle.
mesh_data(vertices, triangles, material=0)Copy XYZ/UV/normal vertices and optional per-triangle material handles; see below.
material(...), material_set(handle, ...), material_info(handle)Create, partially update, or inspect a reusable material.
texture(width, height, pixels, ...), texture_set(handle, ...), texture_info(handle)Copy, update, or inspect a texture.
assign(object, material)Replace every triangle's material; zero restores the object's default material.
capabilities()Query available provider features, geometry counts and resource limits.
transform(object, x=0, y=0, z=0, rx=0, ry=0, rz=0)Replace an object's position and rotation. Omitted fields reset to zero.
remove(object)Remove an object. Its handle becomes invalid.
render()Render synchronously, then transfer the completed color buffer to the shared presenter without copying.
status()Return frames, presented, dropped, render_us, present_us, width, height, and error. Times are cumulative microseconds. presented counts successful presentation calls, not optical refreshes.
close()Release objects, buffers, display ownership and the library lease; repeated close is harmless.

Colors are 0x000000 through 0xffffff. Positions and vertex coordinates are integer design units in -4096 through 4096; rotations are integer degrees. Objects use back-face culling and Gouraud shading, with 16-bit per-pixel depth. A scene supports 32 objects. Each mesh accepts up to 1024 input vertices and 2048 triangles. Vertex winding determines the visible side.

Use a with block or try/finally for prompt cleanup. Interpreter teardown also closes retained scenes after their provider calls finish. Closed scene objects cannot access a later scene. Foreground session switching joins presentation and releases its display claim before another session draws; rendering resumes on return. End a graphics/TUI session or close Emotion faces before opening Jet. The claimed target defers terminal and status-bar painting until the scene closes or suspends, including when a port-shell script targets the display shell's screen.

Lua

Lua exposes the same operations as colon methods. solaros.jet.open(display, fps) defaults to nil, 0; method arguments follow the Python table's order. Lua mesh tables and triangle indices are one-based. Object handles are opaque integers, not array indices. New resource constructors and setters use an options table in Lua, for example scene:material{color=0xffffff, shading="phong"} and scene:material_set(handle, {alpha=160}). Texture creation is scene:texture(width, height, pixels, options); mesh data uses scene:mesh_data(vertices, triangles, material).

local solaros = require("solaros")
local scene <close> = solaros.jet.open(nil)
local cube = scene:cube(150, 150, 150, 0x2192ff)
for angle = 0, 359 do
    if solaros.should_exit() then break end
    scene:transform(cube, 0, 0, 100, angle, angle * 2, 0)
    scene:render()
    solaros.time.sleep_ms(1)
end

The <close> declaration releases the scene on normal completion and errors. Explicit scene:close() is also available. Use Ctrl+] to stop either runtime. Complete two-cube examples are supplied as examples/python/jet.py and examples/lua/jet.lua in the firmware source tree.

Materials, textures and mesh attributes

These operations require Jet library 1.1 or later. The original library still supports the basic methods; capabilities() reports unavailable extensions as false. Resource handles belong to their scene and become invalid when removed or when the scene closes. remove() refuses a material referenced by geometry or a texture referenced by a material. Detach or remove its consumers first.

texture = scene.texture(2, 2, b"\x00\x01\x01\x00", format="index8",
                        palette=(0xffffff, 0x2192ff))
material = scene.material(texture=texture, shading="phong", specular=80, gloss=32)
scene.assign(cube, material)
scene.material_set(material, alpha=180)

material() accepts these keyword options, or positional values in this order. material_set(handle, ...) changes only supplied options.

OptionDefault and range
colorWhite RGB888 tint.
shading"gouraud"; also "flat", "phong", "wireframe", "unlit", "water", "additive".
textureTexture handle, or zero for a solid color.
alpha, diffuse, specular, gloss255, 255, 0, 0 respectively; each 0–255. Gloss sets the Phong specular exponent.
emissive, perspectiveFalse, true; bypass lighting and enable perspective-correct UV interpolation respectively.
water_bias, reflection_max_y0 and -1; water reflection row offset 0–255 and maximum reflection row -1–1023. Water shading needs a scene background gradient.
nameOptional unique material name, fewer than 64 UTF-8 bytes.

texture(width, height, pixels, ...) requires dimensions and pixel bytes as positional arguments. Width and height are 1–1024. All pixel and palette data is copied into provider-owned PSRAM; the script may release its input buffers.

Texture optionDefault and meaning
format"rgb565": two bytes per pixel, most significant byte first. "rgb888" uses R,G,B byte triples; "index8" uses one-byte palette indices.
address"wrap"; also "clamp" and "zero" for out-of-range UVs.
alpha_key-1 disables color-key transparency; otherwise an RGB888 color.
screen_space, reflectionFalse; sample using screen coordinates or reflected view direction.
bilinearFalse; true enables bilinear sampling for RGB textures. Indexed textures use nearest sampling.
offset0–255; palette offset, wrapped by palette length.
paletteRequired for index8: 1–256 RGB888 colors. Every input index must fit this palette.
pixelsReplacement byte buffer for texture_set(); supply creation pixels positionally.
nameOptional unique texture name, fewer than 64 UTF-8 bytes.

texture_set(handle, ...) retains dimensions and format. It can replace pixels or palette and change sampling options. Allocation or validation failure leaves the previous texture usable. texture_info() includes width, height, format, palette size and sampling settings. material_info() returns material settings. A scene allows 16 textures and 32 reusable materials, plus each object's default material.

mesh_data() accepts vertices consistently shaped as (x,y,z), (x,y,z,u,v), or (x,y,z,u,v,nx,ny,nz). UVs are normalized numeric values in -32–32; normals are numeric components in -1–1. Without explicit normals, per-face normals are generated. Supply unit-length normals for smooth lighting. Each triangle is (a,b,c) or (a,b,c,material), with the usual language-specific index base. The optional fourth element overrides the default material argument for that triangle. Zero selects the object's default material.

Complete textured examples are examples/python/jet_materials.py and examples/lua/jet_materials.lua.

Geometry and scene controls

These operations require Jet library 1.2 or later. Python setters accept keyword options; Lua setters take an options table. Setters change only supplied fields. capabilities() reports geometry, scene, max_objects, and max_lights.

MethodContract
primitive(...)Create a primitive and return its object handle.
object_set(object, ...), object_info(object)Update or inspect object properties. Info also includes vertex/triangle counts, six local-space bounds (min XYZ, max XYZ), and lod_levels.
object_action(object, ...)Run a geometry or movement action; return the object handle, or the new handle for clone.
mesh_update(object, vertices, triangles, material=0)Replace geometry using the mesh_data() format while retaining transforms, flags and LOD links. Failed validation or allocation leaves the previous geometry intact.
camera_set(...), camera_info(), camera_action(...)Partially update, inspect, or move the camera.
point_light(...), point_light_set(light, ...), point_light_info(light)Create, update or inspect one of up to eight point lights; remove(light) releases it.
gradient(colors)Copy a sequence of up to 1024 RGB888 color stops, interpolated over the display height. Empty sequence or Python None / Lua nil restores the solid background.
lod(object, levels)Set up to eight alternative object handles, ordered from highest to lowest detail. Empty sequence clears the list. Cycles and removal of referenced objects are rejected.
settings(...), settings_info()Update or inspect scene-wide controls.

Primitive options:

OptionDefault and meaning
kind"cube"; also "grid", "plane", "pyramid", "sphere", "capsule", "cylinder", "billboard", "quad", "debug_cube".
width, height, depth, radius150, 150, 150, 75; each 1–4096. Capsule height is total height and must be at least twice its radius.
segments, rows, cols12, 4, 4; segments 3–32, grid vertex rows/columns 2–32. Generated geometry must fit 2048 triangles and 8192 vertices.
caps, u_scale, v_scale, per_cell_uvTrue, 1, 1, false. Cylinder caps, sphere UV repetition (1–32), and grid UV mode.
material, material2, colorZero, zero, white. Zero selects the object's default material; grid material2 defaults to material. Debug cubes own six colored face materials.
nameOptional unique object name, fewer than 64 UTF-8 bytes.

Object properties are x/y/z, rx/ry/rz, scale_x/scale_y/scale_z (1/64–16), enabled, culling ("back", "front", "none"), blend ("replace", "add", "subtract", "scale", "average", "xor"), billboard, depth_test, depth_write, depth_bias (-128–127), precise_sort, fade_near/fade_far, appear_near/appear_far (0–16384), lod_bias (-8–8), lod_persist, and name. Positions and rotations default to zero, scales to one, culling to back, blend to replace, depth test/write and LOD persistence to true. Fade bands default to disabled (both zero); otherwise far must exceed near. Enabled defaults to true; billboard defaults to false except billboard primitives.

object_action() takes action plus optional x/y/z (default zero) and numerator/denominator (default one, 1–16384). Actions are "translate", "rotate", "look_at", "bake_rotation", "bake_scale", "cache_positions", "invalidate_positions", "clone", "bake_lighting", and "unbake". Bake scale uses the supplied ratio and retains object scale; bake rotation moves rotation into the vertices and resets it. Clones have independent geometry and default materials, and share named materials and LOD references. Position caching is optional and reports an allocation error if its main buffer cannot be created. Baking lighting stores flat directional and ambient lighting per triangle; unbake restores live lighting.

Camera properties are x/y/z, rx/ry/rz, fov, near (1–4095), and far (2–4096), with near less than far. camera_action() accepts action and x/y/z: "translate", "translate_local", "translate_local_y", "rotate", "rotate_local", "look_at", "look_at_x", or "look_at_y". Look-at values are target coordinates.

Point-light options are x/y/z, color (white), range_min (0), range_max (255), enabled (true), and a unique name. Range values are 0–16384 with minimum less than maximum. Scene settings are clear (true), lod_scale (0 disables distance LOD; otherwise 1–16384 design units), lod_bias (-8–8, default zero), and water_time (numeric animation time, 0–1000000). Objects used only as LOD alternatives should be disabled as standalone objects with object_set(alternative, enabled=False).

Complete examples are examples/python/jet_scene.py and examples/lua/jet_scene.lua.

Sprites and picking

Jet library 1.3 or later adds sprite(...), sprite_set(handle, ...), sprite_info(handle), pick(points), and picks(). Python uses keyword options and Lua uses options tables. capabilities() includes sprites, picking, max_sprites (32), and max_picks (32).

Sprites draw after 3D geometry, in ascending z_order, without depth testing. sprite() returns a handle; remove(sprite) unregisters it. An attached material cannot be removed until its sprites and geometry release it.

Sprite optionDefault and meaning
x, yZero; top-left screen pixel, -4096–4096.
width, height32; solid rectangle dimensions, 0–4096. Textured sprites use texture dimensions.
material, colorZero and white. Zero uses the sprite's own solid-color material; color changes this fallback.
alpha, enabled255 and true. Alpha multiplies material alpha.
z_orderZero, -32768–32767; equal orders retain their current relative order.
blend"replace" uses normal alpha compositing; "add" adds source channels with saturation. For additive sprites, nonzero alpha enables the full source contribution, so put falloff into the texture.
scaleOne; integer nearest-neighbor upscale, 1–16.
flip_x, flip_yFalse; reverse texture sampling.
mirror_x, mirror_yFalse; append reflected halves to double the texture extent along that axis. Flips apply to the expanded image.
nameOptional unique sprite name, fewer than 64 UTF-8 bytes.

pick(points) copies up to 32 (x,y) pairs in screen pixels. Coordinates must be inside the display, or -1 to disable a query. Empty sequence or Python None / Lua nil disables picking. New queries clear the previous results; call render() before picks().

picks() returns a list/table of result dictionaries/tables, one per query: hit, object (handle, zero on a miss), triangle, depth, x, and y. Python triangle indices start at zero and use -1 on a miss; Lua indices start at one and use zero on a miss. Results describe the last completed render and retain those handle values even if an object is subsequently removed. The index addresses the selected mesh, including any LOD alternative.

Picking tests filled triangle coverage. Sprites, wireframe outlines, texture color-key holes and per-pixel transparency do not participate. Overlapping triangles are compared using mean camera-space triangle depth rather than an exact ray intersection. Picking is disabled by default; leaving the query list empty skips all query iterations.

cursor = scene.sprite(x=width // 2 - 2, y=height // 2 - 2,
                      width=5, height=5, color=0xff0000)
scene.pick(((width // 2, height // 2),))
scene.render()
selection = scene.picks()[0]
if selection["hit"]:
    selected_object = selection["object"]

Complete examples are examples/python/jet_picking.py and examples/lua/jet_picking.lua.

Asset loading

Jet library 1.4 or later reports assets=True in capabilities() and adds three loaders. They accept file contents, copy retained data into owned storage, and leave the scene unchanged if validation or allocation fails. Read files with the language's normal file APIs; the provider does not open paths or fetch dependencies.

MethodInput and result
load_obj(data, scale=1, material=0, color=0xffffff, name="")OBJ text or bytes, up to 1 MiB; return an object handle. Python accepts keyword options; Lua takes an options table after the data string.
load_mtl(data)MTL text or bytes, up to 256 KiB; return material handles in declaration order. The complete material set is published together.
load_bmp(data, ...)BMP bytes, up to 8 MiB; return a texture handle. Accepts the texture sampling and name options. Format is always "rgb565"; pixels and palette options are not accepted.

OBJ loading supports v, vt, vn, usemtl, positive and negative face indices, and v, v/vt, v//vn, or v/vt/vn face elements. Convex polygons of up to 16 vertices are triangulated as a fan. Missing normals are generated per triangle; supplied normals are retained. An optional fourth position component is the homogeneous divisor. OBJ units are multiplied by 128 * scale, matching Jet's loader convention; scale is 1/1024–32 and final coordinates must fit -4096–4096. A file may define up to 1024 positions, 8192 combined UV/normal records, and 2048 generated triangles. Lines may contain at most 4096 bytes.

usemtl names must match materials already in the scene. Load MTL data first; mtllib, object/group names and smoothing declarations do not load files or split the object. Jet's mt HEX565 [alpha] extension creates an object-owned face material; up to 32 are allowed. md or mr restores the default material.

MTL loading supports newmtl, Kd, Ks, Ns, d, Tr, illum, and map_Kd. Diffuse color and opacity values are 0–1. Specular color is averaged into Jet's scalar specular coefficient; gloss is clamped to 255. illum 0 selects unlit shading, 1 selects Gouraud, and 2–10 select Phong. Other MTL properties are ignored. Texture-map names, including spaces, must exactly match textures loaded or created beforehand; map options are not supported. Material names must be unique and fit the normal 63-byte limit.

BMP loading accepts Windows DIB headers of at least 40 bytes, dimensions 1–1024, top-down or bottom-up rows, and uncompressed 1-, 4-, 8-, 16-, 24-, or 32-bit pixels. Paletted files are expanded to RGB565. Uncompressed 16-bit files use RGB555; 16-bit bit-field files support RGB555 and RGB565 masks. Standard BGR masks are also accepted for 32-bit bit-field files. RLE and other compression are rejected. The 32-bit alpha byte is ignored; use the texture's alpha_key option when color-key transparency is needed.

with open("/sdcard/assets/checker.bmp", "rb") as source:
    texture = scene.load_bmp(source.read(), name="checker.bmp")
with open("/sdcard/assets/model.mtl") as source:
    materials = scene.load_mtl(source.read())
with open("/sdcard/assets/model.obj") as source:
    model = scene.load_obj(source.read(), scale=0.7)

Self-contained examples that build a tiny BMP and import a textured OBJ cube are examples/python/jet_assets.py and examples/lua/jet_assets.lua.

Shared mesh instancing

Jet library 1.5 or later reports instancing=True. A prototype is an immutable copy of an object's local geometry. Its default and object-owned face materials are copied; named materials stay shared and remain editable. The source object can be changed or removed afterward. Its position, rotation, scale and LOD links are not part of the prototype; bake any wanted geometry changes first. Objects that already contain instances cannot become prototypes.

MethodContract
prototype(object, name="")Freeze local geometry and return a prototype handle. Python accepts the name keyword; Lua takes an options table.
prototype_info(prototype)Return id, name, vertex/triangle counts and six local-space bounds.
instance(prototype, ...)Create an object containing one instance; return its object handle.
instance_add(object, prototype, ...)Append an instance to an existing object; return its index. The object may also have its own geometry.
instance_set(object, index, ...)Partially update an instance's local pose and material overrides.
instance_info(object, index)Return local pose/options, prototype handle, per-face material handles, row-major 3×3 basis, and baked.
instance_remove(object, index), instance_clear(object)Remove one or all instances, preserving the object's own geometry. Later indices shift down after removal.
remove(prototype)Release a prototype after all of its instances and clones are removed.

Instance indices are zero-based in Python and one-based in Lua. Options are x/y/z, rx/ry/rz, material (all default zero), and materials (optional per-triangle material handles). Python uses keywords; Lua uses an options table. Zero material handles retain the prototype's original face materials. A nonzero uniform material takes precedence over the per-face table. A supplied table must match the prototype's triangle count; an empty sequence clears it. Material references reject removal while in use.

Instances share prototype geometry and inherit their containing object's transform, culling, fade, LOD and depth flags. Their local bounds must fit -4096–4096. A scene supports 16 prototypes and up to 32 instances per object; the object's own geometry plus all instances must fit 2048 triangles. capabilities() reports max_prototypes and max_instances. object_info() includes the instance count and total geometry counts.

clone copies the batch placements while retaining shared prototypes. bake_rotation moves the object's rotation into its geometry and instance placements. bake_scale creates shared scaled mesh copies within that batch; instance_info().baked then becomes true while prototype identifies the original prototype whose materials remain retained. Later partial instance updates preserve that baked geometry. Pose angles are rounded integer Euler angles; basis exposes the exact retained matrix. assign(object, material) updates both the object's own faces and all instance overrides, clearing per-face override tables.

Picking results additionally include mesh, batch, and instance. object identifies the owning scene object, batch identifies the object whose geometry was selected by LOD, and mesh is the actual object/prototype handle. instance uses the language's index base, or Python -1 / Lua 0 for ordinary geometry. For older providers these extra handles are zero and the instance index indicates ordinary geometry. Results retain the last completed frame's values even after resources are removed.

Complete examples are examples/python/jet_instances.py and examples/lua/jet_instances.lua.

Particles and lens flares

Jet library 1.6 or later reports particles and flares in capabilities(). Both use scene-owned resources released by remove() or close(). They are drawn after opaque geometry; sprites retain their normal overlay ordering. Neither particles nor flare sprites participate in picking. Flares reserve separate occlusion queries without replacing the script's queries.

step(dt) advances enabled particle pools, sets the next flare integration interval, and advances water animation time. Delta time is 0–1 seconds. Rendering does not advance particle physics by itself; flare integration defaults to 1/30 second until step() is called.

Particle methodContract
particles(...)Create a fixed pool of 200 particles and return its handle. Up to four pools may exist.
particles_set(handle, ...)Partially update pool options.
particles_emit(handle, ...)Emit sparks or water splash; return the number spawned. A full pool spawns fewer particles.
particles_info(handle)Return options, active, triangles drawn last frame, capacity, and current random_state.
particles_clear(handle)Clear particles without resetting the random state.

Pool options are world_scale (1/64–16, default one), additive (false), seed (1–4294967295, default one), enabled (true), and unique name. Each pool has an independent deterministic random generator. Setting its seed affects subsequent emissions. Disabled pools pause physics and skip drawing. Particles that expire or leave the -4096–4096 world-coordinate range are removed.

Emitter options are kind ("sparks" or "splash"), x/y/z origin, nx/ny/nz spark normal (default 0,1,0), speed (default 100, 0–4096), count (default 24, 0–200), and inherited velocity vx/vy/vz (default zero). Splash emitters also accept travel direction tx/ty/tz (0,0,1), side direction sx/sy/sz (1,0,0), and up direction ux/uy/uz (0,1,0). Origins and velocity components are numeric values in -4096–4096; normal/direction components are -1–1. Particle colors and lifetimes follow Jet's spark and splash profiles.

Flare methodContract
flare(elements, ...)Create a lens flare from 1–12 element option dictionaries/tables. Up to four flares may exist.
flare_set(handle, ...), flare_info(handle)Update or inspect flare options. Info adds last computed visibility, in_front, on_screen, projected_x/y, and element count.
flare_element_set(handle, index, ...), flare_element_info(handle, index)Update or inspect an element; Python indices start at zero and Lua indices at one.

Flare options are world-space light direction x/y/z (default 0,0,1), fade_speed (default 4, 0–1024 visibility units/second), enabled (true), and unique name. Direction components are -1–1 and the vector must be nonzero; it is normalized before use. Disabled flares hide their sprites and retain their last visibility state. Visibility describes geometry occlusion; sprite alpha also includes the screen-edge fade and element alpha.

Element options are material (zero hides the element), width/height (32, 1–1024), axis (0, -2–2), alpha (1, 0–1), blend ("add" or "replace"), scale (1, 1–16), flip_x/y, mirror_x/y (false), and z_order (100 plus element index by default, -32768–32767). Axis zero pins an element to the light; positive/negative values place it along the light-to-screen-center line. Textured elements use texture dimensions for drawing and width/height for placement. Referenced materials cannot be removed. Additive elements retain the sprite blend contract: zero alpha skips them; nonzero alpha adds the full source. Use replace blending when alpha fading is needed.

Python uses keyword options and Lua uses options tables. Capability limits are max_particle_systems, max_flares, and max_flare_elements. Complete examples are examples/python/jet_particles.py and examples/lua/jet_particles.lua.

Optional effects

Jet library 1.7 or later reports postfx=True. postfx(...) partially updates global effect options; Python uses keywords and Lua an options table. All effects default to disabled. Geometry keeps the direct RGB565 presentation path, and disabled effects allocate no image scratch buffers.

OptionDefault and meaning
fxaaFalse; five-tap luminance edge smoothing.
bloomFalse; thresholded, separable radius-three bloom. RGB565 channel sum above 75 seeds the blur.
crtZero disables; 1–255 darkens odd display rows, with 255 making them black.
motion_blurZero disables; 1–100 is the previous filtered frame's percentage. The first frame initializes history; 100 retains that frame.
chromaticZero disables; 1–32 shifts red and blue sampling in opposite horizontal directions, clamping at edges.
pixelateOne disables; 2–32 fills square blocks from their top-left sample.
cel_bitsZero disables; 1–8 discards low bits of scalar diffuse lighting in the existing raster pass. Ambient and gloss remain continuous.

Image effects run after geometry and particles, before all sprites, in this order: FXAA, bloom, CRT, pixelation, chromatic shift, motion blur. Cel shading acts during lighting. Picking continues to describe geometry before filtering. Filters read and write the configured RGB565 byte order within their work; presentation does not add a conversion pass.

postfx_info() returns settings, image scratch_bytes, and history_valid. postfx_reset() invalidates temporal history without changing settings. postfx_clear() disables all effects and frees their scratch buffers. Disabling every image effect also frees scratch, even if cel shading remains enabled. Newly enabling motion blur resets its history. Failed allocation retains the previous effect configuration.

Scratch is reused between frames: FXAA needs width * 8 bytes, bloom width * height * 4, motion blur width * height * 2, and chromatic shift width * 2. CRT, pixelation and cel shading need no image scratch. Buffers for individually disabled filters remain reusable until all image effects are disabled or postfx_clear() is called. These optional passes add render work; enable only the effects the scene needs.

Complete examples are examples/python/jet_effects.py and examples/lua/jet_effects.lua.

Tiled indexed textures

Jet library 1.8 or later reports tiled_textures=True. Existing texture constructors and setters accept three additional options:

OptionDefault and meaning
tiledFalse; true adds owned 32×32 indexed tile storage. Requires format="index8".
filter"nearest"; also "bilinear", "three_point", and "cached_bilinear". Controls tiled sampling; the ordinary bilinear option still applies to RGB textures.
mip_levelZero; 1–5 selects an owned nearest-downsampled mip at 1/2 through 1/32 dimensions. Tiled 3D sampling uses this mip; sprite images retain their original dimensions and pixels.

Bilinear and three-point filtering decode palette colors before interpolation. Cached bilinear precomputes the exact 1024×1024 integer-UV lookup into 2 MiB of RGB565 storage. It produces the same samples as bilinear filtering. Changes to pixels, palette, offset or mip rebuild this cache before publishing the update; rebuilding can temporarily require another cache's worth of memory. Uncached palette-offset animation does not allocate or copy image pixels.

Tile pages, mip data and caches are owned by the provider. The original row-major indices remain available for sprites. Texture handles and all material references stay stable across updates. Turning off tiled releases tile storage. texture_info() adds tile_bytes, filter_bytes, and sampler_width/height. load_bmp() produces RGB textures and does not accept tiled mode.

sample_texture(texture, u, v) samples any texture and returns its quantized RGB888 color. Coordinates are normalized numeric values in -32–32; wrap, clamp and zero addressing follow the texture settings.

Complete examples are examples/python/jet_tiles.py and examples/lua/jet_tiles.lua.

Procedural shaders

Jet 1.9 adds capabilities().shaders and up to 16 scene-owned shaders. Python uses shader(callback=None, **options) and shader_set(handle, callback=None, **options). Lua uses shader(callback, options) and shader_set(handle, callback, options); pass nil for a built-in shader. shader_info(handle) reports options, calls and failed for the last render. remove(handle) releases an unused shader; materials referring to it prevent removal.

OptionDefaultMeaning
kind"checker"checker, uv, depth, callback
color, secondarywhite, blackChecker colors in RGB888
scale8Checks per UV unit; screen-space cell size in pixels; depth shader white point in depth-buffer units; 1–4096
screen_spacefalseUse pixel X/Y instead of normalized UV coordinates
alpha_key-1Transparent RGB888 color, compared after RGB565 quantization; -1 disables
nameemptyResource name

Bind with material(shader=handle, shading="unlit") in Python or material{shader=handle, shading="unlit"} in Lua. Shaders override texture sampling; the material's lighting, alpha and blend behavior still apply. Use shader=0 to detach. Shader materials support filled 3D geometry, including shared instances and LOD. Wireframe, water reflection, HUD sprites and flare elements reject shader materials. Built-in uv visualizes wrapped U/V as red/green; depth maps depth linearly to grayscale.

Passing a function selects kind="callback". It receives (u, v, params) and returns an integer RGB888 color. UV coordinates are normalized, can extend outside 0–1, and use the material's perspective setting. With screen_space, u and v are pixel coordinates. params contains x, y, depth, frame (starting at zero), local and world. The last two are integer XYZ tuples/tables reconstructed on the source face, including transforms and near/far clipping. Lua accesses params["local"] because local is a keyword.

Callbacks run synchronously for visible fragments after coverage and depth tests. Their functions stay alive with the scene, including through garbage collection; close/remove releases them. Calling Jet from a callback raises an error. A callback exception, invalid result, allocation failure or attempted Lua yield rejects that render without presenting its partial frame. The original script error is raised after the renderer returns. A later render can recover after replacing the callback. Set kind to a built-in to release a callback; omitting it in a partial update preserves the current kind.

Built-in shaders execute natively. Script callbacks allocate a parameter dictionary/table per fragment and can be much slower; use small surfaces or textures for complex patterns. See examples/python/jet_shaders.py and examples/lua/jet_shaders.lua.

Renderer controls

Jet 1.10 reports capabilities().renderer_controls. The existing settings() method additionally accepts texture_lod (false), texture_near (0), texture_far (0), and wireframe (false). settings_info() reports them. Distances are camera-space Z values in 0–16384; enabling texture LOD requires texture_far > texture_near. Invalid updates preserve all previous settings.

With texture LOD enabled, textures retain full detail through texture_near, fade toward the material's base color up to texture_far, then use that color alone. It does not fade procedural shaders. This is independent of mesh LOD. Global wireframe draws every object's outlines against a black background, skipping lighting, textures, shaders and depth. Turn it off to restore material shading; use clear=True for a clean transition. Examples are examples/python/jet_controls.py and examples/lua/jet_controls.lua.

Presentation and memory

Jet library 1.11 adds settings(background_cache=True) (Lua: scene:settings{background_cache=true}). It captures the next successful geometry frame's color and depth before particles, post-processing and sprites. Keep only the static background geometry visible for that frame, render once, then disable or remove it. Later frames restore the cached pixels and depth before drawing moving objects, preserving occlusion without rendering the static geometry again. capabilities() reports background_cache support.

The cache requires clear=True and wireframe=False. It retains a fixed view; camera, lighting, material and geometry changes do not update it automatically. Set background_cache=False to release it, then capture again for a new view. A failed or cancelled capture preserves the previous cache. Captured geometry does not participate in picking after it is disabled or removed.

The cache uses an additional width × height × 4 bytes of PSRAM. Recapture temporarily holds both the old cache and a new pair of buffers. Scenes release all cache storage on close.

Rendering uses the display's full dimensions. Width must be a multiple of eight; both dimensions must be at most 1024 pixels. The host owns three RGB565 color buffers and one depth buffer, each width × height × 2 bytes of PSRAM, in addition to loaded code, geometry and temporary render queues.

RGB565 targets receive wire-order pixels directly. MONO1 targets use luminance and 4×4 ordered dithering, with one additional ceil(width / 8) × height byte buffer. Terminal palette changes do not invert the scene. Transport may still rotate, scale or DMA-stage frames. E-paper refresh policy and settling determine visible motion independently of successful presentation calls.

Scripts provide asset contents to the loaders; the provider does not open paths. Indexed-only display output is not supported. The provider uses one fixed full-width, full-frame renderer profile. Scripts control scene behavior and optional effects; buffer layout, interlace, checkerboard reconstruction, overdraw diagnostics and parallel raster workers are build-time choices. Tiled storage, mip selection and filter caches are managed resources; upstream worker feedback/residency pointers are not script objects. Display size follows the selected target; close and reopen the scene to change targets. Native consumers can use the C provider table in solar_os_jet_abi.h through the resident library service.

Quick reference

OperationPython / Lua
Install providerpkg install jet
Open scenesolaros.jet.open(display, fps); defaults None/nil, 0
Geometryprimitive(...); mesh_data(vertices,triangles,material); mesh_update(object,...)
Surfaces and assetsmaterial(...); texture(...); load_obj(data,...); load_mtl(data); load_bmp(data,...)
Update viewcamera(x,y,z,rx,ry,rz,fov); light(ambient,color,intensity,azimuth,elevation)
Update objecttransform(object,x,y,z,rx,ry,rz); remove(object)
Shared geometryprototype(object,...); instance(prototype,...); instance_set(object,index,...)
Overlays and selectionsprite(...); pick(points); picks()
Animation and effectsparticles(...); flare(elements,...); step(dt); postfx(...)
Procedural surfacesshader(...); shader_set(handle,...); shader_info(handle)
Draw and inspectrender(); status()
Releaseclose(), Python with, Lua <close>
LimitsOne scene, 32 objects, 1024 input vertices and 2048 triangles per mesh; RGB888 colors; design coordinates -4096..4096

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())

Run the interpreter or a saved script from the shell:

lua
lua /apps/demo.lua argument

Use SolarOS services for files, hardware, networking, and UI. Lua's host io, os, and dynamic package loading are unavailable.

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.servo, solaros.motor, solaros.encoder, 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.speech, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc
Emotion enginesolaros.emotion facial animation and reaction sounds
Jet 3D renderersolaros.jet scenes, meshes, lighting and presentation
Input and clipboardsolaros.input, solaros.hid, solaros.clipboard
Board, apps, jobs, and identitysolaros.board, solaros.identity, solaros.jobs, solaros.sessions, solaros.apps
Contacts and messagessolaros.contacts, solaros.messages
SC2 end-to-end chat securitysolaros.sc2
Text user interfacessolaros.tui
Graphics, raster images, and DLP connectionssolaros.gfx, solaros.image, solaros.dlp
Camera, streams, and RTSPsolaros.streams, solaros.camera, solaros.rtsp
Computer visionsolaros.vision
Model inferencesolaros.inference
Native image pipelinessolaros.pipeline

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 requires PSRAM. Optional solaros tables appear only when their services are included in the firmware. Check pkg and board, or test a table before using it:

if solaros.wifi then
    print(solaros.wifi.status())
end

Vision, inference, and pipelines need their own packages; adding Lua alone does not enable them. See Packages and flavors.

Lua strings can contain arbitrary bytes. Byte-oriented calls such as uart.read, i2c.read_reg, and mqtt.read().payload return 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).

Raw SSH/SCP session handles are unavailable in scripts. Use the ssh and scp applications for remote access and file transfers.

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 audio and control API

The solaros.controllers feedback-controller API is documented in Feedback controllers. It provides PID and hysteresis instances, manual stepping, timestamped input, and periodic output bindings.

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

  • solaros.speech: say, cancel, request_status, and queue_status when offline speech is compiled. Start speechd first. say(text[, volume[, drop_if_busy[, pitch[, speed]]]]) copies up to 512 UTF-8 bytes, returns immediately, and returns a request ID. Pitch accepts 50 through 200 and speed accepts 20 through 500; both default to 100.
solaros.jobs.start("speechd", {"/voices/en-US"})
local utterance = solaros.speech.say("Solar O S is ready")
print(solaros.speech.request_status(utterance).state)

The selected directory must contain ta.bin and sg.bin; restarting the job with another voice directory changes the language. Accepted requests survive interpreter exit. Stopping speechd cancels the queue and releases the PicoTTS engine and voice buffers.

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; bind_receive(name, stream, address[, argument]) binds a zero-based incoming argument to a scalar sink and returns its ID. 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 and receive-stream 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.speech, 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

solaros.ble.pair() forgets the remembered keyboard and its bond before it starts pairing a replacement. Pairing replaces a conflicting stale keyboard bond from an older SolarOS release.

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. start(name, true) starts a manual host-selection lease, initially idle. hosts() lists saved HID hosts with address, addr_type, connected, and name. For manual leases, the current encrypted, bonded host's GAP Device Name is read asynchronously, without delaying reports. name and status().host_name are empty until available or when the host does not permit the read. Names are UTF-8 prefixes of at most 63 bytes, kept only for the current connection. Applications can cache them on mounted storage. For manual leases, connect(address, addr_type) advertises only to the selected saved host, forget(address, addr_type) deletes that host pairing, and disconnect() returns to idle while keeping pairings. Address type defaults to zero. The remote host initiates the BLE connection. Reconnect requests use the saved bond's authentication and Secure Connections settings; selecting a saved host does not require enabling pairing again on the host. Manual pair() drops the selected connection and advertises a fresh peripheral identity for a new host, preserving other saved hosts and the local keyboard. The default lease's pair() resets the previous host pairing on the public identity. Call it only for a user-requested new-host pairing flow. When bond storage is full, this explicit pairing flow may replace an unused bond, preserving the remembered input keyboard and all connected peers. 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 board, apps, jobs, and identity API

API overview · Python board, apps, jobs, and identity

solaros.board

solaros.board.info() returns a new table with the configured board information:

FieldValue
idStable board ID, such as solar_term.
nameHuman-readable board name.
moduleModule name, or an empty string when unspecified.
capabilitiesArray of capability names, such as wifi, gpio, and psram.
psram_bytesDeclared PSRAM capacity in bytes; 0 on boards without PSRAM.

These are build-time board properties, not a hardware probe or a list of installed firmware packages. Optional service bindings still depend on the firmware flavor. PSRAM capacity is not the current free heap. The board API is available whenever Lua is included, including headless script runners.

local board = solaros.board.info()
print(board.id, board.name)
for _, capability in ipairs(board.capabilities) do
    print(capability)
end

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, launch, open, can_open

list([include_playground]) includes installed Playground apps by default and returns name, id, title, summary, kind, and runtime. Playground launch names use playground:<id>; pass false to list only native apps.

launch(name[, args]) and open(path_or_url) replace the current script app, so a successful handoff does not return. args is an array of strings without the app name. can_open(path_or_url) reports support for local file extensions and HTTP(S) URLs. find(name) continues to inspect the native registry.

When the launched app exits, the completed interpreter session closes and control returns to the shell.

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 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 registered 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, int, 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.

DRV8830 attachments also accept integer max_mv to configure the voltage represented by full normalized motor drive. See Motor control.

pcnt-encoder requires integer a, b, and mode (1, 2, or 4). Optional integer invert, pull, and filter_ns configure its inputs. See Encoder counts.

Camera DVP attachments also accept GPIO-number bindings d0..d7, siod, sioc, vsync, href, pclk, xclk, and optional pwdn/reset. The camera driver validates required pins and claims; board-owned cameras cannot be detached. For SCCB control, specify either i2c="i2c0" (or another existing named bus) or both siod and sioc GPIO numbers. These modes cannot be combined. A named bus retains its wiring and other attached devices when the camera stops.

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 camera, streams, and RTSP API

API overview · Python media API

Quick reference

  • Local sources: solaros.streams.list(), open(id[, options]), acquire_frame(handle), release_frame(frame), and close(handle).
  • Still images: solaros.camera.snapshot([size[, quality[, source]]]) or capture(path[, size[, quality[, source]]]).
  • Native decode/presentation: solaros.image.from_frame(frame), decode(data), present(image, x, y[, width, height]), and close(image).
  • QR detection: solaros.vision.qrcodes(image[, options]) when service.vision is compiled; see Computer vision for payloads, regions, and results.
  • Network receiver: solaros.rtsp.open(url[, video[, audio]]), status(handle), read_frame(handle[, timeout_ms]), lateness(handle, frame), and close(handle).
  • Handles belong to one runtime. Release promptly; teardown also closes them.

Local streams

  • solaros.streams.list(): registered stream metadata.
  • info(id): registry metadata and the provider's default format.
  • status(handle): metadata with the opened handle's actual direction and negotiated audio/video format.
  • open(id[, options]): acquire an interpreter-owned source/sink handle.
  • close(handle), close_all(): release held frames, sources, and RTSP sessions.
  • read(handle, bytes[, timeout_ms]): bounded byte/PCM read, at most 16384 bytes.
  • write(handle, data[, timeout_ms]): bounded byte/PCM write; returns bytes written.
  • read_scalar(handle[, window_ms[, timeout_ms]]): one numeric sample.
  • acquire_frame(handle): lease one compressed JPEG frame from a video source.
  • frame_info(frame): codec, width, height, length, timestamp_us, and network flag.
  • frame_data(frame): explicitly copy JPEG bytes into the interpreter heap.
  • frame_save(frame, path): write JPEG bytes directly to storage.
  • release_frame(frame): return the native frame.

Open options use a dictionary/table. All streams accept direction (source, sink, or duplex) and timeout_ms. Default direction is source except for sink-only devices. Video accepts width, height, and jpeg_quality (0 selects the provider default; otherwise 1..63). Audio accepts sample_rate, channels, and frames_per_block; other audio properties come from the registered S16_LE format. Unsupported keys are rejected. Requested formats must be supported by the provider; there is no automatic resampling or format conversion.

There are four local handles per interpreter and one leased frame per handle. Frames are limited to 512 KiB. IDs are opaque integers, not pointers; released or closed IDs cannot be reused or passed to another runtime. Acquisition holds the same exclusive camera ownership as native consumers. Inspect info(id).owner before diagnosing a busy source. An active rtspd or cam-webd camera lease is not bypassed by scripts.

Byte reads poll cancellation between native waits of at most 50 ms. Timeouts are 0..60000 ms, with a 1000 ms read/write/open default. Reads may return a partial block or empty data; native timeout failures raise an error. Writes and scalar operations check cancellation before their bounded native operation. Camera acquisition uses the camera service's bounded capture wait. Event streams use their provider's byte-read representation; no generic event schema is invented here.

Camera convenience

Available when service.camera is compiled:

  • solaros.camera.status(): backend, sensor, owner, initialized/leased flags, selected size/quality, orientation (normal, mirror, flip, or 180), frame_buffer_count, capture count, and last error name.
  • snapshot([size[, jpeg_quality[, source]]]): return a native JPEG frame handle. Defaults are qvga, quality 12, and camera0.
  • capture(path[, size[, jpeg_quality[, source]]]): capture directly to a JPEG file and release the temporary source, including on write failure.

Sizes are qvga (320x240) and vga (640x480). A snapshot owns a temporary camera source until streams.release_frame(frame). For repeated captures, open the stream once and acquire/release frames in a loop instead of restarting the camera each time. Files are overwritten by capture/frame_save.

File-save failures include the resolved path, filesystem cause, and numeric errno in the Lua error (for example, No such file or directory (errno 2) for a missing parent). Capture releases its temporary camera source before raising; a failed frame_save leaves the caller's frame leased for retry/release.

Images without temporary files

When media.image is compiled:

  • solaros.image.decode(data): decode compressed JPEG/PNG/GIF/WebP bytes into a native PSRAM-backed image handle; the input is borrowed only during decoding.
  • from_frame(frame): decode a leased camera or RTSP JPEG without copying its compressed bytes into the interpreter heap. The resulting image is independent of the frame and remains valid after frame release.
  • to_rgb(image[, width, height]): return an owned packed RGB888 binary string, optionally resized with nearest-neighbor sampling. Supply both dimensions; the copy remains valid after image closure and must fit the Lua heap. This provides pixel buffers for model-specific normalization and quantization.
  • draw(image, x, y[, width, height]): existing clipped canvas drawing.
  • present(image, x, y[, width, height]): queue a native presentation. On RGB565-capable color displays this bypasses indexed palette conversion; other displays use the existing canvas path. The rectangle must fit the display.

Call gfx.begin() or gfx.begin(target) first. Present does not require a subsequent gfx.present(). Direct RGB565 pixels are not retained in the indexed canvas; a later canvas refresh can overwrite them. Draw chrome first and present video afterwards. The RGB565 cache is allocated lazily in PSRAM and released with the image. GUI-queue presentation failures are reported in the native log.

Image handles retain the existing 16-handle limit, 4 MiB encoded-input limit, and 2-million-pixel decode limit. Close images promptly; do not queue a growing history of video frames. Queued draws/presents retain their own image reference.

RTSP sessions

Available when service.rtsp-client is compiled:

  • solaros.rtsp.open(url[, video[, audio]]): start an asynchronous native session; video and audio default to true.
  • status(handle): negotiated/playing/ended flags, selected tracks, audio format, receive/drop counters, error code/name, and causal error_detail.
  • read_frame(handle[, timeout_ms]): lease one completed JPEG frame or return no value when none is available. Default timeout is zero; maximum is 60000 ms.
  • lateness(handle, frame): signed microseconds relative to the native presentation clock. Negative means early; positive means late.
  • close(handle): cancel, release any held video frame, stop native audio, and wait for the network worker before freeing its state.

One RTSP session is allowed per interpreter. At least one track must be enabled. The session uses the existing UDP RTP/JPEG and L16 receiver, RTCP timing, bounded audio jitter buffer, selected audio output, and global volume. Audio is played natively, not returned as PCM to the script. Audio-only sessions are supported with open(url, false, true) (Python uses False and True).

Open returns before negotiation completes. Check status().ended and error_detail to distinguish failure from an ordinary frame-read timeout. An ended audio-only session also returns no video frame. Use explicit apps.launch("rtsp", ...) if you want the complete native viewer/oscilloscope instead of managing a session.

There is one native compressed video buffer; reception drops frames while it is leased. Decode promptly, use lateness for scheduling or late-frame drops, and release the frame. RTSP frame metadata includes rtp_timestamp (90 kHz) and monotonic arrived_us; its timestamp_us is arrival time, not sender wall-clock time. Local camera timestamps are capture timestamps.

Current RTSP restrictions: trusted LAN, UDP media, JPEG/L16, IPv4/hostnames, and no TCP-interleaved media. Inline user:pass@ or a stored rtsp-auth account answers Basic or Digest MD5 on the control connection. The request URI and error text never include the password. Interpreters are never called from network/audio tasks.

QR detection

solaros.vision.qrcodes(image[, options]) processes an existing native image handle. Release a camera/RTSP frame after image.from_frame and before detection. Results contain binary string payloads and original-image corner coordinates; they remain valid after the image is closed. Processing is limited to 640 by 480 and eight decoded codes. Use pcall to close the image on error. See Computer vision for crop/resize options, result fields, and examples.

Image processing

service.imlib adds native histogram/statistics, binary thresholding/inversion, mean/Gaussian/median filters, erosion/dilation/opening/closing, image difference, and grayscale/LAB blobs under solaros.vision. Transformations return new image handles; source pixels stay unchanged. See Computer vision.

Model inference and background pipelines

solaros.inference loads resident models and checked bundles on ESP32-S3 with PSRAM. Use load_bundle and run_bundle for native image preparation and built-in classification or PICO detection results. Raw models expose named tensor inputs and outputs. See Model inference and Model bundles for the shared Python/Lua contract.

solaros.pipeline runs QR or model processing as an OS-owned background job over stored images, camera streams, or JPEG RTSP feeds. Scripts can configure a job, exit, and read its latest structured result from another session. See Native image pipelines for the positional Lua calls, installation, and camera ownership. Explicitly stop or destroy pipelines and unload models when finished; interpreter teardown leaves them resident.

Cleanup

Explicitly release frames and close sources/images/sessions on all paths. Interpreter teardown also cleans up on normal exit, cancellation, errors, and app handoff. Cleanup waits for native RTSP workers; it never force-deletes a worker using live buffers. If a driver repeatedly fails teardown, SolarOS logs that failure and retains live lease state rather than freeing memory under it. Optional camera/image/RTSP modules follow their native service package gates; adding scripting does not force those hardware/network packages into a build.

Examples

local solaros = require("solaros")
local frame = solaros.camera.snapshot()
local picture
local ok, err = pcall(function()
    solaros.streams.frame_save(frame, "/flash/snapshot.jpg")
    picture = solaros.image.from_frame(frame)
end)
solaros.streams.release_frame(frame)
if not ok then error(err) end

solaros.gfx.begin()
ok, err = pcall(function()
    solaros.image.present(picture, 0, 0, 120, 60)
    solaros.time.sleep_ms(2000)
end)
solaros.image.close(picture)
solaros.gfx["end"]()
if not ok then error(err) end

An audio-only receiver without handing control to another app:

local receiver = solaros.rtsp.open("rtsp://192.168.1.192:8554/music", false, true)
local ok, err = pcall(function()
    while not solaros.should_exit() do
        local state = solaros.rtsp.status(receiver)
        if state.ended then
            print(state.error_detail)
            break
        end
        solaros.time.sleep_ms(100)
    end
end)
solaros.rtsp.close(receiver)
if not ok then error(err) end

TCP byte streams

With TCP byte-stream support, solaros.streams.tcp.listen(name, port) creates a raw listener, connect(name, host, port) configures an outbound endpoint, and remove(name) removes an unclaimed endpoint. These create registered exclusive duplex byte streams; pass their names to solaros.dlp.connect or the stream-dlp expansion driver. Up to four endpoints share the TCP registry. The socket connects on first I/O and closes when its consumer releases the stream. Hostnames resolve at registration. These endpoints persist until removed or rebooted, including after the creating script exits. See stream commands and direct DLP connections.

BLE byte streams

With BLE byte-stream support, solaros.streams.ble.listen(name) registers a native CoC listener, connect(name, address, addr_type) configures an outbound stream, and remove(name) removes an unclaimed endpoint. Address type defaults to 0: 0 public, 1 random, 2 public identity, 3 random identity. Use the address and type from ble scan.

These configurations persist after the script exits. Claiming the stream starts its BLE connection; releasing it frees the connection and its data buffers. Use the name with solaros.dlp.connect or solaros.streams.open, close the consumer on every exit path, then remove configurations owned only by the script. A disconnected handle must be closed and reopened. The OS keyboard connection remains reserved. See BLE stream commands for connection roles, buffering, peripheral lease sharing, and security behavior.

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.list(conversation_id[, offset[, limit]]) reads a disk history page in chronological order. The default page contains the newest 64 messages. Offset skips messages from the newest end; limit is 1 through 64. Increase offset by the returned page length to read older pages.

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

solaros.messages.classes() lists class names, capabilities, and creatable. solaros.messages.add(class_name, name, settings) returns a new instance ID; messages.configure(instance_id, settings) changes its settings. Pass settings as a table of at most eight strings in shell argument order. For example:

local community = solaros.messages.add("irc", "community", {"irc.example.org", "6697", "nils"})
solaros.messages.configure(community, {"sasl", "account", "password"})
solaros.messages.connect(community, true)

The corresponding provider job must run to connect. Authentication settings are saved in the instance profile and are never returned by enumeration.

solaros.messages.instances() lists instance IDs, names, provider classes, capabilities, and connection status without authentication secrets. Use messages.join(instance_id, channel), messages.query(instance_id, nickname), messages.leave(conversation_id), and messages.connect(instance_id, enabled) on that same submodule. Join and query return a conversation ID. Connection settings are saved; the provider job must also be running.

solaros.messages.remove_instance(instance_id) closes and removes a configurable instance, credentials, pending sends, local history, and Inbox notifications. It protects the default instances without backend removal support and requires writable mounted storage. Removed instances are omitted from instances(). Creating the same name again allocates a different ID.

Conversation summaries include instance_id, provider_class, join_state (0 none, 1 left, 2 joining, 3 joined, 4 failed), and join_error. Conversation summaries include the raw title, a display label that identifies MeshCore group transports, and history_only. History-only conversations are readable but reject sends.

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, powered, enable_control, and enabled. Output enable state is separate from the supply state.
  • power(enabled[, name]): change a driver-managed receiver power rail, defaulting to the first receiver, and return the requested state.
  • enable([name]) / disable([name]): enable or silence NMEA output through recognized UBX/CASIC configuration support. Return true or false respectively. Unsupported receivers raise an error; a disable request retains the UART claim and does not power down or electrically isolate it.
  • 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.encoder

Available when the common encoder service is compiled. list() returns metadata records with name, driver, integer mode, and can_reset. status(name) adds signed integer count; read(name) returns that count. reset(name) sets it to zero when supported. Names are required and service errors raise errors. MODE_X1, MODE_X2, and MODE_X4 equal 1, 2, and 4. See Encoder counts for attachment, wiring, and count limits.

solaros.motor

Available when the common motor service is compiled. list() returns endpoint metadata; status(name) returns normalized state and optional driver diagnostics. Use drive(name, value) with a finite signed value from -1.0 to 1.0. Zero coasts. coast(name), brake(name), and clear(name) share the shell semantics; optional unsupported operations raise an error. Names are required.

The same API works with all registered motor drivers. A normalized command is not a measured speed or voltage. See Motor control for record fields, fault-mask constants, driver scaling, lifecycle, and examples.

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

  • Constants: FREQ_MIN, FREQ_MAX describe the native backend's range.
  • providers(): return records with name, driver, outputs, and detachable.
  • status(): return records with pin, allowed, active, channel, provider, output, frequency_group, owner, freq_hz, duty_percent, enabled, reserved, resolution_bits, and pulse_width_us. Pulse width reports configured hardware duty, even when disabled. Reserved outputs remain active while disabled. pin is a native GPIO number, or -1 for outputs without a GPIO mapping. frequency_group identifies outputs sharing timing within that provider.
  • set(output, freq_hz, duty_percent): start or update PWM with duty 0..100.
  • pulse(output, freq_hz, pulse_us): command an integer pulse width in microseconds, rounded to hardware ticks. The pulse must be shorter than the period; zero is accepted.
  • off(output): stop general PWM output.

Pass an output string such as "native:1" to set, pulse, and off. An integer selects a native GPIO. Providers enforce their own shared frequency groups; conflicting requests are rejected within a group.

solaros.servo

Available when the servo service is compiled. Use list(), status(name), open(name, output, min_us, max_us, min_deg, max_deg[, freq_hz[, inverted]]), set(name, angle_deg), pulse(name, pulse_us), disable(name), and close(name) for calibrated positional outputs. Use a boolean for inversion. Opening sends no pulses; commands start output. Outputs belong to their creating runtime and are closed on interpreter exit. Status reports commanded position.

See Servo outputs for calibration, record fields, examples, and resource limits.

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.servo, solaros.motor, solaros.encoder, 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

Direct DLP connections

When dlp_client is selected, solaros.dlp.connect(stream, format=MONO1, timeout_ms=5000) opens a private DLP connection over a registered exclusive duplex byte stream. In Lua, pass optional format and timeout positionally. It does not register a display target or allocate text/graphics composition buffers. TCP endpoints can be configured through solaros.streams.tcp when TCP byte-stream support is available; UART and radio streams are configured through their providers.

local solaros = require("solaros")
solaros.streams.tcp.connect("pico0", "192.0.2.10", 7777)
local ok, err = pcall(function()
    local link <close> = solaros.dlp.connect("pico0")
    local info = link:info()
    link:present(string.rep(string.char(0), info.frame_bytes)) -- white MONO1
end)
solaros.streams.tcp.remove("pico0")
if not ok then error(err) end

Use info() for width, height, format, byte stride, frame_bytes, accepted formats bitmask, capabilities, model, and firmware. present(pixels) requires exactly frame_bytes packed bytes and returns after presentation completes; errors propagate to the script. MONO1 uses black=1, LSB-first horizontal bits, with each row padded to a whole byte. GRAY2/GRAY4 use LSB-first packed indices, GRAY8 uses one byte per pixel, and RGB565_BE uses the high byte first. Unused row-padding bits must be zero.

Format constants are solaros.dlp.MONO1, GRAY2, GRAY4, GRAY8, and RGB565_BE; pass -1 to select the peripheral's preferred format. timeout_ms accepts 100..60000 and sets discovery and the minimum command timeout. Peripheral presentation deadlines may extend command waits.

close() releases the stream claim and is idempotent. Use colon calls for connection methods; Lua 5.4 <close> provides scoped cleanup. Interpreter teardown also closes all outstanding connections, including after an error or cancellation. Each interpreter supports four private connections. After an I/O failure, another present attempts a fresh connection and full repaint; geometry and format capabilities must remain unchanged. Close and reconnect if the peripheral configuration changes.

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)

Raster images

For JPEG data, camera/RTSP frame decoding, and direct RGB565 presentation, see Camera, streams, and RTSP.

When the firmware includes media.image, solaros.image decodes static PNG, JPEG, GIF, and WebP files into native PSRAM-backed handles. This keeps offline map tiles and other large images outside the Lua heap. Up to 16 handles can be open at once.

local image = solaros.image
local tile = image.open("/sdcard/maps/12/2200/1342.png")
local source_width, source_height = image.size(tile)
image.draw(tile, -24, 32)
image.draw(tile, 240, 32, 128, 128)
gfx.present()
image.close(tile)

image.draw(handle, x, y) uses the source size. Supplying both width and height applies nearest-neighbor scaling. Coordinates can be negative and the native renderer clips to the display. A queued draw keeps its own reference, so closing or replacing a tile immediately after queuing it is safe.

Colors

gfx.theme() returns a table with foreground and background RGB888 integers from the current setterm colors. It does not require gfx.begin() or claim a display. Use these literal values for Jet materials and backgrounds. Read the table again after a terminal color change to obtain the new 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])

When media.image is present, solaros.image provides open(path) and its load(path) alias, size(handle), draw(handle, x, y[, width, height]), close(handle), and close_all(). to_rgb(handle[, width, height]) copies packed RGB888 pixels into an owned binary string, optionally resized with nearest-neighbor sampling. Supply both dimensions; the copy remains valid after image closure. Output is limited to 2 million pixels and must fit the Lua heap. Runtime cleanup closes remaining handles.

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

With dlp_client, solaros.dlp.connect(stream[, format[, timeout_ms]]) returns a private connection with :info(), :present(pixels), and idempotent :close(). The default format is solaros.dlp.MONO1, timeout 5000 ms. Lua <close> and interpreter teardown release the exclusive byte-stream claim. present accepts exactly the packed info.frame_bytes bytes and waits for completion. No display is registered. solaros.streams.tcp.connect(name, host, port) creates an outbound stream; solaros.streams.tcp.remove(name) removes it after the connection closes. With BLE byte-stream support, solaros.streams.ble.connect(name, address, addr_type) configures a native CoC stream; solaros.streams.ble.remove(name) removes it after use. See BLE byte streams.

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

Foreground Lua apps can use capture_keyboard(name), read_key(), and release_keyboard() to exclusively capture a ready local key-event source before SolarOS shortcuts and character decoding. read_key() is nonblocking and returns nil or a table with type="key", source metadata, physical_key, canonical HID usage, translated key, modifiers, and action. Zero usage means the source cannot identify that HID key. Actions are KEY_PRESS, KEY_RELEASE, and KEY_REPEAT. Modifier bits follow USB HID order; MOD_CTRL and MOD_ALT match either side.

One foreground Python or Lua app can own a capture. The selected source bypasses focus switching and the usual app-exit key, including while another session has focus; the script must provide an exit chord and check should_exit(). Port characters and other sources retain their normal routing. Release outside pcall on exit; runtime teardown also releases the capture. The 32-event queue returns {type="reset"} before subsequent keys on overflow, source release, readiness loss, or detach. Release forwarded held keys on reset. Detach ends capture of that source even if the source ID is reused. Headless runners cannot claim keyboards.

When service.input-keymap is included, solaros.input.load_keymap(name, path) loads a validated JSON mapping file for the named attachment, and solaros.input.reset_keymap(name) restores its built-in map. Both return no values; failures raise an error. Applying a map releases held keys. Mappings last until detach or reboot; a startup script can reload a saved file. solaros.input.keymap_info(name) reports whether a source supports mapping, physical keys, modifiers, layers, and tap/hold, together with key count and optional matrix metadata. Other drivers currently report supported=false. See source keymaps for fields and mapping formats.

  • solaros.input: sources, read, clear, status for foreground pointer, axis, and gesture events

Generic pointer, axis, and gesture input

Touchscreens report tap, double-tap, hold, swipe, and edge-swipe gestures in addition to pointer events. Match them with solaros.input.GESTURE_TAP, solaros.input.GESTURE_DOUBLE_TAP, solaros.input.GESTURE_HOLD, and solaros.input.GESTURE_SWIPE, and solaros.input.GESTURE_EDGE_SWIPE. See man media.input for timing, direction, and recognition limits.

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.

HTTP URLs are limited to 2047 bytes. Redirect following supports relative URLs and statuses 301, 302, 303, 307, and 308. HTTPS downgrade and credentials in redirect URLs are rejected. Origin changes discard authorization and cookie headers. POST changes to GET after 301/302; 303 changes to GET except for HEAD; 307/308 preserve the method and body.

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 backpressures the native worker until the script drains it or cancels the stream; body bytes are never dropped. 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 SC2 end-to-end chat security API

API overview · Python SC2

solaros.sc2 in Lua has the same functions, arguments and result fields as the Python module: identity, status, keys_publish, add_bundle, peer, safety_number, forget, dm_encrypt, dm_decrypt, room_create, room_share, room_rotate, room_encrypt, room_decrypt, room_info, room_forget, post_sign, post_verify and selftest. Results are tables instead of dicts, nil replaces None, and errors are raised as sc2: <reason>.

Keys never enter the Lua state: scripts see frames, plaintext and public metadata only.

local sc2 = solaros.sc2
local me = sc2.identity().device_id
local frame = sc2.dm_encrypt(peer, '{"t":"text","body":"hi"}')
local msg = sc2.dm_decrypt(line)
if msg.kind == "message" then print(msg.from, msg.body) end

Quick reference

Use solaros.sc2 for SC2 end-to-end encrypted DMs, private rooms and signed public posts; the calls match man python.sc2. Keys stay in C. Errors raise sc2: <reason>. See man lua for runtime conventions.

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, stat, exists, scandir, read_file, write_file, rescan, blocks, block_count, block, usage_for_block, mkdir, makedirs, rmdir, remove, rename, copy, mount_volume, unmount_volume

stat(path) returns type, is_file, is_dir, size, mtime, and mode. scandir(path[, cursor[, limit]]) returns an entries table and a numeric next_cursor, which is absent at the end. The default page size is 32 and the maximum is 128. Directory cursors are offsets into the current enumeration; restart without a cursor if the directory changes between calls.

makedirs(path[, exist_ok]) creates missing parents. exist_ok defaults to true.

mount_volume(image_path, mount_point) also mounts a raw FAT disk image. Use unmount_volume(mount_point) before opening the image in a guest. See disk images.

Creating, saving, and loading files

write_file(path, data[, append]) creates or overwrites a regular file and returns the byte count written. data is a Lua string, which can contain binary data, including zero bytes. Each call accepts up to 65536 bytes. append defaults to false; pass true to add data to the end. Empty data creates an empty file, or truncates an existing file when append is false.

The file is flushed, synced, and closed before success. Parent directories must exist; use makedirs() to create them. A failed write can leave partial data; overwriting is not atomic. Filesystem failures raise a Lua error.

read_file(path[, max_bytes]) returns a string containing up to max_bytes bytes from a regular file. The default is 4096 and the maximum is 65536; larger files are truncated to that limit in the returned string.

Paths follow solaros.storage.resolve(): ordinary paths use the preferred storage, while explicit mount paths select that volume. The preferred persistent storage is SD when mounted, otherwise internal flash.

solaros.storage.makedirs("/notes")
solaros.storage.write_file("/notes/example.txt", "hello from Lua\n")
solaros.storage.write_file("/notes/example.txt", "another line\n", true)
print(solaros.storage.read_file("/notes/example.txt", 512))

solaros.storage.write_file("/notes/empty.txt", "")
solaros.storage.write_file("/notes/data.bin", "\x00\x01\xff")

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

Model bundles and pipelines

Model inference · Python media

Use 0 to browse the hosted catalog and install complete bundles on SD or flash. Installation verifies the release files; load a bundle with model bundle PATH when you want to use it. Installation does not load it automatically.

For OS-owned background processing with one image input, see Native image pipelines. A pipeline keeps running after its configuring script exits and exposes a bounded latest result through the shell, Python, and Lua.

Quick reference

  • Bundles contain bundle.json, one .espdl file, and declared assets.
  • Contract: schemas/solaros-model-bundle.schema.json, version one.
  • Native Python/Lua API: solaros.inference.load_bundle and run_bundle.
  • Python client helper: examples/python/model_bundle.py. Copy it alongside your calling script; it is not a built-in module.
  • Runner: examples/python/infer.py, copied beside model_bundle.py.
  • Hashes and runtime/port compatibility are checked once when opening a bundle.
  • Models belong to the OS and stay resident until explicit unload or reboot.

Contract and validation

Required fields are schema=1, id, version, runtime, model, inputs, outputs, and result. Optional fields are assets and license. Unknown fields are rejected. Manifests are limited to 64 KiB. Identity/version strings contain letters, digits, dots, underscores, or hyphens, at most 100 characters.

runtime declares the backend, exact validated version, and target: {"backend":"espdl","version":"3.3.13","target":"esp32s3"}. model declares relative file and lowercase hex sha256. assets maps relative filenames to hashes, at most 32 entries. license contains id and file, with that file declared in assets. Paths cannot be absolute, contain parent traversal, empty segments, backslashes, or control characters. Hashes detect content mismatch; they are not catalog signatures.

inputs/outputs map exported names to dtype, shape, and exponents, matching native descriptors exactly. Inputs also declare an adapter with type and optional options. Shape axes have the meaning declared by the adapter. Tensor and port limits follow the native service.

Input adapters include tensor, passing raw buffers or typed tensor records directly, and image, accepting caller-owned native image handles. The tensor adapter has no options; image options are described below. Multi-input and non-image models use the same resident loader.

JSON Schema validates structure. The host builder checks adapter semantics, sizes, channel axes, detector geometry, output references, and assets. The native loader independently validates built-in contracts, verifies hashes, and compares actual model interfaces. It rejects duplicate keys, embedded NUL, trailing non-whitespace data, and JSON nesting beyond 32 levels. Numerical validation remains model-specific.

Native image preparation

Python and Lua expose this call when both image and inference are compiled:

solaros.inference.prepare_image(model, input_name, image [, options])

The model port determines output dimensions, type, and quantization.

OptionValues/default
layoutNHWC default, NCHW, HWC, CHW; batch must be one
colorRGB default, BGR, GRAY; three or one output channels
resizestretch default, letterbox; nearest-neighbor floor sampling
mean, stdOne broadcast value or one per output channel; defaults zero/one; finite values and positive standard deviation
padOne or per-channel byte before normalization, default zero
x, y, width, heightSource crop; zero dimensions select the remaining image

Bundle image adapters declare layout, color, and resize explicitly. Supported prepared types are int8, uint8, int16, and float32. Integer outputs use (pixel-mean)/std/2**exponent, rounded with floor(value+0.5) and saturated. Zero point is zero. Quantization is per-tensor or per-output-channel, with the axis determined by layout; supported exponents are -126..126. Float32 uses (pixel-mean)/std. Grayscale uses (77*R+150*G+29*B)>>8 before normalization.

Letterbox preserves aspect ratio, floors resized dimensions to at least one pixel, and centers the content. Extra odd padding goes to right/bottom. Sampling, channel/layout conversion, normalization, and quantization are fused. Temporary lookup storage uses PSRAM and is freed on every return. Processing yields and checks cancellation between rows; it does not modify source pixels.

Results contain data, transform, and preprocess_us. Python data is an owned bytearray; Lua data is an owned binary string. It survives image/model closure and must fit the interpreter heap. Transform fields report source/input sizes, crop origin/size, resized sizes, and left/top padding. Inverse coordinates are:

source_x = crop_x + (input_x-pad_left)*crop_width/resized_width
source_y = crop_y + (input_y-pad_top)*crop_height/resized_height

preprocess_us includes lookup allocation, processing, and native yields; it excludes decoding and interpreter result allocation/construction. Invalid options, unsupported geometry/type, allocation failure, and cancellation raise errors; partially prepared buffers are not returned.

Result adapters and residency

  • raw: owned native output tensors, without interpreting their meaning.
  • classification: one int8/uint8/int16/float32 class vector, checked labels, softmax or identity activation, and configured top-k results.
  • pico-detection: single-class int8 score/DFL stages with declared tensor names, strides, bins, score/IoU thresholds, bounded candidates, and output limits.

Classification payloads contain kind="classification" and classes with id, label, and score. Detection payloads contain kind="detection", coordinates="source_pixels", source dimensions, truncated, and detections with id, label, score, and [x1,y1,x2,y2]. Boxes use inclusive integer source coordinates, clipped to source bounds. NMS operates in source coordinates. truncated reports candidate/result limits, not ordinary NMS suppression. PICO filtering uses its squared-score convention.

from model_bundle import ModelBundle
import solaros

with ModelBundle("/dl/models/imagenet/bundle.json") as model:
    picture = solaros.image.open("/pictures/example.png")
    try:
        result = model.run({"input.1": picture})
        print(result["result"])
    finally:
        solaros.image.close(picture)

run requires all named inputs and returns owned native outputs, native timings, interpreted result, per-input transforms, preprocess_us, postprocess_us, and postprocess_ms. The helper adds call_ms, covering preparation, inference, postprocessing, and binding overhead. Caller image handles are not closed by adapters. Built-in preparation and result decoding run natively; prepared tensors and adapter workspace use PSRAM.

ModelBundle(path) reuses an existing bundle at that resolved path, or loads one when absent. ModelBundle(handle) attaches to an existing bundle. reset() resets the shared resident model. close() explicitly unloads it and is idempotent for that helper. detach() and context exit drop the client handle without unloading. Script exit and execution errors leave the model available to other clients; use model list to discover it and model unload to release it. Failed native bundle validation never publishes a handle. The helper allows 60000 ms for loading, including hash checks, and 10000 ms for execution by default; override load_timeout and timeout separately.

For custom Python adapters, select ModelBundle(path, native=False). This uses the reference interpreter loader over the same OS-owned raw runtime. The native loader supports only the built-in adapter types listed above. Trusted applications can register functions in INPUT_ADAPTERS and RESULT_ADAPTERS before loading. Input functions receive (bundle,name,value,options) and return (buffer,transform_or_none,preprocess_us). Result functions receive (bundle,outputs,transforms,options) and return a payload. Install adapter code separately; manifests do not execute code or automatically import modules. The raw native runtime does not require adapters.

File and stream runner

python /dl/infer.py /dl/models/imagenet/bundle.json /pictures/example.png
python /dl/infer.py /dl/models/pedestrian/bundle.json /pictures/example.png --count 5
python /dl/infer.py /dl/models/pedestrian/bundle.json stream:camera0 --count 20
python /dl/infer.py /dl/models/pedestrian/bundle.json rtsp://host:8554/camera --count 20
python /dl/infer.py /dl/models/arithmetic/bundle.json /inputs.json
model list
python /dl/infer.py 1 /pictures/example.png
model unload 1

The first argument is a bundle path or an existing bundle handle. The runner leaves the model resident after completion or cancellation.

Single-image models accept a picture path. For non-image or multi-input models, an input JSON map names every file, such as {"a":"a.bin","b":"b.bin"}. Relative paths resolve beside that map; absolute storage paths work. Tensor files are binary and images use native decoding. Files/images are reused for repeated calls; every image closes afterward.

--mode single|auto|dual selects execution. --count N limits successful results: files default to one, streams to unlimited; zero means unlimited for streams. --max-age MS defaults to 250; zero disables filtering. SolarOS permits eight app launch arguments, so limit the number of options used together.

Streams open once and lease at most one compressed frame. Stale frames are discarded before decoding. Compressed leases return before preprocessing and inference; independent decoded images close after each result. There is no growing frame queue. Up to four consecutive invalid JPEGs are skipped; the fifth fails and closes the source while keeping the model resident. Local cameras remain exclusive, so a direct stream cannot open a camera already owned by rtspd/cam-webd. Received RTSP video is a separate source.

Stdout contains JSON records with schema=1, event="inference", model_handle, model identity, source, sequence, frame metadata, result, transforms, timings, and cumulative stale/decode-error counts. File records have no frame timestamp. Camera timestamps are capture time; RTSP timestamps are arrival time, with native RTP metadata retained. source_latency_ms measures time since that timestamp, rather than sender-to-result RTSP latency. inference_us measures model execution; preprocess_us and postprocess_us measure native adapters separately. These timings exclude image decoding and do not establish camera FPS. Raw CLI results contain hex data up to 4096 bytes, otherwise SHA-256 and metadata; the library retains the actual bytes. The runner does not publish MQTT/OSC or produce annotated video.

Installing a bundle directory

Copy the complete bundle directory, including its model and declared assets, to a mounted storage volume. Load its bundle.json with model bundle, solaros.inference.load_bundle, or the native inference service table. Installing files does not load the model. A loaded model remains resident until explicitly unloaded or the device reboots.

Model definitions, zoo metadata, publishing tools, validation results, and their documentation are maintained in the separate solar_os_zoo repository. SolarOS owns the runtime bundle specification and its native/interpreter APIs.

Scripting APIs · SolarOS manual

Model inference

Python API · Lua API

Quick reference

  • Enable inference (service.inference) on ESP32-S3 with PSRAM.
  • model backend shows whether the engine is built in or supplied by a library.
  • When the engine is absent, use pkg install espdl, then load a model normally.
  • solaros.inference.load(path) loads a compatible .espdl file and returns a resident integer handle; close(handle) releases it.
  • inputs(handle) and outputs(handle) describe named tensors.
  • run(handle, inputs) accepts every named input and returns owned output buffers, tensor descriptors, and native timing statistics.
  • Use reset(handle) for stateful operators and explicit unload(handle) to release a model.
  • list() discovers resident handles; find(path) locates a loaded model or bundle.
  • load_bundle and run_bundle provide native manifest validation and built-in adapters.

Availability and ownership

solaros.inference requires service.inference, provided by the inference group. Standard flavors enable this group on ESP32-S3 boards with PSRAM. Camera, display, and image services are optional. No models are built into the firmware; put compatible .espdl files on a mounted SolarOS storage volume.

The inference_backend group (backend.inference-espdl) includes the engine in firmware. Custom flavors can enable it; the standard full and compact writerdeck profiles use the downloadable espdl library. Other custom flavors can select either configuration. The shell, Python, Lua, native clients, and bundle adapters use the same APIs in both configurations.

On firmware using the library, mount storage and install it from a signed catalog available for your firmware version:

pkg install espdl
model backend
model load /sdcard/model.espdl

Loading a model acquires the installed library without network access. Model loading never downloads an engine automatically. Models keep it in use until unloaded. After closing all models, pkg unload espdl releases the engine's code and private runtime memory; the installed file remains available for the next load. pkg remove espdl also removes the file. Both commands refuse an engine still used by a model. Built-in firmware uses its built-in engine even when an external library is installed.

load returns an integer handle. The model file and workspace stay resident in native memory until explicit unload(handle)/close(handle), unload_all()/close_all(), or reboot. The OS owns models. Python, Lua, shell commands, and native clients can use the same handles across client lifetimes. Exiting a script cancels its pending request but leaves models loaded. Closed handles are invalid and are never reused during that boot. Removing or replacing a file does not change an already resident model; explicitly unload and reload to use the new file.

The backend is ESP-DL 3.3.13, with ESP32-S3 kernels. Models must be exported for this target and compatible operators. The loader accepts unencrypted, single-model EDL1 and EDL2 containers. Packed and encrypted containers are unsupported. File structure and bounded static interfaces are checked before ESP-DL construction; this is not a sandbox for arbitrary model graphs. Use models from a trusted exporter and validate their numerical behavior.

API

Python and Lua expose the same module functions:

FunctionResult or effect
load(path[, timeout_ms])Load from a shell-style storage path; return an integer handle
list()List all resident handles
find(path)First handle loaded from the resolved path, or Python None / Lua nil
load_bundle(path[, timeout_ms])Validate and load a bundle manifest; return a resident handle
run_bundle(handle, inputs[, timeout_ms])Prepare declared inputs, infer, and decode built-in results
info(handle)Backend, identity, references, memory estimates, and named input/output descriptors
inputs(handle)Map of input names to tensor descriptors
outputs(handle)Map of output names to tensor descriptors
run(handle, inputs[, timeout_ms])Owned output tensors and native timing statistics
reset(handle)Reset stateful operators while keeping the model resident
set_mode(handle, mode)Select "single", "auto", or "dual" execution for subsequent runs
unload(handle) / close(handle)Explicitly release one unreferenced model
unload_all() / close_all()Explicitly release every unreferenced resident model

info contains backend="espdl", backend_version, target="esp32s3", model_bytes, internal_bytes, external_bytes, inputs, and outputs. It also exposes path, references, bundle, bundle_id, bundle_version, and an image_inputs map. Metadata is a client-owned snapshot; later mode changes or unloading do not change a previously returned record. Memory figures are the observed free-heap difference across loading, not peak requirements; other activity and shared operator registration can affect them.

mode reports the selected execution mode, initially "single". "auto" lets ESP-DL choose parallel execution for suitable operators; "dual" requests splitting supported operators between both cores. Operators that cannot split still execute on one core. SIMD acceleration is available in all modes. For example, call solaros.inference.set_mode(model, "auto") before run. Mode changes do not reload or reset a model.

Dual-core worker storage is allocated on first parallel dispatch and reused across runs and resident models. It is released when the last resident model is explicitly unloaded. Load-time memory estimates exclude this later allocation. ESP32-S3 SIMD kernels execute from cached firmware flash; model tensors and workspace use PSRAM.

Every tensor descriptor contains:

FieldMeaning
dtypeElement type, such as int8, int16, or float32
shapeDimensions in the model's native axis order
bytesRequired dense buffer length
exponentsESP-DL power-of-two quantization exponents; empty for floating-point and Boolean ports

Buffers contain contiguous, row-major, little-endian elements. Shapes have no implicit image meaning. Supported descriptor types are int8, uint8, int16, uint16, int32, uint32, int64, uint64, float16, float32, float64, and bool; individual operators support a subset of these types.

For quantized tensors, real_value = stored_value * 2**exponent, with zero point zero. A single exponent applies to the whole tensor. Per-channel exponents are exposed as exported; use the model's contract for their axis. Raw run passes tensors without normalization, quantization, layout conversion, classification, or detection postprocessing. run_bundle performs the adapters declared by a validated bundle in the native service.

The optional image adapter prepare_image(handle, input_name, image[, options]) performs native preprocessing when media.image is also enabled. It is separate from raw run; see Model bundles and pipelines for options, contracts, result adapters, and resident file/stream execution.

Shell and native clients

model bundle /dl/models/imagenet/bundle.json
model list
model info 1
model mode 1 auto
model unload 1

model load loads a raw .espdl file; model unload all explicitly releases all resident models. model load defaults to 10000 ms and model bundle to 60000 ms; an optional trailing timeout_ms overrides either. Loading creates a new instance; use find or a known handle to reuse an existing one. Mode changes and resets affect that shared instance. Native jobs can retain/release a model through the service API. A retained model cannot be unloaded; unload reports busy. Unloading all models checks every reference first, so a retained model prevents any partial unload.

Named inputs and owned outputs

run requires every input exactly once, keyed by its exported name. Supply raw Python buffer objects or Lua binary strings to interpret bytes using the model's port descriptor. Alternatively, supply typed records:

{"data": buffer, "dtype": "int8", "shape": [2, 4], "exponents": [0]}

Typed records require data, dtype, and shape. Optional bytes must equal the buffer length. Optional exponents must match the port; omission inherits the port's quantization. Unknown fields, wrong names, missing inputs, mismatched types/shapes/quantization, and incorrect lengths raise errors.

The result contains outputs, a map of names to descriptors plus data. Python output data is bytes; Lua output data is a binary string. Output data and metadata remain valid across subsequent runs, resets, and model closure. Each call copies its outputs into the interpreter, so large outputs must also fit its heap. Inputs are borrowed only until the synchronous call returns.

input_us, inference_us, output_us, and elapsed_us report native copying and execution time, including cooperative yields. They exclude interpreter argument conversion, returned-object construction, and task startup.

Python example

This example assumes a model with two int8 inputs a, b, each shaped [2,4], and an output named sum. Inspect the descriptors for other models.

from solaros import inference
import struct

model = inference.load("/sdcard/models/arithmetic_int8.espdl")
try:
    print(inference.inputs(model))
    a = struct.pack("<8b", 1, -2, 3, 4, -5, 6, 7, -8)
    b = struct.pack("<8b", 2, 3, -4, 1, 6, -2, 0, 4)
    result = inference.run(model, {
        "a": {"data": a, "dtype": "int8", "shape": [2, 4]},
        "b": b,
    })
    values = struct.unpack("<8b", result["outputs"]["sum"]["data"])
    print(values, result["inference_us"])
finally:
    inference.close(model)

Lua example

local inference = solaros.inference
local model = inference.load("/sdcard/models/arithmetic_int8.espdl")
local ok, result = pcall(function()
    local a = string.pack("bbbbbbbb", 1, -2, 3, 4, -5, 6, 7, -8)
    local b = string.pack("bbbbbbbb", 2, 3, -4, 1, 6, -2, 0, 4)
    return inference.run(model, {
        a = {data = a, dtype = "int8", shape = {2, 4}},
        b = b,
    })
end)
inference.close(model)
assert(ok, result)
print(string.unpack("bbbbbbbb", result.outputs.sum.data))

Limits, cancellation, and errors

The OS can hold up to four resident models in total, subject to available memory. Each model can have up to 16 inputs and 16 outputs, rank up to eight, tensor buffers up to 4 MiB, and names up to 127 bytes. Model files are limited to 16 MiB and graphs to 4096 operators. Interfaces use fixed positive dimensions; dynamic input shapes are unsupported. File, workspace, interpreter heap, and other services must all fit simultaneously.

One native inference operation runs at a time across all clients. Competing load/run/reset/close calls report busy rather than queueing. Timeouts default to 10000 ms and accept 1–60000 ms. Cancellation is checked during file reads and between operators. Model construction and an individual operator are not preempted; a call can finish later than its requested timeout. Cleanup waits for the worker before releasing model or argument memory.

Missing files, unsupported containers/operators, invalid interfaces, allocation failure, busy execution, and timeout raise interpreter errors. A failed load does not publish a handle. Failed execution discards partial outputs and resets the model's operator state; a subsequent valid call can reuse the handle.

Scripting APIs · SolarOS manual

Named runtime buses

A named bus describes a hardware connection: I2C, SPI, UART, MIDI, 1-Wire, or PS/2. Scripts and attached devices refer to its name instead of repeating its wiring. SolarOS tracks which devices use it.

Inspect from the shell

expansion bus list
i2c status i2c0
i2c scan i2c0

Use the names shown on your device. The io app can create and manage buses; Expansion reference lists the wiring arguments.

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 new speed applies to subsequent transfers, including those from attached devices. In io, select the bus and choose Set I2C speed.

Transfer through the registry

Use the API for the bus type: 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

Native foreground apps

The optional set_memory_reclaimer hook lets an app release rebuildable caches while suspended. The host joins audio/presentation before enabling the hook and waits for reclamation before resume or unload. The callback may free owner-safe data only; it must not allocate, wait, or call foreground APIs. Check table size before registering. Ordinary module pointers remain fixed. See memory policy for the shared buffer and paging contracts.

Native foreground apps are trusted ESP32-S3 modules. They use the same sessions, display ownership, keyboard shortcuts and cold-state lifecycle as built-in apps. Emulators such as Apple II Plus, Game Boy and amiga are foreground apps. Their CPU cores and guest state belong to their modules.

Quick reference

pkg installed
pkg install apple2
pkg install gameboy
apple2 /sdcard/apple2/dos33.nib
gameboy /sdcard/roms/game.gb
session create apple2 display0 /sdcard/apple2/dos33.nib
load /sdcard/apple2.app.elf /sdcard/apple2/dos33.nib

Using installed apps

Catalog entries use type = "app" and lifecycle_abi = 2. pkg install ID installs an ELF under modules/apps/ID.elf in the preferred storage volume. Run ID [arguments] from a display shell, or use session create ID display0 [arguments]. load /path/app.app.elf [arguments] also recognizes foreground metadata. The filename does not determine the lifecycle of an app that declares metadata. Lifecycle-1 apps remain synchronous commands; their code is unloaded when main returns.

Relative media paths use the launching shell's current directory, including when session create launches an app on a display from a port shell. Apps that declare file extensions also receive the shell's normal file and directory Tab completion for their arguments.

Discovery reads ELF headers, dynamic symbols and the foreground manifest without loading the whole app into memory. Launch validates the complete ELF and checks that its manifest matches the session's admitted resource declaration.

Installed foreground apps appear in app discovery. An installed foreground app with a built-in app's ID takes precedence while installed. Each app can own one session. Different external apps can remain loaded in suspended sessions. Installing or removing a running or suspended foreground app is refused. Resource declaration changes to an app already discovered require a restart.

App lists and command completion share cached discovery. Package installs and removals, FTP changes, and storage commands refresh it on the next lookup. Changes made through direct file APIs may take up to 30 seconds to appear.

The package catalog has application, job, driver and library categories.

Developer reference: resource declaration

solar_os_native_app_abi.h defines the exported, pointer-free solar_os_native_app_manifest record. It declares the app ID, summary, GUI or TUI class, resumable flag, worker stack and priority, internal-memory and PSRAM minima, event tick interval, and optional file extensions. File extensions are space-separated lowercase suffixes such as .gb; opening a matching file passes its path as the first argument. The host validates this record before executing module code and checks it again when loading the session worker. Catalog lifecycle and ID must match the ELF metadata.

The declared memory minima are admission checks, not reserved heaps or quotas. Bulk guest state and executable images use PSRAM. An app owns its allocations and frees them before returning. Worker resources are allocated on launch; no app-specific startup hook runs at boot.

Large OS buffers use a managed PSRAM region that groups bulk buffers separately from resident executable images. mem shows its free space and largest block. Reclamation releases unused libraries and registered rebuildable caches, including suspended document layouts. Authoritative app state and unsaved edits remain resident. A workload can still be refused when its allocations do not fit.

Suspend, resume and close

Alt+Tab and Alt+Right select the next session; Alt+Left selects the previous session. Display shells also provide session fg ID. A resumable module polls suspend_requested at safe points, including while paused, and calls suspend. The host joins presentation and closes foreground audio before acknowledging the switch. Guest state, the ELF image and its worker remain allocated. Resume restores display ownership, clears held input and allows the worker to continue. The module rebases its timing, restores optional audio and submits a full frame.

Ctrl+Alt+Del, Ctrl+] or session close ID requests cooperative shutdown. The module polls stop_requested, saves any state required by its app, stops its callbacks, frees allocations and returns. The host joins presentation and audio before unloading the image. A worker that fails to suspend or close within five seconds causes a device restart; it is never forcibly deleted while executing module code. Native modules run in the firmware address space.

Foreground service

main(argc, argv) obtains solar_os_native_host_v1() and requests get_service("foreground", 1, required_size). Check the returned table and its size before using appended methods. Calls belong to that app's worker; tables cannot be used by another app or a shell task.

The service provides time and bounded sleep, display palette and asynchronous frame submission, input events including release and reset, path resolution, bounded file reads and atomic replacement, optional PCM audio, exit status, and cooperative suspend and stop. Input events carry logical key bytes defined by SOLAR_OS_KEY_* in solar_os_key_codes.h, alongside physical key identity, HID usage, modifiers, and press/release state. Each app has its own input queue, presenter, audio ownership and retained state. OS session shortcuts pass through captured keyboards. Modules must not leave callbacks or tasks running after main returns.

Physical keyboards and buttons retain their actual press/release timing. The pointer-arrows job supplies held arrow keys while its pointer moves and releases them when movement stops. Logical taps from character-only input have zero physical identity and HID usage. read_key delivers their press immediately and a synthetic release after 140 ms, measured from consumption by the worker. This keeps taps visible to frame-based guests even when the producer queues both halves together. Repeated taps of the same key extend its pulse. Input reset clears pending pulses; modules must clear held controls on reset.

The appended video_dimensions(&width, &height) method reports the calling app's rotated foreground graphics dimensions before video is opened. Modules that render text or other display-sized content can allocate a matching frame instead of assuming a board's resolution. Request a service table large enough to include this method. Fixed-resolution emulators can still submit their own frame size and use the presenter's aspect-preserving fit. Runtime display rotation refits the retained frame without reopening video or changing the guest's frame buffers.

present copies a submitted frame. For rendering into host-owned buffers, check for video_acquire and video_exchange. Acquire once after opening video, fill the returned buffer, then exchange it to queue the frame without a pixel copy. Exchange returns the next writable buffer, whose previous contents are unspecified. Each submitted buffer remains readable until the next exchange; do not write to it. Never free these buffers or mix this path with present. The writing buffer survives suspension, and the host restores the latest frame on resume. All host-owned buffers are released when the worker exits.

Use cooperate at regular CPU-work checkpoints. The host coalesces these requests while allowing periodic idle-task work. Use sleep_ms for timed waits, and continue polling stop and suspend requests. The solar_os_native_foreground_cooperate helper falls back to a short sleep on hosts without this method.

The appended disk_open, disk_read, disk_write, disk_sync, and disk_close methods provide exclusive access to fixed-size raw disk images. Request a table through disk_close before using them. disk_open returns an opaque handle and byte size; reads and writes take byte offsets and at most 64 KiB per call. Writes sync before returning. Claims survive suspension, and the host closes remaining handles before unloading the worker. Unmount a host FAT image before opening it as a guest disk.

Apps that set SOLAR_OS_NATIVE_APP_POINTER_EVENTS receive display-routed pointer events through the appended read_pointer method. Absolute positions use the rotated host surface; relative deltas are unscaled. Buttons are a held-state mask (bit 0 left, bit 1 right). Each opting-in app has a bounded pointer queue. Queue overflow and session switches produce a reset event; the module must release buttons and clear position anchors on reset.

The appended calendar_read method returns the host local calendar date and time. It does not provide a way to set the host clock. Request a service table large enough to include each method before using it.

audio_open opens the selected SolarOS playback output, including I2S expansion outputs, and returns its sample rate and channel count. audio_write accepts signed 16-bit PCM in that format. audio_close releases the output. An app can open and close it to discover the output rate before starting synth audio; direct PCM playback and synth playback cannot be open together.

Packed INDEX2 frames use four shades of the current display theme. The appended synth methods accept a bounded block size and required sample rate. Callbacks render signed 16-bit stereo PCM under a host mutex. The module uses synth_lock and synth_unlock when accessing shared synthesizer state; the host joins synth callbacks on suspend and close. The mutex remains available while suspended. Failed synth startup leaves video and emulation usable without audio.

The memory service provides PSRAM allocation and optional alloc_internal for hot state or caches. Internal allocation preserves the OS reserve and returns NULL when unavailable; modules can omit caches or fall back to PSRAM. The host also provides the current RTC register values, board-button source identity, and presenter statistics. Standard compiler helpers and setjmp/longjmp are exported for synchronous interpreter error handling.

Emulator apps

See Apple II Plus for bundled ROMs, NIB disk requirements, controls, and explicit disk saves. See Game Boy for cartridge compatibility, controls, and automatic cartridge RAM saves. See Tiny386 for bootable PC floppy and IDE images, embedded BIOS, and display requirements. See DOOM for IWAD requirements and keyboard controls. For example, with assets present:

apple2 /sdcard/apple2/dos33.nib
gameboy /sdcard/roms/game.gb
tiny386 /sdcard/tiny386/dos.img

Apple II disk writes remain in memory until explicitly saved. Session switches retain unsaved disk changes and each emulator's pause state.

Scripting APIs · SolarOS manual

Native image pipelines

Quick reference

  • pipeline start qr <source> creates an OS-owned QR processing job.
  • pipeline start model <source> <handle> uses a resident image bundle.
  • pipeline result <id> reads the latest result; stop releases workers and sources; destroy also removes the configuration and latest result.

The pipeline service connects an image source to a native processor in an OS-owned background job. Python and Lua configure and inspect the job; their sessions can exit while processing continues. QR and model processors share the same start, stop, and result commands.

Competing lifecycle controls can report busy. If another client takes control of the public job during startup, a retained configuration remains discoverable through pipeline list and can be explicitly destroyed.

Enable the pipelines group and the desired processor package (vision for QR, inference for ESP-DL). The full flavor includes these groups where supported. RTSP sources additionally require service.rtsp-client and a JPEG video track.

Start and control

Install a model and process the local camera

On ESP32-S3 with PSRAM, open 0, select the pedestrian PICO bundle, and download it. Zoo displays the installed bundle.json path in the entry's details. The example below uses SD storage and version 1.0.0; replace the path with the one shown by Zoo. With flash storage, the path starts with /flash/dl/zoo/ instead of /sdcard/dl/zoo/.

Use stream list to find the camera source and its current owner. If rtspd or cam-webd owns it, stop that job before starting a direct camera pipeline.

model bundle /sdcard/dl/zoo/models/pedestrian-pico/1.0.0/bundle.json
pipeline start model camera0 1
pipeline status 1
pipeline result 1
pipeline destroy 1
model unload 1

Replace the model and pipeline handles with those returned by the commands. pipeline result prints the newest completed record, including detections, source coordinates, and timing. Repeating it does not consume the result. pipeline destroy stops processing and releases the camera; the model remains resident until model unload. Another camera consumer can then be started.

Process a stored picture

model bundle /dl/models/pedestrian/bundle.json
pipeline start model /dl/pedestrian.png 1
pipeline status 1
pipeline result 1
pipeline destroy 1
model unload 1

Use the actual handles returned by model and pipeline. Sources are absolute image paths, native video stream IDs listed by stream list, or rtsp:// URLs. The shell also resolves relative image paths. Files run once when limit is zero; streams run until explicitly stopped. A positive limit selects the number of successful results. interval_ms adds a cancellable delay between frames (0 to 60000, default 0). Use job status pipeline-<id> to inspect ownership and job start pipeline-<id> to restart a retained configuration.

Up to four pipeline records exist until explicitly destroyed. Model processors require a resident native bundle with exactly one image input. General multi-input and non-image models remain available through inference.run and inference.run_bundle. Stopping or finishing releases the job's reference; the model stays resident until explicitly unloaded. Unloading a referenced model reports busy. Jobs waiting for worker memory have not yet retained their model; unloading it before admission makes the eventual start fail.

Local camera streams follow the provider's ownership rules. An exclusive camera source cannot be opened while another job owns it. Source failures terminate the pipeline and appear in last_error; native inference or QR admission contention discards that frame and increments busy_frames.

Python and Lua

Both expose solaros.pipeline with the same positional arguments:

from solaros import pipeline

p = pipeline.start("rtsp://192.168.1.10:8554/camera", "model", model_handle)
print(p)  # Processing continues after this script exits.
from solaros import pipeline

print(pipeline.list())
print(pipeline.status(p))
result = pipeline.result(p, last_sequence)  # None when no newer result exists.
pipeline.stop(p)       # Keep configuration and latest result.
pipeline.destroy(p)    # Stop and remove configuration and result.

Lua uses the same calls and returns tables (nil when no newer result exists). start(source, processor="qr", model=0, limit=0, interval_ms=0) returns an integer handle. Arguments are positional. Absolute paths and ./ paths resolve through the caller's storage context; other strings are interpreted as native stream IDs or RTSP URLs. Interpreter teardown does not stop pipelines. Stale handles fail.

Use detections in a Python script

This example processes ten camera frames with an installed pedestrian bundle. Change the bundle path and source to match your storage and camera. It reuses a resident model when available and reads structured detection records; the script does not need to parse the shell's JSON output.

from solaros import inference, pipeline, time

path = "/sdcard/dl/zoo/models/pedestrian-pico/1.0.0/bundle.json"
model = inference.find(path)
if model is None:
    model = inference.load_bundle(path)

p = pipeline.start("camera0", "model", model, 10)
sequence = 0
try:
    while True:
        record = pipeline.result(p, sequence)
        if record is not None:
            sequence = record["sequence"]
            for detection in record["result"]["result"]["detections"]:
                print(detection["label"], detection["score"], detection["box"])
        status = pipeline.status(p)
        if status["done"]:
            if status["last_error"]:
                raise RuntimeError(status["last_error"])
            break
        time.sleep_ms(100)
finally:
    pipeline.destroy(p)

Readers receive the latest completed result, so they can skip intermediate frames. Replace the print call with your application's logging or display behavior. The example destroys its pipeline and leaves the model resident for reuse; unload it explicitly when no client needs it. Other bundles can return classifications or raw tensors instead of detections. See Model bundles for the built-in result adapters.

Results and statistics

result copies the latest record without consuming it. There is no result queue: slower readers see the newest completed result and can detect skipped sequences. Each record contains pipeline, sequence, source_timestamp_us, decode_us, process_us, frame_us, and a processor result. Sequence increases across job restarts. Source timestamps are capture time for local frames and arrival time for RTSP frames; files use zero. frame_us includes source acquisition, decoding, and processing including processor serialization, but excludes the configured interval and timing-envelope serialization. These timings describe one request and do not assert sustained camera FPS.

The RTSP receiver keeps the latest JPEG, dropping incoming frames while a frame is leased. The pipeline releases the compressed lease immediately after decoding, before processing. Each pipeline owns at most one decoded frame and one latest serialized result. Results are bounded to 64 KiB plus the timing envelope.

QR results include original-image corners, payload_hex, payload length, ECI, QR version, ECC level, data type, candidate/failure counts, processed dimensions, and recognition timings. Hex preserves binary payloads, embedded NUL, and non-UTF-8 bytes. QR processing scales larger images to fit within 640 by 480, preserving aspect ratio and returning corners in original-image coordinates. QR requests use the native five-second timeout.

Model results include the native classification, detections, or raw result, image transforms, preprocessing, inference, copying, and postprocessing timings. Requests use a ten-second timeout. Raw outputs include dtype, shape, exponents, byte count, and data_hex; total raw output bytes must not exceed 24000 and the serialized record must fit the result bound. Larger tensor outputs can be read through the general inference API.

Stopping waits for active work to finish cancelling and releases the source and model reference. The model stays loaded until you explicitly unload it. If startup reports no memory, check internal RAM with mem as well as PSRAM.

Scripting APIs · SolarOS manual

Native libraries

A native library supplies a reusable, versioned C API to native apps, jobs, or firmware services. Libraries require ESP32-S3 with executable PSRAM and the native-module service. Each loaded library has one shared instance.

Quick reference

pkg available
pkg installed
pkg unload <library-id>
pkg remove <library-id>

Use pkg install <library-id> for a library present in the signed catalog. Installation checks the catalog signature, target, host and lifecycle ABIs, capabilities, artifact size and hash, and the library registration. The ELF is stored under modules/libraries/<library-id>.elf in the preferred volume. pkg installed reports whether a library is loaded or only on storage.

Libraries are loaded when a consumer acquires their API. Acquisition reads the installed file; it does not download code. Loaded libraries use PSRAM and remain resident until explicitly unloaded, removed, or reclaimed while unused under memory pressure. Installed libraries are not loaded by the boot scan. After restarting, consumers load them again as needed.

pkg unload releases the resident image but keeps its installed file. pkg remove unloads the library and removes the file. Both refuse while a consumer holds a lease or the library cannot finish shutdown. Unload an unused library before reinstalling it.

mem reclaim unloads unused libraries together and reports free PSRAM and the largest free block before and after cleanup. Active leases prevent reclamation. Application sessions, including suspended foreground apps, keep their state.

Developer reference

Include solar_os_native_library_abi.h. A provider calls solar_os_native_library_host_v1() from its entry point and registers one solar_os_native_library_descriptor_v1_t. Its ID must match the installed module ID. Registration declares a summary, API table, and shutdown callback; it must not start workers or acquire resources. Library lifecycle ABI 1 is independent of the provider's own API version.

Every provider API table starts with uint32_t abi_version, struct_size. Append new fields to compatible tables. Consumers request the exact provider ABI and enough table bytes for the methods they use. Missing or incompatible providers return an error, a zero lease, and a null API pointer.

Native consumers request get_service("library", 1, sizeof(*libraries)) from their host table. The returned solar_os_native_library_service_api_v1_t provides acquire, retain, release, and unload. Results use solar_os_native_service_result_v1_t.

uint32_t lease = 0;
const void *table = NULL;
int result = libraries->acquire("my-library", MY_LIBRARY_ABI,
    sizeof(my_library_api_v1_t), &lease, &table);
if (result == SOLAR_OS_NATIVE_OK) {
    const my_library_api_v1_t *api = table;
    /* Use api; destroy its objects and stop callbacks before releasing. */
    libraries->release(lease);
}

Each acquire returns an independent lease. retain creates another lease for a model, client, or worker that must outlive the original consumer. Keep that lease until its calls, objects, workers, and callbacks have finished. Release all owned leases before the consuming module exits. Lease operations are synchronized; the provider is responsible for synchronizing its own mutable state. The runtime supports at most 128 outstanding library leases. Loading and unloading share native-module admission. A request blocked by another module operation returns SOLAR_OS_NATIVE_ERROR_INVALID_STATE and can be retried when that operation finishes.

Releasing a lease invalidates its token. Releasing or retaining a stale token fails. The API pointer is valid while at least one owned lease protects it; never call through it after releasing all such leases.

With no leases, unloading calls shutdown. The provider joins its workers, releases dependencies and allocations, then returns SOLAR_OS_NATIVE_OK. A failed shutdown leaves the image loaded and allows a later retry. It must leave the API usable when refusing shutdown. Shutdown cannot load or unload another module while the resident operation is in progress, but can release leases on dependencies.

Firmware consumers use solar_os_native_library_acquire, retain, release, and unload through solar_os_native.h; they use the same stable result codes. These APIs do not expose arbitrary firmware symbols to external libraries.

Runtime helpers

Providers can request these optional version-one tables through get_service. Pass the size of the public table and check for NULL before use:

ServiceHeaderOperations
timesolar_os_native_time_abi.hMonotonic microseconds and cooperative sleep on the calling task
threadsolar_os_native_thread_abi.hNormal/recursive mutexes, conditions, task-local keys and joinable workers
controllerssolar_os_native_controllers_abi.hOwned PID instances in the OS registry; caller supplies timestamped samples and handles its actuator
libcsolar_os_native_libc_abi.hBounded formatting, float parsing and math helpers

Thread operations return POSIX error numbers. Destroy synchronization objects and clear task-local values before deleting their keys. Key deletion does not invoke destructors. Any task-exit destructor must finish while a lease still protects its code. worker_start creates a worker pinned to core 0 or 1 with an internal stack. The entry function must clear private task-local values and return. worker_join waits until its code has stopped executing and releases the stack and task storage. Keep the library lease through every worker join.

The optional memory table extensions report free/largest internal or external heap blocks and identify external allocations. Check struct_size before using appended members. Heap snapshots include other OS users and do not reserve RAM.

Scripting APIs · SolarOS manual

Native media and inference

Native ELF modules · Computer vision · Model inference

Native ELF applications and resident job modules discover host-owned services through a versioned function table. They compile against public headers in include/; these interfaces use standard C types and opaque host objects, with no ESP-IDF, ESP-DL or interpreter headers. Python/Lua and native clients use the same underlying services, resident models and processing limits.

Quick reference

  • Discover version-one service tables with the native host's get_service.
  • Enable each required firmware package and check for NULL before use.
  • Release host images/results and destroy clients/sessions before ELF unload.
  • Resident models survive client and module teardown until explicit unload.
  • Allocate native tensor scratch through the memory PSRAM table.

Discovery and compatibility

Application modules import solar_os_native_host_v1 from solar_os_native_abi.h. Check abi_version and struct_size before accessing the appended get_service member. Its presence requires a 24-byte host table on ESP32-S3; declare minimum_host_api_size=24 in the module manifest. Older output-only modules requiring the original 20-byte prefix remain compatible. Resident jobs use the existing solar_os_native_job_host_v1().get_service.

get_service(name, abi_version, minimum_struct_size) returns a borrowed, constant table, or NULL for an unknown name, unsupported ABI, insufficient table size or disabled package. Discovery allocates no workspace and starts no workers. Always check availability before calling the table.

Service namePublic headerFirmware package
memorysolar_os_native_service_abi.hbootstrap.native-modules
imagesolar_os_native_image_abi.hservice.image
media.videosolar_os_native_media_abi.hservice.script-media
vision.qrsolar_os_native_vision_abi.hservice.vision
vision.imlibsolar_os_native_vision_abi.hservice.imlib
inferencesolar_os_native_inference_abi.hservice.inference
tensor.imagesolar_os_native_inference_abi.hservice.inference

Each table has ABI version one and its own struct_size. The accompanying *_types_v1.h data layouts and enum values are fixed for that ABI. New service table members can be appended; incompatible data layouts require a new ABI. Use the target's normal C ABI and enum sizing.

The host additionally exports the standard C helpers memcpy, memset, memmove, memcmp and strlen, including compiler-generated uses. Allocate dynamic module buffers through memory.alloc; they use PSRAM and must be released with memory.free. Zero-size allocation and exhaustion return NULL. Raw allocations keep fixed addresses. Optional buffer_create, buffer_pin, buffer_unpin and buffer_destroy fields provide movable buffers: check the table's struct_size before calling them. Retain a returned pointer only while pinned, balance every pin, and destroy handles before module unload. Destruction returns false while a pin is outstanding. See memory policy.

Images and video

image.open accepts a resolved storage path, such as /sdcard/picture.jpg. decode accepts borrowed compressed bytes; from_pixels copies GRAY8, little-endian RGB565 or RGB888 rows. All return one owned image reference. retain adds a reference and release removes one. pixels borrows immutable RGB888 rows, valid while an image reference remains held. convert writes a crop/resize into caller-owned memory without modifying the source.

media.video.create creates a calling-task-owned session with optional cancellation. open opens a local video stream ID with requested dimensions, JPEG quality and timeout; it rejects non-video streams. acquire leases one compressed JPEG and frame returns its borrowed bytes and metadata. release_frame returns the lease. snapshot combines one-shot source opening and capture; releasing that frame also closes its temporary source.

When capabilities includes SOLAR_OS_NATIVE_MEDIA_RTSP, use rtsp_open, rtsp_read and rtsp_ended. Without that capability these operations return SOLAR_OS_NATIVE_ERROR_NOT_SUPPORTED. RTSP receives JPEG video without audio playback. rtsp_read can succeed with frame ID zero when no complete frame is ready. Source/frame IDs belong to one session and are invalid after release.

Frame timestamps are capture time locally and arrival time for received RTSP. Network views additionally include the original RTP timestamp and arrival time. Decode through image.decode, then return the compressed lease before processing the independent decoded image. Camera exclusivity and bounded latest-frame RTSP behavior follow the existing media service.

close releases a source and any held frame. destroy closes all sources and joins the RTSP worker before freeing the session. It does not release decoded images, which have independent references. If a provider fails to close, destroy returns an error and keeps the session valid for retry. Finish this cleanup before returning from the module or its stop callback.

Vision and models

vision.qr.qrcodes returns an owned result with payload bytes and original-image corners. vision.imlib.defaults fills default options; run selects one of the histogram/statistics, threshold/inversion, filtering, morphology, difference or blob operations. Both use existing image references and leave inputs unchanged. Both tables provide result_free for their own result type.

An imlib transform result owns its returned image. Call image.retain before freeing the result if the image must remain usable. Analysis results and QR payloads survive input image closure, but remain valid only until result release.

inference.create owns client cancellation and metadata state. load and load_bundle return OS-owned resident handles; find and list discover them. info borrows a client metadata snapshot until its next successful info call or destruction. run accepts named dense tensors; run_bundle additionally accepts image pixel views and applies declared adapters. Result buffers belong to the host until result_free and survive subsequent runs or model unloading.

Client destruction does not unload models. Explicit unload/unload_all release them. Retain a shared model while using it across other clients' lifecycle controls; release drops that reference. Retained models report busy on unload. reset and set_mode affect the shared resident instance.

tensor.image.defaults, inspect and prepare expose the same native crop, resize, layout, normalization and quantization used by model bundles. inspect reports the required size and inverse-map geometry. The module owns the destination buffer passed to prepare; borrowed source pixels and tensor descriptors must remain valid throughout the call.

Errors, cancellation and lifetime

The service tables return SOLAR_OS_NATIVE_OK or the negative result constants in solar_os_native_service_abi.h; they do not expose ESP-IDF error numbers. Admission contention returns SOLAR_OS_NATIVE_ERROR_INVALID_STATE. Cooperative cancellation and expired deadlines return SOLAR_OS_NATIVE_ERROR_TIMEOUT after processing workers have joined.

Cancellation callbacks run synchronously on the calling task. Keep callback code and user data alive until their client/session is destroyed. Serialize calls on a client/session. Blocking capture, decode, vision and inference calls belong outside native job tick callbacks.

A command-style module must release all images/results and destroy every client/session before returning. A resident job must perform that cleanup and join any module-owned worker before its stop callback returns. The native runner unloads command modules immediately; resources are not automatically collected. Models may remain resident because they contain no module callbacks.

Minimal image client

#include "solar_os_native_abi.h"
#include "solar_os_native_image_abi.h"

int main(int argc, char **argv)
{
    const solar_os_native_host_api_v1_t *host = solar_os_native_host_v1();
    if (!host || host->abi_version != 1 || host->struct_size < sizeof(*host) || !host->get_service) return 1;
    const solar_os_native_image_api_v1_t *image = host->get_service(
        SOLAR_OS_NATIVE_IMAGE_SERVICE, SOLAR_OS_NATIVE_IMAGE_ABI, sizeof(*image));
    if (!image || argc != 2) return 2;
    solar_os_raster_image_t *picture = NULL;
    int result = image->open(argv[1], &picture);
    if (!result) {
        solar_os_raster_image_pixels_t pixels;
        result = image->pixels(picture, &pixels);
        /* Use pixels.data, width, height and stride while picture is held. */
    }
    image->release(picture);
    return result ? 3 : 0;
}

modules/examples/vision-check contains a standalone client of all six tables, including model reuse and leased video decoding.

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.servo, solaros.motor, solaros.encoder, 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.speech, solaros.synth, solaros.dsp, solaros.controls, solaros.parameters, solaros.midi, solaros.osc
Emotion enginesolaros.emotion facial animation and reaction sounds
Jet 3D renderersolaros.jet scenes, meshes, lighting and presentation
Input and clipboardsolaros.input, solaros.hid, solaros.clipboard
Board, apps, jobs, and identitysolaros.board, solaros.identity, solaros.jobs, solaros.sessions, solaros.apps
Contacts and messagessolaros.contacts, solaros.messages
SC2 end-to-end chat securitysolaros.sc2
Text user interfacessolaros.tui
Graphics, raster images, and DLP connectionssolaros.gfx, solaros.image, solaros.dlp
Camera, streams, and RTSPsolaros.streams, solaros.camera, solaros.rtsp
Computer visionsolaros.vision
Model inferencesolaros.inference
Native image pipelinessolaros.pipeline

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.

Supported language features include 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.

Python integers support the signed 64-bit range. SolarOS service values such as file sizes, timestamps, counters, and storage capacities use integers throughout that range. An unsigned native value above 9223372036854775807 is returned as a decimal string instead of losing precision.

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. Use hashlib for hashing; random is not suitable 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

Python requires PSRAM. Optional solaros modules appear only when their services are included in the firmware. Check pkg and board on your device; the topic table above links to each service's requirements and examples. For an optional module, test availability before using it:

import solaros

if hasattr(solaros, "wifi"):
    print(solaros.wifi.status())

Vision, inference, and pipelines need their own packages; adding Python alone does not enable them. See Packages and flavors.

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.

Raw SSH/SCP session handles are unavailable in scripts. Use the ssh and scp applications for remote access and file transfers.

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 audio and control API

The solaros.controllers feedback-controller API is documented in Feedback controllers. It provides PID and hysteresis instances, manual stepping, timestamped input, and periodic output bindings.

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_receive(name, stream, address[, argument]): map a zero-based incoming argument to a writable scalar stream in its native unit. Returns a 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 and receive-stream 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.speech

Available when the offline speech package is compiled. Start its background worker before submitting text:

solaros.jobs.start("speechd", ["/voices/en-US"])
utterance = solaros.speech.say("Solar O S is ready")
print(solaros.speech.request_status(utterance))
  • say(text[, volume[, drop_if_busy[, pitch[, speed]]]]): copy and queue up to 512 UTF-8 bytes, return immediately, and return a request ID. Volume defaults to the global speaker volume. With drop_if_busy=True, drop a request rather than wait behind speech or another audio owner. Pitch accepts 50 through 200 and speed accepts 20 through 500; both default to 100. Use request_status() to observe a later audio-ownership drop.
  • cancel(request_id): remove queued speech or request cancellation of the active utterance.
  • request_status(request_id): return id, state, error, error_name, progress_done, and progress_total, or None after the bounded result history expires.
  • queue_status(): return running/current state, queue depth and capacity, and completion, cancellation, drop, and failure counters.

The selected directory must contain ta.bin and sg.bin; restarting the job with another voice directory changes the language. Accepted text belongs to the native queue and remains valid if the submitting script exits. job stop speechd cancels the queue and frees the PicoTTS engine and voice buffers.

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.speech, 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(): forget the remembered keyboard and its bond, then start pairing a replacement. A conflicting stale keyboard bond is replaced during 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, manual=False): acquire the application-peripheral lease. The default starts connectable advertising; manual=True starts idle for host selection. The name is 1..26 bytes without an embedded NUL. One remote host can connect at a time. A live lease cannot change its manual setting.
  • hosts(): list saved HID hosts as dictionaries with address, addr_type, connected, and name. Addresses use the same format as BLE scan results. Host encryption keys and identity metadata persist in NVS; the local input keyboard is excluded from this list. For manual leases, SolarOS asynchronously reads the connected host's GAP Device Name after encryption and bonding. name is a UTF-8 prefix of at most 63 bytes, empty until available or if the host does not permit the read. Lookup does not gate keyboard reports. Names live only for that connection; applications can cache them on mounted storage. status()["host_name"] exposes the same current name without rereading the saved host list.
  • connect(address, addr_type=0): for a manual lease, advertise a connection offer filtered to this saved host using the local identity stored during pairing. Private host addresses are resolved using the bond. The host initiates the BLE connection; SolarOS cannot dial a peripheral-to-host connection. Other saved hosts cannot take this offer. Automatic reconnection requires the host's Bluetooth to be enabled and its HID reconnect policy to allow the connection. Reconnect requests use the saved bond's authentication and Secure Connections settings; selecting a saved host does not require enabling pairing again on the host.
  • forget(address, addr_type=0): delete this HID host's pairing. If selected, stop its connection offer and disconnect it. Local input bonds stay intact.
  • disconnect(): cancel a manual lease's host offer and disconnect its host, keeping saved pairings. The service stays registered and idle.
  • pair(): enter explicit new-host pairing mode. A manual lease drops the selected connection and uses a fresh static-random peripheral identity so previously paired hosts cannot reconnect to that offer. Other saved host pairings remain available; each host's local identity is restored by connect(). Without manual mode, SolarOS removes the previous HID host bond and resets pairing on the public identity. Omit pair() on normal startup when using the default automatic-reconnect behavior. If bond storage is full, explicit HID pairing may replace an unused bond. The remembered input keyboard and all connected peers are preserved.
  • 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, manual, pairing, host_selected, 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 board, apps, jobs, and identity API

API overview · Lua board, apps, jobs, and identity

solaros.board

info() returns a new dictionary with the configured board information:

FieldValue
idStable board ID, such as solar_term.
nameHuman-readable board name.
moduleModule name, or an empty string when unspecified.
capabilitiesList of capability names, such as wifi, gpio, and psram.
psram_bytesDeclared PSRAM capacity in bytes; 0 on boards without PSRAM.

These are build-time board properties, not a hardware probe or a list of installed firmware packages. Optional service bindings still depend on the firmware flavor. PSRAM capacity is not the current free heap. The board API is available whenever Python is included, including headless script runners.

import solaros

board = solaros.board.info()
print(board["id"], board["name"])
if "wifi" in board["capabilities"] and hasattr(solaros, "wifi"):
    print(solaros.wifi.status())

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 foreground app registry and hand control to another app.

  • list(include_playground=True): return built-in apps and installed Playground apps. Playground apps are included by default; pass False for native apps only. Entries include name, id, title, summary, kind, and runtime. Playground launch names use the stable playground:<id> form.
  • find(name): return one native app dictionary or None.
  • launch(name, args=[]): replace the current script app with a native or installed Playground app. args is a list or tuple of strings and does not include the app name. A successful handoff does not return to the script.
  • can_open(path_or_url): return whether a registered app handles the local file extension or an HTTP(S) URL.
  • open(path_or_url): replace the current script app with the registered handler. A successful handoff does not return.

When the launched app exits, the completed interpreter session closes and control returns to the shell.

Example:

import solaros

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

if solaros.apps.can_open("/books/guide.epub"):
    solaros.apps.open("/books/guide.epub")

open() selects Web for http:// and https:// URLs and uses the native file association registry for local paths. For example, EPUB and Markdown reading targets select Reader when it is part of the current firmware. Unknown schemes and file types are not opened implicitly.

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 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 registered 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, int, 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.

DRV8830 attachments also accept integer max_mv to configure the voltage represented by full normalized motor drive. See Motor control.

pcnt-encoder requires integer a, b, and mode (1, 2, or 4). Optional integer invert, pull, and filter_ns configure its inputs. See Encoder counts.

Camera DVP attachments also accept GPIO-number bindings d0..d7, siod, sioc, vsync, href, pclk, xclk, and optional pwdn/reset. The camera driver validates required pins and claims; board-owned cameras cannot be detached. For SCCB control, specify either i2c="i2c0" (or another existing named bus) or both siod and sioc GPIO numbers. These modes cannot be combined. A named bus retains its wiring and other attached devices when the camera stops.

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 camera, streams, and RTSP API

API overview · Lua media API

Quick reference

  • Local sources: solaros.streams.list(), open(id[, options]), acquire_frame(handle), release_frame(frame), and close(handle).
  • Still images: solaros.camera.snapshot([size[, quality[, source]]]) or capture(path[, size[, quality[, source]]]).
  • Native decode/presentation: solaros.image.from_frame(frame), decode(data), present(image, x, y[, width, height]), and close(image).
  • QR detection: solaros.vision.qrcodes(image[, options]) when service.vision is compiled; see Computer vision for payloads, regions, and results.
  • Network receiver: solaros.rtsp.open(url[, video[, audio]]), status(handle), read_frame(handle[, timeout_ms]), lateness(handle, frame), and close(handle).
  • Handles belong to one runtime. Release promptly; teardown also closes them.

Local streams

  • solaros.streams.list(): registered stream metadata.
  • info(id): registry metadata and the provider's default format.
  • status(handle): metadata with the opened handle's actual direction and negotiated audio/video format.
  • open(id[, options]): acquire an interpreter-owned source/sink handle.
  • close(handle), close_all(): release held frames, sources, and RTSP sessions.
  • read(handle, bytes[, timeout_ms]): bounded byte/PCM read, at most 16384 bytes.
  • write(handle, data[, timeout_ms]): bounded byte/PCM write; returns bytes written.
  • read_scalar(handle[, window_ms[, timeout_ms]]): one numeric sample.
  • acquire_frame(handle): lease one compressed JPEG frame from a video source.
  • frame_info(frame): codec, width, height, length, timestamp_us, and network flag.
  • frame_data(frame): explicitly copy JPEG bytes into the interpreter heap.
  • frame_save(frame, path): write JPEG bytes directly to storage.
  • release_frame(frame): return the native frame.

Open options use a dictionary/table. All streams accept direction (source, sink, or duplex) and timeout_ms. Default direction is source except for sink-only devices. Video accepts width, height, and jpeg_quality (0 selects the provider default; otherwise 1..63). Audio accepts sample_rate, channels, and frames_per_block; other audio properties come from the registered S16_LE format. Unsupported keys are rejected. Requested formats must be supported by the provider; there is no automatic resampling or format conversion.

There are four local handles per interpreter and one leased frame per handle. Frames are limited to 512 KiB. IDs are opaque integers, not pointers; released or closed IDs cannot be reused or passed to another runtime. Acquisition holds the same exclusive camera ownership as native consumers. Inspect info(id).owner before diagnosing a busy source. An active rtspd or cam-webd camera lease is not bypassed by scripts.

Byte reads poll cancellation between native waits of at most 50 ms. Timeouts are 0..60000 ms, with a 1000 ms read/write/open default. Reads may return a partial block or empty data; native timeout failures raise an error. Writes and scalar operations check cancellation before their bounded native operation. Camera acquisition uses the camera service's bounded capture wait. Event streams use their provider's byte-read representation; no generic event schema is invented here.

Camera convenience

Available when service.camera is compiled:

  • solaros.camera.status(): backend, sensor, owner, initialized/leased flags, selected size/quality, orientation (normal, mirror, flip, or 180), frame_buffer_count, capture count, and last error name.
  • snapshot([size[, jpeg_quality[, source]]]): return a native JPEG frame handle. Defaults are qvga, quality 12, and camera0.
  • capture(path[, size[, jpeg_quality[, source]]]): capture directly to a JPEG file and release the temporary source, including on write failure.

Sizes are qvga (320x240) and vga (640x480). A snapshot owns a temporary camera source until streams.release_frame(frame). For repeated captures, open the stream once and acquire/release frames in a loop instead of restarting the camera each time. Files are overwritten by capture/frame_save.

File-save failures include the resolved path and filesystem cause in an OSError, with the numeric errno in error.args[0] (for example, 2 for a missing parent directory). Capture releases its temporary camera source before raising; a failed frame_save leaves the caller's frame leased for retry/release.

Images without temporary files

When media.image is compiled:

  • solaros.image.decode(data): decode compressed JPEG/PNG/GIF/WebP bytes into a native PSRAM-backed image handle; the input is borrowed only during decoding.
  • from_frame(frame): decode a leased camera or RTSP JPEG without copying its compressed bytes into the interpreter heap. The resulting image is independent of the frame and remains valid after frame release.
  • to_rgb(image[, width, height]): return an owned packed RGB888 bytearray, optionally resized with nearest-neighbor sampling. Supply both dimensions; the copy remains valid after image closure and must fit the interpreter heap. This provides pixel buffers for model-specific normalization and quantization.
  • draw(image, x, y[, width, height]): existing clipped canvas drawing.
  • present(image, x, y[, width, height]): queue a native presentation. On RGB565-capable color displays this bypasses indexed palette conversion; other displays use the existing canvas path. The rectangle must fit the display.

Call gfx.begin() or gfx.begin(target) first. Present does not require a subsequent gfx.present(). Direct RGB565 pixels are not retained in the indexed canvas; a later canvas refresh can overwrite them. Draw chrome first and present video afterwards. The RGB565 cache is allocated lazily in PSRAM and released with the image. GUI-queue presentation failures are reported in the native log.

Image handles retain the existing 16-handle limit, 4 MiB encoded-input limit, and 2-million-pixel decode limit. Close images promptly; do not queue a growing history of video frames. Queued draws/presents retain their own image reference.

RTSP sessions

Available when service.rtsp-client is compiled:

  • solaros.rtsp.open(url[, video[, audio]]): start an asynchronous native session; video and audio default to true.
  • status(handle): negotiated/playing/ended flags, selected tracks, audio format, receive/drop counters, error code/name, and causal error_detail.
  • read_frame(handle[, timeout_ms]): lease one completed JPEG frame or return no value when none is available. Default timeout is zero; maximum is 60000 ms.
  • lateness(handle, frame): signed microseconds relative to the native presentation clock. Negative means early; positive means late.
  • close(handle): cancel, release any held video frame, stop native audio, and wait for the network worker before freeing its state.

One RTSP session is allowed per interpreter. At least one track must be enabled. The session uses the existing UDP RTP/JPEG and L16 receiver, RTCP timing, bounded audio jitter buffer, selected audio output, and global volume. Audio is played natively, not returned as PCM to the script. Audio-only sessions are supported with open(url, false, true) (Python uses False and True).

Open returns before negotiation completes. Check status().ended and error_detail to distinguish failure from an ordinary frame-read timeout. An ended audio-only session also returns no video frame. Use explicit apps.launch("rtsp", ...) if you want the complete native viewer/oscilloscope instead of managing a session.

There is one native compressed video buffer; reception drops frames while it is leased. Decode promptly, use lateness for scheduling or late-frame drops, and release the frame. RTSP frame metadata includes rtp_timestamp (90 kHz) and monotonic arrived_us; its timestamp_us is arrival time, not sender wall-clock time. Local camera timestamps are capture timestamps.

Current RTSP restrictions: trusted LAN, UDP media, JPEG/L16, IPv4/hostnames, and no TCP-interleaved media. Inline user:pass@ or a stored rtsp-auth account answers Basic or Digest MD5 on the control connection. The request URI and error text never include the password. Interpreters are never called from network/audio tasks.

QR detection

solaros.vision.qrcodes(image[, options]) processes an existing native image handle. Release a camera/RTSP frame after image.from_frame and before detection. Results contain binary bytes payloads and original-image corner coordinates; they remain valid after the image is closed. Processing is limited to 640 by 480 and eight decoded codes. See Computer vision for crop/resize options, result fields, cancellation, and examples.

Image processing

service.imlib adds native histogram/statistics, binary thresholding/inversion, mean/Gaussian/median filters, erosion/dilation/opening/closing, image difference, and grayscale/LAB blobs under solaros.vision. Transformations return new image handles; source pixels stay unchanged. See Computer vision.

Model inference and background pipelines

solaros.inference loads resident models and checked bundles on ESP32-S3 with PSRAM. Use load_bundle and run_bundle for native image preparation and built-in classification or PICO detection results. Raw models expose named tensor inputs and outputs. See Model inference and Model bundles for the shared Python/Lua contract.

solaros.pipeline runs QR or model processing as an OS-owned background job over stored images, camera streams, or JPEG RTSP feeds. Scripts can configure a job, exit, and read its latest structured result from another session. See Native image pipelines for installation, camera ownership, and a complete Python detection example. Explicitly stop or destroy pipelines and unload models when finished; interpreter teardown leaves them resident.

Cleanup

Explicitly release frames and close sources/images/sessions on all paths. Interpreter teardown also cleans up on normal exit, cancellation, errors, and app handoff. Cleanup waits for native RTSP workers; it never force-deletes a worker using live buffers. If a driver repeatedly fails teardown, SolarOS logs that failure and retains live lease state rather than freeing memory under it. Optional camera/image/RTSP modules follow their native service package gates; adding scripting does not force those hardware/network packages into a build.

Examples

import solaros

frame = solaros.camera.snapshot()
try:
    print(solaros.streams.frame_info(frame))
    solaros.streams.frame_save(frame, "/flash/snapshot.jpg")
    picture = solaros.image.from_frame(frame)
finally:
    solaros.streams.release_frame(frame)

solaros.gfx.begin()
try:
    solaros.image.present(picture, 0, 0, min(160, solaros.gfx.width()),
                          min(120, solaros.gfx.height()))
    solaros.time.sleep_ms(2000)
finally:
    solaros.image.close(picture)
    solaros.gfx.end()

An audio-only receiver without handing control to another app:

receiver = solaros.rtsp.open("rtsp://192.168.1.192:8554/music", False, True)
try:
    while not solaros.should_exit():
        state = solaros.rtsp.status(receiver)
        if state["ended"]:
            print(state["error_detail"])
            break
        solaros.time.sleep_ms(100)
finally:
    solaros.rtsp.close(receiver)

TCP byte streams

With TCP byte-stream support, solaros.streams.tcp.listen(name, port) creates a raw listener, connect(name, host, port) configures an outbound endpoint, and remove(name) removes an unclaimed endpoint. These create registered exclusive duplex byte streams; pass their names to solaros.dlp.connect or the stream-dlp expansion driver. Up to four endpoints share the TCP registry. The socket connects on first I/O and closes when its consumer releases the stream. Hostnames resolve at registration. These endpoints persist until removed or rebooted, including after the creating script exits. See stream commands and direct DLP connections.

BLE byte streams

With BLE byte-stream support, solaros.streams.ble.listen(name) registers a native CoC listener, connect(name, address, addr_type=0) configures an outbound stream, and remove(name) removes an unclaimed endpoint. The optional address type is positional: 0 public, 1 random, 2 public identity, 3 random identity. Use the address and type reported by solaros.ble.scan() or ble scan.

These configurations persist after the script exits. A stream claim owns the BLE connection; close it in finally or use a DLP context manager, then remove the configuration if it belongs only to this script. A disconnected handle must be closed and reopened. The OS keyboard connection remains reserved.

solaros.streams.ble.connect("ble0", "aa:bb:cc:dd:ee:ff", 0)
try:
    with solaros.dlp.connect("ble0", timeout_ms=8000) as display:
        info = display.info()
        display.present(bytes(info["frame_bytes"]))
finally:
    solaros.streams.ble.remove("ble0")

See BLE stream commands for connection roles, buffering, peripheral lease sharing, and security behavior.

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[, offset[, limit]]): saved history in chronological order. The default page contains the newest 64 messages. Offset skips messages from the newest end; limit is 1 through 64. Increase offset by the returned page length to read older pages.
  • 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.

solaros.messages.instances() lists instance IDs, names, provider classes, capabilities, and connection status without authentication secrets. Use messages.join(instance_id, channel), messages.query(instance_id, nickname), messages.leave(conversation_id), and messages.connect(instance_id, enabled) on that same submodule. Join and query return a conversation ID. Connection settings are saved; the provider job must also be running.

solaros.messages.classes() lists class names, capabilities, and creatable. messages.add(class_name, name, settings) returns the new instance ID; messages.configure(instance_id, settings) changes its settings. Pass settings as a list or tuple of at most eight strings, using the same order as messages add and messages configure in the shell. For example:

community = solaros.messages.add("irc", "community", ["irc.example.org", "6697", "nils"])
solaros.messages.configure(community, ["sasl", "account", "password"])
solaros.messages.connect(community, True)

The corresponding provider job must run to connect. Authentication settings are written to the instance profile and are never returned by enumeration.

solaros.messages.remove_instance(instance_id) closes and removes a configurable instance, credentials, pending sends, local history, and Inbox notifications. Default instances require backend removal support. Removal requires writable mounted storage. Removed instances are omitted from instances(). Creating the same name again allocates a different ID.

Conversation summaries include instance_id, provider_class, join_state (0 none, 1 left, 2 joining, 3 joined, 4 failed), and join_error. Conversation summaries include the raw title, a display label that identifies MeshCore group transports, and history_only. History-only conversations are readable but reject sends.

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, powered, enable_control, and enabled. Output enable state is separate from the supply state.
  • power(enabled[, name]): change a driver-managed receiver power rail, defaulting to the first receiver, and return the requested state.
  • enable([name]) / disable([name]): enable or silence NMEA output through recognized UBX/CASIC configuration support. Return True or False respectively. Unsupported receivers raise an error; a disable request retains the UART claim and does not power down or electrically isolate it.
  • 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.encoder

Available when the common encoder service is compiled. list() returns metadata records with name, driver, integer mode, and can_reset. status(name) adds signed integer count; read(name) returns that count. reset(name) sets it to zero when supported. Names are required and service errors raise exceptions. MODE_X1, MODE_X2, and MODE_X4 equal 1, 2, and 4. See Encoder counts for attachment, wiring, and count limits.

solaros.motor

Available when the common motor service is compiled. list() returns endpoint metadata; status(name) returns normalized state and optional driver diagnostics. Use drive(name, value) with a finite signed value from -1.0 to 1.0. Zero coasts. coast(name), brake(name), and clear(name) share the shell semantics; optional unsupported operations raise an error. Names are required.

The same API works with all registered motor drivers. A normalized command is not a measured speed or voltage. See Motor control for record fields, fault-mask constants, driver scaling, lifecycle, and examples.

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 address named outputs as "provider:number". An integer selects native:<GPIO>, preserving the GPIO API. Providers enforce their own shared frequency groups; conflicts are rejected within a group.

  • Constants: FREQ_MIN, FREQ_MAX describe the native backend's range.
  • providers(): return records with name, driver, outputs, and detachable.
  • status(): return dictionaries with pin, allowed, active, channel, provider, output, frequency_group, owner, freq_hz, duty_percent, enabled, reserved, resolution_bits, and pulse_width_us. Reserved outputs remain active while disabled. Pulse width reflects the configured hardware duty, even while pulses are disabled. pin is the native GPIO number, or -1 for outputs without a GPIO mapping. frequency_group identifies outputs sharing timing within that provider.
  • set(output, freq_hz, duty_percent): start or update a general PWM output. Duty is 0..100.
  • pulse(output, freq_hz, pulse_us): start or update PWM with an integer pulse width in microseconds, rounded to hardware ticks. The pulse must be shorter than the period; zero is accepted.
  • off(output): stop a general PWM output.

Example:

import solaros

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

solaros.servo

Available when the servo service is compiled. Use list(), status(name), open(name, output, min_us, max_us, min_deg, max_deg[, freq_hz[, inverted]]), set(name, angle_deg), pulse(name, pulse_us), disable(name), and close(name) for calibrated positional outputs. Opening sends no pulses; commands start output. Outputs belong to their creating runtime and are closed on interpreter exit. Status reports commanded position.

See Servo outputs for calibration, record fields, examples, and resource limits.

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.servo, solaros.motor, solaros.encoder, 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()

Direct DLP connections

When dlp_client is selected, solaros.dlp.connect(stream, format=MONO1, timeout_ms=5000) opens a private DLP connection over a registered exclusive duplex byte stream. Python accepts positional arguments or these keywords. It does not register a display target or allocate text/graphics composition buffers. TCP endpoints can be configured through solaros.streams.tcp when TCP byte-stream support is available; UART and radio streams are configured through their providers.

import solaros

# Configure once, or use a stream already registered by the shell.
solaros.streams.tcp.connect("pico0", "192.0.2.10", 7777)
try:
    with solaros.dlp.connect("pico0") as link:
        info = link.info()
        link.present(bytes(info["frame_bytes"]))  # white MONO1 image
finally:
    solaros.streams.tcp.remove("pico0")

Use info() for width, height, format, byte stride, frame_bytes, accepted formats bitmask, capabilities, model, and firmware. present(pixels) requires exactly frame_bytes packed bytes and returns after presentation completes; errors propagate to the script. MONO1 uses black=1, LSB-first horizontal bits, with each row padded to a whole byte. GRAY2/GRAY4 use LSB-first packed indices, GRAY8 uses one byte per pixel, and RGB565_BE uses the high byte first. Unused row-padding bits must be zero.

Format constants are solaros.dlp.MONO1, GRAY2, GRAY4, GRAY8, and RGB565_BE; pass -1 to select the peripheral's preferred format. timeout_ms accepts 100..60000 and sets discovery and the minimum command timeout. Peripheral presentation deadlines may extend command waits.

close() releases the stream claim and is idempotent. The connection supports with. Interpreter teardown also closes all outstanding connections, including after an error or cancellation. Each interpreter supports four private connections. After an I/O failure, another present attempts a fresh connection and full repaint; geometry and format capabilities must remain unchanged. Close and reconnect if the peripheral configuration changes.

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

gfx.theme() returns a dictionary with foreground and background RGB888 integers from the current setterm colors. It does not require gfx.begin() or claim a display. Use these literal values for Jet materials and backgrounds. Read the dictionary again after a terminal color change to obtain the new 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 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)

Raster images

For JPEG data, camera/RTSP frame decoding, and direct RGB565 presentation, see Camera, streams, and RTSP.

When the firmware includes media.image, solaros.image decodes static PNG, JPEG, GIF, and WebP files into native PSRAM-backed handles. This keeps map tiles and other large images outside the MicroPython heap. Up to 16 handles can be open at once.

from solaros import gfx, image

tile = image.open("/sdcard/maps/12/2200/1342.png")
try:
    source_width, source_height = image.size(tile)
    image.draw(tile, -24, 32)
    image.draw(tile, 240, 32, 128, 128)
    gfx.present()
finally:
    image.close(tile)

image.draw(handle, x, y) uses the source size. Supplying both width and height applies nearest-neighbor scaling. Coordinates can be negative and the native renderer clips to the display. Drawing is queued with the other graphics operations. A queued draw keeps its own reference, so it is safe to close or replace a tile immediately after queuing it. image.close_all() releases every handle owned by the current runtime; interpreter cleanup also does this.

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.

When media.image is present, solaros.image provides:

  • open(path), load(path): decode a static raster and return a handle.
  • size(handle): return (width, height).
  • to_rgb(handle[, width, height]): copy packed RGB888 pixels into an owned bytearray. Supply both dimensions to resize with nearest-neighbor sampling; otherwise use the source size. The copy remains valid after closing the image. Output is limited to 2 million pixels and must fit the interpreter heap.
  • draw(handle, x, y[, width, height]): queue a clipped raster draw.
  • close(handle): close one handle.
  • close_all(): close all handles owned by this Python runtime.

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

With dlp_client, solaros.dlp.connect(stream, format=solaros.dlp.MONO1, timeout_ms=5000) returns a private connection with info(), present(pixels), and idempotent close(). It supports with; interpreter teardown also releases the exclusive byte-stream claim. present accepts exactly the packed info()["frame_bytes"] bytes and waits for completion. No display is registered. solaros.streams.tcp.connect(name, host, port) creates an outbound stream; solaros.streams.tcp.remove(name) removes it after the connection closes. With BLE byte-stream support, solaros.streams.ble.connect(name, address, addr_type) configures a native CoC stream; solaros.streams.ble.remove(name) removes it after use. See BLE byte streams.

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

When service.input-keymap is included, solaros.input.load_keymap(name, path) loads a validated JSON mapping file for the named attachment, and solaros.input.reset_keymap(name) restores its built-in map. Both return None; failures raise an exception. Applying a map releases held keys. Mappings last until detach or reboot; a startup script can reload a saved file. solaros.input.keymap_info(name) reports whether a source supports mapping, physical keys, modifiers, layers, and tap/hold, together with key count and optional matrix metadata. Other drivers currently report supported=false. See source keymaps for fields and mapping formats.

Touchscreens report tap, double-tap, hold, swipe, and edge-swipe gestures in addition to pointer events. Use GESTURE_TAP, GESTURE_DOUBLE_TAP, GESTURE_HOLD, GESTURE_SWIPE, and GESTURE_EDGE_SWIPE to match their numeric gesture values. See man media.input for timing, direction, and recognition limits.

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().

Local keyboard capture

Foreground scripts can exclusively capture one ready named key-event source:

  • capture_keyboard(name): claim that source before SolarOS shortcuts and character decoding. One Python or Lua app can own a capture at a time.
  • read_key(): nonblocking; return the next captured key dictionary or None. A key dictionary has type="key", source metadata, physical_key, canonical HID usage, translated key, modifiers, and action. A zero usage means the source cannot identify that HID key.
  • release_keyboard(): release this runtime's capture and discard its queue.

Actions are KEY_PRESS, KEY_RELEASE, and KEY_REPEAT. Modifier bits follow USB HID order (left Ctrl, Shift, Alt, GUI, then their right counterparts); MOD_CTRL and MOD_ALT match either side.

Captured input bypasses local focus switching and the usual app-exit key. The script must implement its own exit chord and check should_exit(). Capture applies to the named local source even when another session has input focus. Port characters and other sources keep their normal routing. Headless runners cannot claim a keyboard. Runtime exit and forced stop release the claim automatically; use try/finally for normal cleanup.

The capture queue holds 32 events. Overflow discards queued events and delivers {"type": "reset"} before subsequent keys. Source release, readiness loss, and detach also deliver a reset. Release any forwarded held keys on reset; detach ends capture of that source, including if its source ID is reused.

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.

HTTP URLs are limited to 2047 bytes. Redirect following supports relative URLs and statuses 301, 302, 303, 307, and 308. HTTPS downgrade and credentials in redirect URLs are rejected. Origin changes discard authorization and cookie headers. POST changes to GET after 301/302; 303 changes to GET except for HEAD; 307/308 preserve the method and 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. A full queue backpressures the native worker until the script drains it or cancels the stream; SolarOS 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 SC2 end-to-end chat security API

API overview · Lua SC2 · Commands

solaros.sc2 exposes the device's SC2 (SolarOS Chat v2, suite SC2-1) session layer: X3DH key agreement, the double ratchet for direct messages, epoch keys with per-sender chains for private rooms, and signed posts for public rooms. The module exists in builds with the sc2 group (enabled in the full flavor on boards with PSRAM).

Keys never enter the Python heap. Scripts pass plaintext and relay frames in and get frames, plaintext and public metadata out. Identity keys, prekeys, ratchet states and room epoch keys stay in the C service. Messages that carry keys (t:"epoch_key", t:"history") are consumed inside C and only reported as events; scripts cannot send them (dm_encrypt and room_encrypt refuse epoch_key, history and removed inner types).

Plaintext is the inner JSON of the protocol, an object with a t field, for example {"t":"text","id":"…","body":"hello","ts":1760889600}. Build it with json.dumps and read it back with json.loads. Inner JSON is limited to 8191 bytes; it is padded to 256…8192-byte buckets before sealing.

Frames are complete relay lines (dm.send, room.send, room.post, keys.publish) ready to write to a relay connection, and the decrypt and verify calls take the relay's dm.msg, room.msg and room.pub lines. This module does not open network connections.

Identity and peers

  • solaros.sc2.identity(): {"device_id", "fingerprint", "ik", "dk", "spk_id", "opk_count"}. The first call on a device generates the identity. Public values only.
  • solaros.sc2.status(): {"ready", "core", "hash", "device_id", "store", "psram", "peers", "sessions", "rooms", "opk_count"}, or "error" when the service cannot start (for example, internal flash not mounted).
  • solaros.sc2.keys_publish(opk_target=16, rid=None): returns a keys.publish frame. Tops up one-time prekeys to opk_target (at most 32) and rotates the signed prekey after 30 days; the new private keys are stored before the frame is returned.
  • solaros.sc2.add_bundle(frame): verify a relay keys.bundle line (device ID must match the identity key; both signatures must verify) and store it. Returns the peer's device ID. Needed before the first DM in either direction.
  • solaros.sc2.peer(device_id): {"session", "opk", "spk_id", "fingerprint"} or None.
  • solaros.sc2.safety_number(device_id): {"short", "words", "full"}. Compare it with the contact over another channel before trusting them.
  • solaros.sc2.forget(device_id): delete the stored bundle and DM session.

Direct messages

  • solaros.sc2.dm_encrypt(device_id, inner_json, rid=None): returns a dm.send frame. The first message to a peer runs X3DH from the stored bundle; the session is saved before the frame is returned, so send what you get or the receiver will see a gap.
  • solaros.sc2.dm_decrypt(frame): open a dm.msg line. Returns {"from", "seq", "kind", "new_session", ...}. With kind == "message" the dict has "body" (the inner JSON). With "epoch_key" or "history" a room key was installed and the dict has "room" and "epoch"; "ignored" means a key from someone who is not the room's admin was dropped. Replays, tampering and unknown senders raise OSError("sc2: …").

Private rooms

Private rooms use one random epoch key per epoch, distributed by the admin over pairwise DMs, and one sending chain per member. Every room message is also Ed25519-signed by its sender.

  • solaros.sc2.room_create(room=None): {"room", "epoch"}; this device is the admin. room is 32 hex characters (random when omitted).
  • solaros.sc2.room_share(room, device_id, rid=None): admin only; returns a dm.send frame that carries the current epoch key to one member.
  • solaros.sc2.room_rotate(room): admin only; starts a new epoch and returns its number. Share it with every remaining member. The previous epoch stays readable for late messages.
  • solaros.sc2.room_encrypt(room, inner_json, rid=None): returns a room.send frame.
  • solaros.sc2.room_decrypt(frame): {"room", "from", "seq", "epoch", "n", "body"} for a room.msg line. The sender's bundle must be stored.
  • solaros.sc2.room_info(room): {"admin", "is_admin", "epoch", "sent", "senders"} or None.
  • solaros.sc2.room_forget(room): delete the room keys.

This API does not check membership commits (room.commit); the sc2-sync worker verifies them for the rooms it manages. A device therefore accepts epoch keys for a room it does not know from whoever sends them first, and afterwards only newer epochs from that admin.

Public rooms

Public rooms (pub: names) are signed, not encrypted.

  • solaros.sc2.post_sign(relay_id, room, name, body, ts=0, ref=None, delete=False, rid=None): returns a room.post frame. ts=0 uses the current time. delete=True retracts the post named by ref.
  • solaros.sc2.post_verify(relay_id, frame, max_body=1024): {"valid", "room", "from", "id", "ref", "name", "ts", "seq", "deleted", "body"} for a room.pub line. A post that fails verification is returned with valid=False so it can be shown as unverified or dropped.

Selftest

solaros.sc2.selftest() returns {"passed", "failed"} after the same checks as the sc2 selftest shell command. It uses throwaway in-memory devices and never touches stored keys.

Example

import json, solaros
sc2 = solaros.sc2
me = sc2.identity()["device_id"]
publish = sc2.keys_publish()          # send to the relay
peer = sc2.add_bundle(bundle_line)    # bundle_line from keys.claim
frame = sc2.dm_encrypt(peer, json.dumps({"t": "text", "body": "hi"}))
msg = sc2.dm_decrypt(incoming_line)
if msg["kind"] == "message":
    print(msg["from"], json.loads(msg["body"])["body"])
print(sc2.safety_number(peer)["words"])

Keys at rest

Keys live on internal flash in /.sc2/ (/flash/.sc2/ on boards with an SD slot), never on removable cards. Each record is versioned, CRC-checked and kept in two alternating copies, so an interrupted write falls back to the previous copy. Flash encryption is off by default: anyone who holds the device can read its flash, copy the identity and session keys, impersonate it and read messages that arrive afterwards until contacts block it. Deleted records are overwritten before removal, but the flash wear-levelling layer can keep older sector copies until they are erased.

The hardware RNG is only a true RNG while Wi-Fi or Bluetooth is running. SC2 mixes it with a seed carried across boots, and the identity is created on first use rather than at boot; create it with Wi-Fi connected.

Quick reference

Use solaros.sc2 for SC2 end-to-end encrypted DMs, private rooms and signed public posts. Keys stay in C: pass inner JSON and relay lines, get frames and plaintext back. Call keys_publish() once and add_bundle() for each contact, then dm_encrypt/dm_decrypt, room_* and post_sign/ post_verify. Errors raise OSError("sc2: …"). See man python for runtime conventions.

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.
  • stat(path): return metadata with type, is_file, is_dir, size, mtime, and mode fields. type is file, directory, or other.
  • exists(path): return whether the path exists.
  • scandir(path[, cursor[, limit]]): return one bounded directory page as {"entries": [...], "next_cursor": ...}. cursor defaults to the start and limit defaults to 32; the maximum limit is 128. Each entry has a name plus the stat() metadata fields. next_cursor is None at the end.
  • read_file(path[, max_bytes]): return up to max_bytes bytes from a regular file. The default is 4096 and the maximum is 65536.
  • write_file(path, data[, append]): create or overwrite a regular file and return the byte count written. data accepts a string (stored as UTF-8), bytes, or another readable buffer, up to 65536 bytes per call. append defaults to False; pass True to add data to the end. Empty data creates an empty file, or truncates an existing file when append is False. The file is flushed, synced, and closed before success. Parent directories must exist; use makedirs() to create them. A failed write can leave partial data; overwriting is not atomic.
  • 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.
  • makedirs(path[, exist_ok]): create the directory and missing parents. exist_ok defaults to True; pass False to fail when the final directory already exists.
  • 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, or a raw FAT disk image at an explicit mount point.
  • 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("/"))
solaros.storage.makedirs("/notes")
solaros.storage.write_file("/notes/example.txt", "hello from Python\n")
solaros.storage.write_file("/notes/example.txt", "another line\n", True)
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"])

cursor = None
while True:
    page = solaros.storage.scandir("/apps", cursor, 16)
    for entry in page["entries"]:
        print(entry["name"], entry["type"], entry["size"])
    cursor = page["next_cursor"]
    if cursor is None:
        break

Directory cursors are numeric offsets into the current enumeration. If files are added or removed between calls, restart at None to obtain a coherent view.

read_file() returns bytes and can truncate its result at max_bytes. Use .decode("utf-8") to load saved text, or open() to read larger files.

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 saved across reboots without 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. The default BLE device name also uses this hostname. A BLE peripheral app can use its own name while active; the device hostname is restored when it closes.

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 or BLE 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 or BLE 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.

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.

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

The locally focused display receives keyboard and board-control input. Touch or browser input may target a particular display. Inspect or change focus:

session focus
session focus oled0

Changing focus does not move or restart an app. On split displays, use Ctrl+Alt+Arrow to move focus in the split's direction. At an edge, focus stays where it is. On independent displays, those shortcuts cycle through targets. Either Alt key works, including AltGr. Plain Ctrl+Arrow remains available to the app.

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() includes installed Playground apps, and launch(), open(), and can_open() provide replacement handoff from Python and Lua scripts.

System services · SolarOS manual

MeshCore companion messaging

SolarOS exposes MeshCore through two background jobs. meshcore implements a non-forwarding companion node on a locally attached packet radio. meshcore-ble uses the companion protocol to connect to a separate MeshCore device and lets that device own the radio, identity, contact table, and channel configuration. Both publish through the provider-neutral Contacts and Messages services, so Contacts, Chat, and Inbox use the same interfaces. Both jobs can run together. Each maintains its configured instances: separate local radios or separate BLE companions have independent identities, channels, routing, and connection state. A physical radio or companion address can bind only one instance.

The local-radio implementation exchanges signed adverts, end-to-end encrypted direct messages, acknowledgements, and shared-key group messages. 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, 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

BLE companion transport

Use the peer address and address type reported by ble scan. Supply the companion's six-digit PIN to request BLE encryption and bonding:

job start meshcore-ble 68:ee:8f:69:5f:35 public 123456
job status meshcore-ble
contacts
chat

The job discovers the MeshCore UART-style GATT service, subscribes to device notifications, starts protocol version 3, sets the device time, and synchronizes contacts, all eight channel slots, and queued messages. New adverts trigger a contact refresh, and a messages-waiting notification drains the device queue. Direct sends remain in sending state until the companion reports the expected MeshCore acknowledgement; group sends become sent after the device accepts them.

Job status lists each instance, its connection state, and its current detail. SolarOS can retain up to 512 contacts across providers; imports do not evict existing contacts when this store is full. The companion's own firmware can impose a smaller limit. The status bar shows the radio icon while the companion is online. Group labels identify [companion] or [radio]; groups absent from the current channel configuration remain readable as (history) entries.

The optional PIN is saved in the instance profile on mounted storage. Status and enumeration do not expose it; setup commands are excluded from shell and Chat input history. MeshCore's direct-message and shared-channel protection remains independent of BLE transport encryption. The job reconnects with bounded exponential backoff after a disconnect. SolarOS's multi-peer BLE host lets an established BLE keyboard remain connected while the companion is online.

The external device remains authoritative for its identity, channels, radio region, and contact routing. Configure those on the companion device. The meshcore shell command applies only to the local-radio job, and virtual serial ports are currently available only with that local implementation.

Named instances

Create independent connections with the common Messaging commands:

messages add meshcore field radio radio0 meshcore-eu868
messages add meshcore desk ble 68:ee:8f:69:5f:35 public 123456
messages connect field
messages connect desk
job start meshcore
job start meshcore-ble
messages join field #general
messages instances

The jobs take no arguments when using saved bindings. Legacy starts with radio or companion arguments save and enable that binding, using the default instance when it is unbound or allocating a new instance otherwise. Connection failures retry with bounded backoff. Resource admission still depends on available radios, BLE peer slots, shared credential capacity, and memory.

messages join NAME CHANNEL selects a configured group. On a local radio, joining a new #hashtag creates its derived channel key; joining Public enables the built-in public channel. BLE groups must already exist on the companion. messages query NAME PUBLIC_KEY_HEX opens a direct conversation through that instance. Local channel, name, and identity settings also use the common interface:

messages configure field name field-unit
messages configure field channel team BASE64_PSK
messages configure field public off
messages configure field identity generate

These changes stop the affected local runtime while applying settings; its job restarts it when enabled. Other instances continue running. messages configure NAME radio RADIO PROFILE or messages configure NAME ble ADDRESS TYPE [PIN] changes a binding within its backend type. Chat accepts the same settings after /add and /configure. Legacy meshcore commands address the default instance when it is unbound or uses a local radio. If that default is absent or uses BLE, they use the sole local-radio instance. With several local-radio instances and no local default, use messages instances to list them and the named Messaging commands to manage each one.

messages remove NAME or /remove NAME closes that instance, cancels its sends, and deletes its profile credentials and local message history. Default instances can also be removed. Settings use .chat/profiles; identity and channel keys use .settings/credentials.bin. The previous NVS name and public-channel keys migrate only after a verified profile save. BLE bonding keys remain owned by the BLE stack. Remove any virtual serial ports bound to a local instance before removing it; an instance with those resources still attached reports an invalid state.

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. Identity and group keys use the unencrypted mounted credential file; access to its volume can recover those secrets.

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 provides ordered serial delivery with up to two frames in flight and receiver credits. It carries up to 78 data bytes per MeshCore packet and uses 2048-byte transmit and receive queues. Acknowledgements piggyback on replies; standalone ACKs become due after 250 milliseconds, or earlier after two new frames. Retries take 12 to 16 seconds, and a peer is treated as absent after 180 seconds. Both endpoints must support Link stream protocol v3. 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>.

Requirements and diagnostics

Both transports require PSRAM. Local-radio messaging also needs a supported packet radio; the companion transport needs BLE. Use meshcore status for traffic, packet usage, retries, and duplicates. If startup reports no memory, check mem and stop unused jobs: the local-radio worker needs a 7168-byte internal stack and the BLE companion needs 8192 bytes, plus other buffers.

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
job start meshcore-ble <address> <public|random|rpa_public|rpa_random> [six-digit-pin]
job status meshcore-ble
job stop meshcore-ble

System services · SolarOS manual

Messaging identities and security

Run messages without arguments to open the tabbed instance, connection, and message manager. It supports provider settings forms, connection diagnostics, conversation and unread browsing, pending-send cancellation, and deletion of individual local messages. c opens the existing Chat as a child and returns to the browser when Chat closes. See Application controls.

To start a direct conversation by name, open contacts, select the person, and press c. Chat opens their conversation and returns to Contacts when closed. The Chat application must be included in the firmware.

Provider classes describe protocols; named instances describe individual connections. Multiple IRC instances can share a class while keeping server settings, rooms, nicknames, and message routing separate. Chat groups by instance and accepts chat INSTANCE, chat CLASS, or a conversation ID. Use /join INSTANCE TARGET, /query INSTANCE NICK, and /leave in Chat, or messages join|query INSTANCE TARGET and messages leave CONVERSATION_ID from the shell. See Native IRC.

messages remove INSTANCE (name or ID), or /remove INSTANCE in Chat, removes a configurable provider instance. It closes the connection, stops retries, cancels queued sends, and removes the saved connection settings and credentials. Local messages, conversations, and associated Inbox notifications are deleted. Gateway, MeshCore, and Link support removing their default instances too. Removal requires writable mounted storage.

messages classes lists provider classes; messages instances lists configured connections. messages add gateway NAME URL [TOKEN] creates a Gateway connection. messages add link NAME TRANSPORT creates a Link projection. Enable it with messages connect NAME, and run the corresponding transport job. Chat accepts the same arguments after /add, and changes settings with /configure NAME .... MeshCore settings are messages add meshcore NAME radio RADIO PROFILE or messages add meshcore NAME ble ADDRESS TYPE [PIN]; see MeshCore.

ClassSettings after messages add CLASS NAMEWorker
gatewayURL [TOKEN]gateway-sync
ircHOST PORT NICKirc-sync
meshcoreradio RADIO PROFILE or ble ADDRESS TYPE [PIN]meshcore or meshcore-ble
linkTRANSPORTIts radio-link or espnow-link transport with chat=on
sc2DOMAIN [ADMIT] [key=B64], or chats://HOST:PORT[#pin=sha256/B64] [ADMIT] [key=B64] or chat://HOST:PORT [ADMIT] [key=B64]sc2-sync

New instances start disabled. Use messages connect NAME and start the worker. For SC2, use messages connect NAME and job start sc2-sync. Enabling or configuring an instance, restoring it after a reboot, and sending a message do not start the job. Messages queue while the worker is stopped. Use job stop sc2-sync to stop the worker, or messages disconnect NAME to disable one instance. Configured job autostart can start the worker at boot. messages configure NAME SETTINGS... changes connection settings; IRC also accepts sasl ACCOUNT PASSWORD, sasl off, pass PASSWORD|off, and ca FILE|default. Chat exposes /classes, /instances, /add CLASS NAME SETTINGS..., /configure NAME SETTINGS..., and /remove NAME. Python and Lua expose the same class registry and instance operations through solaros.messages.

A small record containing only the retired instance ID, name, and class stays on mounted storage. Retired IDs are never reused, so contacts and retained messages cannot be routed through a different account. Creating the same name again creates an independent instance. messages clear INSTANCE_ID deletes history without removing an active instance. Interrupted removals finish deleting history on the next boot.

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 can retain up to 512 contacts and 576 endpoints. They are saved in /.contacts/contacts.bin (under 192 KiB). If storage is unavailable, contacts remain usable for the current boot; check contacts status for save errors. Imports keep existing contacts and report skipped entries when full.

The credential store holds provider keys and tokens. Secret values are not shown in Inbox, logs, autocomplete, agent tools, Python, or Lua. They are saved in .settings/credentials.bin on mounted storage. Existing NVS records migrate only after a checked file is committed. The file is not encrypted. SSH keys, agent API keys, and other provider tokens keep their separate stores.

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 disk history. Stable 64-bit message IDs are printed as hexadecimal strings so they survive shell and scripting runtimes without precision loss.

Received and sent messages are saved in .messages/history.bin on the selected storage volume. The 64-message RAM cache does not limit disk history. History grows with available disk space; gateway sync and cache eviction do not delete older messages. Chat loads each conversation from disk. Use Page Up and Page Down to move through older and newer history pages. messages list ID reads the complete saved conversation.

SolarOS selects mounted storage when creating the archive and saves that location in internal flash. If SD mounts after startup, an internal archive moves to SD through a checked, synced copy before the saved location changes. Existing files on SD are not overwritten. An unavailable saved volume is retried when it returns; a new empty archive does not replace its history. If storage is unavailable or a write fails, messages status reports the error. New sends are queued only after their history is saved.

Existing full message records and incoming message records from the compact Inbox store migrate to the archive. Their original files are retained.

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.

MeshCore group labels distinguish [radio] from [companion]. A retained group outside the current transport or channel configuration has a (history) label: its messages remain readable, but it cannot accept new sends. Outbound queues are held in memory; after a restart, retained queued or sending messages become failed and must be resent explicitly.

Security flags

Conversations and messages carry a security bit set, which messages conversations and messages list print in hex, and which Python and Lua expose as security_flags. A conversation also inherits the bits of its messages.

BitNameMeaning
0x01encryptedContent is end-to-end encrypted
0x02peer key knownThe peer's public key is known
0x04peer trustedThe user trusted this endpoint
0x08shared keyEncrypted with a key shared by a group
0x10sender unverifiedThe sender could not be verified
0x20transport securedThe link to the server uses TLS
0x40public signedNot encrypted; every message is signed by its sender and was verified
0x80legacy insecureLegacy protocol without end-to-end protection or sender authentication

An sc2 instance is one SC2 relay. All relays share the device identity in /.sc2. Its rooms are pub:NAME public rooms, marked public signed, and 32-hex private rooms, marked encrypted and shared key. Direct conversations use the peer's 32-hex device ID and are marked encrypted; they also get peer key known once the peer's key bundle is stored. messages configure NAME admit CODE|off and pin sha256/B64|off change the admission code and the TLS pin. With a pin, the pinned key alone authenticates the relay (a self-signed certificate works; host name and expiry are not checked); without one the certificate must chain to the ESP-IDF CA bundle. sc2-sync connects the first enabled sc2 instance; sc2 relay shows its state. messages query NAME PEER and sc2 safety PEER take the 32-hex device ID, a unique prefix of at least 6 hex digits of a peer whose keys or session are stored, or a nickname saved with sc2 alias NAME DEVICE.

Private rooms: sc2 room create "Team room" makes a named end-to-end encrypted room on the primary relay with this device as its admin and prints its invite, a sc2i:... text that holds the room's invite secret. Whoever has the invite can join, so pass it on privately (sc2 room invite ROOM prints it again). Names may contain spaces and are limited to 48 UTF-8 bytes. The encrypted membership checkpoint supplies the name to joining devices. Names are display labels; the room keeps its stable 32-hex ID. sc2 room invite "Team room" accepts a unique exact name, and room names and IDs support Tab completion. Omitting the name retains the room XXXXXXXX label. On the other device, sc2 room join sc2i:... (or sc2 room join @FILE when the invite is longer than the input line) asks to join. The joining device also sends the admin an encrypted proof of invite possession. The admin checks that proof before admitting the member and sending its room key and membership checkpoint. A relay notification alone cannot admit a device. Both devices need firmware with this admission exchange; older clients remain waiting. Both rooms then appear as room XXXXXXXX conversations: use messages send as for any room, and messages leave to leave one (this also deletes the device's copy of the room record; an admin that leaves cannot invite again). sc2 rooms lists the instance's rooms with their state (waiting for an admin, waiting for the room key, joined). Room keys are accepted only from the invited admin after membership validation. Later membership changes require an authorized admin signature and a matching commit chain. Sending waits for the new epoch key after a membership change. Private text messages carry the sender's SolarOS user@hostname label inside the authenticated encryption. Public posts use that identity as their signed display name. Older peers without a sender label appear by device ID. Display names do not replace device keys or safety-number verification. Invites cannot be revoked from the device yet, and members cannot be removed; create a new room instead.

Safety numbers: in Chat, /safety on an sc2 DM (or /safety PEER) opens a screen with the short code, the eight words and the full number; compare them with the other device, then press V to mark the peer verified (U takes it back). /verify opens the same screen, /unverify clears the mark. From the shell: sc2 safety PEER, sc2 verify PEER, sc2 unverify PEER. The mark is stored in the SC2 store and Chat labels sc2 DMs (verified) or (unverified). The device ID is a hash of the peer's identity key, so a peer cannot change its key under a verified ID. Public sc2 rooms are labelled (public); the first time one is opened in Chat (or joined with messages join) a notice explains that posts are signed and non-deniable. It is shown once per room.

Adding a relay by domain: messages add sc2 NAME DOMAIN [ADMIT] [key=B64] fetches https://DOMAIN/.well-known/solaros-relay. The connection must verify against the CA bundle and the host name; there is no plain-HTTP, unverified or pin-on-first-use fallback, and a failure stops with messages: refused: .... The relay's signed descriptor gives the chats:// endpoint (port, optional pin) and up to three fallback relays, each with its key. The descriptor must verify against the relay key it names. With key=B64 (the relay_key from the operator) that key must match as well. The relay key and the fallback keys are pinned in the instance. At every login the relay must sign a fresh nonce with its pinned key. A different key is a hard failure: sc2 relay shows "relay key differs from the pinned key", and the pin is never changed by itself. The one exception is a rotation signed by the pinned key, which the relay announces at login (one step). To accept a new relay key otherwise, remove the instance and add it again. messages configure NAME key B64|off sets or clears the primary's pin, URL key=B64 sets it together with a new URL (a new URL without key= drops the pin), and fallback keys go after each fallback URL: fallback URL key=B64 URL key=B64. The fetch uses one 1.6 KiB buffer while it runs.

Fallback relays: messages configure NAME fallback URL [URL URL] gives an sc2 instance up to three fallback relays (same URL forms as the primary, each with its own #pin=); it replaces the whole list, fallback off clears it. sc2-sync keeps one connection. After two failed attempts in a row (the normal reconnect backoff paces them) it moves to the next relay. On a fallback it probes the primary every 15 minutes and stays there once it works. On the primary it visits each fallback in turn for about 30 seconds every 15 minutes (every minute until each relay has seen this device once) to publish the key bundle and collect DMs that peers sent there; visits wait while messages are queued. sc2 relay shows the relay in use. Trade-offs:

  • The identity, admission code and room list are the same on every relay; the admission code must work on all of them.
  • One-time prekeys are published on the primary only, so the same prekey can never be claimed on two relays. A peer that starts a new session through a fallback uses the signed prekey alone; the first message of that session then lacks the extra forward secrecy a one-time prekey gives. Existing sessions are not affected.
  • Peers only meet on a relay they both use. If you and a peer are on different relays, a DM waits in the other relay's mailbox until the next visit (up to about 15 minutes).
  • A public room on two relays is one conversation: posts from each relay are kept apart by relay index, so each relay resumes from its own position. Reordering the fallback list may re-fetch or skip some recent history. Private rooms live on the primary relay: while a fallback is in use they wait, and a message to one fails with "private room is on the primary relay".
  • Queued messages are sent through whichever relay is in use, never during a visit.

Reconnects: a relay may close a session on purpose and says why before it does. A session that only sent keepalive pings for the relay's idle timeout (408 idle), one that reached the relay's session lifetime (503 reconnect) and an idle session closed to make room on a full relay (503 evicted) are normal: sc2-sync reconnects after one to three seconds, does not count a failed attempt and does not move to a fallback. DMs wait in the relay's mailbox and rooms catch up at the next sync. When the same device key logs in on another connection, the relay closes the older session with 409 replaced; sc2-sync then waits 4 seconds, longer each time it happens again in a row (up to a minute), and stays on the same relay. Two devices that share one identity keep replacing each other this way; give every device its own. Every other drop, including 503 server_full, goes through the backoff.

A device can also be the relay: job start chatd serves a small SC2 mini-relay (login, DM mailbox, prekeys and the public room pub:general, with fixed caps) next to the v1 gateway on the same port. Add it with messages add sc2 NAME chat://IP:7777 [TOKEN] key=B64, using the key chatd logs at start. See the chatd job reference for its limits. chat:// is plain TCP, so the relay sees your IP; message contents stay end-to-end encrypted.

Gateway conversations use the v1 protocol. They are always marked legacy insecure, and also transport secured with chats:// or tls://. The server can read and change these messages, and sender names are not signed by the sender.

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
messages status
messages conversations
messages list CONVERSATION_ID
messages send CONVERSATION_ID TEXT [--allow-untrusted]
messages read CONVERSATION_ID
messages delete MESSAGE_ID
messages remove INSTANCE
messages clear INSTANCE|INSTANCE_ID|all
messages outbox
messages cancel MESSAGE_ID
outbox [list]
outbox cancel MESSAGE_ID

Conversations hold message history. Inbox shows incoming notifications from messaging and other apps. Outbox holds pending sends.

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

Native IRC

IRC instances connect servers directly to the unified Chat application. Each instance has its own server, nickname, optional authentication, channel list, connection state, and outbound queue routing. Several instances can run at once. Identical channels and nicknames on different instances remain separate.

Quick reference

irc add community irc.example.org 6697 pocket
irc connect community
job start irc-sync
irc join community #general
irc query community anothernick
irc status
messages conversations
chat
chat community
chat irc

Replace the example server and nicknames with your settings. irc add saves a disabled instance. irc connect enables that instance; irc-sync owns the live connections. Enable several instances before or after starting the job.

irc set community irc.example.org 6697 pocket
irc sasl community account password
irc sasl community off
irc pass community server-password
irc pass community off
irc ca community /sdcard/certs/network-ca.pem
irc ca community default
irc disconnect community
irc remove community
irc leave CONVERSATION_ID
job status irc-sync
job stop irc-sync

The common interface also supports messages add irc community HOST PORT NICK and messages configure community HOST PORT NICK. For authentication, use messages configure community sasl ACCOUNT PASSWORD or sasl off; server passwords use pass PASSWORD|off and CA files use ca FILE|default. Chat accepts the same arguments after /add irc and /configure. Setup commands are excluded from input history. Nicknames must use IRC nickname characters; nils-sol is valid, while nils@sol is rejected before configuration is saved.

Shell and Chat setup commands use the same quoting rules. Single quotes preserve spaces and backslashes literally; double quotes allow backslash escapes. Unclosed quotes are rejected without changing saved settings.

TLS always verifies the server certificate and hostname. The default trust source is the firmware certificate bundle. irc ca selects a PEM CA file on mounted storage, limited to 8191 bytes. Keep the system clock correct for certificate validation. SASL uses PLAIN inside verified TLS and is required when configured: authentication failure does not fall back to anonymous login. The server password is separate from the SASL account password.

Chat controls

Chat groups conversations by instance. Membership controls use the same API for every provider class that supports them:

/join community #general
/query community anothernick
/leave
/connect community
/disconnect community
/remove community
/server another-network irc.example.net 6697 pocket
/sasl another-network account password
/sasl another-network off
/status

/server creates or updates an IRC instance on mounted storage. /connect and /disconnect save its desired connection state. The background job continues when Chat closes. Entering a room waits for its join confirmation and reports join failures. Private conversations do not require adding a contact first. An IRC nickname is display and routing data, not proof of a person's identity. TLS secures the connection to the server; it does not provide end-to-end message encryption.

Persistence and connection behavior

Profiles are stored in .chat/profiles/NNN.bin on the selected mounted storage. They include the stable instance ID, server, nickname, authentication settings, CA path, enabled state, and up to eight desired channels. Saving uses a checked staged file and replacement with recovery of the previous file. Saving requires writable mounted storage. IRC adds no NVS settings. Saved authentication data is readable to someone with access to the volume; status output does not display passwords.

irc remove INSTANCE, messages remove INSTANCE, and Chat's /remove INSTANCE close the connection and remove its configuration, credentials, pending sends, local messages, conversations, and associated Inbox notifications. The other IRC instances keep running. A small credential-free record preserves the old label and reserves its ID. A new instance with the same name gets a different ID. Interrupted history deletion completes on the next boot.

irc-sync maintains independent connections, reconnects with exponential backoff up to 60 seconds, and rejoins desired channels. Channel joins, parts, and chat sends share a limit of one command per second per instance. Protocol registration and keepalive replies take priority. Incoming lines and control queues are bounded, and each instance receives a limited processing budget.

IRC receives live messages only. No history or echo-message capability is requested, and no bouncer is required. Messages sent while disconnected can be missed. Local retained history uses the shared Messaging store. Outbound sent means a complete IRC command was written to the connection; it does not confirm delivery to another user. An interrupted send becomes failed and is not automatically replayed. Queued messages that have not begun sending remain in the volatile shared outbox until the instance can send them or the user cancels them. Queues do not survive a device reboot.

To start the worker after reboot, add job start irc-sync to the startup script reported by setterm startup.

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.

Each named transport has an independent Chat instance. For example, a radio link and an ESP-NOW link can run together with chat=on; their contacts, conversations, send queues, and acknowledgements stay separate. The first legacy projection uses link; further projections receive names such as link-link1. Use messages instances to see their names and IDs.

To choose a name before starting its transport, use messages add link NAME TRANSPORT, then messages connect NAME. messages join NAME broadcast opens the broadcast conversation; messages query NAME HEX_DEVICE_ID opens a direct conversation. messages configure NAME TRANSPORT changes the transport binding. Only one Chat instance can bind each transport. Bindings and enabled state are saved on mounted storage. messages remove NAME deletes that projection and its local history while keeping the transport and byte streams running.

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 .settings/espnow-peers.bin on mounted storage; legacy NVS peers migrate after a verified save. 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|listList 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, and ordered delivery with cumulative acknowledgements. ESP-NOW allows up to eight data frames in flight. Sparse writes send immediately; queued bursts fill the window automatically. Lost frames trigger go-back-N retransmission after 0.8 to 1.2 seconds.

Stream protocol v3 carries an acknowledgement and an absolute receive-window edge in each 20-byte header. The sender stays within the receiver's advertised 2048-byte capacity, including bytes still in flight. Reading data replenishes credit; a sender blocked by a lost credit update probes for the current window. Credit and acknowledgements are scoped to the transmit session and tolerate duplicates and counter wraparound. A separate 2048-byte TX queue applies backpressure to the shell or bridge. Stream data is split to the active Link MTU: ESP-NOW carries up to 216 data bytes per stream frame.

Acknowledgements and credit updates piggyback on outbound traffic. Standalone ACKs become due after 10 milliseconds, or earlier after half a window of new data. This allows prompt keystroke echo while acknowledging sustained transfers in batches. link stream status reports the frame window and remaining transmit credit alongside queue sizes, retries, and byte/frame counts. MeshCore uses its own smaller window and longer acknowledgement and retry timings. The raw radio-link worker retains one frame in flight so it can listen for replies between transmissions, including with long LoRa packet airtimes.

Both endpoints must support stream protocol v3; v2 stream peers must be updated. The outer Link packet protocol, Chat, and the byte-stream consumer APIs are unchanged. Fifteen-second open frames also detect a disconnected or restarted peer.

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. A temporary transport stop preserves the open endpoints' session and sequence state, so unacknowledged bytes can be retried. If either endpoint closes and reopens, the peers establish fresh session state before resuming ordered delivery.

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

Boards and hardware targets

Run board to identify your target and its capabilities. Commands and apps also depend on the firmware flavor; use pkg, apps, and jobs to check it. For attaching hardware to an existing board, start with Expansion.

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.
solar_writersolar_writerSolarWriter, built from the Elecrow CrowPanel 5.79-inch792x272 dual-SSD1683 display, INMP441 microphone, shared PCM5102 line output and MAX98357 speaker output, LC709203F fuel gauge, DS3231 RTC, microSD, CH340/UART console, and GPIO21 reserved for jack detect.
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.
qdtech_es3c28pqdtech_es3c28pQDtech ES3C28P 2.8-inch ESP32-S3 Touch DisplayCapacitive-touch ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 landscape ILI9341V display, FT6336 pointer, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, one WS2812B, native USB CDC, and UART/I2C/GPIO expansion connectors.
qdtech_es3n28pqdtech_es3n28pQDtech ES3N28P 2.8-inch ESP32-S3 DisplayNon-touch ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 landscape ILI9341V display, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, one WS2812B, 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.
elecrow_crowpanel_esp32_s3_5_79_epaperelecrow_crowpanel_esp32_s3_5_79_epaperElecrow CrowPanel ESP32-S3 5.79-inch E-paperESP32-S3-WROOM-1-N8R8 target with a 792x272 dual-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.
waveshare_esp32_s3_epaper_3_97waveshare_esp32_s3_epaper_3_97Waveshare ESP32-S3-ePaper-3.97ESP32-S3-WROOM-1-N16R8 target with an SSD1677 e-paper display that defaults to 800x480 landscape, AXP2101 battery/charger monitoring, QMI8658 six-axis IMU, four-bit SDMMC, native USB CDC, UART, PCF85063 RTC, SHTC3 temperature/humidity sensor, ES8311 speaker/microphone audio, rotary navigation, Wi-Fi, BLE, and expansion I2C/UART.
seeed_reterminal_stickyseeed_reterminal_stickySeeed Studio reTerminal StickyESP32-S3R8 target with 32 MB flash, 8 MB PSRAM, 3.97-inch 800x480 monochrome e-paper with SSD1677/SSD2677 startup detection, GT911 capacitive touch, three physical buttons (AI/Power: sleep/wake on tap, shutdown on hold; Up: arrow on tap, BLE pairing on hold; Down: arrow on tap), Wi-Fi, and BLE. Console is UART0 (CH343).
xteink_x4_proxteink_x4_proXteink X4 ProESP32-S3 target with 16 MB flash, 8 MB OPI PSRAM, an 800x480 landscape e-paper display with cached SSD1677/UC8179/UC8279 identification, fixed-mix frontlight brightness, GT911 touch and Home pad, one-bit SDMMC, BM8563-compatible RTC, CW2017 gauge, native USB CDC, UART0, navigation buttons, Wi-Fi, and BLE.
cl_32cl_32Pi0cket CL-32ESP32-S3-WROOM-1-N16R8 target with an automatically detected 384x168 ST7305 reflective LCD or SSD1685 e-paper panel, 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 I2C/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.
goouuu_esp32_s3camgoouuu_esp32_s3camGOOUUU ESP32-S3CAMHeadless ESP32-S3 camera target with 16 MB flash, 8 MB PSRAM, native USB CDC, CH340/UART console, one-bit SDMMC, an onboard WS2812, Wi-Fi, BLE, and fixed DVP/SCCB camera wiring. Its required driver.camera-esp32 package exposes OV2640/OV5640 detection and bounded QVGA/VGA JPEG capture through service.camera; cam-webd adds a single-client MJPEG stream and JPEG snapshots with optional authentication over the shared HTTP server.
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.
thinknode_m9thinknode_m9Elecrow ThinkNode M9ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, a 320x240 ST7789 display, a 37-key STC8H-scanned QWERTY keypad with backlight, microSD over SDSPI, PCF8563 RTC, ATGM336H or BM72A GNSS over NMEA, battery ADC, passive buzzer, CH340C-class (CH340K) USB/UART console, Wi-Fi, and BLE. The keypad I2C address identifies the board revision (0x6c is v1.0, 0x6d is v1.1). The GNSS enable rail is active-low on v1.0, which the manifest declares; a v1.1 board, active-high, sets active = 1 on gnss0. GNSS power starts disabled and is enabled with gnss power on. The keypad's Home key exits the foreground app, Back is escape, Messages/Maps/Pin are F1/F2/F3, long-press Pin is F4, and the Message key arms the one-shot compose layer that supplies tab, pipe, backslash, brackets, and the other characters the keypad cannot type directly. Composed up and down page through the scrollback and composed right is tab (see the stc8h-keyboard driver reference). The onboard LR1110 LoRa radio is driven by the lr11xx driver as radio0 on the shared SPI bus, declared at 915 MHz with a 3.3 V TCXO and the board's own DIO5/DIO6 antenna-switch table; an 868 MHz unit changes the freq binding. It has exchanged LoRa packets with an RNode on the bench; 2.4 GHz, GNSS scanning, and Wi-Fi scanning are out of scope (the board has a separate ATGM336H GNSS receiver).
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 M5Stack TAB5 keyboard on the board-owned I2C bus at address 0x6d, INT on GPIO41, and a fixed primary 400x300 SSD1683 display on SPI2 using GPIO3/GPIO7 through GPIO12. GPIO40 remains available for expansion.

Build pio run -e cl_32 for either CL-32 screen. The board-owned cl32-display adapter identifies ST7305 RLCD or SSD1685 e-paper at startup and attaches the matching concrete driver as display0. Both controllers remain available as manual expansion drivers; cl32-display is hidden from that path. The former cl32_epd target is replaced by cl_32. RLCD modules without register readback are identified through their reset/TE response on GPIO14; the SSD1685 software-reset BUSY pulse distinguishes e-paper.

T-Deck Plus registers its factory L76K or MIA-M10Q receiver as gnss0 through the nmea driver, listening at 9600 baud and trying 38400 if needed. Use gnss list and gnss fix gnss0 to inspect it. This NMEA path supplies position, UTC, and satellite information; position accuracy and PDOP are unavailable. The receiver has no independent power control. The standard T-Deck Plus has no onboard NFC reader.

The Grove connector is the T-Deck's only expansion connector. Its UART signals (ESP32 TX GPIO43, RX GPIO44) are shared with the onboard GNSS receiver. The I/O map shows this connector, not the internal display, radio, or audio wiring. gnss0 attaches automatically with origin=board, but can be detached to release uart0:

expansion detach gnss0
com uart0

Exit COM with the app-exit key before restoring the board receiver:

expansion attach gnss0

Restoring it uses the board's original bindings and retains origin=board. If an external device changed the bus rate, run uart baud uart0 9600 before restoring GNSS so detection starts at the factory rate and can try 38400. Detachment lasts until restoration or reboot, including while other board devices retry attachment. Detaching stops the GNSS reader and releases the software UART claim; the receiver remains powered and electrically connected. gnss disable gnss0 first silences NMEA output on recognized L76K/CASIC and MIA-M10Q/UBX receivers. gnss enable gnss0 restores output. These commands do not release uart0; detach afterward when another consumer needs it. Reattaching the GNSS driver restores muted NMEA output automatically. Settings are changed in receiver RAM only, without changing sentence rates or baud. Software silencing does not guarantee that the receiver's TX pin becomes high impedance. An external transmitter on the same signal still needs electrical isolation unless the receiver's hardware specifies a suitable disconnected state.

Define a custom board

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/manifests/common/<base>.toml            private shared hardware template
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

Public profiles can use extends to share a template from boards/manifests/common/. Templates are inherited by board ID and do not appear as build targets or selectable boards. CL-32 has one public manifest, cl_32, which selects its display driver at runtime.

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. For a display with panel profiles, select the panel and rotation. Each rotation shows the resulting display size.
  6. Select existing buses or create new buses, then enter addresses, pins, chip selects, ports, and optional driver parameters.
  7. Confirm the generated profile.

Display profiles select supported glass and its driver parameters together. For example, ST7305 offers SolarTerm's 300x400 panel and CL-32's 168x384 panel; their default rotations give 400x300 and 384x168 displays. SSD1683 offers its 400x300 panels and the 792x272 dual-controller panel. The tool writes the matching panel and rotation bindings and derives the board's logical width and height. SSD1685, SSD1677, UC8179, and UC8279 also derive geometry from their panel profile and rotation. Runtime expansion commands continue to use numeric panel and rotation bindings.

SSD1306 and SH1106 also offer named OLED panel profiles and all four rotations. These configure auxiliary displays; the primary display's geometry stays with its own driver and panel selection.

Catalog display profiles declare native width and height, a named label, the panel selector bindings, and the default rotation. Exactly one profile per driver has default = true. All profiles for a driver use the same selector keys, with distinct supported values. For example:

[[drivers.st7305.display_profiles]]
name = "168x384"
label = "CL-32 2.9-inch RLCD"
width = 168
height = 384
bindings = { panel = 1 }
rotation = 3

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. Display dimensions follow the exported panel and rotation bindings. 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=writerdeck \
  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

Board devices attach automatically and report origin=board. By default they are fixed and cannot be detached at runtime. A manifest can set detachable = true on an individual [[devices]] entry to permit expansion detach <name> and restoration with expansion attach <name>. Restoration retains the board origin and manifest bindings. 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.

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.
CAMERAA board-integrated camera interface and fixed pin contract are present. This hardware fact does not by itself imply that a camera driver or capture service is included.
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
custom_solaros_default_flavor = classic
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 short-press action, which defaults to suspend. Any KEY press resumes from suspend. A long press defaults to forgetting the remembered BLE keyboard and starting pairing. Configure the two gestures independently with setterm powerkey short <action> and setterm powerkey long <action>. 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=classic 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 classic profile to make a specialized 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/classic.toml \
  --board freenove_esp32_wrover_v3 \
  --layout single

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, and its PlatformIO environment defaults to the classic profile:

pio run -e ttgo_vga32_v14

To create a specialized build, start from classic in os_builder, adjust the granular groups, and build against this board's single-image limit:

python3 scripts/os_builder.py \
  --input flavors/classic.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.

All classic ESP32 targets default to the compact classic flavor, including Python, Playground, buttons, and ADC D-pad support on ODROID-GO. See Packages and flavors.

The board does not have CDC, a board-defined I2C bus, 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.

Create an I2C bus on the free P2 header pins with:

expansion bus create i2c i2c0 port=i2c0 sda=gpio4 scl=gpio15 speed=100000
i2c scan i2c0

Either I2C controller (i2c0 or i2c1) can be selected. GPIO4 and GPIO15 must be unused; while the bus exists, they cannot also serve as external SPI chip selects, GPIO, PWM, or another runtime bus. No I2C bus is created at boot.

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

QDtech ES3C28P and ES3N28P 2.8-inch ESP32-S3 Displays

The qdtech_es3c28p target supports the capacitive-touch ES3C28P SKU, while qdtech_es3n28p supports the non-touch ES3N28P SKU. SolarOS runs their native 240x320 ILI9341V panel as a 320x240 landscape primary display at 40 MHz, enables the panel's required color inversion, and controls the active-high GPIO45 backlight with PWM.

On ES3C28P, the FT6336 controller shares i2c0 with the ES8311 codec and registers as the default attachment touch0. GPIO17 is its interrupt and GPIO18 is its reset. It emits absolute pointer events rotated into the landscape display0 coordinates. Inspect it with expansion devices, input touch, or input test touch0, and store optional logical-range correction with input calibrate touch0 .... The ES3N28P target does not enable pointer support; GPIO17/GPIO18 remain reserved for its unpopulated touch footprint.

The PlatformIO hardware definition selects the published 80 MHz DIO flash mode for the 16 MB external flash and OPI mode for the integrated 8 MB PSRAM.

The fixed devices are the ILI9341V display, four-bit SDMMC card slot, ES8311 duplex speaker/microphone path, GPIO9 battery ADC, and the GPIO42 WS2812B named pixels0. The shared i2c0 bus is exposed on P4 and also controls the audio codec. P3 exposes runtime GPIO2, GPIO3, GPIO14, and GPIO21; GPIO2, GPIO3, and GPIO14 support ADC. P2 exposes the releasable uart0 signals on GPIO43 and GPIO44. The LCD uses the internal spi0 bus, including its wired GPIO13 MISO signal; it is not an expansion SPI connector.

Build the standalone target with:

pio run -e qdtech_es3c28p
pio run -e qdtech_es3n28p

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 green-sticker revision starts in refresh=pulse with the Fastwave phase builder adapted to its UC8276 controller. display mode display0 reports waveform=fastwave-uc8276. Its preset is 8 drive cycles, 8 scan frames, raw PLL code 12, asynchronous scanning with a 67 ms new-image interval, supply cycling, fast ramp, and 20000 kHz pulse SPI. Fixed cleanup runs every 8 changed targets consumed by the worker. It shares per-pixel drive budgets and the scan worker with the other pulse-capable panels. The builder drives only changing pixels, with no reset kick and one neutral settle frame. The adapter keeps the UC8276's own voltage, PLL, RAM, and power commands. pulse-period accepts 67–1000 ms between new images. Settling drives run as BUSY permits, independently of this interval. New targets can interrupt settling between drives; identical targets retain their remaining budgets. The requested interval does not guarantee an achieved rate. Runtime tuning remains available through display mode display0; internal scan frequency is separate from image update rate. Use display mode display0 refresh=auto to restore the native waveform. The original Elecrow 4.2-inch panel currently rejects pulse mode.

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.

Elecrow CrowPanel ESP32-S3 5.79-inch E-paper

The built-in elecrow_crowpanel_esp32_s3_5_79_epaper target covers Elecrow's DIS08792E 792x272 monochrome CrowPanel. It uses the same N8R8 module, console, microSD, controls, status LED, power gates, and expansion-header wiring as the 4.2-inch board. The display is different: two cascaded SSD1683 controllers drive two 396x272 halves. Their 400-pixel RAM widths create an internal eight-pixel address gap, which the SolarOS driver keeps white while presenting one contiguous 792x272 graphics surface.

The panel uses GPIO12 SCK, GPIO11 MOSI, GPIO47 reset, GPIO46 D/C, GPIO45 chip select, GPIO48 BUSY, and GPIO7 display-power enable. The driver follows Elecrow's master/slave RAM addressing and temperature-load sequence. The board starts in asynchronous monochrome pulse mode with a 67 ms minimum new-image interval, held supplies, and automatic cleanup, as described under SolarWriter. To select the native waveform, use display mode display0 refresh=auto. Auto mode uses one full cleanup refresh after initialization, then transfers changed frames with Elecrow's non-flashing 0xDC partial-update waveform; unchanged frames are skipped. Use display mode display0 refresh=full when an explicit full cleanup is needed.

The wide panel can also run two independent side-by-side sessions:

display split display0 --horizontal left0 right0
session create shell right0

Session 0 moves to left0 automatically. Use Ctrl+Alt+Left/Right, or Ctrl+AltGr+Left/Right on keyboards without a left Alt key, to move local input focus between the two viewports. display unsplit display0 restores the single 792x272 target without discarding session 0 shell state.

Build and flash the target with:

pio run -e elecrow_crowpanel_esp32_s3_5_79_epaper
pio run -e elecrow_crowpanel_esp32_s3_5_79_epaper -t upload --upload-port /dev/ttyUSB0

The onboard CH340 normally enumerates as /dev/ttyUSB* on Linux; use the actual serial-device path reported by the host when it differs.

The same 8 MB OTA and serial partition layouts documented for the 4.2-inch CrowPanel apply to this board.

SolarWriter

The solar_writer target combines the Elecrow CrowPanel 5.79-inch N8R8 board with a GY-INMP441 microphone, Adafruit PCM5102 DAC, Adafruit I2S Amplifier BFF (MAX98357), Adafruit LC709203F battery gauge, and DS3231 real-time clock. It inherits the CrowPanel's 792x272 dual-SSD1683 display, microSD, controls, CH340/UART console, Wi-Fi, BLE, 8 MB flash, and 8 MB OPI PSRAM. The default firmware flavor is writerdeck.

The following connections are fixed in this target:

FunctionGPIOU5 pinModule connection
I2C SDA820LC709203F SDI/SDA and DS3231 SDA
I2C SCL92LC709203F SCL and DS3231 SCL
Shared I2S bit clock1419INMP441 SCK, PCM5102 BCK, BFF A2
Shared I2S word select153INMP441 WS, PCM5102 WSEL, BFF A1
Playback data1618PCM5102 DIN and BFF A0
Microphone data174INMP441 SD
Jack detect, reserved216Future isolated switch to GND, active low

U5 pins 11–14 supply regulated 3.3 V and pins 7–10 are GND, as connected in Elecrow's published schematic. Power the microphone VDD, PCM5102 VIN, fuel-gauge VIN, DS3231 VCC, and amplifier BFF's 5V input pad from 3.3 V for this configuration. The BFF's 3V pad does not supply the amplifier. Use short power wiring and allow for speaker current when sizing the supply. Higher-power speaker operation requires a suitable separate supply with common ground; do not inject 5 V into U5's 3.3 V rail.

The battery connects through the LC709203F's parallel battery sockets to the CrowPanel BAT connector. Use a polarity-correct adapter for the different connectors. The gauge chip is battery-powered; its VIN connection sets the I2C logic level. No thermistor or interrupt wire is required. Fuel-gauge compensation uses an assumed 25.0 °C, not a measured temperature.

audio0 attaches automatically through the reusable inmp441 driver. It appears as INMP441 mic + I2S DAC/amp in audio device information and publishes audio0.capture as 16 kHz mono and audio0.playback as 16 kHz stereo. The microphone uses received slot 1. The DAC and amplifier share one playback data stream; the DAC provides stereo line output and the amplifier plays its default mono mix. The PCM5102 jack requires a line-level load, such as powered speakers or a headphone amplifier.

The 5.79-inch dual-SSD1683 display starts in monochrome pulse mode on both SolarWriter and the base CrowPanel 5.79-inch target. It uses the shared EPD pixel-budget layer and the Fastwave SSD1683 waveform builder. Mode output identifies this adapter as waveform=fastwave. If the scan worker cannot start, the display retains native refresh=auto mode. Inspect the mode with display mode display0, or select and tune it with:

display mode display0 refresh=pulse
display mode display0 drive-cycles=6
display mode display0 pulse-frames=8
display mode display0 waveform-fr=4
display mode display0 cleanup-every=64
display mode display0 cleanup-policy=adaptive
display mode display0 pulse-scan=async
display mode display0 pulse-period=67
display mode display0 pulse-power=hold
display mode display0 pulse-ramp=fast
display mode display0 pulse-spi-khz=20000
display mode display0

Each changed pixel receives drive-cycles completed waveform activations (1–32). Settled pixels select the neutral waveform; submitting the same target does not refill its budget. pulse-frames is the scan-frame duration of each drive phase (1–32); waveform-fr is a raw SSD1683 frame-rate code (1–5 or 9–12), not a frequency in Hz. Defaults are 6 cycles, 8 scan frames, code 4, and adaptive cleanup with cleanup-every=64. The builder uses no opposite-polarity reset kick, the configured drive duration, and one VSS settle frame. Both controllers receive the same generated LUT and nominal source rails (+15 V / -15 V), with VCOM set to 0x08. EOPT is excluded from the voltage-register write mask. Grayscale rendering continues to use the existing dithering path.

Each successful drive accumulates in a per-pixel history since the last native cleanup. Adaptive cleanup runs when the most-driven pixel reaches cleanup-every * drive-cycles activations (384 at the defaults), or after 4 * cleanup-every changed images (256 at the defaults), whichever comes first. Sparse changes can therefore run longer than repeated transitions at the same pixels. The age bound also limits time spent applying neutral waveforms to unchanged pixels; it is an activity heuristic, not an optical ghost detector. Idle time and repeated identical targets do not age cleanup.

Use cleanup-policy=fixed to clean up every cleanup-every changed images. Synchronous mode counts completed changed presentations; asynchronous mode counts changed targets consumed by the worker. At either limit, the asynchronous worker schedules a native cleanup even without another presentation. Cleanup uses its newest consumed target and can interrupt unfinished budgets. display mode display0 reports cleanup-policy, cleanup-peak-cycles, cleanup-limit-cycles, and cleanup-age for tuning. The cycle limit applies only in adaptive mode. Drive history counts completed activations, including reversals; it supplies no measurement of ink settling. Settings are runtime-only. Pulse mode is experimental: its speed, polarity, ghosting, and optical settling require verification on the physical panel. It does not provide native grayscale. Lower budgets can complete without visibly moving the ink, so a successful BUSY cycle does not prove optical settling. Reduce the cycle budget and frame duration only while inspecting the screen in both transition directions.

With pulse-scan=sync, the driver completes all pending cycles before returning from a presentation. Increasing the cycle budget increases that presentation's blocking time. The default pulse-scan=async submits the newest image to a persistent scan worker. It delivers one pulse per scan and continues settling without further presentations. New targets can arrive between cycles; queued images coalesce to the latest target. Settling can span several scans. The SSD1683 adapter also accepts monochrome raster frames from native foreground apps. The shared presenter scales and dithers indexed emulator frames before submission. These frames use the same refresh mode and latest-image queue as ordinary UI updates; physical cadence and ink settling follow the panel waveform. Select asynchronous operation with:

display mode display0 pulse-scan=async
display mode display0 pulse-period=67

On this dual-controller panel, pulse-period requests a minimum new-image interval of 67–1000 ms; the default is 67 ms. Settling drives continue as BUSY permits without waiting for another image slot. When its slot is due, a queued target replaces the current target between drives, even with older pixels still settling. Pixels whose target is unchanged retain their remaining budgets. Hardware work and pixel settling can require longer than the requested interval; it does not promise 15 fully settled images per second. The six-cycle, eight-frame preset retains its settling budget. display mode display0 reports scan-last-us, scan-interval-us, completed scan-count, submitted/coalesced image counts, and pending pixel count. frame-count counts distinct target snapshots processed successfully, including native baselines; frame-interval-us measures their start-to-start interval. Repeated identical submissions do not count as new frames. Drive count, new-image count, and optical settling are separate quantities.

Additional panel-specific controls are pulse-power=<cycle,hold>, pulse-ramp=<normal,fast>, and pulse-spi-khz=<100..20000>. The default power policy is hold, which retains the supply stages during a settling sequence and powers down when all budgets finish. It enables the analog stages on the first pulse of a sequence and uses display-only activations for subsequent pulses. A new sequence after idle power-down or reset enables the stages again. The normal booster ramp uses approximately 40/40/10 ms phases; the default fast uses 10/10/10 ms with the same current settings and voltage targets. Both ramp settings require optical calibration. Pulse SPI speed defaults to 20000 kHz; its override applies only in pulse mode, with the normal clock restored for native operation. These controls do not guarantee a particular frame rate.

The first presentation establishes a native full-refresh baseline. Logs report changed pixel count, completed cycles or scans, SPI transfer time, and waveform BUSY time separately in log show. Asynchronous scan logs also report the interval between waveform activations. A failed transfer or waveform does not consume the failed cycle's budget and returns the driver to refresh=auto. Synchronous mode recovers on the next paint; asynchronous mode immediately performs a native recovery of the newest accepted target. Sleep suspends scans; waking reseeds from a native baseline. Use display mode display0 refresh=auto to restore the standard waveform; the next presentation resets the controller and performs a full cleanup. For repeated native black/white recovery cycles, use display relax display0 (60 seconds) or display relax display0 3600 (one hour). The command restores the saved image and pulse settings when it finishes or is interrupted. See Display commands for supported panels and cancellation. The pattern script tests/hil/epd_pulse_pattern.py exercises both transition directions and moves patches across both controller halves. Its printed messages mark queued presentations; gfx.present() returns before physical completion. Read the driver timings in log show and judge optical settling and ghosting on the actual screen. tests/hil/epd_pulse_scan_pattern.py queues targets at 67 ms intervals with moving black and dithered patches and a stationary shade ramp, then holds the final image so its pixels can settle. Use the driver statistics and physical screen to assess contrast, erased trails, static shades, and the panel seam.

battery0 attaches automatically at I2C address 0x0b. Battery capacity defaults to 3000 mAh and remains configurable:

expansion devices
audio devices
battery status
battery capacity 3000

rtc0 attaches automatically through the reusable ds3231 driver at 0x68 on the same 100 kHz i2c0 bus as battery0. Its driver is included in every SolarWriter flavor. The clock stores UTC and supports calendar dates from 2000–2099 and second-resolution alarms. Set the clock after first power-up or loss of backup power; oscillator-stop status leaves time unreliable until set.

rtc status
date
ntp
rtc alarm set 07:30:15

ntp needs a network connection and the NTP package. INT/SQW is not wired in this profile, so RTC alarms have no GPIO wake signal. The DS3231 has no countdown timer; use schedule for portable timed actions. See DS3231 usage.

GPIO21 is reserved in the pin metadata and is not available for general GPIO or PWM. Plug detection and automatic output switching are not enabled in this profile. The PCM5102 board ties its jack switch contacts to audio nets; a separate isolated detection circuit is required. Both outputs receive playback regardless of plug insertion. GPIO18, GPIO19, GPIO20, and GPIO38 remain free; the optional DAC mute and fuel-gauge interrupt signals are not claimed.

Build with pio run -e solar_writer. The target uses the same 8 MB OTA and serial partition layouts as the base CrowPanel.

Waveshare 4G E-paper

The waveshare_esp32_s3_sim7670g_4g_epaper target uses the monochrome Waveshare 4.2-inch V2/revision-2.2 SSD1683 profile at 400x300. Native refresh defaults to auto, with dirty-window partial updates and periodic full cleanup. The display remains on its existing board-owned SPI bus and pins.

The optional experimental pulse adapter uses row-major transfers on one controller, the shared per-pixel drive budget and history, asynchronous scans, and adaptive native cleanup. Select it with:

display mode display0 refresh=pulse
display mode display0

Initial pulse defaults are 3 drive cycles, 8 scan frames, FR code 4, asynchronous operation, a 100 ms requested minimum new-image interval, supply cycling, normal booster ramp, the configured board SPI clock, and adaptive cleanup with a baseline of 64. These values are a calibration starting point; optical contrast, erasure, ghosting, and achieved cadence require inspection on the physical panel. The requested interval is not a measured frame rate.

The adapter exposes the same pulse controls as SolarWriter, including drive-cycles, pulse-frames, waveform-fr, cleanup-every, cleanup-policy, pulse-scan, pulse-period, pulse-power, pulse-ramp, and pulse-spi-khz. Mode output reports scan timings, pending pixels, and cleanup counters. log show includes native transfer/drive timings and pulse scan timings for comparison. Faster ramp, held supplies, and higher SPI clocks require separate panel and wiring validation.

The waveform uses the power rails and VCOM setting published for Waveshare's 4.2-inch V2 custom-LUT path; it is a monochrome differential drive waveform, not native grayscale. Switching back with display mode display0 refresh=auto resets the controller and cleans up on the next presentation. A failed asynchronous pulse disables scanning and immediately restores the newest accepted target with a native full refresh.

Waveshare ESP32-S3-ePaper-3.97

The built-in waveshare_esp32_s3_epaper_3_97 target covers Waveshare's 3.97-inch board with an ESP32-S3-WROOM-1-N16R8 module. SolarOS presents the native 800x480 monochrome SSD1677 panel with a 480x800 portrait base orientation. The display defaults to 800x480 landscape, rotated 90 degrees counterclockwise from that portrait orientation (display orientation display0 270). A saved orientation preference takes precedence over this factory default. The fixed devices include four-bit SDMMC storage, a PCF85063 RTC, an SHTC3 temperature and humidity sensor, and ES8311 duplex speaker/microphone audio. Native USB provides the primary CDC console; GPIO43/GPIO44 remain available as uart0 on the breakout pads.

The SSD1677 runs at the vendor reference clock of 20 MHz. Automatic refresh starts with a full cleanup, skips unchanged frames, uses byte-aligned dirty partial windows, and performs another full cleanup after 19 partial updates. Refreshes use the differential waveform with separate current and previous frame data; full cleanups force every pixel to transition. The display attachment sets the AXP2101 ALDO3 rail to 3.3 V, enables it before controller initialization, and disables it when the panel sleeps. This profile follows Waveshare's current AXP2101 reference source and schematic; a board revision fitted with a different power-management IC needs a separate profile or power callback before it is safe to use. Override the refresh policy when testing or clearing ghosting:

display mode display0 refresh=auto
display mode display0 refresh=partial
display mode display0 refresh=full

The onboard controls are mapped as follows:

  • GPIO0 BOOT: foreground app exit.
  • GPIO4 rotary up: Up.
  • GPIO6 rotary down: Down.
  • GPIO5 rotary press: Enter.

The exposed SDA/SCL pads share the fixed i2c0 bus with the onboard RTC, environment sensor, audio codec, PMIC, and IMU. The RX/TX pads expose uart0. The fixed AXP2101 attachment publishes its fuel-gauge data through battery and its charging state and controls as charger0. The board profile applies the vendor reference defaults of a 1500 mA VBUS input limit, 200 mA constant charge current, and 4.2 V target voltage. Use battery status and charger status charger0 to inspect them. The fixed imu0 attachment polls the QMI8658 at the reference repository's low I2C address (0x6a) and exposes plus/minus 8 g acceleration and 512 degrees-per-second angular velocity ranges in SI units. Use imu sample imu0 to read both vectors. Orientation is not advertised because this profile enables the raw accelerometer and gyroscope rather than the chip's AttitudeEngine.

Build the target with:

pio run -e waveshare_esp32_s3_epaper_3_97

Seeed Studio reTerminal Sticky

The seeed_reterminal_sticky target uses an ESP32-S3R8 with 32 MB flash and 8 MB PSRAM. It defaults to the full flavor. Its 3.97-inch 800x480 e-paper panel defaults to native landscape orientation. The sticky-display device identifies SSD1677 and SSD2677 panels at startup, then registers the selected controller and driver under display0. Detection checks command 0x70 and stable post-reset BUSY polarity twice. BUSY polarity also supports panels without working SPI readback; floating or inconsistent BUSY signals fail attachment. The result is checked on every attachment rather than cached in NVS. Pins follow the Seeed hardware overview:

  • E-paper: SCK GPIO13, MOSI GPIO14, CS GPIO15, DC GPIO16, RST GPIO17, BUSY GPIO18, EN GPIO47
  • Touch (GT911): SCL GPIO2, SDA GPIO3, INT GPIO21, RST GPIO41, EN GPIO42; 180-degree panel alignment before display rotation.
  • Buttons: AI/Power GPIO4 (tap: light sleep/wake, hold: shutdown), Up GPIO5 (tap: Up arrow, hold for two seconds: BLE pairing), Down GPIO6 (tap: Down arrow, hold for two seconds: app exit); all active-low.
  • Power: PWR_HOLD GPIO45, PWR_LOCK GPIO46; BQ25616 charger enable GPIO39 is configured as an output and driven LOW at boot to enable charging.
  • Console: UART0 GPIO43/44 (CH343 USB-C bridge), DIO flash
  • Sensor I2C: SCL GPIO0, SDA GPIO1; PCF8563 RTC at 0x51, SHT40 at 0x44, BQ27220 at 0x55
  • microSD: shared display SPI bus, MISO GPIO12, CS GPIO8, power GPIO10
  • Buzzer: PWM GPIO48, attached as audio0 through audio-pwm.

The default devices are display0, touch0, rtc0, environment0, battery0, imu0, storage0, audio0, and mic0. The console uses the USB-UART bridge; GPIO19/20 carry the PDM microphone clock/data. The 32 MiB partition table keeps two 7 MiB OTA application slots and reserves nearly 18 MiB for FAT storage at /flash, mounted alongside /sdcard. The serial-only layout uses one 14 MiB application slot and the same storage region. Updating from the former 16 MiB layout requires flashing the partition table over USB; an OTA image cannot change it. Back up flash files before changing layouts or formatting storage.

The SD rail stays powered while the shared display SPI bus is active. Use expansion attach storage0 after insertion or reinsertion to attach the slot or recover its mounted card.

An SSD1677 display starts with asynchronous refresh=pulse / waveform=fastwave at 20 MHz. The Sticky panel profile (panel=1) uses matching black/white data in both RAM planes for its factory full-refresh waveform. Pulse mode uses short monochrome drives with per-pixel settling budgets. Defaults are 100 ms between new images, five drive cycles, eight scan frames, frame-rate code 8, retained supplies during settling, and adaptive cleanup with baseline 64. refresh=fast aliases native refresh=partial; waveform=otp restores auto. If pulse resources cannot be allocated at startup, native auto mode remains available. Pulse buffers are allocated in PSRAM when enabled (about 2.75 MiB including worker snapshots). Optical contrast and ghosting depend on the panel. Returning to a native mode releases the pulse buffers, snapshots, and worker. pulse-scan=sync releases the asynchronous worker and its snapshots. The native framebuffer prefers PSRAM; SPI DMA staging stays in internal SRAM. Touch swaps the GT911 axes before its 180-degree board rotation to match landscape. The direct MONO1 frame presenter supports scaled animation and games at a preferred 10 presentations per second on SSD1677. Pulse settling continues asynchronously.

SSD2677 uses its own Sticky 3D97 V2 monochrome profile at 20 MHz. Sticky's panel=1 starts in asynchronous refresh=pulse / waveform=fastwave. The driver uploads an SSD2677-specific 535-byte RAM waveform and uses the same pixel-budget and scan worker as SSD1677. Defaults are drive-cycles=8, pulse-frames=4, waveform-fr=5 (85 Hz), pulse-period=100, pulse-power=hold, and adaptive cleanup every 64 changed targets. Static pixels continue settling after the application stops painting; newer frames replace queued targets without waiting for every preceding frame to settle. drive-cycles and pulse-frames accept 1..32. SSD2677 waveform-fr accepts 1..5, corresponding to 25, 50, 65, 75, and 85 Hz. Other pulse tuning, cleanup, inversion, synchronous scan, and SPI-rate controls follow SSD1677 semantics. The panel's own OTP supplies analog voltage settings. Full cleanup, recovery, and final power-state images use factory waveforms. Pulse mode uses aligned RAM windows around active pixels, including the preceding mask so settled pixels are cleared to HOLD. The first mask initializes the full visible RAM; subsequent transfers cover only that window.

Both Sticky controller revisions default to display0 scale 2 (a 400x240 logical canvas on the 800x480 panel). A saved per-display scale takes precedence.

Native refresh=auto|fast|partial|full modes remain available. The panel=0 alias starts in auto mode. refresh=partial selects factory partial refresh; refresh=fast selects partial refresh with a fixed 85 Hz scan rate. refresh=full forces factory full refresh. Auto and fast modes perform a full cleanup after 19 changed partial updates; explicit partial mode bypasses periodic cleanup. Full refreshes always use factory scan timing. Unchanged frames are skipped. This profile transmits full-frame old/new two-bit transition pairs. The panel scans 680 gate lines while accepting the 480 visible image rows. Temperature readings select the vendor waveform bucket, with a 25 C fallback for unavailable readback. Partial updates reuse that selection and resample temperature every five seconds; full refreshes always resample temperature and reload the factory waveform. The MONO1 raster presenter supports all four rotations, palette inversion, scaled animation and games, with a preferred rate of ten presentations per second. Actual scan time depends on the waveform, transfer cost, and temperature. display mode display0 reports scan/submission/coalescing statistics, pending pixels, the configured SPI rate, and the last native refresh, transfer, and panel drive times in microseconds. The driver reverses the V2 gate-row order while preserving the source X direction so the default orientation and touch calibration match SSD1677 units. Recovery (display relax), final power-state presentation, and sleep/resume use native full refreshes. The driver restores its short RAM waveform after the next pulse baseline. The SSD1677 LUT/register format cannot be used directly on SSD2677. This profile does not support generic SSD2677 color panels.

The LSM6DS3TR-C at 0x6a supplies acceleration and angular velocity through imu0. Its GPIO7 interrupt also supplies hardware tap, double-tap, tilt, wake, and free-fall input gestures. With gestures enabled, acceleration runs at 416 Hz and angular velocity at 104 Hz. Single taps wait 650 ms for a possible double tap. Use input test imu0 to inspect events and gesture bind source=imu0 to assign actions. wake reports motion; it does not automatically wake the OS. Fused orientation is not provided. The microphone provides mic0.capture: 16 kHz, signed 16-bit mono PCM through I2S0's PDM receiver. GPIO38 and capture DMA are enabled only while its stream is open; capture can run alongside the PWM buzzer output. Use audio mic 1000 for a level check, or select mic0.capture in arecord or Recorder. Native four-level grayscale and full peripheral power optimization remain unimplemented for this target. Battery percentage comes from the BQ27220 state-of-charge register; charging state is inferred from its discharge flag.

Power-key shutdown runs after release and releases the hardware power latch after jobs, storage, and display shut down. With USB power connected, the shutdown path falls back to deep sleep. Light sleep retains the latch and resumes the current session. An Up hold starts pairing once and does not send an arrow on release.

Build and flash:

pio run -e seeed_reterminal_sticky pio run -e seeed_reterminal_sticky -t upload

Xteink X4 Pro

The xteink_x4_pro target uses an ESP32-S3, 16 MB QIO flash at 80 MHz, and 8 MB OPI PSRAM. Its 800x480 e-paper display defaults to native landscape orientation. One firmware image covers SSD1677, UC8179, and UC8279 units. When SolarOS has no cached controller ID, it reads the UltraChip version/status registers twice over the bidirectional MOSI line. Recognized UltraChip responses select UC8179 or UC8279; repeatable floating-high responses select the board's SSD1677 alternative. Ambiguous responses leave the display unavailable and are not saved. After controller initialization, SolarOS stores its own controller ID in NVS namespace x4pro, key controller, and reuses it on subsequent boots. These IDs are independent of the stock firmware's calibration values. To repeat identification, use nvs erase x4pro controller; a successful erase reboots the unit. An invalid cached ID is reported instead of silently selecting a different controller.

Use display list to inspect display readiness and brightness support. The display supports display mode display0 refresh=auto, refresh=partial, and refresh=full. Automatic mode skips unchanged frames; UltraChip controllers perform a full cleanup every 20 changed frames. Partial mode uses a full refresh to establish the first frame after initialization or resume.

The frontlight uses two active-high PWM outputs: cool on GPIO8 and warm on GPIO9, at 25 kHz with 10-bit resolution. setterm brightness 0..100 controls their combined brightness at a fixed 50/50 mix. Zero turns both off; display suspend also turns both off and resume restores the selected brightness. Brightness uses the existing SolarOS persistence setting. Color-temperature adjustment is not exposed.

The shared 400 kHz i2c0 bus uses SDA39/SCL38 and carries GT911 touch (touch0, 0x5d with 0x14 fallback), BM8563-compatible RTC (rtc0, 0x51), and CW2017 battery gauge (battery0, 0x63). Touch uses reset GPIO4, interrupt GPIO10, active-low power GPIO2, and a portrait-to-landscape coordinate transform. The capacitive Home pad sends app exit on release. The left/right buttons (GPIO0/GPIO7) send Up/Down. GPIO3 is the system key: a short press defaults to app exit. Its short and long actions are independently configurable with setterm powerkey short <action> and setterm powerkey long <action>. Holding GPIO0 during reset selects the ESP32 bootloader.

The one-bit SDMMC slot (storage0) uses CLK41/CMD42/DAT0=40. Its active-low GPIO5 rail is power-cycled before card initialization. GPIO1 holds the shared peripheral rail on. The charger STAT signal on GPIO21 is reserved; USB/VBUS presence is not advertised. Native USB CDC and UART0 provide console access; the profile does not declare an external expansion connector.

Hardware mapping follows the FreeInk X4 Pro hardware profile and its controller-identification implementation.

pio run -e xteink_x4_pro

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. The short press defaults to suspend and the long press to BLE keyboard replacement. Configure either with setterm powerkey short <action> or setterm powerkey long <action>. A press of any duration wakes suspend or sleep. 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.

GOOUUU ESP32-S3CAM GPIO

GPIO2, GPIO3, GPIO45 and GPIO46 are available for runtime GPIO and PWM. GPIO2 and GPIO3 also support ADC. GPIO46 supports both input and output on ESP32-S3. Configure them explicitly with gpio mode, pwm or an expansion attachment.

GPIO2 may carry the board's status LED load. GPIO3 and GPIO46 are also labeled as onboard switch/shutter signals. Check that circuitry before driving outputs. GPIO3, GPIO45 and GPIO46 remain strapping pins during reset. GPIO3 selects a JTAG source, GPIO45 can select the flash/PSRAM supply voltage, and GPIO46 participates in boot-mode and ROM-message selection. External wiring must preserve the levels required for boot, including when a peripheral is powered before the board. These reset functions are independent of runtime GPIO policy. See the ESP32-S3 datasheet for strapping and eFuse overrides.

The runtime GPIO/PWM set is GPIO1-GPIO3, GPIO14, GPIO21, GPIO41, GPIO42 and GPIO45-GPIO47. ADC is available on GPIO1-GPIO3 and GPIO14. Camera, USB, PSRAM, SDMMC, console UART, BOOT/KEY and WS2812 pin assignments remain reserved.

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

A flavor selects which features are compiled into a firmware image. A group is a convenient selection, such as scripting or vision. A package supplies a particular app, job, service, or driver. Required dependencies are included automatically; unsupported packages are removed for the selected board.

Inspect the running image

pkg system
board
apps
jobs

pkg system shows compiled packages. pkg installed lists separately installed native modules. A compiled driver is available to use, but does not mean its hardware is attached; check expansion devices.

Choose features

Use the SolarOS Builder to select a board and flavor, or create a custom flavor below. The full flavor includes these optional groups where the board supports them:

GroupUseGuide
visionDecode QR codes and process images.Computer vision
inferenceRun resident models on ESP32-S3 with PSRAM.Model inference
inference_backendInclude the ESP-DL engine in firmware; otherwise install the espdl library.Model inference
jetInclude the script/display frontend for the separately installed Jet library. Also selected by Python and Lua on supported boards.Jet 3D renderer
zooBrowse and install model bundles.Zoo
pipelinesProcess images or video in the background. Also select the desired processor.Image pipelines
servoControl calibrated positional servos.Servo outputs
pwmUse native PWM outputs where supported.GPIO and PWM
pca9685Add 16 PWM outputs over expansion I2C.PCA9685
drv8830Control brushed DC motors over named I2C buses.Motor control
motorCommon motor service, shell controls and Python/Lua APIs.Motor control
encoderCommon encoder counts, shell controls and Python/Lua APIs.Encoder counts
pcnt_encoderAttach native A/B encoders with explicit x1/x2/x4 counting.Encoder counts
ds3231Add a battery-backed I2C clock and hardware alarm.DS3231

Python or Lua alone does not enable optional media, vision, or inference services. Stored-image processing needs no camera or display. Audio can use an expansion output on a board without a built-in speaker.

For external hardware, select its driver group and check the board's expansion capabilities and available pins. Built-in hardware uses fixed instances of the same drivers. See Expansion for attachment commands and Boards for hardware limits.

Build reference

Package definitions live in packages/solar_os_packages.toml; flavor files normally select groups. The following rules apply when maintaining these build files.

Ownership Rules

  • bootstrap is immutable and contains only the runtime and shell needed to start SolarOS. A flavor cannot disable its members. On ESP32-S3 boards with PSRAM it also contains the native-module loader service and its load maintainer command, plus signed-catalog installation through pkg; target and capability pruning remove those services from other boards.
  • Native module manifests and signed catalogs carry an explicit app, job, driver, or library type. Each type has a versioned runtime contract. Installed artifacts use separate /modules/apps, /modules/jobs, and /modules/drivers, and /modules/libraries namespaces. Application modules run synchronously and unload; jobs and drivers remain resident while their callbacks are registered. Libraries load on first acquisition and remain resident until unloaded; leases protect their API while consumers use it. See Native libraries. A per-entry lifecycle ABI is distinct from the native host ABI.
  • 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, invaders, 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 invaders group selects the built-in Invaders game. Game Boy and Apple II are installed as external foreground apps through the module catalog and require native-module support in the firmware. They do not add firmware flavor groups.

SolarOS provides three firmware profiles. full is the standard profile with the broad application and hardware-service selection. classic is a compact profile with ADC D-pad support for ODROID-GO, the I/O app, control bindings, and 1-Wire. It is the default for all classic ESP32 targets, including boards with 4 MiB flash. writerdeck is the compact profile used by e-paper boards with smaller application partitions.

classic retains Python, Playground, writing, media, communications, and the selected board's required hardware drivers. Lua, built-in games, video playback, and image-processing pipelines are omitted. ODROID-GO retains its buttons and ADC D-pad; VGA32 retains its default PS/2 keyboard attachment.

writerdeck includes Python, Playground, editing, Files, Reader, Writer, Notes, View, audio playback, recording, speech synthesis, Web, Agent, WireGuard, messaging, and file transfer. It includes SC2 messaging. The synthesizer and controls worker are omitted; native apps that use the Synth service require a custom flavor with synth enabled. Its transfer tools include FTP/FTPD, SSH, SCP, SFTP, SFTP Sync, and Curl. PCM5102/I2S and PWM audio remain available for runtime expansion outputs. Broad expansion-driver collections, external radio stacks, Lua, built-in games, and RTSP are omitted. Board and target requirements still prune unsupported features from every profile.

On ESP32-S3 boards with PSRAM, all profiles include pkg for installing native modules. Native-module installation is unavailable on classic ESP32; Python and Playground remain available on its PSRAM boards. On supported targets, all profiles use the downloadable espdl library for inference. Custom profiles can select inference_backend to include the ESP-DL engine in firmware.

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/writerdeck.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 system on the device to inspect the resolved firmware package list. Use pkg without arguments to browse signed native modules in the package manager.

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 system on-device to inspect the resolved firmware.