UNSTABLE — still in development; largely untested on real hardware. The
crepuscularity-embeddedcrate andcrepus embeddedcommands are experimental. Treat snapshots and host simulators as the supported validation path until board bring-up is documented. APIs may change without a semver-major bump. Pin exact crate versions in firmware projects and expect regressions.
Status: CI covers parsing, layout, and host-side PPM snapshots. End-to-end SPI/LTDC/ESP-LCD paths are not production-ready and have not been broadly exercised on physical panels.
Also: Documentation home · DSL · Components · CLI · TUI · Native shells
Render .crepus templates into a fixed-size RGB565 framebuffer for microcontrollers, SPI displays, and simulators. The primary integration path is Rust: depend on crepuscularity-embedded, use Ui, and push ui.rgb565() to your display driver.
[dependencies]
crepuscularity-embedded = "0.1.9"Default features include parsing and include (via crepuscularity-core). For layout/paint only on device, use default-features = false and construct trees on the host (advanced).
use crepuscularity_embedded::ui;
const UI: &str = include_str!("ui/dashboard.crepus");
let mut ui = ui!(UI, 240, 320, "cpu" => 42, "status" => "nominal");
loop {
ui.render().expect("render");
panel.flush_rgb565(ui.rgb565()); // your SPI / LTDC / DMA driver
if let Some(id) = ui.hit(touch_x, touch_y) {
// handle element #id
}
}Without the macro:
use crepuscularity_embedded::Ui;
let mut ui = Ui::new(240, 320, UI)
.with("cpu", 42)
.with("status", "nominal");
ui.set("cpu", 88); // in-loop updates
ui.render()?;Same crate, feature-gated modules under panel. Implements Rgb565Display so you can ui.flush(&mut panel) without wiring mipidsi / Embassy / ESP-IDF yourself.
| Feature | Target |
|---|---|
mipidsi |
Raw RGB565 blit into mipidsi Display |
embassy-stm32 |
ILI9341 / ST7789 on STM32 blocking SPI |
esp-idf |
esp_lcd_panel_draw_bitmap wrapper |
crepuscularity-embedded = { version = "0.1.9", default-features = false, features = ["embassy-stm32"] }use crepuscularity_embedded::panel::embassy_stm32::ili9341_240x320_static;
let panel = ili9341_240x320_static(spi, sck, mosi, miso, cs, dc, rst);
ui.render()?;
ui.flush(panel)?;PanelPreset — Ili9341_240x320, St7789_240x320, etc. — sets byte order and Ui size (always available, no extra feature).
Board walkthroughs: examples/embedded-stm32/ · examples/embedded-esp32/.
crepuscularity-embedded fills an RGB565 buffer; optional panel helpers or your own driver push it to the glass.
.crepus → Ui::render() → ui.rgb565() → SPI (ILI9341/ST7789) | LTDC framebuffer | esp_lcd → LCD
↑
ui.hit(x, y) ← touch controller (FT6336, XPT2046, …)
| Setup | MCU examples | Panel IC | Bus | Match Ui::new(w, h, …) |
|---|---|---|---|---|
| Cheap 2.4″ SPI module | STM32F4, F7, H7 | ILI9341 | SPI | 240×320 |
| ESP32 dev boards | ESP32-S3 | ST7789 | SPI / QSPI | 240×320, 240×280, … |
| Pico Display Pack | RP2040 | ST7789 | SPI | 240×135 |
| Discovery / HMI | STM32F746, H743 | RGB parallel | LTDC (+ SDRAM FB) | board resolution (e.g. 480×272) |
STM32 + ILI9341 — feature embassy-stm32 or blit a host-built frame.bin (examples/embedded-stm32).
ESP32-S3 + ST7789 — feature esp-idf + examples/embedded-esp32.
LTDC (framebuffer in RAM) — avoid a copy; render into scan-out memory:
let mut view = Rgb565View::new(screen_size, &mut ltdc_framebuffer[..])?;
ui.render_into(&mut view)?;Touch: read (x, y) from your controller driver → ui.hit(x, y) returns the deepest #id (e.g. #btn-save in .crepus).
On-device parsing today needs the default std feature (heap). Works well on ESP32 and larger Cortex-M parts; validate templates in CI with crepus embedded check and build.rs (compile_crepus).
Step-by-step board notes: examples/embedded-dashboard/README.md.
examples/embedded-dashboard/ — runnable on the host (no hardware required):
cargo test -p embedded-dashboard
cargo run -p embedded-dashboardbuild.rs validates ui.crepus at compile time with crepuscularity_core::build::compile_crepus.
Embedded rendering injects the same target flags as other backends:
| Variable | Value |
|---|---|
crepus_target |
"embedded" |
is_embedded |
true |
is_tui / is_web / is_gui |
false |
screen_width / screen_height |
display size in pixels |
Use in templates: if {is_embedded && screen_width == 240}.
After render(), use ui.hit(x, y) or ui.document() for node_by_id and layout bounds. Events are not wired yet; hit-testing is for touch/button layers you add in firmware.
If the panel buffer is already allocated:
use crepuscularity_embedded::{Rgb565View, Ui};
let mut ram = [0u16; 240 * 320];
let mut view = Rgb565View::new(screen, &mut ram)?;
ui.render_into(&mut view)?;
// `ram` is ready for DMA| Command | Purpose |
|---|---|
crepus embedded check ui.crepus |
Parse-only validation for CI / build.rs |
crepus embedded snapshot ui.crepus --width 240 --height 320 --out preview.ppm |
Debug only — RGB888 PPM preview, not for production firmware |
--ctx (JSON) and --var key=value match crepus native ir conventions on snapshot.
Supported today: column/row flex, sizing (w-full, h-full, fractions, w-[Npx]), spacing (padding, margin, gap), alignment, borders, Tailwind CSS v4 named colors (bg-zinc-900, text-green-500, arbitrary bg-[#…]), opacity, font-size tokens, ASCII bitmap text, include with the same path rules as TUI/native.
Palette is generated from Tailwind v4.3 theme.css (scripts/gen-tailwind-colors.mjs). Regenerate with: bun scripts/gen-tailwind-colors.mjs --theme /path/to/theme.css.
Not in v1: direct LVGL runtime adapter in the framebuffer path, animation, input events, responsive/dark variants, grid/shadows, no_std parsing on device. Use the separate LVGL target when you want LVGL Pro XML output.
Template— file-backed templates, custom framebuffersrender_template_to_framebuffer— one-shot renderEmbeddedDocument— retained tree withby_idandnode_at