SolarOS

Hardware workflow · Tutorial

Turn an expansion prototype into a custom board

SolarOS lets you move from loose wires to a reproducible firmware target without describing untested hardware by hand. Start with a supported board, create buses and attach drivers at runtime, test the real devices, export the working topology, and promote that snapshot into a custom board manifest.

This tutorial follows the configuration that became the waveshare_esp32_s3_sim7670g_4g_epaper target. It adds a CardKB keyboard and a 400×300 SSD1683 E-paper display to the supported Waveshare ESP32-S3-SIM7670G-4G V2.0 board. The modem, GNSS receiver, battery gauge, SD slot, and status LED continue to come from the original board profile.

What this workflow is for

Use this flow when the base board and expansion drivers already exist in SolarOS, but your final wiring does not. It gives you three useful stages:

  1. A supported base board that is safe to flash and inspect.
  2. A runtime prototype whose pins and drivers can change while you experiment.
  3. A custom target whose settled devices are owned by the board and start automatically.

The export is a hardware contract, not a complete device backup. It does not contain Wi-Fi or modem credentials, network policy, jobs, application state, or shell startup commands.

Before you begin

You need the Waveshare ESP32-S3-SIM7670G-4G V2.0, a compatible CardKB, a Waveshare V2-style 400×300 SSD1683 module, a FAT32 SD card, and a checkout of SolarOS. The firmware must include the cardkb and ssd1683 expansion drivers.

Power the board off before changing wires. For this example set its DIP switches to:

CAM OFF
HUB ON
4G  OFF
USB OFF

CAM OFF makes the unused camera pins available to the expansion system. 4G OFF leaves modem-rail control to SolarOS, while USB OFF keeps the USB path away from the modem.

Build and flash the supported base target first:

cd solar_os
pio run -e waveshare_esp32_s3_sim7670g_4g -t upload --upload-port <PORT>

Use pio device list to find <PORT>. On Linux it is commonly a /dev/ttyACM* device exposed by the onboard CH343 bridge.

Wire the prototype

The CardKB reuses the base board's shared i2c0 bus. The display gets a new runtime SPI bus on SPI2.

Device signalBoard signalHeader position
CardKB SDAGPIO15 / i2c0 SDAP2 pin 8
CardKB SCLGPIO16 / i2c0 SCLP2 pin 9
E-paper SCLKGPIO7P2 pin 10
E-paper MOSI / DINGPIO8P2 pin 11
SPI MISOGPIO3P3 pin 12
E-paper CSGPIO9P2 pin 12
E-paper DCGPIO10P2 pin 13
E-paper RESETGPIO11P2 pin 14
E-paper BUSYGPIO12P2 pin 15

Connect both modules to the appropriate 3.3 V and ground pins. Check the labels on your particular modules before applying power; do not infer supply voltage from signal-level compatibility alone.

Create and test the runtime configuration

Open io on the device. Its bus editor shows the usable SPI controllers and pins from the live board map. Create a shared SPI bus named spi0 with SPI2, SCLK GPIO7, MOSI GPIO8, MISO GPIO3, CS GPIO9, and a 4096-byte maximum transfer.

Then open expansion. Attach cardkb as keyboard0 on i2c0 at address 0x5f, and attach ssd1683 as display0 on spi0 with CS GPIO9, DC GPIO10, RESET GPIO11, and BUSY GPIO12. The selectors exclude resources that are already claimed and show the connector positions beside eligible pins.

The equivalent shell commands are useful as a reproducible transcript:

expansion bus create spi spi0 host=spi2 sclk=gpio7 mosi=gpio8 miso=gpio3 cs=gpio9 max=4096
expansion attach cardkb keyboard0 i2c=i2c0 addr=0x5f
expansion attach ssd1683 display0 spi=spi0 cs=gpio9 dc=gpio10 reset=gpio11 busy=gpio12

Inspect the result before going further:

expansion devices
display list
display test display0

Type on the CardKB and confirm that the shell receives its keys. Confirm that display0 appears in display list and completes a test refresh. A successful attach only proves that the driver acquired its resources; this physical test is what validates the wiring.

If you want the prototype to survive reboots while you continue testing, save the SPI bus from its io detail view first, then save each attachment from its expansion detail view. SolarOS adds the normalized commands to the selected startup script. Persistence is optional for export: the exporter reads the currently attached runtime topology, not the startup file.

Export the settled hardware

Insert and mount the SD card, then check expansion devices one last time. It must show both keyboard0 and display0 as runtime devices. Export exactly that live state:

expansion export /sdcard/4g-epaper.toml

For this example the command exports one runtime bus and two runtime devices. The CardKB is included even though it references the board-owned i2c0 bus. The base board's modem, battery, storage, and status LED are not duplicated; the future target inherits them from the base manifest.

The generated file begins like this:

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 = "cardkb"
name = "keyboard0"
bindings = { i2c = "i2c0", addr = 0x5f }

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

Move 4g-epaper.toml from the SD card to the computer containing your SolarOS checkout. Treat it as generated interchange data: it records what worked on the device, while the board manifest created in the next step becomes the maintained source.

Promote the snapshot

Run the desktop board configurator from the SolarOS source directory:

python3 scripts/board_config.py --expansion-manifest /path/to/4g-epaper.toml

The snapshot already identifies the base target, so the configurator asks only for the new board identity. The worked example used:

Board ID: waveshare_esp32_s3_sim7670g_4g_epaper
Board name: Waveshare ESP32-S3-SIM7670G-4G E-paper
Vendor: Custom
Module: ESP32-S3R8 with 16 MB external flash

That reference target is now included in SolarOS. To repeat the tutorial in a current checkout without overwriting it, choose a unique ID such as my_4g_epaper; the remaining commands below use that name.

The configurator validates the imported buses, driver bindings, controller ownership, and pin conflicts before writing boards/manifests/my_4g_epaper.toml. It also promotes the SSD1683 to the board's primary display because the headless base has no existing display. GPIO3 and GPIO7 through GPIO12 become fixed board resources, SPI2 leaves the runtime controller pool, and the two devices become automatic board-owned attachments.

You can inspect the checked-in 0 to see the compact inherited result.

Validate, build, and flash

Validate the new manifest directly before compiling:

python3 scripts/generate_board_profile.py \
  --manifest boards/manifests/my_4g_epaper.toml \
  --manifest-dir boards/manifests \
  --drivers boards/expansion_drivers.toml \
  --validate-only

A custom manifest does not create another PlatformIO environment. Build with the base board's environment and select the new SolarOS profile on every invocation:

SOLAR_OS_BOARD=my_4g_epaper \
  pio run -e waveshare_esp32_s3_sim7670g_4g

Then flash the same profile:

SOLAR_OS_BOARD=my_4g_epaper \
  pio run -e waveshare_esp32_s3_sim7670g_4g \
  -t upload --upload-port <PORT>

The browser Flasher lists published targets, so a local custom profile must be built and flashed from its source checkout unless you add it to your own release pipeline.

Remove any matching temporary bus and attachment commands from the shell startup script. The generated board now owns those resources and attaches the devices during boot.

Verify the custom target

After the new firmware starts, run:

expansion devices
display list
status

keyboard0 and display0 should now report board origin rather than runtime origin, and they should not be detachable. display0 should be the primary 400×300 display. The inherited storage0, battery0, pixels0, and modem0 devices should still be present.

Test the keyboard, refresh the display, mount the SD card, and check the modem on the physical target. Manifest validation and a successful build establish configuration consistency; only this final hardware test establishes that the custom board works.

What the export deliberately leaves out

The result is intentionally small: an inherited board manifest contains only the hardware added to the supported base and the resources that addition makes permanent.

Join us on: