An example of using Svelte and Bun to build an offline-first web UI that runs on embedded systems. The frontend is compiled and bundled into C++ header files, then flashed directly onto an ESP32-S3 where it's served from the device's own web server - no cloud, no internet, no external dependencies.
|
Camera Feed |
IMU Orientation |
BUILD TIME (on your computer)
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Svelte Components ──► Bun Build ──► main.js ──► web_assets.h │
│ (src/ui/*.svelte) (compile, (bundled (C++ PROGMEM │
│ minify) JS+CSS) string) │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
▼ Flash via Arduino
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME (on ESP32) │
│ │
│ Browser ◄──── WiFi ────► ESP32 WebServer (C++) │
│ │ │ │
│ │ GET / └──► Serve index.html from flash │
│ │ GET /api/telemetry └──► Read MPU-6050 via I2C │
│ │ GET /api/stream └──► Stream camera frames │
│ │
└─────────────────────────────────────────────────────────────────────┘
Bun is only used at build time to compile Svelte and generate assets. The ESP32 runs pure C++ (Arduino) - no JavaScript runtime on the device.
bun install
bun run build:esp32
arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32S3 tactical_console/
arduino-cli upload --fqbn esp32:esp32:XIAO_ESP32S3 -p /dev/cu.usbmodem* tactical_console/
tactical123)| Component | Description | Connection |
|---|---|---|
| XIAO ESP32-S3 Sense | Main board with camera | - |
| MPU-6050 | 6-axis gyroscope/accelerometer | I2C |
| MPU-6050 Pin | ESP32-S3 Pin |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SCL | D5 (GPIO6) |
| SDA | D4 (GPIO5) |
| ADO | GND |
tactical-console/
├── tactical_console/ # Arduino sketch (runs on ESP32)
│ ├── tactical_console.ino # WebServer + camera + IMU
│ ├── camera_pins.h # GPIO config
│ └── web_assets.h # Generated frontend (PROGMEM)
├── src/
│ ├── server/ # Dev server (mock data for testing UI)
│ └── ui/ # Svelte frontend
├── build.ts # Custom Svelte bundler for Bun
└── package.json
The dev server provides mock sensor data so you can work on the UI without hardware:
bun run dev # Start dev server at localhost:3000
bun run build # Build frontend only
bun run build:esp32 # Build + generate ESP32 assets
bun test # Run tests
Bun doesn't natively compile .svelte files. The custom plugin in build.ts:
.svelte imports during bundling| Option | Value | Purpose |
|---|---|---|
generate |
"dom" |
DOM manipulation code |
css |
"injected" |
Embed styles in JS bundle |
dev |
false |
Production mode, smaller size |
| Component | File | Size |
|---|---|---|
| Frontend HTML | dist/index.html |
545 B |
| Frontend JS | dist/assets/main.js |
~30 KB |
| ESP32 firmware | .bin |
~1.2 MB |
The main thread is blocked by the camera when streaming is enabled. This is a problem because the main thread is also used to serve the web server and handle the HTTP requests. This means that the web server will not be able to serve the requests while the camera is streaming so gyroscope data will not be updated and camera recording status remains on unless the webpage is refreshed.
Currently the esp exposes a rest api for the frontend to make requests to. Then the frontend uses setInterval to refetch telemetry data every 100 ms. This is expensive and does not lend itself to real time multi client usage. A better alternative would be to write an adapter in the bun server that reads the sensor and camera data throigh Unix sockets then expose a websocket server that the clients can subscribe to.
Portions of this codebase were generated with assistance from a private LLM hosted and operated locally. All generated code has been reviewed, tested, and modified as needed.