Playground for building and testing different setups for the Waveshare ESP32-S3-RLCD-4.2 development board.
The ESP32-S3-RLCD-4.2 is a development board featuring a 4.2" reflective LCD that requires no backlight.
| Component | Specification |
|---|---|
| Display | 4.2" RLCD, 400×300 pixels, 1-bit monochrome, SPI (ST7305 driver) |
| SoC | ESP32-S3-WROOM-1-N16R8 (dual-core Xtensa LX7 @ 240MHz) |
| Memory | 16MB Flash, 8MB PSRAM, 512KB SRAM |
| Connectivity | WiFi 2.4GHz, Bluetooth 5 LE |
| Audio | ES8311 codec, ES7210 ADC with dual-mic array |
| Sensors | SHTC3 (temperature/humidity) |
| RTC | PCF85063 |
| Power | 18650 battery holder, USB-C |
| Storage | TF card slot (FAT32) |
ESP-IDF firmware that turns the board into a self-contained SSH terminal and server dashboard. Connect a Bluetooth keyboard, point it at a Linux server, and get a full interactive shell on the 400x300 reflective display.
- Terminal mode -- full VT100/xterm emulator rendering htop, vim, and other TUI apps on the 1-bit display
- Dashboard mode -- curated server stats (CPU, memory, disk, network, GPU, Docker, screen sessions) refreshed on a configurable interval
- SSH client -- libssh 0.11.4 (vendored in-tree as
components/libssh/) with hardware-accelerated AES-128-CTR, Ed25519 / ECDSA P-256 / RSA-SHA2 auth, TOFU host key verification - BLE keyboard -- NimBLE HID host with auto-reconnect to bonded devices, full keycode-to-terminal translation (arrows, function keys, Ctrl combos)
- WiFi -- auto-connect to known networks by signal strength; new networks are added via SD card JSON config (on-screen password entry is not implemented).
- 3 font sizes -- 80x37 (5x7), 66x30 (6x9), 50x23 (8x12) -- cycle with button or F-key
- Menu system -- overlay navigable via keyboard or physical buttons
- Power management -- WiFi modem-sleep during active SSH (~20mA). Light sleep is disabled because ST7305 needs continuous SPI power.
- Recovery firmware -- 512KB factory-partition recovery image with auto-fallback: if main fails to
mark_app_validwithin 30s, bootloader boots recovery which keeps USB-JTAG CDC stable and displays a "RECOVERY MODE" banner on the LCD. Recovery exposes a minimal REPL (ping/info/reboot-ota/erase-nvs/coredump-dump)
The firmware is tuned for the ESP32-S3's specific hardware profile:
| Decision | Why |
|---|---|
| AES-128-CTR over AES-GCM | Hardware AES-CTR primitive: 7.5 MB/s (ESP32-S3 crypto peripheral). AES-GCM GHASH in software: 1.35 MB/s (5.5x slower). Note: end-to-end SSH throughput measures ~1 MB/s, bounded by the application send path in ssh_client (16-byte queue items, 5 ms poll), not the crypto primitive. |
| Ed25519 over RSA-2048 | Ed25519 end-to-end auth ~813 ms (post-swap measured: TCP connect + KEX + pubkey). libssh vendors the Ed25519 reference impl so mbedTLS doesn't need to support it — that's why libssh2+mbedTLS cannot deliver Ed25519 at all and the swap to libssh happened. |
| Framebuffer in DMA SRAM | SPI DMA requires internal SRAM. PSRAM would need cache coherence workaround |
| Scrollback in PSRAM | Large, sequential access pattern benefits from 64-byte cache line prefetch |
| 64KB data cache | Trades 32KB SRAM for substantially better PSRAM throughput |
| NimBLE over Bluedroid | Saves ~170KB flash and ~30KB RAM. ESP32-S3 is BLE-only anyway |
| SSH on Core 1 | Isolates crypto from WiFi/BLE protocol stacks on Core 0 |
Core 0 (Protocol) Core 1 (Application)
WiFi Driver (P:23) Display/SPI DMA (P:12)
BLE Controller (P:23) SSH/libssh (P:10)
lwIP TCP/IP (P:18) ANSI Parser (P:8)
BLE HID Input (P:9) Dashboard Poll (P:5)
| Offset | Size | Name | Purpose |
|---|---|---|---|
| 0x9000 | 24 KB | nvs |
WiFi/SSH credentials (plaintext; encryption deferred) |
| 0x10000 | 8 KB | otadata |
rollback state (main → factory fallback) |
| 0x20000 | 512 KB | factory |
recovery firmware — USB-JTAG REPL + "RECOVERY MODE" banner |
| 0xA0000 | 10 MB | ota_0 |
main application (single OTA slot) |
| 0xAA0000 | 4 MB | littlefs |
SSH keys, known_hosts, dashboard config |
| 0xEA0000 | 1 MB | coredump |
ELF coredump after panic |
One-time migration from the old dual-OTA layout: see
MIGRATION-factory-ota.md. Subsequent dev
iterations use idf.py -C esp32_rendering flash to rewrite only the
main slot.
Requires ESP-IDF v5.5+ and the 1bit-display library:
cd esp32_rendering
# Set the path to the 1bit-display library (default: ../../1bit-display relative to esp32_rendering/)
export ONEBIT_LIB_DIR=/path/to/1bit-display
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor- Flash firmware and power on
- Pair a BLE keyboard -- long-press Button A to enter pairing mode (30s timeout)
- Connect WiFi -- the device auto-connects to networks saved in NVS or in
/sdcard/wifi.json. Adding a new network from the UI is not implemented. - Configure SSH -- drop a JSON file into
/sdcard/servers/with{name, host, port, username, dashboard[]}, then pick it from the Servers menu. (In-app Settings editor is not yet implemented.) - Authenticate -- password on first connect; for key auth, set
key_pathin the server JSON pointing to a LittleFS-resident Ed25519 PEM private key
Create /littlefs/dashboard.cfg with one command per line (label|command format):
CPU|cat /proc/loadavg
Memory|free -m
Disk|df -h
GPU|cat /sys/class/drm/card0/device/gpu_busy_percent
Screens|screen -ls
Falls back to built-in defaults if no config file exists.
A Python/Pygame simulator for prototyping 1-bit display designs before deploying to hardware. Includes a rendering toolkit inspired by Lucas Pope's Mars After Midnight visual techniques.
- Portable rendering core - Pure Python modules that map cleanly to C++ for ESP32 porting
- Optimized 1-bit framebuffer - Byte-aligned operations for efficient rendering
- Drawing primitives - Bresenham lines, scanline polygon fill, midpoint circles
- Bayer dithering - 5 pattern levels for visual texture (0%, 25%, 50%, 75%, 100%)
- Bezier curves - Cubic beziers with auto-smooth tangents and texture-ball strokes
- Vector typography - Full A-Z alphabet, numerals 0-9, punctuation with scalable stroke width
- Animation system - Breathing, wiggle, and transition effects for organic movement
- Layout helpers - Centered, right-aligned, and multi-line text rendering
cd simulator
python -m venv venv
source venv/bin/activate # or `venv\Scripts\activate` on Windows
pip install -r requirements.txt
python main.py --scale 2Controls:
SPACE- Cycle through demo modes1-5- Jump to specific modeA- Toggle animation effectsS- Save screenshotQ/ESC- Quit
Demo Modes:
- Patterns - All 5 dither patterns in hexagonal shapes (breathing animation)
- Bezier - Organic curves with texture-ball strokes (wiggle animation)
- Numerals - Full digit set at multiple sizes with live clock
- Clock Sketch - Combined composition preview (wiggle + breathing)
- Typography - Full A-Z alphabet, sample phrases, and mixed text
ESP-IDF firmware implementing a dual-channel VU meter using the onboard microphone array.
- Real-time audio level visualization
- Dual VU meters (left/right channels) with 16 segments each
- Automatic gain control with adaptive noise floor
- 20 FPS display refresh
cd hello_vu
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitorRLCD/
├── esp32_rendering/ # SSH Terminal & Dashboard (main project)
│ ├── main/ # Application entry point and boot sequence
│ ├── components/
│ │ ├── st7305/ # ST7305 reflective LCD driver (SPI + DMA)
│ │ ├── rendering/ # Graphics primitives (clock face, shapes)
│ │ ├── wifi_manager/ # WiFi lifecycle, NVS credentials, auto-connect
│ │ ├── ssh_client/ # libssh SSH with optimized cipher suites
│ │ ├── ble_hid/ # NimBLE BLE keyboard HID host
│ │ ├── input_queue/ # Unified FreeRTOS event queue
│ │ ├── app/ # Menu, dashboard, terminal mode, settings
│ │ ├── buttons/ # Debounced button handler
│ │ ├── sensors/ # RTC, temperature/humidity, battery
│ │ └── i2c_bsp/ # I2C bus abstraction
│ ├── sdkconfig.defaults # ESP32-S3 optimized config
│ └── partitions.csv # 16MB flash layout with OTA + LittleFS
├── simulator/ # Python/Pygame display simulator
├── hello_vu/ # ESP-IDF VU meter project
├── REFERENCES/ # Component datasheets
└── README.md
This repository uses:
- Python 3.9+ with Pygame for the simulator
- ESP-IDF v5.x for firmware development
- Python 3.9+
- ESP-IDF v5.x or later
- VS Code with ESP-IDF extension (recommended)
The 16 MB flash carries a 512 KB factory recovery firmware alongside a
10 MB main application slot. If the main app boots badly (panic before
esp_ota_mark_app_valid_cancel_rollback(), 30 s stability check fails, etc.)
the bootloader automatically boots the recovery firmware — which keeps
USB-JTAG CDC visible so the host can always reflash without pressing BOOT.
First-time / layout-change flash: ./flash.sh builds both projects
and writes all four regions (bootloader + partition table + factory +
main) per-offset, preserving NVS so WiFi credentials survive. See
MIGRATION-factory-ota.md.
Dev inner loop: ./dev-flash.sh [port] [test] rewrites only the
main slot at 0xA0000. Recovery, partition table, and NVS are
untouched. Do NOT use idf.py -C esp32_rendering flash — ESP-IDF
defaults its app offset to the first app partition (factory, 0x20000)
and would overwrite the recovery firmware.
Force recovery boot: hold button A (GPIO 18) at reset for ≥5 s. The bootloader erases otadata and boots factory on the next cycle — useful when ota_0 is marked valid but broken anyway.
Two complementary test surfaces:
Pure-logic unit tests under host_test/app/ run without hardware.
cd host_test
cmake -S . -B build
cmake --build build -j
ctest --test-dir build --output-on-failureThe pytest + pyserial harness at host_test/scenarios/ drives real
firmware over the ESP32-S3's built-in USB-JTAG CDC via the
test_console REPL component — ~40 commands covering input
injection, NVS, filesystem, introspection, and runtime control.
14 scenarios cover boot, menu cycling, font settings, pairing,
WiFi, servers, settings persistence, and the legacy-NVS migration
path. See host_test/scenarios/README.md for fixture docs, the
full command set, and concurrency caveats.
Build the test-overlay firmware and run the suite:
cd esp32_rendering
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.test" \
-p /dev/cu.usbmodem4101 flash
cd ../host_test/scenarios
pip install -r requirements.txt
export TEST_WIFI_SSID='your-ssid' TEST_WIFI_PASS='your-pass'
python -m pytest -vThe test overlay routes the primary console to USB-JTAG CDC
(default production builds use UART0) and enables FreeRTOS trace
facilities for the tasklist REPL command. Production firmware
has zero test-console code — the component is gated by
CONFIG_TEST_CONSOLE_ENABLED and produces empty sources when
disabled.
- Mars After Midnight - Working in One Bit - Lucas Pope's devlog on 1-bit graphics techniques
See LICENSE file.