The Pico W WiFi showcase examples are a paid-overlay feature now (the WiFi
engine moved to the overlay). Read them through a build-time `@pro` seam so the
SSR prerender + gallery + sitemap include them when built with the overlay, and
OSS gets an empty stub.
- data/examples.ts: import { proExamples } from '@pro/data/proExamples' (static,
build-time) instead of the local examples-picow-wifi.ts; delete that file.
- src/__pro_stub__/data/proExamples.ts: OSS no-op (empty list) for the @pro alias.
- vitest.config.ts: mirror the @pro alias (stub by default / overlay when
VITE_PRO_BUILD) so tests loading examples.ts resolve it.
- scripts/generate-sitemap.mjs: also parse <PRO_OVERLAY_PATH>/data/proExamples.ts
when building with the overlay (the script reads example IDs from source text,
so it can't follow the alias).
- Tests: drop the picow-wifi import/usage from the 5 OSS example tests (they
validate the OSS set now); prune the 4 obsolete picow netlist snapshots. The
overlay's proExamples get their own coverage in pro/.../__tests__/.
Confirmed the RP2040 GPIO -> PinManager -> wokwi-led path is fully wired
(identical to the AVR path): a wokwi-led wired anode->GP2, cathode->GND
lights when MicroPython drives GP2 HIGH. The board's componentId is its
boardType ('pi-pico-w') since loadExample's addBoard gives the first
board of a kind id == kind.
Add a red LED on GP2 to picow-wifi-relay-web-server and flip the relay
logic to active-high (ON => GP2 HIGH => LED lit) so the toggle is visible
on the canvas, not just in the device panel.
The first iframe panel was a full-screen modal with a backdrop: it
covered the canvas and blocked the editor, so you couldn't watch the
board react or keep clicking buttons/wiring while it was open, and it
couldn't be moved.
Now it's a small floating panel docked top-right, draggable by its title
bar, resizable, with NO backdrop — the canvas and editor stay fully
interactive underneath.
Also: the relay and async-led device pages now show a big colored ON/OFF
indicator (these are web-server demos; the GP2/onboard LED isn't drawn on
the canvas, so the panel is where you see the state flip).
The Pico W emulation runs in THIS browser tab via requestAnimationFrame.
Opening the gateway with target=_blank / window.open backgrounds the
emulation tab; the browser then pauses its rAF, the simulated chip
freezes, and the gateway can no longer reach the server on it — the
request times out (502) and toggles do nothing.
Render the served page in a same-tab iframe panel (openDeviceGateway) for
the Pico W so the emulation stays in the foreground and keeps answering.
The ESP32 is unchanged (its server runs in QEMU on the backend, immune to
tab visibility), so it keeps opening in a new tab.
Also: the async-led page now shows the LED state (the board's onboard LED
isn't drawn on the canvas) so the toggle has visible feedback.
The served page loads under /api/gateway/<id>/, so absolute fetches like
fetch('/on') hit velxio.dev/on instead of the chip — the LED/relay/servo
controls did nothing. Use relative paths (fetch('on')) so they resolve
under the gateway. The relay example now has real ON/OFF buttons and
wraps its blocking accept loop in try/except so a dropped browser
connection can't kill it.
e2e now fires two sequential requests (first with a browser-sized header)
against a non-resilient blocking server and asserts both are served.
- SerialMonitor linkifies http://10.13.37.x (the Pico W subnet) the same
way it already does http://192.168.4.x for the ESP32, turning the
sketch's printed URL into an 'Open IoT Gateway' link.
- SimulatorCanvas shows the clickable WiFi badge for the Pico W too
(normalizing its 'started' status, which carries the fixed IP, to
got_ip so it reuses the ESP32 badge styling + launcher).
- async-led and servo-web examples print a clickable http://<ip>/ line
so the gateway link appears (relay-web-server already did).
Gated e2e (CYW43_GATEWAY_E2E=1) drives the real emulator + a running
backend and asserts the served page comes back through /api/gateway.
The RP2040 MicroPython loader always fetched the plain RPI_PICO build, which
ships no `network` module and no CYW43 WiFi driver. Every Pico W WiFi/MQTT
example therefore failed at `import network` ("no module named 'network'"),
which surfaced as a compile/run error in the editor.
- getFirmware()/loadUserFiles() are now variant-aware. pi-pico-w boards load
RPI_PICO_W-20230426-v1.20.0 (network/socket/ssl + the CYW43439 driver) and
write the LittleFS at the W board's flash offset (0x12c000, 212 blocks)
instead of the plain Pico's 0xa0000/352. The W firmware spans flash to
~0xab000 and would otherwise be clobbered by the filesystem. Each variant
gets its own IndexedDB cache key.
- The variant is selected by the presence of the already-wired CYW43 emulator
(attachCyw43 runs for pi-pico-w boards only).
- loadMicroPython swaps in a fresh RP2040 each run, so the CYW43 PIO-FIFO hooks
are re-installed on the new instance; otherwise the driver's gSPI traffic
never reaches the emulator and WiFi never comes up.
- Bundle micropython-rp2040w.uf2 as the offline fallback.
- Point the ThingsBoard example at the simulator's Velxio-GUEST network.
8 MicroPython examples that use `import network` (Blynk IoT relay, ThingsBoard
IoT, OTA update, DHT11 HTTP CSV logger, async LED control, web servo, websocket
LED, IoT relay web server) had boardType "raspberry-pi-pico". A plain Pico
(RP2040) has no WiFi and no `network` module, so they failed at runtime with
`ImportError: no module named 'network'` (the banner even shows "Raspberry Pi
Pico with RP2040"). Move them all to "pi-pico-w", which has WiFi + network.
Adds a self-contained ESP32 networking example for the /examples gallery
(addresses feature request #115). The sketch joins the emulator AP
"Velxio-GUEST", connects to a public MQTT broker (broker.hivemq.com:1883),
then publishes to its own topic and subscribes to it so each message
round-trips through the broker and toggles GPIO2 -- no external client or
local broker needed; just open the Serial Monitor.
Verified end to end in QEMU: WiFi associates (IP 192.168.4.15), DNS resolves
and outbound TCP to :1883 succeeds via slirp NAT. PubSubClient is auto-
installed via the example's `libraries` field.
Add a working microSD card part backed by a FAT16 image, following the
Wokwi storage model: the project's own workspace files are auto-copied
onto the card (free), and an optional "SD Card" panel uploads extra
files (gated as a paid feature by the velxio.dev overlay; OSS default
allows it).
Frontend (in-browser AVR / RP2040):
- ProtocolParts.ts: rewrite the microsd-card part from a handshake stub
into a real SD-over-SPI device (reply-first Ncr timing, SDSC byte
addressing, single/multi-block read+write, CSD/CID, full CMD set).
- utils/fatImage.ts: dependency-free FAT16 super-floppy builder (8.3 + LFN).
- utils/sdCardFiles.ts: assemble the card image from workspace files plus
uploaded files; base64 helpers.
- components/simulator/SdCardPanel.tsx + ComponentPropertyDialog: upload UI.
- DynamicComponent + useSimulatorStore: build and inject the image on run.
- lib/proSdCardGate.ts: overlay-installable gate for the upload action.
- data/examples-storage-microsd.ts: Arduino Uno + ESP32 gallery examples.
Backend (ESP32 via QEMU):
- services/esp32_sd_slave.py: synchronous SD-over-SPI slave (Python port of
the browser part) with a sparse backing store, idle-state R1 tracking and
real CRC16 on data blocks when the host enables CRC (CMD59) -- both
required by ESP-IDF's sdspi driver.
- esp32_worker.py: route SPI bytes to the slave (returns MISO synchronously)
and feed write-only bulk transfers.
- esp32_lib_manager.py + routes/simulation.py: forward the FAT image
(sd_card.image_b64) from the start config into the worker.
Tested:
- frontend: protocol-parts, fat-image, sd-card-gate and microsd-real-firmware
(real Arduino SD.h on avr8js) -- 86 passing.
- backend: test_esp32_sd_slave (10) covering the ESP-IDF init sequence and
CRC16; validated end to end by running a real SD.h sketch in libqemu-xtensa
(mount, directory listing, read and write-readback).
lcd-hello -> ['LiquidCrystal'], uno-servo -> ['Servo']. These were the only
non-ESP32 gallery examples using a USER library without a manifest; loading +
compiling them now sends the library scope (resolved from the content-addressed
cache) instead of falling back to the global scan-all. Every other non-ESP32
example is core-only (Wire/SPI are core-bundled; the RP2040 core bundles Servo,
so pico-servo needs no manifest) or already declared its libraries.
Auto-completed the library manifests for the 9 ESP32-family examples that use
external libraries, so each declares its full dependency set (direct +
transitive). Found genuinely-missing deps that the previous fields omitted:
- esp32-dht22, c3-dht22: + Adafruit Unified Sensor
- esp32-mpu6050, esp32-bmp280, esp32-oled, esp32-doom: + Adafruit BusIO
- esp32cam-lcd-preview: add manifest [Adafruit GFX Library, Adafruit BusIO, Adafruit ILI9341]
Each completed manifest was validated by compiling the example against ONLY
its manifest (manifest-scoped resolution, no fallback). esp32-servo / c3-servo
were already complete. This unblocks turning on scoped resolution for the
gallery (P2.3): with complete manifests, scope picks the declared libs and
excludes strays, and the P2.3-safety fallback covers any residual gap.
A board example proving digital and analog coexist in ONE circuit: the Arduino
drives two logic levels, a physical AND gate combines them, and the AND output
switches an NPN 2N2222 transistor that drives the "motor" LED. Verified live: it
compiles, the MCU drives the AND gate (5 V), the transistor conducts and the LED
lights — MCU -> logic gate -> transistor -> load works across the digital and
ngspice motors together.
Known limitation (sim-mixedmode step 2, pending): the ngspice side does not
re-solve on every MCU pin edge, so a fast (1 Hz) blink does not track in real
time — the analog output changes on a slower cadence. User-driven / slow changes
track fine. Snapshot updated for the new example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AND Gate Alarm needed BOTH inputs HIGH at once, but it used momentary
pushbuttons buffered through an Arduino — with one mouse you can only hold one
button at a time, so the AND never fired and the alarm could never be
demonstrated.
Rebuilt it as a board-less digital circuit: two SLIDE switches (they latch) feed
a real AND gate that drives the alarm LED. Slide both switches ON and they stay,
so the alarm arms. No MCU / compilation — it runs on the digital gate engine.
Verified live: the LED lights only on 11 (00/01/10 -> off, 11 -> on).
Snapshot updated: the new board-less and-gate-alarm netlist, plus the digital
bucket count label (38 -> 39) from the earlier ripple-counter example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the first board-less SEQUENTIAL gallery example (digital-ripple-counter-4bit):
four T flip-flops chained into a ripple counter, LEDs showing the binary count,
clocked by a slide switch. Impossible on the SPICE engine (no edge detection at
DC) - it runs on the digital gate engine.
Controller fix (found by testing the counter live): the controller rebuilt the
network on every change, which reset flip-flop state so a counter never counted.
Now the network is built once and KEPT ALIVE; a switch toggle applies
incrementally via setSwitch (preserving sequential state), and a rebuild happens
only on a structural change (components/wires). Correct for combinational AND
sequential circuits.
examples-digital.test.ts: flip-flop examples are digital-engine-only, so they are
exempt from the SPICE-mapping / has-a-gate / netlist checks (the "logic" check
now accepts a gate OR a flip-flop). digitalgate-engine-examples: a correctness
test clocks the real counter example and asserts it counts 1..15,0 in binary.
Verified live (?digitalgates default ON): the counter counts 0..6 on the canvas;
and the complex examples all work - comparator-4bit (A=B correct), decoder-3to8
(perfect one-hot x8), alu-slice-1bit (32 combos deterministic), multiplier-2x2
(3*3=9, 7 distinct products), adder-subtractor-4bit (5+3=8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gallery example loaded but the Z80 never visibly ran: the screen stayed
frozen on garbage. Two multi-chip async-load races, neither caught by the
existing headless tests (which drive RESET manually and attach the display
before boot):
1. RESET edge-vs-level race. The Z80 only left reset on the RISING edge of
RESET (a pin watch). In the browser the 7 chips instantiate asynchronously,
so the small power-on-reset chip releases RESET before the larger Z80 has
registered its watch -> the edge is lost and the CPU stays in reset forever.
Fix: on_clock samples the RESET level (hardware-accurate; RESET is
level-sensitive) so a missed edge self-corrects. An undriven RESET reads low,
so the CPU safely stays in reset until something drives it high.
Repro/guard: chipbus-galaksija-reset-race (race ordering must still boot).
2. Display-snoop load-order race. galaksija-display was a passive write-snoop;
the ROM paints the screen ONCE at boot then idles, so a display that comes up
late misses every write and shows stale content forever. A snoop cannot
recover writes it never saw. Fix: fold the screen into the RAM chip
(galaksija-ram-display) and render from the ACTUAL video RAM (0x2800-0x2BFF,
internal 0x0800 with A0-A12 wiring) on a ~30 fps timer - correct regardless
of load order, exactly how the real machine scans video RAM.
Repro/guard: chipbus-galaksija-display-snoop-race (late snoop shows nothing)
+ chipbus-galaksija-ram-display (renders even when first paint is post-boot).
The example now has 6 chips (RAM+display merged, gdisp dropped), 76 wires.
Verified live in the browser: boots to "@'READY", shows the ">" prompt, and
pressing A echoes ">A_" through keyboard -> Z80 -> video RAM -> display. The
full chipbus suite is 45/45.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a memory-mapped keyboard so you can type into the Galaksija. Based on
the libretro Galaksija core's scheme (not guessed): reading 0x2000+offset
returns 0xFE when the key at that matrix offset is held, 0xFF otherwise;
the keyMap gives the offset per key ('A'=1 ... Enter=48, Space=31, etc.).
- galaksija-keyboard.c: drives reads of 0x2000-0x203F from a keys[] table and
exports set_key(offset, down) for the host to push key events. Never drives
outside the keyboard range.
- galaksija-ram.c: ram-64k variant that yields reads of 0x2000-0x203F to the
keyboard (writes still go to RAM), so the two never fight for the bus.
- ChipRuntime: ChipInstance.hasKeyboard + setKey() expose the chip's set_key.
- CustomChipPart: bridges browser keydown/keyup (by KeyboardEvent.code, via
GALAKSIJA_KEY_OFFSET) into the chip, ignoring keystrokes while the code
editor or an input is focused so typing code is never hijacked.
- The gallery example gains the keyboard chip (now 7 chips, 99 wires) and uses
galaksija-ram.
Test chipbus-galaksija-keyboard: pressing 'A' (offset 1) makes the BASIC
monitor echo "A" after its ">" prompt and advances the cursor. 41 chipbus
tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ships the full Galaksija (1983 Z80 home computer) as a runnable Retro
gallery example, plus the pieces needed to run a multi-chip bus live in the
browser.
Gallery example (examples-retro-intel.ts, id 'galaksija-z80-computer'):
Z80 + galaksija-rom (public-domain ROM A+B) + ram-64k + inverter (A13
decode) + galaksija-display + a power-on reset chip, wired chip-to-chip
over the bus (76 wires), no board. Click Resume and it boots the real ROM
to the "READY" prompt on the green display. Chip wasm is embedded
(wasmBase64) so it runs without a backend compile.
- ChipRuntime.tickTimers gains a wall-clock budget (CustomChipPart passes
6 ms): a faithful-but-slow event-driven bus can't run a real-time CPU in
one animation frame, so without a cap a Z80 fetching over the settle
kernel froze the tab. With the budget the sim advances slower than real
time (boots over a few seconds) and the UI stays responsive; fast
single-chip examples finish under budget and are unaffected.
- galaksija-display: blits its framebuffer on a ~30 fps timer instead of on
every character write, so a clear-screen burst doesn't flood the canvas.
- reset-gen: power-on reset (pulses RESET high, ties WAIT/BUSREQ/INT/NMI
high) so the machine boots on Resume without a manual reset.
- chipbus flag now defaults ON (override with ?chipbus=off): chip-to-chip
buses are a core capability; single-chip and board nets never take this
path, so the only thing enabled is multi-chip buses, previously broken.
Verified live in the browser: the example boots and renders "@'READY" with
the ">_" prompt, responsive. Full suite 2084 pass (5 pre-existing,
unrelated env failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ePaper panels rendered rotated/misaligned on AVR and RP2040 (e.g. the 2.13"
Pico clock came out sideways and clipped). The ESP32 worker decoder was just
taught to compose in the controller's native RAM geometry and rotate to the
display orientation, but the browser-side SSD168xDecoder (used by AVR/RP2040)
still composed at display dims with no rotation, so the two diverged.
- SSD168xDecoder.ts: port the worker's native-window compose + rotation.
* Size RAM to the longer side both ways so a rotated native layout
(128x296 behind a 296x128 panel) isn't truncated.
* Compose in the active RAM window, then rotate via the inverse of
Adafruit_GFX setRotation(1). Detect orientation by BYTE width so a
non-multiple-of-8 native width (the 2.13" panel is 122 px) is handled.
* Track the UNION of windows per frame: paged drivers (GxEPD2 page height
< panel) set one partial window per page, so compose must use the full
native area, not just the last page's strip. Fixes the all-white render
on paged panels (1.54" Uno, 4.2" Pico, 7.5" ESP32).
* Add an isBwr option: B/W panels treat 0x26 as a 2nd mono plane (white
only if both planes white), tri-colour panels keep red-wins.
* Default the active window to display geometry; the firmware overrides it.
- EPaperPart.ts: pass isBwr = cfg.palette === 'bwr' to the decoder.
- esp32_spi_slaves.py / esp32_worker.py: mirror the byte-aware rotation +
window-union in the worker, and derive is_bwr from panel_kind on the
runtime sensor_attach path too (fixes the tri-colour ESP32 alert badge).
- test_epaper/ssd168x_decoder.py: re-port the golden reference to match
(keeps the 3-way TS/Python/worker identity invariant). Tests updated to
construct tri-colour cases with is_bwr/palette='bwr'.
- examples-displays-epaper.ts: the Pico VCC wire referenced '3V3(OUT)',
which the velxio-pi-pico-w element doesn't expose (it has '3V3'), so the
wire snapped to the board corner. Use '3V3'.
The pushbutton SPICE mapper reads pins '1.l' and '2.l', but killbits/counter
wired the power side to '2.r' (an un-unioned sub-pin), so netLookup('2.l')
returned null and the button was omitted from the netlist entirely — pressing
did nothing electrically board-less. Wire the power side via '2.l' so the
button becomes a real (pressed -> 0.01 ohm) bridge to VCC, which the pull-down
+ connectChipInputsToSolve then turn into a HIGH the chip reads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chip-output board-less path existed (chipPinDrives -> SPICE voltage sources
-> LEDs). The INPUT direction was missing: a chip pin wired to a pushbutton had
its net solved by ngspice, but nothing fed that net's state back to the
PinManager key the chip reads via vx_pin_read. So a board-less chip could light
LEDs but never read a button (verified: i8080 counter stayed at 0 on press).
connectChipInputsToSolve subscribes to the electrical store and, after each
solve, thresholds every wired chip input pin's net voltage to HIGH/LOW and
triggerPinChange()s the chip's synthetic pin — updating getPinState (polling)
and firing onPinChange edges. Pins the chip is actively driving are skipped so
it never fights its own outputs. Hooked alongside connectAnalogInputsToMcu in
start.ts. Solver-agnostic; reads only the electrical store shape.
Also gives the board-less button examples a pull-down on each chip BTN pin so
they read a clean LOW when open (a button-to-VCC floats HIGH otherwise):
i8080-button-counter (2) and i8080-killbits (8).
- new connectChipInputsToSolve.ts; start.ts wiring.
- examples-retro-intel: pull-down resistors + wires for the button examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the run-system/UX work.
Stop bug: a programmable chip kept running after Stop when a board was present.
The chip rAF tick gated only on board presence (!boardless), so with a board it
ticked forever. Now it gates on the actual run state: board-less -> electrical
paused flag; with board(s) -> board.running. handleStop also clears every chip's
output drives (clearAllChipDrives) and re-solves so chip-driven LEDs go dark on
Stop instead of freezing at their last frame.
Examples to board-less (regulated power supply, no Arduino — the Arduino only
ever supplied 5V):
- z80-larson-scanner -> 'Z80 Comet Scanner': board-less, a faster TWO-LED comet
(scanner.s) so it's visually distinct from z80-larson-no-board's single-bit
walk; green/blue LEDs.
- i8080-killbits -> board-less (psu + resistors), keeps killbits.s as the chip's
editable program; buttons re-powered from the supply.
- i8080-button-counter -> board-less (psu + resistors); behaviour chip, program
baked in, so it shows a note (no editable file) and runs standalone.
banner-streamer stays Arduino-based (its TX/RX go through the AVR USART bridge).
- CustomChipPart: run-state-aware tick gate.
- EditorToolbar: clearAllChipDrives() helper + handleStop clears chip drives.
- examples-retro-intel: 3 conversions; drop now-unused sketch consts; add the
larsonScannerAsm comet program.
- Tests: board+chip routing now uses an inline synthetic example (gallery chip
examples are all board-less).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes from live testing feedback:
1. Adding a programmable chip (Z80/8080) from the gallery created NO program
group — only the chip(s) from the example had one. Root cause: 'programmable'
was detected by a non-empty programFile, but a fresh chip's programFile is
empty until the user writes one. Now detection uses the canonical signal —
chip.json's programTargets — via isProgrammableChip(). When such a chip
lands with no program yet, the file explorer seeds an editable program.c
(DEFAULT_CHIP_PROGRAM_C, a working walking-LED skeleton) into its own group
and stamps programFile/programTarget onto the component so Compile/Run can
build it. Behaviour/driver and predefined chips (no programTargets) still
get no group — edited in the chip designer.
2. z80-led-chaser-c now runs board-less on a regulated power supply (no Arduino,
mirroring z80-larson-no-board) — the Arduino only ever supplied 5V and added
confusion. chaser.c stays the chip's editable program in its own section.
- romCompileService: isProgrammableChip(), DEFAULT_CHIP_PROGRAM_FILE/_C.
- FileExplorer: detect by programTargets; auto-seed program.c + persist
programFile/programTarget for fresh chips.
- examples-retro-intel: chaser-c -> board-less (psu + 8 resistors + 8 LEDs),
drop the now-unused Arduino sketch const; fix a stale sdcc --code-loc comment.
- Tests: board+chip case moved to z80-larson-scanner (still board-based);
isProgrammableChip unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A programmable custom-chip (a CPU emulator that runs a ROM/program, e.g. the
Z80 or 8080) now keeps its program (larson.s, chaser.c, ...) in a dedicated
editor file group — group-chip-<chipId> — rendered as its own collapsible
section in the file explorer, exactly like each board owns its sketch group.
Behaviour/driver chips and predefined chips carry no programFile and get no
group; they stay editable only in the chip designer.
Fixes two reported issues on the Z80 examples:
- /example/z80-larson-no-board: the board-less chip example now opens its
program (larson.s) as the active group, editable on the left — previously
the editor showed but no file appeared.
- /example/z80-led-chaser-c: the chip program (chaser.c) no longer shows as
a sibling tab inside the Arduino sketch group; it sits in its own chip
section instead. The board group shows only sketch.ino.
Details:
- useEditorStore: chipFileGroupId()/CHIP_GROUP_PREFIX helpers.
- loadExample: seedChipProgramGroups() routes each chip's programFile into its
own group (seeded from the example files), sweeps stale chip groups, keeps
the program OUT of the board group, and for a board-less chip example makes
the chip group active so the program is the editable file shown.
- EditorToolbar.prepareCustomChips: resolves the program from the chip's own
group (falls back to board files for older projects) before assembling ROM.
- FileExplorer: renders one collapsible section per programmable chip with an
IC icon; clicking switches the editor to the chip group. Lazy-creates a
group for chips dropped on the canvas.
- projectPayload + vlxFile: serialise chip groups alongside board groups and
include them in the dirty-check hash, so chip-program edits persist on
save / autosave / .vlx export and round-trip via replaceFileGroups on load.
- Regression tests for board-less + board+chip routing and stale-group sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two UX bugs in the board-less "Z80 Larson Scanner (no board)" example:
- It loaded "running" (electrical sim defaults to paused=false), so Run was
disabled and Stop enabled even though the chip hadn't started — the user had
to Stop then Run. loadExample now starts a board-less example that contains a
custom chip in the STOPPED state (paused=true) so Run is enabled; pure
analog/digital circuits stay live.
- The chip's program wasn't editable: it shipped a pre-baked ROM and the
board-less loader only setCode'd into an orphan file group (no-op → blank
editor). The example now ships larson.s as a real file (programFile), and
the board-less loader points the editor at the default group and loadFiles()
the example's files, so the program shows on the left and is editable, like
the board-backed examples. Run compiles it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Velxio can now simulate one or more custom-chip CPUs with NO Arduino/ESP32
board on the canvas — a general-purpose electronics simulator, not an
MCU-only one.
- DynamicComponent: board-less parts get the real shared flat PinManager
(instead of a no-op stub) so a custom chip's digital pin writes/reads reach
the LEDs/inputs wired to it.
- CustomChipPart: the rAF tick respects board-less Run/Stop (freezes while
the electrical sim is paused); board behaviour is unchanged.
- EditorToolbar.handleRun: board-less Run compiles each chip's WASM/ROM and
re-attaches the parts (restartParts) so they pick up the fresh WASM, then
resumes the solver.
- useSimulatorStore.restartParts(): bump hexEpoch to force part re-attach.
- New example "Z80 Larson Scanner (no board)": a programmable Z80 + 8 LEDs +
the adjustable power-supply component, no MCU. The chip drives the LEDs
through the synthetic-pin + ngspice path added earlier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SDCC treats plain `char` as unsigned on Z80, so `dir = -1` read back as 255,
`if (dir > 0)` was always true, the "walk right" branch never ran, and the
bit just shifted left until it fell off the end and the LEDs went dark after
one pass. Use `signed char dir`. Verified in a chip-WASM harness: with plain
char the chaser does 8 LED writes then stops; with signed char it walks the
bit back and forth continuously (14894 writes). Completes the C example fix
together with dropping --code-loc 0x100 in c_compile.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SDCC's z80 crt0 sets SP=0x0000 and makes its first stack push at 0xFFFF.
The chip only mapped RAM at 0x8000-0xBFFF (0xC000+ was MMIO/ignored), so the
stack landed on unmapped memory and a plain C program crashed inside crt0 —
before main — which is why z80-led-chaser-c compiled but drove nothing.
Extend RAM to cover 0x8000-0xFFFF (32 KB) with the MMIO window 0xC000-0xC0FF
carved out and checked first, in scripts/make-z80-cpu.py + regenerated
z80-cpu.c. Now SDCC's default stack works and "write C from scratch, click
Run" just works — no manual `LD SP` needed (dropped from chaser.c). Bumped
the chip WASM initial memory to 4 pages to hold the larger RAM buffer. Larson
(asm, SP=0xBFFF, LED at 0xC000) is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SDCC's z80 crt0 defaults SP to 0x0000; on the z80-cpu chip's memory map
(RAM 0x8000-0xBFFF, MMIO at 0xC000+) the stack would grow into unmapped
high memory and the program crashed on the first CALL (delay), so the LEDs
never moved. Set SP to the top of RAM (0xBFFF) at the start of main, the
same thing the asm Larson example does with "LD SP, 0xBFFF". Verified the
ROM runs and walks the LEDs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`(*(volatile unsigned char __at(0xC000)))` uses __at as a cast operator,
which neither avr-gcc nor sdcc accept (sdcc: "syntax error: token -> ')'").
__at is a storage specifier, not an operator. Use the portable absolute-
address pointer form `(*(volatile unsigned char *)0xC000)`, which sdcc -mz80
compiles cleanly. Verified: produces a 462-byte ROM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compile/Run now makes every custom-chip on the canvas live in a single
click instead of requiring a manual trip through the chip designer plus a
separate ROM compile:
- Each custom-chip's C source is auto-compiled to WASM when it has none
yet (via /api/compile-chip), and programmable CPU chips get their
program file (larson.s, chaser.c, ...) assembled/compiled to ROM bytes
(via /api/compile-rom) and injected, all before the board starts.
- Chip-program files are excluded from the arduino-cli sketch build, so
SDCC-only syntax such as __at(0xC000) no longer breaks the Arduino
compile (this is what made the Z80 LED-chaser-C example error out).
Fixes the Z80 examples that either errored on Run (z80-led-chaser-c) or
compiled but did nothing (z80-larson-scanner, whose chip never had WASM
or ROM). Works for any circuit built from scratch with a programmable
CPU chip, not just the bundled examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full-screen Wolfenstein/Doom-style raycaster for ESP32 + ILI9341 over
hardware SPI (Adafruit_ILI9341, block writes), with auto-demo and 4 control
buttons. Doubles as an emulation-speed benchmark. Category: games.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six gpiozero (Python) examples to exercise the Pi 3/4/5 QEMU Linux boards
with different sensors/actuators. All strictly digital — the Pi has no ADC
and PWM is not simulated, so this covers the GPIO in/out paths that work:
- [Pi 3] Blink an LED
- [Pi 3] Running Lights (5 LEDs)
- [Pi 4] Button Toggles LED
- [Pi 4] RGB LED Color Cycle (digital, 7 colors, pwm=False)
- [Pi 5] PIR Motion Alarm
- [Pi 5] Traffic Light
Structure mirrors the existing Pi example (boards[] + vfsFiles['script.py'],
run via 'python3 /home/pi/script.py'); LEDs wired directly like
nano-button-led. gpiozero is used because it works across Pi 3/4/5 (RPi.GPIO
doesn't on Pi 5). Adds a smoke test loading all six (board kind, components,
wiring consistency, gpiozero script present in the VFS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ESP32 / Raspberry Pi / STM32 / Pico-W bridges built their WebSocket URL
from a bespoke API_BASE() that read only VITE_API_BASE (fallback
localhost:8001) and ignored the desktop shell's runtime-injected
window.__VELXIO_API_BASE__. On the desktop the sidecar runs on a random
127.0.0.1 port, so the sim WebSocket dialed localhost:8001 and never
connected: compile succeeded but the simulation never started. Honor
__VELXIO_API_BASE__ first; web (/api) and dev (localhost:8001) unchanged.
- nano-button-led example: button was wired D2->1a and 1b->GND (same
terminal), tying D2 to GND permanently. Rewire D2->1.l and GND->2.l
(opposite terminals), matching the other examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stepper-motor and biaxial-stepper parts only decoded a one-hot wave-drive coil sequence, so they never rotated under the common two-phase full-step / Stepper.h / AccelStepper drive that Wokwi's own examples use -- only the servo moved. Rewrote both decoders to track the net magnetic-field vector of the coils (atan2 of the H-bridge currents), so the rotor follows wave, two-phase full-step and half-step drive alike, whether driven directly from GPIO or through a driver's outputs.
Also adds an A4988 STEP/DIR stepper driver (parity with Wokwi's wokwi-a4988): velxio-a4988 element renders the real Pololu A4988 Fritzing breadboard SVG (public/components/a4988.svg); MotorDriverParts.ts finds the wired stepper via the netlist and advances it one (micro)step per STEP rising edge in the DIR direction (MS1-3 microstep + active-low ENABLE). Metadata in component-overrides.json. Three examples (Uno/ESP32/Pico) wire MCU STEP/DIR -> A4988 -> stepper, coil map aligned to Wokwi (1A->B+,1B->B-,2A->A+,2B->A-).
Verified in-browser: motor rotates on Arduino Uno (avr8js) and Raspberry Pi Pico (rp2040js). tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KY-040 rotary encoder was already fully simulated (wokwi-ky-040 element + PartSimulationRegistry 'ky-040' driving CLK/DT quadrature and the SW button) and present in the catalog, but unfindable: named 'KY040', in the 'other' category, with no rotary/encoder search tags and a placeholder thumbnail. A user searching 'rotary encoder' got nothing (issue #104).
- generate-component-metadata.ts: let component-overrides.json patch category, description and tags on scanned wokwi parts (previously only name/thumbnail) -- the fields the picker category tab and ComponentRegistry.search() actually use. - component-overrides.json: ky-040 override -> name 'KY-040 Rotary Encoder', category 'input', rotary/encoder/knob tags, description, real encoder thumbnail SVG. - examples.ts: KY-040 + Arduino Uno example (quadrature read + SW reset). Regenerated components-metadata.json; searching rotary/encoder/knob now returns the KY-040. tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.
tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OSError: [Errno 19] ENODEV at ssd1306.SSD1306_I2C(...) on
100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython.
MicroPython SoftI2C bit-bangs GPIO directly. Velxio's ESP32 QEMU
bridge listens on the emulated I2C peripheral (registers slaves like
0x3C wokwi-ssd1306 against it) and doesn't decode bit-banged GPIO
toggles as I2C frames, so the OLED never sees any writes and
i2c.writeto() returns ENODEV on first use.
machine.I2C(0, ...) routes through the hardware I2C peripheral that
QEMU emulates, the registered slave receives the bytes, the OLED
panel updates. Same code path the other working SSD1306 MicroPython
examples on this repo already use.
API surface is identical to SoftI2C — only the constructor differs —
so the rest of the user sketch needs zero changes.
Two related fixes for the ESP32 Arduino-compat compile path:
(a) backend/app/services/esp-idf-template/main/CMakeLists.txt:
Demote -Werror=comment / =parentheses / =sign-compare / =narrowing
/ =write-strings / =missing-field-initializers / =reorder back to
plain warnings. ESP-IDF's project defaults are stricter than what
Arduino/arduino-cli users expect, so common Arduino idioms (nested
/* */, missing field initializers in struct literals, etc.) were
failing builds that compile fine in the Arduino IDE. -Wall stays
on; we just stop the abort.
(b) examples-robot-desktop.ts (robot-desktop-eyes example):
Replace the nested /* xTaskCreatePinnedToCore( ... /* Task function. */
... */ block with `#if 0 / #endif` so the inner block comments
don't terminate the outer one. Even with -Wno-error=comment the
real-syntax-level issue (the first inner `*/` closes the outer
comment, leaving the rest of the lines as bare code) would still
bite, so this needs an actual code fix.
Sketch fails to compile out of the box with
fatal error: ESP32Servo.h: No such file or directory
because the ESP32Servo / U8g2 / DHT / Adafruit Unified Sensor libs
aren't part of arduino-esp32 and weren't declared on the example.
loadExample.ts already iterates `example.libraries` and runs
arduino-cli lib install for any missing entry before the user
touches Compile. Adding the four real deps the sketch needs gets
the example compiling cleanly on a fresh container without any
manual Library Manager dance.
robot-desktop-eyes and the day-30/100-days OLED example wired the
SSD1306 OLED with SDA/SCL/VCC, but the wokwi-ssd1306 element
exposes pinInfo as DATA/CLK/VIN/GND. The mismatched names couldn't
resolve, so all three wire endpoints fell back to (0,0) of the
component and visually attached to the corner instead of the pins.
Same class of bug on the wokwi-big-sound-sensor in
robot-desktop-eyes: the element has AOUT/DOUT (no plain OUT). The
sketch uses digitalRead(SOUND_PIN), so route to DOUT.
The COMPONENT_PIN_ALIASES map in wokwiZip.ts only normalises on
.zip import — static examples have to use the real pinInfo names.
The 100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython
example had components: [] and wires: [] — the MicroPython code wired
an SSD1306 OLED on I2C (GPIO 21/22) plus two buttons (GPIO 14, 27) but
the circuit had nothing on the canvas, so users saw a bare ESP32 board
and the simulation was missing every peripheral the code drives.
Adds:
- wokwi-ssd1306 on I2C (3V3 / GND / SDA=21 / SCL=22)
- two wokwi-pushbuttons wired HIGH-when-pressed (3V3 → 1.l, 2.l → GPIO
14 / 27) to match the `if pin.value(): pressed` check in main.py
Pulls https://github.com/davidmonterocrespo24/robot_desktop into the
examples gallery as a real-world ESP32 + sensors project. Cozmo-style
desktop robot: SSD1306 OLED face that blinks, looks around, and shows
emotions; DHT11 weather mode triggered after 10 min idle; PIR wakeup
from sleep; LDR-driven sleep when the room goes dark; sound-triggered
reactions; and two eyebrow servos.
Ships as 34 separate files (one .ino + 33 headers / source) rather
than the usual single-sketch flatten. The face engine
(Eye / EyeTransition / EyeVariation / FaceBehavior / FaceExpression
/ FaceEmotions / BlinkAssistant / LookAssistant / …) splits
responsibility across enough classes that flattening would obscure
the design. Velxio's multi-file `files: [{ name, content }]`
mechanism handles this cleanly — the editor mounts the .ino as the
active sketch and the rest sit in the same workspace.
Pre-placed components match the original board's pin map verbatim
from Common.h:
- SSD1306 OLED on I²C (SDA=21, SCL=22 — ESP32 default)
- DHT11 on GPIO 15
- PIR motion on GPIO 4
- Big sound sensor on GPIO 2
- Photoresistor on GPIO 34 (ADC1)
- Right eyebrow servo on GPIO 12
- Left eyebrow servo on GPIO 13
Arduino libraries (U8g2lib, DHT, ESP32Servo, Adafruit_Sensor) are
auto-installed by velxio's Library Manager on the first compile.
Category 'displays', difficulty 'advanced', tags cover both the
sensor list and the project's identity (cozmo / robot / animation /
eyes) so the gallery search surfaces it from multiple angles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ATtiny85 (AVRSimulator + collectPinStates + connectAnalogInputsToMcu + SimulatorCanvas + Attiny85Element + examples):
- Add attiny85AdcConfig with correct register addresses (ADMUX=0x27,
ADCSRA=0x26, ADCSRB=0x23, ADCL=0x24, ADCH=0x25, DIDR0=0x34, adcInterrupt=0x08).
Without this, analogRead() polled the wrong address forever and the
firmware hung on first ADC read.
- Add attiny85Timer0Config + instantiate AVRTimer so OVF fires at the
ATTinyCore-expected ~1.024 ms cadence. delay() advance is still blocked
on avr8js TIFR auto-clear semantics (separate upstream issue, see
ATTINY85_TIMER0_UPSTREAM_ISSUE.md in velxio-prod test plan).
- Map ATtiny85 ADC channels to PB-style pin names (PB5/PB2/PB4/PB3 -> 0..3)
in connectAnalogInputsToMcu so SPICE node voltages reach the right ADC
channel.
- Recognise /^PB\d+$/ in collectPinStates.pinNameToArduinoPin so wires
named "PB1" emit v_attiny85_pb1 V-source and the LED responds to MCU
writes. Previously every PB-wire returned -1 and SPICE saw no source.
- SimulatorCanvas: subscribe pin 1 (PB1) for the built-in LED on the
attiny85 board kind (Digispark convention), instead of falling through
to the pin-13 default.
- Attiny85Element: remove the hand-drawn "yellow LED" circle that was
floating above the chip. The bare DIP-8 has no on-board LED; examples
wire a real wokwi-led + resistor instead.
- examples.ts: add a real wokwi-led + 220 Ohm wokwi-resistor + wires to
attiny85-blink, and add missing series resistors to attiny85-button-led
and attiny85-ntc-sensor. attiny85-pwm-fade was already correct.
Custom-chip pipeline (CustomChipPart + simulatorBridges):
- Add a requestAnimationFrame loop that calls instance.tickTimers() every
frame in CustomChipPart. Chips that register vx_timer_create (e.g. an
i8080 stepping its core, or a sensor publishing samples) had timers
added to the queue but nothing fired them; tickTimers was dead code.
- Gate the ESP32 backend path with detectSimulatorKind(sim)==='esp32'.
The previous `typeof sim.registerSensor === 'function'` check matched
AVR and RP2040 simulators too (they expose registerSensor for I2C
sensor proxies), routing client-side chips to a non-existent ESP32
worker on those boards.
- Replace direct simulator.usart.writeByte calls in avrUartTx with a
JS-level FIFO + setTimeout(1ms) drainer. avr8js writeByte drops bytes
under burst load (a chip emitting print_string lost ~99% of bytes via
non-immediate, or kept only the last byte via immediate). The drainer
attempts one non-immediate write per tick and retries on RXC busy /
RXEN off. Added a guard for ATtiny85 (no USART -> would queue forever).
End-to-end verified: i8080-banner-streamer now prints the boot banner
followed by "uptime ticks: 0xNN" lines stepping every ~50 ms, executing
real Intel 8080 instructions inside the WASM chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three independent fixes uncovered during a systematic example-by-example
audit (plan/full_test_plan/):
1. DynamicComponent.handleMouseDown was calling e.stopPropagation()
unconditionally in the capture phase. That swallowed pointerdown
BEFORE wokwi-potentiometer / pushbutton / slide-switch / joystick
could see it, so the rotary knob would not rotate and buttons
wouldn't press even with a real OS mouse. Now we skip the swallow
when the click target is an inner wokwi-* element during a live
simulation, letting the wokwi component own its own pointerdown
while still allowing the canvas drag-to-rearrange flow on the
wrapper / non-interactive surface.
2. examples.ts uno-ntc (and pico-ntc) sketch had the NTC divider
formula inverted relative to both the SPICE mapper topology
(VCC -> R_NTC -> A1 -> R_pull -> GND, the standard module wiring)
and real wokwi-ntc-temperature-sensor modules. Moving the slider
to 60 C made the firmware print -3.42 C. Flipped the formula to
r = SERIES_R * (VCC - v) / v. Now slider 60 C -> Serial reports
60.12 C and A1 voltmeter shows 4.00 V.
3. componentToSpice.ts photoresistor mapper was only registered under
the bare key `photoresistor`, but example components use the
metadataId `photoresistor-sensor`. Added an alias so the LDR +
pull-down divider gets emitted for the real component instance.
All three reproduce visually in seconds; documented per-example in
plan/full_test_plan/examples/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a third format to /api/compile-rom: `c` (C source compiled by SDCC
to Z80 bytes). Same chip-program flow as 8080/Z80 asm — write C in a
project file, click Compile, click Run.
Backend:
- backend/app/services/c_compile.py — async SDCC wrapper. Locates the
sdcc binary on PATH (or via SDCC env var, or common Windows install
paths) and shells out with target=mz80 + --code-loc 0x100 --data-loc
0x8000. Parses the resulting Intel HEX into raw ROM bytes. Pure 8080
is rejected with a clear error (SDCC has no 8080 backend; Z80 ROMs
also run on the i8080-cpu chip if you avoid Z80-only ops).
- rom_compile.py: compile_rom is now async; the new c branch delegates
to c_compile. compile_rom_endpoint awaits it.
Frontend:
- romCompileService: RomFormat gains 'c'; formatForFile maps .c/.cpp to
'c'. isChipProgramFile intentionally still excludes .c — disambiguation
happens at the EditorToolbar level.
- EditorToolbar: the chip-program path also fires when a custom-chip
has programFile === activeFile.name (regardless of extension). That
lets .c files route to /api/compile-rom (SDCC) when bound to a CPU
chip, while .c files NOT bound to any chip continue to route to
arduino-cli as before.
Docker:
- Dockerfile.standalone adds `sdcc` to the apt-get install list, so the
prod image ships with SDCC out of the box.
Example:
- /examples/z80-led-chaser-c — z80-cpu chip + chaser.c (a Larson
scanner written in C with __at() MMIO definitions). Compiles cleanly
with SDCC's --code-loc 0x100 default crt0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Zilog Z80 to the programmable-retro-CPU lineup. Same compile-rom
flow that landed for the 8080 in PR #189: write Z80 asm in a project
file, click Compile (backend assembles via in-tree two-pass asm-z80),
click Run, the chip emulator boots from the resulting ROM bytes.
Backend:
- backend/app/services/asmz80.py — two-pass Z80 assembler covering the
practical demo subset: LD r,n / r,r' / rp,nn / (nn),A / A,(nn) +
ALU r/n + INC/DEC + JP/JR/DJNZ/CALL/RET + PUSH/POP + IN/OUT +
EX/EXX + LDIR/LDDR/IM/NEG + RLCA/RRCA/RLA/RRA + the simple
ED-prefix variants. Not yet: CB-prefix bit ops, DD/FD index ops.
- rom_compile.py routes target=z80 through the new assembler.
Chip:
- frontend/src/components/customChips/examples/intel/z80-cpu.{c,chip.json}
Generated by scripts/make-z80-cpu.py from the existing z80.c emulator
(same clean-room implementation that passes ZEXDOC end-to-end). The
external pin/bus protocol is replaced with internal RAM + ROM + MMIO
for LED/BTN/UART. 35 KB WASM.
Example:
- /examples/z80-larson-scanner — Knight-Rider-style walking LED.
Demonstrates JR/DJNZ/RLCA which the 8080 can't run.
Plus a small Z80 smoke-test asm under scripts/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>