Commit Graph

1005 Commits

Author SHA1 Message Date
David Montero ee41f361b6 fix(frontend): don't clobber a project's saved library manifest on save
buildSavePayload omitted libraries_json=[] whenever the manifest store was empty
— so an autosave right after loading a project (whose manifest the store hadn't
restored) wiped the saved manifest. Now omit libraries_json entirely when the
store value is null (unknown), so the backend preserves the saved manifest. The
compiler reads it server-side regardless (get_project_libraries hook).
2026-06-07 02:13:58 +02:00
David Montero b7954fa8c5 feat(esp32): scope ESP-IDF resolution to the project's SAVED library manifest
Adds the get_project_libraries hook: the compile route reads a saved project's
declared library manifest (by project_id) and uses it as the ESP-IDF resolution
scope, preferring it over the client-sent manifest. So a saved project always
compiles against only its own declared libraries — never another user's, or
another project's, stray install in the shared dir — authoritatively from the
server, independent of frontend wiring. Client-sent manifest still used for
unsaved examples; None/empty → legacy scan-all. Overlay fills the hook in
register_pro; OSS default is no-op (None).
2026-06-07 02:01:08 +02:00
David Montero 4a21c4f938 fix(frontend): restore project library manifest inside buildLoadPayload (P2.4)
The inline manifest-restore in the load .then was being tree-shaken out of the
lazy ProjectByIdPage chunk (the deployed bundle had the save wiring but not the
load). Move it into buildLoadPayload, which is an exported helper (used by tests)
so its body is never dropped. Reloaded projects now re-send their manifest.
2026-06-07 01:13:55 +02:00
David Montero 288ab46521 feat(frontend): persist + restore project library manifest (P2.4 projects)
Saved projects now round-trip their declared library manifest (compile scope):
buildSavePayload includes libraries_json from useLibraryManifestStore; loading a
project restores it (and clears any stale example manifest). Existing projects
load with an empty manifest -> legacy scan-all (unchanged); new saves capture
whatever manifest is active. Pairs with the backend libraries_json column.
2026-06-07 00:47:39 +02:00
David Montero e947f1e600 feat(frontend): send the example library manifest as the compile scope (P2.3)
Activates manifest-scoped ESP-IDF resolution for the gallery. loadExample now
records the example's declared libraries in useLibraryManifestStore; EditorToolbar
passes them to compileCode, which sends them as `libraries` in the compile
request. The backend then merges exactly those libraries (P2.0 scope) instead of
picking a stray same-named lib from the shared dir.

Safe: a core-only example sends null (legacy scan-all); a stale/incomplete
manifest degrades to scan-all via the backend graceful fallback, never a wrong
build. Ignored by the backend for non-ESP32 (arduino-cli) boards. Example
manifests were completed (incl. transitive deps) in c671c9b.
2026-06-07 00:10:15 +02:00
David Montero 472973f05b fix(esp32): auto-retry once on transient infrastructure build failures
Per-variant build dirs fixed the cross-compile staleness, but the FIRST build of
a cold variant can still occasionally hit ESP-IDF nested-build flakiness (cmake /
bootloader / managed-components / sdkconfig). These are infrastructure failures,
never user code, and clear on a retry once the variant dir is warmer. Retry the
attempt once when the failure matches infrastructure markers (not a user-sketch
or missing-library error). Cheap via ccache + ninja incremental.
2026-06-06 21:55:25 +02:00
David Montero c996ca3717 fix(esp32): per-variant persistent build dirs instead of wiping one shared dir
Replaces the wipe-on-change approach (which left ESP-IDF's nested bootloader /
managed-components build in a broken state under rapid reconfigure -
intermittent 'managed_components_list.temp.cmake: No such file'). Each distinct
configuration (board options x resolved library set, via the variant key the
caller already computes) now gets its OWN persistent project dir with its OWN
build/, never wiped or reconfigured for a different config:

  - same config  -> same dir -> warm ninja incremental + ccache (fast iterate);
  - different config -> different dir -> isolated, consistent, no staleness,
    no nested-build breakage;
  - the scoped vs scan-all fallback attempts land in different dirs, so the
    double-compile no longer corrupts a shared build/.

Variant dirs are LRU-bounded (_MAX_BUILD_VARIANTS per target); the global ccache
warms a fresh/evicted variant in seconds. Cleans up the old single-project
layout on first run. Fixes the cross-compile staleness for legacy AND manifest
compiles, and the fallback regression.
2026-06-06 21:13:42 +02:00
David Montero 33fbc03429 fix(esp32): reset build dir on library-set change via options hash, not mid-compile wipe
Supersedes the mid-_compile_in_dir build/ wipe (161deb9): wiping build/ AFTER
materializing libs but right before cmake left ESP-IDF's config half-regenerated
during the fallback's scoped->scan-all double-compile, intermittently failing
with 'sdkconfig.h: No such file'.

Instead fold the effective library set (the manifest, else the sketch's
non-core external includes) into the per-attempt build-dir hash, so a changed
lib set — or the scan-all fallback after a scoped attempt — resets the
persistent build/ at _prepare_persistent_project_dir time (before any cmake).
That is the existing, well-tested early-wipe path, so the configure is always
clean. Same lib set across compiles keeps the warm ccache/ninja cache; core-only
sketches share one dir (core headers filtered out of the token) so they never
trigger a spurious wipe.

Fixes both the original cross-compile staleness (intermittent cmake-configure
failures + stale-object false positives) and the fallback regression.
2026-06-06 20:53:18 +02:00
David Montero 161deb93e2 fix(esp32): wipe persistent build/ when the resolved library set changes
The persistent per-target build/ caches ESP-IDF's cmake configuration, ninja's
incremental graph and ccache-backed objects, all assuming a stable component
set. When consecutive compiles on the same dir have a DIFFERENT resolved
user_libs set (a different project/user, or a different library manifest) that
cache is inconsistent and produces two real failures:

  - cmake reconfigure intermittently fails ('cmake configure failed') even
    though each manifest compiles fine on a clean dir;
  - ninja/ccache reuse a previous compile's objects/headers, letting a
    now-absent library slip through as a false-positive success against a lib
    the current sketch/manifest no longer includes.

Fingerprint the materialized user_libs/ (sorted relative paths + sizes) and,
when it changes vs the last compile on this dir, wipe build/ to force a clean
configure. ccache (enabled) refills the objects so the rebuild stays cheap.
No-op on the ephemeral path and the first compile. Fixes both symptoms; will be
superseded by the fully ephemeral per-compile workspace (P1).
2026-06-06 20:32:26 +02:00
David Montero c671c9b21c data(examples): complete ESP32 example library manifests with transitive deps (P2.4)
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.
2026-06-06 20:14:17 +02:00
David Montero 5feb4d54ea feat(esp32): graceful fallback for incomplete library manifests (P2.3 safety)
A manifest-scoped compile that fails because a header isn't in the manifest
(an undeclared/transitive dependency) now retries once with scan-all, so a
project with an incomplete manifest still compiles instead of regressing.
The response reports manifest_incomplete=true and
manifest_suggested_libraries={header: [candidate lib names]} so the manifest
can be auto-completed (P2.4) or the user prompted to add the missing library.

This de-risks turning on manifest sending (P2.3): an incomplete example/project
manifest can never break a build that worked before.

- compile(): _attempt(allowed) helper; retry scan-all on missing-lib failure.
- _missing_library_headers / _suggest_libraries_for_headers helpers.
- CompileResponse.manifest_incomplete + manifest_suggested_libraries.
2026-06-06 18:09:49 +02:00
David Montero 1d643797b4 fix(esp32): manifest scope resolves to the DECLARED lib, not first-match
P2.0 first cut took the first-alphabetical lib providing a header and then
checked manifest membership. When several installed libs ship the same header
(e.g. DHT118266, DHT_sensor_library, servodht11 all have DHT.h), the stray
first-match got rejected and the header was dropped even though the declared
lib provides it.

_find_manifest_library_for_header: when a manifest is supplied, pick the first
DECLARED library that provides the header. This both selects the right lib and
excludes undeclared ones. No manifest = legacy first-match.

Test strengthened with a stray same-header lib that sorts first.
2026-06-06 09:01:03 +02:00
velxio-deploy 11fabfc67c chore(examples): refresh 3 thumb file(s) [auto] 2026-06-06 08:56:44 +02:00
David Montero ee88479356 chore(examples): add 13 missing gallery thumbnails 2026-06-06 08:46:48 +02:00
David Montero 47e220b72f feat(esp32): project library manifest scopes ESP-IDF resolution (P2.0)
When a compile supplies a 'libraries' manifest, _resolve_library_components
merges a USER-installed library only if it's declared in that set. A sketch
therefore never picks up an unrelated library from the shared dir (another
user's install, or a same-named clash) — the manifest is the resolution scope.

- _resolve_library_components(allowed_libraries): gate user-lib merges on
  manifest membership; match by folder name OR library.properties name=,
  normalised (display name vs on-disk folder differ by separators/case).
  Core/bundled libs are never gated. None = legacy scan-all (unchanged).
- Threaded through compile() -> _compile_in_dir.
- compile.py: CompileRequest.libraries; folded into the async dedup _job_key
  so a different manifest doesn't dedup to a job built with another.

Opt-in: omitting 'libraries' preserves current behaviour exactly.
Regression: test/backend/unit/test_espidf_core_first.py::TestManifestScope
2026-06-06 08:46:05 +02:00
David Montero Crespo 6b281c9d6d
Merge pull request #217 from davidmonterocrespo24/feat/chipbus-phase0
Feat/chipbus phase0
2026-06-06 03:18:45 -03:00
David Montero Crespo 70558eb31b feat(examples): mixed digital+analog coexistence demo (MCU + AND gate + transistor)
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>
2026-06-05 23:09:41 -03:00
David Montero Crespo a5edeb74c7 fix(examples): AND Gate Alarm uses latching slide switches (was momentary buttons)
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>
2026-06-05 22:42:15 -03:00
David Montero Crespo b93b1b42c5 feat(digital-gate-engine): 4-bit ripple counter gallery example + sequential controller fix
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>
2026-06-05 21:58:42 -03:00
David Montero Crespo 8aa6e93460 feat(digital-gate-engine): Phase 5 - sequential logic (D/T/JK flip-flops)
Flip-flops are edge-triggered and hold state, which the combinational settle
kernel cannot model alone. buildDigitalNetwork now gives each flip-flop explicit
state + rising-CLK-edge detection (reusing the LogicGateParts sample semantics):
sample the data nets on the edge, drive Q + Qbar. Because a flip-flop only
updates on the clock edge, a Q->D / Q->CLK feedback (counter / shift register)
does not oscillate the settle loop. isAllDigital now accepts a gate OR a
flip-flop, so pure sequential circuits qualify.

Test digitalgate-sequential (4): D (capture + hold), T (toggle), JK
(hold/set/reset/toggle), and a 2-bit ripple counter (FF0.Qbar clocks FF1)
counting 1,2,3,0,1 - impossible on the SPICE path (no edge detection at DC, no
SPICE mapper). The controller already routes all-digital circuits through
buildDigitalNetwork, so a board-less counter/shift-register example would run
live; authoring those gallery examples is the only follow-up. Full digitalgate
+ examples-digital + circuit-simulation-service suites green (127 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:16:43 -03:00
David Montero Crespo 77c85eefac feat(digital-gate-engine): Phase 3 core - digital/analog boundary handoff
buildMixedNetwork evaluates the gate (digital) side of a MIXED circuit on the
settle kernel and exposes the boundary with the analog (ngspice) domain. Unlike
buildDigitalNetwork it does not bail on non-primitive components - those are the
analog side; their pins mark the nets they touch as boundary. Exposes
boundaryNets, readBoundary(net) (digital->analog: the gate-driven level to seed
an ngspice voltage source) and setBoundaryInput(net, level) (analog->digital:
ngspice's solved+thresholded level, which re-evaluates downstream gates).

Test digitalgate-mixed-boundary (4): the boundary nets are exactly the
digital/analog bridges; both directions track; a digital->analog->digital
coupler loop converges. No ngspice needed - the analog side is supplied by the
test. Wiring the handoff to the live ngspice netlist (0/Vcc sources + threshold
+ settle<->solve iteration) is the remaining step; it needs the running solver
(the node loader is broken by a pre-existing path bug) and a mixed example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:01:05 -03:00
David Montero Crespo 4f4ee0bf60 feat(digital-gate-engine): sweep all 38 examples + default the flag on
Phase 4 (brought forward before the mixed-mode boundary). digitalgate-sweep
proves the engine handles 38/38 gallery digital examples: every one builds,
resolves every LED, and never oscillates. Tightened isAllDigital to also require
at least one logic gate, so a degenerate analog {source, resistor, LED} circuit
stays on ngspice rather than being claimed by the digital path. Flipped
digitalGatesEnabled() default to ON (override with ?digitalgates=off).

Full frontend suite 2120 pass / 5 fail — the 5 are the same pre-existing
unrelated failures (ngspice node-path, attiny85 arduino-cli, component-to-spice
catalog); the default flip adds no new breakage and examples-digital +
circuit-simulation-service stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:29:32 -03:00
David Montero Crespo b08df89c9b feat(digital-gate-engine): evaluate logic gates on the event-driven kernel
Board-less digital circuits (logic gates + switches + LEDs) run today as ngspice
analog B-sources, which is fragile for deep logic: a 4-bit ripple adder re-solves
but never lights its result LEDs live. This adds an event-driven digital motor
that reuses the multichip-bus settle kernel, so the same engine that boots a Z80
over a chip bus evaluates a gate network exactly and instantly.

Phases 0-2 (project/digital-gate-engine/), all behind ?digitalgates=on (default
OFF — flag off is byte-for-byte the old behaviour):

- digitalGateEngine.ts: buildDigitalNetwork(components, wires) does union-find
  over the wires (merging pass-through resistors), identifies the rail/gnd from
  the signal-generator, registers drivers (rail STRONG-1, gnd 0, pull resistors
  PULL, slide-switch as a pass-gate) and event-driven gates (reusing the
  LogicGateParts boolean semantics), settles on busKernel, and exposes
  setSwitch / readLed / netOf. Tolerant of both the raw example `type` and the
  store `metadataId`. Returns {ok:false} for any non-primitive, so mixed/analog
  circuits stay entirely on ngspice.

- digitalGateController.ts + a SimulatorCanvas useEffect: when the flag is on and
  the circuit is all-digital, rebuild from the store on switch-toggle / load
  (rAF-coalesced) and paint the wokwi-led DOM. CircuitSimulationService.tick()
  skips the SPICE solve for all-digital circuits when the flag is on, so the two
  motors never fight over the LEDs.

Tests: digitalgate-kernel (22 — single gates -> half/full adder -> 4-bit
adder/subtractor -> exhaustive ADD 256 -> mux/decoder/comparator/parity/
multiplier) and digitalgate-engine-examples (6 — the real gallery data for
and/or/xor/not + the full adder/subtractor). Verified live: ?digitalgates=on
lights the adder's result LEDs that the SPICE path leaves dark. Full suite
2117 pass / 5 pre-existing unrelated fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:21:33 -03:00
David Montero Crespo 47adb0b1c8 fix(chipbus): Galaksija boots + displays + types live in the browser
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>
2026-06-05 15:47:30 -03:00
David Montero Crespo 94627b99d2 feat(chipbus): Galaksija keyboard - type BASIC over the bus
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>
2026-06-05 14:37:42 -03:00
David Montero Crespo a393e3e91d feat(chipbus): Galaksija home computer gallery example + browser perf throttle
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>
2026-06-05 13:49:33 -03:00
David Montero Crespo a3562ba93f fix(chipbus): Galaksija display renders legible text (ASCII font8x8)
Galaksija stores ASCII codes in its 0x2800 video RAM (verified by snooping
the boot: it writes "@'READY" + ">_" prompt). The original CHRGEN ROM uses
a hardware-specific addressing that does not map char-code*8 to a glyph, so
rendering through it produced garbled output. Render the ASCII codes with
the public-domain IBM/VGA 8x8 font (font8x8 by Daniel Hepper / Marcel
Sondaar) instead -- legible green-on-black phosphor text. The boot screen
now reads "@'READY" with the ">_" input prompt, exactly like a real
Galaksija. Tests updated to check the bright-green channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:21:17 -03:00
David Montero Crespo e3d21cd6fc feat(chipbus): Galaksija video display chip - full computer renders READY
galaksija-display.c: a 32x16 text video chip that renders the Galaksija
video RAM. It is a passive bus snoop -- watches WR + address + data, and on
a write into the 0x2800 video region stores the character and renders that
cell into a 256x128 framebuffer using the public-domain CHRGEN font (code*8,
bit 0 = lit). It never drives the bus. The host blits the framebuffer to the
chip canvas (vx_framebuffer_init / vx_buffer_write).

Two tests:
- chipbus-galaksija-display: snoop+render smoke test (a write of 'R' to
  0x2802 lights its cell; unwritten cells stay blank).
- chipbus-galaksija-computer: the COMPLETE machine over the chip-to-chip bus
  (Z80 + galaksija-rom + ram-64k + inverter decode + galaksija-display) boots
  the public-domain ROM and renders the monitor's "READY" prompt on screen.

40 chipbus tests across 10 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:14:40 -03:00
David Montero Crespo 8441d370d3 test(chipbus): a real Galaksija (1983 Z80 home computer) boots over the bus
The public-domain Galaksija ROM (Voja Antonic; ROM A monitor + integer
BASIC, ROM B float BASIC, 8 KB) runs on a standalone Z80 + external ROM +
RAM + an inverter for address decode, all chip-to-chip over the shared bus,
no board:

  ROM 0x0000-0x1FFF   rom.CE = A13
  RAM 0x2000-0x3FFF   ram.CE = NOT A13   (the inverter chip)
  RD -> both OE ; WR -> RAM WE

Pin-level boot proof (mirrors test_intel/test_z80/galaksija.test.js): watch
M1, read the address bus on each opcode fetch, and confirm the Z80 leaves
the reset vector (DI; SUB A; JP 0x03DA), reaches the init routine at 0x03DA,
and runs 1000+ fetches across 50+ distinct ROM addresses -- the real
firmware executing end-to-end through the settle-kernel bus. The on-screen
"READY" prompt is the next milestone (needs the video display chip
rendering the 0x2800 video RAM).

galaksija-rom.c embeds the public-domain ROM A+B image. 38 chipbus tests
across 8 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:55:28 -03:00
David Montero Crespo 0cd2dc2062 test(chipbus): Phase 3 core - Z80 + ROM + RAM + address decode over the bus
The architectural heart of the retro computer, proven on real chips. A Z80,
a 32K ROM, a 64K RAM and an inverter (address-decode glue) are wired
chip-to-chip over a shared address + data bus, no board:

  ROM at 0x0000-0x7FFF   rom.CE = A15
  RAM at 0x8000-0xFFFF   ram.CE = NOT A15  (the inverter chip)
  RD -> both OE ; WR -> RAM WE

The ROM program writes 0x5A to RAM at 0x8000, clears A, reads it back, and
HALTs only if the byte survived. HALT going low proves the full core works:
the Z80 runs from ROM, the inverter decodes A15 to select RAM (the settle
kernel drives the combinational glue across hops), and the RAM latches a
write and returns it on a read over the shared tri-state bus, all within
synchronous bus cycles. Adds z80-ram-rom.c (boot image) + ram-64k/inverter
fixtures. 37 chipbus tests across 7 files pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:23:57 -03:00
David Montero Crespo 89a5298f47 test(chipbus): live proof - a real Z80 boots from a ROM over the bus
End-to-end validation of Phases 0-2 on an actual CPU. The real Z80
(examples/intel/z80.c) and a 32K EPROM (z80-boot-rom.c, a rom-32k variant
holding JP 0x0006 / HALT) are wired chip-to-chip over a shared address +
data bus with no board. RD drives the ROM's OE; CE is left enabled.

Booting exercises all three phases at once: the Z80 drives the address ->
the ROM reacts on the shared net key (Phase 0); asserts RD -> the ROM
tri-state-drives the data bus while the Z80 released it (Phase 1); and reads
the data bus in the SAME tickTimers step, getting the settled byte
(Phase 2 settle-before-read). The Z80 fetches C3,06,00, jumps to 0x0006,
fetches 76, and HALTs -> drives HALT low, which the test observes.

z80.wasm is compiled from the committed examples/intel/z80.c; the boot ROM
source + chip.json live in test_custom_chips/sdk/examples. All 36 chipbus
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:07:26 -03:00
David Montero Crespo 3137c40a90 feat(chipbus): Phase 2 - synchronous settle kernel (settle-before-read)
Fixes root cause B: a CPU bus cycle drives address+strobe then reads the
data bus in the same tickTimers call, so the memory chip must react before
the read. Phase 0/1 applied each net change by firing PinManager listeners
immediately, which recurses one JS frame per hop - deep glue chains are
deep recursion and a combinational loop overflows the stack.

- busKernel.ts: a delta-cycle settle loop. A net change is recorded in a
  pending set, not applied recursively; settle() drains it in batches
  (deltas), applying each and letting the driven chips re-dirty the next,
  until a fixed point or DELTA_CAP trips (oscillation -> warn, not hang).
  Two-phase: a drive lands in pending and is applied on the next delta, so
  a chip evaluating mid-settle reads last-stable nets. The first drive of a
  cycle settles synchronously before returning to the chip's C code, so the
  in-cycle vx_pin_read sees settled data.
- busNets: publishes resolved levels through the kernel instead of calling
  triggerPinChange directly.

Tests (chipbus-buskernel): multi-hop chain settles; settle-before-read; a
5000-hop chain settles without stack overflow; a ring oscillator trips the
cap and warns instead of hanging. The two-real-chip integration still
exchanges 0xA5 through the kernel. Full suite 2079 pass flag-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:36:54 -03:00
David Montero Crespo 23516247a6 feat(chipbus): Phase 1 - 4-valued logic + drive-strength tri-state buses
A chip-to-chip net is now resolved by (value, strength), not last-writer-
wins, so a real multi-driver bus works: many chips on one data line, only
the enabled one drives, the rest release to Hi-Z.

- busLogic.ts: 4-valued (0/1/Z/X) + drive-strength resolution. Strongest
  driver wins; equal strength + opposite = X (contention); no driver = Z;
  pull resistor = pull strength. modeToDrive maps VX_OUTPUT -> strong,
  VX_INPUT -> Hi-Z (the rom/ram/8255 "release by input" idiom becomes real
  tri-state), VX_INPUT_PULLUP/DOWN -> pull.
- busNets.ts: per-net driver registry; resolves and pushes the resolved
  level into PinManager; warns once on contention.
- syntheticPins.ts: isSyntheticNetPin distinguishes bus net keys.
- ChipRuntime.ts: pin register/write/set_mode route bus-net pins through
  busNets (gated by chipBusEnabled + isSyntheticNetPin); non-bus pins keep
  the legacy path; dispose releases the chip's bus drivers. SPICE source
  emission is skipped for bus pins (digital fast path beside SPICE).

Tests: busLogic (14), busNets (6, incl. tri-state hand-off + contention),
and the two-real-chip integration now exchanges 0xA5 through the registry.
Full suite 2074 pass with the flag off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:18:47 -03:00
David Montero Crespo 474d132368 test(chipbus): Phase 0 live proof - two real WASM chips exchange a byte
End-to-end proof of the chip-to-chip net-key fix through the real
ChipRuntime + PinManager (not a unit stub). Two chips compiled from C
with wasi-sdk:
- bus-driver.c: drives 0xA5 onto D0..D7 at setup.
- bus-reader.c: polls D0..D7 on a 1ms timer, mirrors onto OUT0..OUT7.
Wired chip-to-chip with no board; with the chipbus flag both chips' Dn
pins resolve to one shared net key, so the reader reproduces 0xA5.

- sdk/examples/bus-{driver,reader}.{c,chip.json}: the proof chips.
- __tests__/fixtures/chipbus/*.wasm: committed fixtures (regenerate with
  the test_intel/scripts/compile-chip.sh flags).
- __tests__/chipbus-twochip-integration.test.ts: loads the fixtures via a
  relative path; skipIf they are absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:10:08 -03:00
David Montero Crespo c050ae6e49 feat(chipbus): Phase 0 - net-identity shared key for chip-to-chip buses
Fixes root cause A of the multi-chip digital bus track
(project/multichip-bus/): chip-to-chip nets were keyed per-endpoint by
syntheticChipPin(chipId, pinName), so two chips on one wire resolved to
two different PinManager keys and never shared a net.

- chipNets.ts: union-find over the wire graph mints one canonical
  syntheticNetPin per net; resolveChipNetKey returns it only for pure
  chip-to-chip nets (>=2 chip endpoints, no board pin). Reuses the
  existing spice/unionFind.ts.
- syntheticPins.ts: add syntheticNetPin(netId), same allocator/space.
- DynamicComponent.tsx: traceDetailed consults resolveChipNetKey at
  depth 0 before the chipNeighbour fallback. Board priority (rule 1) and
  chip-to-component (rules 2/3) are unchanged.
- Gated behind ?chipbus=on / localStorage.velxio.chipbus (off by default).

Proof (D-008 go/no-go): __tests__/chipbus-netkey.test.ts - a byte written
on one chip's keys is visible synchronously to another via PinManager.
9 new tests; 85 resolver/PinManager/parts regression tests green flag-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:18:08 -03:00
David Montero ac3a8a8e4c fix(esp32): core arduino-esp32 headers never resolve to user libs
A user library that ships a core-named header (e.g. WiFiEspAT/src/WiFi.h)
could shadow the arduino-esp32 core during ESP-IDF library resolution.
WiFiEspAT shadowing WiFi.h pulled EspAtDrv.cpp into the build, whose
const char OK[]/STATUS[] collide with ESP-IDF's enum STATUS in
rom/ets_sys.h, breaking every ESP32 sketch that #include <WiFi.h>.

_resolve_library_components now:
- skips a header entirely when the arduino-esp32 core provides it
  (computed set from cores/ + libraries/, cached), so a user lib can
  never shadow WiFi.h/Wire.h/SPI.h/WebServer.h/...
- skips a resolved user lib whose library.properties architectures=
  excludes esp32/*.

Regression: test/backend/unit/test_espidf_core_first.py

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 05:11:36 +02:00
David Montero Crespo 6e468e1a30 docs(wiki): update ePaper emulation guide for all controllers + decoder internals
Bring docs/wiki/epaper-emulation.md up to date with the recent work:
- three controller families (SSD168x, UC8159c, UC8179) and the
  controllerFamily → decoder/slave dispatch table
- "Decoder internals": native-window-compose-then-rotate, byte-aware
  orientation, paged window-union, RAM Y-counter wrap, B/W vs is_bwr
- UltraChip decoders (UC8159c linear ACeP, UC8179 mono partial-window)
- BUSY polarity per family (UltraChip idles HIGH, SSD168x LOW) and the
  10s "Busy Timeout" failure mode
- WS plumbing (flat epaper_update emit) and the double-'data' blank bug
- debugging gotchas (slow render, early clear flush, backend restart)
- refreshed library matrix, roadmap, and code map
2026-06-04 23:53:59 -03:00
David Montero Crespo bfe94a19f5 feat(epaper): decode the UC8179/GD7965 7.5" panel + fix its BUSY polarity
The 7.5" 800x480 dashboard (GxEPD2_750_T7) rendered blank: it is a UC8179 /
GD7965 controller, but the panel config claimed controllerFamily 'ssd168x',
so the SSD168x decoder (which only reads 0x24/0x26/0x44/0x45) ignored its
0x10/0x13 DTM stream.

- Add a Uc8179 decoder (worker Uc8179EpaperSlave + browser Uc8179Decoder).
  UC8179 is the same UltraChip command family as the UC8159c (0x10/0x13 DTM,
  0x12 refresh) but mono (1 bit/px). GxEPD2 writes the visible image to 0x13
  (DTM2 "current"; 0x10 is the ignored "previous"), framed by 0x91/0x90
  (partial window, pixel coords MSB-first)/0x13 data/0x92. Data lands at
  absolute pixel coords inside the window, so compose is just the RAM. The
  Frame reuses the SSD168x palette (0=black, 1=white) so paintFrame renders it.
- EPaperPanels.ts: add the 'uc8179' family and point epaper-7in5-bw at it.
  EPaperPart.ts + esp32_worker.py dispatch 'uc8179' to the new decoder.
- Fix the BUSY polarity: UC8179 (like the UC8159c) idles BUSY HIGH, not LOW.
  The worker seeded BUSY LOW for every non-uc8159c panel, so GxEPD2_750_T7's
  _PowerOn()/_InitDisplay() busy-wait timed out (~10 s, "Busy Timeout!") on
  every refresh. Now _PowerOn returns in ~129 us.
- esp32_worker.py: the runtime sensor_attach epaper path still emitted the
  epaper_update payload nested under 'data' (the old double-wrap bug); emit
  it flat like the init path.

The 5.65" ACeP UC8159c example already rendered (it has its own decoder and
got the WS-plumbing fix); verified the 7 colour bars are correct.
2026-06-04 23:45:37 -03:00
David Montero Crespo 3bb6f95a67 fix(epaper): wrap RAM Y counter at window end (tri-colour red plane)
The 2.9" tri-colour ESP32 alert badge rendered the red ALERT pill as white:
the red plane (0x26) was received but landed out of bounds and was dropped.

GxEPD2_3C writes the 0x24 (black) plane then the 0x26 (red) plane WITHOUT
re-seeking the RAM address counter between them — it relies on the SSD168x
counter wrapping back to the window start after the last byte of the window.
Our decoder advanced Y past the window end instead of wrapping, so every
0x26 byte hit y >= rows and was discarded (red_ram stayed all-init).

Mirror the hardware: when the X cursor wraps at the end of a row, advance Y
with a wrap at the active window boundary (yrange), honouring the data-entry
Y direction. Applied identically to the worker slave, the browser decoder,
and the Python golden reference so the three stay in lockstep. No regression
on the mono panels (their counter is re-seeked per plane, so the wrap is a
no-op for them); verified the tri-colour pill now renders red and the 2.9"
weather / 2.13" clock / 1.54" hello panels are unchanged.
2026-06-04 23:15:37 -03:00
David Montero Crespo 9ba8687743 fix(epaper): correct orientation across all boards + Pico VCC wire
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'.
2026-06-04 23:15:37 -03:00
David Montero Crespo 2b528bfefc fix(esp32): render GxEPD2 ePaper panels (WS plumbing + native rotation)
ESP32 ePaper examples (e.g. epaper-2in9-esp32-weather) rendered as a blank
white panel. Two bugs, both above the SPI layer:

1. The worker's epaper_update event nested its payload under 'data', unlike
   every other (flat) worker event. The backend qemu_callback re-wraps the
   post-'type' payload under 'data', so the frontend received
   msg.data.data.component_id (undefined) and EPaperPart bailed on
   id !== componentId, so paintFrame/putImageData never ran. Emit it flat.

2. Ssd168xEpaperSlave was sized to the display dims (296x128), but GxEPD2
   with setRotation(1) writes the controller's NATIVE RAM (128x296). The
   _y < height bound dropped rows 128-295 (half the image) and compose
   never rotated. Size RAM to the longer side, compose in the native
   active-window geometry (0x44/0x45), then rotate to the display
   orientation (inverse of Adafruit_GFX rotation 1). Add is_bwr (from
   panel_kind): B/W panels init the 0x26 plane white and compose
   white-only-if-both (GDEY029T94 mirrors the image into 0x26); tri-colour
   panels keep red init 0x00 and red-wins.
2026-06-04 23:15:37 -03:00
David Montero 7b483f6109 feat(examples): add a Retro category to the gallery sidebar
A tag-based 'Retro' tab (next to All) collects the Z80 / Intel / vintage-CPU
examples via their 'retro' tag, regardless of board filter (they still also
appear under Digital). One-file change: BOARD_TABS + an isRetro predicate
special-cased in the filter and the tab count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:08:43 +02:00
David Montero 5f04b42bdd fix(canvas): while running, the canvas is interact-only (no wire/pin/edit)
Reported: on a running circuit, clicking a pushbutton SELECTED the wire under it
instead of pressing the button — and you could still move wires / pick pins to
make connections during a run.

Root cause: component dragging was already locked during a run, but the
canvas-level onClick (wire selection via findWireNearPoint) wasn't — so a click
on a button bubbled to the canvas and selected the wire. The button press itself
fired (shadow DOM), but the wire-select made it feel broken.

Gate every EDIT interaction on the existing interactionRunning predicate while
keeping part interaction (buttons/switches/pots) and pan/zoom:
- canvas onClick wire-selection + onDoubleClick waypoint-insert
- wire segment / waypoint drag handles (mouse + touch)
- pin-click wire creation
- touch tap wire-selection
- hide the PinOverlay (was gated on !running, so board-less runs still showed
  clickable pins) and skip wire-hover highlighting while running
- clear any wire/component selection when a run starts so leftover handles don't
  linger over the live circuit

Component drag + property dialog were already gated on interactionRunning; this
extends the same 'freeze to edit, run to interact' model to wires and pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:14:51 +02:00
David Montero dd322b27e3 feat(editor): per-target compilation console — a section per board/chip
Phase 4 of the run-system work. The compile console now groups output into a
section per run target (board or chip) with a status glyph and label, the way
multiple Arduinos already stream — instead of one flat list.

- CompilationLog gains an optional target { id, label, kind: 'board'|'chip' }.
  message/type are unchanged so the pro overlay (diagnose-with-AI prompt +
  errorCount slot) and the console's length-based clear/auto-error heuristics
  are untouched. parseCompileResult stamps the target on every produced line.
- Producers stamp their lines: compileAllBoards (per-board, dropping the old
  '<label>: ' string prefix the header now carries), prepareCustomChips
  (per-chip, WASM + ROM), handleCompile + handleRun MicroPython (single board) —
  including the Pi / MicroPython / FQBN / error paths so a target's lines never
  fragment across sections.
- CompilationConsole groups filteredLogs into consecutive-run sections at RENDER
  time only (the flat array is unchanged); each target section shows ✓/✕/▸ +
  name + kind tag, with no-target lines ('Compiling all targets', 'Done') as
  plain narration around them.

Reviewed by an adversarial pass; the flagged un-stamped edge paths (Pi /
MicroPython / single-board errors) are now stamped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:56:54 +02:00
David Montero 6ae1ed560d feat(editor): unified Compile-All / Run-All across boards + programmable chips
Phase 3 of the run-system work. Generalises the boards-only Compile-All/Run-All
to RUN TARGETS = boards + programmable custom-chips, so a board+chip or several
chips compile and run together, the same way multiple Arduinos do.

- targetCount = boards + programmable chips; the Compile-All/Run-All buttons now
  appear when targetCount > 1 (was boards.length > 1). Cheap string predicate
  (no JSON.parse) since the selector runs on every sim tick.
- compileAllBoards builds chips (WASM+ROM) AND boards; works with zero boards;
  prepareCustomChips now returns a failure count folded into the Done summary so
  a failed chip no longer shows green / calls markCompiled.
- handleRunAll: compiles all targets, starts every board, then restartParts() so
  chips pick up fresh WASM/ROM, and resumes the electrical solver when NO board
  actually started (board-less, or a board that compiled to nothing) so chips
  aren't left frozen.

Review fixes (2-agent adversarial pass):
- Stop now stops EVERY running board (Run-All can start several); otherwise a
  non-active board kept the chip ticking after Stop.
- Run-All / Stop disabled gates use anyBoardRunning (+ digitalRunning) instead of
  the flat active-board  flag, which misreports multi-target runs.
- shared isQemuBoardKind() helper so handleRun and handleRunAll can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:21:39 +02:00
David Montero a68e7f8e94 feat(editor): rename boards & custom chips; show which target owns each file
Phase 2 of the run-system/UX work.

- BoardInstance gains an optional user ; boardDisplayName(board) resolver
  (name || kind label) routes every INSTANCE-label surface: file-explorer
  section header, compile console (EditorToolbar), canvas selector/tooltip/
  context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options
  subtitle. Board/component pickers keep the KIND label (they pick new boards).
- Inline rename on board AND chip section headers (double-click the name, or a
  hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in
  properties. Enter commits, Escape cancels (cancel-flag ref guards the
  unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'.
- FileTabs shows an owner badge naming the board/chip whose files are shown
  (resolved as a selector so it doesn't re-render on every sim pin toggle).
- CustomChipDialog no longer clobbers a user-given chipName: chip.json's name
  only seeds the blank defaults (My Chip / Custom Chip); loading an example
  relabels explicitly.
- Persistence: board name round-trips via projectPayload (+ dirty hash),
  vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json.
- Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal
  (early return before a useCallback).

Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness);
all major findings folded in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 06:22:25 +02:00
David Montero aa4c8123f2 fix(examples): wire board-less button power via 2.l so the pushbutton enters SPICE
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>
2026-06-04 05:23:51 +02:00
David Montero dd62b94c1d feat(sim): custom chips read inputs (buttons/switches/sensors) with no board
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>
2026-06-04 05:09:51 +02:00
David Montero 694f038988 feat(sim): Stop halts custom chips + LEDs go dark; retro chip examples go board-less
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>
2026-06-04 04:32:44 +02:00
David Montero Crespo 19c0787f18 docs(wiki): ESP32 SPI display latency investigation and fixes
Document the full esp32-doom performance story: the bottleneck (per-event SPI
C->Python ctypes crossings, not QEMU compute / libqemu -O level / transport /
core version), the dead ends that gave zero gain and why (-O2 rebuild, async
_emit, blind CS-flush suppression), the two fixes (batch SPI data, gate CS
crossings) for 0.04 -> ~1 FPS (~26-37x), the build/test playbook, key files and
remaining headroom (the DC pin).
2026-06-03 23:23:04 -03:00