SolarOS

ESP32 operating environment · 4.13.4

Small system.
Real tools.

SolarOS lets makers build embedded devices without starting from bare metal. It combines a shell, scripting, applications, networking, and hardware access into a single lightweight operating environment.

solar@pocket:/ apps
agent   aplay   arecord
recorder   funcgen   player
calc   chat   clock
com   curl   webradio
help   edit   hexedit
launcher   files   ftp
flash   contacts   inbox
email   io   invaders
gameboy   less   logic
lua   notes   plot
playground  python   reader
writer   scp   sftpsync
sheet   ssh   synth
telnet   view   sketch
web

solar@pocket:/ _

Everything you need. Nothing you don't.

Inside the system

A compact, capable runtime.

The same shell, apps, and jobs work across handheld and headless boards.

01

Local shell

History, aliases, scripts, completion, storage, and resumable sessions.

02

Foreground apps

Edit, read, inspect data, connect remotely, browse, and capture logic.

03

Background jobs

Logging, acquisition, NTP, HTTP, SLIP, bridges, and battery monitoring.

04

Python & Lua

Automate services and hardware with embedded scripting APIs.

05

Connected hardware

Wi-Fi, BLE, SD, RTC, GPIO, ADC, PWM, I²C, SPI, audio, and sensors.

06

Firmware flavors

Build focused or full images from capability-aware packages.

What's new

Recently added.

01

Gesture-driven automation

Map gestures from any compatible input source to shell commands, scripts, or key chords such as Alt+Left and Alt+Right, with live source and gesture completion and a normal background-job lifecycle.

Explore gesture bindings →
02

Palm Graffiti handwriting

Turn the whole touchscreen into an overlay-free Palm Graffiti keyboard: start in the left two-thirds for letters or the right third for numbers while the foreground application keeps receiving touch input.

Explore Graffiti input →
03

Skywriter 3D gestures

Attach an MGC3130 Skywriter over I2C for normalized 3D axes, flick, tap, and AirWheel events, plus absolute hover-pointer motion without treating its electrode regions as a touch surface.

Explore the MGC3130 driver →
04

Incremental SFTP transfers

Copy changed files or directory trees in either chosen direction over a standard SSH server, with recursive and dry-run modes, per-file progress, Ctrl+C cancellation, and Python and Lua bindings.

Explore sftpsync →
05

SD-aware shell startup

Keep startup commands with a board-owned SD card automatically, with a safe fallback to internal flash when the card cannot be mounted and explicit flash-only or SD-only overrides when needed.

Explore startup storage →
06

Expansion prototypes to custom boards

Export tested runtime buses and attached devices as a versioned hardware snapshot, then import it into the desktop configurator to create an inherited, board-owned target.

Follow the custom-board tutorial →
07

Guided expansion setup

Attach hardware with selectors built from the live I/O map: compatible buses and pins show their routes and connector positions, while resources already in use stay out of the way.

Explore guided attachments →
08

Waveshare 4G board support

Run SolarOS on the Waveshare ESP32-S3-SIM7670G-4G V2.0 with LTE and GNSS, SD storage, battery monitoring, a status pixel, software modem power control, and camera-off expansion pins.

Explore the Waveshare 4G target →

SolarOS documentation

gesture-listener

Gesture-to-command listener. Rules are configured independently with gesture bind, while this job owns observation and dispatch:

gesture bind source=gesture0 gesture=flick direction=east -- input emit ALT+RIGHT
gesture bind source=gesture0 gesture=flick direction=west -- input emit ALT+LEFT
job start gesture-listener
job status gesture-listener
job stop gesture-listener

Stopping the job preserves the rules, prevents new gesture actions, and drops queued actions. A command already executing finishes normally. Use gesture bindings to inspect rules and counters, gesture unbind <id> to remove one, or gesture unbind all to clear the table and reset the next ID to 1. Rules and job state are volatile; recreate them in the startup script when persistence is needed. The command worker is created on demand and releases its internal stack when its queue is idle.

SolarOS documentation

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.

SolarOS documentation

Drivers and Bindings

Run expansion drivers on the device to see the exact compiled set.

DriverDeviceRequired bindingsResult after attach
manualResource-only profileAny valid bus, address, chip-select, GPIO, ADC, or PWM bindingsClaims resources without initializing hardware.
rfm69HopeRF RFM69W/CW packet radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers a packet-radio target with PA0 power from -18 through 13 dBm.
rfm69hHopeRF RFM69HW/HCW high-power packet radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers a packet-radio target with PA_BOOST power from -2 through 20 dBm.
rfm95HopeRF RFM95W multimode radiospi=<bus> cs=<pin>; optional irq=<pin> reset=<pin>Registers an FSK/GFSK/MSK/GMSK/OOK/LoRa target for the radio command.
sx1262Semtech SX1262 packet radiospi=<bus> cs=<pin> busy=<pin>; optional reset=<pin> irq=<pin>Registers a LoRa/(G)FSK packet-radio target for the radio command.
pcd854484x48 SPI LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>Registers an auxiliary display target.
ssd1683400x300 monochrome e-paperspi=<bus> cs=<pin> dc=<pin> reset=<pin> busy=<pin>; optional power=<pin> clock=<khz> rotation=<0..3> panel=<0..3>Registers an auxiliary target, or Elecrow's fixed display0, with auto, fast, and full refresh modes. Panel 0 auto-detects the Elecrow revision; 1 is legacy Elecrow, 2 is green-sticker Elecrow, and 3 is Waveshare V2.
ssd1306128x64 I2C OLEDi2c=<bus> addr=<address>Registers an auxiliary display target.
sh1106128x64 I2C OLED with SH1106 addressingi2c=<bus> addr=<address>Registers an auxiliary display target with the two-column offset.
st7305400x300 or 384x168 reflective LCDspi=<bus> cs=<pin> dc=<pin> reset=<pin>; optional `panel=01 rotation=13`ESP32 and ESP32-S3. Panel 0 is the native 300x400 Waveshare panel; panel 1 is the native 168x384 CL-32 panel. Both boards register their panel as fixed display0.
ili9341320x240 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> active=01 pwm=01`ESP32 and ESP32-S3; ODROID-GO registers it as fixed display0.
st7796480x320 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> active=01 pwm=01`ESP32 and ESP32-S3; Freenove registers it as fixed display0.
st7789320x240 color TFTspi=<bus> cs=<pin> dc=<pin>; optional `reset=<pin> bl=<pin> power=<pin> active=01 pwm=01`ESP32-S3 driver; T-Deck Plus registers it as fixed display0 with shared peripheral power and GPIO backlight control.
cvbs-pal384x288 or 320x200 monochrome PAL composite outputi2s=i2s0 out=gpio25Classic ESP32 driver; ESP32-WROVER v3.0 registers it as fixed display0.
vga32Build-selected RGB222 VGA outputr0=<pin> r1=<pin> g0=<pin> g1=<pin> b0=<pin> b1=<pin> hsync=<pin> vsync=<pin>Classic ESP32 driver; claims I2S1 and TTGO VGA32 registers it as fixed display0.
cardkbM5Stack Unit CardKBi2c=<bus> addr=0x5fPolls released keys into the shared input service for shells and foreground apps.
tdeck-keyboardLilyGO T-Deck raw-matrix keyboardi2c=<bus> addr=0x55Polls the raw matrix and publishes keys through the shared input service. The built-in T-Deck attachment uses its board keymap; other boards can attach the same controller to a named I2C bus.
tca8418TCA8418 4x10 matrix keyboardi2c=<bus> addr=0x34; optional irq=<pin> backlight=<pwm-pin>Polls the key-event FIFO and publishes keyboard input; the optional PWM binding controls keyboard backlight brightness.
cl32-coreIntegrated CL-32 ATmega808 controlleri2c=<bus> addr=0x08Fixed CL-32-only core0; polls keyboard press/release events into keyboard0 and provides battery0 from the AVR voltage, USB-power, and charging state. It is not runtime-probeable or detachable.
gpio-keysActive-low pull-up buttonsOne or more key:<name>=<gpio> bindingsPublishes press/release keyboard events and releases all GPIO claims on detach.
ps2-keyboardPS/2 scan-code set 2 keyboardps2=<bus>Publishes canonical keyboard press/release events from an exclusive PS/2 bus.
ps2-mouseStandard three-button PS/2 mouseps2=<bus>Enables reporting and publishes relative pointer motion and button events.
analog-joystickTwo-axis analog joystickx=<scalar-stream> y=<scalar-stream> min=<value> center=<value> max=<value>; optional deadzone=<value>Normalizes two scalar streams into X/Y axis events without generating keys.
rotary-encoderQuadrature rotary encodera=<pin> b=<pin>Publishes Up or Down input for each decoded detent.
ft6336Board-integrated FT6336 touch controllerBoard-defined I2C, address, reset, and IRQ bindingsPublishes absolute pointer events as a default board attachment.
gt911GT911 capacitive touch controlleri2c=<bus> addr=0x5d irq=<pin> rotation=<0..3>; optional alt_addr=0x14Polls absolute pointer events into the shared input service. When both addresses are declared, it claims both and probes them in order; otherwise it uses only addr.
mgc3130MGC3130/Skywriter 3D gesture sensor`i2c=<bus> addr=0x420x43 transfer=<pin> reset=<pin> rotation=<0..3>; optional airwheel=01`Registers in the gesture input class and uses the open-drain transfer handshake to publish normalized X/Y/Z axes, recognized gesture/tap/AirWheel events, and buttonless display-scaled absolute hover motion. It is not registered as a touch source, because the five electrode regions do not provide a continuous drawing surface. AirWheel defaults on; set it to 0 to make the mutually exclusive circle gestures available. Use gesture bind to map its generic gesture events to local keys, scripts, or other shell commands.
pcf85063PCF85063 real-time clocki2c=<bus> addr=0x51; optional irq=<pin>Registers an RTC provider with wall-clock, alarm, timer, and optional interrupt support.
shtc3SHTC3 temperature and humidity sensori2c=<bus> addr=0x70Registers temperature and humidity providers.
battery-adcADC battery monitoradc=<pin> divider=<1000..10000>Registers a battery provider using the configured divider ratio in thousandths.
bq27220BQ27220 fuel gaugei2c=<bus> addr=0x55Registers a battery provider backed by the fuel gauge.
max17048MAX17048 fuel gaugei2c=<bus> addr=0x36Registers a battery provider with gauge-reported voltage and state of charge. The chip does not report charger or external-power state, so charging remains unknown unless another provider supplies it.
bq25896BQ25896 battery chargeri2c=<bus> addr=0x6b; optional charge_current=<0..3008> and charge_voltage=<3840..4608>Registers a charger provider; optional values configure milliamps and millivolts within the device's supported steps.
xl9555XL9555 16-line GPIO controlleri2c=<bus> addr=<0x20..0x27>; optional output=<0..65535> direction=<0..65535>Registers named digital control lines such as gpiox0:4; initial output latches are applied before output directions.
ublox-mia-m10qu-blox MIA-M10Q GNSS receiveruart=<bus>; optional power=<gpio-or-controller-line>Registers a GNSS provider and controls its optional power line around the UART-backed receiver.
sim7670SIM7670 LTE modem and GNSS receiveruart=<bus>; optional active-high power=<gpio-or-controller-line> and active-low reset=<gpio-or-controller-line>Registers modem profile, power/reset, AT/status, GNSS, and a generic IPv4 PPP service bound to the exclusively claimed UART. Without a reset line, the driver can reset through the power line. PPP-enabled targets reserve the UART for network frames until disconnected.
st25r3916ST25R3916 NFC-A readerspi=<bus> cs=<pin> irq=<pin>; optional power=<gpio-or-controller-line>Registers an NFC provider for ISO14443A discovery and card reads.
bhi260apBosch BHI260AP six-axis IMUi2c=<bus> addr=<address> irq=<pin>Uploads the sensor firmware during attachment and registers acceleration, angular-rate, and orientation measurements.
drv2605TI DRV2605 haptic controlleri2c=<bus> addr=0x5a; optional power=<gpio-or-controller-line>Registers a haptic provider for the controller's ROM effects.
sdmmcNative SD/MMC card slotclk=<pin> cmd=<pin> d0=<pin>; optional four-bit set d1=<pin> d2=<pin> d3=<pin>Built-in slots register as fixed storage0; runtime attachments mount removable FAT storage at /sdcard. Classic ESP32 uses its native slot-1 pins.
sdspiSPI microSD card adapterspi=<bus> cs=<pin>On boards without built-in SD, mounts removable FAT storage at /sdcard; run disk umount before detach.
neopixelWS2812/NeoPixel GRB stripdata=<pin> count=<1..256>Claims the data GPIO and registers a named strip for the neopixel command and script API.
audio-pwmLEDC PWM mono audio outputpwm=<pin>Claims the PWM GPIO and registers a 16 kHz mono playback device. One instance can be attached.
pcm1808PCM1808 four-wire I2S ADCmclk=<pin> bck=<pin> ws=<pin> dout=<pin>Requires expansion_i2s, claims four GPIOs and a runtime I2S controller, then registers a 16 kHz stereo capture device and stream. One instance can be attached.
es7210ES7210 I2S microphone arrayi2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin>Requires I2C and expansion_i2s, claims its bindings, then registers a 16 kHz stereo capture device and stream with microphone-gain control. One instance can be attached.
pcm5102PCM5102A three-wire I2S DACi2s=<port> bck=<pin> din=<pin> rck=<pin>Requires expansion_i2s, claims the selected runtime I2S controller and three GPIOs, then registers a 16 kHz stereo playback device and stream. One instance can be attached.
i2s-outputGeneric I2S DAC or speaker amplifieri2s=<port> bck=<pin> din=<pin> rck=<pin>Claims the selected runtime I2S controller and registers the same 16 kHz stereo playback device and stream for an integrated or external Philips-I2S receiver without assuming a specific DAC.
es8311-es7210ES8311 playback with ES7210 capturei2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin> dout=<pin> pa=<pin>ESP32-S3 primary audio backend with stereo capture and playback. Waveshare registers it as fixed audio0.
es8311-duplexES8311 duplex codeci2c=<bus> i2s=<port> mclk=<pin> bck=<pin> ws=<pin> din=<pin> dout=<pin> pa=<pin>ESP32-S3 primary audio backend with mono codec capture and playback. Freenove registers it as fixed audio0.
esp32-dacClassic ESP32 internal DAC`pos=gpio25gpio26; optional neg=gpio25gpio26 amp=<pin> active=01`Registers a primary playback backend. ODROID-GO and TTGO VGA32 provide fixed audio0 attachments.

Manual profiles are useful when another app or workflow operates the hardware but SolarOS still needs to prevent conflicting claims:

expansion attach manual radio0 spi0 cs=gpio10 irq=gpio4 reset=gpio5
expansion attach manual sensor0 i2c0 addr=0x40
expansion detach radio0

Binding names may be explicit (spi=spi0, i2c=i2c0) or, where unambiguous, supplied as positional bus names. ce= aliases cs= and rst= aliases reset= for common module labels.

The name display0 is reserved for the board's primary display attachment. Use another name such as lcd0 for a runtime-attached display. The seven integrated-display drivers attach before display-service initialization when a board profile declares them as fixed defaults. When a controller driver is not already attached, a later attachment registers an auxiliary target for display test, graphical sessions, and applications. Each controller driver supports one attached instance. A fixed board default cannot be detached.

Select the RFM69 driver from the module variant, not from the requested power. The rfm69h driver uses PA1 through 13 dBm, PA1+PA2 through 17 dBm, and applies the datasheet high-power OCP/TestPA settings only during 18-20 dBm transmit. Those settings are restored before standby, receive, or sleep. Both module families require 3.3 V power and a band-appropriate antenna.

WS2812/NeoPixel strip

Use a runtime-safe expansion GPIO for DIN. The driver uses an ESP32 RMT transmit channel and supports up to 256 GRB pixels per attached strip:

5V supply + -> strip 5V       supply GND -> strip GND and SolarOS board GND
GPIO1 -> level shifter -> strip DIN

expansion attach neopixel pixels0 data=gpio1 count=8
neopixel set pixels0 0 32 0 0
neopixel fill pixels0 0 0 16
neopixel clear pixels0
expansion detach pixels0

Use an external supply sized for the strip; full-white WS2812 pixels can draw roughly 60 mA each. Do not power a multi-pixel strip from a board GPIO. A 3.3 V data signal may work with short wiring when the strip supply is low enough, but a 3.3-to-5 V logic-level shifter is the reliable arrangement. Put the usual bulk capacitor across the strip supply and a small series resistor near DIN.

LEDC PWM audio output

Use a runtime-safe PWM pin. The driver updates an 8-bit, 78.125 kHz LEDC carrier from a GPTimer-paced 16 kHz mono PCM stream. It registers the attached name as an audio device and <name>.playback as its stream:

expansion attach audio-pwm pwm0 pwm=gpio1
audio device pwm0
audio default pwm0
aplay /audio/example.mp3

audio default is runtime-only because attached expansion devices are also runtime-only. Run audio default auto to return to the first compatible output. Detaching the selected device also returns selection to auto.

Do not connect a speaker directly to the GPIO. The pin provides a 3.3 V PWM signal centered near 50 percent duty during silence. Use a reconstruction low-pass filter, a DC-blocking/coupling stage, and an amplifier suitable for the speaker impedance. Keep the board and amplifier grounds common. Stop playback before detaching; detach reports busy while the playback stream is open.

PCM5102A I2S audio output

Wire the module's BCK, DIN, and RCK pins to three runtime-safe output GPIOs and connect SCK to ground. The driver is an I2S master in Philips format with 32-bit slots, so its 16-bit stereo stream supplies the 64 BCK cycles per frame needed by the PCM5102A PLL at 16 kHz. The attached name becomes an audio device and <name>.playback becomes an exclusive 16 kHz, signed 16-bit stereo PCM sink:

PCM5102A VCC -> module-rated supply   PCM5102A GND -> SolarOS GND
PCM5102A SCK -> GND                   PCM5102A BCK -> GPIO1
PCM5102A DIN -> GPIO2                 PCM5102A RCK -> GPIO3

expansion attach pcm5102 dac0 i2s=i2s1 bck=gpio1 din=gpio2 rck=gpio3
audio device dac0
audio default dac0
aplay /audio/example.mp3

The example pins are the Waveshare board's runtime-safe expansion GPIOs. Mono streams are duplicated to left and right. Volume is applied in software before samples reach I2S. Select a runtime-safe controller explicitly with the i2s= binding; this leaves other I2S controllers available to onboard audio or composite video. The PCM5102A output is line level: use a powered input or a suitable amplifier, not a passive speaker. Stop playback before detaching; detach reports busy while the playback stream is open. Run audio default auto after testing to restore automatic output selection.

PCM1808 I2S audio input

The driver makes the ESP32 the I2S clock master. Configure the PCM1808 mode pins before power-up: MD1=LOW, MD0=LOW selects slave mode and FMT=LOW selects 24-bit Philips I2S. Wire the driver MCLK signal to the module's SCKI or SCK pin. The PCM1808 mode pins are hardware straps, not runtime driver GPIOs:

PCM1808 +5V -> SolarOS 5V               PCM1808 3.3V -> SolarOS 3.3V
PCM1808 GND -> SolarOS GND
PCM1808 MD1 -> GND                      PCM1808 MD0 -> GND
PCM1808 FMT -> GND                      PCM1808 SCKI/SCK -> GPIO1
PCM1808 BCK -> GPIO2                    PCM1808 LRCK -> GPIO3
PCM1808 DOUT -> GPIO17

expansion attach pcm1808 adc0 mclk=gpio1 bck=gpio2 ws=gpio3 dout=gpio17
audio device adc0
arecord -d 5 -i adc0.capture /sdcard/pcm1808.wav

The example uses all four runtime-safe Waveshare expansion GPIOs. The driver generates 4.096 MHz MCLK and 64 BCK cycles per 16 kHz stereo frame. It receives the converter's 24-bit I2S slots, keeps the most-significant 16 bits, and publishes adc0.capture as an exclusive signed 16-bit stereo PCM source. It uses the board's runtime I2S controller (I2S1 on current ESP32-S3 profiles), so it can coexist with fixed onboard audio on I2S0 but not with another attachment that owns I2S1. Stop recording before detach. A raw PCM1808 circuit requires the analog supply, digital supply, reference components, and input network from the manufacturer reference design; follow the breakout's own supply markings when using a module. Common modules expose separate +5V analog and 3.3V digital supply pins; both rails must be powered.

SolarOS documentation

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.

SolarOS documentation

Storage and shell paths

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

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

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

Use disk for both internal and removable persistent storage:

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

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

Formatting

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

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

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

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

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

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

Inspect before writing

import solaros

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

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

Volumes and directories

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

Quick reference

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

SolarOS documentation

Attach deliberately

Use real bus and pin names returned by discovery:

expansion attach pcd8544 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5
display test lcd0
expansion detach lcd0

Detaching releases the resources. Do not invent a target name or copy bindings from a different board.

To restore a runtime attachment after reboot, open its device detail in expansion and press S. SolarOS reconstructs the normalized expansion attach command and adds it once to the startup script selected by setterm startup. If the device uses a runtime bus, save that bus for startup from io first so its expansion bus create command appears earlier in the script. Compiled board devices already start automatically and do not need a saved command.

When an experimental wiring arrangement becomes permanent, export its hardware description instead of copying startup commands:

expansion export /sdcard/my-hardware.toml

The versioned solaros-expansion manifest contains the current board ID plus runtime-created buses and runtime-attached catalog devices. References to board-owned buses remain references, so the base board hardware is not duplicated. Readiness, leases, service state, secrets, and shell startup commands are not part of the file. A manual attachment cannot be exported because it has no catalog driver contract that the board compiler can validate. The destination is written through a temporary file and atomically replaced.

The export format is a strict TOML contract. Schema 1 has this shape:

schema = 1
kind = "solaros-expansion"

[base]
board = "waveshare_esp32_s3_sim7670g_4g"
firmware = "4.13.1"

[[buses]]
name = "spi0"
protocol = "spi"
sharing = "shared"
host = "SPI2_HOST"
sclk = 7
mosi = 8
miso = 3
cs = [9]
max_transfer_size = 4096

[[devices]]
driver = "ssd1683"
name = "display0"
bindings = { spi = "spi0", cs = 9, dc = 10, reset = 11, busy = 12 }

base.board selects the manifest that the generated target inherits; base.firmware records export provenance and is not a compatibility gate. Only runtime-created I2C, SPI, UART, MIDI, 1-Wire, and PS/2 buses appear in buses. Each bus carries its concrete controller and protocol-specific wiring. Only runtime catalog devices appear in devices; each device carries the normalized string or integer bindings accepted by its driver. Board-owned buses may still be referenced by an exported device without being duplicated. The importer rejects unsupported schema versions, unknown fields, duplicate names, missing base boards, unavailable controllers or pins, unknown drivers, invalid bindings, and conflicts with existing board resources. Treat the file as generated interchange data; make lasting hardware changes in the resulting board manifest.

Move the file to a SolarOS source tree and run:

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

The configurator loads the recorded base manifest, asks for the new board identity, validates the combined pin and bus ownership, and writes a small inherited board profile. Imported hardware then starts as board-owned after the custom target is built and flashed. Remove any matching temporary startup commands yourself; they are intentionally outside the export contract.

A Waveshare 4.2-inch V2 monochrome e-paper module uses the SSD1683 expansion driver and registers a 400x300 display target:

expansion attach ssd1683 epd0 spi=spi0 cs=gpio10 dc=gpio17 reset=gpio16 busy=gpio15
display test epd0
display mode epd0 refresh=fast
expansion detach epd0

The runtime defaults are the Waveshare V2 panel profile, 2 MHz SPI, and rotation 0. Optional power=<gpio>, clock=<khz>, rotation=<0..3>, and panel=<0..3> bindings adapt the same driver to integrated panels. Panel 0 selects Elecrow BUSY-based revision detection and defaults to rotation 2; panels 1, 2, and 3 select the legacy Elecrow, green-sticker Elecrow, and Waveshare V2 profiles.

Use the module's eight-wire SPI connector and power it from the same 3.3 V logic domain as the ESP32. The module keeps its last image after detach. If SolarOS creates a display shell on epd0, run sessions, close that session with session close <id>, and then detach the expansion.

An M5Stack Unit CardKB attaches at its fixed I2C address and becomes a shared keyboard source for the shell and foreground apps:

expansion attach cardkb cardkb0 i2c=i2c0 addr=0x5f
input test cardkb0
expansion detach cardkb0

The CardKB firmware produces characters after key release. SolarOS maps its four navigation values to the same logical arrow keys used by PS/2 and BLE keyboards. The module's Fn combinations are device-specific and are ignored.

RTC and environmental sensor modules use the same named-I2C lifecycle. Environmental adapters register named providers with the generic sensor service. A provider can supply temperature, humidity, or both, and multiple providers can be active at the same time:

expansion attach pcf85063 rtc0 i2c=i2c0 addr=0x51
expansion attach shtc3 environment0 i2c=i2c0 addr=0x70
date
temperature
humidity
temperature list
humidity list
expansion detach environment0
expansion detach rtc0

The PCF85063 irq=<gpio> binding is optional. The Waveshare board profile reserves its routed RTC interrupt on GPIO15; external modules can omit irq when only clock and calendar access is required. The generic RTC service discovers alarm, countdown, and interrupt-status support from the attached chip adapter, so applications do not depend on the PCF85063 register interface. The generic sensor service similarly exposes provider names, driver names, and temperature/humidity capabilities without exposing a chip register interface.

battery-adc takes an ADC pin and a divider ratio in thousandths. For a 2:1 resistive divider, use divider=2000:

expansion attach battery-adc battery0 adc=gpio4 divider=2000
battery
expansion detach battery0

Use a high-impedance divider suitable for the expected battery voltage. The divided voltage must remain inside the ESP32 ADC input range.

Other input devices follow the same lifecycle:

expansion attach gpio-keys keys0 key:UP=gpio17 key:ENTER=gpio2
expansion attach rotary-encoder wheel0 a=gpio17 b=gpio2
expansion bus create ps2 ps2mouse clock=gpio17 data=gpio18
expansion attach ps2-mouse mouse0 ps2=ps2mouse
expansion attach analog-joystick joystick0 x=adc2 y=adc4 min=0 center=1650 max=3300 deadzone=100
input status

Use only bindings listed by expansion drivers and resources shown on the running board. A rotary encoder decodes interrupts from its quadrature A/B signals and publishes Up/Down detents; wire its independent push switch through gpio-keys. A PS/2 mouse publishes relative pointer events. An analog joystick consumes two scalar streams and publishes axes, never keys. Foreground Python and Lua applications receive those pointer, axis, and gesture events through solaros.input; use solaros.tui.getch() for keyboard characters.

On a board without built-in SD hardware, an SPI microSD adapter can provide removable storage. The SPI bus must include MISO and declare the selected CS pin. Attaching mounts the detected FAT volume at /sdcard. Unmount it before detaching the adapter:

expansion attach sdspi card0 spi=spi0 cs=gpio4
disk lsblk
disk umount
expansion detach card0

If the adapter has a switched supply, add power=<gpio> or power=<controller:line>. The SD driver enables that line before probing and disables it after unmount and detach. The SPI bus and CS remain native ESP peripheral resources.

The sdspi attach command prints the card probe result to the invoking shell. The report includes the card identity, type, negotiated speed, capacity, CSD/SSR details, mount point, and any underlying block-I/O or FatFs mount error.

An SDMMC adapter uses direct clock, command, and data bindings. On ESP32-S3 the signals can use the GPIO matrix. Classic ESP32 accepts only the native slot-1 pinout: CLK GPIO14, CMD GPIO15, D0 GPIO2, and optionally D1 GPIO4, D2 GPIO12, D3 GPIO13.

expansion attach sdmmc card0 clk=gpio14 cmd=gpio15 d0=gpio2
disk lsblk
disk umount
expansion detach card0

An RFM95W wired to the ESP32-S3-DevKitC-1 spi0 bus with NSS on GPIO4 and reset on GPIO5 attaches as a multimode packet radio:

expansion attach rfm95 radio0 spi=spi0 cs=gpio4 reset=gpio5
radio status radio0
radio profile apply radio0 lora-eu868

Connect an antenna suitable for the module band before transmitting. See the expansion reference for the complete wiring and modulation configuration.

Use rfm69 for the 13 dBm RFM69W/CW modules and rfm69h for the 20 dBm RFM69HW/HCW variants. For example:

expansion attach rfm69h radio0 spi=spi0 cs=gpio4 reset=gpio5
radio config radio0 power 20

Both variants are 3.3 V devices. High-power transmission requires a supply that can sustain the module's transmit-current peak and an antenna appropriate for the selected band.

A WS2812/NeoPixel strip uses one runtime-safe GPIO and a declared pixel count:

expansion attach neopixel pixels0 data=gpio1 count=8
neopixel fill pixels0 16 0 0
neopixel set pixels0 3 0 16 0
neopixel clear pixels0
expansion detach pixels0

SolarOS stores colors in RGB form and transmits the strip's standard GRB wire order. Attach clears all declared pixels. set and fill refresh immediately in the shell; scripting APIs buffer changes until show().

An LEDC PWM audio output uses one runtime-safe PWM pin and appears as a normal mono playback device:

expansion attach audio-pwm pwm0 pwm=gpio1
audio devices
audio default pwm0
aplay /audio/example.mp3
audio default auto
expansion detach pwm0

The output is 8-bit PWM with a 78.125 kHz carrier and a 16 kHz native PCM rate. It is a signal output, not a speaker driver. Put a reconstruction low-pass filter, DC-blocking/coupling stage, and suitable amplifier between the GPIO and a speaker. A bare speaker can overload and damage the GPIO. Only one LEDC PWM audio device can be attached at a time.

A PCM5102A module uses three runtime-safe GPIOs and appears as a normal stereo playback device:

expansion attach pcm5102 dac0 i2s=i2s1 bck=gpio1 din=gpio2 rck=gpio3
audio devices
audio default dac0
aplay /audio/example.mp3
audio default auto
expansion detach dac0

The example pins are valid on the Waveshare board. Connect the module's SCK pin to ground; the driver emits 64 BCK cycles per stereo frame so the PCM5102A can derive its system clock with the internal PLL. The driver registers dac0.playback as an exclusive 16 kHz, 16-bit PCM sink. Mono input is duplicated to both channels and device volume is applied in software. Select a runtime-safe controller explicitly with the i2s= binding, leaving other I2S controllers available to onboard audio or composite video. PCM5102A modules provide line-level output; connect an amplifier or powered input rather than a passive speaker.

A PCM1808 module uses four runtime-safe GPIOs and appears as a stereo capture device:

expansion attach pcm1808 adc0 mclk=gpio1 bck=gpio2 ws=gpio3 dout=gpio17
audio devices
arecord -d 5 -i adc0.capture /sdcard/pcm1808.wav
expansion detach adc0

The example consumes every runtime-safe GPIO on the Waveshare expansion header. Before applying power, configure the module for slave I2S mode: MD1=0, MD0=0, and FMT=0. Connect MCLK to the module's SCKI or SCK pin, WS to LRCK, and DOUT to DOUT. The driver supplies a 4.096 MHz system clock and 64 BCK cycles per 16 kHz stereo frame, receives the PCM1808's 24-bit samples, and registers adc0.capture as an exclusive signed 16-bit PCM source. Use the module-rated supply and a common ground; raw PCM1808 circuits need separate analog and digital supplies as specified by the manufacturer. On modules that expose both rails, connect +5V for the analog supply and 3.3V for the digital supply; powering only 3.3V leaves the converter's analog section unpowered.

The integrated codec and classic ESP32 DAC backends are also attachable when the target MCU and board resources support them. Codec attachments need a named I2C bus, an I2S controller, and all six audio GPIO signals:

expansion attach es8311-es7210 audio0 i2c=i2c0 i2s=i2s0 mclk=gpio38 bck=gpio14 ws=gpio13 din=gpio12 dout=gpio45 pa=gpio46
expansion attach es8311-duplex audio0 i2c=i2c0 i2s=i2s0 mclk=gpio42 bck=gpio10 ws=gpio11 din=gpio12 dout=gpio9 pa=gpio46

es8311-es7210 and es8311-duplex are ESP32-S3 drivers. esp32-dac is for the classic ESP32 internal DAC on GPIO25 or GPIO26. Its optional neg binding enables differential output, and amp plus active=0|1 controls an amplifier enable pin:

expansion attach esp32-dac audio0 pos=gpio26 amp=gpio25 active=1

Only one of these primary audio backends can be attached at a time. A fixed board-default audio0 cannot be detached.

The integrated display controller drivers can also create auxiliary display targets. Use a name other than the reserved primary name display0 for a runtime attachment. Each controller driver supports one attached instance, so its board-integrated display0 and an auxiliary instance cannot coexist:

expansion attach st7305 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5
expansion attach ili9341 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 bl=gpio6 pwm=1 active=1
expansion attach st7796 lcd0 spi=spi0 cs=gpio10 dc=gpio4 reset=gpio5 bl=gpio6 pwm=1 active=1
display test lcd0
expansion detach lcd0

st7305, st7796, and ili9341 are available on ESP32 and ESP32-S3 firmware. The I2S-based cvbs-pal and vga32 drivers are available on classic ESP32. Composite PAL uses I2S0 and GPIO25. VGA32 uses I2S1 plus its six RGB and two sync GPIOs; see the expansion reference for its full binding list. A fixed board-default display0 cannot be detached.

SolarOS documentation

Built-In Targets

The current tree includes these board targets:

TargetPlatformIO envHardwareHighlights
solar_termsolar_termSolarTerm, built from the Waveshare ESP32-S3-RLCD-4.2Primary ST7305 reflective display target with SDMMC, CDC, UART, RTC, SHTC3, battery ADC, ES8311/ES7210 audio, expansion I2C/SPI/UART/GPIO/ADC/PWM, and runtime-routable SPI3 on GPIO1/GPIO2/GPIO3/GPIO17. SolarOS refers to this hardware configuration as SolarTerm.
freenove_esp32_s3_display_4_0freenove_esp32_s3_display_4_0Freenove ESP32-S3 Display 4.0-inch (FNK0104S)Integrated 480x320 ST7796 display, FT6336 capacitive pointer, ES8311 speaker and microphone, four-bit SDMMC, battery ADC, native USB CDC, and UART/I2C/GPIO expansion connectors.
elecrow_crowpanel_esp32_s3_4_2_epaperelecrow_crowpanel_esp32_s3_4_2_epaperElecrow CrowPanel ESP32-S3 4.2-inch E-paperESP32-S3-WROOM-1-N8R8 target with a 400x300 SSD1683 e-paper display, microSD over SDSPI, CH340C/UART console, rotary/menu/exit controls, status LED, Wi-Fi, BLE, and expansion I2C/SPI/UART/1-Wire/GPIO/ADC/PWM.
cl_32cl_32CL-32ESP32-S3-WROOM-1-N16R8 target with a 384x168 ST7305 reflective LCD, an ATmega808-backed keyboard and battery monitor, native USB CDC, UART, microSD over SDSPI, PCF85063 RTC, onboard PWM buzzer, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S.
odroid_goodroid_goHardkernel ODROID-GOClassic ESP32 target with ILI9341 display, SD over VSPI/SDSPI, battery ADC, ESP32 DAC speaker, buttons, ADC D-pad, status LED, display brightness, expansion SPI/UART/GPIO/PWM, and runtime GPIO4/GPIO15.
freenove_esp32_wrover_v3freenove_esp32_wrover_v3Freenove ESP32-WROVER v3.0 (FNK0060)Classic ESP32 target with 8 MB PSRAM, CH340/UART console, one-bit SDMMC, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, and a 384x288 monochrome PAL composite display on GPIO25.
ttgo_vga32_v14ttgo_vga32_v14LilyGO TTGO VGA32 v1.4ESP32-PICO-D4 desktop target with 8 MB external PSRAM, build-selectable 320x200@70Hz, 320x240@60Hz, 640x400@70Hz, or 640x480@60Hz VGA output through the onboard RGB222 resistor DAC, GPIO25 mono DAC audio, a default-attached PS/2 keyboard, v1.4 microSD wiring over HSPI, USB-UART, Wi-Fi, BLE disabled by default, and two input-only expansion GPIOs.
esp32_s3_devkitc1_n16r8esp32_s3_devkitc1_n16r8Espressif ESP32-S3-DevKitC-1-N16R8Headless ESP32-S3 target with CDC, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM, graphics through attachable display targets, and no primary display or onboard sensors.
esp32_devkitc_v4_wroveresp32_devkitc_v4_wroverEspressif ESP32-DevKitC V4 with ESP32-WROVER-EHeadless classic ESP32 target with PSRAM, UART, Wi-Fi, BLE, a GPIO0 BOOT/KEY button, expansion I2C/SPI/UART/GPIO/ADC/PWM/I2S, graphics through attachable display targets, and no built-in peripherals.
devkitc1_epaper_workbenchesp32_s3_devkitc1_n16r8 with SOLAR_OS_BOARD=devkitc1_epaper_workbenchESP32-S3 DevKitC-1 E-paper WorkbenchManifest-generated development target with fixed CardKB, 400x300 SSD1683 display, and SDSPI storage attachments.
t_lora_pagert_lora_pagerLilyGO T-LoRa-Pager (SX1262)ESP32-S3-WROOM-1-N16R8 target with a 480x222 ST7796 display, a TCA8418 4x10 matrix keyboard, an SX1262 LoRa radio, PCF85063 RTC, microSD over SDSPI, ES8311 audio, a BQ27220 battery gauge, a BQ25896 charger, a rotary encoder, an XL9555 I2C GPIO controller for integrated power rails, a MIA-M10Q GNSS receiver, an ST25R3916 NFC-A reader, a BHI260AP IMU, a DRV2605 haptic controller, native USB CDC, Wi-Fi, BLE, and expansion I2C/SPI/UART/GPIO/ADC/PWM. SD power and its external pull-up start enabled; GNSS and NFC power start disabled and are enabled with gnss power on and nfc power on. Use haptic play EFFECT for DRV2605 ROM effects 1 through 117. The Pager fixes BQ25896 fast-charge current at 704 mA during attachment; inspect or explicitly change charger settings with charger.
t_deck_plust_deck_plusLilyGO T-Deck PlusESP32-S3FN16R8 target with a 320x240 ST7789 display, GT911 touch, I2C keyboard, five-way trackball, separate I2S speaker output and ES7210 microphone capture, SX1262 LoRa radio, microSD over SDSPI, battery ADC, internal GPS UART, native USB CDC, Wi-Fi, and BLE.
waveshare_esp32_s3_sim7670g_4gwaveshare_esp32_s3_sim7670g_4gWaveshare ESP32-S3-SIM7670G-4G V2.0Headless ESP32-S3R8 target with 16 MB flash, 8 MB PSRAM, CH343/UART console, SIM7670G AT, GNSS, and IPv4 PPP access over UART1, MAX17048 battery gauge, one-bit SDMMC, one WS2812B, Wi-Fi, BLE, and expansion GPIO/ADC/PWM/SPI/I2S. Set the DIP switches to CAM OFF, HUB ON, 4G OFF, USB OFF. With CAM off, the unused camera signal pins are available as expansion GPIOs and can form runtime SPI or I2S buses; GPIO15/GPIO16 remain the shared MAX17048 I2C bus and GPIO46 remains blocked as an input-only strapping pin. SolarOS drives GPIO21 active-high to control the modem VBAT rail; leave the 4G switch OFF to avoid contention and permit software power-off/reset. The modem UART belongs exclusively to PPP while connected; disconnect it before AT or GNSS use. Camera support is not included.
waveshare_esp32_s3_sim7670g_4g_epaperwaveshare_esp32_s3_sim7670g_4g with SOLAR_OS_BOARD=waveshare_esp32_s3_sim7670g_4g_epaperWaveshare ESP32-S3-SIM7670G-4G E-paper workbenchExpansion-export reference target with a fixed CardKB keyboard on the board-owned I2C bus at address 0x5f and a fixed primary 400x300 SSD1683 display on SPI2 using GPIO3/GPIO7 through GPIO12.

Join us on: