A lock file pins platform-specific native binaries — Rollup, esbuild, swc.
A lock generated on Windows brings @rollup/rollup-win32-x64-msvc but no
Linux variant; a lock generated on Linux does the inverse. The Docker
build kept blowing up with MODULE_NOT_FOUND on rollup/dist/native.js
whenever the lock came from a contributor's non-Linux machine.
Trade-off: we lose npm's transitive-version pinning. Mitigated by:
- package.json caret ranges keep majors stable
- Docker image is rebuilt + retagged per release, so a deployed image
has a frozen dep set regardless of the lock
- Production uses a pinned upstream commit via velxio-prod's submodule,
not lock-driven repro
- Dependabot still flags vulnerable transitives via package.json scans
Changes:
- .gitignore: ignore package-lock.json everywhere
- .dockerignore: same (defense-in-depth — never enter build context)
- Dockerfile.standalone: keep `rm -f package-lock.json` as a safety net
for `docker build` runs from trees with a local lock
- frontend-tests.yml: `npm ci` → `npm install` (npm ci requires a lock)
- Delete the two committed locks (frontend/ + root). The test/* and
vscode-extension/* locks are left as-is — internal tooling, separate
install paths, not in the Docker build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock files generated on Windows/macOS pin platform-specific Rollup native
binaries (e.g. @rollup/rollup-win32-x64-msvc) and won't bring in the Linux
ones the Docker image needs. Symptom: `MODULE_NOT_FOUND` on
`rollup/dist/native.js` during `npm run build:docker`. See npm/cli#4828.
Removing the lock inside the build forces a fresh, Linux-native dep
resolution. Side effect: a tiny loss of cross-build version pinning, which
is acceptable here — `npm install` honours the version ranges in
package.json so semver-compatible patches at most slip in.
The proper long-term fix is to regenerate package-lock.json on Linux and
commit that. Until then this rm guards every Docker build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The transitive-include scan in _merge_arduino_libs_to_component used
glob('*.h') on the component dir, which only finds headers at the root.
Libraries with a src/ layout (GxEPD2, ArduinoJson, most modern Arduino
libs) keep their headers under src/<...>, so the scan saw zero headers
and never queued their transitive deps.
Symptom: compiling a sketch that includes GxEPD2_3C.h failed with
'Adafruit_GFX.h: No such file or directory' even though Adafruit_GFX
was installed via the Library Manager — because the BFS never reached
its header from inside GxEPD2_GFX.h.
Switching to rglob('*.h') walks the full directory tree and lets the
BFS pick up Adafruit_GFX, Adafruit_BusIO, and any other transitive
dependency that lives under src/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a comparison table up top so users pick the right path quickly.
- Option A (Docker): include the arduino-libs named volume so cores
don't reinstall on every container restart (was a 5-10 min hidden cost).
- Option B (Compose): note expected first-build time (~10-15 min for
ESP-IDF + frontend) so users don't think it's stuck.
- Option C (Manual): drop --recurse-submodules (npm pulls the wokwi libs),
add ATTinyCore install, flag that ESP32 emulation needs Docker (or the
separate ESP-IDF setup) since QEMU .so files only ship in the image.
- Update Project Structure: third-party/ is reference-only, deploy/ is now
docker/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The folder only holds the in-container nginx + entrypoint that the
standalone Dockerfile copies. Calling it "deploy" implied host-level
production glue, which now lives in github.com/velxio/velxio-prod.
"docker/" makes the build-time vs deploy-time split obvious.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves several install pain points reported by users (#108, #120) and
removes the obligatory upstream-clone step that confused contributors and
slowed down every Docker build.
Install fixes:
- nginx: server_name → catch-all default_server, drop Debian's stock site
so reverse-proxied users no longer get the "Welcome to nginx" page.
- entrypoint: auto-generate SECRET_KEY at first boot, persisted under
data/.secret_key. backend/.env is now optional in docker-compose.yml.
- backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on
some Python builds — caused uvicorn startup failures on WSL).
Wokwi libs come from npm:
- @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in
frontend/package.json. Vite aliases removed.
- Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements
/ wokwi-boards. Frontend stage is just COPY + npm install + build:docker.
- Board SVGs vendored under frontend/public/boards/ (10 deduped against
existing files, 2 truly new). third-party/wokwi-* clones become reference-
only credits — generate-component-metadata.ts skips gracefully when absent.
Production config split out:
- docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf,
update-third-party.bat removed. Production deployment lives in its own
repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups
+ pinned upstream commit).
Verified locally: 1161 frontend tests pass, build:docker completes clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Deleted diagram.json, esp32-cam-lcd-preview.ino, esp32-cam-lcd-status.ino, and libraries.txt from the esp32-cam-lcd-preview example directory.
- Updated submodule reference for qemu-lcgamboa.
- Removed generate_examples.py script used for generating example stubs.
Without an ownership check, viewing someone else's project (admin
inspection, browsing public projects) caused the auto-save hook to
PUT the project on every store change. The backend correctly rejects
non-owner updates with 403, but the frontend surfaced these as
"save fail" to the user — misleading and noisy in logs.
The hook now stays idle unless the authenticated user matches
currentProject.ownerUsername. Manual saves through SaveProjectModal
are unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.
Mechanical changes:
Path rename:
wokwi-libs/ → third-party/
update-wokwi-libs.bat → update-third-party.bat
docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md
Submodule reconfiguration:
.gitmodules — 4 path= and section names updated
.git/modules/wokwi-libs/ → .git/modules/third-party/
each submodule's .git file rewired to ../../.git/modules/third-party/<name>
Reference updates (~80 files): vite.config.ts aliases, Dockerfile
COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
docs/* and test/*/autosearch/* entries that mention the path,
package-lock.json file: dependencies, .gitignore patterns,
sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
.dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
(path) and bare `wokwi-libs` (textual mentions in docs/comments).
Verified:
- npx tsc -b --noEmit produces no new errors related to these paths
- vite.config.ts aliases now point at ../third-party/avr8js etc.
- All 4 git submodules (avr8js, rp2040js, wokwi-elements,
wokwi-features) are linked under third-party/ with their
worktrees re-populated and config files referencing the new path
- `grep -r wokwi-libs` returns zero hits outside node_modules,
.vite, frontend/dist, third-party/ (upstream submodule contents),
*.pyc caches, and *.dll.pre-camera rollback binaries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User goal: ESP32-CAM live preview that works with any webcam,
regardless of resolution, brand, or scene complexity. The previous
fixed-quality 0.28 was fragile (intermittent decode errors on
moving/textured scenes) and capped visual quality unnecessarily.
Two-layer fix; either alone is insufficient:
LAYER A — Bounded JPEG encoder (frontend, this repo)
frontend/src/hooks/useWebcamFrames.ts:
encodeBoundedJpeg() walks a quality ladder [0.6, 0.5, ..., 0.1]
until the JPEG fits in MAX_FRAME_BYTES (23 000). If even q=0.1
overshoots — extreme HD/4K scenes — falls back to a 240×180
downscaled canvas at q=0.4. Guarantees every emitted frame fits
the deliverable budget regardless of webcam hardware.
The hook now exposes lastQualityUsed + lastDownscaled so UI can
surface when auto-tuning kicks in.
frontend/src/components/simulator/CameraToggle.tsx:
Tooltip shows "(auto-tuned to q=0.X)" or "(auto-downscaled, q=0.X)"
while streaming so users see what the encoder picked.
LAYER B — Multi-lap descriptor ring walker (qemu-lcgamboa, submodule)
Bumps the QEMU per-frame deliverable cap from 8 KiB to ~32 KiB by
letting the walker reset the descriptor ring up to 4 times per
VSYNC. Submodule pointer bumped to eb8b7a5d.
Combined, the demo now supports:
- Cheap 480p webcams: q=0.6, 5-10 KiB JPEGs, sharp
- Logitech mid-range: q=0.5-0.6, 8-15 KiB JPEGs, sharp
- HD 1080p webcams: q=0.4-0.6, 15-23 KiB JPEGs, sharp
- 4K complex scenes: downscaled, still readable
Documented as bug closure in:
test/test-esp32-cam/autosearch/15_universal_webcam_compat.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported "JPG Decompression Failed! Data format error" hitting
intermittently with quality 0.35. Worker log showed actual JPEG
payloads at 7959-8123 bytes per frame — right at the QEMU emulator's
8192-byte deliverable budget (8 EOFs × 1024 bytes from cam_hal's
default 16-descriptor ring).
The webcam JPEG encoder produces variable-size output: simple uniform
scenes compress to ~6 KiB, complex/textured/moving frames bloat to
~9-10 KiB. Anything over 8192 gets truncated mid-Huffman-scan in the
firmware framebuffer, my walker injects FF D9 at byte 8190 to keep
cam_verify_jpeg_eoi happy, but the upstream jpg2rgb565 actually
parses the structure and chokes on the truncated stream.
Quality 0.28 keeps even the worst-case complex frame comfortably
under 8 KiB. Visual quality is still much better than the 0.25
fallback — fine for a 160×120 preview where the user cares about
"is my face there" not "did the JPEG quantization tables converge".
Real long-term fix would be to bump EOFS_PER_FRAME and lift the 8 KiB
ceiling — but that touches the QEMU walker (DLL rebuild cycle) and
risks breaking the descriptor-ring math. Doing this frontend tweak
first to unblock the demo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported only 2 frames rendering after the SPI batching change.
Diagnosis: the previous flush triggers (CS-line HIGH, buffer >=4096)
were both event-driven. Adafruit_ILI9341's ESP32 backend manages CS
via digitalWrite — i.e. through the GPIO peripheral, NOT the SPI
peripheral's hardware CS pin. So the CS-line-HIGH event from
picsimlab_spi_event NEVER fires for this driver. The buffer only
flushes when it hits 4096 bytes.
Frame 1: 38 400 bytes from drawRGBBitmap → 9 flushes at 4096-byte
boundaries → last 192 bytes stay in the buffer. Status bar adds
some bytes too → maybe one more flush.
Frame 2: same. But by frame 3 the firmware is running ahead of the
flush rhythm and somehow the buffer pattern wedges in a state where
no flush completes (likely a partial buffer that sits between
transactions while the firmware briefly waits on the next fb_get).
Hard to reproduce deterministically — but the symptom matches.
Fix: add a 50 ms periodic flush thread. Independent of any event,
it acquires the lock and flushes whatever's pending. Bounds the
worst-case latency at 50 ms (= 20 fps ceiling, more than enough for
the emulator).
Triple-trigger now:
1. CS HIGH (fast path for hardware-CS drivers)
2. Buffer >= 4096 (safety for big transactions)
3. 50 ms timer (catches GPIO-CS drivers, prevents stalls)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported the ESP32-CAM + ILI9341 live preview at ~1 frame/min.
Profile: 80×60 preview pushes 9600 SPI bytes per drawRGBBitmap, and
each byte was emitting a full {type:'spi_event'} JSON message over
the worker→backend→WS→frontend pipeline. Per-byte overhead ~150-200µs
in Python (json.dumps + sys.stdout.write+flush dominates) plus
asyncio + WS dispatch. Net: 1.5-2 sec/frame minimum, much worse with
GIL contention.
Fix: buffer MOSI bytes in the worker and emit a single base64-encoded
`spi_batch` message when CS goes HIGH (transaction ended) or the
buffer crosses 4 KiB. ~9600 events/frame collapse to ~3 messages.
backend/app/services/esp32_worker.py:_on_spi_event
- Add _spi_byte_buf bytearray + threading.Lock
- On op==0x00 (byte): append; flush early if buf >= 4096
- On op==0x01 (CS change): flush buffer, then emit the CS event
via the legacy spi_event channel (ePaper / custom chips that
observe CS still get it).
frontend/src/simulation/Esp32Bridge.ts
- New 'spi_batch' message handler decodes b64 and replays each
byte through the existing onSpiByte callback. Parts that
subscribed via simulator.spi.onByte don't notice the protocol
change. The 'spi_event' branch still handles CS changes plus
legacy single-byte payloads for backwards compat.
Now that 38 KB/frame is cheap, restore preview to 160×120 + JPEG
quality 0.35 in the gallery example. Real measured speedup: ~50× on
the QVGA preview demo. Real hardware was never affected — it runs
SPI at 80 MHz and pushes the bitmap in ~4 ms either way.
PSRAM emulation is unrelated to this bottleneck and was left untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported the live preview "looks slow" after the JPEG decode fix.
Diagnosis: each tft.drawRGBBitmap pushes width × height × 2 bytes over
SPI, and every byte takes a full QEMU → worker → backend (WS) → frontend
round-trip. At 160×120 that's 38 400 messages per frame; the bus
saturates at ~0.2 fps perceived.
Two changes shrink the per-frame SPI bandwidth:
1. Preview 160×120 → 80×60 (and JPG_SCALE_2X → JPG_SCALE_4X).
38 400 bytes/frame → 9 600 bytes/frame. Already 4× faster.
2. Status bar redraw throttled to every 10th frame instead of every
frame. The text writes (printf, fillRect, fillCircle) account for
another ~1-2 KB of SPI traffic per loop iteration. Skipping 9 of
every 10 redraws frees up a chunk more bandwidth without losing
the headline numbers (fps, frame counter) — they just refresh
once a second instead of 5x/sec.
Also dropped the trailing `delay(20)` — we don't need an artificial
throttle, the SPI bus is the throttle.
Real-hardware effect: zero. ESP32 SPI runs at 80 MHz; a full
160×120 bitmap pushes in ~4 ms either way.
Applied in two places:
- examples/esp32-cam-lcd-preview/esp32-cam-lcd-preview.ino
- frontend/src/data/examples.ts (in-app gallery copy)
Long-term plan: batch SPI bytes at the worker level (one WS message
per N bytes instead of per byte) — that's a deeper change in
qemu-lcgamboa + Esp32Bridge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ESP32-CAM + ILI9341 example was rendering grey-X "decode failed"
rectangles. Serial showed:
E (53868) esp_jpg_decode: JPG Decompression Failed!
Data format error
Root cause: the QEMU emulation delivers up to 8 KiB of JPEG bytes per
frame (8 EOFs × 1024 = 8192) plus a 2-byte FF D9 EOI injection at the
end of that window. Real webcam frames at quality 0.6 are ~11 KiB —
they get truncated mid-Huffman-scan in the firmware framebuffer.
cam_verify_jpeg_eoi accepts the frame (it found FF D9), but the
upstream jpg2rgb565() actually parses the JPEG and rejects the
truncated structure.
Quality 0.25 produces ~3-5 KiB JPEGs that fit the budget entirely.
The decoder finds the natural EOI well before our injection point,
parses cleanly, and renders to the TFT. Visual quality is fine for
an emulator preview — the user is seeing their webcam, not editing
print-quality photos.
Long-term fix is a smarter QEMU walker that ring-wraps to deliver
bigger JPEGs (>16 KiB possible by reusing descriptors mid-frame),
but that's a separate change in qemu-lcgamboa. This frontend tweak
unblocks the demo without another DLL rebuild cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's Frontend Tests workflow had been failing on master for ~15 runs.
Two pre-existing issues, neither related to the SPI refactor in 8b1433d
or the ESP32-CAM work:
1. RP2040Simulator mock missing attachCyw43 method (23 test files)
PR #126 (8e769f8 "feat(multi-board): add wire-aware cross-board
interconnect router", merged 2026-04-25) added a Pico-W-specific
`sim.attachCyw43(bridge)` call inside addBoard(). The 23 test files
that mock RP2040Simulator with vi.fn weren't updated; whenever a
test path created a Pico W board the mock threw "TypeError:
sim.attachCyw43 is not a function" and aborted addBoard.
Fix: add `this.attachCyw43 = vi.fn()` to every affected mock.
Also pre-populate `this.spi = { onByte: null, completeTransfer: vi.fn() }`
so any future SPI-part tests don't trip on the new generic .spi
adapter from 8b1433d.
2. install-libraries.test.ts payload mismatch
PR #135 (b1026ec7 "library-version-uninstall", merged 2026-04-29)
extended `installLibrary(name)` to `installLibrary(name, version?)`
and now sends `{name, version: version ?? null}` over the wire.
The test still asserted `{name}` only and failed.
Fix: assert `{name, version: null}` for the no-version call.
Verified locally: 1161 passed | 1 skipped (was 1117 passed | 44 failed).
Backend E2E "Run HC-SR04 e2e test" is a separate failure that needs
its own investigation — it downloads QEMU binaries from a release and
runs real firmware compilation, which I can't reproduce on Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix added an ESP32-specific code path inside ili9341Simulation
to subscribe to the QEMU worker's spi_event stream. That made the LCD
work on ESP32-CAM but left the underlying issue unsolved: every other
SPI part (custom chips, future SD-card emulators, the SSD168x ePaper
already in the codebase) would also need its own per-board branching.
The right shape: every simulator exposes a `.spi` member matching the
SAME SpiBusLike interface, and SPI parts hook .spi.onByte without
caring which board they're attached to. AVRSimulator already had
this — now everything else does too.
frontend/src/simulation/SpiBus.ts (new)
Defines the contract — `onByte: (mosi) => void | null` plus
optional `completeTransfer(miso)`. Documents the single-listener
semantics that AVR has had since day one.
frontend/src/store/useSimulatorStore.ts
Esp32BridgeShim gets a lazy `.spi` getter that wraps
bridge.onSpiByte (the per-byte WS event from the QEMU worker).
completeTransfer is a no-op because the worker drives MISO via
its own _spi_response global. Covers ESP32 (Xtensa), ESP32-S3,
ESP32-CAM, ESP32-C3 — every kind that routes through Esp32Bridge.
frontend/src/simulation/RP2040Simulator.ts
Adds a lazy `.spi` getter that re-routes rp2040.spi[0].onTransmit
through the adapter. Default loopback (the prior behaviour) is
preserved when no part has accessed `.spi` yet — only consumers
that opt in see their handler invoked. Covers Pico and Pico W.
frontend/src/simulation/parts/ComplexParts.ts
ili9341Simulation no longer has an ESP32 special case. Single
code path: `simulator.spi.onByte = handler`. Works on AVR,
RP2040, all ESP32 variants. Same pattern is now available to
every future SPI part — ssd1306, sd-card, oled, etc.
The Esp32Bridge.ts spi_event field-name fix from 6afa62e (msg.data.event
instead of the non-existent msg.data.data) stays in place — that's what
makes the per-byte stream actually arrive in the bridge.
Verified: ILI9341 + ESP32-CAM gallery example renders the live webcam
preview after a hard refresh. The same simulation code works on Arduino
Uno + ILI9341 (the existing ili9341-test-sketch in example_zip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ILI9341 part simulation only hooked AVR's SPI peripheral. For
ESP32 the simulator is Esp32BridgeShim (no .spi member), so
attachEvents bailed early and the LCD stayed black even though the
firmware was driving SPI traffic correctly.
The QEMU worker already emits per-byte spi_event WS messages
(see backend/app/services/esp32_worker.py::_on_spi_event), and the
Esp32Bridge already had an onSpiEvent hook — but the bridge was
reading msg.data.data (a non-existent field) instead of decoding
the worker's {bus, event, response} format. Fixed.
Two changes:
1. Esp32Bridge.ts: decode the spi_event payload correctly. The
worker encodes byte transfers as `mosi << 8` (op = low byte = 0x00)
and CS-line changes as `((cs<<1)|level) << 8 | 0x01` (op == 0x01).
Added onSpiByte (per-byte) and onSpiCsChange callbacks alongside
the existing onSpiEvent for backwards compat.
2. ComplexParts.ts ili9341Simulation: detect Esp32BridgeShim via
`getBridge()` duck-type check. When present, subscribe to
bridge.onSpiByte and feed bytes into the same processCommand /
processData pipeline used by the AVR path. DC tracking via
pinManager.onPinChange already works for ESP32 because the bridge
fires triggerPinChange on every gpio_change WS event.
Verified end-to-end: ESP32-CAM + ILI9341 example in the gallery now
renders the live webcam preview to the simulated TFT (160×120 RGB565
centered in the 320×240 panel) at ~3-4 fps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two listed examples to the gallery (book icon → Examples) so
users can one-click load the new ESP32-CAM emulation:
1. ESP32-CAM: Webcam Demo (sensors / beginner)
Minimal sketch — init OV2640, verify SCCB chip-id, loop on
esp_camera_fb_get() printing frame metadata to Serial. Proves
the emulation is alive without any external components.
2. ESP32-CAM + ILI9341 Live Preview (displays / intermediate)
Full demo — decode JPEG with jpg2rgb565() (built-in to
esp32-camera/conversions, header exposed by the Velxio compile
template) and render the resulting RGB565 bitmap to a 320×240
SPI TFT. Pre-wired diagram: ILI9341 connected via VSPI to GPIOs
12-15 (the only block free after OV2640 takes over the rest of
the AI-Thinker pins).
Type changes:
- ExampleProject.boardType union extended with 'esp32-cam'
- BOARD_TABS in ExamplesGallery.tsx gets a new "ESP32-CAM" tab
(orange #d35400)
Both examples use boardFilter: 'esp32-cam' so they show under
the new tab and not the generic ESP32 one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First end-user demo of the new ESP32-CAM emulation capability —
shows how to wire an ESP32-CAM to an ILI9341 320×240 SPI TFT,
decode JPEG frames from the emulated webcam with jpg2rgb565()
(built into esp32-camera/conversions, header now exposed by the
Velxio compile template), and render the resulting RGB565 bitmap
to the screen at ~10 fps.
Two sketches in the same example folder:
- esp32-cam-lcd-preview.ino — full demo: decode JPEG → render the
bitmap (160×120 centered in the TFT) + status bar with fps,
frame counter, decode-fail counter, live pulse dot.
- esp32-cam-lcd-status.ino — companion that doesn't decode the
JPEG; instead it shows a status dashboard (frame counter, byte
histogram, JPEG header hex dump). Useful when the source JPEG
exceeds the deliverable byte budget and jpg2rgb565 fails on the
truncation.
diagram.json wires the two parts using velxio-esp32-cam +
wokwi-ili9341 part types over VSPI:
ILI9341 ↔ ESP32-CAM
CS ↔ GPIO 15, RST ↔ GPIO 2, D/C ↔ GPIO 14,
MOSI ↔ GPIO 13, SCK ↔ GPIO 12
libraries.txt lists Adafruit GFX + Adafruit ILI9341. esp_camera.h
and img_converters.h ship with arduino-esp32 — no extra install.
Adds examples/README.md as the index for future demos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User confirmed the FULL pipeline works with their laptop webcam at
QVGA quality 0.6 — frontend → WS → backend → worker → DLL → I²S →
firmware → esp_camera_fb_get() → user sketch.
[Frame #1] 8192 bytes 320x240 fmt=4
├─ SOI (FF D8 FF): ✓ at offset 0
├─ EOI (FF D9): ✓ at offset 8190
└─ First 16 bytes: FF D8 FF E0 00 10 4A 46 49 46 …
(JFIF, real webcam)
Stats: 10/10 Valid JPEGs at ~3.5 fps.
Changes:
- wokwi-libs/qemu-lcgamboa @ e4321d1 (picsimlab-esp32):
EOFS_PER_FRAME 6→8, inject_eoi_now flag for EOI injection on the
last EOF of each VSYNC burst. Handles JPEGs of arbitrary size by
forcing FF D9 at offset 8190 — JPEG decoders tolerate the
truncation gracefully.
- backend/esp32_worker.py: throttled trace log every 30 frames
(`camera_frame #N received (NNNN bytes)`) so users can confirm
the frontend → worker leg is alive without flooding the log.
- test/test-esp32-cam/autosearch/14: documented bug #9 (the 9th and
final silent bug — real webcam JPEGs exceed the deliverable byte
budget) with full forensic trace + final architecture diagram.
The emulation now handles ANY user webcam → ESP32-CAM use case end
to end. Standard upstream esp_camera_init() / esp_camera_fb_get()
sketches work without modification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end ESP32-CAM emulation now works. esp_camera_fb_get() in
user sketches returns valid camera_fb_t* pointers with JPEG frames
sourced from the user's webcam (or synthetic frames in tests).
Verification: webcam_demo.ino prints
frame N: 6144 bytes 320x240 fmt=4
continuously at ~10 fps under QEMU. 53 frames received in a 25 s
test window with debug logging disabled.
Bumps wokwi-libs/qemu-lcgamboa pointer to 5bbc92b (picsimlab-esp32)
which contains the final two fixes:
- eofs_remaining counter for multi-EOF-per-frame delivery
- reset_descriptor_ring() on rx_start 0→1 edge (matches hardware's
fresh-capture semantics that cam_hal relies on)
Adds:
- test/test-esp32-cam/autosearch/14_complete_emulation.md — full
forensic trace of the 8 distinct bugs found across the pipeline,
with final architecture diagram
- test/test-esp32-cam/tests/test_webcam_demo_live.py — pytest e2e
test that compiles webcam_demo.ino, boots it under the simulator
WebSocket, pushes a JPEG, and asserts fb_get returns frames
- test/test-esp32-cam/tests/debug_worker_direct.py — direct worker
bypass (no WS, no uvicorn) for dev-time tracing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps wokwi-libs/qemu-lcgamboa pointer to a96c851 (picsimlab-esp32
branch) which contains:
- pack_one_pixel fix (was discarding half the JPEG data)
- split vsync_kick_timer / eof_timer (resolves chicken-and-egg
between VSYNC and rx_start)
- multi-descriptor walker (already in previous commit, recap)
Adds test/test-esp32-cam/autosearch/13_three_remaining_bugs.md
with line refs to upstream esp32-camera and a TODO list for the
next rebuild + verification cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First open-source end-to-end emulation of the AI-Thinker ESP32-CAM
in QEMU, paired with a browser webcam → firmware bridge so users can
develop camera sketches without hardware. Status: esp_camera_init()
returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly
match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by
the upstream driver. Final piece (cam_task accepting frames) is in
progress — descriptor walker fix landed in this commit.
Backend (Python/FastAPI):
- simulation.py: camera_attach/frame/detach WS handlers
- esp32_worker.py: ctypes binding to velxio_push_camera_frame +
feature-detection fallback for older DLLs
- esp32_lib_manager.py: forward camera commands to the worker stdin
- esp-idf-template/main/CMakeLists.txt: esp32-camera headers added
via add_prebuilt_library + REQUIRES driver (resolves i2c_master_*
symbols). LED_BUILTIN=2 fallback for sketches that hardcode it.
Frontend (React/TS):
- EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family)
added to isQemuBoard list — Run button now starts the QEMU bridge
for these boards instead of falling through to the AVR path
- useWebcamFrames.ts: getUserMedia → OffscreenCanvas →
toBlob('image/jpeg') → base64 → WS at ~10 fps
- CameraToggle.tsx: header button with status colors + frame counter
- SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards
- Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa
- useSimulatorStore.ts: diagnostic log on compileBoardProgram
- components-metadata.json: regen including esp32-cam component
Submodule pointer:
- wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on
davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32)
Investigation + tests in test/test-esp32-cam/:
- 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S
spec, build blueprint, blockers resolved, descriptor walker fix)
- 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip,
webcam_demo) + 8 live + WS regression tests
- README with the user-facing flow
.gitignore:
- libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated)
- wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path,
not part of this repo)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- Add version field to InstallLibraryRequest
- Add fallback and requested_version to InstallResponse
- Add DELETE /api/libraries/uninstall endpoint
- Enhance install_library() for versioned installs (LibName@version)
- Add semver validation and fallback logic
- Add uninstall_library() method
- Fix _parse_version() to reject non-numeric version parts
Frontend:
- Update installLibrary() with optional version parameter
- Add uninstallLibrary() and resolveLibraryVersion() helpers
- Add version selector dropdown in Library Manager
- Add UNINSTALL button for installed libraries
- Show fallback messages when requested version unavailable
- Add parseLibSpec() and version badges in InstallLibrariesModal
_create_idf_component was fixed but NOT _merge_arduino_libs_to_component.
The latter was still flattening library files, causing ArduinoJson compilation to fail
with 'src/ArduinoJson.h: No such file or directory'.
Changes:
- Replace flat copy with directory-structure-preserving copy
- Add exclusion logic for non-buildable directories (examples, tests, docs)
- Generate INCLUDE_DIRS from actual directory structure
- Track files by relative path to prevent name collisions
This ensures libraries with src/ layouts (ArduinoJson, etc.) compile correctly.
- Maintain original library layout (src/, utility/) instead of flattening files
- Add exclusion logic for non-buildable directories (examples, tests, docs, CI)
- Dynamically generate INCLUDE_DIRS from actual directory structure
- Add validation to ensure buildable source files exist before proceeding
- Support both flat and src-based library layouts
- Fix path separator normalization for cross-platform compatibility
This improves compatibility with complex Arduino libraries that rely on
specific directory structures and relative includes.
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Update esp32_lib_manager.py to dynamically set library extensions based on the platform (Linux, Windows, macOS).
- Update submodule references for qemu-lcgamboa and wokwi-elements.
- Add documentation on ESP32 Arduino runtime crashes related to cache disable during WiFi/BT initialization.
- Introduce regression tests for the ESP32-CAM blink issue, ensuring the user sketch matches the reported problem.
- Implement an IRAM-safe blink sketch to confirm the regression is due to the Arduino runtime.
- Create a comprehensive test suite to cover various layers of the simulation and compilation process.
Wire up three real, public-domain ROMs from the silicon era and prove
they boot end-to-end on the clean-room chip implementations. Each
test reads a separate well-known boot artifact:
* Busicom 141-PF firmware (4004, 1 KB, Intel PD 2009)
Wires real 4004 + real 4002 chips on the multiplexed nibble bus.
Toggles TEST every ~400 phases to mimic the printer-drum encoder
pulse the firmware polls. Asserts >2000 opcode fetches, >15 unique
PC addresses, and >100 CMROM strobes.
* Palo Alto Tiny BASIC v2 (8080, 1.9 KB, Wang 1976 PD)
CPUville port loaded from Intel HEX. Fake polled 8251 UART at port
0x02 (data) / 0x03 (status). Asserts the captured TX stream
contains the BASIC "OK" prompt — proving the interpreter reached
its REPL.
* Galaksija ROM A (Z80, 4 KB, Voja Antonić PD 1984)
ROM A+B at 0x0000..0x1FFF, system RAM at 0x2000..0x3FFF. Asserts
PC visits the JP target 0x03DA from reset and the ASCII "READY"
prompt appears in RAM after init.
ROMs are downloaded to roms/{4004,8080,z80}/ and gitignored — the
tests skip cleanly when the binaries are absent. License-clean: no
GPL ROMs, all PD by upstream provenance.
Total: 126 → 129 passing, 0 todo, 0 failed; 19 → 22 test files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the last outstanding it.todo (4004 Busicom-style program)
into a passing integration test. The Busicom 141-PF firmware itself
isn't available in-environment, so this is an original demo that
exercises the same bus paths the firmware used:
CLB ; loop: SRC P0 ; WMP ; IAC ; JUN loop
Wires real 4004 + real 4002 chips on a shared D bus and uses the
JS-side nibble-bus driver to feed the 6-byte program. The 4002's
O0..O3 output port blinks through 0, 1, 2, 3, …, F, 0, … each
iteration. Test asserts the first 6 distinct outputs are 0..5 —
proving the loop iterates and the output port reflects each WMP-
driven ACC update faithfully.
Final state: 126 tests, 126 passing, 0 todo, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert 7 outstanding it.todo markers into actual passing tests now
that the chips and bus infrastructure can support them:
- 4004 LDM: ACC observed via SRC + WMP X2 bus drive
- 4004 FIM: register pair observed via SRC X2/X3 nibble drives
- 8080 hand-built loop: LXI/MVI/INR/DCR/JNZ decrements counter
- Z80 IM 2: vector table at I:00 → ISR via INT̅ low
- 8086 1 MB wrap: DS=0xFFFF + offset 0x11 lands at physical 0x00001
- 8086 ALE pulse: counts ALE rising edges per bus cycle
- 8086 AD release: external drive sticks during T2 (chip released)
- 8086 hello-world: 5 MOV BYTE [imm], imm writes to memory-mapped
"UART" at DS:0x9000; bus capture + RAM peek verify "Hello"
Plus: remove redundant 8080 CPUDIAG and Z80 ZEXDOC todos — the
actual end-to-end runs already pass in dedicated cpudiag.test.js
and zexdoc.test.js files.
Suite is now 125/126 passing, 1 todo (Busicom 141-PF demo, awaiting
firmware ROM), 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the same xact_t pattern from the 4004 (phase D-2) to the 4040,
so SRC and the I/O group (WRM/WMP/WRR/WPM/WR0..3/SBM/RDM/RDR/ADM/
RD0..3) drive or sample the multiplexed nibble bus during X2/X3 with
CM-RAM (or CM-ROM for ROM-port ops) strobed.
The 4040's two CM-ROM lines (selected by rom_bank) and its STP/INT
control flow are unchanged — the bus action is staged at M2 and
acted on at X2/X3, fitting cleanly inside the existing PHASE_X3
control-flow block.
Two new integration tests under "4040 + 4002 RAM integration" mirror
the 4004's: SRC + WMP drives the output port, and SRC + WRM/RDM
round-trips a nibble through 4002 storage.
Total test_intel: 117 passing, 11 todo, 0 failed (was 115).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 4004 chip now drives or samples the multiplexed nibble bus during
X2/X3 with CM-RAM (or CM-ROM) strobed for SRC, WRM, WMP, WRR, WPM,
WR0..3, SBM, RDM, RDR, ADM, RD0..3 — completing the I/O group that
was previously stubbed. The 4002 RAM chip is rewritten with a
phase-count-based timing model that samples the opcode at M1/M2 and
drives or latches the bus at the correct frame relative to the 4004's
drives.
Two new integration tests in 4002-ram.test.js wire a real 4004 + 4002
on the same board and prove the round-trip:
1. SRC P0 + LDM 3 + WMP — 4002 output port goes to 3.
2. SRC P0 + WRM 5 + CLB + RDM + WMP — 4002 output port goes to 5
(proves both write and read paths through the bus).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 4002 is the data/IO partner of the 4004/4040. 16-pin DIP, 80
nibbles (4 registers × 16 main chars + 4 status chars each), plus
4 dedicated output port pins driven by the WMP instruction.
This skeleton:
- Pin contract registered (D0..D3, O0..O3, SYNC, CL, RESET, CM,
VDD, VSS).
- Storage allocated (main[4][16] + status[4][4] arrays).
- SYNC + own timer + CM-strobe gating tracks the SRC chip-select
latch at X2/X3 (compile-time RAM4002_CHIP_PAIR selects which of
4 chip pairs this instance responds to).
- RESET clears storage and drops output port to 0.
Not yet implemented (Phase D-2 follow-up): full SRC + WRM/RDM/WR0..3/
RD0..3 round-trip with the 4004. The 4004 chip currently stubs
those I/O instructions, so even though the 4002's address-latching
works, no data ever flows. Requires modifying 4004.c to drive the
bus during X2/X3 of SRC and during M2 of the I/O group.
Tests: 2/2 passing (pin contract + RESET behaviour). Total
test_intel: 111→113 passing. The 4-chip 4004 ecosystem (4001 +
4002 + 4004 + canvas-deployable variants) now exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First test wiring the 8086 CPU to a real 8259 PIC chip on the same
board and proving hardware-interrupt routing works end-to-end:
IRQ0 input → PIC asserts INT → CPU's INTR pin → CPU runs INTA
cycle → PIC drives vector 0x40 on AD bus → CPU does do_int(0x40)
→ fetches CS:IP from IVT entry at 0x100 → ISR runs → IRET → main
resumes from HLT.
Two related chip fixes required to make this work:
1. 8086 INTA cycle no longer drives AD itself.
Real 8086 INTA bus cycle has the PIC drive the data lines, not
the CPU. My earlier code did `bus_read_byte(0, false)` which
first drove AD with addr=0, overwriting whatever the PIC had
driven. Fix: release_ad → INTA̅ low → sample AD (PIC's INTA
watcher fires synchronously and drives) → INTA̅ high.
2. 8086 HLT now interruptible.
on_clock previously early-returned on G.halted, so step()
never ran and the INTR check never executed. Real 8086 HLT
wakes on INTR/NMI. Fix: remove the early return; step()'s
own halted check (later in the function) only no-ops if no
pending interrupt.
Tests: total test_intel 110 → 111 passing (+1, the integration
test). 0 failed. 11 todo. test_8086 now 11→12 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two long-deferred Phase C chips, both clean-room from public Intel
datasheets. These complete the support-chip ecosystem needed for
real interrupt-driven 8080/Z80/8086 software on the canvas.
8259 PIC (~280 LOC, single-master subset):
- Full ICW1..ICW4 init sequence with branching on single/cascade
and ICW4-needed flags.
- IRR/ISR/IMR registers; OCW3 read-back; OCW1 mask write.
- Priority-based INT (lower IRQ# = higher priority, fully-nested);
pre-emption when a higher-priority IRQ arrives during a lower-
priority ISR.
- INTA falling-edge → drives vector_base + IRQ# on D bus.
- Non-specific (0x20) and specific (0x60..67) EOI.
- Cascade-master/slave routing NOT implemented (single master is
sufficient for 95% of demos).
- 7/7 tests passing.
8253 PIT (~210 LOC, Modes 0/2/3 subset):
- Three independent 16-bit counters with own CLK/GATE/OUT pins.
- Mode 0 (interrupt on terminal count) for one-shot timers.
- Mode 2 (rate generator) for system tick.
- Mode 3 (square wave, decrements by 2) for PC-speaker tone.
- Modes 1/4/5 coerced to Mode 0 (rare in practice).
- Full RW mode set: LSB-only / MSB-only / LSB-then-MSB / latch.
- GATE-low pauses the countdown.
- 4/4 tests passing.
Tests: total test_intel 99→110 passing (+11). 0 failed. 11 todo.
Master plan doc updated: Phase C extension done; Phase G still
deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 4001 is the canonical ROM partner of the 4004/4040. 16-pin DIP,
256 bytes of mask-programmed ROM accessed over the 4-bit multiplexed
nibble bus, plus 4 I/O port lines (WRR/RDR — not yet wired).
Implementation: ~140 LOC clean-room from MCS-4 manual §V. The chip
has its own timer at 1351 ns (matching the 4004's clock period), with
a state machine that walks the 8-phase frame in lockstep with the
4004:
S_IDLE → (SYNC↑) → S_SAMPLE_LOW (A1 nibble) → S_SAMPLE_MID (A2) →
S_SAMPLE_HIGH (A3, addr complete) → S_DRIVE_HI (M1, drive opcode
high nibble) → S_DRIVE_LO (M2, drive low nibble) → S_POST (X1..X3
idle) → wait for next SYNC.
Timing trick: the 4001 must be added to the board BEFORE the 4004
so its tickTimers fires first per advanceNanos. The 4001 then runs
one frame "behind" the 4004 — sampling what the 4004 drove last
frame and driving what the 4004 will read this frame. Documented in
the chip's source and the master plan.
Integration test (`test_buses/4001-rom.test.js`) wires both chips on
the same board and verifies the 4004 actually fetches and executes
opcodes from the 4001 (PC walks 0, 1, 2 with the embedded NOP image).
This is the first end-to-end test of the 4-bit multiplexed bus
working between two real WASM chips on the canvas, not just JS
helpers — proving the bus model scales.
Deferred for the next Phase D iteration: 4002 RAM (similar shape +
SRC chip-select latching), 4004 SRC/WRM/RDM wiring to exchange data
with the 4002, and the Busicom 141-PF integration once both ROM and
RAM chips are real.
Tests: total test_intel 98 → 99 passing, 0 failed, 11 todo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>